use std::net::TcpStream; use std::process::{Child, Command}; use std::{env, panic, thread}; const HOST: &str = "localhost"; const PORT: &str = "8443"; #[test] #[ignore] // This test requires the client & server binaries be present. fn client_server_integration() { let server_addr = format!("{}:{}", HOST, PORT); if TcpStream::connect(&server_addr).is_ok() { panic!("cannot run tests; something is already listening on {server_addr}"); } let valgrind = env::var("VALGRIND").ok(); let standard_server = TestCase { name: "Standard client/server tests", server_opts: ServerOptions { valgrind: valgrind.clone(), env: vec![], }, client_tests: standard_client_tests(valgrind.clone()), }; let vectored_server = TestCase { name: "Vectored server tests", server_opts: ServerOptions { valgrind: valgrind.clone(), env: vec![("VECTORED_IO", "1")], }, client_tests: standard_client_tests(valgrind.clone()), }; let mandatory_client_auth_server = TestCase { name: "Mandatory client auth tests", server_opts: ServerOptions { valgrind: valgrind.clone(), env: vec![("AUTH_CERT", "testdata/minica.pem")], }, client_tests: vec![ ClientTest { name: "No client auth", valgrind: valgrind.clone(), env: vec![("CA_FILE", "testdata/minica.pem")], expect_error: true, // Client connecting w/o AUTH_CERT/AUTH_KEY should err. }, ClientTest { name: "Valid client auth", valgrind: valgrind.clone(), env: vec![ ("CA_FILE", "testdata/minica.pem"), ("AUTH_CERT", "testdata/localhost/cert.pem"), ("AUTH_KEY", "testdata/localhost/key.pem"), ], expect_error: false, }, ], }; let mandatory_client_auth_server_with_crls = TestCase { name: "Mandatory client auth w/ CRLs tests", server_opts: ServerOptions { valgrind: valgrind.clone(), env: vec![ ("AUTH_CERT", "testdata/minica.pem"), ("AUTH_CRL", "testdata/test.crl.pem"), ], }, client_tests: vec![ ClientTest { name: "Valid client auth", valgrind: valgrind.clone(), env: vec![ ("CA_FILE", "testdata/minica.pem"), ("AUTH_CERT", "testdata/example.com/cert.pem"), ("AUTH_KEY", "testdata/example.com/key.pem"), ], expect_error: false, }, ClientTest { name: "Revoked client auth", valgrind: valgrind.clone(), env: vec![ ("CA_FILE", "testdata/minica.pem"), ("AUTH_CERT", "testdata/localhost/cert.pem"), ("AUTH_KEY", "testdata/localhost/key.pem"), ], expect_error: true, // Client connecting w/ revoked cert should err. }, ], }; let custom_ciphersuites = TestCase { name: "client/server with limited ciphersuites", server_opts: ServerOptions { valgrind: valgrind.clone(), env: vec![("RUSTLS_CIPHERSUITE", "TLS13_CHACHA20_POLY1305_SHA256")], }, client_tests: vec![ ClientTest { name: "limited ciphersuite, supported by server", valgrind: valgrind.clone(), env: vec![ ("NO_CHECK_CERTIFICATE", "1"), ("RUSTLS_CIPHERSUITE", "TLS13_CHACHA20_POLY1305_SHA256"), ], expect_error: false, }, ClientTest { name: "limited ciphersuite, not supported by server", valgrind: valgrind.clone(), env: vec![ ("NO_CHECK_CERTIFICATE", "1"), ("RUSTLS_CIPHERSUITE", "TLS13_AES_128_GCM_SHA256"), ], expect_error: true, // Unsupported ciphersuite. }, ], }; TestCases(vec![ standard_server, vectored_server, mandatory_client_auth_server, mandatory_client_auth_server_with_crls, custom_ciphersuites, ]) .run(); } fn standard_client_tests(valgrind: Option) -> Vec { vec![ ClientTest { name: "rustls-platform-verifier", valgrind: valgrind.clone(), env: vec![("RUSTLS_PLATFORM_VERIFIER", "1")], expect_error: true, }, ClientTest { name: "With CA_FILE", valgrind: valgrind.clone(), env: vec![("CA_FILE", "testdata/minica.pem")], expect_error: false, }, ClientTest { name: "No certificate validation", valgrind: valgrind.clone(), env: vec![("NO_CHECK_CERTIFICATE", "1")], expect_error: false, }, ClientTest { name: "Client Vectored I/O", valgrind: valgrind.clone(), env: vec![("CA_FILE", "testdata/minica.pem"), ("USE_VECTORED", "1")], expect_error: false, }, ClientTest { name: "Client authentication", valgrind: valgrind.clone(), env: vec![ ("CA_FILE", "testdata/minica.pem"), ("AUTH_CERT", "testdata/localhost/cert.pem"), ("AUTH_KEY", "testdata/localhost/key.pem"), ], expect_error: false, }, ] } struct ClientTest { name: &'static str, valgrind: Option, env: Vec<(&'static str, &'static str)>, expect_error: bool, } impl ClientTest { fn run(&self) { let client_binary = client_binary(); let args = vec![HOST, PORT, "/"]; let (program, args) = match &self.valgrind { None => (client_binary.as_str(), args), Some(valgrind) => ( valgrind.as_str(), [vec![client_binary.as_str()], args].concat(), ), }; let result = Command::new(program) .args(args) .envs(self.env.clone()) .output() .unwrap_or_else(|_| panic!("failed to run client binary {client_binary}")); let passed = result.status.success() != self.expect_error; if !passed { println!( "client test failed. Failed process output:\n {}", String::from_utf8_lossy(&result.stderr) ); } assert!(passed, "client test failed"); } } struct ServerOptions { valgrind: Option, env: Vec<(&'static str, &'static str)>, } impl ServerOptions { fn run_server(&self) -> Child { let server_binary = server_binary(); let args = vec!["testdata/localhost/cert.pem", "testdata/localhost/key.pem"]; let (program, args) = match &self.valgrind { None => (server_binary.as_str(), args), Some(valgrind) => ( valgrind.as_str(), [vec![server_binary.as_str()], args].concat(), ), }; Command::new(program) .args(args) .envs(self.env.clone()) .spawn() .unwrap_or_else(|_| panic!("failed to run server binary {server_binary}")) } } struct TestCases(Vec); impl TestCases { fn run(&self) { for test_case in &self.0 { assert!(test_case.run().is_ok(), "client test panicked"); } } } struct TestCase { name: &'static str, server_opts: ServerOptions, client_tests: Vec, } impl TestCase { fn run(&self) -> thread::Result<()> { println!("\nRunning {:?}", self.name); let mut server = self.server_opts.run_server(); let result = panic::catch_unwind(|| { for client_test in &self.client_tests { println!("\nRunning client {:?}\n", client_test.name); client_test.run(); } }); server.kill().expect("failed to kill server"); result } } fn client_binary() -> String { let custom_client_binary = env::var("CLIENT_BINARY").ok(); #[cfg(not(target_os = "windows"))] { custom_client_binary.unwrap_or(format!("{}/client", target_dir())) } #[cfg(target_os = "windows")] { custom_client_binary.unwrap_or(format!("{}/client.exe", target_dir())) } } fn server_binary() -> String { let custom_server_binary = env::var("SERVER_BINARY").ok(); #[cfg(not(target_os = "windows"))] { custom_server_binary.unwrap_or(format!("{}/server", target_dir())) } #[cfg(target_os = "windows")] { custom_server_binary.unwrap_or(format!("{}/server.exe", target_dir())) } } fn target_dir() -> String { env::var("CARGO_TARGET_DIR") .unwrap_or_else(|_| "target".to_string()) .to_string() }