net-cat 0.2.0

Minimal hand-rolled HTTP/1.1 client over std::net::TcpStream. Plain HTTP in v0; v0.2.0 adds an optional `tls` feature that wires rustls + webpki-roots Mozilla CA bundle so `https://` URLs work via the same `fetch` / `exchange` entry points. No external HTTP crate; framing and parsing are local. No `mut` beyond FFI carve-outs (`TcpStream::read_to_end`, rustls `Stream::new(&mut conn, &mut sock)`). Sixth sub-crate of a Servo-replacement webview runtime targeting Tauri.
//! HTTPS transport over rustls + webpki-roots.
//!
//! Compiled when the `tls` feature is enabled.  `rustls::Stream`
//! requires `&mut ClientConnection` and `&mut TcpStream` and the
//! handshake's `ClientConnection::new` requires `Arc<ClientConfig>`
//! -- these are the FFI carve-outs documented in the crate
//! `CLAUDE.md`.  The `Arc` is owned by a single fn-local binding
//! and dropped at exchange end; nothing is shared across threads.

use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::Arc;

use rustls::pki_types::ServerName;
use rustls::{ClientConfig, ClientConnection, RootCertStore, Stream};

use crate::error::Error;

/// Connect to `host:port` over TCP + TLS, write `request_bytes`,
/// read until EOF, and return the raw response bytes.
///
/// # Errors
///
/// Returns [`Error::InvalidUrl`] if `host` is not a valid SNI
/// server name; [`Error::Tls`] on handshake / read / write
/// failures from rustls; [`Error::Io`] on TCP failures.
pub fn exchange_https(host: &str, port: u16, request_bytes: &[u8]) -> Result<Vec<u8>, Error> {
    install_default_crypto_provider();
    let address = format!("{host}:{port}");
    let stream = TcpStream::connect(address)?;
    let config = make_client_config();
    let server_name = ServerName::try_from(host.to_owned()).map_err(|_| Error::InvalidUrl {
        source: host.to_owned(),
    })?;
    let connection =
        ClientConnection::new(Arc::new(config), server_name).map_err(|err| Error::Tls {
            message: format!("{err}"),
        })?;
    transmit(connection, stream, request_bytes)
}

pub(crate) fn install_default_crypto_provider() {
    // `install_default` returns `Err` if a provider is already
    // installed; either way the slot ends up populated, which is
    // the postcondition we want.  Dropping the `Result` is
    // intentional.
    let _ = rustls::crypto::ring::default_provider().install_default();
}

pub(crate) fn make_client_config() -> ClientConfig {
    let root_store: RootCertStore = webpki_roots::TLS_SERVER_ROOTS.iter().cloned().collect();
    ClientConfig::builder()
        .with_root_certificates(root_store)
        .with_no_client_auth()
}

fn transmit(
    connection: ClientConnection,
    stream: TcpStream,
    request_bytes: &[u8],
) -> Result<Vec<u8>, Error> {
    // FFI carve-out: `rustls::Stream::new` requires `&mut` on
    // both halves.  We rebind to local `mut` bindings scoped to
    // this fn and own both ends, so no aliasing is possible.
    let mut connection = connection;
    let mut stream = stream;
    let mut tls = Stream::new(&mut connection, &mut stream);
    tls.write_all(request_bytes)
        .map_err(|err| io_to_tls_error(&err))?;
    tls.flush().map_err(|err| io_to_tls_error(&err))?;
    let mut buffer = Vec::new();
    tls.read_to_end(&mut buffer)
        .map_err(|err| io_to_tls_error(&err))?;
    Ok(buffer)
}

fn io_to_tls_error(err: &std::io::Error) -> Error {
    Error::Tls {
        message: format!("{err}"),
    }
}