mini-static 0.20.0

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
use super::*;

use tokio::io::AsyncWriteExt;

/// Binds an ephemeral listener, connects a client to it, and returns both ends —
/// `(server_side, client_side)` — so a test can drive `read_header_prefix` against a
/// real socket without a full `Server`/`serve_connection` in the loop.
async fn connected_pair() -> (TcpStream, TcpStream) {
    let listener = TcpListener::bind(("127.0.0.1", 0)).await.unwrap();
    let addr = listener.local_addr().unwrap();
    let client = TcpStream::connect(addr).await.unwrap();
    let (server_side, _) = listener.accept().await.unwrap();
    (server_side, client)
}

#[tokio::test]
async fn reads_exactly_up_to_and_including_the_terminating_blank_line() {
    let (mut server_side, mut client) = connected_pair().await;

    client
        .write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
        .await
        .unwrap();

    let prefix = read_header_prefix(&mut server_side)
        .await
        .unwrap_or_else(|_| {
            panic!("expected a complete header block to be read");
        });

    assert_eq!(prefix, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
}

// Disproves a version that only inspects the newest chunk for `\r\n\r\n`: writing the
// blank line in a separate write (and thus, almost always, a separate read) after the
// rest of the headers would make that version wait forever, since the terminator
// never appears within a single chunk. Also pins the tail-only scan in
// `read_header_prefix` — a terminator straddling two reads must still be seen.
#[tokio::test]
async fn assembles_a_header_block_split_across_multiple_writes() {
    let (mut server_side, mut client) = connected_pair().await;

    client
        .write_all(b"GET /page HTTP/1.1\r\nHost: localhost\r")
        .await
        .unwrap();
    client.write_all(b"\n\r\n").await.unwrap();

    let prefix = read_header_prefix(&mut server_side)
        .await
        .unwrap_or_else(|_| {
            panic!("expected a complete header block to be read across multiple writes");
        });

    assert_eq!(prefix, b"GET /page HTTP/1.1\r\nHost: localhost\r\n\r\n");
}

// Bytes past the header block (a pipelined second request, here) must be preserved
// verbatim in the returned prefix — `PrefixedIo` depends on this to replay them to
// hyper untouched.
#[tokio::test]
async fn preserves_bytes_sent_past_the_header_block() {
    let (mut server_side, mut client) = connected_pair().await;

    let first = b"GET /a HTTP/1.1\r\nHost: localhost\r\n\r\n";
    let second = b"GET /b HTTP/1.1\r\nHost: localhost\r\n\r\n";
    let mut sent = Vec::new();
    sent.extend_from_slice(first);
    sent.extend_from_slice(second);
    client.write_all(&sent).await.unwrap();

    let prefix = read_header_prefix(&mut server_side)
        .await
        .unwrap_or_else(|_| {
            panic!("expected a complete header block to be read");
        });

    assert_eq!(
        &prefix, &sent,
        "pipelined bytes past the first header block must survive intact"
    );
}

#[tokio::test]
async fn errors_with_connection_closed_when_client_disconnects_before_headers_complete() {
    let (mut server_side, client) = connected_pair().await;
    drop(client);

    match read_header_prefix(&mut server_side).await {
        Err(HeaderReadError::ConnectionClosed) => {}
        Err(_) => panic!("expected ConnectionClosed, got a different error variant"),
        Ok(_) => {
            panic!("expected an error, got a complete header block from a closed connection")
        }
    }
}

// Disproves an unbounded buffer: without the `MAX_HEADER_BYTES` check, this would
// hang consuming memory forever instead of erroring, since the client never sends the
// terminating blank line.
#[tokio::test]
async fn errors_with_too_large_once_max_header_bytes_is_exceeded_without_a_terminator() {
    let (mut server_side, mut client) = connected_pair().await;

    let garbage = vec![b'a'; MAX_HEADER_BYTES + 1];
    client.write_all(&garbage).await.unwrap();

    match read_header_prefix(&mut server_side).await {
        Err(HeaderReadError::TooLarge) => {}
        Err(_) => panic!("expected TooLarge, got a different error variant"),
        Ok(_) => {
            panic!("expected an error, got a complete header block from unterminated garbage")
        }
    }
}

#[tokio::test]
async fn prefixed_io_replays_the_prefix_before_reading_from_the_live_socket() {
    let (server_side, mut client) = connected_pair().await;
    let mut io = PrefixedIo::new(b"buffered-prefix".to_vec(), server_side);

    client.write_all(b"-live-bytes").await.unwrap();

    let mut collected = Vec::new();
    let mut chunk = [0u8; 8];
    while collected.len() < b"buffered-prefix-live-bytes".len() {
        let n = io.read(&mut chunk).await.unwrap();
        assert!(n > 0, "read returned 0 before all expected bytes arrived");
        collected.extend_from_slice(&chunk[..n]);
    }

    assert_eq!(collected, b"buffered-prefix-live-bytes");
}