rsurl 0.1.16

A pure-Rust implementation of curl. Library, C FFI, and CLI for HTTP/HTTPS/FTP/FTPS.
Documentation
//! A persistent async TLS stream: plaintext [`AsyncConn`] over an encrypted one.
//!
//! The [`asyncio`](super::asyncio) driver runs a [`Machine`](super::Machine) to
//! *completion* (one request/response), which is the right shape for HTTP but
//! not for a long-lived duplex like a WebSocket. This wraps the same sans-IO TLS
//! engine ([`TlsEngine`]) the [`TlsClient`](crate::proto::tls::TlsClient) machine
//! uses, but exposes it as a *persistent* [`AsyncConn`]: run the handshake once,
//! then read/write application plaintext for as long as the connection lives.
//!
//! It is itself an [`AsyncConn`], so anything that speaks that trait (the async
//! WebSocket in `crate::aio`) can run over `wss://` exactly as it runs over a
//! plain socket.

use std::io;

use crate::error::Result;
use crate::io::runtime::AsyncConn;
use crate::proto::tls::TlsEngine;
use crate::tls::{ClientEngine, TlsSettings};

/// Map a crate error surfaced by the TLS engine into an [`io::Error`], so the
/// [`AsyncConn`] surface stays `io`-typed like a real socket.
fn to_io(e: crate::error::Error) -> io::Error {
    io::Error::other(e.to_string())
}

/// Run a client TLS handshake with server `host` over `conn`, configured from
/// `tls` (roots, `-k`, client certificate, ciphers, version bounds, ...), and
/// return the established engine.
///
/// The post-handshake checks the engine leaves to its driver (public-key
/// pinning, the caller verify callback, the SAN-required hostname check; see
/// [`crate::tls::verify_peer_chain`]) run here, *before* the caller sends any
/// application data, so a pin mismatch never leaks a request to the wrong
/// server. Every byte read from `conn` is handed to the engine, so nothing is
/// left buffered outside it: a caller may keep driving the returned engine
/// over the same `conn` (e.g. through [`TlsClient`](crate::proto::tls::TlsClient)).
pub(crate) async fn handshake<C: AsyncConn>(
    conn: &mut C,
    host: &str,
    tls: &TlsSettings,
) -> Result<ClientEngine> {
    // Files (CA bundle, client cert/key, CRL) are read and pins parsed here,
    // so a bad setting fails before the handshake starts.
    let mut opts = tls.to_opts(&[])?;
    let pins = opts.pinned_spki_sha256.clone();
    let mut engine = crate::tls::build_client_engine(host, &mut opts)?;
    let mut inbuf = vec![0u8; 16 * 1024];

    // Standard sans-IO handshake pump: flush whatever the engine wants to
    // send, stop once it is no longer handshaking, otherwise feed it the
    // next inbound flight.
    loop {
        let mut out = Vec::new();
        engine.drain_outgoing(&mut out);
        if !out.is_empty() {
            conn.write_all(&out).await?;
            conn.flush().await?;
        }
        if !engine.is_handshaking() {
            break;
        }
        let n = conn.read(&mut inbuf).await?;
        if n == 0 {
            return Err(crate::error::Error::UnexpectedEof);
        }
        engine.feed_incoming(&inbuf[..n])?;
    }

    tls.verify_peer(&engine.tls_params().peer_certificates, host, &pins)?;
    Ok(engine)
}

/// An async, plaintext byte stream layered on the encrypted `conn`.
///
/// Reads are cancel-safe as long as `conn.read` is: ciphertext is only fed to
/// the engine after a read completes. Writes are not: once plaintext has been
/// sealed into records, a `conn.write_all` that fails or is dropped part-way
/// leaves an unknown prefix of those records on the wire, so the stream is
/// marked poisoned and every later operation fails instead of desynchronising
/// the TLS record layer.
pub(crate) struct AsyncTlsStream<C> {
    conn: C,
    engine: ClientEngine,
    /// Scratch for inbound ciphertext reads.
    inbuf: Vec<u8>,
    /// Set while ciphertext is being written, and left set if that write fails
    /// or its future is dropped.
    poisoned: bool,
}

impl<C: AsyncConn> AsyncTlsStream<C> {
    /// Wrap `conn`, driving the TLS handshake for server name `host` to
    /// completion (and the post-handshake trust checks, see [`handshake`])
    /// before returning. `tls` carries the verification / client-auth settings.
    pub(crate) async fn connect(
        mut conn: C,
        host: &str,
        tls: &TlsSettings,
    ) -> Result<AsyncTlsStream<C>> {
        let engine = handshake(&mut conn, host, tls).await?;
        Ok(AsyncTlsStream {
            conn,
            engine,
            inbuf: vec![0u8; 16 * 1024],
            poisoned: false,
        })
    }

    fn check_not_poisoned(&self) -> io::Result<()> {
        if self.poisoned {
            return Err(io::Error::new(
                io::ErrorKind::BrokenPipe,
                "tls: stream unusable after an interrupted or failed write",
            ));
        }
        Ok(())
    }

    /// Write out every record the engine has queued (and flush when asked),
    /// poisoning the stream for the duration — see the type docs.
    async fn send_queued(&mut self, flush: bool) -> io::Result<()> {
        let mut out = Vec::new();
        self.engine.drain_outgoing(&mut out);
        if out.is_empty() && !flush {
            return Ok(());
        }
        self.poisoned = true;
        if !out.is_empty() {
            self.conn.write_all(&out).await?;
        }
        if flush {
            self.conn.flush().await?;
        }
        self.poisoned = false;
        Ok(())
    }
}

impl<C: AsyncConn> AsyncConn for AsyncTlsStream<C> {
    async fn read(&mut self, dst: &mut [u8]) -> io::Result<usize> {
        self.check_not_poisoned()?;
        loop {
            // Drain already-decrypted plaintext first.
            let n = self.engine.read_plaintext(dst).map_err(to_io)?;
            if n > 0 {
                return Ok(n);
            }
            // None buffered — pull more ciphertext and decrypt it.
            let m = self.conn.read(&mut self.inbuf).await?;
            if m == 0 {
                return Ok(0); // peer closed the underlying connection
            }
            let chunk = self.inbuf[..m].to_vec();
            self.engine.feed_incoming(&chunk).map_err(to_io)?;
            // Feeding may have produced records to send (e.g. a TLS 1.3
            // post-handshake message or key update) — flush them.
            self.send_queued(true).await?;
        }
    }

    async fn write_all(&mut self, src: &[u8]) -> io::Result<()> {
        self.check_not_poisoned()?;
        self.engine.write_plaintext(src);
        self.send_queued(false).await
    }

    async fn flush(&mut self) -> io::Result<()> {
        self.check_not_poisoned()?;
        self.send_queued(true).await
    }
}