kz-proxy 0.3.0

MITM proxy and subprocess sandbox for blind secret injection
Documentation
// Shared test utilities for E2E tests. Not a separate crate; included by test binaries that need it.
// We use a single test file that contains the helper to avoid multi-crate complexity.

use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::thread;

/// Start a TCP server that accepts one connection, reads the raw request into `captured`,
/// responds with HTTP/1.1 200 OK, then closes. Returns (port, handle, captured).
/// Caller must join the handle after the request has been made.
pub fn start_capture_server() -> Result<
    (u16, thread::JoinHandle<()>, Arc<Mutex<String>>),
    Box<dyn std::error::Error + Send + Sync>,
> {
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let port = listener.local_addr()?.port();
    let captured = Arc::new(Mutex::new(String::new()));
    let captured_clone = captured.clone();
    let handle = thread::spawn(move || {
        let (mut stream, _) = match listener.accept() {
            Ok(x) => x,
            Err(_) => return,
        };
        let mut buf = vec![0u8; 65535];
        let n = match stream.read(&mut buf) {
            Ok(n) => n,
            Err(_) => return,
        };
        let req = String::from_utf8_lossy(&buf[..n]).to_string();
        *captured_clone.lock().unwrap() = req;
        let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
        let _ = stream.write_all(response.as_bytes());
        let _ = stream.flush();
    });
    Ok((port, handle, captured))
}

/// Start an HTTPS server (TLS) that accepts one connection, reads the raw request into `captured`,
/// responds with HTTP/1.1 200 OK, then closes. Uses a self-signed cert; clients should use `-k` (insecure).
/// Returns (port, handle, captured, server_cert_path). Caller must join the handle after the request has been made.
/// server_cert_path is a temp PEM file of the server cert (for use with with_upstream_ca when testing MITM).
pub fn start_https_capture_server() -> Result<
    (
        u16,
        thread::JoinHandle<()>,
        Arc<Mutex<String>>,
        std::path::PathBuf,
    ),
    Box<dyn std::error::Error + Send + Sync>,
> {
    let captured = Arc::new(Mutex::new(String::new()));
    let captured_clone = captured.clone();
    let (port_tx, port_rx) = mpsc::sync_channel(0);

    let handle = thread::spawn(move || {
        // rustls 0.23 requires a process-level crypto provider when using ServerConfig::builder()
        let _ = rustls::crypto::ring::default_provider().install_default();

        let rt = tokio::runtime::Builder::new_current_thread()
            .enable_all()
            .build()
            .expect("runtime");
        rt.block_on(async {
            let CertifiedKey { cert, signing_key } =
                rcgen::generate_simple_self_signed(vec!["127.0.0.1".to_string()]).expect("cert");
            let cert_der = cert.der().to_vec();
            let cert_pem = cert.pem();
            let key_der = signing_key.serialize_der();

            let key_der = rustls::pki_types::PrivateKeyDer::Pkcs8(
                rustls::pki_types::PrivatePkcs8KeyDer::from(key_der),
            );
            let config = rustls::ServerConfig::builder()
                .with_no_client_auth()
                .with_single_cert(
                    vec![rustls::pki_types::CertificateDer::from(cert_der)],
                    key_der,
                )
                .expect("server config");
            let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));

            let cert_path = std::env::temp_dir().join(format!(
                "blinders-test-server-cert-{}.pem",
                std::time::SystemTime::now()
                    .duration_since(std::time::UNIX_EPOCH)
                    .unwrap()
                    .as_nanos()
            ));
            std::fs::write(&cert_path, &cert_pem).expect("write server cert");
            let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
                .await
                .expect("bind");
            let port = listener.local_addr().expect("addr").port();
            let _ = port_tx.send((port, cert_path));

            let (tcp, _) = listener.accept().await.expect("accept");
            let mut tls = acceptor.accept(tcp).await.expect("tls accept");
            use tokio::io::{AsyncReadExt, AsyncWriteExt};
            let mut buf = vec![0u8; 65535];
            let n = tls.read(&mut buf).await.expect("read");
            let req = String::from_utf8_lossy(&buf[..n]).to_string();
            *captured_clone.lock().unwrap() = req;
            let response = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
            tls.write_all(response).await.expect("write");
            tls.flush().await.expect("flush");
        });
    });

    let (port, server_cert_path) = port_rx
        .recv()
        .map_err(|_| "server thread did not send port")?;
    Ok((port, handle, captured, server_cert_path))
}

use rcgen::CertifiedKey;

/// Returns true if `curl` appears to be available on PATH.
pub fn curl_available() -> bool {
    std::process::Command::new("curl")
        .arg("--version")
        .output()
        .is_ok()
}