//! Serve fresh world files and stream the /api mount through standard WASI HTTP. //! Stdin EOF stops the server, including waits for connections and HTTP. use clap::Parser; use std::{io, net::SocketAddr}; use wasi::{ http::{ outgoing_handler, types::{Fields, Method, OutgoingBody, OutgoingRequest, Scheme}, }, io::{ poll::{Pollable, poll}, streams::{InputStream, OutputStream, StreamError}, }, sockets::{ instance_network::instance_network, network::*, tcp_create_socket::create_tcp_socket, }, }; #[derive(Parser)] struct Options { #[arg(long, default_value = "0.0.0.0:8080")] listen: SocketAddr, #[arg(long, default_value = "/srv/www")] root: std::path::PathBuf, } fn error(e: impl std::fmt::Debug) -> io::Error { io::Error::other(format!("{e:?}")) } fn wait(stdin: &InputStream, event: &Pollable) -> io::Result<()> { let stop = stdin.subscribe(); loop { let ready = poll(&[&stop, event]); match stdin.read(65536) { Err(StreamError::Closed) => std::process::exit(0), Err(e) => return Err(error(e)), Ok(_) => {} } if ready.contains(&1) { return Ok(()); } } } fn write(stdin: &InputStream, stream: &OutputStream, mut bytes: &[u8]) -> io::Result<()> { while !bytes.is_empty() { wait(stdin, &stream.subscribe())?; let count = (stream.check_write().map_err(error)? as usize).min(bytes.len()); stream.write(&bytes[..count]).map_err(error)?; bytes = &bytes[count..]; } stream.flush().map_err(error)?; loop { wait(stdin, &stream.subscribe())?; if stream.check_write().map_err(error)? > 0 { return Ok(()); } } } fn wait_response(stdin: &InputStream, event: &Pollable, client: &InputStream) -> io::Result<()> { let stop = stdin.subscribe(); let disconnect = client.subscribe(); loop { let ready = poll(&[&stop, event, &disconnect]); match stdin.read(65536) { Err(StreamError::Closed) => std::process::exit(0), Err(e) => return Err(error(e)), Ok(_) => {} } if ready.contains(&2) { client.read(65536).map_err(error)?; } if ready.contains(&1) { return Ok(()); } } } fn proxy( stdin: &InputStream, client: &InputStream, method: &str, path: &str, headers: (Option<&str>, Option<&str>, Option<&str>), body: &[u8], output: &OutputStream, ) -> io::Result<()> { let (content_type, origin, host) = headers; let headers = Fields::new(); headers .append("content-length", &body.len().to_string().into_bytes()) .map_err(error)?; if let Some(value) = content_type { headers .append("content-type", value.as_bytes()) .map_err(error)?; } for (key, value) in [("origin", origin), ("x-forwarded-host", host)] { if let Some(value) = value { headers.append(key, value.as_bytes()).map_err(error)?; } } let request = OutgoingRequest::new(headers); let method = match method { "GET" => Method::Get, "POST" => Method::Post, "PUT" => Method::Put, "DELETE" => Method::Delete, "PATCH" => Method::Patch, "HEAD" => Method::Head, "OPTIONS" => Method::Options, other => Method::Other(other.into()), }; request.set_method(&method).map_err(error)?; request.set_scheme(Some(&Scheme::Http)).map_err(error)?; request.set_authority(Some("host.sausage")).map_err(error)?; request .set_path_with_query(Some(&format!("/{path}"))) .map_err(error)?; let outgoing = request.body().map_err(error)?; let future = outgoing_handler::handle(request, None).map_err(error)?; if !body.is_empty() { let stream = outgoing.write().map_err(error)?; write(stdin, &stream, body)?; } OutgoingBody::finish(outgoing, None).map_err(error)?; wait_response(stdin, &future.subscribe(), client)?; let response = future .get() .ok_or_else(|| error("missing HTTP response"))? .map_err(error)? .map_err(error)?; let status = response.status(); let content_type = response .headers() .get("content-type") .first() .map(|s| String::from_utf8_lossy(s).into_owned()) .unwrap_or_else(|| "application/octet-stream".into()); let incoming = response.consume().map_err(error)?; let stream = incoming.stream().map_err(error)?; write(stdin, output, format!("HTTP/1.1 {status} Response\r\nContent-Type: {content_type}\r\nConnection: close\r\n\r\n").as_bytes())?; loop { wait_response(stdin, &stream.subscribe(), client)?; match stream.read(65536) { Ok(chunk) => write(stdin, output, &chunk)?, Err(StreamError::Closed) => break, Err(e) => return Err(error(e)), } } Ok(()) } fn serve( stdin: &InputStream, input: &InputStream, output: &OutputStream, root: &std::path::Path, ) -> io::Result<()> { let mut bytes = Vec::new(); let end = loop { if let Some(index) = bytes.windows(4).position(|s| s == b"\r\n\r\n") { break index + 4; } wait(stdin, &input.subscribe())?; bytes.extend(input.read(65536).map_err(error)?); }; let headers = std::str::from_utf8(&bytes[..end]) .map_err(error)? .to_owned(); let mut lines = headers.lines(); let mut line = lines.next().unwrap_or("").split_whitespace(); let method = line.next().unwrap_or(""); let path = line.next().unwrap_or(""); let headers: Vec<_> = lines .filter_map(|s| s.split_once(':')) .map(|(k, v)| (k.to_ascii_lowercase(), v.trim())) .collect(); let header = |key: &str| headers.iter().find(|(k, _)| k == key).map(|(_, v)| *v); let length = header("content-length") .unwrap_or("0") .parse::() .map_err(error)?; while bytes.len() < end + length { wait(stdin, &input.subscribe())?; bytes.extend(input.read(65536).map_err(error)?); } if let Some(path) = path.strip_prefix("/api/") { return proxy( stdin, input, method, path, (header("content-type"), header("origin"), header("host")), &bytes[end..end + length], output, ); } let mut file = root.to_path_buf(); let path = path.split('?').next().unwrap_or(""); for part in std::path::Path::new(path).components() { match part { std::path::Component::Normal(name) => file.push(name), std::path::Component::RootDir | std::path::Component::CurDir => {} _ => return not_found(stdin, output), } } if file.is_dir() { file.push("index.html"); } let body = match std::fs::read(&file) { Ok(bytes) => bytes, Err(_) => return not_found(stdin, output), }; let kind = match file.extension().and_then(|s| s.to_str()).unwrap_or("") { "html" => "text/html; charset=utf-8", "css" => "text/css; charset=utf-8", "js" | "mjs" => "text/javascript; charset=utf-8", "json" => "application/json", "svg" => "image/svg+xml", "png" => "image/png", "jpg" | "jpeg" => "image/jpeg", "ico" => "image/x-icon", "txt" | "md" => "text/plain; charset=utf-8", "wasm" => "application/wasm", _ => "application/octet-stream", }; let status = 200; if method == "HEAD" { return write(stdin, output, format!("HTTP/1.1 200 OK\r\nContent-Type: {kind}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", body.len()).as_bytes()); } write(stdin, output,format!("HTTP/1.1 {status} Response\r\nContent-Type: {kind}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",body.len()).as_bytes())?; write(stdin, output, &body) } fn not_found(stdin: &InputStream, output: &OutputStream) -> io::Result<()> { write(stdin, output, b"HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: close\r\n\r\nNot found") } fn main() -> io::Result<()> { let options = Options::parse(); let address = options.listen; let stdin = wasi::cli::stdin::get_stdin(); let (family, address) = match address { SocketAddr::V4(a) => ( IpAddressFamily::Ipv4, IpSocketAddress::Ipv4(Ipv4SocketAddress { port: a.port(), address: a.ip().octets().into(), }), ), SocketAddr::V6(a) => ( IpAddressFamily::Ipv6, IpSocketAddress::Ipv6(Ipv6SocketAddress { port: a.port(), flow_info: a.flowinfo(), address: a.ip().segments().into(), scope_id: a.scope_id(), }), ), }; let socket = create_tcp_socket(family).map_err(error)?; socket .start_bind(&instance_network(), address) .map_err(error)?; wait(&stdin, &socket.subscribe())?; socket.finish_bind().map_err(error)?; socket.start_listen().map_err(error)?; wait(&stdin, &socket.subscribe())?; socket.finish_listen().map_err(error)?; loop { match socket.accept() { Ok((_connection, input, output)) => { let _ = serve(&stdin, &input, &output, &options.root); } Err(ErrorCode::WouldBlock) => wait(&stdin, &socket.subscribe())?, Err(e) => return Err(error(e)), } } }