#![cfg(feature = "tls")]
use bytes::Bytes;
use http_body_util::Full;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper::Request as HyperRequest;
use hyper::Response as HyperResponse;
use oxihttp::RequestTlsConfig;
use oxitls::rcgen_bridge::{generate_self_signed_ed25519, CertifiedKey};
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::net::TcpListener;
use tokio_rustls::TlsAcceptor;
fn make_cert() -> CertifiedKey {
generate_self_signed_ed25519(&["localhost"]).expect("cert gen")
}
fn make_acceptor(ck: &CertifiedKey) -> TlsAcceptor {
let cert = CertificateDer::from(ck.cert_der.clone());
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(ck.pkcs8_der.clone()));
let server_cfg = oxitls::tls13::ServerBuilder::new()
.with_der_cert_and_key(vec![cert], key)
.build()
.expect("server TLS config");
TlsAcceptor::from(Arc::new(server_cfg))
}
async fn echo_handler(
_req: HyperRequest<hyper::body::Incoming>,
body: &'static str,
) -> Result<HyperResponse<Full<Bytes>>, Infallible> {
Ok(HyperResponse::new(Full::new(Bytes::from_static(
body.as_bytes(),
))))
}
async fn spawn_tls_server(acceptor: TlsAcceptor, body: &'static str) -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
let addr = listener.local_addr().expect("local addr");
tokio::spawn(async move {
loop {
let Ok((stream, _)) = listener.accept().await else {
break;
};
let acceptor = acceptor.clone();
tokio::spawn(async move {
let Ok(tls_stream) = acceptor.accept(stream).await else {
return;
};
let io = hyper_util::rt::TokioIo::new(tls_stream);
let _ = http1::Builder::new()
.serve_connection(io, service_fn(move |req| echo_handler(req, body)))
.await;
});
}
});
addr
}
#[tokio::test]
async fn per_request_trusted_cert_overrides_global_config() {
let ck_a = make_cert();
let ck_b = make_cert();
let acceptor_a = make_acceptor(&ck_a);
let acceptor_b = make_acceptor(&ck_b);
let addr_a = spawn_tls_server(acceptor_a, "server-a").await;
let addr_b = spawn_tls_server(acceptor_b, "server-b").await;
let client = oxihttp::Client::builder()
.with_trusted_cert_der(ck_a.cert_der.clone())
.build_https()
.expect("build_https (cert A)");
let url_a = format!("https://localhost:{}/", addr_a.port());
let resp = client
.get(&url_a)
.expect("GET server_a")
.send()
.await
.expect("send to server_a");
assert_eq!(
resp.status(),
oxihttp::StatusCode::OK,
"server_a (trusted globally) should return 200"
);
let body = resp.body_text().await.expect("body text server_a");
assert_eq!(body, "server-a");
let pinned_client = client
.with_request_tls_config(RequestTlsConfig::new().with_trusted_cert(ck_b.cert_der.clone()))
.expect("build per-request client for server_b");
let url_b = format!("https://localhost:{}/", addr_b.port());
let resp = pinned_client
.get(&url_b)
.expect("GET server_b (overridden)")
.send()
.await
.expect("send to server_b with override");
assert_eq!(
resp.status(),
oxihttp::StatusCode::OK,
"server_b (trusted via override) should return 200"
);
let body = resp.body_text().await.expect("body text server_b");
assert_eq!(body, "server-b");
let result = client
.get(&url_b)
.expect("GET server_b (global)")
.send()
.await;
assert!(
result.is_err(),
"server_b cert is not trusted by global client; expected TLS error"
);
let err = result.expect_err("expected TLS error");
let is_tls_like = matches!(err, oxihttp::OxiHttpError::Tls(_))
|| matches!(&err, oxihttp::OxiHttpError::Hyper(msg) if msg.contains("Connect"));
assert!(is_tls_like, "expected TLS-related error, got: {err:?}");
}
#[tokio::test]
async fn per_request_accept_invalid_bypasses_verification() {
let ck = make_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server(acceptor, "bypass-ok").await;
let client = oxihttp::Client::builder()
.with_webpki_roots()
.build_https()
.expect("build_https (webpki roots)");
let url = format!("https://localhost:{}/", addr.port());
let result_no_override = client.get(&url).expect("GET").send().await;
assert!(
result_no_override.is_err(),
"self-signed cert should fail without override"
);
let bypass_client = client
.with_request_tls_config(RequestTlsConfig::new().with_accept_invalid_certs())
.expect("build per-request bypass client");
let resp = bypass_client
.get(&url)
.expect("GET with override")
.send()
.await
.expect("send with accept_invalid_certs override");
assert_eq!(
resp.status(),
oxihttp::StatusCode::OK,
"accept_invalid_certs override should bypass cert verification"
);
let body = resp.body_text().await.expect("body text");
assert_eq!(body, "bypass-ok");
}
#[tokio::test]
async fn per_request_config_is_chainable() {
let ck_a = make_cert();
let ck_b = make_cert();
let acceptor_b = make_acceptor(&ck_b);
let addr_b = spawn_tls_server(acceptor_b, "chain-ok").await;
let client_a = oxihttp::Client::builder()
.with_trusted_cert_der(ck_a.cert_der.clone())
.build_https()
.expect("build_https (cert A)");
let client_b = client_a
.with_request_tls_config(RequestTlsConfig::new().with_trusted_cert(ck_b.cert_der.clone()))
.expect("first override");
let client_b2 = client_b
.with_request_tls_config(RequestTlsConfig::new())
.expect("chained override (no change)");
let url_b = format!("https://localhost:{}/", addr_b.port());
let resp = client_b2
.get(&url_b)
.expect("GET")
.send()
.await
.expect("send");
assert_eq!(
resp.status(),
oxihttp::StatusCode::OK,
"chained override should still trust cert B"
);
let body = resp.body_text().await.expect("body text");
assert_eq!(body, "chain-ok");
}