Skip to main content

postrust_proxy/tls/
server.rs

1//! TLS server configuration for the HTTPS listener.
2//!
3//! Kept out of `acme.rs` deliberately: serving TLS from a certificate on disk
4//! should not require the `acme` feature, which pulls in `rustls-acme` and is
5//! compiled out of some builds.
6
7use std::io::BufReader;
8use std::sync::Arc;
9
10use tokio_rustls::rustls::ServerConfig;
11
12use crate::error::{ProxyError, ProxyResult};
13
14/// ALPN protocols offered, in preference order.
15///
16/// This is what makes HTTP/2 reachable the way browsers actually use it. Without
17/// it a TLS listener negotiates nothing and every client falls back to
18/// HTTP/1.1, leaving h2 available only as cleartext h2c.
19pub const ALPN_PROTOCOLS: [&[u8]; 2] = [b"h2", b"http/1.1"];
20
21/// Select the rustls crypto provider for this process.
22///
23/// Both `aws-lc-rs` and `ring` end up in the dependency tree -- rustls is
24/// configured for the former, reqwest pulls the latter -- so rustls refuses to
25/// guess and panics on first use unless one is installed explicitly. Idempotent:
26/// a second call returns Err and is ignored.
27fn install_crypto_provider() {
28    static ONCE: std::sync::Once = std::sync::Once::new();
29    ONCE.call_once(|| {
30        let _ = tokio_rustls::rustls::crypto::aws_lc_rs::default_provider().install_default();
31    });
32}
33
34/// Build a rustls `ServerConfig` from PEM bytes, with ALPN configured.
35pub fn build_server_config(cert_pem: &[u8], key_pem: &[u8]) -> ProxyResult<Arc<ServerConfig>> {
36    install_crypto_provider();
37
38    let certs: Vec<_> = rustls_pemfile::certs(&mut BufReader::new(cert_pem))
39        .filter_map(|r| r.ok())
40        .collect();
41
42    if certs.is_empty() {
43        return Err(ProxyError::Tls("no certificates found in PEM".into()));
44    }
45
46    let key = rustls_pemfile::private_key(&mut BufReader::new(key_pem))
47        .map_err(|e| ProxyError::Tls(format!("could not parse private key: {}", e)))?
48        .ok_or_else(|| ProxyError::Tls("no private key found in PEM".into()))?;
49
50    let mut config = ServerConfig::builder()
51        .with_no_client_auth()
52        .with_single_cert(certs, key)
53        .map_err(|e| ProxyError::Tls(format!("could not build TLS config: {}", e)))?;
54
55    config.alpn_protocols = ALPN_PROTOCOLS.iter().map(|p| p.to_vec()).collect();
56
57    Ok(Arc::new(config))
58}
59
60/// Load a certificate and key from disk and build a `ServerConfig`.
61pub async fn load_server_config(cert_file: &str, key_file: &str) -> ProxyResult<Arc<ServerConfig>> {
62    let cert_pem = tokio::fs::read(cert_file)
63        .await
64        .map_err(|e| ProxyError::Tls(format!("could not read {}: {}", cert_file, e)))?;
65    let key_pem = tokio::fs::read(key_file)
66        .await
67        .map_err(|e| ProxyError::Tls(format!("could not read {}: {}", key_file, e)))?;
68    build_server_config(&cert_pem, &key_pem)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_alpn_offers_h2_first() {
77        // Order is preference order. If h2 is dropped or demoted, HTTP/2 stops
78        // being reachable over TLS and every client silently falls back to
79        // HTTP/1.1 -- which is exactly the state this module was added to fix.
80        assert_eq!(ALPN_PROTOCOLS[0], b"h2");
81        assert_eq!(ALPN_PROTOCOLS[1], b"http/1.1");
82    }
83
84    #[test]
85    fn test_rejects_pem_without_certificate() {
86        let err = build_server_config(b"not a certificate", b"not a key").unwrap_err();
87        assert!(
88            err.to_string().contains("no certificates"),
89            "unexpected error: {err}"
90        );
91    }
92
93    #[test]
94    fn test_rejects_certificate_without_key() {
95        // A well-formed but empty cert block, and no key at all.
96        let cert = b"-----BEGIN CERTIFICATE-----\nMA==\n-----END CERTIFICATE-----\n";
97        let err = build_server_config(cert, b"").unwrap_err();
98        assert!(
99            err.to_string().contains("no private key"),
100            "unexpected error: {err}"
101        );
102    }
103}