use ullun::prelude::*;
#[test]
#[should_panic(expected = "Invalid cookie name")]
fn test_cookie_injection_name() {
let _cookie = Cookie::new("session;admin=true", "value");
}
#[test]
#[should_panic(expected = "Invalid cookie value")]
fn test_cookie_injection_value() {
let _cookie = Cookie::new("session", "abc;HttpOnly=false");
}
#[test]
#[should_panic(expected = "Invalid cookie name")]
fn test_cookie_control_chars_name() {
let _cookie = Cookie::new("session\r\nSet-Cookie: admin=true", "value");
}
#[test]
#[should_panic(expected = "Invalid cookie value")]
fn test_cookie_control_chars_value() {
let _cookie = Cookie::new("session", "value\nHTTP/1.1 200 OK");
}
#[test]
fn test_cookie_samesite_none_forces_secure() {
let cookie = Cookie::new("session", "value").same_site(ullun::cookies::SameSite::None);
let header = cookie.to_header_value();
assert!(header.contains("Secure"));
assert!(header.contains("SameSite=None"));
}
#[tokio::test]
async fn test_path_traversal_attacks() {
use hyper::Request;
use hyper_util::rt::TokioIo;
use std::fs;
use tokio::net::TcpStream;
fs::create_dir_all("test_static").ok();
fs::write("test_static/public.txt", "public").ok();
fs::create_dir_all("test_static/private").ok();
fs::write("test_static/private/secret.txt", "secret").ok();
let server = tokio::spawn(async {
App::new()
.serve_static("/files/", "test_static")
.run("127.0.0.1:3101")
.await
});
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let stream = TcpStream::connect("127.0.0.1:3101").await.unwrap();
let io = TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
tokio::task::spawn(async move {
let _ = conn.await;
});
let req = Request::builder()
.uri("http://127.0.0.1:3101/files/../Cargo.toml")
.body(String::new())
.unwrap();
let response = sender.send_request(req).await.unwrap();
assert_eq!(response.status(), 404, "Should block ../ traversal");
let req = Request::builder()
.uri("http://127.0.0.1:3101/files/..%2F..%2FCargo.toml")
.body(String::new())
.unwrap();
let response = sender.send_request(req).await.unwrap();
assert_eq!(response.status(), 404, "Should block encoded traversal");
let req = Request::builder()
.uri("http://127.0.0.1:3101/files//etc/passwd")
.body(String::new())
.unwrap();
let response = sender.send_request(req).await.unwrap();
assert_eq!(response.status(), 404, "Should block absolute paths");
server.abort();
fs::remove_dir_all("test_static").ok();
}
#[tokio::test]
async fn test_body_size_limit_enforcement() {
use http_body_util::BodyExt;
use hyper::Request;
use hyper_util::rt::TokioIo;
use tokio::net::TcpStream;
let server = tokio::spawn(async {
App::new()
.max_body_size(Some(100)) .post("/upload", |_req: ullun::request::Request| async {
Ok(Response::text("OK"))
})
.run("127.0.0.1:3102")
.await
});
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let stream = TcpStream::connect("127.0.0.1:3102").await.unwrap();
let io = TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
tokio::task::spawn(async move {
let _ = conn.await;
});
let large_body = "x".repeat(200);
let req = Request::builder()
.method("POST")
.uri("http://127.0.0.1:3102/upload")
.header("content-length", large_body.len().to_string())
.body(large_body)
.unwrap();
let response = sender.send_request(req).await.unwrap();
assert_eq!(response.status(), 413, "Should reject oversized body");
let body_bytes = response.into_body().collect().await.unwrap().to_bytes();
let body_str = String::from_utf8_lossy(&body_bytes);
assert!(
body_str.contains("too large"),
"Error message should explain issue"
);
server.abort();
}
#[tokio::test]
async fn test_header_injection_protection() {
use hyper::Request;
use hyper_util::rt::TokioIo;
use tokio::net::TcpStream;
let server = tokio::spawn(async {
App::new()
.get("/echo", |_req: ullun::request::Request| async {
let cookie = Cookie::new("test", "value");
Ok(Response::text("OK").cookie(cookie))
})
.run("127.0.0.1:3103")
.await
});
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let stream = TcpStream::connect("127.0.0.1:3103").await.unwrap();
let io = TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
tokio::task::spawn(async move {
let _ = conn.await;
});
let req = Request::builder()
.uri("http://127.0.0.1:3103/echo")
.body(String::new())
.unwrap();
let response = sender.send_request(req).await.unwrap();
let set_cookie = response
.headers()
.get("set-cookie")
.unwrap()
.to_str()
.unwrap();
assert!(!set_cookie.contains("\r\n"), "Should not contain CRLF");
assert!(!set_cookie.contains("\n"), "Should not contain LF");
assert_eq!(
set_cookie.matches("Set-Cookie").count(),
0,
"Should not duplicate header"
);
server.abort();
}
#[test]
fn test_query_parameter_safety() {
let query = Query::parse_query_string("<script>alert('xss')</script>=value&key=<img src=x>");
assert!(query.get("<script>alert('xss')</script>").is_some());
assert!(query.get("key").is_some());
}
#[test]
fn test_error_message_safety() {
let err =
Error::internal("Database connection failed: postgres://admin:secret123@localhost/db");
assert_eq!(err.status_code(), 500);
}
#[test]
fn test_cookie_parsing_malformed() {
let _cookies = Cookies::parse(";;;===");
let cookies = Cookies::parse("a=b; =noname; novalue=");
assert!(cookies.get("a").is_some());
assert_eq!(cookies.get("a"), Some("b"));
assert!(cookies.get("novalue").is_some());
}
#[tokio::test]
async fn test_concurrent_request_safety() {
use hyper::Request;
use hyper_util::rt::TokioIo;
use tokio::net::TcpStream;
let server = tokio::spawn(async {
App::new()
.get("/slow", |_req: ullun::request::Request| async {
tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
Ok(Response::text("OK"))
})
.run("127.0.0.1:3104")
.await
});
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
let mut handles = vec![];
for _ in 0..10 {
let handle = tokio::spawn(async {
let stream = TcpStream::connect("127.0.0.1:3104").await.unwrap();
let io = TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();
tokio::task::spawn(async move {
let _ = conn.await;
});
let req = Request::builder()
.uri("http://127.0.0.1:3104/slow")
.body(String::new())
.unwrap();
sender.send_request(req).await.unwrap()
});
handles.push(handle);
}
for handle in handles {
let response = handle.await.unwrap();
assert_eq!(response.status(), 200);
}
server.abort();
}