use std::collections::HashMap;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Semaphore;
use tokio::task::JoinHandle;
use super::{CONNECTION_DEADLINE, MAX_CONNECTIONS, ServeConfig, accept_forever};
use crate::serve::auth;
pub(super) struct RunningServer {
addr: SocketAddr,
handle: JoinHandle<()>,
}
impl RunningServer {
pub(super) fn addr(&self) -> SocketAddr {
self.addr
}
}
impl Drop for RunningServer {
fn drop(&mut self) {
self.handle.abort();
}
}
pub(super) fn write_tree(root: &Path, files: &[(&str, &str)]) {
std::fs::create_dir_all(root).unwrap();
for (path, contents) in files {
let full = root.join(path);
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(full, contents).unwrap();
}
}
pub(super) fn config(root: &Path) -> ServeConfig {
ServeConfig {
root: std::fs::canonicalize(root).unwrap(),
bind: SocketAddr::from(([127, 0, 0, 1], 0)),
spa: false,
listing: false,
hidden: false,
auth: None,
follow_symlinks: false,
connection_deadline: CONNECTION_DEADLINE,
}
}
const TEST_USER: &str = "alice:s3cret";
pub(super) fn config_with_auth(root: &Path) -> ServeConfig {
let creds_path = root
.parent()
.expect("root must sit inside the caller's own tempdir")
.join("creds");
std::fs::write(&creds_path, format!("{TEST_USER}\n")).unwrap();
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&creds_path, std::fs::Permissions::from_mode(0o600)).unwrap();
}
let mut built = config(root);
built.auth = Some(auth::load(&creds_path).unwrap());
built
}
pub(super) fn config_with_spa(root: &Path) -> ServeConfig {
let mut built = config(root);
built.spa = true;
built
}
pub(super) fn config_with_listing(root: &Path) -> ServeConfig {
let mut built = config(root);
built.listing = true;
built
}
pub(super) fn config_with_hidden(root: &Path) -> ServeConfig {
let mut built = config(root);
built.hidden = true;
built
}
pub(super) fn config_with_follow_symlinks(root: &Path) -> ServeConfig {
let mut built = config(root);
built.follow_symlinks = true;
built
}
pub(super) async fn serve_on_free_port(config: ServeConfig) -> RunningServer {
let listener = TcpListener::bind(config.bind).await.unwrap();
let addr = listener.local_addr().unwrap();
let semaphore = Arc::new(Semaphore::new(MAX_CONNECTIONS));
let handle = tokio::spawn(accept_forever(listener, Arc::new(config), semaphore));
RunningServer { addr, handle }
}
#[derive(Debug)]
pub(super) struct Response {
pub(super) status: u16,
pub(super) headers: HashMap<String, String>,
pub(super) body: String,
}
fn parse_response(raw: &[u8]) -> Response {
let text = String::from_utf8_lossy(raw);
let mut halves = text.splitn(2, "\r\n\r\n");
let head = halves.next().unwrap_or_default();
let body = halves.next().unwrap_or_default().to_string();
let mut lines = head.split("\r\n");
let status = lines
.next()
.and_then(|line| line.split_whitespace().nth(1))
.and_then(|code| code.parse().ok())
.unwrap_or(0);
let mut headers = HashMap::new();
for line in lines {
if let Some((name, value)) = line.split_once(':') {
headers.insert(name.trim().to_ascii_lowercase(), value.trim().to_string());
}
}
Response {
status,
headers,
body,
}
}
async fn send_request(addr: SocketAddr, request: &str) -> Response {
let mut stream = tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(addr))
.await
.expect("connect must not hang")
.unwrap();
tokio::time::timeout(Duration::from_secs(5), stream.write_all(request.as_bytes()))
.await
.expect("write must not hang")
.unwrap();
let mut buf = Vec::new();
tokio::time::timeout(Duration::from_secs(5), stream.read_to_end(&mut buf))
.await
.expect("read must not hang")
.unwrap();
parse_response(&buf)
}
pub(super) async fn get(addr: SocketAddr, target: &str) -> Response {
send_request(
addr,
&format!("GET {target} HTTP/1.1\r\nHost: localhost\r\n\r\n"),
)
.await
}
pub(super) async fn get_with_accept(addr: SocketAddr, target: &str, accept: &str) -> Response {
send_request(
addr,
&format!("GET {target} HTTP/1.1\r\nHost: localhost\r\nAccept: {accept}\r\n\r\n"),
)
.await
}
pub(super) async fn get_auth(addr: SocketAddr, target: &str) -> Response {
send_request(
addr,
&format!(
"GET {target} HTTP/1.1\r\nHost: localhost\r\nAuthorization: Basic {}\r\n\r\n",
base64(TEST_USER)
),
)
.await
}
pub(super) async fn request_with_method(addr: SocketAddr, method: &str, target: &str) -> Response {
send_request(
addr,
&format!("{method} {target} HTTP/1.1\r\nHost: localhost\r\n\r\n"),
)
.await
}
fn base64(input: &str) -> String {
auth::base64_encode(input.as_bytes())
}