use std::fs;
use std::process::{Command, Stdio};
use tempfile::tempdir;
fn guidebook_bin() -> String {
let mut path = std::env::current_exe().unwrap();
path.pop(); path.pop(); path.push("guidebook");
path.to_string_lossy().to_string()
}
fn create_minimal_book(dir: &std::path::Path) {
fs::write(dir.join("book.json"), r#"{"title": "Test"}"#).unwrap();
fs::write(dir.join("SUMMARY.md"), "# Summary\n* [Intro](README.md)\n").unwrap();
fs::write(dir.join("README.md"), "# Test\nHello\n").unwrap();
}
fn find_available_port() -> u16 {
std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port()
}
fn wait_for_server(port: u16) -> bool {
for _ in 0..100 {
if std::net::TcpStream::connect(format!("127.0.0.1:{}", port)).is_ok() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
false
}
#[test]
fn test_serve_blocks_dotdot_traversal() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
fs::create_dir_all(&source).unwrap();
create_minimal_book(&source);
fs::write(temp.path().join("secret.txt"), "LEAKED").unwrap();
let port = find_available_port();
let mut child = Command::new(guidebook_bin())
.arg("serve")
.arg(source.to_str().unwrap())
.arg("-p")
.arg(port.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Failed to start serve");
if !wait_for_server(port) {
child.kill().ok();
panic!("Server did not start on port {}", port);
}
let client = reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let resp = client
.get(format!("http://127.0.0.1:{}/../secret.txt", port))
.send();
if let Ok(resp) = resp {
assert_ne!(resp.status().as_u16(), 200, "../ should not return 200");
assert!(
!resp.text().unwrap_or_default().contains("LEAKED"),
"../ must not leak file"
);
}
let resp = client
.get(format!("http://127.0.0.1:{}/..%2Fsecret.txt", port))
.send();
if let Ok(resp) = resp {
assert_ne!(resp.status().as_u16(), 200, "..%2F should not return 200");
assert!(
!resp.text().unwrap_or_default().contains("LEAKED"),
"..%2F must not leak file"
);
}
child.kill().ok();
child.wait().ok();
}
#[test]
fn test_serve_blocks_html_fallback_traversal() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
fs::create_dir_all(&source).unwrap();
create_minimal_book(&source);
let port = find_available_port();
let mut child = Command::new(guidebook_bin())
.arg("serve")
.arg(source.to_str().unwrap())
.arg("-p")
.arg(port.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Failed to start serve");
if !wait_for_server(port) {
child.kill().ok();
panic!("Server did not start on port {}", port);
}
let client = reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.unwrap();
let resp = client
.get(format!("http://127.0.0.1:{}/../etc/passwd", port))
.send();
if let Ok(resp) = resp {
let status = resp.status().as_u16();
assert!(
status == 403 || status == 404,
"html fallback traversal should be blocked (got {})",
status
);
}
child.kill().ok();
child.wait().ok();
}
#[test]
fn test_serve_normal_access_works() {
let temp = tempdir().unwrap();
let source = temp.path().join("book");
fs::create_dir_all(&source).unwrap();
create_minimal_book(&source);
let port = find_available_port();
let mut child = Command::new(guidebook_bin())
.arg("serve")
.arg(source.to_str().unwrap())
.arg("-p")
.arg(port.to_string())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("Failed to start serve");
if !wait_for_server(port) {
child.kill().ok();
panic!("Server did not start on port {}", port);
}
let client = reqwest::blocking::Client::new();
let resp = client
.get(format!("http://127.0.0.1:{}/", port))
.send()
.unwrap();
assert_eq!(
resp.status().as_u16(),
200,
"Normal access should return 200"
);
let body = resp.text().unwrap();
assert!(body.contains("Test"), "Should contain book content");
let resp = client
.get(format!("http://127.0.0.1:{}/nonexistent", port))
.send()
.unwrap();
assert_eq!(
resp.status().as_u16(),
404,
"Non-existent page should return 404"
);
child.kill().ok();
child.wait().ok();
}