Files
sausage/userland/web/src/main.rs
T

274 lines
9.5 KiB
Rust

//! 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::<usize>()
.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)),
}
}
}