use std::convert::Infallible;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Empty, Full};
use hyper::body::{Bytes, Incoming};
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::client::legacy::Client;
use hyper_util::rt::{TokioExecutor, TokioIo};
use tokio::net::{TcpListener, TcpStream};
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName};
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
use tokio_rustls::TlsConnector;
use waf_core::{
Config, TlsConfig, WafMode,
};
use std::sync::atomic::{AtomicUsize, Ordering};
use waf_proxy::tls::{
acceptor_from_config, build_server_config, FileCertSource, TlsCertSource, TlsError, TlsMaterial,
};
use waf_proxy::Proxy;
type TestBody = BoxBody<Bytes, hyper::Error>;
fn empty_body() -> TestBody {
Empty::new().map_err(|never| match never {}).boxed()
}
fn provider() -> Arc<tokio_rustls::rustls::crypto::CryptoProvider> {
Arc::new(tokio_rustls::rustls::crypto::ring::default_provider())
}
const SQLI_PATH: &str = "/?q=1%20UNION%20SELECT%20a%2Cb%20FROM%20users--";
fn cert_fixture() -> (PathBuf, PathBuf, CertificateDer<'static>) {
let ck = rcgen::generate_simple_self_signed(vec!["localhost".to_string()]).unwrap();
let der = ck.cert.der().clone();
let dir = std::env::temp_dir();
let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
let cert_path = dir.join(format!("waf-tls-{nanos}-cert.pem"));
let key_path = dir.join(format!("waf-tls-{nanos}-key.pem"));
std::fs::write(&cert_path, ck.cert.pem()).unwrap();
std::fs::write(&key_path, ck.key_pair.serialize_pem()).unwrap();
(cert_path, key_path, der)
}
async fn start_echo_backend() -> std::net::SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
tokio::spawn(async move {
http1::Builder::new()
.serve_connection(
TokioIo::new(stream),
service_fn(|req: Request<Incoming>| async move {
let pq = req.uri().path_and_query().map(|p| p.as_str()).unwrap_or("/").to_string();
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(format!("ok:{pq}")))))
}),
)
.await
.ok();
});
}
});
addr
}
fn base_config(backend: std::net::SocketAddr) -> Config {
let mut c = Config::default();
c.proxy.listen = "127.0.0.1:0".parse().unwrap();
c.proxy.backend = format!("http://{backend}");
c.waf.mode = WafMode::Blocking;
c
}
fn tls_config(backend: std::net::SocketAddr, cert: &Path, key: &Path) -> Config {
let mut c = base_config(backend);
c.tls.enabled = true;
c.tls.cert_path = cert.to_string_lossy().into_owned();
c.tls.key_path = key.to_string_lossy().into_owned();
c.tls.alpn = vec!["h2".to_string(), "http/1.1".to_string()];
c
}
async fn tls_get(
addr: std::net::SocketAddr,
trust: Option<CertificateDer<'static>>,
alpn: &[&[u8]],
path: &str,
) -> Result<(Vec<u8>, u16), String> {
let mut roots = RootCertStore::empty();
if let Some(c) = trust {
roots.add(c).unwrap();
}
let mut cfg = ClientConfig::builder_with_provider(provider())
.with_safe_default_protocol_versions()
.unwrap()
.with_root_certificates(roots)
.with_no_client_auth();
cfg.alpn_protocols = alpn.iter().map(|p| p.to_vec()).collect();
let connector = TlsConnector::from(Arc::new(cfg));
let tcp = TcpStream::connect(addr).await.map_err(|e| e.to_string())?;
let tls = connector
.connect(ServerName::try_from("localhost").unwrap(), tcp)
.await
.map_err(|e| e.to_string())?;
let negotiated = tls.get_ref().1.alpn_protocol().map(|p| p.to_vec()).unwrap_or_default();
let req = Request::builder().method("GET").uri(path).body(empty_body()).unwrap();
let status = if negotiated == b"h2" {
let (mut s, conn) = hyper::client::conn::http2::handshake(TokioExecutor::new(), TokioIo::new(tls))
.await
.map_err(|e| e.to_string())?;
tokio::spawn(async move { let _ = conn.await; });
s.send_request(req).await.map_err(|e| e.to_string())?.status().as_u16()
} else {
let (mut s, conn) = hyper::client::conn::http1::handshake(TokioIo::new(tls)).await.map_err(|e| e.to_string())?;
tokio::spawn(async move { let _ = conn.await; });
s.send_request(req).await.map_err(|e| e.to_string())?.status().as_u16()
};
Ok((negotiated, status))
}
async fn start_tls_proxy() -> (std::net::SocketAddr, CertificateDer<'static>, PathBuf, PathBuf) {
let backend = start_echo_backend().await;
let (cert_path, key_path, der) = cert_fixture();
let proxy = Proxy::bind(&tls_config(backend, &cert_path, &key_path)).await.unwrap();
let addr = proxy.local_addr().unwrap();
tokio::spawn(proxy.run());
(addr, der, cert_path, key_path)
}
fn cleanup(cert: PathBuf, key: PathBuf) {
std::fs::remove_file(cert).ok();
std::fs::remove_file(key).ok();
}
#[tokio::test]
async fn h2_over_tls_benign_is_forwarded() {
let (addr, der, cert, key) = start_tls_proxy().await;
let (alpn, status) = tls_get(addr, Some(der), &[b"h2"], "/hello").await.unwrap();
assert_eq!(alpn, b"h2", "must negotiate h2");
assert_eq!(status, 200);
cleanup(cert, key);
}
#[tokio::test]
async fn h1_over_tls_benign_is_forwarded() {
let (addr, der, cert, key) = start_tls_proxy().await;
let (alpn, status) = tls_get(addr, Some(der), &[b"http/1.1"], "/hello").await.unwrap();
assert_eq!(alpn, b"http/1.1", "must negotiate http/1.1");
assert_eq!(status, 200);
cleanup(cert, key);
}
#[tokio::test]
async fn sqli_blocked_over_h2_tls() {
let (addr, der, cert, key) = start_tls_proxy().await;
let (alpn, status) = tls_get(addr, Some(der), &[b"h2"], SQLI_PATH).await.unwrap();
assert_eq!(alpn, b"h2");
assert_eq!(status, 403, "SQLi over h2-over-TLS must be blocked");
cleanup(cert, key);
}
#[tokio::test]
async fn sqli_blocked_over_h1_tls() {
let (addr, der, cert, key) = start_tls_proxy().await;
let (_, status) = tls_get(addr, Some(der), &[b"http/1.1"], SQLI_PATH).await.unwrap();
assert_eq!(status, 403, "SQLi over h1-over-TLS must be blocked");
cleanup(cert, key);
}
#[tokio::test]
async fn untrusted_cert_handshake_fails_but_listener_survives() {
let (addr, der, cert, key) = start_tls_proxy().await;
let untrusted = tls_get(addr, None, &[b"h2"], "/x").await;
assert!(untrusted.is_err(), "handshake with an untrusted cert must fail");
let (_, status) = tls_get(addr, Some(der), &[b"h2"], "/x").await.unwrap();
assert_eq!(status, 200, "the listener must still serve after a failed handshake");
cleanup(cert, key);
}
#[tokio::test]
async fn cleartext_request_to_tls_port_is_rejected() {
let (addr, _der, cert, key) = start_tls_proxy().await;
let client: Client<HttpConnector, TestBody> =
Client::builder(TokioExecutor::new()).build(HttpConnector::new());
let res = client
.request(Request::builder().uri(format!("http://{addr}/x")).body(empty_body()).unwrap())
.await;
assert!(res.is_err(), "cleartext to a TLS port must fail, never fall back to plaintext");
cleanup(cert, key);
}
#[tokio::test]
async fn h2c_cleartext_served_when_tls_off() {
let backend = start_echo_backend().await;
let proxy = Proxy::bind(&base_config(backend)).await.unwrap();
let addr = proxy.local_addr().unwrap();
tokio::spawn(proxy.run());
let tcp = TcpStream::connect(addr).await.unwrap();
let (mut s, conn) = hyper::client::conn::http2::handshake(TokioExecutor::new(), TokioIo::new(tcp))
.await
.unwrap();
tokio::spawn(async move { let _ = conn.await; });
let resp = s
.send_request(Request::builder().method("GET").uri("/hello").body(empty_body()).unwrap())
.await
.unwrap();
assert_eq!(resp.status(), 200);
}
struct CountingCertSource {
inner: FileCertSource,
loads: Arc<AtomicUsize>,
}
impl TlsCertSource for CountingCertSource {
fn load(&self) -> Result<TlsMaterial, TlsError> {
self.loads.fetch_add(1, Ordering::Relaxed);
self.inner.load()
}
}
#[tokio::test]
async fn injected_cert_source_overrides_file_config() {
let backend = start_echo_backend().await;
let (cert_path, key_path, der) = cert_fixture();
let mut cfg = base_config(backend);
cfg.tls.enabled = true;
cfg.tls.cert_path = "/no/such/cert.pem".to_string();
cfg.tls.key_path = "/no/such/key.pem".to_string();
cfg.tls.alpn = vec!["h2".to_string(), "http/1.1".to_string()];
let loads = Arc::new(AtomicUsize::new(0));
let source = Arc::new(CountingCertSource {
inner: FileCertSource::new(&cert_path, &key_path),
loads: loads.clone(),
});
let proxy = Proxy::builder(&cfg).cert_source(source).build().await.unwrap();
let addr = proxy.local_addr().unwrap();
tokio::spawn(proxy.run());
let (alpn, status) = tls_get(addr, Some(der), &[b"h2"], "/hello").await.unwrap();
assert_eq!(alpn, b"h2", "must negotiate h2 via the injected source");
assert_eq!(status, 200, "TLS must terminate from the injected source, not the broken file config");
assert!(loads.load(Ordering::Relaxed) >= 1, "injected cert source must be consulted on the datapath");
cleanup(cert_path, key_path);
}
#[test]
fn acceptor_disabled_is_none() {
assert!(acceptor_from_config(&TlsConfig::default()).unwrap().is_none());
}
#[test]
fn acceptor_missing_cert_file_is_fatal_error() {
let mut tls = TlsConfig::default();
tls.enabled = true;
tls.cert_path = "/no/such/cert.pem".to_string();
tls.key_path = "/no/such/key.pem".to_string();
tls.alpn = vec!["h2".to_string()];
assert!(acceptor_from_config(&tls).is_err());
}
#[test]
fn server_config_sets_alpn_from_config() {
let (cert, key, _der) = cert_fixture();
let source = FileCertSource::new(&cert, &key);
let alpn = vec!["h2".to_string(), "http/1.1".to_string()];
let cfg = build_server_config(&source, &alpn).unwrap();
assert_eq!(cfg.alpn_protocols, vec![b"h2".to_vec(), b"http/1.1".to_vec()]);
cleanup(cert, key);
}