Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / crates / npro-test / h1 / requests / headers-put-name.http
Author[]Andy Green <andy@warmcat.com> 2026-10-03 09:24 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-10-03 09:24 UTC
Tree91beaf32fb0eab913402ac03639ff3aa3be7feba   Raw Patch
 
Placeholder
Placeholder
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..53811bf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,141 @@ +# lws AGENTS.md + +## Overview + +Please err on the side of high quality, not lazy, implementation decisions, because the code +will have to be maintained for a long time. Everybody, LLM or person, is able to work +better if we keep the code clean and to a high standard to start with. + +Even if your instructions don't include specific admonishment about quality, it is always +necessary. Lws started in 2010 in C, now also in Rust, the main goal when working on a +feature is to improve the library with the feature. That means shortcuts and desperate +incomplete hacks to deliver the feature are always wrong. + +Our work should follow the existing usage of apis in the project as much as possible. + +## Context + +Read docs/agent-context.md before starting: it distils what was learned working on the C +library with agents, the decisions already taken for this project, and where the C design +documents are. + +## Interacting + +We will be working on the same sources, do not build into ./build since I will be using it; make +your own ./build-claude or whatever. + +Often although we are working on the same sources, they are being tested on devices you don't have +access to. So you must ask for access to data state on those remote machines; looking at the local +machine you are running on for config or data state directly is of zero use in those circumstances. + +## Churn management + +When you produced fixes, if possible (main branch, target is within 8 patches back, +no intervening non-sai- tag) it's preferable to --amend apply the fixes directly to +the patch that originated the problem, essentially editing the history, even if +it means just doing that and not adding any fix patch or explanation. Similarly, unless +asked to produce a new patch or the goal is an explicit phased series, if it's on the +main branch and we are iterating on the same work, and HEAD patch is yours from the +last iteration, it's preferable to directly use --amend on it to commit. + +If the changes are for mixed purposes, if you initiate new core library changes or fixes, +these should be broken out into their own patch, even if the rest relates to recent +changes and is squashed in with those. + +## Completeness + +If you are unable to complete something your coding partner expects from the interaction +with you, you must clearly explain to the user which parts are incomplete in this phase and need +further work. DO NOT leave it silently incomplete and act like it is done without making the +situation crystal clear to your partner. + +Often adding / modifying or removing features has a very strong expectation that you will +also take responsibility about certain side-effects. For example, adding a switch to a + +minimal example always means modifying the associated --help and the example's markdown +accordingly. If we add significant new code, it must also bring with it api-test or other +example code to confirm it works properly. These side-effects are expected to be taken +care of in the same phase of work, not "later", and even if not explicitly requested. + +## Example code + +The examples are not chaotic dumping grounds for trash. They are supposed to show the user +the best way we know how to do things, that they can use in their own code reliably. We +should make an extra effort to keep them clean and as quality exemplars. + +## Rust coding + +We are very concerned about: + + - security, architecturally and in the code + - portability + - cleanliness and hygiene of code + - testing from several directions, static analysis and audits to + discover problems before attackers do + + - Every crate root carries `#![forbid(unsafe_code)]`. Not `deny`: `forbid` + cannot be re-allowed further down the file. No `unsafe` blocks, no `-sys` + crates, no FFI in the protocol crates. If bindings to C are ever wanted + they are a separate crate that is not a dependency of anything here. + + - A panic is a bug, not error handling. Nothing reachable from network + data may `unwrap()`, `expect()`, index a slice with `[]`, or hit + `unreachable!()`. Errors are returned as `Result` with a small error + enum per crate, never `String` or `Box<dyn Error>`. Arithmetic on + lengths and offsets is `checked_*` or `saturating_*`; integer narrowing is + `try_from`, never `as`. On a device a panic is a reboot; on a server it + is a remotely triggered abort. + + - The core crates are `#![no_std]`, with `alloc` only where a feature + turns it on. No `std::io`, `std::net`, threads, `Instant`, or `async` + in a protocol crate. Bytes come in as caller-owned `&[u8]`, output is + written into caller-owned `&mut [u8]`, and time is passed in as a value + from the adapter. Sockets, tls and the event loop live in the IO + adapter crate and nowhere else. + + - State lives in enums, not bools. Each machine from the C state tables + is an `enum` whose variants carry the data that only exists in that + state; a `match` on state has no `_ =>` arm; no `Option` that is + "always Some when we are in state X"; no flag that duplicates something + the state already says. Ids and sizes are newtypes (`StreamId`, not + `u32`). If a field can be set to something the state forbids, it is not + `pub`. + + - Every buffer, queue, table and header set has a declared maximum, and no + parser recurses on external input. Depth and size limits are part of + the type or the config, not a comment. + + - Dependencies are close to zero and each one is justified in the commit + adding it: MSRV-compatible, no `build.rs` that touches the network, no + large proc-macro trees behind the core. `cargo deny` and `cargo audit` + configs are committed and gate CI. Default features are the minimum + that builds something useful; everything else is opt-in. + + - No `Rc<RefCell<_>>` or `Arc<Mutex<_>>` inside the protocol crates. + Interior mutability in the core means the ownership of the design is + wrong; fix the design. Whether things are `Send` is the adapter's + decision. + + - CI gates, all with warnings as errors: `cargo fmt --check`, + `cargo clippy --all-targets --all-features -D warnings` with + `clippy::unwrap_used`, `expect_used`, `indexing_slicing`, + `arithmetic_side_effects` and `as_conversions` enabled in the core, + `cargo doc` with `missing_docs` denied on public items, + `cargo semver-checks` on release, and a build at the pinned + `rust-version`. A lint is silenced only with an `#[allow]` on the one + item, carrying a reason. + + - Tests are part of the feature. Every parser has a `cargo-fuzz` target + seeded from the C corpus; state machines get property tests; public + behaviour gets doctests, which compile and run. Where the C library + has the same input, a differential test against it is expected. + + - Public API follows the Rust API guidelines: no `get_` prefixes, no + out-parameters, `#[must_use]` on anything returning a `Result` or a + value the caller must act on, and rustdoc on every public item with an + example. + + - `unexpected_cfgs` must be a denied lint + + - Cargo features must stay additive: a feature that removes or changes + behaviour breaks any dependent that enables a different set diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0d76fbe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "lws" +version = "0.0.1" +license = "MIT" +homepage = "https://libwebsockets.org" +description = "Modern all-safe networking library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls" +edition = "2024" +readme = "README.md" +repository = "https://libwebsockets.org/git/lws-rs" +keywords = ["websocket", "http2", "http3", "sans-io", "networking"] +categories = ["network-programming", "web-programming::websocket"] +rust-version = "1.85" +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c2f156 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Placeholder + +This is a placeholder for lws by Andy Green <andy@warmcat.com> + diff --git a/docs/agent-context.md b/docs/agent-context.md new file mode 100644 index 0000000..a56a3af --- /dev/null +++ b/docs/agent-context.md @@ -0,0 +1,308 @@ +# lws agent context + +This is distilled from the working notes that coding agents kept while +working on the C libwebsockets tree with Andy, up to 2026-09-29. It holds +what an agent starting on lws-rs would otherwise have to rediscover: who you +are working with, how Andy wants the work done, the decisions already taken +for this project, and what the C library taught us the hard way. + +It is a starting point, not the authority. Where it disagrees with the C +tree's own documents, the C tree wins; where it disagrees with AGENTS.md, +AGENTS.md wins. Facts about the C code are as of the date above and the C +tree keeps moving. + +Agent memory does not persist between cloud sessions: each one starts on a +fresh machine. If you learn something durable that a later session needs, +propose an edit to this file in your commit rather than relying on memory. + +## Who you are working with + +Andy Green is the author and maintainer of libwebsockets (C, MIT, since +2010: http/1, h2, h3/quic, ws, WebTransport, mqtt, tls on several backends, +many platforms down to microcontrollers). Andy has deep C and protocol +knowledge and wants precise root-cause analysis, not hand-holding. + +Andy is relatively new to Rust. This port exists so that Andy's +understanding of lws carries over, so: + +- explain Rust-specific choices in terms of what the C does and why the + Rust shape is better or safer, briefly, in commit messages and replies; +- never lean on "that's idiomatic" alone as a justification; +- keep the correspondence to the C design visible (names, state machines, + the sans-IO interface) unless there is a stated reason to depart. + +Since mid-2026 Andy has used AI models to find and fix bugs in C lws and +pushes the results straight to the public tree, which is also deployed to +production Internet servers. Treat that as the bar: what you commit may +be running on a public server soon after. + +## How the work is to be done + +These came from real incidents, not taste. + +- **One commit per logical change.** A fix, a feature and a refactor are + separate commits, each building and passing tests on its own, so history + can be reviewed and bisected. Subjects are prefixed with the area + touched, as in the C tree (`h2: ...`, `quic: ...`). +- **A claim needs a test.** Twice in C, an agent's confident reading of a + flow was wrong ("the form posts here", "releasing this early is safe"), + every existing test passed, and production broke within the hour. + "All tests pass" is not evidence for a path no test exercises. Before + relying on a behavioural claim, add a test that exercises it, or read the + code that defines the other side (the peer, the asset, the RFC text). + Treat "by design" and "not reachable" as unverified until shown. +- **No exploit-style reproducers.** Find and fix problems by reading, by + tests of correct behaviour, and by fuzzing; do not write attack tools or + crafted attack payloads as deliverables. +- **Don't brute-force sporadic failures.** Many parallel test loops once + exhausted a machine's memory and orphaned processes, and the bug was + then found by reading. Prefer one instrumented run and reading. Keep + test parallelism moderate (`-j4` rather than `-j8` on a shared box). +- **Ask before installing tools or system packages.** Never install + Node.js or anything with an npm-style uncontrolled dependency tree, even + in a throwaway environment. New crate dependencies are covered by + AGENTS.md: close to zero, each justified in its commit. +- **Say what is not done.** If a phase ends incomplete, list exactly + what is missing. Never leave something silently half-done. +- **When a partner makes a design call, record it and don't reopen it.** + Suggest alternatives once, with reasons, then follow the decision. + +## Decisions already taken for lws-rs + +- Brand and crate name is `lws` (reserved on crates.io as a placeholder, + 0.0.1, MIT, edition 2024). Not `libwebsockets`: in Rust a crate named + after a C library reads as bindings or a wrapper. The repository may be + called `lws-rs`, but no crate name carries a `-rs` suffix. +- Home is libwebsockets.org; the repository is + https://libwebsockets.org/git/lws-rs . There is no `.rs` domain. +- One repository, one Cargo workspace, members under `crates/*`: + `lws-core` (the sans-IO core), protocol crates `lws-h1`, `lws-h2`, + `lws-h3`, `lws-ws`, `lws-wt`, an IO crate `lws-io` for sockets, tls and + the event loop, and `lws` as the facade crate re-exporting the rest + behind features. quinn / quinn-proto is the model for the split. No + git submodules. Member crate names are not yet reserved on crates.io: + check before assuming one is free. +- Trust posture: `forbid(unsafe_code)`, a tiny dependency tree, the + conformance suites (autobahn for ws, h2spec, h3spec), cargo-fuzz seeded + from the C corpus, differential tests against C lws, clippy with + warnings as errors, a pinned MSRV, cargo deny / cargo audit, and a README + that says plainly that the port is AI-driven and how it is checked. +- Publishing tokens go through a cargo credential provider + (`cargo:libsecret`), never the plaintext `credentials.toml`; tokens are + scoped and expiring; CI publishing uses trusted publishing. + +## The C tree as your reference + +A checkout of C lws is your reference, never your output. In cloud +sessions the environment's setup script puts one outside this repository; +if you cannot find it, say so rather than cloning it into this tree. Do +not copy C sources in, and do not transliterate C: the goal is the same +behaviour and design with Rust's guarantees. + +Read these first; they are the design documents written for this port: + +- `READMEs/README.sans-io-split.md`: the two halves, the interface + between them, the contract, and a section "Open before it is done" + whose part "Before it is the port's source" lists what the C split + still gets wrong for a port. Read that list before designing an + interface. +- `READMEs/README.wsi-state-machines.md`: the per-connection state + machines, what each state means, the events that move them, and the + close invariants. + +Where the C code's history matters, the commit messages explain the +reasons for most of the rules below; `git log -S` on the C tree finds +them, if the reference checkout has history. + +## sans-IO: what was learned splitting the C library + +The C library was restructured in September 2026 into a sansIO half and +an IO half precisely so this port could follow it. Terms: call them +**sansIO** and **IO**; "core" in C lws means `lib/core`, not the sansIO +half. + +- The placement test: if deleting the code would change the bytes on the + wire or the state transitions for a given input, it is sansIO; if it + only changes when or how the same bytes move, it is IO. +- rx: IO hands sansIO bytes and learns how many were consumed. A + zero-length rx is the peer closing. Datagram rx is taken whole with + its peer address and ECN bits. +- tx is a **pull**: IO offers a buffer and a maximum; sansIO writes from + where it got to. No parking of composed output on the heap. The one + exception C kept for now is the application's own data (`lws_write()` + framing in place, zero-copy); the README lists turning that into a pull + as required for the port. +- The client response body is deliberately a pull at the application's + pace (`lws_http_client_read()` in C): the application's buffer and the + TCP window provide back-pressure. Do not replace it with buffering. +- Hangup: POLLHUP was found more trustworthy than the read's return value + across platforms and tls libraries. In Rust this is the IO crate's + concern, but the sansIO side must accept "peer closed" as an input at + any point. +- Time must be an input. The C sansIO half still reads the clock itself + in about 56 places (quic congestion control, loss detection, pacing + among them); the port must pass "now" in on every entry point instead. +- IO's requests of sansIO turned out to be about twenty calls, not four: + want-write, want-read, deadline and close, plus transport phase + changes, tls session queries and a few more. "The contract" in the + README lists them; that is what `lws-io` implements. +- A test transport that replaces the socket, driving both a client and a + server through in-memory buffers, is how C tests the split + (`api-test-sansio`, `api-test-sansio-split`). Design `lws-core` so the + same harness is trivial: that is the no-IO test surface for everything. + +## State machines: lessons that apply directly + +- A connection in C had one flat state enum, and it was found to be four + machines stacked in one word: **transport** (dns, connect, tls, + socks/proxy legs), **carrier** (the per-protocol handshake: h2 preface + and SETTINGS, h1 upgrade...), **transaction** (headers, body, file + serving, completion) and **close**. 203 distinct transitions were + observed, 99 of them crossing between these notional machines. In Rust + each machine is its own enum, as AGENTS.md says. +- Carrier and transaction are sequential, not stacked, and the boundary + differs per protocol: on an h1 client, "waiting for the server's reply" + is a carrier state for the first request only and a per-transaction + phase for later pipelined ones. Design this per role; do not apply one + recipe everywhere. +- Most C bugs of the last phase were **bools living beside the state** + that some resets cleared and some did not. Every one of them was + folded into a state or derived from one. The Rust rule in AGENTS.md + (state in enums, no duplicate flags) is the cure. +- Attributes that belong to the connection rather than the protocol must + survive a change of role (h1 -> ws, h1 -> h2c, quic -> h3). Losing one + at a role transition hung every http client once. +- Transitions are driven by named events through a table; an unlisted + transition is a bug. A table row that accepts **any** source state + hides bugs: one such row masked a raw socket reporting "connected" in + the middle of a SOCKS handshake. Prefer explicit source states. In + Rust, exhaustive `match` without `_ =>` gives this for free. +- A client multiplexed connection (h2, h3) whose last stream has closed + is kept warm for a few seconds (idle state with a timeout) so a new + request to the same endpoint joins it. This is design intent, not dead + code. +- Parked question, do not settle without Andy: a request that joins a + kept-warm h2/h3 connection is told "established" at the join with + status 0, before any response arrives. Secure Streams relies on that + today. +- A client redirect is a transport phase ("restarting") of the same + connection object, and its tests found three separate defects in the C + h2 path. Test redirects over every protocol. + +## Protocol traps found in C (each deserves a Rust test) + +- **A fatal verdict must stop parsing.** An h2 connection error was + queued as GOAWAY while the parser returned success and carried on over + the rest of the buffer; connection-scoped hpack state then drove + per-stream header storage out of bounds. Every check was individually + correct; the bridge between "detected" and "enforced" was missing. + Ask of every verdict: what does the caller do with it? +- **h2c upgrade**: stream 1's send window was overwritten with our own + advertised window, so the upgraded stream could not send until the peer + happened to send WINDOW_UPDATE (most clients do, so it went unseen). +- An **h2 client stream with no body** went straight to "established" + with no response timeout; it must wait for the reply like h1 and h3. +- **Header presence** must be explicit. C decides whether a header + exists from a flag each fragment creator had to remember to set; one + path forgot, and every h3 check for forbidden headers (Connection, TE, + Transfer-Encoding; RFC 9114 4.2) was silently dead. h2/h3 treat + `:authority` as Host when no Host header came. +- **Chunked bodies**: one decoder shared by client and server; chunk + extensions and trailers skipped within a bound (4096 bytes in C); at + least one hex digit; only a lone `chunked` Transfer-Encoding is + accepted (anything else is 501). An h1 POST with neither + Content-Length nor Transfer-Encoding is counted by other means in C + because the C client sends multipart bodies like that: decide this + explicitly for Rust, with a test. +- **Body bytes that arrive with the headers** must be kept before the + handler runs: in C a status page reused the receive buffer and + clobbered them. +- **Expect: 100-continue**: send 100 only if nothing has answered yet; + other Expect values get 417. A client must swallow any 1xx and keep + waiting for the final status. +- **Range requests (RFC 7233)**: multipart delimiters must be real MIME + (`--boundary`, close delimiter, `boundary=` in Content-Type); the + boundary is random per response; END_STREAM follows the last byte of + the range, not end of file; at most 16 ranges; a 416 carries + `Content-Range: bytes */len`. +- **Multiplexed fairness walk**: rotating a serviced child to the tail + while iterating with a cached next pointer made two children alternate + forever in one pass. Bound every fair-share walk by the child count + taken at the start. +- **quic output accounting**: data sent but not yet acknowledged is not + "pending output". Treating it as such made h3 file responses go one + fragment per round trip and spun the loop after a client vanished. A + stream waiting only on an ACK, congestion window or flow control must + not ask to write; the event that lifts the limit wakes it. +- **Receive flow control is rx only**: a role that stopped servicing when + rx was paused also stopped its tx, and wedged. +- **TLS client handshake may complete synchronously** on its first step + (a resumed TLS 1.3 session on loopback). The C path then skipped peer + certificate verification on some backends and ran ALPN selection twice. + One step function for every step; ALPN handled once, after the peer is + confirmed. +- **h3 to h2 fallback**: when quic loses the race, the h2 stream's + "closed" arrives a pass after its "completed". A client driver that + starts the next job on "completed" must ignore the previous job's late + "closed". +- **SOCKS5 replies**: consume only the reply's length; bytes after it + belong to the next protocol. +- **Socket address family is platform-dependent**: macOS reports an IPv4 + peer on a dual-stack socket as a plain IPv4 address and refuses to send + to it on that socket; Linux does neither. Normalise a received address + to the socket's family before recording or comparing it. +- **Silent limits**: C's default connection budget quietly stopped + accepting on listeners when reached, with no log. Every limit the + port has should be visible when hit. + +## Parsers + +- Streaming parsers must be restartable at any byte boundary. The oracle + that found the most bugs in C's newer parsers compares a one-shot pass + with a fragmented pass (input split at random, output sink deferring) + and requires identical results. Use the same idea in property tests + and fuzz targets. +- A C JSON parser silently dropped elements 2..n of a scalar array when a + broader wildcard pattern came later in its match list, truncating a + retry-backoff table to its first entry for five years. Whatever does + JSON for the port, test arrays of scalars explicitly. +- A parse that returns "ok" having consumed less than its input is a held + byte, not an error; callers loop while progress is made. + +## DNS, DNSSEC and the DHT + +- C's async DNS validates DNSSEC by walking the DS chain from the root. + Not done in C: NSEC/NSEC3 (unsigned delegations fail closed) and + Ed25519 (algorithm 15). +- Andy's production lws DHT network is **two nodes**. Any quorum written + as a fixed peer count (eg. "three peers agree on our external address") + can never be met; derive thresholds from what the routing table can + actually supply. + +## Testing heritage to port + +- C api-tests that are the most useful behavioural references: + `api-test-http-transfer` (every body framing, h1/h2/h3, proxy, + redirect, reuse and keep-warm cases), `api-test-http-ranges`, + `api-test-keep-warm`, `api-test-ws-close`, `api-test-raw-close`, + `api-test-sansio`, `api-test-sansio-split`, `api-test-dnssec-chain`, + `api-test-ws-h2-txcredit`. Each lives under + `minimal-examples-lowlevel/api-tests/` in the C tree. +- C fuzz targets are in `fuzz/fuzz-<name>/` with committed seeds in + `fuzz/fuzz-<name>/seeds/`; seeds named `regress-*` are past crashes. + Targets relevant to the port: h1, h2, ws, ws-pmd, qpack, adns, jose, + cose, lejp, lecp, tokenize. Seed the matching cargo-fuzz targets from + them. +- Conformance: C lws is checked with h3spec against the minimal quic + client-server example; autobahn (ws), h2spec and h3spec are the suites + the Rust crates are expected to pass. + +## Working from a cloud session + +- Andy has no shell on the cloud machine and sees only what you report and + what you commit. Report failures with the command and its output. +- Results come back to Andy's own git server by fetching your branch and + cherry-picking, so every commit must stand alone: builds, passes its + tests, one purpose. +- Do not push anywhere, and do not add credentials, tokens or remotes to + the repository or the environment. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +}
Page fetched 0s ago, creation time: 2ms (vhost etag hits: 0%, cache hits: 0%)