gatewayd: net, TCP or verified TLS to the Mattermost server
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
//! Connections, plain and TLS, against local servers with TEST-ONLY certificates (M4a spec,
|
||||
//! section 5): the right CA is trusted through `ca_file`; an unknown CA and a wrong name are
|
||||
//! refused, so verification is on. Do not edit.
|
||||
|
||||
#[path = "support/tls_server.rs"]
|
||||
mod tls_server;
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
use gatewayd::config::ServerUrl;
|
||||
use gatewayd::net::{Connector, NetError, Stream};
|
||||
use tls_server::{echo_line, fixture, serve, server_config};
|
||||
|
||||
const T: Duration = Duration::from_secs(5);
|
||||
|
||||
fn url(tls: bool, host: &str, port: u16) -> ServerUrl {
|
||||
ServerUrl {
|
||||
tls,
|
||||
host: host.to_string(),
|
||||
port,
|
||||
}
|
||||
}
|
||||
|
||||
fn round_trip(mut stream: Stream) -> String {
|
||||
stream.write_all(b"hello over the wire\n").unwrap();
|
||||
stream.flush().unwrap();
|
||||
let mut line = String::new();
|
||||
BufReader::new(stream).read_line(&mut line).unwrap();
|
||||
line
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_tcp() {
|
||||
let addr = serve(None, echo_line);
|
||||
let c = Connector::new(url(false, "127.0.0.1", addr.port()), None).unwrap();
|
||||
let stream = c.connect(T).unwrap();
|
||||
assert!(matches!(stream, Stream::Plain(_)));
|
||||
assert_eq!(round_trip(stream), "hello over the wire\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_to_a_server_signed_by_the_ca_file() {
|
||||
let addr = serve(Some(server_config("server")), echo_line);
|
||||
for host in ["localhost", "127.0.0.1"] {
|
||||
let c =
|
||||
Connector::new(url(true, host, addr.port()), Some(&fixture("test-ca.pem"))).unwrap();
|
||||
let stream = c.connect(T).unwrap_or_else(|e| panic!("{host}: {e}"));
|
||||
assert!(matches!(stream, Stream::Tls(_)));
|
||||
assert_eq!(round_trip(stream), "hello over the wire\n", "{host}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_ca_is_refused_at_connect() {
|
||||
let addr = serve(Some(server_config("other-server")), echo_line);
|
||||
let c = Connector::new(
|
||||
url(true, "localhost", addr.port()),
|
||||
Some(&fixture("test-ca.pem")),
|
||||
)
|
||||
.unwrap();
|
||||
match c.connect(T) {
|
||||
Err(NetError::Tls(why)) => assert!(why.to_lowercase().contains("certificate"), "{why}"),
|
||||
Err(e) => panic!("expected a TLS error, got {e}"),
|
||||
Ok(_) => panic!("a server signed by an unknown CA was accepted"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wrong_name_is_refused_at_connect() {
|
||||
let addr = serve(Some(server_config("wrong-name")), echo_line);
|
||||
let c = Connector::new(
|
||||
url(true, "localhost", addr.port()),
|
||||
Some(&fixture("test-ca.pem")),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(c.connect(T), Err(NetError::Tls(_))),
|
||||
"a certificate for wrong.example was accepted for localhost"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_to_a_plain_server_fails_and_does_not_hang() {
|
||||
let addr = serve(None, |_conn| std::thread::sleep(Duration::from_secs(30)));
|
||||
let c = Connector::new(
|
||||
url(true, "localhost", addr.port()),
|
||||
Some(&fixture("test-ca.pem")),
|
||||
)
|
||||
.unwrap();
|
||||
let started = std::time::Instant::now();
|
||||
assert!(c.connect(Duration::from_millis(300)).is_err());
|
||||
assert!(started.elapsed() < Duration::from_secs(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bad_ca_file_is_an_error_before_any_connection() {
|
||||
let missing = fixture("no-such.pem");
|
||||
assert!(matches!(
|
||||
Connector::new(url(true, "localhost", 1), Some(&missing)),
|
||||
Err(NetError::Roots(_))
|
||||
));
|
||||
let not_pem = fixture("README.md");
|
||||
assert!(matches!(
|
||||
Connector::new(url(true, "localhost", 1), Some(¬_pem)),
|
||||
Err(NetError::Roots(_))
|
||||
));
|
||||
// A plain server needs no certificates at all.
|
||||
assert!(Connector::new(url(false, "localhost", 1), Some(&missing)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nothing_listening_is_a_connect_error() {
|
||||
let port = {
|
||||
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
l.local_addr().unwrap().port()
|
||||
};
|
||||
let c = Connector::new(url(false, "127.0.0.1", port), None).unwrap();
|
||||
assert!(matches!(c.connect(T), Err(NetError::Connect(_))));
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Small TCP and TLS servers for tests, using the TEST-ONLY certificates in `fixtures/tls/`.
|
||||
//! Each serves connections on its own thread with a function of the connection. Do not edit.
|
||||
|
||||
#![allow(dead_code)] // each test file uses a different part of this module
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{SocketAddr, TcpListener, TcpStream};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustls::pki_types::pem::PemObject;
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
|
||||
use rustls::{ServerConfig, ServerConnection, StreamOwned};
|
||||
|
||||
pub fn fixture(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/tls")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
/// A server certificate (`server`, `wrong-name`, `other-server`) and its key.
|
||||
pub fn server_config(which: &str) -> Arc<ServerConfig> {
|
||||
let certs: Vec<CertificateDer<'static>> =
|
||||
CertificateDer::pem_file_iter(fixture(&format!("{which}.pem")))
|
||||
.unwrap()
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap();
|
||||
let key = PrivateKeyDer::from_pem_file(fixture(&format!("{which}.key"))).unwrap();
|
||||
let provider = Arc::new(rustls::crypto::ring::default_provider());
|
||||
let config = ServerConfig::builder_with_provider(provider)
|
||||
.with_safe_default_protocol_versions()
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(certs, key)
|
||||
.unwrap();
|
||||
Arc::new(config)
|
||||
}
|
||||
|
||||
/// Anything a test server can serve: plain TCP, or TLS over it.
|
||||
pub trait Conn: Read + Write + Send {}
|
||||
impl<T: Read + Write + Send> Conn for T {}
|
||||
|
||||
/// Serve every connection on 127.0.0.1 with `handle`, in plain TCP (`tls` None) or TLS.
|
||||
pub fn serve<F>(tls: Option<Arc<ServerConfig>>, handle: F) -> SocketAddr
|
||||
where
|
||||
F: Fn(Box<dyn Conn>) + Send + Sync + 'static,
|
||||
{
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let handle = Arc::new(handle);
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
let Ok(stream) = stream else { continue };
|
||||
let handle = Arc::clone(&handle);
|
||||
let tls = tls.clone();
|
||||
std::thread::spawn(move || match tls {
|
||||
None => handle(Box::new(stream)),
|
||||
Some(config) => {
|
||||
let conn = ServerConnection::new(config).unwrap();
|
||||
let tls_stream: StreamOwned<ServerConnection, TcpStream> =
|
||||
StreamOwned::new(conn, stream);
|
||||
handle(Box::new(tls_stream));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
addr
|
||||
}
|
||||
|
||||
/// A handler that reads one line and writes it back.
|
||||
pub fn echo_line(mut conn: Box<dyn Conn>) {
|
||||
let mut line = Vec::new();
|
||||
let mut byte = [0u8; 1];
|
||||
while conn.read(&mut byte).map(|n| n == 1).unwrap_or(false) {
|
||||
line.push(byte[0]);
|
||||
if byte[0] == b'\n' {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = conn.write_all(&line);
|
||||
let _ = conn.flush();
|
||||
}
|
||||
Reference in New Issue
Block a user