mini-static 0.29.0

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
mod common;

use std::fs;

use mini_static::Server;
use tempfile::TempDir;

/// A root holding the two files this feature exists to stop leaking, a legitimate
/// `.well-known` resource, and an ordinary file.
fn root_with_dotfiles() -> TempDir {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join(".env"), b"API_KEY=super-secret").unwrap();
    fs::create_dir(root.path().join(".git")).unwrap();
    fs::write(root.path().join(".git/config"), b"[remote origin]").unwrap();
    fs::create_dir(root.path().join(".well-known")).unwrap();
    fs::write(root.path().join(".well-known/security.txt"), b"Contact: x").unwrap();
    fs::write(root.path().join(".well-known/.hidden"), b"still secret").unwrap();
    fs::write(root.path().join("real.html"), b"<html>Real</html>").unwrap();
    root
}

#[tokio::test]
async fn dotfiles_are_denied_by_default() {
    let root = root_with_dotfiles();
    let server = Server::new(root.path()).unwrap();

    for path in ["/.env", "/.git/config", "/.git/"] {
        let response = common::get(&server, path).await;
        assert_eq!(response.status().as_u16(), 404, "{path} should not serve");

        let body = common::body_bytes(response).await;
        assert!(
            !String::from_utf8_lossy(&body).contains("super-secret"),
            "{path} leaked file contents"
        );
    }
}

/// A hidden file that exists and one that does not must be indistinguishable, or the
/// response becomes an oracle for what the root contains — the same property the
/// traversal collapse maintains.
#[tokio::test]
async fn an_existing_dotfile_is_indistinguishable_from_a_missing_one() {
    let root = root_with_dotfiles();
    let server = Server::new(root.path()).unwrap();

    let present = common::get(&server, "/.env").await;
    let absent = common::get(&server, "/.nonexistent-file").await;

    assert_eq!(present.status(), absent.status());
    assert_eq!(
        common::body_bytes(present).await,
        common::body_bytes(absent).await
    );
}

/// Denying `/.well-known/` would break ACME certificate renewal on every site this
/// crate serves.
#[tokio::test]
async fn well_known_is_served_despite_its_leading_dot() {
    let root = root_with_dotfiles();
    let server = Server::new(root.path()).unwrap();

    let response = common::get(&server, "/.well-known/security.txt").await;

    assert_eq!(response.status().as_u16(), 200);
    assert_eq!(
        common::body_bytes(response).await.as_ref(),
        b"Contact: x",
        "the .well-known exception must serve real content"
    );
}

/// The exception is the first segment only — it is not a general amnesty for dot names
/// that happen to live under `.well-known/`.
#[tokio::test]
async fn a_dotfile_nested_under_well_known_is_still_denied() {
    let root = root_with_dotfiles();
    let server = Server::new(root.path()).unwrap();

    let response = common::get(&server, "/.well-known/.hidden").await;

    assert_eq!(response.status().as_u16(), 404);
}

/// `.` is a same-directory reference, not a hidden name; treating it as one would 404
/// perfectly ordinary requests.
#[tokio::test]
async fn a_same_directory_reference_is_not_a_hidden_segment() {
    let root = root_with_dotfiles();
    let server = Server::new(root.path()).unwrap();

    let response = common::get(&server, "/./real.html").await;

    assert_eq!(response.status().as_u16(), 200);
    assert_eq!(
        common::body_bytes(response).await.as_ref(),
        b"<html>Real</html>"
    );
}

#[tokio::test]
async fn ordinary_files_are_unaffected() {
    let root = root_with_dotfiles();
    let server = Server::new(root.path()).unwrap();

    let response = common::get(&server, "/real.html").await;

    assert_eq!(response.status().as_u16(), 200);
}

/// Percent-encoding the dot must not sneak a hidden path past the check: the segment
/// scan runs on the decoded path, after `%2E` has become `.`.
#[tokio::test]
async fn a_percent_encoded_dot_does_not_bypass_the_check() {
    let root = root_with_dotfiles();
    let server = Server::new(root.path()).unwrap();

    for path in ["/%2Eenv", "/%2e%67it/config"] {
        assert_eq!(
            common::get(&server, path).await.status().as_u16(),
            404,
            "{path} bypassed the hidden-file check"
        );
    }
}

#[tokio::test]
async fn with_hidden_files_opts_back_in() {
    let root = root_with_dotfiles();
    let server = Server::new(root.path()).unwrap().with_hidden_files();

    let response = common::get(&server, "/.env").await;

    assert_eq!(response.status().as_u16(), 200);
    assert_eq!(
        common::body_bytes(response).await.as_ref(),
        b"API_KEY=super-secret",
        "with_hidden_files must restore the old behavior in full"
    );
}