use std::fs::File;
use std::io::BufReader;
use std::sync::Arc;
use std::time::Duration;
use quinn::{ClientConfig, ServerConfig, TransportConfig};
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use rustls::RootCertStore;
use tracing::{debug, info};
use crate::error::{QrpcError, QrpcResult};
pub(crate) const ALPN_QRPC: &[u8] = b"qrpc-v1";
pub(crate) const DEFAULT_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
pub(crate) const DEFAULT_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(600);
#[derive(Clone, Copy, Debug)]
pub(crate) struct TransportOptions {
pub(crate) keep_alive_interval: Option<Duration>,
pub(crate) max_idle_timeout: Option<Duration>,
}
impl Default for TransportOptions {
fn default() -> Self {
Self {
keep_alive_interval: Some(DEFAULT_KEEP_ALIVE_INTERVAL),
max_idle_timeout: Some(DEFAULT_MAX_IDLE_TIMEOUT),
}
}
}
pub(crate) fn ensure_rustls_provider() -> QrpcResult<()> {
let _ = rustls::crypto::ring::default_provider().install_default();
debug!("rustls provider ensured");
Ok(())
}
pub(crate) fn build_server_config(
ca_cert_path: &str,
cert_path: &str,
key_path: &str,
transport_options: TransportOptions,
) -> QrpcResult<ServerConfig> {
info!(
ca_cert_path = ca_cert_path,
cert_path = cert_path,
"building server tls config"
);
let ca_der = load_cert_der(ca_cert_path)?;
let cert_chain = load_cert_der(cert_path)?;
let key = load_key_der(key_path)?;
let mut trusted_client_ca = RootCertStore::empty();
for cert in ca_der {
trusted_client_ca.add(cert)?;
}
let client_verifier =
rustls::server::WebPkiClientVerifier::builder(Arc::new(trusted_client_ca))
.build()
.map_err(|e| {
QrpcError::MessageDecode(format!("failed to build client verifier: {e}"))
})?;
let mut tls = rustls::ServerConfig::builder()
.with_client_cert_verifier(client_verifier)
.with_single_cert(cert_chain, key)
.map_err(|e| QrpcError::MessageDecode(format!("failed to build server tls config: {e}")))?;
tls.alpn_protocols = vec![ALPN_QRPC.to_vec()];
let crypto = quinn::crypto::rustls::QuicServerConfig::try_from(tls).map_err(|e| {
QrpcError::MessageDecode(format!("failed to convert server tls to quic: {e}"))
})?;
let mut transport = TransportConfig::default();
transport.keep_alive_interval(transport_options.keep_alive_interval);
transport.max_idle_timeout(to_idle_timeout(transport_options.max_idle_timeout)?);
let transport = Arc::new(transport);
let mut server = ServerConfig::with_crypto(Arc::new(crypto));
server.transport_config(transport);
debug!("server tls config built");
Ok(server)
}
pub(crate) fn build_client_config(
ca_cert_path: &str,
cert_path: &str,
key_path: &str,
transport_options: TransportOptions,
) -> QrpcResult<ClientConfig> {
info!(
ca_cert_path = ca_cert_path,
cert_path = cert_path,
"building client tls config"
);
let ca_der = load_cert_der(ca_cert_path)?;
let cert_chain = load_cert_der(cert_path)?;
let key = load_key_der(key_path)?;
let mut trusted_server_ca = RootCertStore::empty();
for cert in ca_der {
trusted_server_ca.add(cert)?;
}
let mut tls = rustls::ClientConfig::builder()
.with_root_certificates(trusted_server_ca)
.with_client_auth_cert(cert_chain, key)
.map_err(|e| QrpcError::MessageDecode(format!("failed to build client tls config: {e}")))?;
tls.alpn_protocols = vec![ALPN_QRPC.to_vec()];
let crypto = quinn::crypto::rustls::QuicClientConfig::try_from(tls).map_err(|e| {
QrpcError::MessageDecode(format!("failed to convert client tls to quic: {e}"))
})?;
let mut transport = TransportConfig::default();
transport.keep_alive_interval(transport_options.keep_alive_interval);
transport.max_idle_timeout(to_idle_timeout(transport_options.max_idle_timeout)?);
let transport = Arc::new(transport);
let mut client = ClientConfig::new(Arc::new(crypto));
client.transport_config(transport);
debug!("client tls config built");
Ok(client)
}
fn to_idle_timeout(timeout: Option<Duration>) -> QrpcResult<Option<quinn::IdleTimeout>> {
timeout
.map(|d| {
d.try_into().map_err(|e| {
QrpcError::MessageDecode(format!("invalid max idle timeout {d:?}: {e}"))
})
})
.transpose()
}
pub(crate) fn load_cert_der(path: &str) -> QrpcResult<Vec<CertificateDer<'static>>> {
debug!(path = path, "loading certificate file");
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let certs = rustls_pemfile::certs(&mut reader).collect::<std::io::Result<Vec<_>>>()?;
if certs.is_empty() {
return Err(QrpcError::MessageDecode(format!(
"certificate file is empty: {path}"
)));
}
debug!(path = path, count = certs.len(), "certificate file loaded");
Ok(certs)
}
pub(crate) fn load_key_der(path: &str) -> QrpcResult<PrivateKeyDer<'static>> {
debug!(path = path, "loading private key file");
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let key = rustls_pemfile::private_key(&mut reader)?
.ok_or_else(|| QrpcError::MessageDecode(format!("private key file is empty: {path}")))?;
debug!(path = path, "private key loaded");
Ok(key)
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use super::*;
fn tmp_file(name: &str, content: &[u8]) -> String {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time")
.as_nanos();
let path = PathBuf::from(std::env::temp_dir()).join(format!("qrpc_{name}_{ts}.pem"));
fs::write(&path, content).expect("write temp file");
path.to_string_lossy().to_string()
}
#[test]
fn load_cert_missing_fails() {
assert!(load_cert_der("/tmp/qrpc_not_exists.pem").is_err());
}
#[test]
fn load_key_empty_fails() {
let p = tmp_file("empty_key", b"");
assert!(load_key_der(&p).is_err());
let _ = fs::remove_file(p);
}
#[test]
fn build_quic_configs_from_test_certs() {
ensure_rustls_provider().expect("provider init");
let ca = "tests/certs/ca.crt";
let cert = "tests/certs/server.crt";
let key = "tests/certs/server.key";
let transport_options = TransportOptions::default();
let server_cfg = build_server_config(ca, cert, key, transport_options).expect("server cfg");
let client_cfg = build_client_config(ca, cert, key, transport_options).expect("client cfg");
let _ = server_cfg;
let _ = client_cfg;
}
}