#![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::DangerousNoVerification;
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_localhost_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 acme_like_handler(
_req: HyperRequest<hyper::body::Incoming>,
) -> Result<HyperResponse<Full<Bytes>>, Infallible> {
let resp = HyperResponse::builder()
.status(200)
.header("replay-nonce", "test-nonce-12345")
.header("location", "https://acme.example.com/order/abc")
.header("content-type", "text/plain")
.body(Full::new(Bytes::from_static(b"acme-body")))
.expect("build response");
Ok(resp)
}
async fn spawn_tls_server_with_headers(acceptor: TlsAcceptor) -> 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(acme_like_handler))
.await;
});
}
});
addr
}
#[tokio::test]
async fn danger_accept_invalid_certs_bool_true_accepts_self_signed() {
let ck = make_localhost_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server_with_headers(acceptor).await;
let client = oxihttp::Client::builder()
.with_webpki_roots()
.danger_accept_invalid_certs(true)
.build_https()
.expect("build_https with danger_accept_invalid_certs(true)");
let url = format!("https://localhost:{}/", addr.port());
let resp = client
.get(&url)
.expect("GET")
.send()
.await
.expect("send — danger_accept_invalid_certs(true) must not fail on self-signed cert");
assert_eq!(
resp.status(),
oxihttp::StatusCode::OK,
"expected 200 OK from self-signed TLS server when skip-verify is enabled"
);
}
#[tokio::test]
async fn danger_accept_invalid_certs_bool_false_rejects_self_signed() {
let ck = make_localhost_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server_with_headers(acceptor).await;
let client = oxihttp::Client::builder()
.with_webpki_roots()
.danger_accept_invalid_certs(false)
.build_https()
.expect("build_https");
let url = format!("https://localhost:{}/", addr.port());
let result = client.get(&url).expect("GET").send().await;
assert!(
result.is_err(),
"expected TLS error when connecting to self-signed server with verification enabled"
);
}
#[tokio::test]
async fn with_custom_cert_verifier_dangerous_no_verification_accepts_self_signed() {
let ck = make_localhost_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server_with_headers(acceptor).await;
let provider = oxitls::pure_provider();
let verifier = Arc::new(DangerousNoVerification::new(provider));
let client = oxihttp::Client::builder()
.with_custom_cert_verifier(verifier)
.build_https()
.expect("build_https with custom verifier");
let url = format!("https://localhost:{}/", addr.port());
let resp = client
.get(&url)
.expect("GET")
.send()
.await
.expect("send — custom DangerousNoVerification verifier must accept self-signed cert");
assert_eq!(
resp.status(),
oxihttp::StatusCode::OK,
"expected 200 OK from self-signed TLS server with DangerousNoVerification verifier"
);
}
#[tokio::test]
async fn with_custom_cert_verifier_pin_specific_cert_rejects_other() {
let ck_a = make_localhost_cert();
let ck_b = make_localhost_cert();
let acceptor_a = make_acceptor(&ck_a);
let acceptor_b = make_acceptor(&ck_b);
let addr_a = spawn_tls_server_with_headers(acceptor_a).await;
let addr_b = spawn_tls_server_with_headers(acceptor_b).await;
let client_a = 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_a
.get(&url_a)
.expect("GET server_a")
.send()
.await
.expect("cert A trusted");
assert_eq!(resp.status(), oxihttp::StatusCode::OK);
let url_b = format!("https://localhost:{}/", addr_b.port());
let result = client_a.get(&url_b).expect("GET server_b").send().await;
assert!(
result.is_err(),
"cert B should not be trusted by a client pinned to cert A"
);
}
#[tokio::test]
async fn response_header_accessor_reads_replay_nonce_and_location() {
let ck = make_localhost_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server_with_headers(acceptor).await;
let client = oxihttp::Client::builder()
.with_trusted_cert_der(ck.cert_der.clone())
.build_https()
.expect("build_https (trusted self-signed cert)");
let url = format!("https://localhost:{}/", addr.port());
let resp = client.get(&url).expect("GET").send().await.expect("send");
assert_eq!(resp.status(), oxihttp::StatusCode::OK);
let nonce = resp.header("replay-nonce");
assert_eq!(
nonce,
Some("test-nonce-12345"),
"replay-nonce header must be readable via Response::header()"
);
let location = resp.header("location");
assert_eq!(
location,
Some("https://acme.example.com/order/abc"),
"location header must be readable via Response::header()"
);
let missing = resp.header("x-does-not-exist");
assert!(missing.is_none(), "absent header must yield None");
let ct = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok());
assert_eq!(ct, Some("text/plain"), "headers() bulk accessor must work");
}
#[tokio::test]
async fn response_header_accessor_plain_http() {
use bytes::Bytes;
use http_body_util::Full;
use hyper::server::conn::http1;
use hyper::service::service_fn;
let listener = tokio::net::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;
};
tokio::spawn(async move {
let io = hyper_util::rt::TokioIo::new(stream);
let _ = http1::Builder::new()
.serve_connection(
io,
service_fn(|_req: HyperRequest<hyper::body::Incoming>| async {
let resp = HyperResponse::builder()
.status(200)
.header("x-custom-header", "hello-from-server")
.body(Full::new(Bytes::from_static(b"ok")))
.expect("build response");
Ok::<_, Infallible>(resp)
}),
)
.await;
});
}
});
let client = oxihttp::Client::builder().build().expect("build");
let url = format!("http://127.0.0.1:{}/", addr.port());
let resp = client.get(&url).expect("GET").send().await.expect("send");
assert_eq!(resp.status(), oxihttp::StatusCode::OK);
assert_eq!(
resp.header("x-custom-header"),
Some("hello-from-server"),
"custom header must be readable via Response::header()"
);
assert!(
resp.header("x-not-present").is_none(),
"absent header must return None"
);
}