mini-static 0.14.7

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

use hyper::Method;
use mini_static::Server;
use std::fs;
use tempfile::TempDir;

/// Every method other than GET and HEAD must be rejected by the method gate.
const REJECTED_METHODS: [Method; 7] = [
    Method::DELETE,
    Method::POST,
    Method::PUT,
    Method::PATCH,
    Method::OPTIONS,
    Method::TRACE,
    Method::CONNECT,
];

#[tokio::test]
async fn disallowed_methods_return_405_with_an_allow_header_and_nosniff() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("file.txt"), b"secret content").unwrap();
    let server = Server::new(root.path()).unwrap();

    for method in REJECTED_METHODS {
        let response = common::request(&server, &method, "/file.txt").await;

        assert_eq!(response.status().as_u16(), 405, "{method} should be rejected");
        assert_eq!(
            response.headers().get("Allow").map(|v| v.to_str().unwrap()),
            Some("GET, HEAD"),
            "{method}'s 405 must advertise the methods that are allowed"
        );
        assert_eq!(
            response
                .headers()
                .get("X-Content-Type-Options")
                .map(|v| v.to_str().unwrap()),
            Some("nosniff"),
            "{method}'s 405 must still carry the nosniff header"
        );

        // The method gate runs before any file I/O, so a rejected method can never
        // disclose the file's contents — even for a path that exists and is readable.
        let body = common::body_bytes(response).await;
        assert!(
            !body.windows(6).any(|w| w == b"secret"),
            "{method}'s 405 body must not contain the file's content"
        );
    }
}

#[tokio::test]
async fn get_and_head_succeed_for_an_existing_file() {
    let root = TempDir::new().unwrap();
    fs::write(root.path().join("file.txt"), b"content").unwrap();
    let server = Server::new(root.path()).unwrap();

    for method in [Method::GET, Method::HEAD] {
        let response = common::request(&server, &method, "/file.txt").await;
        assert_eq!(response.status().as_u16(), 200, "{method} on an existing file");
    }
}

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

    for method in [Method::GET, Method::HEAD] {
        let response = common::request(&server, &method, "/missing.txt").await;
        assert_eq!(response.status().as_u16(), 404, "{method} on a missing file");
    }
}