#![cfg(feature = "tls")]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use thunder::server::{
spawn_listener, AuthError, Credentials, Dispatch, ListenerConfig, Principal, ServerInfo,
Session,
};
use thunder::wire::config::{ErrorConvention, Handshake, HelloStyle, PushPolicy, TlsPolicy};
use thunder::{Client, ClientConfig, ClientError, ClientTls, Config, ServerTls, Value};
struct Echo;
impl Dispatch for Echo {
type Identity = ();
async fn dispatch(
&self,
_session: &Session,
command: &str,
mut args: Vec<Value>,
) -> Result<Value, String> {
match command {
"PING" => Ok(Value::Str("PONG".to_owned())),
"ECHO" => Ok(if args.is_empty() {
Value::Null
} else {
args.swap_remove(0)
}),
other => Err(format!("ERR unknown command '{other}'")),
}
}
async fn authenticate(&self, _creds: Credentials) -> Result<Principal, AuthError> {
Ok(Principal::new("tls-test".to_owned()))
}
}
fn profile() -> Config {
Config {
scheme: "test",
default_port: 0,
handshake: Handshake::None,
hello_style: HelloStyle::NotUsed,
push: PushPolicy::Reserved,
max_frame_bytes: 1024 * 1024,
max_in_flight: 64,
error_codes: ErrorConvention::None,
tls: TlsPolicy::Optional,
}
}
fn info() -> ServerInfo {
ServerInfo {
name: "tls-test".to_owned(),
version: "0".to_owned(),
}
}
fn loopback() -> SocketAddr {
SocketAddr::from(([127, 0, 0, 1], 0))
}
fn self_signed() -> (String, String) {
let signed = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
(signed.cert.pem(), signed.key_pair.serialize_pem())
}
fn write_temp(contents: &str, suffix: &str) -> PathBuf {
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
let path =
std::env::temp_dir().join(format!("thunder-tls-{}-{n}-{suffix}", std::process::id()));
std::fs::write(&path, contents).unwrap();
path
}
#[tokio::test]
async fn tls_round_trip_encrypts_request_and_response() {
let (cert_pem, key_pem) = self_signed();
let cert_path = write_temp(&cert_pem, "cert.pem");
let key_path = write_temp(&key_pem, "key.pem");
let handle = spawn_listener(
Arc::new(Echo),
profile(),
info(),
ListenerConfig::new(loopback()).with_tls(ServerTls {
cert_path: cert_path.clone(),
key_path: key_path.clone(),
}),
)
.await
.unwrap();
let addr = handle.local_addr().to_string();
let client = Client::connect_with(
&addr,
profile(),
ClientConfig::new().with_tls(ClientTls {
server_name: Some("localhost".to_owned()),
ca_path: Some(cert_path.clone()),
}),
)
.await
.unwrap();
assert_eq!(
client.call("PING", vec![]).await.unwrap().as_str(),
Some("PONG")
);
assert_eq!(
client
.call("ECHO", vec![Value::Str("secret-over-tls".to_owned())])
.await
.unwrap()
.as_str(),
Some("secret-over-tls")
);
client.close().await;
handle.stop().await;
let _ = std::fs::remove_file(cert_path);
let _ = std::fs::remove_file(key_path);
}
#[tokio::test]
async fn plaintext_still_works_when_tls_is_unused() {
let handle = spawn_listener(
Arc::new(Echo),
profile(),
info(),
ListenerConfig::new(loopback()),
)
.await
.unwrap();
let addr = handle.local_addr().to_string();
let client = Client::connect(&addr, profile()).await.unwrap();
assert_eq!(
client.call("PING", vec![]).await.unwrap().as_str(),
Some("PONG")
);
client.close().await;
handle.stop().await;
}
#[tokio::test]
async fn cert_mismatch_is_a_connection_error() {
let (server_cert, server_key) = self_signed();
let (other_cert, _other_key) = self_signed();
let cert_path = write_temp(&server_cert, "cert.pem");
let key_path = write_temp(&server_key, "key.pem");
let wrong_ca = write_temp(&other_cert, "wrongca.pem");
let handle = spawn_listener(
Arc::new(Echo),
profile(),
info(),
ListenerConfig::new(loopback()).with_tls(ServerTls {
cert_path: cert_path.clone(),
key_path: key_path.clone(),
}),
)
.await
.unwrap();
let addr = handle.local_addr().to_string();
let err = Client::connect_with(
&addr,
profile(),
ClientConfig::new().with_tls(ClientTls {
server_name: Some("localhost".to_owned()),
ca_path: Some(wrong_ca.clone()),
}),
)
.await
.unwrap_err();
assert!(
matches!(err, ClientError::Connection { .. }),
"expected a Connection error from an untrusted cert, got {err:?}"
);
handle.stop().await;
let _ = std::fs::remove_file(cert_path);
let _ = std::fs::remove_file(key_path);
let _ = std::fs::remove_file(wrong_ca);
}