use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use hydracache_redis_compat::RedisRespServer;
use rustls::pki_types::{
pem::{Error as PemError, PemObject},
CertificateDer, PrivateKeyDer,
};
use thiserror::Error;
use tokio::net::TcpListener;
use tokio::sync::watch;
use tokio_rustls::TlsAcceptor;
use crate::admin_http::SharedServerRuntime;
use crate::config::TlsConfig;
#[derive(Debug, Error)]
pub enum RedisTcpError {
#[error("redis tcp accept error: {0}")]
Accept(#[from] std::io::Error),
}
#[derive(Clone)]
pub struct RedisTlsAcceptor {
inner: TlsAcceptor,
}
impl RedisTlsAcceptor {
pub fn from_tls_config(config: &TlsConfig) -> Result<Self, RedisTlsError> {
let cert_path = config
.cert_path
.as_deref()
.ok_or(RedisTlsError::MissingCertPath)?;
let key_path = config
.key_path
.as_deref()
.ok_or(RedisTlsError::MissingKeyPath)?;
Self::from_pem_files(cert_path, key_path)
}
pub fn from_pem_files(cert_path: &Path, key_path: &Path) -> Result<Self, RedisTlsError> {
install_default_rustls_provider();
let certs = read_certs(cert_path)?;
let key = read_private_key(key_path)?;
let config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|source| RedisTlsError::InvalidTlsConfig {
cert_path: cert_path.to_path_buf(),
key_path: key_path.to_path_buf(),
source: Box::new(source),
})?;
Ok(Self {
inner: TlsAcceptor::from(Arc::new(config)),
})
}
async fn accept(
&self,
stream: tokio::net::TcpStream,
) -> Result<tokio_rustls::server::TlsStream<tokio::net::TcpStream>, std::io::Error> {
self.inner.accept(stream).await
}
}
#[derive(Debug, Error)]
pub enum RedisTlsError {
#[error("redis rediss requires tls.cert_path")]
MissingCertPath,
#[error("redis rediss requires tls.key_path")]
MissingKeyPath,
#[error("failed to read redis TLS certificate {path}: {source}")]
CertRead {
path: PathBuf,
source: std::io::Error,
},
#[error("redis TLS certificate {path} does not contain any certificate")]
EmptyCert { path: PathBuf },
#[error("failed to parse redis TLS certificate {path}: {source}")]
CertParse {
path: PathBuf,
source: PemError,
},
#[error("failed to read redis TLS private key {path}: {source}")]
KeyRead {
path: PathBuf,
source: std::io::Error,
},
#[error("redis TLS private key {path} does not contain a private key")]
EmptyKey { path: PathBuf },
#[error("failed to parse redis TLS private key {path}: {source}")]
KeyParse {
path: PathBuf,
source: PemError,
},
#[error("invalid redis TLS certificate/key pair {cert_path} / {key_path}: {source}")]
InvalidTlsConfig {
cert_path: PathBuf,
key_path: PathBuf,
source: Box<rustls::Error>,
},
}
pub async fn serve_redis_listener(
listener: TcpListener,
server: Arc<RedisRespServer>,
runtime: SharedServerRuntime,
tls: Option<RedisTlsAcceptor>,
mut shutdown: watch::Receiver<bool>,
) -> Result<(), RedisTcpError> {
loop {
if *shutdown.borrow() {
return Ok(());
}
tokio::select! {
changed = shutdown.changed() => {
if changed.is_err() || *shutdown.borrow() {
return Ok(());
}
}
accepted = listener.accept() => {
let (stream, _) = accepted?;
configure_redis_stream(&stream)?;
if !runtime
.lock()
.expect("server runtime mutex")
.begin_redis_connection()
{
continue;
}
let guard = RedisConnectionGuard::new(Arc::clone(&runtime));
let server = Arc::clone(&server);
let tls = tls.clone();
tokio::spawn(async move {
let _guard = guard;
match tls {
Some(tls) => {
if let Ok(stream) = tls.accept(stream).await {
let _ = server.serve_connection(stream).await;
}
}
None => {
let _ = server.serve_connection(stream).await;
}
}
});
}
}
}
}
fn configure_redis_stream(stream: &tokio::net::TcpStream) -> std::io::Result<()> {
stream.set_nodelay(true)
}
fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>, RedisTlsError> {
let pem = fs::read(path).map_err(|source| RedisTlsError::CertRead {
path: path.to_path_buf(),
source,
})?;
let certs = CertificateDer::pem_slice_iter(&pem)
.collect::<Result<Vec<_>, _>>()
.map_err(|source| RedisTlsError::CertParse {
path: path.to_path_buf(),
source,
})?;
if certs.is_empty() {
return Err(RedisTlsError::EmptyCert {
path: path.to_path_buf(),
});
}
Ok(certs)
}
fn read_private_key(path: &Path) -> Result<PrivateKeyDer<'static>, RedisTlsError> {
let pem = fs::read(path).map_err(|source| RedisTlsError::KeyRead {
path: path.to_path_buf(),
source,
})?;
match PrivateKeyDer::from_pem_slice(&pem) {
Ok(key) => Ok(key),
Err(PemError::NoItemsFound) => Err(RedisTlsError::EmptyKey {
path: path.to_path_buf(),
}),
Err(source) => Err(RedisTlsError::KeyParse {
path: path.to_path_buf(),
source,
}),
}
}
fn install_default_rustls_provider() {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
}
}
struct RedisConnectionGuard {
runtime: SharedServerRuntime,
}
impl RedisConnectionGuard {
fn new(runtime: SharedServerRuntime) -> Self {
Self { runtime }
}
}
impl Drop for RedisConnectionGuard {
fn drop(&mut self) {
self.runtime
.lock()
.expect("server runtime mutex")
.finish_redis_connection();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn accepted_redis_socket_disables_nagle_before_serving() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let client = tokio::spawn(tokio::net::TcpStream::connect(address));
let (server_stream, _) = listener.accept().await.unwrap();
configure_redis_stream(&server_stream).unwrap();
assert!(server_stream.nodelay().unwrap());
client.await.unwrap().unwrap();
}
}