#![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 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 handle_request(
req: HyperRequest<hyper::body::Incoming>,
) -> Result<HyperResponse<Full<Bytes>>, Infallible> {
use http_body_util::BodyExt;
let method = req.method().clone();
let path = req.uri().path().to_string();
match (method, path.as_str()) {
(hyper::Method::GET, "/hello") => {
Ok(HyperResponse::new(Full::new(Bytes::from("hello tls"))))
}
(hyper::Method::POST, "/echo") => {
let body = req.into_body().collect().await.expect("collect").to_bytes();
Ok(HyperResponse::new(Full::new(body)))
}
(hyper::Method::GET, "/alpn") => {
Ok(HyperResponse::new(Full::new(Bytes::from("alpn-ok"))))
}
_ => {
let mut resp = HyperResponse::new(Full::new(Bytes::from("not found")));
*resp.status_mut() = hyper::StatusCode::NOT_FOUND;
Ok(resp)
}
}
}
async fn spawn_tls_server(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(handle_request))
.await;
});
}
});
addr
}
#[tokio::test]
async fn test_https_get() {
let ck = make_localhost_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server(acceptor).await;
let client = oxihttp::Client::builder()
.with_trusted_cert_der(ck.cert_der.clone())
.build_https()
.expect("build_https");
let url = format!("https://localhost:{}/hello", addr.port());
let resp = client.get(&url).expect("GET").send().await.expect("send");
assert_eq!(resp.status(), oxihttp::StatusCode::OK);
let body = resp.body_text().await.expect("body text");
assert_eq!(body, "hello tls");
}
#[tokio::test]
async fn test_https_post_echo() {
let ck = make_localhost_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server(acceptor).await;
let client = oxihttp::Client::builder()
.with_trusted_cert_der(ck.cert_der.clone())
.build_https()
.expect("build_https");
let url = format!("https://localhost:{}/echo", addr.port());
let payload = b"ping over tls";
let resp = client
.post(&url)
.expect("POST")
.body(Bytes::from_static(payload))
.send()
.await
.expect("send");
assert_eq!(resp.status(), oxihttp::StatusCode::OK);
let body = resp.body_bytes().await.expect("body bytes");
assert_eq!(body.as_ref(), payload);
}
#[tokio::test]
async fn test_https_invalid_cert_rejected() {
let ck = make_localhost_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server(acceptor).await;
let client = oxihttp::Client::builder()
.with_webpki_roots()
.build_https()
.expect("build_https should succeed when webpki roots are configured");
let url = format!("https://localhost:{}/hello", addr.port());
let result = client.get(&url).expect("GET").send().await;
assert!(result.is_err(), "untrusted self-signed cert should fail");
let err = result.expect_err("tls/connect 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 test_https_danger_accept_invalid() {
let ck = make_localhost_cert();
let acceptor = make_acceptor(&ck);
let addr = spawn_tls_server(acceptor).await;
let client = oxihttp::Client::builder()
.with_danger_accept_invalid_certs()
.build_https()
.expect("build_https");
let url = format!("https://localhost:{}/hello", addr.port());
let resp = client.get(&url).expect("GET").send().await.expect("send");
assert_eq!(resp.status(), oxihttp::StatusCode::OK);
let body = resp.body_text().await.expect("body text");
assert_eq!(body, "hello tls");
}
#[tokio::test]
async fn test_https_webpki_roots_builds() {
let result = oxihttp::Client::builder().with_webpki_roots().build_https();
assert!(
result.is_ok(),
"webpki roots client should build: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_http_uri_with_tls_client() {
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 std::convert::Infallible;
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;
};
tokio::spawn(async move {
let _ = http1::Builder::new()
.serve_connection(
hyper_util::rt::TokioIo::new(stream),
service_fn(|_req: HyperRequest<hyper::body::Incoming>| async {
Ok::<_, Infallible>(HyperResponse::new(Full::new(Bytes::from(
"plain ok",
))))
}),
)
.await;
});
}
});
let client = oxihttp::Client::builder()
.with_danger_accept_invalid_certs()
.build_https()
.expect("build_https");
let url = format!("http://{addr}/");
let resp = client.get(&url).expect("GET").send().await.expect("send");
assert_eq!(resp.status(), oxihttp::StatusCode::OK);
let body = resp.body_text().await.expect("body text");
assert_eq!(body, "plain ok");
}