diff --git a/Cargo.toml b/Cargo.toml index cbcf463..6768516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,8 @@ rust-version = "1.85" tokio-rustls = { version = "0.26.6", default-features = false, features = ["ring", "tls12"], optional = true } openssl = { version = "0.10.30", optional = true } tokio-openssl = { version = "0.6.5", optional = true } -tokio = { version = "1.54.1", features = ["net", "io-util", "rt", "fs", "sync", "time"] } -socket2 = "0.6.5" +tokio = { version = "1.54.1", features = ["net", "io-util", "rt", "fs", "sync", "time"], optional = true } +socket2 = { version = "0.6.5", optional = true } serde = { version = "1.0.123", optional = true } serde_json = { version = "1.0.59", optional = true } @@ -25,21 +25,30 @@ version = "0.1.2" [dependencies.octane_http] path = "octane_http" version = "0.1" +default-features = false [features] -default = ["faithful", "query_strings", "cookies", "url_variables", "raw_headers"] -faithful = [] -test_crate = [] -raw_headers = [] -query_strings = [] -extended_queries = [] -cookies = [] -json = ["serde", "serde_json"] +default = ["std", "faithful", "query_strings", "cookies", "url_variables", "raw_headers"] +# The tokio server and everything built on it. Without it the crate is +# `#![no_std]` and only exposes the allocation-free core, `octane::portable`. +std = ["tokio", "socket2", "octane_http/std"] +faithful = ["std"] +test_crate = ["std"] +raw_headers = ["std"] +query_strings = ["std"] +extended_queries = ["std"] +cookies = ["std"] +json = ["std", "serde", "serde_json"] +# url variables like `/users/:id`, for both the tokio and the WASIp3 router url_variables = [] -rustls = ["tokio-rustls"] -openSSL = ["openssl", "tokio-openssl"] +rustls = ["std", "tokio-rustls"] +openSSL = ["std", "openssl", "tokio-openssl"] +# `octane::wasip3`: serve from a WASIp3 component over wasi:sockets. Only has +# an effect on wasm32-wasip3, and doesn't need `std` (use it with +# `default-features = false`) +wasm = ["octane_http/wasm"] # exposes the parsers to the fuzz targets in octane_http/fuzz, not a stable API -fuzzing = [] +fuzzing = ["std"] [dev-dependencies] tokio = { version = "1.54.1", features = ["macros", "rt-multi-thread"] } diff --git a/guests/README.md b/guests/README.md new file mode 100644 index 0000000..4ab22f5 --- /dev/null +++ b/guests/README.md @@ -0,0 +1,139 @@ +# WASI guests + +Programs that run octane as WebAssembly components inside wasmtime, using +`octane` with `default-features = false` (no tokio) and the `wasm` feature, +which adds `octane::wasip3` on `wasm32-wasip3`. + +| Guest | What it is | +| --- | --- | +| [`wasip3-server`](wasip3-server) | HTTP/1.1 server as a WASIp3 component: `octane::wasip3::serve` with an async handler | + +Each guest is its own Cargo workspace, so its target and nightly settings +don't affect the main octane build. + +## Setup + +You need three things: Rust nightly with `rust-src`, wasi-sdk 34 or newer +and wasmtime with WASIp3 support. The steps below work on macOS and Linux +(arm64 and x86_64). + +### 1. Rust nightly + +`wasm32-wasip3` is a tier 3 target with no prebuilt standard library, so it +is built from source with `-Zbuild-std`. That needs nightly and `rust-src`: + +```sh +rustup toolchain install nightly --component rust-src +``` + +Each guest has a `rust-toolchain.toml` that selects nightly, so nothing has +to be set as the default. + +### 2. wasi-sdk + +wasi-sdk provides the WASIp3 libc, `crt1-reactor.o`, `wasm-ld` and +`wasm-component-ld`. The `wasm-component-ld` that ships with rustc is too +old for WASIp3, so the guests use wasi-sdk's. + +Download the latest release from + into `~/wasi-sdk`: + +```sh +# latest release number, e.g. 34 +WASI_SDK_VERSION=$(curl -fsSL https://api.github.com/repos/WebAssembly/wasi-sdk/releases/latest \ + | sed -n 's/.*"tag_name": *"wasi-sdk-\([0-9.]*\)".*/\1/p') + +case "$(uname -m)" in arm64|aarch64) ARCH=arm64;; x86_64|amd64) ARCH=x86_64;; esac +case "$(uname -s)" in Darwin) OS=macos;; Linux) OS=linux;; esac +NAME=wasi-sdk-$WASI_SDK_VERSION.0-$ARCH-$OS + +mkdir -p ~/wasi-sdk +curl -fL "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$WASI_SDK_VERSION/$NAME.tar.gz" \ + | tar -xz -C ~/wasi-sdk +echo "installed ~/wasi-sdk/$NAME" +``` + +Then point `WASI_SDK_PATH` at it. Add this to `~/.zshrc` (or `~/.bashrc`) +so new shells have it: + +```sh +export WASI_SDK_PATH=~/wasi-sdk/wasi-sdk-34.0-arm64-macos # the folder printed above +``` + +Check it works: + +```sh +"$WASI_SDK_PATH/bin/wasm-ld" --version # LLD 23 or newer +ls "$WASI_SDK_PATH/share/wasi-sysroot/lib/wasm32-wasip3" # the WASIp3 libc +``` + +**macOS:** a wasi-sdk downloaded with a browser is quarantined by +Gatekeeper. Its tools then fail, often silently or with an +"Apple could not verify" dialog. `curl` doesn't quarantine files, so the +steps above aren't affected. If you downloaded it some other way, clear the +quarantine once: + +```sh +xattr -dr com.apple.quarantine "$WASI_SDK_PATH" +find "$WASI_SDK_PATH" -type l -exec xattr -s -d com.apple.quarantine {} \; +``` + +### 3. wasmtime + +Install it with the official script, or update an existing install to the +latest version: + +```sh +curl https://wasmtime.dev/install.sh -sSf | bash +wasmtime --version +``` + +WASIp3 needs a release that has the `-S p3` option (tested with 47.0.3): + +```sh +wasmtime run -S help | grep p3 +``` + +## Build and run + +```sh +cd guests/wasip3-server +cargo build --release +wasmtime run -S p3 -S inherit-network -W component-model-async \ + target/wasm32-wasip3/release/wasip3_server.wasm +``` + +The flags: + +- `-S p3` enables the WASIp3 interfaces +- `-S inherit-network` lets the guest bind and accept on the host's network +- `-W component-model-async` enables async lifting, `stream` and + `future` + +The first build takes a minute while the standard library is compiled. +Later builds only rebuild the guest. + +In another terminal: + +```sh +curl localhost:8080/ +curl -X POST -d 'hello' localhost:8080/echo +curl -N 'localhost:8080/stream?chunks=5&ms=500' # body arrives in chunks + +# 10 requests that each wait 1 s finish in about 1 s, not 10 +time (for i in $(seq 10); do curl -s 'localhost:8080/sleep?ms=1000' & done; wait) +``` + +See the [wasip3-server README](wasip3-server/README.md) for its routes and +how it is put together. + +## Troubleshooting + +| Error | Cause | +| --- | --- | +| `set WASI_SDK_PATH to the wasi-sdk install` | `WASI_SDK_PATH` isn't exported in this shell | +| `.../lib/wasm32-wasip3 is missing` | wasi-sdk older than 34, it has no WASIp3 libc | +| `can't find crate for core` | `rust-src` isn't installed for nightly, see step 1 | +| `undefined symbol: __wasm_get_stack_pointer` | `crt1-reactor.o` isn't linked, check `build.rs` ran with `WASI_SDK_PATH` set | +| `failed to resolve import env::__wasm_set_tls_base` | rustc's bundled `wasm-component-ld` ran instead of wasi-sdk's, check `.cargo/config.toml` uses `tools/wasm-component-ld` | +| wasmtime: `unknown option p3` | wasmtime too old, update it (step 3) | diff --git a/guests/wasip3-server/.cargo/config.toml b/guests/wasip3-server/.cargo/config.toml new file mode 100644 index 0000000..1ecbb1e --- /dev/null +++ b/guests/wasip3-server/.cargo/config.toml @@ -0,0 +1,15 @@ +[build] +target = "wasm32-wasip3" + +[unstable] +# tier 3 target, no prebuilt std +build-std = ["std", "panic_abort"] + +[target.wasm32-wasip3] +# runs wasi-sdk's wasm-component-ld and wasm-ld, see the script +linker = "tools/wasm-component-ld" +rustflags = [ + # libc and crt1-reactor.o come from wasi-sdk (see build.rs), rustc has + # no self-contained copies for this target + "-C", "link-self-contained=no", +] diff --git a/guests/wasip3-server/.gitignore b/guests/wasip3-server/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/guests/wasip3-server/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/guests/wasip3-server/Cargo.toml b/guests/wasip3-server/Cargo.toml new file mode 100644 index 0000000..771b668 --- /dev/null +++ b/guests/wasip3-server/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "wasip3-server" +version = "0.0.0" +edition = "2021" +publish = false + +# A WASIp3 guest serving HTTP/1.1 over wasi:sockets with octane's +# allocation-free core. See README.md for building and running it. + +# the `wasi:cli/run` export comes from `export!` below, not a `main` +[lib] +crate-type = ["cdylib"] + +[dependencies] +# without tokio: `octane::wasip3` serves over wasi:sockets with octane's +# router, `url_variables` enables routes like `/hello/:name` +octane = { path = "../..", default-features = false, features = ["wasm", "url_variables"] } +# the `wasi:cli/run` export and the clock used by `/sleep` and `/stream` +wasip3 = { version = "0.9.0", default-features = false } + +[profile.dev] +panic = "abort" + +[profile.release] +panic = "abort" + +# not part of the octane workspace, it builds for its own target +[workspace] diff --git a/guests/wasip3-server/README.md b/guests/wasip3-server/README.md new file mode 100644 index 0000000..bb80681 --- /dev/null +++ b/guests/wasip3-server/README.md @@ -0,0 +1,64 @@ +# wasip3-server + +An HTTP/1.1 server built as a WASIp3 component with octane's `wasm` +feature: + +```toml +octane = { path = "../..", default-features = false, features = ["wasm", "url_variables"] } +``` + +Routes are added to an `octane::wasip3::Router` through octane's `Route` +trait (`get`, `post`, `add` for middleware, `:name` url variables), the +same as on the tokio server, with async handlers made by `handler(...)`. +`octane::wasip3::serve` creates the listening socket over +`wasi:sockets@0.3.0` and runs each connection as its own task, so +`src/lib.rs` only defines routes. Connections are served concurrently on +one thread while they wait on sockets, timers or streamed bodies. + +## Build and run + +See [the guests README](../README.md) for installing nightly, wasi-sdk and +wasmtime. Then: + +```sh +export WASI_SDK_PATH=~/wasi-sdk/wasi-sdk-34.0-arm64-macos +cargo build --release +wasmtime run -S p3 -S inherit-network -W component-model-async \ + target/wasm32-wasip3/release/wasip3_server.wasm +``` + +## How it builds + +- `rust-toolchain.toml` selects nightly, and `.cargo/config.toml` builds + `std` for `wasm32-wasip3` with `-Zbuild-std`. +- `build.rs` links wasi-sdk's WASIp3 libc and `crt1-reactor.o`. rustc has + no copies of these for a tier 3 target. +- `tools/wasm-component-ld` is the linker. It runs wasi-sdk's + `wasm-component-ld` and `wasm-ld`, because the ones bundled with rustc + are too old for cooperative threading. + +## Routes + +| Route | Response | +| --- | --- | +| every request | logged by a middleware (`router.add`), then routing continues | +| `GET /` | a greeting, at once | +| `GET /hello/:name` | greets `name`, a url variable | +| `GET /sleep?ms=N` | waits N ms on the host clock, then replies | +| `GET /stream?chunks=N&ms=M` | writes the body in N chunks, M ms apart | +| `POST /echo` | the request body | +| anything else | `404`, sent by the server when no route answered | + +Like on the tokio server, a route only answers the method it was added +with: `HEAD /` is a `404` unless it's added with `router.head`. + +10 parallel `curl localhost:8080/sleep?ms=1000` finish in about 1 s +in total, not 10, and requests made while `/stream` is writing are +answered between its chunks. + +## Atomics + +`-C target-feature=+atomics,+bulk-memory` builds and runs, but there's +nothing to gain: WASIp3 tasks are cooperative and share one thread, and +wasm-ld refuses `--cooperative-threading` together with `--shared-memory`, +so the atomic instructions operate on unshared memory. diff --git a/guests/wasip3-server/build.rs b/guests/wasip3-server/build.rs new file mode 100644 index 0000000..572353d --- /dev/null +++ b/guests/wasip3-server/build.rs @@ -0,0 +1,28 @@ +// Link against wasi-sdk's WASIp3 libc and its `crt1-reactor.o`. rustc has +// no self-contained sysroot for this tier 3 target, and for a cdylib it would +// otherwise take crt1-reactor.o from there. That object sets up the per-task +// context: without it wasm-ld doesn't generate the +// `__wasm_get_stack_pointer`/`__wasm_set_stack_pointer` imports that every +// object built for cooperative threading needs. +use std::env; +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-env-changed=WASI_SDK_PATH"); + if env::var("CARGO_CFG_TARGET_ENV").as_deref() != Ok("p3") { + return; + } + let sdk = env::var("WASI_SDK_PATH") + .expect("set WASI_SDK_PATH to the wasi-sdk install (34 or newer has WASIp3)"); + let libc = PathBuf::from(sdk).join("share/wasi-sysroot/lib/wasm32-wasip3"); + assert!( + libc.is_dir(), + "{} is missing, is wasi-sdk 34+?", + libc.display() + ); + println!("cargo:rustc-link-search=native={}", libc.display()); + + let crt = libc.join("crt1-reactor.o"); + assert!(crt.is_file(), "{} is missing", crt.display()); + println!("cargo:rustc-link-arg={}", crt.display()); +} diff --git a/guests/wasip3-server/rust-toolchain.toml b/guests/wasip3-server/rust-toolchain.toml new file mode 100644 index 0000000..e68a88d --- /dev/null +++ b/guests/wasip3-server/rust-toolchain.toml @@ -0,0 +1,4 @@ +# wasm32-wasip3 is tier 3, its std is built from source with -Zbuild-std +[toolchain] +channel = "nightly" +components = ["rust-src"] diff --git a/guests/wasip3-server/src/lib.rs b/guests/wasip3-server/src/lib.rs new file mode 100644 index 0000000..a0376b6 --- /dev/null +++ b/guests/wasip3-server/src/lib.rs @@ -0,0 +1,139 @@ +//! An HTTP/1.1 server as a WASIp3 component, routed by octane's router. +//! +//! Routes are added with the same `Route` trait as on octane's tokio server +//! and `octane::wasip3::serve` does the sockets, connections and buffers. +//! Handlers are async: while one waits on a timer or streams its body, the +//! other connections are served. +//! +//! Routes: +//! - every request is logged by a middleware +//! - `GET /` replies at once +//! - `GET /hello/:name` greets `name`, a url variable +//! - `GET /sleep?ms=N` waits N ms on the host clock before replying +//! - `GET /stream?chunks=N&ms=M` writes the body in N chunks, M ms apart +//! - `POST /echo` replies with the request body +//! - anything else gets a 404 from the server + +use octane::portable::config::Config; +use octane::wasip3::{handler, serve, Flow, MatchedRequest, Responder, Route, Router, SendError}; +use octane::StatusCode; +use std::net::SocketAddr; +use wasip3::clocks::monotonic_clock; + +/// 16 KiB per request (head and body), 1 KiB per response head +const CONFIG: Config<{ 16 * 1024 }, 1024> = Config::new(); + +const TEXT: [(&str, &str); 1] = [("Content-Type", "text/plain")]; + +wasip3::cli::command::export!(Server); + +struct Server; + +impl wasip3::exports::cli::run::Guest for Server { + async fn run() -> Result<(), ()> { + let router = routes().map_err(|error| eprintln!("invalid route: {}", error))?; + let addr: SocketAddr = "127.0.0.1:8080".parse().unwrap(); + serve( + CONFIG, + addr, + |addr| println!("listening on http://{}", addr), + router, + ) + .await + .map_err(|error| eprintln!("cannot serve on {}: {:?}", addr, error)) + } +} + +fn routes() -> Result { + let mut router = Router::new(); + + router.add(handler(async |req, _res| { + println!("{} {}", req.method, req.target); + Flow::Next + }))?; + + router.get( + "/", + handler(async |_req, res| { + let _ = res + .send(StatusCode::Ok, &TEXT, b"hello from a wasip3 component\n") + .await; + Flow::Stop + }), + )?; + + router.get( + "/hello/:name", + handler(async |req, res| { + let name = req.vars.get("name").copied().unwrap_or("there"); + let _ = res + .send( + StatusCode::Ok, + &TEXT, + format!("hello, {}\n", name).as_bytes(), + ) + .await; + Flow::Stop + }), + )?; + + router.get( + "/sleep", + handler(async |req, res| { + let ms = query(&req, "ms").unwrap_or(1000).min(60_000); + sleep_ms(ms).await; + let _ = res + .send( + StatusCode::Ok, + &TEXT, + format!("slept {} ms\n", ms).as_bytes(), + ) + .await; + Flow::Stop + }), + )?; + + router.get( + "/stream", + handler(async |req, res| { + let chunks = query(&req, "chunks").unwrap_or(5).min(100); + let delay = query(&req, "ms").unwrap_or(200).min(10_000); + let _ = stream(res, chunks, delay).await; + Flow::Stop + }), + )?; + + router.post( + "/echo", + handler(async |req, res| { + let _ = res.send(StatusCode::Ok, &[], req.body).await; + Flow::Stop + }), + )?; + + Ok(router) +} + +fn query(req: &MatchedRequest<'_>, name: &str) -> Option { + req.query_pairs() + .find(|(key, _)| *key == name) + .and_then(|(_, value)| value.parse().ok()) +} + +/// Send `chunks` lines, `delay_ms` apart, as one response body +async fn stream(res: &mut Responder<'_>, chunks: u64, delay_ms: u64) -> Result<(), SendError> { + let line = |i: u64| format!("chunk {} of {}\n", i + 1, chunks); + let len = (0..chunks).map(|i| line(i).len()).sum(); + let mut body = res.stream(StatusCode::Ok, &TEXT, len).await?; + for i in 0..chunks { + if i > 0 { + sleep_ms(delay_ms).await; + } + body.write(line(i).as_bytes()).await?; + } + Ok(()) +} + +async fn sleep_ms(ms: u64) { + monotonic_clock::wait_for(ms * 1_000_000).await; +} diff --git a/guests/wasip3-server/tools/wasm-component-ld b/guests/wasip3-server/tools/wasm-component-ld new file mode 100755 index 0000000..789f422 --- /dev/null +++ b/guests/wasip3-server/tools/wasm-component-ld @@ -0,0 +1,8 @@ +#!/bin/sh +# Link with wasi-sdk's linkers instead of the ones bundled with rustc. +# +# rustc ships an older wasm-component-ld, which can't turn the per-task +# `env::__wasm_{get,set}_{stack_pointer,tls_base}` imports of cooperative +# threading into component built-ins, and puts its own lld first in PATH. +: "${WASI_SDK_PATH:?set WASI_SDK_PATH to the wasi-sdk install (34 or newer)}" +exec "$WASI_SDK_PATH/bin/wasm-component-ld" "$@" --wasm-ld-path "$WASI_SDK_PATH/bin/wasm-ld" diff --git a/octane_http/Cargo.toml b/octane_http/Cargo.toml index f1d2810..5302404 100644 --- a/octane_http/Cargo.toml +++ b/octane_http/Cargo.toml @@ -12,8 +12,24 @@ path = "../octane_macros" version = "0.1.2" [dependencies] -tokio = "1.54.1" +tokio = { version = "1.54.1", optional = true } + +# only built for WASIp3, and only with the `wasm` feature +[target.'cfg(all(target_os = "wasi", target_env = "p3"))'.dependencies] +wasip3 = { version = "0.9.0", default-features = false, features = ["async-spawn"], optional = true } + +[features] +default = ["std"] +# The protocol core (parsing, encoding, the blocking serve loop) only needs +# `core`. `std` adds the tokio reader and an adapter for `std::io` streams. +std = ["tokio"] +# `wasip3`: serving over wasi:sockets from a WASIp3 component. It only has an +# effect when building for wasm32-wasip3. +wasm = ["dep:wasip3"] [dev-dependencies] tokio = { version = "1.54.1", features = ["rt"] } +[[example]] +name = "portable_server" +required-features = ["std"] diff --git a/octane_http/examples/portable_server.rs b/octane_http/examples/portable_server.rs new file mode 100644 index 0000000..aa97107 --- /dev/null +++ b/octane_http/examples/portable_server.rs @@ -0,0 +1,42 @@ +//! The allocation-free core served over `std::net`, which also works on +//! WASI preview 2: +//! +//! ```sh +//! cargo run --example portable_server +//! cargo build --example portable_server --target wasm32-wasip2 +//! wasmtime serve -S cli -S inherit-network target/wasm32-wasip2/debug/examples/portable_server.wasm +//! ``` +//! +//! On a platform without `std`, implement `Connection` for its socket type +//! and call `CONFIG.serve` the same way. +use octane_http::config::Config; +use octane_http::http1x::Method; +use octane_http::server::StdIo; +use octane_http::StatusCode; +use std::net::TcpListener; + +// 16 KiB for requests, 1 KiB for response heads, 17 KiB of stack in all +const CONFIG: Config<{ 16 * 1024 }, 1024> = Config::new(); + +fn main() -> std::io::Result<()> { + let listener = TcpListener::bind("127.0.0.1:8080")?; + println!("listening on http://{}", listener.local_addr()?); + + // one connection at a time, so one pair of buffers is enough + let mut buffers = CONFIG.buffers(); + for stream in listener.incoming() { + let result = CONFIG.serve_with(StdIo(stream?), &mut buffers, |req, res| { + match (req.method, req.path) { + (Method::Get, "/hello") => res + .header("Content-Type", b"text/plain") + .body(b"Hello from no_std!"), + (Method::Post, "/echo") => res.body(req.body), + _ => res.status(StatusCode::NotFound).body(b"Not Found"), + }; + }); + if let Err(error) = result { + eprintln!("connection error: {}", error); + } + } + Ok(()) +} diff --git a/octane_http/src/config.rs b/octane_http/src/config.rs new file mode 100644 index 0000000..e98d46b --- /dev/null +++ b/octane_http/src/config.rs @@ -0,0 +1,159 @@ +//! Sizing the memory a connection uses. +//! +//! The serve loop never allocates, so every byte it needs is in two fixed +//! buffers whose sizes are part of the [`Config`] type: +//! +//! - `INPUT` holds the request being read, head and body together. It bounds +//! the largest request that can be served, bigger ones get a `431` or `413`. +//! - `OUTPUT` holds the response head. Response bodies are not copied into it. +//! +//! Without the `std` feature there is no default: the sizes have to be +//! chosen for the target, since on bare metal they come out of a stack (or a +//! static) that is often only a few KiB. [`Config::stack_size`] says how much +//! [`Config::serve`] puts on the stack, [`Config::serve_with`] takes the +//! buffers from elsewhere instead. +//! +//! ``` +//! use octane_http::config::Config; +//! +//! // 4 KiB for requests, 512 bytes for response heads +//! const CONFIG: Config<4096, 512> = Config::new(); +//! assert_eq!(CONFIG.stack_size(), 4608); +//! ``` +//! +//! Sizes below [`MIN_INPUT_SIZE`] or [`MIN_OUTPUT_SIZE`] don't compile: +//! +//! ```compile_fail +//! use octane_http::config::Config; +//! +//! let config = Config::<16, 512>::new(); +//! ``` + +/// The smallest `INPUT`, enough for a short request with a `Host` header +pub const MIN_INPUT_SIZE: usize = 128; + +/// The smallest `OUTPUT`, enough for any error response the server sends +/// itself, so a request that fails to parse always gets an answer +pub const MIN_OUTPUT_SIZE: usize = 128; + +/// The `INPUT` size of [`DefaultConfig`] +#[cfg(feature = "std")] +pub const DEFAULT_INPUT_SIZE: usize = 16 * 1024; + +/// The `OUTPUT` size of [`DefaultConfig`] +#[cfg(feature = "std")] +pub const DEFAULT_OUTPUT_SIZE: usize = 1024; + +/// Sizes for hosts with large stacks, only available with `std`. +/// It takes 17 KiB of stack per connection served with [`Config::serve`]. +#[cfg(feature = "std")] +pub type DefaultConfig = Config; + +/// The buffer sizes of a connection, see the [module docs](self) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Config { + _sized: (), +} + +impl Config { + /// Fails to compile if a size is below its minimum + pub const fn new() -> Self { + assert_sizes::(); + Self { _sized: () } + } + + pub const fn input_size(&self) -> usize { + INPUT + } + + pub const fn output_size(&self) -> usize { + OUTPUT + } + + /// The bytes of stack [`Config::serve`] uses for its buffers, on top of + /// what the transport and the handler need + pub const fn stack_size(&self) -> usize { + INPUT + OUTPUT + } + + /// Zeroed buffers of the configured sizes + pub const fn buffers(&self) -> Buffers { + Buffers::new() + } +} + +/// The buffers one connection is served with. `new` is a `const fn`, so on a +/// target with a small stack they can be put in a `static` and lent to +/// [`Config::serve_with`]. +pub struct Buffers { + pub(crate) input: [u8; INPUT], + pub(crate) output: [u8; OUTPUT], +} + +impl Buffers { + /// Fails to compile if a size is below its minimum + pub const fn new() -> Self { + assert_sizes::(); + Self { + input: [0; INPUT], + output: [0; OUTPUT], + } + } +} + +// The sizes are still type parameters, so `Default` doesn't make them optional +impl Default for Config { + fn default() -> Self { + Self::new() + } +} + +impl Default for Buffers { + fn default() -> Self { + Self::new() + } +} + +const fn assert_sizes() { + const { + assert!( + INPUT >= MIN_INPUT_SIZE, + "the INPUT buffer must be at least MIN_INPUT_SIZE bytes" + ); + assert!( + OUTPUT >= MIN_OUTPUT_SIZE, + "the OUTPUT buffer must be at least MIN_OUTPUT_SIZE bytes" + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::response::error_response; + use crate::StatusCode; + + #[test] + fn the_smallest_output_fits_every_error_response() { + // the longest reason phrase among the statuses the server sends + for status in [ + StatusCode::RequestHeaderFieldsTooLarge, + StatusCode::HttpVersionNotSupported, + StatusCode::InternalServerError, + StatusCode::NetworkAuthenticationRequired, + ] { + let mut out = [0u8; MIN_OUTPUT_SIZE]; + assert!(error_response(&mut out, status).is_ok(), "{:?}", status); + } + } + + #[test] + fn sizes_are_reported() { + const CONFIG: Config<4096, 256> = Config::new(); + assert_eq!(CONFIG.input_size(), 4096); + assert_eq!(CONFIG.output_size(), 256); + assert_eq!(CONFIG.stack_size(), 4352); + let buffers = CONFIG.buffers(); + assert_eq!((buffers.input.len(), buffers.output.len()), (4096, 256)); + } +} diff --git a/octane_http/src/http1x/buffer.rs b/octane_http/src/http1x/buffer.rs new file mode 100644 index 0000000..24b4def --- /dev/null +++ b/octane_http/src/http1x/buffer.rs @@ -0,0 +1,237 @@ +use super::raw_request::RawRequest1x; +use super::request::Request; +use super::{HeadTerminatorSearch, MAX_HEAD_SIZE}; +use crate::StatusCode; + +/// The outcome of [`RequestBuffer::poll`] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Status { + /// A whole request, head and body, is buffered + Complete(T), + /// More bytes are needed, read into [`RequestBuffer::unfilled`] + Partial, +} + +/// Where a buffered request ends, found once its head is complete +#[derive(Debug, Clone, Copy)] +struct Framing { + head_len: usize, + body_len: usize, +} + +/// Accumulates requests in caller-provided storage, without allocating and +/// without doing any I/O itself. +/// +/// The transport reads into [`unfilled`](Self::unfilled) and reports how +/// much it read with [`advance`](Self::advance), then [`poll`](Self::poll) +/// says whether a whole request has arrived. The storage bounds both the head +/// and the body, a request that can't fit is refused with a `431` or `413`. +/// After handling a request, [`consume`](Self::consume) drops it and keeps +/// any pipelined bytes that followed it. +/// +/// ``` +/// use octane_http::http1x::{RequestBuffer, Status}; +/// +/// let mut storage = [0u8; 1024]; +/// let mut buf = RequestBuffer::new(&mut storage); +/// +/// for chunk in [&b"POST / HTTP/1.1\r\nHost: a\r\nCont"[..], b"ent-Length: 2\r\n\r\nhi"] { +/// buf.unfilled()[..chunk.len()].copy_from_slice(chunk); +/// buf.advance(chunk.len()); +/// } +/// +/// match buf.poll().unwrap() { +/// Status::Complete(req) => assert_eq!(req.body, b"hi"), +/// Status::Partial => unreachable!(), +/// } +/// buf.consume(); +/// ``` +pub struct RequestBuffer<'b> { + storage: &'b mut [u8], + filled: usize, + search: HeadTerminatorSearch, + framing: Option, +} + +impl<'b> RequestBuffer<'b> { + pub fn new(storage: &'b mut [u8]) -> Self { + Self { + storage, + filled: 0, + search: HeadTerminatorSearch::default(), + framing: None, + } + } + + /// The free space the transport should read into. It is empty once the + /// storage is full, [`poll`](Self::poll) reports that as an error. + pub fn unfilled(&mut self) -> &mut [u8] { + &mut self.storage[self.filled..] + } + + /// Record that `n` bytes were written to the front of + /// [`unfilled`](Self::unfilled) + /// + /// # Panics + /// + /// If `n` is larger than the unfilled space + pub fn advance(&mut self, n: usize) { + assert!( + n <= self.storage.len() - self.filled, + "advanced past the buffer" + ); + self.filled += n; + } + + /// The bytes buffered so far + pub fn filled(&self) -> &[u8] { + &self.storage[..self.filled] + } + + /// Whether nothing is buffered, so the connection is between requests + pub fn is_empty(&self) -> bool { + self.filled == 0 + } + + /// Check for a complete request. The returned request's `body` is + /// exactly the body declared by its `Content-Length`. + pub fn poll(&mut self) -> Result>, StatusCode> { + let framing = match self.framing { + Some(framing) => framing, + None => match self.find_framing()? { + Some(framing) => framing, + None => return Ok(Status::Partial), + }, + }; + let end = framing.head_len + framing.body_len; + if self.filled < end { + return Ok(Status::Partial); + } + let buf = &self.storage[..end]; + let mut raw = RawRequest1x::parse_with_head_end(buf, framing.head_len)?; + raw.body_remainder = &buf[framing.head_len..]; + Request::from_raw(raw).map(Status::Complete) + } + + /// Drop the request the last [`poll`](Self::poll) completed, moving any + /// bytes buffered after it to the front. Does nothing if no request is + /// complete. + pub fn consume(&mut self) { + let framing = match self.framing { + Some(framing) => framing, + None => return, + }; + let end = framing.head_len + framing.body_len; + if self.filled < end { + return; + } + self.storage.copy_within(end..self.filled, 0); + self.filled -= end; + self.search = HeadTerminatorSearch::default(); + self.framing = None; + } + + /// Look for the end of the head and, once found, validate the head and + /// work out the body length + fn find_framing(&mut self) -> Result, StatusCode> { + let head_limit = self.storage.len().min(MAX_HEAD_SIZE); + let searchable = self.filled.min(head_limit); + let head_len = match self.search.find(&self.storage[..searchable]) { + Some(head_len) => head_len, + None if searchable == head_limit => { + return Err(StatusCode::RequestHeaderFieldsTooLarge) + } + None => return Ok(None), + }; + let head = &self.storage[..head_len]; + let request = Request::from_raw(RawRequest1x::parse_with_head_end(head, head_len)?)?; + // the body has to fit in what's left of the storage + let body_len = request + .headers + .content_length(self.storage.len() - head_len)?; + let framing = Framing { head_len, body_len }; + self.framing = Some(framing); + Ok(Some(framing)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn feed(buf: &mut RequestBuffer<'_>, bytes: &[u8]) { + buf.unfilled()[..bytes.len()].copy_from_slice(bytes); + buf.advance(bytes.len()); + } + + fn body_of(buf: &mut RequestBuffer<'_>) -> Option<[u8; 8]> { + match buf.poll().unwrap() { + Status::Complete(req) => { + let mut out = [0u8; 8]; + out[..req.body.len()].copy_from_slice(req.body); + Some(out) + } + Status::Partial => None, + } + } + + #[test] + fn waits_for_head_and_body_one_byte_at_a_time() { + let raw = b"POST /x HTTP/1.1\r\nHost: a\r\nContent-Length: 3\r\n\r\nabc"; + let mut storage = [0u8; 128]; + let mut buf = RequestBuffer::new(&mut storage); + + for (i, byte) in raw.iter().enumerate() { + feed(&mut buf, &[*byte]); + let done = body_of(&mut buf); + assert_eq!(done.is_some(), i == raw.len() - 1, "byte {}", i); + } + assert_eq!(&body_of(&mut buf).unwrap()[..3], b"abc"); + } + + #[test] + fn keeps_pipelined_requests() { + let mut storage = [0u8; 256]; + let mut buf = RequestBuffer::new(&mut storage); + feed( + &mut buf, + b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 2\r\n\r\nhiGET /2 HTTP/1.1\r\nHost: a\r\n\r\n", + ); + + match buf.poll().unwrap() { + Status::Complete(req) => assert_eq!((req.path, req.body), ("/", &b"hi"[..])), + Status::Partial => panic!("first request incomplete"), + } + buf.consume(); + match buf.poll().unwrap() { + Status::Complete(req) => assert_eq!((req.path, req.body), ("/2", &b""[..])), + Status::Partial => panic!("second request incomplete"), + } + buf.consume(); + assert!(buf.is_empty()); + } + + #[test] + fn refuses_what_does_not_fit() { + let mut storage = [0u8; 32]; + let mut buf = RequestBuffer::new(&mut storage); + feed(&mut buf, &b"GET / HTTP/1.1\r\nHost: aaaaaaaaaaaaaaa"[..32]); + assert_eq!(buf.poll(), Err(StatusCode::RequestHeaderFieldsTooLarge)); + + let mut storage = [0u8; 64]; + let mut buf = RequestBuffer::new(&mut storage); + feed( + &mut buf, + b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 30\r\n\r\n", + ); + assert_eq!(buf.poll(), Err(StatusCode::PayloadTooLarge)); + } + + #[test] + fn reports_invalid_heads() { + let mut storage = [0u8; 64]; + let mut buf = RequestBuffer::new(&mut storage); + feed(&mut buf, b"GET / HTTP/1.1\r\n\r\n"); + assert_eq!(buf.poll(), Err(StatusCode::BadRequest)); + } +} diff --git a/octane_http/src/http1x/headers.rs b/octane_http/src/http1x/headers.rs new file mode 100644 index 0000000..bd08d2f --- /dev/null +++ b/octane_http/src/http1x/headers.rs @@ -0,0 +1,318 @@ +use crate::{StatusCode, CRLF}; +use core::str; + +/// One `name: value` field of a request head, borrowed from the buffer it +/// was parsed out of. The value has its surrounding whitespace removed. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Header<'a> { + pub name: &'a str, + pub value: &'a [u8], +} + +impl<'a> Header<'a> { + /// Split and validate a single header line (without its CRLF). + /// + /// The name must be a token (RFC 9110 section 5.1), which also rejects + /// obsolete line folding, and the value may not contain control + /// characters other than HTAB. + pub fn parse(line: &'a [u8]) -> Result { + let colon = line + .iter() + .position(|&b| b == b':') + .ok_or(StatusCode::BadRequest)?; + let (name, value) = (&line[..colon], &line[colon + 1..]); + if !is_token(name) { + return Err(StatusCode::BadRequest); + } + let value = trim_ows(value); + if value.iter().any(|&b| is_ctl(b) && b != b'\t') { + return Err(StatusCode::BadRequest); + } + Ok(Self { + // tokens are ASCII, so this never fails + name: str::from_utf8(name).map_err(|_| StatusCode::BadRequest)?, + value, + }) + } + + /// The value as a `str`, `None` if it isn't valid UTF-8 + pub fn value_str(&self) -> Option<&'a str> { + str::from_utf8(self.value).ok() + } +} + +/// The validated header block of a request head. +/// +/// Nothing is copied or collected: lookups and iteration walk the original +/// bytes. Every line was checked once by [`Headers::parse`], so iterating +/// can't fail. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct Headers<'a> { + block: &'a [u8], +} + +impl<'a> Headers<'a> { + /// Validate a header block, the lines between the request-line and the + /// empty line, joined by CRLF. An empty block has no headers. + pub fn parse(block: &'a [u8]) -> Result { + for line in Lines(Some(block).filter(|block| !block.is_empty())) { + Header::parse(line)?; + } + Ok(Self { block }) + } + + /// Iterate over every header in the order it was sent + pub fn iter(&self) -> HeaderIter<'a> { + HeaderIter { + lines: Lines(Some(self.block).filter(|block| !block.is_empty())), + } + } + + /// The first header named `name`, compared ASCII case-insensitively + pub fn get(&self, name: &str) -> Option<&'a [u8]> { + self.get_all(name).next() + } + + /// The first header named `name` as a `str` + pub fn get_str(&self, name: &str) -> Option<&'a str> { + self.get(name).and_then(|value| str::from_utf8(value).ok()) + } + + /// Every header named `name`, compared ASCII case-insensitively + pub fn get_all<'n>(&self, name: &'n str) -> impl Iterator + 'n + where + 'a: 'n, + { + self.iter() + .filter(move |header| header.name.eq_ignore_ascii_case(name)) + .map(|header| header.value) + } + + /// Whether any header is named `name` + pub fn contains(&self, name: &str) -> bool { + self.get(name).is_some() + } + + pub fn is_empty(&self) -> bool { + self.block.is_empty() + } + + /// The raw, validated header block + pub fn as_bytes(&self) -> &'a [u8] { + self.block + } + + /// The body length declared by `Content-Length`, 0 if it isn't present + /// (RFC 9112 section 6.3). Repeated identical values, in one field like + /// `5, 5` or across fields, are accepted. A value that isn't a decimal + /// number or values that disagree give a `400`, a length over `max` + /// gives a `413`. + pub fn content_length(&self, max: usize) -> Result { + let mut digits: Option<&[u8]> = None; + for value in self.get_all("content-length") { + for member in value.split(|&b| b == b',') { + let member = trim_ows(member); + if member.is_empty() || !member.iter().all(u8::is_ascii_digit) { + return Err(StatusCode::BadRequest); + } + // compare numbers rather than spellings, "05" and "5" agree + let zeros = member.iter().take_while(|&&b| b == b'0').count(); + let member = &member[zeros..]; + if digits.is_some_and(|digits| digits != member) { + return Err(StatusCode::BadRequest); + } + digits = Some(member); + } + } + // anything past usize::MAX is too large anyway + let length = digits.unwrap_or(b"").iter().fold(0usize, |n, b| { + n.saturating_mul(10).saturating_add(usize::from(b - b'0')) + }); + if length > max { + return Err(StatusCode::PayloadTooLarge); + } + Ok(length) + } + + /// Whether the `Connection` header lists `option`, like `close` + pub fn connection_has(&self, option: &str) -> bool { + self.get_all("connection").any(|value| { + value + .split(|&b| b == b',') + .any(|member| trim_ows(member).eq_ignore_ascii_case(option.as_bytes())) + }) + } +} + +impl<'a> IntoIterator for Headers<'a> { + type Item = Header<'a>; + type IntoIter = HeaderIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// Iterator over the fields of a [`Headers`] block +#[derive(Debug, Clone)] +pub struct HeaderIter<'a> { + lines: Lines<'a>, +} + +impl<'a> Iterator for HeaderIter<'a> { + type Item = Header<'a>; + + fn next(&mut self) -> Option { + // the block was validated by `Headers::parse` + self.lines.next().and_then(|line| Header::parse(line).ok()) + } +} + +/// CRLF separated lines, `None` once exhausted +#[derive(Debug, Clone)] +struct Lines<'a>(Option<&'a [u8]>); + +impl<'a> Iterator for Lines<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + let rest = self.0?; + match crate::http1x::find_in_slice(rest, CRLF) { + Some(end) => { + self.0 = Some(&rest[end + CRLF.len()..]); + Some(&rest[..end]) + } + None => { + self.0 = None; + Some(rest) + } + } + } +} + +/// tchar from RFC 9110 section 5.6.2 +#[inline] +pub(crate) fn is_tchar(byte: u8) -> bool { + matches!( + byte, + b'0'..=b'9' + | b'A'..=b'Z' + | b'a'..=b'z' + | b'!' + | b'#' + | b'$' + | b'%' + | b'&' + | b'\'' + | b'*' + | b'+' + | b'-' + | b'.' + | b'^' + | b'_' + | b'`' + | b'|' + | b'~' + ) +} + +#[inline] +pub(crate) fn is_token(value: &[u8]) -> bool { + !value.is_empty() && value.iter().all(|&b| is_tchar(b)) +} + +#[inline] +pub(crate) fn is_ctl(byte: u8) -> bool { + byte <= 0x1f || byte == 0x7f +} + +/// Strip optional whitespace (SP and HTAB) from both ends +pub(crate) fn trim_ows(mut value: &[u8]) -> &[u8] { + while let [b' ' | b'\t', rest @ ..] = value { + value = rest; + } + while let [rest @ .., b' ' | b'\t'] = value { + value = rest; + } + value +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn iterates_and_looks_up_case_insensitively() { + let headers = Headers::parse(b"Host: a\r\nX-Test: b\tc \t\r\nhost: d").unwrap(); + let mut iter = headers.iter(); + assert_eq!( + iter.next(), + Some(Header { + name: "Host", + value: b"a" + }) + ); + assert_eq!(iter.next().unwrap().value, b"b\tc"); + assert_eq!(iter.next().unwrap().name, "host"); + assert_eq!(iter.next(), None); + + assert_eq!(headers.get("HOST"), Some(&b"a"[..])); + assert_eq!(headers.get_str("x-test"), Some("b\tc")); + assert_eq!(headers.get_all("host").count(), 2); + assert!(!headers.contains("missing")); + } + + #[test] + fn empty_block_has_no_headers() { + let headers = Headers::parse(b"").unwrap(); + assert!(headers.is_empty()); + assert_eq!(headers.iter().count(), 0); + } + + #[test] + fn rejects_malformed_lines() { + for block in [ + &b"NoColon"[..], + b": empty-name", + b"Bad Name: x", + b" Folded: x", + b"Host: a\r\n\tcontinued", + b"X: bell\x07", + b"Host: a\r\n", + ] { + assert_eq!(Headers::parse(block), Err(StatusCode::BadRequest)); + } + // HTAB is the only control character a value may hold + assert!(Headers::parse(b"X: a\tb").is_ok()); + } + + #[test] + fn content_length() { + let parse = |block: &[u8]| Headers::parse(block).unwrap().content_length(100); + assert_eq!(parse(b"Host: a"), Ok(0)); + assert_eq!(parse(b"Content-Length: 42"), Ok(42)); + assert_eq!(parse(b"Content-Length: 007"), Ok(7)); + assert_eq!(parse(b"Content-Length: 0"), Ok(0)); + assert_eq!(parse(b"Content-Length: 5\r\ncontent-length: 5"), Ok(5)); + assert_eq!(parse(b"Content-Length: 5, 05"), Ok(5)); + + let bad = Err(StatusCode::BadRequest); + assert_eq!(parse(b"Content-Length: 5\r\nContent-Length: 6"), bad); + assert_eq!(parse(b"Content-Length: +5"), bad); + assert_eq!(parse(b"Content-Length: 0x10"), bad); + assert_eq!(parse(b"Content-Length: "), bad); + assert_eq!(parse(b"Content-Length: 5,"), bad); + + let too_large = Err(StatusCode::PayloadTooLarge); + assert_eq!(parse(b"Content-Length: 101"), too_large); + assert_eq!(parse(b"Content-Length: 99999999999999999999999"), too_large); + } + + #[test] + fn connection_options() { + let headers = Headers::parse(b"Connection: Upgrade, Close").unwrap(); + assert!(headers.connection_has("close")); + assert!(headers.connection_has("upgrade")); + assert!(!headers.connection_has("keep-alive")); + } +} diff --git a/octane_http/src/http1x/mod.rs b/octane_http/src/http1x/mod.rs index 5f319ea..52b85a9 100644 --- a/octane_http/src/http1x/mod.rs +++ b/octane_http/src/http1x/mod.rs @@ -1,22 +1,23 @@ -use crate::http1x::raw_request::RawRequest1x; -use crate::{StatusCode, DOUBLE_CRLF}; - -use std::future::Future; -use std::io; -use std::mem::MaybeUninit; -use std::pin::Pin; -use std::task::{Context, Poll}; -use tokio::io::{AsyncRead, ReadBuf}; +use crate::DOUBLE_CRLF; +pub mod buffer; +pub mod headers; pub mod raw_request; +#[cfg(feature = "std")] +mod reader; +pub mod request; -// 8 kb static read buffer, the standard chunk size across nginx and friends. -const BUFFER_SIZE: usize = 8192; +pub use buffer::{RequestBuffer, Status}; +pub use headers::{Header, Headers}; +#[cfg(feature = "std")] +pub use reader::{http_1x_reader, Http1xReader}; +pub use request::{Method, Request}; // Upper bound on the complete request head (request-line, headers and the // terminating CRLFCRLF). A peer that never sends the terminator must not be -// able to grow `data` without limit. -const MAX_HEAD_SIZE: usize = 64 * 1024; +// able to grow `data` without limit. [`RequestBuffer`] is bounded by the +// size of the storage it is handed instead. +pub const MAX_HEAD_SIZE: usize = 64 * 1024; /// Incremental CRLFCRLF search state. /// @@ -25,12 +26,12 @@ const MAX_HEAD_SIZE: usize = 64 * 1024; /// boundary makes fragmented, one-byte-at-a-time input linear rather than /// repeatedly searching the whole accumulated head. #[derive(Default)] -struct HeadTerminatorSearch { - next_start: usize, +pub(crate) struct HeadTerminatorSearch { + pub(crate) next_start: usize, } impl HeadTerminatorSearch { - fn find(&mut self, data: &[u8]) -> Option { + pub(crate) fn find(&mut self, data: &[u8]) -> Option { let start = self.next_start.min(data.len()); let found = find_head_end(data, start); @@ -61,124 +62,6 @@ fn find_head_end(data: &[u8], start: usize) -> Option { None } -/// Poll one read without retaining an 8 KiB scratch array in every suspended -/// reader future. The temporary storage lives only on the executor's stack -/// while this closure is actively being polled, and `MaybeUninit` avoids -/// clearing bytes that the I/O source immediately overwrites. -struct ReadChunk<'a, R> { - reader: &'a mut R, - data: &'a mut Vec, - read_limit: usize, -} - -impl Future for ReadChunk<'_, R> -where - R: AsyncRead + Unpin, -{ - type Output = io::Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - debug_assert!(this.read_limit <= BUFFER_SIZE); - let mut storage = [MaybeUninit::::uninit(); BUFFER_SIZE]; - let mut buf = ReadBuf::uninit(&mut storage[..this.read_limit]); - - match Pin::new(&mut *this.reader).poll_read(cx, &mut buf) { - Poll::Ready(Ok(())) => { - let bytes = buf.filled(); - let count = bytes.len(); - this.data.extend_from_slice(bytes); - Poll::Ready(Ok(count)) - } - Poll::Ready(Err(error)) => Poll::Ready(Err(error)), - Poll::Pending => Poll::Pending, - } - } -} - -fn read_chunk<'a, R>( - reader: &'a mut R, - data: &'a mut Vec, - read_limit: usize, -) -> ReadChunk<'a, R> -where - R: AsyncRead + Unpin, -{ - ReadChunk { - reader, - data, - read_limit, - } -} - -/// Reads and parses the head of an HTTP/1.x request straight off an -/// [`AsyncRead`] source, with zero copies of the parsed fields. -/// -/// Bytes are streamed into the caller-owned `data` buffer until the -/// `CRLFCRLF` head terminator is seen (or the peer hangs up). The returned -/// [`RawRequest1x`] then borrows directly from `data` — the method, url, -/// version, header block and already-buffered body are all sub-slices of it. -/// -/// The reader is borrowed rather than consumed, so the caller still owns it -/// afterwards and can stream the rest of the body off the same source. -pub struct Http1xReader; - -impl Http1xReader { - pub async fn new<'a, R>( - reader: &mut R, - data: &'a mut Vec, - ) -> Result, StatusCode> - where - R: AsyncRead + Unpin, - { - let mut terminator = HeadTerminatorSearch::default(); - - // Keep reading until the head is complete. We only need the head - // buffered to parse the request; the caller can stream the rest of the - // body afterwards from the same reader. - loop { - // Only the first MAX_HEAD_SIZE bytes can contain an admissible - // terminator. This also bounds work when a caller supplies a large - // pre-populated buffer containing an already-buffered body. - let searchable_len = data.len().min(MAX_HEAD_SIZE); - if let Some(head_end) = terminator.find(&data[..searchable_len]) { - // Reuse the delimiter position so the zero-copy parser does - // not make a second pass over a potentially 64 KiB head. - return RawRequest1x::parse_with_head_end(&data[..], head_end); - } - - // At exactly the limit, an incomplete delimiter can only finish - // beyond it. Checking before the next read keeps the cap exact and - // independent of the source's chunk size. - if searchable_len == MAX_HEAD_SIZE { - return Err(StatusCode::RequestHeaderFieldsTooLarge); - } - - let read_limit = BUFFER_SIZE.min(MAX_HEAD_SIZE - data.len()); - let n = read_chunk(reader, data, read_limit) - .await - .map_err(|_| StatusCode::BadRequest)?; - - if n == 0 { - // EOF before a complete head arrived. - return Err(StatusCode::BadRequest); - } - } - } -} - -/// Free-function form of [`Http1xReader::new`], kept for call sites that read -/// a request without holding onto a reader type. -pub async fn http_1x_reader<'a, R>( - reader: &mut R, - data: &'a mut Vec, -) -> Result, StatusCode> -where - R: AsyncRead + Unpin, -{ - Http1xReader::new(reader, data).await -} - pub fn find_in_slice(haystack: &[T], needle: &[T]) -> Option { // naive algorithm only meant for small needles if needle.len() > haystack.len() { @@ -203,88 +86,6 @@ pub fn find_in_slice(haystack: &[T], needle: &[T]) -> Option { mod tests { use super::*; - struct ChunkedReader<'a> { - remaining: &'a [u8], - max_chunk: usize, - reads: usize, - } - - impl<'a> ChunkedReader<'a> { - fn new(bytes: &'a [u8], max_chunk: usize) -> Self { - assert!(max_chunk > 0); - Self { - remaining: bytes, - max_chunk, - reads: 0, - } - } - } - - impl AsyncRead for ChunkedReader<'_> { - fn poll_read( - self: Pin<&mut Self>, - _cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let this = self.get_mut(); - let count = this - .remaining - .len() - .min(this.max_chunk) - .min(buf.remaining()); - let (chunk, remaining) = this.remaining.split_at(count); - buf.put_slice(chunk); - this.remaining = remaining; - this.reads += 1; - Poll::Ready(Ok(())) - } - } - - fn request_with_head_len(head_len: usize) -> Vec { - const PREFIX: &[u8] = b"GET / HTTP/1.1\r\nX: "; - assert!(head_len >= PREFIX.len() + DOUBLE_CRLF.len()); - - let mut request = Vec::with_capacity(head_len); - request.extend_from_slice(PREFIX); - request.resize(head_len - DOUBLE_CRLF.len(), b'a'); - request.extend_from_slice(DOUBLE_CRLF); - request - } - - fn block_on(fut: F) -> F::Output { - tokio::runtime::Builder::new_current_thread() - .build() - .unwrap() - .block_on(fut) - } - - #[test] - fn reads_a_request_split_across_chunks() { - // A `&[u8]` is an `AsyncRead`; reading from it advances the slice, so - // this exercises the multi-`read` accumulation loop. - let raw = b"GET /hi HTTP/1.1\r\nHost: a\r\n\r\nbody-bytes"; - let mut data = Vec::new(); - let mut reader = &raw[..]; - - let req = block_on(Http1xReader::new(&mut reader, &mut data)).unwrap(); - - assert_eq!(req.request_method, b"GET"); - assert_eq!(req.request_url, b"/hi"); - assert_eq!(req.headers, b"Host: a"); - assert_eq!(req.body_remainder, b"body-bytes"); - } - - #[test] - fn errors_when_peer_hangs_up_before_head_completes() { - let raw = b"GET / HTTP/1.1\r\nHost: a\r\n"; // no terminating CRLFCRLF - let mut data = Vec::new(); - let mut reader = &raw[..]; - - let result = block_on(Http1xReader::new(&mut reader, &mut data)); - - assert_eq!(result, Err(StatusCode::BadRequest)); - } - #[test] fn find_in_slice_locates_and_misses() { assert_eq!(find_in_slice(b"abcXYdef", b"XY"), Some(3)); @@ -292,84 +93,21 @@ mod tests { assert_eq!(find_in_slice(b"a", b"abc"), None); } - #[test] - fn accepts_a_head_exactly_at_the_limit_for_any_chunking() { - let request = request_with_head_len(MAX_HEAD_SIZE); - - for chunk_size in [1, 17, BUFFER_SIZE, usize::MAX] { - let mut reader = ChunkedReader::new(&request, chunk_size); - let mut data = Vec::new(); - - let parsed = block_on(Http1xReader::new(&mut reader, &mut data)).unwrap(); - assert_eq!(parsed.request_method, b"GET"); - assert!(parsed.body_remainder.is_empty()); - assert_eq!(data.len(), MAX_HEAD_SIZE, "chunk size {}", chunk_size); - } - } - - #[test] - fn rejects_a_head_one_byte_over_the_limit_for_any_chunking() { - let request = request_with_head_len(MAX_HEAD_SIZE + 1); - - for chunk_size in [1, 17, BUFFER_SIZE, usize::MAX] { - let mut reader = ChunkedReader::new(&request, chunk_size); - let mut data = Vec::new(); - - let result = block_on(Http1xReader::new(&mut reader, &mut data)); - assert_eq!( - result, - Err(StatusCode::RequestHeaderFieldsTooLarge), - "chunk size {}", - chunk_size - ); - assert_eq!(data.len(), MAX_HEAD_SIZE, "chunk size {}", chunk_size); - } - } - - #[test] - fn does_not_charge_an_already_buffered_body_to_the_head_limit() { - let head = b"POST / HTTP/1.1\r\nContent-Length: 131072\r\n\r\n"; - let body_len = MAX_HEAD_SIZE * 2; - let mut data = Vec::with_capacity(head.len() + body_len); - data.extend_from_slice(head); - data.resize(head.len() + body_len, b'x'); - let mut reader = ChunkedReader::new(&[], 1); - - let parsed = block_on(Http1xReader::new(&mut reader, &mut data)).unwrap(); - - assert_eq!(parsed.body_remainder.len(), body_len); - assert_eq!(reader.reads, 0); - } - #[test] fn one_byte_fragmentation_searches_each_start_position_once() { - let mut data = Vec::with_capacity(MAX_HEAD_SIZE); + let mut data = [0u8; MAX_HEAD_SIZE]; + let mut len = 0; let mut search = HeadTerminatorSearch::default(); let mut candidate_starts = 0usize; - assert_eq!(search.find(&data), None); + assert_eq!(search.find(&data[..len]), None); for _ in 0..MAX_HEAD_SIZE { - data.push(b'a'); - candidate_starts += - (data.len() - search.next_start).saturating_sub(DOUBLE_CRLF.len() - 1); - assert_eq!(search.find(&data), None); + data[len] = b'a'; + len += 1; + candidate_starts += (len - search.next_start).saturating_sub(DOUBLE_CRLF.len() - 1); + assert_eq!(search.find(&data[..len]), None); } assert_eq!(candidate_starts, MAX_HEAD_SIZE - DOUBLE_CRLF.len() + 1); } - - #[test] - fn suspended_reader_does_not_retain_the_scratch_buffer() { - let raw = b""; - let mut data = Vec::new(); - let mut reader = &raw[..]; - let future = Http1xReader::new(&mut reader, &mut data); - let future_size = std::mem::size_of_val(&future); - - assert!( - future_size < 1024, - "reader future unexpectedly retained {} bytes", - future_size - ); - } } diff --git a/octane_http/src/http1x/reader.rs b/octane_http/src/http1x/reader.rs new file mode 100644 index 0000000..5e61697 --- /dev/null +++ b/octane_http/src/http1x/reader.rs @@ -0,0 +1,285 @@ +use super::raw_request::RawRequest1x; +use super::{HeadTerminatorSearch, MAX_HEAD_SIZE}; +use crate::StatusCode; + +use core::future::Future; +use core::mem::MaybeUninit; +use core::pin::Pin; +use core::task::{Context, Poll}; +use std::io; +use std::vec::Vec; +use tokio::io::{AsyncRead, ReadBuf}; + +// 8 kb static read buffer, the standard chunk size across nginx and friends. +const BUFFER_SIZE: usize = 8192; + +/// Poll one read without retaining an 8 KiB scratch array in every suspended +/// reader future. The temporary storage lives only on the executor's stack +/// while this closure is actively being polled, and `MaybeUninit` avoids +/// clearing bytes that the I/O source immediately overwrites. +struct ReadChunk<'a, R> { + reader: &'a mut R, + data: &'a mut Vec, + read_limit: usize, +} + +impl Future for ReadChunk<'_, R> +where + R: AsyncRead + Unpin, +{ + type Output = io::Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + debug_assert!(this.read_limit <= BUFFER_SIZE); + let mut storage = [MaybeUninit::::uninit(); BUFFER_SIZE]; + let mut buf = ReadBuf::uninit(&mut storage[..this.read_limit]); + + match Pin::new(&mut *this.reader).poll_read(cx, &mut buf) { + Poll::Ready(Ok(())) => { + let bytes = buf.filled(); + let count = bytes.len(); + this.data.extend_from_slice(bytes); + Poll::Ready(Ok(count)) + } + Poll::Ready(Err(error)) => Poll::Ready(Err(error)), + Poll::Pending => Poll::Pending, + } + } +} + +fn read_chunk<'a, R>( + reader: &'a mut R, + data: &'a mut Vec, + read_limit: usize, +) -> ReadChunk<'a, R> +where + R: AsyncRead + Unpin, +{ + ReadChunk { + reader, + data, + read_limit, + } +} + +/// Reads and parses the head of an HTTP/1.x request straight off an +/// [`AsyncRead`] source, with zero copies of the parsed fields. +/// +/// Bytes are streamed into the caller-owned `data` buffer until the +/// `CRLFCRLF` head terminator is seen (or the peer hangs up). The returned +/// [`RawRequest1x`] then borrows directly from `data` — the method, url, +/// version, header block and already-buffered body are all sub-slices of it. +/// +/// The reader is borrowed rather than consumed, so the caller still owns it +/// afterwards and can stream the rest of the body off the same source. +pub struct Http1xReader; + +impl Http1xReader { + pub async fn new<'a, R>( + reader: &mut R, + data: &'a mut Vec, + ) -> Result, StatusCode> + where + R: AsyncRead + Unpin, + { + let mut terminator = HeadTerminatorSearch::default(); + + // Keep reading until the head is complete. We only need the head + // buffered to parse the request; the caller can stream the rest of the + // body afterwards from the same reader. + loop { + // Only the first MAX_HEAD_SIZE bytes can contain an admissible + // terminator. This also bounds work when a caller supplies a large + // pre-populated buffer containing an already-buffered body. + let searchable_len = data.len().min(MAX_HEAD_SIZE); + if let Some(head_end) = terminator.find(&data[..searchable_len]) { + // Reuse the delimiter position so the zero-copy parser does + // not make a second pass over a potentially 64 KiB head. + return RawRequest1x::parse_with_head_end(&data[..], head_end); + } + + // At exactly the limit, an incomplete delimiter can only finish + // beyond it. Checking before the next read keeps the cap exact and + // independent of the source's chunk size. + if searchable_len == MAX_HEAD_SIZE { + return Err(StatusCode::RequestHeaderFieldsTooLarge); + } + + let read_limit = BUFFER_SIZE.min(MAX_HEAD_SIZE - data.len()); + let n = read_chunk(reader, data, read_limit) + .await + .map_err(|_| StatusCode::BadRequest)?; + + if n == 0 { + // EOF before a complete head arrived. + return Err(StatusCode::BadRequest); + } + } + } +} + +/// Free-function form of [`Http1xReader::new`], kept for call sites that read +/// a request without holding onto a reader type. +pub async fn http_1x_reader<'a, R>( + reader: &mut R, + data: &'a mut Vec, +) -> Result, StatusCode> +where + R: AsyncRead + Unpin, +{ + Http1xReader::new(reader, data).await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::DOUBLE_CRLF; + use std::vec::Vec; + + struct ChunkedReader<'a> { + remaining: &'a [u8], + max_chunk: usize, + reads: usize, + } + + impl<'a> ChunkedReader<'a> { + fn new(bytes: &'a [u8], max_chunk: usize) -> Self { + assert!(max_chunk > 0); + Self { + remaining: bytes, + max_chunk, + reads: 0, + } + } + } + + impl AsyncRead for ChunkedReader<'_> { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + let count = this + .remaining + .len() + .min(this.max_chunk) + .min(buf.remaining()); + let (chunk, remaining) = this.remaining.split_at(count); + buf.put_slice(chunk); + this.remaining = remaining; + this.reads += 1; + Poll::Ready(Ok(())) + } + } + + fn request_with_head_len(head_len: usize) -> Vec { + const PREFIX: &[u8] = b"GET / HTTP/1.1\r\nX: "; + assert!(head_len >= PREFIX.len() + DOUBLE_CRLF.len()); + + let mut request = Vec::with_capacity(head_len); + request.extend_from_slice(PREFIX); + request.resize(head_len - DOUBLE_CRLF.len(), b'a'); + request.extend_from_slice(DOUBLE_CRLF); + request + } + + fn block_on(fut: F) -> F::Output { + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on(fut) + } + + #[test] + fn reads_a_request_split_across_chunks() { + // A `&[u8]` is an `AsyncRead`; reading from it advances the slice, so + // this exercises the multi-`read` accumulation loop. + let raw = b"GET /hi HTTP/1.1\r\nHost: a\r\n\r\nbody-bytes"; + let mut data = Vec::new(); + let mut reader = &raw[..]; + + let req = block_on(Http1xReader::new(&mut reader, &mut data)).unwrap(); + + assert_eq!(req.request_method, b"GET"); + assert_eq!(req.request_url, b"/hi"); + assert_eq!(req.headers, b"Host: a"); + assert_eq!(req.body_remainder, b"body-bytes"); + } + + #[test] + fn errors_when_peer_hangs_up_before_head_completes() { + let raw = b"GET / HTTP/1.1\r\nHost: a\r\n"; // no terminating CRLFCRLF + let mut data = Vec::new(); + let mut reader = &raw[..]; + + let result = block_on(Http1xReader::new(&mut reader, &mut data)); + + assert_eq!(result, Err(StatusCode::BadRequest)); + } + + #[test] + fn accepts_a_head_exactly_at_the_limit_for_any_chunking() { + let request = request_with_head_len(MAX_HEAD_SIZE); + + for chunk_size in [1, 17, BUFFER_SIZE, usize::MAX] { + let mut reader = ChunkedReader::new(&request, chunk_size); + let mut data = Vec::new(); + + let parsed = block_on(Http1xReader::new(&mut reader, &mut data)).unwrap(); + assert_eq!(parsed.request_method, b"GET"); + assert!(parsed.body_remainder.is_empty()); + assert_eq!(data.len(), MAX_HEAD_SIZE, "chunk size {}", chunk_size); + } + } + + #[test] + fn rejects_a_head_one_byte_over_the_limit_for_any_chunking() { + let request = request_with_head_len(MAX_HEAD_SIZE + 1); + + for chunk_size in [1, 17, BUFFER_SIZE, usize::MAX] { + let mut reader = ChunkedReader::new(&request, chunk_size); + let mut data = Vec::new(); + + let result = block_on(Http1xReader::new(&mut reader, &mut data)); + assert_eq!( + result, + Err(StatusCode::RequestHeaderFieldsTooLarge), + "chunk size {}", + chunk_size + ); + assert_eq!(data.len(), MAX_HEAD_SIZE, "chunk size {}", chunk_size); + } + } + + #[test] + fn does_not_charge_an_already_buffered_body_to_the_head_limit() { + let head = b"POST / HTTP/1.1\r\nContent-Length: 131072\r\n\r\n"; + let body_len = MAX_HEAD_SIZE * 2; + let mut data = Vec::with_capacity(head.len() + body_len); + data.extend_from_slice(head); + data.resize(head.len() + body_len, b'x'); + let mut reader = ChunkedReader::new(&[], 1); + + let parsed = block_on(Http1xReader::new(&mut reader, &mut data)).unwrap(); + + assert_eq!(parsed.body_remainder.len(), body_len); + assert_eq!(reader.reads, 0); + } + + #[test] + fn suspended_reader_does_not_retain_the_scratch_buffer() { + let raw = b""; + let mut data = Vec::new(); + let mut reader = &raw[..]; + let future = Http1xReader::new(&mut reader, &mut data); + let future_size = std::mem::size_of_val(&future); + + assert!( + future_size < 1024, + "reader future unexpectedly retained {} bytes", + future_size + ); + } +} diff --git a/octane_http/src/http1x/request.rs b/octane_http/src/http1x/request.rs new file mode 100644 index 0000000..00e4f8d --- /dev/null +++ b/octane_http/src/http1x/request.rs @@ -0,0 +1,333 @@ +use super::headers::{is_token, Headers}; +use super::raw_request::RawRequest1x; +use crate::{HttpVersion, StatusCode}; +use core::fmt; +use core::str; + +/// The request method. Methods without a variant of their own are kept as +/// [`Method::Other`], borrowed from the request. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum Method<'a> { + Get, + Head, + Post, + Put, + Delete, + Connect, + Options, + Trace, + Patch, + Other(&'a str), +} + +impl<'a> Method<'a> { + /// Parse a method token. Methods are case-sensitive (RFC 9110 section + /// 9.1), so `get` is an unknown method rather than `GET`. + pub fn parse(method: &'a [u8]) -> Result { + if !is_token(method) { + return Err(StatusCode::BadRequest); + } + Ok(match method { + b"GET" => Self::Get, + b"HEAD" => Self::Head, + b"POST" => Self::Post, + b"PUT" => Self::Put, + b"DELETE" => Self::Delete, + b"CONNECT" => Self::Connect, + b"OPTIONS" => Self::Options, + b"TRACE" => Self::Trace, + b"PATCH" => Self::Patch, + // tokens are ASCII, so this never fails + other => Self::Other(str::from_utf8(other).map_err(|_| StatusCode::BadRequest)?), + }) + } + + pub fn as_str(&self) -> &'a str { + match self { + Self::Get => "GET", + Self::Head => "HEAD", + Self::Post => "POST", + Self::Put => "PUT", + Self::Delete => "DELETE", + Self::Connect => "CONNECT", + Self::Options => "OPTIONS", + Self::Trace => "TRACE", + Self::Patch => "PATCH", + Self::Other(method) => method, + } + } +} + +impl fmt::Display for Method<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +/// A fully validated HTTP/1.x request, borrowing every field from the buffer +/// it was parsed out of. Building one never allocates. +/// +/// ``` +/// use octane_http::http1x::{Method, Request}; +/// use octane_http::HttpVersion; +/// +/// let buf = b"GET /a/b?x=1 HTTP/1.1\r\nHost: example.com\r\n\r\n"; +/// let req = Request::parse(buf).unwrap(); +/// +/// assert_eq!(req.method, Method::Get); +/// assert_eq!(req.path, "/a/b"); +/// assert_eq!(req.query, "x=1"); +/// assert_eq!(req.version, HttpVersion::Http11); +/// assert_eq!(req.headers.get_str("host"), Some("example.com")); +/// ``` +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct Request<'a> { + pub method: Method<'a>, + /// The request-target exactly as it was sent + pub target: &'a str, + /// The path of the target, without its query. `*` for an asterisk-form + /// `OPTIONS` request, and the path part of an absolute-form target. + /// It is not percent-decoded. + pub path: &'a str, + /// The query, without the leading `?`. Empty if there is none + pub query: &'a str, + /// [`HttpVersion::Http10`] or [`HttpVersion::Http11`] + pub version: HttpVersion, + pub headers: Headers<'a>, + /// The body bytes that were buffered alongside the head. The request's + /// declared body may be shorter (the rest is the next pipelined request) + /// or longer (the rest hasn't been read yet), see + /// [`RequestBuffer`](super::RequestBuffer) for a reader that handles both. + pub body: &'a [u8], +} + +impl<'a> Request<'a> { + /// Parse a complete request head out of `buf` + pub fn parse(buf: &'a [u8]) -> Result { + Self::from_raw(RawRequest1x::parse(buf)?) + } + + /// Validate the fields that [`RawRequest1x`] only split apart. + /// + /// Besides the syntax, this enforces what the core needs to frame + /// requests safely: only HTTP/1.0 and HTTP/1.1 are served (`505` + /// otherwise), HTTP/1.1 needs exactly one `Host` and `Transfer-Encoding` + /// isn't supported (`501`), so a body is always delimited by + /// `Content-Length`. + pub fn from_raw(raw: RawRequest1x<'a>) -> Result { + let method = Method::parse(raw.request_method)?; + let version = parse_version(raw.request_version)?; + let target = parse_target(raw.request_url)?; + let (path, query) = split_target(target, &method)?; + let headers = Headers::parse(raw.headers)?; + + if version == HttpVersion::Http11 && headers.get_all("host").count() != 1 { + return Err(StatusCode::BadRequest); + } + if headers.contains("transfer-encoding") { + return Err(StatusCode::NotImplemented); + } + + Ok(Self { + method, + target, + path, + query, + version, + headers, + body: raw.body_remainder, + }) + } + + /// The non-empty segments of the path, skipping `.` segments. + /// `..` segments never get here, they are refused while parsing. + pub fn segments(&self) -> impl Iterator { + self.path + .split('/') + .filter(|segment| !segment.is_empty() && *segment != ".") + } + + /// The `name=value` pairs of the query, not percent-decoded. A pair + /// without `=` has an empty value. + pub fn query_pairs(&self) -> impl Iterator { + self.query + .split('&') + .filter(|pair| !pair.is_empty()) + .map(|pair| pair.split_once('=').unwrap_or((pair, ""))) + } + + /// Whether the connection stays open after this request: HTTP/1.1 + /// unless `Connection: close`, HTTP/1.0 only with `Connection: keep-alive` + pub fn keep_alive(&self) -> bool { + match self.version { + HttpVersion::Http11 => !self.headers.connection_has("close"), + _ => self.headers.connection_has("keep-alive"), + } + } +} + +fn parse_version(version: &[u8]) -> Result { + match version { + b"HTTP/1.1" => Ok(HttpVersion::Http11), + b"HTTP/1.0" => Ok(HttpVersion::Http10), + [b'H', b'T', b'T', b'P', b'/', major, b'.', minor] + if major.is_ascii_digit() && minor.is_ascii_digit() => + { + Err(StatusCode::HttpVersionNotSupported) + } + _ => Err(StatusCode::BadRequest), + } +} + +/// A request-target may only hold visible ASCII (RFC 9112 section 3.2) +fn parse_target(target: &[u8]) -> Result<&str, StatusCode> { + if target.is_empty() || !target.iter().all(|&b| (0x21..=0x7e).contains(&b)) { + return Err(StatusCode::BadRequest); + } + str::from_utf8(target).map_err(|_| StatusCode::BadRequest) +} + +fn split_target<'a>( + target: &'a str, + method: &Method<'_>, +) -> Result<(&'a str, &'a str), StatusCode> { + // asterisk-form, only meaningful for OPTIONS + if target == "*" { + return match method { + Method::Options => Ok(("*", "")), + _ => Err(StatusCode::BadRequest), + }; + } + // authority-form, only for CONNECT; the "path" is the authority + if let Method::Connect = method { + return Ok((target, "")); + } + let (path, query) = target.split_once('?').unwrap_or((target, "")); + let path = if path.starts_with('/') { + path + } else { + // absolute-form, keep the path after the scheme and authority + let rest = strip_prefix_ignore_case(path, "http://") + .or_else(|| strip_prefix_ignore_case(path, "https://")) + .ok_or(StatusCode::BadRequest)?; + match rest.find('/') { + Some(slash) => &rest[slash..], + None => "/", + } + }; + // `..` can't be resolved without a stack of segments, refuse it rather + // than hand a handler a path that climbs out of where it points + if path.split('/').any(|segment| segment == "..") { + return Err(StatusCode::BadRequest); + } + Ok((path, query)) +} + +fn strip_prefix_ignore_case<'a>(value: &'a str, prefix: &str) -> Option<&'a str> { + let head = value.get(..prefix.len())?; + if head.eq_ignore_ascii_case(prefix) { + Some(&value[prefix.len()..]) + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_a_request() { + let req = Request::parse(b"POST /a/./b?x=1&y HTTP/1.1\r\nHost: h\r\n\r\nbody").unwrap(); + assert_eq!(req.method, Method::Post); + assert_eq!(req.target, "/a/./b?x=1&y"); + assert_eq!(req.path, "/a/./b"); + assert_eq!(req.query, "x=1&y"); + assert_eq!(req.body, b"body"); + + let mut segments = req.segments(); + assert_eq!(segments.next(), Some("a")); + assert_eq!(segments.next(), Some("b")); + assert_eq!(segments.next(), None); + + let mut pairs = req.query_pairs(); + assert_eq!(pairs.next(), Some(("x", "1"))); + assert_eq!(pairs.next(), Some(("y", ""))); + assert_eq!(pairs.next(), None); + } + + #[test] + fn unknown_methods_are_kept() { + let req = Request::parse(b"PURGE / HTTP/1.0\r\n\r\n").unwrap(); + assert_eq!(req.method, Method::Other("PURGE")); + assert_eq!(req.method.as_str(), "PURGE"); + } + + #[test] + fn target_forms() { + let req = Request::parse(b"GET http://h/x?q HTTP/1.1\r\nHost: h\r\n\r\n").unwrap(); + assert_eq!((req.path, req.query), ("/x", "q")); + let req = Request::parse(b"GET HTTPS://h HTTP/1.1\r\nHost: h\r\n\r\n").unwrap(); + assert_eq!(req.path, "/"); + let req = Request::parse(b"OPTIONS * HTTP/1.1\r\nHost: h\r\n\r\n").unwrap(); + assert_eq!(req.path, "*"); + let req = Request::parse(b"CONNECT h:443 HTTP/1.1\r\nHost: h\r\n\r\n").unwrap(); + assert_eq!(req.path, "h:443"); + + let bad = Err(StatusCode::BadRequest); + assert_eq!(Request::parse(b"GET * HTTP/1.1\r\nHost: h\r\n\r\n"), bad); + assert_eq!(Request::parse(b"GET x HTTP/1.1\r\nHost: h\r\n\r\n"), bad); + assert_eq!( + Request::parse(b"GET /a/../.. HTTP/1.1\r\nHost: h\r\n\r\n"), + bad + ); + assert_eq!( + Request::parse(b"GET /\x7f HTTP/1.1\r\nHost: h\r\n\r\n"), + bad + ); + } + + #[test] + fn versions() { + assert_eq!( + Request::parse(b"GET / HTTP/2.0\r\n\r\n"), + Err(StatusCode::HttpVersionNotSupported) + ); + assert_eq!( + Request::parse(b"GET / HTDP/1.1\r\n\r\n"), + Err(StatusCode::BadRequest) + ); + assert_eq!( + Request::parse(b"GET / HTTP/1.10\r\n\r\n"), + Err(StatusCode::BadRequest) + ); + } + + #[test] + fn framing_rules() { + let bad = Err(StatusCode::BadRequest); + // HTTP/1.1 needs exactly one Host, HTTP/1.0 doesn't + assert_eq!(Request::parse(b"GET / HTTP/1.1\r\n\r\n"), bad); + assert_eq!( + Request::parse(b"GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n"), + bad + ); + assert!(Request::parse(b"GET / HTTP/1.0\r\n\r\n").is_ok()); + assert_eq!( + Request::parse(b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n"), + Err(StatusCode::NotImplemented) + ); + assert_eq!(Request::parse(b"G@T / HTTP/1.1\r\nHost: a\r\n\r\n"), bad); + } + + #[test] + fn keep_alive() { + let parse = |buf: &'static [u8]| Request::parse(buf).unwrap().keep_alive(); + assert!(parse(b"GET / HTTP/1.1\r\nHost: a\r\n\r\n")); + assert!(!parse( + b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n" + )); + assert!(!parse(b"GET / HTTP/1.0\r\n\r\n")); + assert!(parse(b"GET / HTTP/1.0\r\nConnection: Keep-Alive\r\n\r\n")); + } +} diff --git a/octane_http/src/lib.rs b/octane_http/src/lib.rs index 2d639dc..398b22f 100644 --- a/octane_http/src/lib.rs +++ b/octane_http/src/lib.rs @@ -1,5 +1,29 @@ +//! The HTTP/1.x protocol core of Octane. +//! +//! The crate is `#![no_std]` and never allocates: requests are parsed in +//! place out of a caller-provided buffer and responses are encoded into one. +//! Only the transport differs between platforms, so the same code serves on +//! Linux, WASI or a bare-metal TCP stack. See [`config`] for sizing the +//! buffers, [`server`] for the serve loop and [`server::Connection`] for the +//! transport trait. +//! +//! The `wasm` feature adds the `wasip3` module on `wasm32-wasip3`: a +//! server over `wasi:sockets` that creates the listener and serves each +//! connection as a concurrent task. +//! +//! The `std` feature (on by default) adds the tokio based +//! [`http1x::Http1xReader`] used by the `octane` server and +//! [`server::StdIo`], an adapter for any `std::io::Read + Write` stream. +#![no_std] + +#[cfg(any(feature = "std", test))] +extern crate std; + +#[cfg(all(feature = "wasm", target_os = "wasi", target_env = "p3"))] +extern crate alloc; + +use core::fmt; use octane_macros::status_codes; -use std::fmt; pub const SP: char = ' '; pub const HT: char = '\t'; @@ -8,8 +32,13 @@ pub const LF: char = '\n'; pub const CRLF: &[u8; 2] = b"\r\n"; pub const DOUBLE_CRLF: &[u8; 4] = b"\r\n\r\n"; +pub mod config; mod eater; pub mod http1x; +pub mod response; +pub mod server; +#[cfg(all(feature = "wasm", target_os = "wasi", target_env = "p3"))] +pub mod wasip3; /// Holds the http versions. You can match the /// variants by doing a comparison with the version /// in the request_line @@ -31,7 +60,7 @@ pub mod http1x; /// }), /// ); /// ``` -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum HttpVersion { #[doc(hidden)] Http11, @@ -45,8 +74,10 @@ pub enum HttpVersion { HttpInvalid, } -impl ToString for HttpVersion { - fn to_string(&self) -> std::string::String { +impl HttpVersion { + /// The version number as it appears after `HTTP/`, empty for + /// [`HttpVersion::HttpInvalid`] + pub fn as_str(&self) -> &'static str { match self { Self::Http11 => "1.1", Self::Http10 => "1.0", @@ -54,7 +85,12 @@ impl ToString for HttpVersion { Self::Http02 => "0.2", _ => "", } - .to_owned() + } +} + +impl fmt::Display for HttpVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) } } diff --git a/octane_http/src/response.rs b/octane_http/src/response.rs new file mode 100644 index 0000000..2cbf400 --- /dev/null +++ b/octane_http/src/response.rs @@ -0,0 +1,412 @@ +//! Encoding responses into a caller-provided buffer, without allocating. + +use crate::http1x::headers::{is_ctl, is_token}; +use crate::{HttpVersion, StatusCode, CRLF}; + +/// Why a response couldn't be encoded +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum EncodeError { + /// The output buffer can't hold the response head + BufferTooSmall, + /// A header name isn't a token, a value holds CR, LF or another control + /// character, or the header is one the server sets itself + InvalidHeader, + /// The status code isn't three digits, its reason phrase holds a control + /// character, or it is an informational (1xx) status + InvalidStatus, +} + +/// Writes a response head (status-line and headers) into `out`. +/// +/// Headers are written as they are added and the status-line is put in +/// front of them by [`finish`](Self::finish), so the status can be changed +/// up until then. +/// +/// ``` +/// use octane_http::response::ResponseHead; +/// use octane_http::{HttpVersion, StatusCode}; +/// +/// let mut out = [0u8; 128]; +/// let mut head = ResponseHead::new(&mut out, HttpVersion::Http11, StatusCode::Ok); +/// head.header("Content-Type", b"text/plain").unwrap(); +/// head.content_length(5).unwrap(); +/// let head = head.finish().unwrap(); +/// +/// assert_eq!(head, b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\n"); +/// ``` +pub struct ResponseHead<'o> { + out: &'o mut [u8], + len: usize, + version: HttpVersion, + status: StatusCode, +} + +impl<'o> ResponseHead<'o> { + pub fn new(out: &'o mut [u8], version: HttpVersion, status: StatusCode) -> Self { + Self { + out, + len: 0, + version, + status, + } + } + + pub fn set_status(&mut self, status: StatusCode) { + self.status = status; + } + + pub fn status(&self) -> StatusCode { + self.status + } + + /// Append a header. `value` may not contain CR, LF or any control + /// character other than HTAB, so it can't inject other headers. + pub fn header(&mut self, name: &str, value: &[u8]) -> Result<&mut Self, EncodeError> { + if !is_token(name.as_bytes()) || value.iter().any(|&b| is_ctl(b) && b != b'\t') { + return Err(EncodeError::InvalidHeader); + } + self.push_header(name.as_bytes(), value)?; + Ok(self) + } + + /// Append a `Content-Length` header + pub fn content_length(&mut self, len: usize) -> Result<&mut Self, EncodeError> { + let mut digits = [0u8; 20]; + let digits = format_decimal(len, &mut digits); + self.push_header(b"Content-Length", digits)?; + Ok(self) + } + + /// Put the status-line in front of the headers and terminate the head, + /// returning the encoded bytes + pub fn finish(self) -> Result<&'o [u8], EncodeError> { + let (code, reason) = self.status.fetch(); + if !(200..=999).contains(&code) || reason.bytes().any(is_ctl) { + return Err(EncodeError::InvalidStatus); + } + let version: &[u8] = match self.version { + HttpVersion::Http10 => b"HTTP/1.0 ", + _ => b"HTTP/1.1 ", + }; + let mut digits = [0u8; 20]; + let code = format_decimal(code as usize, &mut digits); + let line_len = version.len() + code.len() + 1 + reason.len() + CRLF.len(); + let total = line_len + self.len + CRLF.len(); + if total > self.out.len() { + return Err(EncodeError::BufferTooSmall); + } + + self.out.copy_within(..self.len, line_len); + let mut at = 0; + for part in [version, code, b" ", reason.as_bytes(), CRLF] { + self.out[at..at + part.len()].copy_from_slice(part); + at += part.len(); + } + self.out[total - CRLF.len()..total].copy_from_slice(CRLF); + Ok(&self.out[..total]) + } + + fn push_header(&mut self, name: &[u8], value: &[u8]) -> Result<(), EncodeError> { + let len = name.len() + 2 + value.len() + CRLF.len(); + let end = self + .len + .checked_add(len) + .ok_or(EncodeError::BufferTooSmall)?; + let dest = self + .out + .get_mut(self.len..end) + .ok_or(EncodeError::BufferTooSmall)?; + let mut at = 0; + for part in [name, b": ", value, CRLF] { + dest[at..at + part.len()].copy_from_slice(part); + at += part.len(); + } + self.len = end; + Ok(()) + } +} + +/// A response being built by a handler in the [`server`](crate::server) +/// loop. The head goes into the output buffer, the body is only borrowed and +/// written straight to the connection afterwards, so it can be a `'static` +/// slice or a part of the request without being copied. +/// +/// Setters chain and never fail; the first invalid header is remembered and +/// the response is replaced by a `500` when it's sent. `Content-Length` and +/// `Connection` are set by the server, the handler may not set them. +pub struct Response<'o, 'b> { + head: ResponseHead<'o>, + body: &'b [u8], + close: bool, + error: Option, +} + +/// An encoded response, ready to be written +#[derive(Debug, PartialEq, Eq)] +pub struct Encoded<'o, 'b> { + pub head: &'o [u8], + pub body: &'b [u8], + /// Whether the connection has to be closed after this response + pub close: bool, +} + +impl<'o, 'b> Response<'o, 'b> { + /// A `200 OK` with an empty body + pub fn new(out: &'o mut [u8], version: HttpVersion) -> Self { + Self { + head: ResponseHead::new(out, version, StatusCode::Ok), + body: b"", + close: false, + error: None, + } + } + + pub fn status(&mut self, status: StatusCode) -> &mut Self { + self.head.set_status(status); + self + } + + pub fn header(&mut self, name: &str, value: &[u8]) -> &mut Self { + if self.error.is_some() { + return self; + } + let reserved = ["content-length", "transfer-encoding", "connection"] + .iter() + .any(|reserved| name.eq_ignore_ascii_case(reserved)); + let result = if reserved { + Err(EncodeError::InvalidHeader) + } else { + self.head.header(name, value).map(|_| ()) + }; + self.error = result.err(); + self + } + + pub fn body(&mut self, body: &'b [u8]) -> &mut Self { + self.body = body; + self + } + + /// Close the connection after this response + pub fn close(&mut self) -> &mut Self { + self.close = true; + self + } + + /// Add the framing headers and encode the head. + /// + /// `head_request` drops the body (a `HEAD` response only describes it) + /// and `keep_alive` is whether the request asked for a persistent + /// connection. No body or `Content-Length` is sent for `204` and `304`. + pub fn finish( + self, + head_request: bool, + keep_alive: bool, + ) -> Result, EncodeError> { + let body = self.body; + let body_len = body.len(); + let (head, close, bodiless) = self.encode(body_len, keep_alive)?; + Ok(Encoded { + head, + body: if head_request || bodiless { b"" } else { body }, + close, + }) + } + + /// Encode the head of a response whose body of `body_len` bytes is + /// written separately afterwards, ignoring any body set with + /// [`body`](Self::body). The caller has to write exactly `body_len` + /// bytes, unless the request was `HEAD` or the status is `204` or `304` + /// (the returned body is empty then too). + pub fn finish_streamed( + self, + body_len: usize, + keep_alive: bool, + ) -> Result, EncodeError> { + let (head, close, _) = self.encode(body_len, keep_alive)?; + Ok(Encoded { + head, + body: b"", + close, + }) + } + + /// Add the framing headers and encode the head, returning it with + /// whether to close and whether the status forbids a body + fn encode( + mut self, + body_len: usize, + keep_alive: bool, + ) -> Result<(&'o [u8], bool, bool), EncodeError> { + if let Some(error) = self.error { + return Err(error); + } + let close = self.close || !keep_alive; + let bodiless = self.is_bodiless(); + if !bodiless { + self.head.content_length(body_len)?; + } + match (self.head.version, close) { + (HttpVersion::Http10, false) => self.head.push_header(b"Connection", b"keep-alive")?, + (HttpVersion::Http10, true) => {} + (_, true) => self.head.push_header(b"Connection", b"close")?, + (_, false) => {} + } + Ok((self.head.finish()?, close, bodiless)) + } + + /// Whether the status forbids a body, so none is sent + pub fn is_bodiless(&self) -> bool { + matches!( + self.head.status(), + StatusCode::NoContent | StatusCode::NotModified + ) + } +} + +/// Encode a bodiless error response that closes the connection +pub fn error_response(out: &mut [u8], status: StatusCode) -> Result<&[u8], EncodeError> { + let mut head = ResponseHead::new(out, HttpVersion::Http11, status); + head.content_length(0)?; + head.push_header(b"Connection", b"close")?; + head.finish() +} + +/// Write `n` in decimal at the end of `buf`, returning the digits +fn format_decimal(mut n: usize, buf: &mut [u8; 20]) -> &[u8] { + let mut start = buf.len(); + loop { + start -= 1; + buf[start] = b'0' + (n % 10) as u8; + n /= 10; + if n == 0 { + break; + } + } + &buf[start..] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encodes_a_head() { + let mut out = [0u8; 128]; + let mut head = ResponseHead::new(&mut out, HttpVersion::Http10, StatusCode::Ok); + head.header("X-A", b"1").unwrap(); + head.set_status(StatusCode::NotFound); + assert_eq!( + head.finish().unwrap(), + b"HTTP/1.0 404 Not Found\r\nX-A: 1\r\n\r\n" + ); + } + + #[test] + fn refuses_injection_and_bad_statuses() { + let mut out = [0u8; 128]; + let mut head = ResponseHead::new(&mut out, HttpVersion::Http11, StatusCode::Ok); + assert_eq!( + head.header("X", b"a\r\nSet-Cookie: x").err(), + Some(EncodeError::InvalidHeader) + ); + assert_eq!( + head.header("X Y", b"a").err(), + Some(EncodeError::InvalidHeader) + ); + assert!(head.header("X", b"a\tb").is_ok()); + + for status in [ + StatusCode::Continue, + StatusCode::Other(42, "Short"), + StatusCode::Other(299, "Bad\r\nReason"), + ] { + let mut out = [0u8; 128]; + let head = ResponseHead::new(&mut out, HttpVersion::Http11, status); + assert_eq!(head.finish().err(), Some(EncodeError::InvalidStatus)); + } + } + + #[test] + fn reports_a_full_buffer() { + let mut out = [0u8; 16]; + let mut head = ResponseHead::new(&mut out, HttpVersion::Http11, StatusCode::Ok); + head.header("X", b"1").unwrap(); + assert_eq!(head.finish().err(), Some(EncodeError::BufferTooSmall)); + + let mut out = [0u8; 4]; + let mut head = ResponseHead::new(&mut out, HttpVersion::Http11, StatusCode::Ok); + assert_eq!( + head.header("Long", b"value").err(), + Some(EncodeError::BufferTooSmall) + ); + } + + #[test] + fn response_framing() { + let mut out = [0u8; 256]; + let mut res = Response::new(&mut out, HttpVersion::Http11); + res.header("Content-Type", b"text/plain").body(b"hello"); + let encoded = res.finish(false, true).unwrap(); + assert_eq!( + encoded.head, + b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\n" + ); + assert_eq!((encoded.body, encoded.close), (&b"hello"[..], false)); + + // HEAD keeps the length but drops the body, HTTP/1.1 close is explicit + let mut out = [0u8; 256]; + let mut res = Response::new(&mut out, HttpVersion::Http11); + res.body(b"hello"); + let encoded = res.finish(true, false).unwrap(); + assert_eq!( + encoded.head, + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\n" + ); + assert_eq!((encoded.body, encoded.close), (&b""[..], true)); + + // HTTP/1.0 keep-alive is explicit, 204 has no length + let mut out = [0u8; 256]; + let mut res = Response::new(&mut out, HttpVersion::Http10); + res.status(StatusCode::NoContent).body(b"ignored"); + let encoded = res.finish(false, true).unwrap(); + assert_eq!( + encoded.head, + b"HTTP/1.0 204 No Content\r\nConnection: keep-alive\r\n\r\n" + ); + assert_eq!(encoded.body, b""); + } + + #[test] + fn streamed_heads_declare_the_length() { + let mut out = [0u8; 256]; + let mut res = Response::new(&mut out, HttpVersion::Http11); + res.header("X", b"1").body(b"ignored"); + let encoded = res.finish_streamed(1000, false).unwrap(); + assert_eq!( + encoded.head, + b"HTTP/1.1 200 OK\r\nX: 1\r\nContent-Length: 1000\r\nConnection: close\r\n\r\n" + ); + assert_eq!((encoded.body, encoded.close), (&b""[..], true)); + } + + #[test] + fn handlers_cannot_set_framing_headers() { + let mut out = [0u8; 256]; + let mut res = Response::new(&mut out, HttpVersion::Http11); + res.header("content-length", b"1").header("X-Ok", b"1"); + assert_eq!( + res.finish(false, true).err(), + Some(EncodeError::InvalidHeader) + ); + } + + #[test] + fn error_responses() { + let mut out = [0u8; 128]; + assert_eq!( + error_response(&mut out, StatusCode::BadRequest).unwrap(), + b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + ); + } +} diff --git a/octane_http/src/server.rs b/octane_http/src/server.rs new file mode 100644 index 0000000..fdbeba6 --- /dev/null +++ b/octane_http/src/server.rs @@ -0,0 +1,439 @@ +//! A transport-agnostic HTTP/1.x server loop that never allocates. +//! +//! The loop only needs something that can read and write bytes, a +//! [`Connection`]. Accepting connections is left to the platform: a Linux +//! socket, a WASI socket, a smoltcp/embassy TCP socket or an in-memory pipe +//! all work the same way. +//! +//! ``` +//! use octane_http::config::Config; +//! use octane_http::http1x::Method; +//! use octane_http::server::Connection; +//! use octane_http::StatusCode; +//! +//! // A connection that replays a request and records the response +//! struct Pipe<'a> { +//! input: &'a [u8], +//! output: [u8; 256], +//! written: usize, +//! } +//! +//! impl Connection for Pipe<'_> { +//! type Error = (); +//! +//! fn read(&mut self, buf: &mut [u8]) -> Result { +//! let n = self.input.len().min(buf.len()); +//! buf[..n].copy_from_slice(&self.input[..n]); +//! self.input = &self.input[n..]; +//! Ok(n) +//! } +//! +//! fn write_all(&mut self, buf: &[u8]) -> Result<(), ()> { +//! self.output[self.written..self.written + buf.len()].copy_from_slice(buf); +//! self.written += buf.len(); +//! Ok(()) +//! } +//! } +//! +//! let mut conn = Pipe { +//! input: b"GET /hello HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n", +//! output: [0; 256], +//! written: 0, +//! }; +//! // the buffer sizes have to be chosen, there is no default without `std` +//! let config = Config::<1024, 256>::new(); +//! +//! config.serve(&mut conn, |req, res| { +//! match (req.method, req.path) { +//! (Method::Get, "/hello") => res.header("Content-Type", b"text/plain").body(b"Hello from no_std!"), +//! _ => res.status(StatusCode::NotFound).body(b"Not Found"), +//! }; +//! }) +//! .unwrap(); +//! +//! assert!(conn.output[..conn.written].ends_with(b"\r\n\r\nHello from no_std!")); +//! ``` + +use crate::config::{Buffers, Config}; +use crate::http1x::{Method, Request, RequestBuffer, Status}; +use crate::response::{error_response, Response}; +use crate::StatusCode; + +/// A bidirectional byte stream, typically one accepted TCP connection +pub trait Connection { + type Error; + + /// Read into `buf`, returning how many bytes were read. `0` means the + /// peer closed its side of the connection. + fn read(&mut self, buf: &mut [u8]) -> Result; + + /// Write all of `buf` + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error>; + + /// Flush anything the transport buffered, called after every response + fn flush(&mut self) -> Result<(), Self::Error> { + Ok(()) + } +} + +impl Connection for &mut C { + type Error = C::Error; + + fn read(&mut self, buf: &mut [u8]) -> Result { + (**self).read(buf) + } + + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + (**self).write_all(buf) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + (**self).flush() + } +} + +/// Adapts any `std::io::Read + Write` stream, like a `std::net::TcpStream` +/// (also on WASI), into a [`Connection`] +#[cfg(feature = "std")] +#[derive(Debug)] +pub struct StdIo(pub T); + +#[cfg(feature = "std")] +impl Connection for StdIo { + type Error = std::io::Error; + + fn read(&mut self, buf: &mut [u8]) -> Result { + loop { + match self.0.read(buf) { + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + result => return result, + } + } + } + + fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> { + self.0.write_all(buf) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + self.0.flush() + } +} + +impl Config { + /// Serve requests on `conn` until either side closes it, with the + /// buffers on the stack ([`Config::stack_size`] bytes). + /// + /// Requests that don't parse or don't fit in `INPUT` get an error + /// response and the connection is closed: a malformed request gets a + /// `400`, as does an HTTP/1.1 request without exactly one `Host` header. + /// Pipelined requests are answered in order. + /// + /// Timeouts are the transport's job: a `read` that never returns stalls + /// this connection. + pub fn serve(&self, conn: C, handler: H) -> Result<(), C::Error> + where + C: Connection, + H: for<'r, 'o> FnMut(&Request<'r>, &mut Response<'o, 'r>), + { + let mut buffers = self.buffers(); + self.serve_with(conn, &mut buffers, handler) + } + + /// Like [`Config::serve`], with buffers that live elsewhere, like a + /// `static` on a target whose stack is too small to hold them + pub fn serve_with( + &self, + conn: C, + buffers: &mut Buffers, + handler: H, + ) -> Result<(), C::Error> + where + C: Connection, + H: for<'r, 'o> FnMut(&Request<'r>, &mut Response<'o, 'r>), + { + serve_connection(conn, &mut buffers.input, &mut buffers.output, handler) + } +} + +fn serve_connection( + mut conn: C, + input: &mut [u8], + output: &mut [u8], + mut handler: H, +) -> Result<(), C::Error> +where + C: Connection, + H: for<'r, 'o> FnMut(&Request<'r>, &mut Response<'o, 'r>), +{ + let mut requests = RequestBuffer::new(input); + loop { + let close = match requests.poll() { + Ok(Status::Complete(request)) => respond(&mut conn, output, &request, &mut handler)?, + Ok(Status::Partial) => { + let n = conn.read(requests.unfilled())?; + if n == 0 { + // the peer is done, whether or not it was mid-request + return Ok(()); + } + requests.advance(n); + continue; + } + Err(status) => { + send_error(&mut conn, output, status)?; + true + } + }; + if close { + return Ok(()); + } + requests.consume(); + } +} + +/// Run the handler and write its response, returning whether to close +fn respond<'r, C, H>( + conn: &mut C, + output: &mut [u8], + request: &Request<'r>, + handler: &mut H, +) -> Result +where + C: Connection, + H: for<'o> FnMut(&Request<'r>, &mut Response<'o, 'r>), +{ + let mut response = Response::new(output, request.version); + handler(request, &mut response); + match response.finish(request.method == Method::Head, request.keep_alive()) { + Ok(encoded) => { + conn.write_all(encoded.head)?; + if !encoded.body.is_empty() { + conn.write_all(encoded.body)?; + } + conn.flush()?; + Ok(encoded.close) + } + Err(_) => { + send_error(conn, output, StatusCode::InternalServerError)?; + Ok(true) + } + } +} + +fn send_error( + conn: &mut C, + output: &mut [u8], + status: StatusCode, +) -> Result<(), C::Error> { + // an output buffer too small for even this gets the connection closed + // without a response + if let Ok(head) = error_response(output, status) { + conn.write_all(head)?; + conn.flush()?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Feeds `input` in `chunk` sized reads and collects what is written + struct Pipe<'a> { + input: &'a [u8], + chunk: usize, + output: [u8; 2048], + written: usize, + } + + impl<'a> Pipe<'a> { + fn new(input: &'a [u8], chunk: usize) -> Self { + Self { + input, + chunk, + output: [0; 2048], + written: 0, + } + } + + fn written(&self) -> &[u8] { + &self.output[..self.written] + } + } + + impl Connection for Pipe<'_> { + type Error = (); + + fn read(&mut self, buf: &mut [u8]) -> Result { + let n = self.input.len().min(buf.len()).min(self.chunk); + buf[..n].copy_from_slice(&self.input[..n]); + self.input = &self.input[n..]; + Ok(n) + } + + fn write_all(&mut self, buf: &[u8]) -> Result<(), ()> { + let end = self.written + buf.len(); + self.output + .get_mut(self.written..end) + .ok_or(())? + .copy_from_slice(buf); + self.written = end; + Ok(()) + } + } + + fn serve(input: &[u8], chunk: usize, input_len: usize) -> Pipe<'_> { + let mut conn = Pipe::new(input, chunk); + let mut input = [0u8; 512]; + let mut output = [0u8; 256]; + serve_connection( + &mut conn, + &mut input[..input_len], + &mut output, + |req, res| { + match (req.method, req.path) { + (Method::Get | Method::Head, "/") => res.body(b"root"), + (Method::Post, "/echo") => res.body(req.body), + (Method::Get, "/bad") => res.header("Bad Name", b"x"), + _ => res.status(StatusCode::NotFound), + }; + }, + ) + .unwrap(); + conn + } + + #[test] + fn serves_pipelined_keep_alive_requests() { + let input = b"GET / HTTP/1.1\r\nHost: a\r\n\r\n\ +POST /echo HTTP/1.1\r\nHost: a\r\nContent-Length: 4\r\n\r\nping\ +HEAD / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n\ +GET /never HTTP/1.1\r\nHost: a\r\n\r\n"; + let expected: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nroot\ +HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\nping\ +HTTP/1.1 200 OK\r\nContent-Length: 4\r\nConnection: close\r\n\r\n"; + for chunk in [1, 7, usize::MAX] { + assert_eq!( + serve(input, chunk, 512).written(), + expected, + "chunk {}", + chunk + ); + } + } + + #[test] + fn errors_close_the_connection() { + let bad_request = + b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; + assert_eq!( + serve( + b"GET / HTTP/1.1\r\n\r\nGET / HTTP/1.1\r\nHost: a\r\n\r\n", + 64, + 512 + ) + .written(), + &bad_request[..] + ); + assert_eq!( + serve(b"GET /bad HTTP/1.1\r\nHost: a\r\n\r\n", 64, 512).written(), + &b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"[..] + ); + assert_eq!( + serve( + b"POST /echo HTTP/1.1\r\nHost: a\r\nContent-Length: 100\r\n\r\n", + 64, + 64 + ) + .written(), + &b"HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"[..] + ); + } + + #[test] + fn http11_needs_exactly_one_host() { + let bad_request: &[u8] = + b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; + for request in [ + &b"GET / HTTP/1.1\r\n\r\n"[..], + b"GET / HTTP/1.1\r\nHost: a\r\nHost: a\r\n\r\n", + b"GET / HTTP/1.1\r\nHost: a\r\nX: 1\r\nhOST: b\r\n\r\n", + ] { + assert_eq!(serve(request, 64, 512).written(), bad_request); + } + // HTTP/1.0 predates Host + assert!(serve(b"GET / HTTP/1.0\r\n\r\n", 64, 512) + .written() + .starts_with(b"HTTP/1.0 200 OK")); + } + + #[test] + fn config_bounds_requests_by_its_input_size() { + const CONFIG: Config<128, 128> = Config::new(); + let mut buffers = CONFIG.buffers(); + let handler = |req: &Request<'_>, res: &mut Response<'_, '_>| { + res.body(if req.body.is_empty() { + b"empty" + } else { + b"body" + }); + }; + + let mut conn = Pipe::new( + b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n", + 64, + ); + CONFIG.serve_with(&mut conn, &mut buffers, handler).unwrap(); + assert!(conn.written().ends_with(b"\r\n\r\nempty")); + + // 47 bytes of head leave 81 for the body + let mut conn = Pipe::new( + b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 82\r\n\r\n", + 64, + ); + CONFIG.serve(&mut conn, handler).unwrap(); + assert!(conn + .written() + .starts_with(b"HTTP/1.1 413 Payload Too Large")); + } + + #[test] + fn http10_closes_by_default() { + assert_eq!( + serve(b"GET / HTTP/1.0\r\n\r\nGET / HTTP/1.0\r\n\r\n", 64, 512).written(), + &b"HTTP/1.0 200 OK\r\nContent-Length: 4\r\n\r\nroot"[..] + ); + } + + #[test] + fn peer_closing_mid_request_ends_quietly() { + assert_eq!(serve(b"GET / HTTP/1.1\r\nHo", 64, 512).written(), b""); + } + + #[cfg(feature = "std")] + #[test] + fn serves_a_std_tcp_stream() { + use std::io::{Read, Write}; + use std::net::{TcpListener, TcpStream}; + + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + let server = std::thread::spawn(move || { + let (stream, _) = listener.accept().unwrap(); + crate::config::DefaultConfig::new() + .serve(StdIo(stream), |_, res| { + res.body(b"over tcp"); + }) + .unwrap(); + }); + + let mut client = TcpStream::connect(addr).unwrap(); + client + .write_all(b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n") + .unwrap(); + let mut response = std::vec::Vec::new(); + client.read_to_end(&mut response).unwrap(); + server.join().unwrap(); + assert!(response.ends_with(b"\r\n\r\nover tcp")); + } +} diff --git a/octane_http/src/wasip3.rs b/octane_http/src/wasip3.rs new file mode 100644 index 0000000..3a60e67 --- /dev/null +++ b/octane_http/src/wasip3.rs @@ -0,0 +1,366 @@ +//! Serving HTTP/1.x from a WASIp3 component over `wasi:sockets`. +//! +//! Available with the `wasm` feature when building for `wasm32-wasip3`. +//! [`serve`] creates the listening socket and runs every accepted connection +//! as its own task, so connections are served concurrently on the +//! component's single thread: while one waits on its socket, a timer or a +//! streamed body, the others make progress. +//! +//! Handlers are async and answer through a [`Responder`], either with a +//! whole body ([`Responder::send`]) or by streaming one +//! ([`Responder::stream`]). +//! +//! ```ignore +//! use octane_http::config::Config; +//! use octane_http::http1x::Request; +//! use octane_http::wasip3::{serve, Responder}; +//! use octane_http::StatusCode; +//! +//! const CONFIG: Config<{ 16 * 1024 }, 1024> = Config::new(); +//! +//! async fn handle(req: &Request<'_>, res: &mut Responder<'_>) { +//! let _ = res.send(StatusCode::Ok, &[("Content-Type", "text/plain")], b"hi").await; +//! } +//! +//! // inside the component's async `run` export +//! serve(CONFIG, "127.0.0.1:8080".parse().unwrap(), |addr| println!("on {}", addr), handle).await +//! ``` +//! +//! Parsing stays allocation-free. Each connection's [`Buffers`] are boxed, +//! since a task outlives the stack frame that spawned it, and the stream ABI +//! takes ownership of what is written, so writes copy into a `Vec`. + +use crate::config::{Buffers, Config}; +use crate::http1x::{Method, Request, RequestBuffer, Status}; +use crate::response::{error_response, EncodeError, Response}; +use crate::{HttpVersion, StatusCode}; +use ::wasip3::sockets::types::{ + IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, TcpSocket, +}; +use ::wasip3::wit_bindgen::rt::async_support::{StreamResult, StreamWriter}; +use ::wasip3::{spawn_local, wit_stream}; +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::vec::Vec; +use core::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}; + +/// The `wasi:sockets` error code +pub use ::wasip3::sockets::types::ErrorCode; + +/// Largest single read off a connection's stream +const READ_CHUNK: usize = 4096; + +/// Listen on `addr` and serve every connection until the listener closes. +/// +/// `on_listen` is called with the bound address once the socket listens, +/// which tells the port when `addr` asks for port 0. Errors creating, +/// binding or listening on the socket are returned; a failing connection +/// only ends that connection. +pub async fn serve( + config: Config, + addr: SocketAddr, + on_listen: L, + handler: H, +) -> Result<(), ErrorCode> +where + L: FnOnce(SocketAddr), + H: AsyncFn(&Request<'_>, &mut Responder<'_>) + 'static, +{ + let family = match addr { + SocketAddr::V4(_) => IpAddressFamily::Ipv4, + SocketAddr::V6(_) => IpAddressFamily::Ipv6, + }; + let listener = TcpSocket::create(family)?; + listener.bind(to_wasi(addr))?; + let mut connections = listener.listen()?; + on_listen(from_wasi(listener.get_local_address()?)); + + let handler = Rc::new(handler); + while let Some(socket) = connections.next().await { + let handler = Rc::clone(&handler); + spawn_local(async move { + // the error only concerns this connection + let _ = serve_connection(config, socket, &*handler).await; + }); + } + Ok(()) +} + +/// Serve one accepted connection until either side closes it, for callers +/// that set up the listener themselves +pub async fn serve_connection( + config: Config, + socket: TcpSocket, + handler: &H, +) -> Result<(), ErrorCode> +where + H: AsyncFn(&Request<'_>, &mut Responder<'_>), +{ + let (mut incoming, _receive_done) = socket.receive(); + let (mut outgoing, outgoing_reader) = wit_stream::new::(); + // the host drains the stream into the socket while this is pending + let send_done = socket.send(outgoing_reader); + + let mut buffers = Box::new(config.buffers()); + let Buffers { input, output } = &mut *buffers; + let mut requests = RequestBuffer::new(&mut input[..]); + + loop { + let close = match requests.poll() { + Ok(Status::Complete(request)) => { + let mut responder = Responder::new(&mut output[..], &mut outgoing, &request); + handler(&request, &mut responder).await; + responder.finish().await + } + Ok(Status::Partial) => { + let spare = requests.unfilled(); + let want = spare.len().min(READ_CHUNK); + let (result, bytes) = incoming.read(Vec::with_capacity(want)).await; + match result { + StreamResult::Complete(_) if !bytes.is_empty() => { + spare[..bytes.len()].copy_from_slice(&bytes); + requests.advance(bytes.len()); + continue; + } + // a zero length read can't make progress, treat it as EOF + _ => true, + } + } + Err(status) => { + if let Ok(head) = error_response(&mut output[..], status) { + write(&mut outgoing, head).await; + } + true + } + }; + if close { + break; + } + requests.consume(); + } + + // closing the writer ends the stream, then wait for the host to flush it + drop(outgoing); + send_done.await +} + +/// Why a response couldn't be sent +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum SendError { + /// A response was already sent for this request + AlreadySent, + /// The head couldn't be encoded, a `500` was sent instead + Encode(EncodeError), + /// More body bytes were written than the declared length + BodyTooLong, + /// The peer closed the connection + Closed, +} + +/// Answers one request. A handler that doesn't send anything gets a `404` +/// sent for it. +pub struct Responder<'c> { + output: &'c mut [u8], + outgoing: &'c mut StreamWriter, + version: HttpVersion, + head_request: bool, + keep_alive: bool, + sent: bool, + close: bool, + /// Declared body bytes a streamed response still owes + owed: usize, +} + +impl<'c> Responder<'c> { + fn new( + output: &'c mut [u8], + outgoing: &'c mut StreamWriter, + request: &Request<'_>, + ) -> Self { + Self { + output, + outgoing, + version: request.version, + head_request: request.method == Method::Head, + keep_alive: request.keep_alive(), + sent: false, + close: false, + owed: 0, + } + } + + /// Send a response with the whole `body`. `Content-Length` and + /// `Connection` are set by the server. + pub async fn send( + &mut self, + status: StatusCode, + headers: &[(&str, &str)], + body: &[u8], + ) -> Result<(), SendError> { + self.claim()?; + let mut response = build(self.output, self.version, status, headers); + response.body(body); + let encoded = match response.finish(self.head_request, self.keep_alive) { + Ok(encoded) => encoded, + Err(error) => return Err(self.fail(error).await), + }; + self.close = encoded.close; + if !(write(self.outgoing, encoded.head).await && write(self.outgoing, encoded.body).await) { + self.close = true; + return Err(SendError::Closed); + } + Ok(()) + } + + /// Send the head of a response with a `body_len` bytes long body and + /// return a writer for the body. Writing fewer bytes than declared + /// closes the connection once the handler returns. + pub async fn stream( + &mut self, + status: StatusCode, + headers: &[(&str, &str)], + body_len: usize, + ) -> Result, SendError> { + self.claim()?; + let response = build(self.output, self.version, status, headers); + // HEAD responses and 204/304 describe a body without sending it + let discard = self.head_request || response.is_bodiless(); + let encoded = match response.finish_streamed(body_len, self.keep_alive) { + Ok(encoded) => encoded, + Err(error) => return Err(self.fail(error).await), + }; + self.close = encoded.close; + if !write(self.outgoing, encoded.head).await { + self.close = true; + return Err(SendError::Closed); + } + self.owed = if discard { 0 } else { body_len }; + Ok(BodyWriter { + responder: self, + remaining: body_len, + discard, + }) + } + + /// Whether a response was sent + pub fn is_sent(&self) -> bool { + self.sent + } + + /// Mark the request as answered, only once + fn claim(&mut self) -> Result<(), SendError> { + if self.sent { + return Err(SendError::AlreadySent); + } + self.sent = true; + Ok(()) + } + + /// Answer an unencodable response with a `500` and close + async fn fail(&mut self, error: EncodeError) -> SendError { + self.close = true; + if let Ok(head) = error_response(self.output, StatusCode::InternalServerError) { + write(self.outgoing, head).await; + } + SendError::Encode(error) + } + + /// Send a `404` if nothing was sent, returning whether to close + async fn finish(mut self) -> bool { + if !self.sent { + let _ = self.send(StatusCode::NotFound, &[], b"").await; + } + self.close || self.owed > 0 + } +} + +/// Writes the body of a [`Responder::stream`] response +pub struct BodyWriter<'r, 'c> { + responder: &'r mut Responder<'c>, + remaining: usize, + discard: bool, +} + +impl BodyWriter<'_, '_> { + /// Write the next part of the body + pub async fn write(&mut self, bytes: &[u8]) -> Result<(), SendError> { + if bytes.len() > self.remaining { + return Err(SendError::BodyTooLong); + } + self.remaining -= bytes.len(); + if self.discard { + return Ok(()); + } + if !write(self.responder.outgoing, bytes).await { + self.responder.close = true; + return Err(SendError::Closed); + } + self.responder.owed = self.remaining; + Ok(()) + } + + /// Body bytes still to write + pub fn remaining(&self) -> usize { + self.remaining + } +} + +/// A response head with the handler's status and headers. Invalid headers +/// surface as an error when it's encoded. +fn build<'o, 'b>( + output: &'o mut [u8], + version: HttpVersion, + status: StatusCode, + headers: &[(&str, &str)], +) -> Response<'o, 'b> { + let mut response = Response::new(output, version); + response.status(status); + for (name, value) in headers { + response.header(name, value.as_bytes()); + } + response +} + +/// Write all of `bytes`, false if the peer went away +async fn write(outgoing: &mut StreamWriter, bytes: &[u8]) -> bool { + bytes.is_empty() || outgoing.write_all(bytes.to_vec()).await.is_empty() +} + +fn to_wasi(addr: SocketAddr) -> IpSocketAddress { + match addr { + SocketAddr::V4(addr) => { + let [a, b, c, d] = addr.ip().octets(); + IpSocketAddress::Ipv4(Ipv4SocketAddress { + port: addr.port(), + address: (a, b, c, d), + }) + } + SocketAddr::V6(addr) => { + let [a, b, c, d, e, f, g, h] = addr.ip().segments(); + IpSocketAddress::Ipv6(Ipv6SocketAddress { + port: addr.port(), + flow_info: addr.flowinfo(), + address: (a, b, c, d, e, f, g, h), + scope_id: addr.scope_id(), + }) + } + } +} + +fn from_wasi(addr: IpSocketAddress) -> SocketAddr { + match addr { + IpSocketAddress::Ipv4(addr) => { + let (a, b, c, d) = addr.address; + SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(a, b, c, d), addr.port)) + } + IpSocketAddress::Ipv6(addr) => { + let (a, b, c, d, e, f, g, h) = addr.address; + SocketAddr::V6(SocketAddrV6::new( + Ipv6Addr::new(a, b, c, d, e, f, g, h), + addr.port, + addr.flow_info, + addr.scope_id, + )) + } + } +} diff --git a/src/error.rs b/src/error.rs index 714f980..76cb097 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,8 +3,6 @@ use crate::responder::Response; use crate::Octane; use octane_http::StatusCode; use std::error; -use std::fmt; -use std::fmt::{Display, Formatter}; use std::marker::Unpin; use tokio::io::AsyncWrite; @@ -21,9 +19,6 @@ pub struct Error { kind: StatusCode, } -#[derive(Debug, Clone, PartialEq, Eq)] -/// Custom error type for invalid paths -pub struct InvalidPathError; #[derive(Debug, Clone, PartialEq, Eq)] // Custom error type for invalid SSL certificates pub struct InvalidCertError; @@ -59,12 +54,3 @@ impl Error { Ok(()) } } - -impl Display for InvalidPathError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { - // TODO: make more informative - write!(f, "Invalid path error") - } -} - -impl error::Error for InvalidPathError {} diff --git a/src/http/http11.rs b/src/http/http11.rs index 55aee62..c8d95b8 100644 --- a/src/http/http11.rs +++ b/src/http/http11.rs @@ -2,7 +2,9 @@ use crate::http::{Http, KeepAliveState}; use octane_http::StatusCode; pub fn http11_check(validator: &mut Http) { - if validator.request.headers.get("host").is_none() { + // a missing Host and repeated ones (even with the same value) are both + // malformed (RFC 9112 section 3.2) + if validator.request.headers.host_count() != 1 { validator.set(StatusCode::BadRequest) } if let Some(connection_type) = validator.request.headers.get("connection") { diff --git a/src/lib.rs b/src/lib.rs index 7ba0b48..44f98ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,15 @@ #![allow(clippy::all)] #![allow(clippy::type_complexity)] #![forbid(unsafe_code)] +// wasm32-wasip3 has std, only tokio is missing there, so the router is +// available to the WASIp3 server too +#![cfg_attr( + not(any( + feature = "std", + all(feature = "wasm", target_os = "wasi", target_env = "p3") + )), + no_std +)] //! Octane is a web server that's modelled after express (a very //! popular and easy to use web framework) for rust. //! @@ -21,6 +30,8 @@ //! and then in your main.rs, //! //! ```no_run +//! # #[cfg(feature = "std")] +//! # mod example { //! use octane::prelude::*; //! use std::error::Error; //! @@ -39,6 +50,8 @@ //! println!("Server running on {}", port) //! }).await //! } +//! # } +//! # fn main() {} //! ``` //! and now you can see the page at [http://localhost:8080](http://localhost:8080). //! @@ -55,53 +68,118 @@ //! - `raw_headers`: To have access to original, un-normalized headers. //! - `rustls`: To use rustls for ssl. //! - `openSSL`: To use openssl for ssl. -//! - `default`: The default set includes faithful, query_strings, cookies, +//! - `std`: The tokio based server, everything except [`portable`]. All the +//! features above imply it. +//! - `default`: The default set includes std, faithful, query_strings, cookies, //! url_variables, raw_headers. //! +//! ## `no_std` +//! +//! With `default-features = false` the crate is `#![no_std]` and exposes only +//! [`portable`], the allocation-free HTTP/1.x core: a parser that borrows from +//! a caller-provided buffer, a response encoder and a serve loop that runs on +//! any transport implementing [`portable::server::Connection`]. That covers +//! WASI, bare-metal TCP stacks like smoltcp, or raw sockets without tokio. +//! +//! ```toml +//! octane = { version = "0.1.1", default-features = false } +//! ``` +//! +//! On `wasm32-wasip3` the `wasm` feature adds `octane::wasip3`: octane's +//! router with async handlers, filled through the same +//! [`Route`](router/trait.Route.html) trait as the tokio server, and a +//! `serve` that creates the listening socket over `wasi:sockets` and serves +//! every connection as a concurrent task. tokio isn't used there; add +//! `url_variables` for routes like `/users/:id`: +//! +//! ```toml +//! octane = { version = "0.1.1", default-features = false, features = ["wasm", "url_variables"] } +//! ``` +//! //! **Note**: If both `rustls` and `openSSL` features are enabled then //! octane will throw a `compile_error!` //! //! The rust code you would use to setup openssl will be same as rustls (not including certs and keys). //! If you want to switch between any of them, its just the matter of editing your cargo.toml //! to enable the feature. +#[cfg(feature = "std")] /// Configurations for Octane web server pub mod config; +#[cfg(any( + feature = "std", + all(feature = "wasm", target_os = "wasi", target_env = "p3") +))] pub(crate) mod constants; #[cfg(feature = "cookies")] /// Module for cookie parsing and handling pub mod cookie; +#[cfg(feature = "std")] pub(crate) mod error; +#[cfg(feature = "std")] pub(crate) mod file_handler; #[cfg(feature = "fuzzing")] #[doc(hidden)] pub mod fuzzing; +#[cfg(feature = "std")] pub(crate) mod http; -pub(crate) mod middlewares; +#[cfg(any( + feature = "std", + all(feature = "wasm", target_os = "wasi", target_env = "p3") +))] +pub(crate) mod method; +#[cfg(any( + feature = "std", + all(feature = "wasm", target_os = "wasi", target_env = "p3") +))] pub(crate) mod path; +#[cfg(feature = "std")] pub(crate) mod query; +#[cfg(feature = "std")] /// Request module contains the ongoing request and methods to read from it pub mod request; +#[cfg(feature = "std")] /// Responder module contains the response which will be sent pub mod responder; +#[cfg(feature = "std")] /// The router module has utilities to create routes and custom routers pub mod router; +#[cfg(any( + feature = "std", + all(feature = "wasm", target_os = "wasi", target_env = "p3") +))] +pub(crate) mod routes; +#[cfg(feature = "std")] pub(crate) mod server; +#[cfg(feature = "std")] /// Server struct that manages request/response and allows the routes to enter in pub use crate::server::Octane; +#[cfg(feature = "std")] pub(crate) mod server_builder; +#[cfg(feature = "std")] pub(crate) mod time; +#[cfg(feature = "std")] pub(crate) mod tls; +#[cfg(feature = "std")] pub(crate) mod util; // re exports #[cfg(feature = "extended_queries")] pub use crate::query::QueryValue; +/// The allocation-free HTTP/1.x core, available with and without `std`. +/// See the [`no_std`](index.html#no_std) section. +pub use octane_http as portable; +/// Serving from a WASIp3 component with octane's router, see the +/// [`no_std`](index.html#no_std) section. Needs the `wasm` feature and the +/// `wasm32-wasip3` target. +#[cfg(all(feature = "wasm", target_os = "wasi", target_env = "p3"))] +pub mod wasip3; pub use octane_http::StatusCode; /// Prelude here brings in scope, the [`Route`](router/trait.Route.html) and /// [`Config`](config/trait.Config.html) trait, [`Octane`](struct.Octane.html) main server /// and [`Router`](router/struct.Router.html) struct with the [`Flow`](router/enum.Flow.html) /// enum, and the [`route`](macro.route.html), [`route_next`](macro.route_next.html), /// [`route_stop`](macro.route_stop.html) macros +#[cfg(feature = "std")] pub mod prelude { pub use crate::config::Config; pub use crate::Octane; diff --git a/src/method.rs b/src/method.rs new file mode 100644 index 0000000..5f41452 --- /dev/null +++ b/src/method.rs @@ -0,0 +1,54 @@ +use octane_http::http1x::Method; + +/// Holds the type of request method, like GET, +/// POST etc. +#[derive(Debug, PartialEq, Eq, Clone, Hash, Copy)] +pub enum RequestMethod { + #[doc(hidden)] + Options, + #[doc(hidden)] + Get, + #[doc(hidden)] + Head, + #[doc(hidden)] + Post, + #[doc(hidden)] + Put, + #[doc(hidden)] + Delete, + #[doc(hidden)] + Trace, + #[doc(hidden)] + Connect, + #[doc(hidden)] + Patch, + #[doc(hidden)] + All, + #[doc(hidden)] + None, +} + +impl RequestMethod { + /// Return false if the RequestMethod is `None`, otherwise it's true + pub fn is_some(&self) -> bool { + !matches!(self, Self::None) + } +} + +impl From> for RequestMethod { + /// Methods without a variant of their own map to `None` + fn from(method: Method<'_>) -> Self { + match method { + Method::Get => Self::Get, + Method::Head => Self::Head, + Method::Post => Self::Post, + Method::Put => Self::Put, + Method::Delete => Self::Delete, + Method::Connect => Self::Connect, + Method::Options => Self::Options, + Method::Trace => Self::Trace, + Method::Patch => Self::Patch, + Method::Other(_) => Self::None, + } + } +} diff --git a/src/middlewares/mod.rs b/src/middlewares/mod.rs deleted file mode 100644 index cd2db5b..0000000 --- a/src/middlewares/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::router::Closure; - -pub struct Closures { - pub closure: Closure, - pub index: usize, -} diff --git a/src/path.rs b/src/path.rs index 4215af3..98e3431 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,12 +1,25 @@ -use crate::error::InvalidPathError; use crate::{default, deref}; use std::collections::{hash_map, HashMap}; use std::convert::TryFrom; +use std::error; use std::fmt; use std::iter::{FromIterator, Iterator, Map}; use std::path::PathBuf as StdPathBuf; use std::str::FromStr; +#[derive(Debug, Clone, PartialEq, Eq)] +/// Custom error type for invalid paths +pub struct InvalidPathError; + +impl fmt::Display for InvalidPathError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + // TODO: make more informative + write!(f, "Invalid path error") + } +} + +impl error::Error for InvalidPathError {} + // Custom PathBuf for managing custom url queries and variables, // Provides methods to match and check on urls #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -273,6 +286,8 @@ impl PathNode { } } +// nothing walks a trie by reference yet; `Router.paths` used to be public +#[allow(dead_code)] pub struct PathNodeIterator<'a, T> { stack: Vec>>, curvec: Option<&'a Vec>>, @@ -327,7 +342,8 @@ impl<'a, T> Iterator for PathNodeIterator<'a, T> { } impl PathNode { - pub fn iter(&self) -> PathNodeIterator { + #[allow(dead_code)] + pub fn iter(&self) -> PathNodeIterator<'_, T> { match self { PathNode::Node(n) => PathNodeIterator { stack: vec![n.values()], @@ -438,6 +454,8 @@ deref!(MatchedPath<'a, T>, T, data); deref!(PathBuf, Vec, chunks); // CTL from RFC 5234: %x00-1F / %x7F +// only the tokio request parser uses it +#[cfg_attr(not(feature = "std"), allow(dead_code))] pub fn is_ctl(c: char) -> bool { c <= '\x1f' || c == '\x7f' } diff --git a/src/request.rs b/src/request.rs index afbb065..371d546 100644 --- a/src/request.rs +++ b/src/request.rs @@ -17,40 +17,7 @@ use std::str; use std::string::ToString; use tokio::io::{AsyncRead, AsyncReadExt}; -/// Holds the type of request method, like GET, -/// POST etc. -#[derive(Debug, PartialEq, Eq, Clone, Hash, Copy)] -pub enum RequestMethod { - #[doc(hidden)] - Options, - #[doc(hidden)] - Get, - #[doc(hidden)] - Head, - #[doc(hidden)] - Post, - #[doc(hidden)] - Put, - #[doc(hidden)] - Delete, - #[doc(hidden)] - Trace, - #[doc(hidden)] - Connect, - #[doc(hidden)] - Patch, - #[doc(hidden)] - All, - #[doc(hidden)] - None, -} - -impl RequestMethod { - /// Return false if the RequestMethod is `None`, otherwise it's true - pub fn is_some(&self) -> bool { - !matches!(self, Self::None) - } -} +pub use crate::method::RequestMethod; /// The RequestLine struct represents the first /// line of the http request, which contains @@ -230,6 +197,9 @@ pub struct Headers { pub raw: Vec
, #[cfg(not(feature = "raw_headers"))] raw: PhantomData<()>, + // how many Host fields were sent, they are joined in `parsed` so a + // duplicate can't be told apart from a single comma separated value + host_count: usize, } impl Headers { @@ -242,8 +212,12 @@ impl Headers { let mut headers: HashMap = HashMap::new(); #[cfg(feature = "raw_headers")] let mut raw_headers: Vec
= Vec::new(); + let mut host_count = 0; for tok in toks { let parsed = Header::parse(str::from_utf8(tok).ok()?)?; + if parsed.name.eq_ignore_ascii_case("host") { + host_count += 1; + } #[cfg(feature = "raw_headers")] { let name = parsed.name.to_ascii_lowercase(); @@ -283,9 +257,16 @@ impl Headers { raw: raw_headers, #[cfg(not(feature = "raw_headers"))] raw: PhantomData, + host_count, }) } + /// How many `Host` fields the request had. HTTP/1.1 requests need + /// exactly one (RFC 9112 section 3.2) + pub(crate) fn host_count(&self) -> usize { + self.host_count + } + /// The body length declared by the `Content-Length` header, 0 if it /// isn't present (RFC 9112 section 6.3). Repeated identical values like /// `5, 5` are accepted. A value that isn't a decimal number or values @@ -326,8 +307,12 @@ impl Headers { let mut parsed: HashMap = HashMap::new(); #[cfg(feature = "raw_headers")] let mut raw: Vec
= Vec::new(); + let mut host_count = 0; for (name, value) in pairs { let lower = name.to_ascii_lowercase(); + if lower == "host" { + host_count += 1; + } let sep = if lower == "cookie" { "; " } else { ", " }; parsed .entry(lower) @@ -348,6 +333,7 @@ impl Headers { raw, #[cfg(not(feature = "raw_headers"))] raw: PhantomData, + host_count, } } } @@ -822,6 +808,14 @@ mod test { assert_eq!(req.value(), "a\tb"); } + #[test] + fn counts_host_fields() { + let count = |block: &str| Headers::parse(block).unwrap().host_count(); + assert_eq!(count(""), 0); + assert_eq!(count("Host: a"), 1); + assert_eq!(count("Host: a\r\nX: 1\r\nhOST: a"), 2); + } + #[test] fn success_cookie_headers_joined_with_semicolons() { let headers = Headers::parse("Cookie: a=1\r\nCookie: b=2".to_string()).unwrap(); diff --git a/src/router.rs b/src/router.rs index f9364e6..5875951 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,221 +1,22 @@ use crate::default; -use crate::error::InvalidPathError; -use crate::middlewares::Closures; -use crate::path::PathNode; -use crate::request::{MatchedRequest, Request, RequestMethod}; +use crate::method::RequestMethod; +use crate::request::{MatchedRequest, Request}; use crate::responder::Response; +pub(crate) use crate::routes::RouterResult; +use crate::routes::Routes; +pub use crate::routes::{Flow, Route}; +#[cfg(feature = "url_variables")] use std::collections::HashMap; -use std::result::Result; -// The type of HashMap where we will be storing the all the closures -pub(crate) type Paths = HashMap>; /// The Closure type is a type alias for the type /// that the routes should return pub type Closure = Box Fn(&'a MatchedRequest, &'a mut Response) -> Flow + Send + Sync>; -// RouterResult is the type which the app.METHOD methods return -pub(crate) type RouterResult = Result<(), InvalidPathError>; -/// The flow enum works just like the next() callback -/// in express. The variant returns decides whether -/// the execution should go to the next similar route -/// or not -/// -/// # Example -/// -/// ``` -/// use octane::prelude::*; -/// -/// let mut app = Octane::new(); -/// app.get( -/// "/", -/// route!( -/// |req, res| { -/// res.send("Hello, World"); -/// Flow::Stop -/// } -/// ), -/// ); -/// ``` -#[derive(Copy, Clone, Debug)] -pub enum Flow { - /// Don't execute the next closure which is similar in url with the initial one - Stop, - /// jump on the next closure which is similar in url with the initial one - Next, -} - -impl Flow { - /// Returns true if the variant is `Flow::Next` - pub fn should_continue(self) -> bool { - matches!(self, Self::Next) - } -} -/// The route trait adds the app.METHOD behaviour -/// to the router/Octane structures along with some -/// handful methods that can be used accordingly. -/// -/// # Example -/// -/// ``` -/// use octane::prelude::*; -/// -/// let mut app = Octane::new(); -/// app.get( -/// "/", -/// route!( -/// |req, res| { -/// res.send("Hello, World"); -/// Flow::Stop -/// } -/// ), -/// ); -/// ``` -/// While you are using these methods directly on your -/// Octane struct, you can add them on a router instance -/// which can be appended to your main server (octane) -/// struct. It's called Router -/// -/// TODO: Include router example here -pub trait Route { - /// add_route() is a duplicate of add() but with a - /// specified url on where it should run. - /// It runs on the given path and on all types of requests - fn add_route(&mut self, path: &str, closure: Closure) -> RouterResult; - /// Part of app.METHOD, runs on when the request is on the - /// path given and the request method is HEAD - /// - /// # Example - /// - /// ``` - /// use octane::prelude::*; - /// - /// let mut app = Octane::new(); - /// app.head( - /// "/", - /// route!( - /// |req, res| { - /// res.send("Hello, World"); - /// Flow::Stop - /// } - /// ), - /// ); - /// ``` - fn head(&mut self, path: &str, closure: Closure) -> RouterResult; - /// Part of app.METHOD, runs on when the request is on the - /// path given and the request method is POST - /// - /// # Example - /// - /// ``` - /// use octane::prelude::*; - /// - /// let mut app = Octane::new(); - /// app.post( - /// "/", - /// route!( - /// |req, res| { - /// res.send("Hello, World"); - /// Flow::Stop - /// } - /// ), - /// ); - /// ``` - fn post(&mut self, path: &str, closure: Closure) -> RouterResult; - /// Part of app.METHOD, runs on when the request is on the - /// path given and the request method is GET - /// - /// # Example - /// - /// ``` - /// use octane::prelude::*; - /// - /// let mut app = Octane::new(); - /// app.get( - /// "/", - /// route!( - /// |req, res| { - /// res.send("Hello, World"); - /// Flow::Stop - /// } - /// ), - /// ); - /// ``` - fn get(&mut self, path: &str, closure: Closure) -> RouterResult; - /// Part of app.METHOD, runs on when the request is on the - /// path given and the request method is PUT - /// - /// # Example - /// - /// ``` - /// use octane::prelude::*; - /// - /// let mut app = Octane::new(); - /// app.put( - /// "/", - /// route!( - /// |req, res| { - /// res.send("Hello, World"); - /// Flow::Stop - /// } - /// ), - /// ); - /// ``` - fn put(&mut self, path: &str, closure: Closure) -> RouterResult; - /// Part of app.METHOD, runs on when the request is on the - /// path given and the request method is DELETE - /// - /// # Example - /// - /// ``` - /// use octane::prelude::*; - /// - /// let mut app = Octane::new(); - /// app.delete( - /// "/", - /// route!( - /// |req, res| { - /// res.send("Hello, World"); - /// Flow::Stop - /// } - /// ), - /// ); - /// ``` - fn delete(&mut self, path: &str, closure: Closure) -> RouterResult; - /// Part of app.METHOD, runs on when the request is on the - /// path given and the request method is PATCH - /// - /// # Example - /// - /// ``` - /// use octane::prelude::*; - /// - /// let mut app = Octane::new(); - /// app.patch( - /// "/", - /// route!( - /// |req, res| { - /// res.send("Hello, World"); - /// Flow::Stop - /// } - /// ), - /// ); - /// ``` - fn patch(&mut self, path: &str, closure: Closure) -> RouterResult; - /// add() is like `app.use` in express, it runs on all the - /// paths and all types of valid methods, the request comes - /// on - fn add(&mut self, entity: Closure) -> RouterResult; -} /// The router structure defines the routes and stores them along with /// their indexes. Router methods can be used with this struct and also /// the main server structure. pub struct Router { - /// Holds a counter and increments on new additions of routes - pub route_counter: usize, - /// A vector of middleware closures - pub middlewares: Vec, - /// The router paths which are to be executed on requests - pub paths: Paths, + pub(crate) routes: Routes, } impl Router { @@ -225,66 +26,22 @@ impl Router { /// instance by doing `app.use_router(router)`. pub fn new() -> Self { Router { - route_counter: 0, - middlewares: Vec::new(), - paths: HashMap::new(), + routes: Routes::new(), } } // append the routes stored in a custom Router to the self Router pub(crate) fn append(&mut self, router: Self) { - let self_count = self.route_counter; - let other_count = router.route_counter; - for (methods, paths) in router.paths.into_iter() { - let updated_paths = paths.into_iter().map(|mut v| { - v.data.index += self_count; - v - }); - if let Some(x) = self.paths.get_mut(&methods) { - x.extend(updated_paths); - } else { - self.paths.insert(methods, updated_paths.collect()); - } - } - - self.middlewares - .extend(router.middlewares.into_iter().map(|mut v| { - v.index += self_count; - v - })); - self.route_counter += other_count; + self.routes.append(router.routes); } - - // Fetch the closure according to the request path, run that - // specific closure. + // Fetch the closures matching the request path and run them in + // registration order until one returns `Flow::Stop`. pub(crate) fn run(&self, parsed_request: Request<'_>, res: &mut Response) { let method = parsed_request.request_line.method; - // `MatchedPath` values borrow their input path when URL variables are + // `Matched` values borrow their input path when URL variables are // enabled. A path-only clone lets the router move the much larger // request (including all headers) into `MatchedRequest` unchanged. let match_path = parsed_request.request_line.path.clone(); - - let mut method_routes = self - .paths - .get(&method) - .map_or_else(Vec::new, |functions| { - let mut routes = functions.get(&match_path); - routes.sort_unstable_by_key(|route| route.index); - routes - }) - .into_iter() - .peekable(); - // Run RequestMethod::All regardless of the request method. - let mut all_routes = self - .paths - .get(&RequestMethod::All) - .map_or_else(Vec::new, |functions| { - let mut routes = functions.get(&match_path); - routes.sort_unstable_by_key(|route| route.index); - routes - }) - .into_iter() - .peekable(); - let mut middlewares = self.middlewares.iter().peekable(); + let routes = self.routes.matched(method, &match_path); #[cfg(feature = "url_variables")] let mut matched = MatchedRequest { @@ -296,80 +53,18 @@ impl Router { request: parsed_request, }; - loop { - let method_index = method_routes.peek().map_or(usize::MAX, |route| route.index); - let all_index = all_routes.peek().map_or(usize::MAX, |route| route.index); - let middleware_index = middlewares - .peek() - .map_or(usize::MAX, |middleware| middleware.index); - - if method_index == usize::MAX - && all_index == usize::MAX - && middleware_index == usize::MAX + for route in routes { + #[cfg(feature = "url_variables")] { - break; + matched.vars = route.vars; } - - // Registration indexes are globally ordered. Merge the three - // already-sorted streams directly instead of allocating a vector - // for middleware matches plus bookkeeping vectors on every request. - let flow = if method_index <= all_index && method_index <= middleware_index { - let route = method_routes.next().unwrap(); - #[cfg(feature = "url_variables")] - { - matched.vars = route.vars; - } - (&route.data.closure)(&matched, res) - } else if all_index <= middleware_index { - let route = all_routes.next().unwrap(); - #[cfg(feature = "url_variables")] - { - matched.vars = route.vars; - } - (&route.data.closure)(&matched, res) - } else { - let middleware = middlewares.next().unwrap(); - #[cfg(feature = "url_variables")] - matched.vars.clear(); - (&middleware.closure)(&matched, res) - }; - if !flow.should_continue() { + if !(route.data)(&matched, res).should_continue() { break; } } } } -/// The route macro makes it easy to pass anonymous -/// functions to app.METHODs. -/// -/// # Example -/// -/// ``` -/// use octane::prelude::*; -/// -/// let mut app = Octane::new(); -/// app.get( -/// "/", -/// route!( -/// |req, res| { -/// res.send("Hello, World"); -/// Flow::Stop -/// } -/// ), -/// ); -/// ``` -/// -/// The closure's environment is captured by value explictly, -/// so you are basically writing (pseudo code below) -/// -/// ```ignore -/// use octane::prelude::*; -/// -/// route!(move |req, res| { -/// res.send("Hello, World"); -/// Flow::Stop -/// }); -/// ``` + #[macro_export] macro_rules! route { ( | $req : ident, $res : ident | $body : expr ) => {{ @@ -436,61 +131,33 @@ macro_rules! route_stop { default!(Router); -macro_rules! inject_method { - ( $instance: expr, $path: expr, $closure: expr, $method: expr ) => { - use crate::middlewares::Closures; - use crate::path::{PathBuf, PathNode}; - $instance - .paths - .entry($method) - .or_insert(PathNode::new()) - .insert( - PathBuf::parse($path)?, - Closures { - closure: $closure, - index: $instance.route_counter, - }, - ); - $instance.route_counter += 1; - }; -} - impl Route for Router { + type Handler = Closure; + fn head(&mut self, path: &str, closure: Closure) -> RouterResult { - inject_method!(self, path, closure, RequestMethod::Head); - Ok(()) + self.routes.insert(RequestMethod::Head, path, closure) } fn put(&mut self, path: &str, closure: Closure) -> RouterResult { - inject_method!(self, path, closure, RequestMethod::Put); - Ok(()) + self.routes.insert(RequestMethod::Put, path, closure) } fn get(&mut self, path: &str, closure: Closure) -> RouterResult { - inject_method!(self, path, closure, RequestMethod::Get); - Ok(()) + self.routes.insert(RequestMethod::Get, path, closure) } fn delete(&mut self, path: &str, closure: Closure) -> RouterResult { - inject_method!(self, path, closure, RequestMethod::Delete); - Ok(()) + self.routes.insert(RequestMethod::Delete, path, closure) } fn post(&mut self, path: &str, closure: Closure) -> RouterResult { - inject_method!(self, path, closure, RequestMethod::Post); - Ok(()) + self.routes.insert(RequestMethod::Post, path, closure) } fn patch(&mut self, path: &str, closure: Closure) -> RouterResult { - inject_method!(self, path, closure, RequestMethod::Patch); - Ok(()) + self.routes.insert(RequestMethod::Patch, path, closure) } fn add(&mut self, closure: Closure) -> RouterResult { - self.middlewares.push(Closures { - closure, - index: self.route_counter, - }); - self.route_counter += 1; + self.routes.add(closure); Ok(()) } fn add_route(&mut self, path: &str, closure: Closure) -> RouterResult { - inject_method!(self, path, closure, RequestMethod::All); - Ok(()) + self.routes.insert(RequestMethod::All, path, closure) } } @@ -520,9 +187,9 @@ mod test { .delete("/", route!(|req, res| { Flow::Next })) .unwrap(); // middleware with path - assert_eq!(5, router.paths.len()); + assert_eq!(5, router.routes.paths.len()); // middleware without paths - assert_eq!(1, router.middlewares.len()); + assert_eq!(1, router.routes.middlewares.len()); } #[test] @@ -565,10 +232,11 @@ mod test { .unwrap(); first_router.append(second_router); // middleware tied without path - assert_eq!(2, first_router.middlewares.len()); + assert_eq!(2, first_router.routes.middlewares.len()); assert_eq!( 2, first_router + .routes .paths .get(&RequestMethod::Get) .unwrap() @@ -579,6 +247,7 @@ mod test { assert_eq!( 2, first_router + .routes .paths .get(&RequestMethod::Delete) .unwrap() @@ -588,6 +257,7 @@ mod test { assert_eq!( 2, first_router + .routes .paths .get(&RequestMethod::Post) .unwrap() @@ -597,6 +267,7 @@ mod test { assert_eq!( 2, first_router + .routes .paths .get(&RequestMethod::Head) .unwrap() @@ -606,6 +277,7 @@ mod test { assert_eq!( 2, first_router + .routes .paths .get(&RequestMethod::Put) .unwrap() diff --git a/src/routes.rs b/src/routes.rs new file mode 100644 index 0000000..12ef569 --- /dev/null +++ b/src/routes.rs @@ -0,0 +1,390 @@ +//! The routing engine shared by the tokio [`Router`](crate::router::Router) +//! and the WASIp3 router: routes are stored per method in a path trie and +//! matched together with the middleware in registration order. What a +//! handler is, sync closure or async function, is up to the router. + +use crate::method::RequestMethod; +use crate::path::{InvalidPathError, PathBuf, PathNode}; +use std::collections::HashMap; + +// RouterResult is the type which the app.METHOD methods return +pub(crate) type RouterResult = Result<(), InvalidPathError>; + +/// The flow enum works just like the next() callback +/// in express. The variant returns decides whether +/// the execution should go to the next similar route +/// or not +/// +/// # Example +/// +/// ``` +/// use octane::prelude::*; +/// +/// let mut app = Octane::new(); +/// app.get( +/// "/", +/// route!( +/// |req, res| { +/// res.send("Hello, World"); +/// Flow::Stop +/// } +/// ), +/// ); +/// ``` +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Flow { + /// Don't execute the next closure which is similar in url with the initial one + Stop, + /// jump on the next closure which is similar in url with the initial one + Next, +} + +impl Flow { + /// Returns true if the variant is `Flow::Next` + pub fn should_continue(self) -> bool { + matches!(self, Self::Next) + } +} +/// The route trait adds the app.METHOD behaviour +/// to the router/Octane structures along with some +/// handful methods that can be used accordingly. +/// +/// # Example +/// +/// ``` +/// use octane::prelude::*; +/// +/// let mut app = Octane::new(); +/// app.get( +/// "/", +/// route!( +/// |req, res| { +/// res.send("Hello, World"); +/// Flow::Stop +/// } +/// ), +/// ); +/// ``` +/// While you are using these methods directly on your +/// Octane struct, you can add them on a router instance +/// which can be appended to your main server (octane) +/// struct. It's called Router +/// +/// The tokio [`Router`](crate::router::Router) and [`Octane`](crate::Octane) +/// take [`Closure`](crate::router::Closure)s, the WASIp3 router takes async +/// handlers: that's the [`Route::Handler`] type. +/// +/// TODO: Include router example here +pub trait Route { + /// What runs when a route matches + type Handler; + + /// add_route() is a duplicate of add() but with a + /// specified url on where it should run. + /// It runs on the given path and on all types of requests + fn add_route(&mut self, path: &str, closure: Self::Handler) -> RouterResult; + /// Part of app.METHOD, runs on when the request is on the + /// path given and the request method is HEAD + /// + /// # Example + /// + /// ``` + /// use octane::prelude::*; + /// + /// let mut app = Octane::new(); + /// app.head( + /// "/", + /// route!( + /// |req, res| { + /// res.send("Hello, World"); + /// Flow::Stop + /// } + /// ), + /// ); + /// ``` + fn head(&mut self, path: &str, closure: Self::Handler) -> RouterResult; + /// Part of app.METHOD, runs on when the request is on the + /// path given and the request method is POST + /// + /// # Example + /// + /// ``` + /// use octane::prelude::*; + /// + /// let mut app = Octane::new(); + /// app.post( + /// "/", + /// route!( + /// |req, res| { + /// res.send("Hello, World"); + /// Flow::Stop + /// } + /// ), + /// ); + /// ``` + fn post(&mut self, path: &str, closure: Self::Handler) -> RouterResult; + /// Part of app.METHOD, runs on when the request is on the + /// path given and the request method is GET + /// + /// # Example + /// + /// ``` + /// use octane::prelude::*; + /// + /// let mut app = Octane::new(); + /// app.get( + /// "/", + /// route!( + /// |req, res| { + /// res.send("Hello, World"); + /// Flow::Stop + /// } + /// ), + /// ); + /// ``` + fn get(&mut self, path: &str, closure: Self::Handler) -> RouterResult; + /// Part of app.METHOD, runs on when the request is on the + /// path given and the request method is PUT + /// + /// # Example + /// + /// ``` + /// use octane::prelude::*; + /// + /// let mut app = Octane::new(); + /// app.put( + /// "/", + /// route!( + /// |req, res| { + /// res.send("Hello, World"); + /// Flow::Stop + /// } + /// ), + /// ); + /// ``` + fn put(&mut self, path: &str, closure: Self::Handler) -> RouterResult; + /// Part of app.METHOD, runs on when the request is on the + /// path given and the request method is DELETE + /// + /// # Example + /// + /// ``` + /// use octane::prelude::*; + /// + /// let mut app = Octane::new(); + /// app.delete( + /// "/", + /// route!( + /// |req, res| { + /// res.send("Hello, World"); + /// Flow::Stop + /// } + /// ), + /// ); + /// ``` + fn delete(&mut self, path: &str, closure: Self::Handler) -> RouterResult; + /// Part of app.METHOD, runs on when the request is on the + /// path given and the request method is PATCH + /// + /// # Example + /// + /// ``` + /// use octane::prelude::*; + /// + /// let mut app = Octane::new(); + /// app.patch( + /// "/", + /// route!( + /// |req, res| { + /// res.send("Hello, World"); + /// Flow::Stop + /// } + /// ), + /// ); + /// ``` + fn patch(&mut self, path: &str, closure: Self::Handler) -> RouterResult; + /// add() is like `app.use` in express, it runs on all the + /// paths and all types of valid methods, the request comes + /// on + fn add(&mut self, entity: Self::Handler) -> RouterResult; +} + +/// A handler with the position it was registered at +pub(crate) struct Indexed { + pub(crate) data: T, + pub(crate) index: usize, +} + +/// A handler that matched a request, with the url variables of its path +pub(crate) struct Matched<'a, T> { + pub(crate) data: &'a T, + #[cfg(feature = "url_variables")] + pub(crate) vars: HashMap<&'a str, &'a str>, +} + +/// Routes per method and the middleware, numbered in registration order +pub(crate) struct Routes { + pub(crate) counter: usize, + pub(crate) middlewares: Vec>, + pub(crate) paths: HashMap>>, +} + +impl Routes { + pub(crate) fn new() -> Self { + Self { + counter: 0, + middlewares: Vec::new(), + paths: HashMap::new(), + } + } + + pub(crate) fn insert(&mut self, method: RequestMethod, path: &str, data: T) -> RouterResult { + let path = PathBuf::parse(path)?; + let index = self.next_index(); + self.paths + .entry(method) + .or_insert_with(PathNode::new) + .insert(path, Indexed { data, index }); + Ok(()) + } + + pub(crate) fn add(&mut self, data: T) { + let index = self.next_index(); + self.middlewares.push(Indexed { data, index }); + } + + /// Append `other`'s routes, which run after the ones already here + pub(crate) fn append(&mut self, other: Self) { + let offset = self.counter; + for (method, paths) in other.paths.into_iter() { + let updated_paths = paths.into_iter().map(|mut v| { + v.data.index += offset; + v + }); + if let Some(x) = self.paths.get_mut(&method) { + x.extend(updated_paths); + } else { + self.paths.insert(method, updated_paths.collect()); + } + } + self.middlewares + .extend(other.middlewares.into_iter().map(|mut v| { + v.index += offset; + v + })); + self.counter += other.counter; + } + + /// Everything that runs for `method` on `path`, in registration order: + /// the routes for that method, the routes for every method and the + /// middleware. Middleware have no url variables. + pub(crate) fn matched<'a>( + &'a self, + method: RequestMethod, + path: &'a PathBuf, + ) -> Vec> { + let routes = |method| { + self.paths.get(&method).map_or_else(Vec::new, |node| { + let mut routes = node.get(path); + routes.sort_unstable_by_key(|route| route.index); + routes + }) + }; + let mut method_routes = routes(method).into_iter().peekable(); + // RequestMethod::All runs regardless of the request method + let mut all_routes = routes(RequestMethod::All).into_iter().peekable(); + let mut middlewares = self.middlewares.iter().peekable(); + + let mut matched = Vec::new(); + loop { + let method_index = method_routes.peek().map_or(usize::MAX, |r| r.index); + let all_index = all_routes.peek().map_or(usize::MAX, |r| r.index); + let middleware_index = middlewares.peek().map_or(usize::MAX, |m| m.index); + if method_index == usize::MAX + && all_index == usize::MAX + && middleware_index == usize::MAX + { + break; + } + // registration indexes are globally ordered, so merging the three + // already sorted lists gives the order to run them in + let route = if method_index <= all_index && method_index <= middleware_index { + method_routes.next() + } else if all_index <= middleware_index { + all_routes.next() + } else { + let middleware = middlewares.next().unwrap(); + matched.push(Matched { + data: &middleware.data, + #[cfg(feature = "url_variables")] + vars: HashMap::new(), + }); + continue; + }; + let route = route.unwrap(); + matched.push(Matched { + data: &route.data.data, + #[cfg(feature = "url_variables")] + vars: route.vars, + }); + } + matched + } + + fn next_index(&mut self) -> usize { + let index = self.counter; + self.counter += 1; + index + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn order(routes: &Routes, method: RequestMethod, path: &str) -> Vec { + let path = PathBuf::parse(path).unwrap(); + routes + .matched(method, &path) + .iter() + .map(|m| *m.data) + .collect() + } + + #[test] + fn merges_in_registration_order() { + let mut routes = Routes::new(); + routes.add(0); + routes.insert(RequestMethod::All, "/", 1).unwrap(); + routes.insert(RequestMethod::Get, "/", 2).unwrap(); + routes.insert(RequestMethod::Post, "/", 99).unwrap(); + routes.insert(RequestMethod::Get, "/other", 98).unwrap(); + routes.add(3); + assert_eq!(order(&routes, RequestMethod::Get, "/"), [0, 1, 2, 3]); + assert_eq!(order(&routes, RequestMethod::Put, "/"), [0, 1, 3]); + } + + #[test] + fn appended_routes_run_after() { + let mut first = Routes::new(); + first.insert(RequestMethod::Get, "/", 0).unwrap(); + let mut second = Routes::new(); + second.add(1); + second.insert(RequestMethod::Get, "/", 2).unwrap(); + first.append(second); + first.add(3); + assert_eq!(order(&first, RequestMethod::Get, "/"), [0, 1, 2, 3]); + } + + #[test] + #[cfg(feature = "url_variables")] + fn routes_get_their_variables() { + let mut routes = Routes::new(); + routes.add(0); + routes.insert(RequestMethod::Get, "/users/:id", 1).unwrap(); + let path = PathBuf::parse("/users/42").unwrap(); + let matched = routes.matched(RequestMethod::Get, &path); + assert!(matched[0].vars.is_empty()); + assert_eq!(matched[1].vars.get("id"), Some(&"42")); + } +} diff --git a/src/server.rs b/src/server.rs index c076b86..02874ff 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,7 +1,6 @@ use crate::config::{Config, OctaneConfig, Ssl}; use crate::error::Error; use crate::http::Http; -use crate::middlewares::Closures; use crate::request::{Headers, Request}; use crate::responder::{ResBody, Response}; use crate::router::{Closure, Route, Router, RouterResult}; @@ -299,6 +298,8 @@ impl Octane { default!(Octane); impl Route for Octane { + type Handler = Closure; + fn head(&mut self, path: &str, closure: Closure) -> RouterResult { self.router.head(path, closure) } @@ -318,12 +319,7 @@ impl Route for Octane { self.router.patch(path, closure) } fn add(&mut self, closure: Closure) -> RouterResult { - self.router.middlewares.push(Closures { - closure, - index: self.router.route_counter, - }); - self.router.route_counter += 1; - Ok(()) + self.router.add(closure) } fn add_route(&mut self, path: &str, closure: Closure) -> RouterResult { self.router.add_route(path, closure) diff --git a/src/wasip3.rs b/src/wasip3.rs new file mode 100644 index 0000000..65b25da --- /dev/null +++ b/src/wasip3.rs @@ -0,0 +1,189 @@ +//! Serving from a WASIp3 component with octane's router. +//! +//! Routes are added through the same [`Route`] trait as on the tokio server +//! (`get`, `post`, `add` for middleware, url variables like `/users/:id` +//! with the `url_variables` feature) and run in registration order until +//! one returns [`Flow::Stop`]. Handlers are async, so a handler that waits +//! on a timer or streams a body doesn't hold up other connections. They +//! answer through a [`Responder`]; a request nothing answered gets a `404`. +//! +//! ```ignore +//! use octane::portable::config::Config; +//! use octane::wasip3::{handler, serve, Flow, Route, Router}; +//! use octane::StatusCode; +//! +//! const CONFIG: Config<{ 16 * 1024 }, 1024> = Config::new(); +//! +//! let mut router = Router::new(); +//! router.get("/users/:id", handler(async |req, res| { +//! let id = req.vars.get("id").copied().unwrap_or(""); +//! let _ = res.send(StatusCode::Ok, &[], id.as_bytes()).await; +//! Flow::Stop +//! }))?; +//! +//! // inside the component's async `run` export +//! serve(CONFIG, "127.0.0.1:8080".parse().unwrap(), |_| {}, router).await +//! ``` + +use crate::default; +pub use crate::method::RequestMethod; +/// The error the [`Route`] methods return for a path that climbs above the root +pub use crate::path::InvalidPathError; +use crate::path::PathBuf; +pub use crate::routes::{Flow, Route}; +use crate::routes::{RouterResult, Routes}; +use octane_http::config::Config; +use octane_http::http1x::Request; +pub use octane_http::wasip3::{BodyWriter, ErrorCode, Responder, SendError}; +#[cfg(feature = "url_variables")] +use std::collections::HashMap; +use std::future::Future; +use std::net::SocketAddr; +use std::ops::Deref; +use std::pin::Pin; +use std::rc::Rc; + +/// The request a handler sees: the parsed request and, with the +/// `url_variables` feature, the variables of the route that matched. It +/// derefs to [`Request`]. +pub struct MatchedRequest<'r> { + /// The request coming from the client + pub request: &'r Request<'r>, + /// The url variables of the matched route, empty for middleware + #[cfg(feature = "url_variables")] + pub vars: HashMap<&'r str, &'r str>, +} + +impl<'r> Deref for MatchedRequest<'r> { + type Target = Request<'r>; + + fn deref(&self) -> &Self::Target { + self.request + } +} + +/// A boxed async handler, made with [`handler`] +pub type Handler = Box< + dyn for<'a, 'r, 'c> Fn( + &'a MatchedRequest<'r>, + &'a mut Responder<'c>, + ) -> Pin + 'a>>, +>; + +/// Box an async closure or `async fn` into a [`Handler`] for the +/// [`Route`] methods +pub fn handler(f: F) -> Handler +where + F: AsyncFn(&MatchedRequest<'_>, &mut Responder<'_>) -> Flow + 'static, +{ + let f = Rc::new(f); + Box::new(higher_ranked(move |req, res| { + let f = Rc::clone(&f); + Box::pin(async move { (*f)(req, res).await }) + })) +} + +/// Pins the closure's signature to the higher-ranked one of [`Handler`], +/// which closure inference doesn't do on its own +fn higher_ranked(closure: C) -> C +where + C: for<'a, 'r, 'c> Fn( + &'a MatchedRequest<'r>, + &'a mut Responder<'c>, + ) -> Pin + 'a>>, +{ + closure +} + +/// The WASIp3 router, filled through the [`Route`] trait +pub struct Router { + routes: Routes, +} + +impl Router { + pub fn new() -> Self { + Self { + routes: Routes::new(), + } + } + + /// Append the routes of `router`, which run after the ones already here + pub fn append(&mut self, router: Router) { + self.routes.append(router.routes); + } + + /// Run the handlers matching `request` in registration order until one + /// returns [`Flow::Stop`] + pub async fn run(&self, request: &Request<'_>, res: &mut Responder<'_>) { + let method = RequestMethod::from(request.method); + // the parser already refused `..` segments, so this can't fail by + // climbing above the root + let path = match PathBuf::parse(request.path) { + Ok(path) => path, + Err(_) => return, + }; + for route in self.routes.matched(method, &path) { + let matched = MatchedRequest { + request, + #[cfg(feature = "url_variables")] + vars: route.vars, + }; + if !(route.data)(&matched, res).await.should_continue() { + break; + } + } + } +} + +default!(Router); + +impl Route for Router { + type Handler = Handler; + + fn add_route(&mut self, path: &str, handler: Handler) -> RouterResult { + self.routes.insert(RequestMethod::All, path, handler) + } + fn head(&mut self, path: &str, handler: Handler) -> RouterResult { + self.routes.insert(RequestMethod::Head, path, handler) + } + fn post(&mut self, path: &str, handler: Handler) -> RouterResult { + self.routes.insert(RequestMethod::Post, path, handler) + } + fn get(&mut self, path: &str, handler: Handler) -> RouterResult { + self.routes.insert(RequestMethod::Get, path, handler) + } + fn put(&mut self, path: &str, handler: Handler) -> RouterResult { + self.routes.insert(RequestMethod::Put, path, handler) + } + fn delete(&mut self, path: &str, handler: Handler) -> RouterResult { + self.routes.insert(RequestMethod::Delete, path, handler) + } + fn patch(&mut self, path: &str, handler: Handler) -> RouterResult { + self.routes.insert(RequestMethod::Patch, path, handler) + } + fn add(&mut self, handler: Handler) -> RouterResult { + self.routes.add(handler); + Ok(()) + } +} + +/// Listen on `addr` and serve every connection with `router`, each as its +/// own task. `on_listen` gets the bound address once the socket listens. +/// Errors creating, binding or listening on the socket are returned. +pub async fn serve( + config: Config, + addr: SocketAddr, + on_listen: L, + router: Router, +) -> Result<(), ErrorCode> +where + L: FnOnce(SocketAddr), +{ + octane_http::wasip3::serve( + config, + addr, + on_listen, + async move |req: &Request<'_>, res: &mut Responder<'_>| router.run(req, res).await, + ) + .await +} diff --git a/tests/common.rs b/tests/common.rs index f6419b7..1b6388f 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "std")] use octane::Octane; use std::io::{Read, Write}; use std::net::TcpStream; diff --git a/tests/headers.rs b/tests/headers.rs index 1ce8de4..f295100 100644 --- a/tests/headers.rs +++ b/tests/headers.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "std")] use octane::prelude::*; mod common; diff --git a/tests/router.rs b/tests/router.rs index 5a783e0..69cfc89 100644 --- a/tests/router.rs +++ b/tests/router.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "std")] use octane::prelude::*; mod common; diff --git a/tests/server.rs b/tests/server.rs index 5cee2d6..f871ccf 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "std")] use octane::prelude::*; mod common; @@ -73,3 +74,21 @@ pub fn basic_server_x_req() { assert_eq!(200, common::request(8082, "HEAD", "/", &[]).status); }); } + +#[test] +pub fn http11_needs_exactly_one_host() { + let mut app = Octane::new(); + app.get("/", route_next!(|req, res| res.send("ok"))) + .unwrap(); + common::run(app, 8089, || { + let missing = common::raw_request(8089, b"GET / HTTP/1.1\r\n\r\n"); + assert_eq!(400, missing.status); + let repeated = common::raw_request(8089, b"GET / HTTP/1.1\r\nHost: a\r\nHost: a\r\n\r\n"); + assert_eq!(400, repeated.status); + let single = common::raw_request(8089, b"GET / HTTP/1.1\r\nHost: a\r\n\r\n"); + assert_eq!((200, "ok"), (single.status, single.text())); + // HTTP/1.0 predates Host + let http10 = common::raw_request(8089, b"GET / HTTP/1.0\r\n\r\n"); + assert_eq!(200, http10.status); + }); +}