use super::*;
#[tokio::test]
async fn hsts_upgrade_prevents_http_request() {
let store = aioduct::hsts::HstsStore::new();
let mut sts_headers = http::HeaderMap::new();
sts_headers.insert(
http::header::HeaderName::from_static("strict-transport-security"),
"max-age=31536000".parse().unwrap(),
);
store.store_from_response("hsts-host.example.com", &sts_headers);
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.hsts(store)
.timeout(Duration::from_secs(2))
.build()
.unwrap();
let result = client
.get("http://hsts-host.example.com/path")
.unwrap()
.send()
.await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
!err.to_string().contains("HTTPS only"),
"HSTS should have upgraded the URI, error should be connection-related, got: {err}"
);
}
#[cfg(feature = "rustls")]
#[tokio::test]
async fn hsts_stores_sts_header_from_response() {
install_crypto();
let store = aioduct::hsts::HstsStore::new();
assert!(
!store.should_upgrade("127.0.0.1"),
"HSTS store should not know about 127.0.0.1 initially"
);
let (addr, cert_der, _counter) =
aioduct_test_server::tls::tls_server_with(&[b"http/1.1"], |_req| async move {
Ok::<_, Infallible>(
Response::builder()
.header("strict-transport-security", "max-age=31536000")
.body(Full::new(Bytes::from("secure response")))
.unwrap(),
)
})
.await;
let cert = aioduct::tls::Certificate::from_der(cert_der.to_vec());
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.add_root_certificates(&[cert])
.danger_accept_invalid_hostnames(true)
.hsts(store.clone())
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("https://127.0.0.1:{}/", addr.port()))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
assert_eq!(resp.text().await.unwrap(), "secure response");
assert!(
store.should_upgrade("127.0.0.1"),
"HSTS store should record host from Strict-Transport-Security response header"
);
}
#[tokio::test]
async fn digest_auth_retry_with_http_version() {
let attempt = Arc::new(AtomicU32::new(0));
let attempt_clone = attempt.clone();
let (addr, _counter) = h1_server_with(move |req| {
let attempt = attempt_clone.clone();
async move {
let n = attempt.fetch_add(1, Ordering::SeqCst);
if n == 0 {
Ok::<_, Infallible>(
Response::builder()
.status(401)
.header(
"www-authenticate",
r#"Digest realm="version-test", nonce="version123", qop="auth""#,
)
.body(Full::new(Bytes::from("unauthorized")))
.unwrap(),
)
} else {
let auth = req
.headers()
.get("authorization")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
let version = format!("{:?}", req.version());
Ok(Response::new(Full::new(Bytes::from(format!(
"version={version} auth_present={}",
!auth.is_empty()
)))))
}
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.digest_auth("user", "pass")
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.version(http::Version::HTTP_11)
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body = resp.text().await.unwrap();
assert!(
body.contains("auth_present=true"),
"digest auth retry should include authorization, got: {body}"
);
assert!(
body.contains("version=HTTP/1.1"),
"digest auth retry should preserve HTTP version, got: {body}"
);
assert_eq!(attempt.load(Ordering::SeqCst), 2);
}
#[tokio::test]
async fn digest_auth_retry_applies_middleware() {
let attempt = Arc::new(AtomicU32::new(0));
let attempt_clone = attempt.clone();
let (addr, _counter) = h1_server_with(move |req| {
let attempt = attempt_clone.clone();
async move {
let n = attempt.fetch_add(1, Ordering::SeqCst);
if n == 0 {
Ok::<_, Infallible>(
Response::builder()
.status(401)
.header(
"www-authenticate",
r#"Digest realm="mw-test", nonce="mwnonce1", qop="auth""#,
)
.body(Full::new(Bytes::from("unauthorized")))
.unwrap(),
)
} else {
let mw_header = req
.headers()
.get("x-middleware-retry")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
let auth = req
.headers()
.get("authorization")
.map(|v| v.to_str().unwrap().to_owned())
.unwrap_or_default();
Ok(Response::new(Full::new(Bytes::from(format!(
"mw={mw_header} auth_present={}",
auth.starts_with("Digest ")
)))))
}
}
})
.await;
let client = HttpEngineSend::<TokioRuntime, TcpConnector>::builder()
.digest_auth("user", "pass")
.middleware(
|req: &mut http::Request<aioduct::body::RequestBodySend>, _uri: &http::Uri| {
req.headers_mut().insert(
http::header::HeaderName::from_static("x-middleware-retry"),
http::header::HeaderValue::from_static("applied"),
);
},
)
.timeout(Duration::from_secs(5))
.build()
.unwrap();
let resp = client
.get(&format!("http://{addr}/"))
.unwrap()
.send()
.await
.unwrap();
assert_eq!(resp.status(), 200);
let body = resp.text().await.unwrap();
assert!(
body.contains("mw=applied"),
"middleware should be applied on digest auth retry, got: {body}"
);
assert!(
body.contains("auth_present=true"),
"digest auth should be present on retry, got: {body}"
);
assert_eq!(attempt.load(Ordering::SeqCst), 2);
}