#![cfg(any(feature = "smol", feature = "tokio", feature = "async-std"))]
use std::{fs, path::PathBuf};
use tempfile::TempDir;
use trillium::Status;
use trillium_static::StaticFileHandler;
use trillium_testing::{TestServer, block_on};
fn setup() -> (TempDir, PathBuf) {
let outer = TempDir::new().unwrap();
fs::write(outer.path().join("secret.txt"), "secret content").unwrap();
let www = outer.path().join("www");
fs::create_dir(&www).unwrap();
fs::write(www.join("public.txt"), "public content").unwrap();
fs::create_dir(www.join("subdir")).unwrap();
fs::write(www.join("subdir/nested.txt"), "nested content").unwrap();
(outer, www)
}
#[test]
fn serves_existing_file() {
let (_outer, www) = setup();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/public.txt")
.await
.assert_ok()
.assert_body("public content");
});
}
#[test]
fn returns_404_for_missing_file() {
let (_outer, www) = setup();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/nonexistent.txt")
.await
.assert_status(Status::NotFound);
});
}
#[test]
fn serves_file_in_subdir() {
let (_outer, www) = setup();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/subdir/nested.txt")
.await
.assert_ok()
.assert_body("nested content");
});
}
#[test]
fn dot_segment_is_resolved() {
let (_outer, www) = setup();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/./public.txt").await.assert_ok();
app.get("/subdir/./nested.txt").await.assert_ok();
});
}
#[test]
fn dotdot_within_root_is_resolved() {
let (_outer, www) = setup();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/subdir/../public.txt")
.await
.assert_ok()
.assert_body("public content");
});
}
#[test]
fn dotdot_from_root_is_blocked() {
let (_outer, www) = setup();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/../secret.txt")
.await
.assert_status(Status::NotFound);
});
}
#[test]
fn multiple_dotdots_are_blocked() {
let (_outer, www) = setup();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/../../secret.txt")
.await
.assert_status(Status::NotFound);
});
}
#[test]
fn dotdot_after_subdir_that_escapes_root_is_blocked() {
let (_outer, www) = setup();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/subdir/../../secret.txt")
.await
.assert_status(Status::NotFound);
});
}
#[test]
fn path_prefix_not_confused_with_path_starts_with() {
let outer = TempDir::new().unwrap();
let www = outer.path().join("www");
let wwwother = outer.path().join("wwwother");
fs::create_dir(&www).unwrap();
fs::create_dir(&wwwother).unwrap();
fs::write(www.join("public.txt"), "public").unwrap();
fs::write(wwwother.join("secret.txt"), "sibling secret").unwrap();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/../wwwother/secret.txt")
.await
.assert_status(Status::NotFound);
});
}
#[test]
#[cfg(unix)]
fn symlink_within_root_is_followed() {
use std::os::unix::fs::symlink;
let (_outer, www) = setup();
symlink(www.join("public.txt"), www.join("link.txt")).unwrap();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/link.txt")
.await
.assert_ok()
.assert_body("public content");
});
}
#[test]
#[cfg(unix)]
fn symlink_escaping_root_is_followed() {
use std::os::unix::fs::symlink;
let (_outer, www) = setup();
symlink(
_outer.path().join("secret.txt"),
www.join("via_symlink.txt"),
)
.unwrap();
block_on(async {
let app = TestServer::new(StaticFileHandler::new(&www)).await;
app.get("/via_symlink.txt")
.await
.assert_ok()
.assert_body("secret content");
});
}