mini-static 0.38.7

A secure, async static file server with streaming, traversal protection, and connection limits.
Documentation
//! `Server::respond` — the file-serving engine with no server around it.
//!
//! The entry point a router calls once it has already split and decoded the path. The
//! division of responsibility is the whole design: **the router decides what the segments
//! are; this crate decides whether they are safe to open with.**
//!
//! That second half is not a formality. A router that follows RFC 3986 §3.3 splits before
//! decoding, so `%2F` stays inside a segment as a literal `/` — correct for routing, and
//! an escape if handed to `Path::push` unchecked. A router that has done everything right
//! can still pass a segment this crate must refuse.

mod common;

use std::fs;

use hyper::{Method, Request};
use mini_static::Server;
use tempfile::TempDir;

fn root() -> TempDir {
	let root = TempDir::new().unwrap();
	fs::create_dir(root.path().join("admin")).unwrap();
	fs::write(root.path().join("admin/config"), b"SECRET").unwrap();
	fs::write(root.path().join("page.html"), b"<html>page</html>").unwrap();
	root
}

fn request(path: &str) -> Request<()> {
	Request::builder().method(Method::GET).uri(path).body(()).unwrap()
}

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

	let segments = vec!["admin".to_string(), "config".to_string()];
	let response = server.respond(&request("/admin/config"), &segments).await;

	assert_eq!(response.status().as_u16(), 200);
	assert_eq!(&common::body_bytes(response).await[..], b"SECRET");
}

/// **The composed-path escape.** This is exactly what `mini-serve` hands over for
/// `/admin%2Fconfig`: one segment whose decoded form contains a separator. Passing it to
/// `Path::push` unchecked reopens the hole that `0.32.0` closed — from the other side.
#[tokio::test]
async fn a_segment_containing_a_separator_is_refused() {
	let root = root();
	let server = Server::new(root.path()).unwrap();

	let segments = vec!["admin/config".to_string()];
	let response = server.respond(&request("/admin%2Fconfig"), &segments).await;

	assert_eq!(
		response.status().as_u16(),
		404,
		"a segment holding a literal separator reached a nested file"
	);
}

/// Hidden files are *served* here, and that is load-bearing. Under the default policy
/// `..` is refused by the dotfile check — it starts with a dot — so a `..` test written
/// against a default server passes with the traversal guard deleted. That has now
/// produced a vacuous test twice in this crate; see `an_encoded_dot_dot_is_still_refused`
/// for the first.
#[tokio::test]
async fn a_dot_dot_segment_is_refused() {
	let root = root();
	let server = Server::new(root.path()).unwrap().with_hidden_files();

	let segments = vec!["admin".to_string(), "..".to_string(), "page.html".to_string()];
	let response = server.respond(&request("/admin/../page.html"), &segments).await;

	assert_eq!(response.status().as_u16(), 404, "a `..` segment was followed");
}

#[tokio::test]
async fn a_segment_containing_a_backslash_is_refused() {
	let root = root();
	fs::write(root.path().join(r"admin\config"), b"BACKSLASH-NAMED").unwrap();
	let server = Server::new(root.path()).unwrap();

	let segments = vec![r"admin\config".to_string()];
	let response = server.respond(&request("/admin%5Cconfig"), &segments).await;

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

/// The two entry points must not drift: given a path this crate would decode to the same
/// segments a router would, they answer identically.
#[tokio::test]
async fn respond_and_handle_request_agree() {
	let root = root();
	let server = Server::new(root.path()).unwrap();

	let via_handle = common::get(&server, "/page.html").await;
	let segments = vec!["page.html".to_string()];
	let via_respond = server.respond(&request("/page.html"), &segments).await;

	assert_eq!(via_handle.status(), via_respond.status());
	assert_eq!(
		common::body_bytes(via_handle).await,
		common::body_bytes(via_respond).await
	);
}

/// A method the engine does not serve is still refused when a router calls it directly —
/// `respond` is not a way around the checks `handle_request` makes.
#[tokio::test]
async fn respond_refuses_a_method_it_does_not_serve() {
	let root = root();
	let server = Server::new(root.path()).unwrap();

	let req = Request::builder()
		.method(Method::DELETE)
		.uri("/page.html")
		.body(())
		.unwrap();
	let response = server.respond(&req, &["page.html".to_string()]).await;

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