use anyhow::{Context, Result};
use http::{Method, Request, StatusCode};
use jsonwebtoken::{encode, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio_rustls::rustls::pki_types::ServerName;
use tokio_rustls::rustls::ClientConfig;
use tokio_rustls::TlsConnector;
#[derive(Debug, Serialize, Deserialize)]
struct JwtClaims {
token_id: String,
user_id: i32,
allowed_regions: Vec<String>,
exp: i64,
iat: i64,
}
fn generate_test_token(secret: &str) -> Result<String> {
let now = chrono::Utc::now().timestamp();
let claims = JwtClaims {
token_id: "test-token-h2-client".to_string(),
user_id: 42,
allowed_regions: vec!["us-east".to_string(), "eu-west".to_string()],
exp: now + 3600,
iat: now,
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(secret.as_ref()),
)
.context("Failed to generate JWT token")?;
Ok(token)
}
fn create_tls_config() -> Arc<ClientConfig> {
use std::sync::Arc as StdArc;
use tokio_rustls::rustls;
#[derive(Debug)]
struct DangerousNoVerifier;
impl rustls::client::danger::ServerCertVerifier for DangerousNoVerifier {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::ED448,
]
}
}
let mut config = ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(StdArc::new(DangerousNoVerifier))
.with_no_client_auth();
config.alpn_protocols = vec![b"h2".to_vec()];
Arc::new(config)
}
#[tokio::test]
async fn test_h2_missing_auth_returns_407() -> Result<()> {
println!("\n=== Test 1: Missing Auth → 407 ===");
let stream = TcpStream::connect("127.0.0.1:8443")
.await
.context("Failed to connect to proxy")?;
let connector = TlsConnector::from(create_tls_config());
let domain = ServerName::try_from("localhost").unwrap().to_owned();
let tls_stream = connector
.connect(domain, stream)
.await
.context("TLS handshake failed")?;
let (mut client, h2) = h2::client::handshake(tls_stream)
.await
.context("Failed to perform HTTP/2 handshake")?;
tokio::spawn(async move {
if let Err(e) = h2.await {
eprintln!("HTTP/2 connection error: {}", e);
}
});
let request = Request::builder()
.method(Method::CONNECT)
.uri("example.com:443")
.body(())
.unwrap();
let (response, _) = client
.send_request(request, true)
.context("Failed to send request")?;
let response = response.await.context("Failed to receive response")?;
println!("Status: {:?}", response.status());
println!("Headers: {:?}", response.headers());
assert_eq!(
response.status(),
StatusCode::PROXY_AUTHENTICATION_REQUIRED,
"Expected 407 for missing auth"
);
assert!(
response.headers().contains_key("proxy-authenticate"),
"Expected Proxy-Authenticate header"
);
println!("✅ Test passed: 407 with Bearer challenge");
Ok(())
}
#[tokio::test]
async fn test_h2_invalid_jwt_returns_403() -> Result<()> {
println!("\n=== Test 2: Invalid JWT → 403 ===");
let stream = TcpStream::connect("127.0.0.1:8443")
.await
.context("Failed to connect to proxy")?;
let connector = TlsConnector::from(create_tls_config());
let domain = ServerName::try_from("localhost").unwrap().to_owned();
let tls_stream = connector
.connect(domain, stream)
.await
.context("TLS handshake failed")?;
let (mut client, h2) = h2::client::handshake(tls_stream)
.await
.context("Failed to perform HTTP/2 handshake")?;
tokio::spawn(async move {
if let Err(e) = h2.await {
eprintln!("HTTP/2 connection error: {}", e);
}
});
let request = Request::builder()
.method(Method::CONNECT)
.uri("example.com:443")
.header("proxy-authorization", "Bearer invalid.jwt.token")
.body(())
.unwrap();
let (response, _) = client
.send_request(request, true)
.context("Failed to send request")?;
let response = response.await.context("Failed to receive response")?;
println!("Status: {:?}", response.status());
assert_eq!(
response.status(),
StatusCode::FORBIDDEN,
"Expected 403 for invalid JWT"
);
println!("✅ Test passed: 403 for invalid JWT");
Ok(())
}
#[tokio::test]
async fn test_h2_valid_jwt_returns_200() -> Result<()> {
println!("\n=== Test 3: Valid JWT → 200 or 502 ===");
let stream = TcpStream::connect("127.0.0.1:8443")
.await
.context("Failed to connect to proxy")?;
let connector = TlsConnector::from(create_tls_config());
let domain = ServerName::try_from("localhost").unwrap().to_owned();
let tls_stream = connector
.connect(domain, stream)
.await
.context("TLS handshake failed")?;
let (mut client, h2) = h2::client::handshake(tls_stream)
.await
.context("Failed to perform HTTP/2 handshake")?;
tokio::spawn(async move {
if let Err(e) = h2.await {
eprintln!("HTTP/2 connection error: {}", e);
}
});
let token = generate_test_token("test_secret_at_least_32_characters!!")?;
let request = Request::builder()
.method(Method::CONNECT)
.uri("neverssl.com:443")
.header("proxy-authorization", format!("Bearer {}", token))
.body(())
.unwrap();
let (response, _stream) = client
.send_request(request, false)
.context("Failed to send request")?;
let response = response.await.context("Failed to receive response")?;
println!("Status: {:?}", response.status());
assert!(
response.status() == StatusCode::OK || response.status() == StatusCode::BAD_GATEWAY,
"Expected 200 or 502, got {:?}",
response.status()
);
println!("✅ Test passed: Valid JWT accepted");
Ok(())
}
#[test]
fn test_instructions() {
println!("\n╔═══════════════════════════════════════════════════════════╗");
println!("║ HTTP/2 Extended CONNECT Test Harness ║");
println!("╚═══════════════════════════════════════════════════════════╝");
println!();
println!("To run these tests:");
println!();
println!("1. Start the proxy server in another terminal:");
println!(" $ TLS_CERT_PATH=/tmp/test-certs/cert.pem \\");
println!(" TLS_KEY_PATH=/tmp/test-certs/key.pem \\");
println!(" PROXY_PORT=8443 \\");
println!(" JWT_SECRET='test_secret_at_least_32_characters!!' \\");
println!(" cargo run --release");
println!();
println!("2. Run the HTTP/2 client tests:");
println!(" $ cargo test --test h2_client_harness -- --nocapture");
println!();
println!("Tests will validate:");
println!(" ✓ Missing auth → 407 Proxy Authentication Required");
println!(" ✓ Invalid JWT → 403 Forbidden");
println!(" ✓ Valid JWT → 200 OK or 502 Bad Gateway");
println!();
println!("Note: TLS verification is disabled for self-signed certs");
println!();
}