crates/npro-test/transcripts/README.lws.md

lws sansIO transcripts

Each file here is one connection's life as data: the times, the bytes its peer sent, the bytes lws wrote, what lws gave the application, and its close. api-test-sansio produces them by driving lws' protocol half with no socket and no clock of its own (READMEs/README.sans-io-split.md, "Time"), and checks, in every build that runs it, that what lws does now is still what is recorded here. They are also the specification a port of the sansIO half is checked against: fed the same bytes at the same times, with the same application, it must write the same bytes and deliver the same payloads.

Format

{
 "format": "lws-transcript/1",
 "case": "h1-ws-server",
 "side": "server",
 "t0_us": 1000000000,
 "t0_wall": 1767225600,
 "seed": 0,
 "steps": [
  {"t": 1000, "rx": "474554..."},
  {"t": 1000, "tx": "485454..."},
  ...
 ]
}
membermeaning
formatlws-transcript/1. A change to what follows is a new version
casethe connection's name, and the file's
sideserver: the peer connected to lws. client: lws connected to the peer
t0_usthe monotonic time, in microseconds, when the run starts: every step's t is after it
t0_wallthe wall time, seconds since 1970, at t0_us. The wall time moves with the monotonic time
seed0: nothing in the connection's bytes depends on lws' random. Otherwise lws' random is the xoshiro256** stream seeded with it (lws_fi_random_seed()), see "Random" below
stepswhat happened, in order

Each step has t, microseconds since t0_us, and one of:

kindmeaning
rxthe peer sent these bytes, and they are handed to lws at t
txlws wrote these bytes to the peer at t. Bytes written in several writes at the same time are one step: how many writes it took is not part of the behaviour
app_rxlws delivered this payload to the application: a response body, or a ws message's payload. How a message or a body was split into deliveries follows how much lws was handed at a time, and is not behaviour: what a replay must match is the concatenation of the app_rx steps of each ws message, and of each response body, and where each of those ends relative to the other steps
closelws released the connection's transport. The value is empty

Bytes are lowercase hex. A replay hands lws each rx at its t, telling it the time first, and runs anything that falls due at a t before what happens then. The tx, app_rx and close steps are what it must see, in order, at their times.

Random

A connection whose bytes depend on lws' random (a ws client's Sec-WebSocket-Key and its frame masks) has a nonzero seed, and the peer's side of it depends on those bytes too: the 101 response carries the accept value of the key the client chose. Such a transcript reproduces only when the random is the seeded stream drawn in the same order. The stream starts afresh from seed when the connection does, so what it draws does not depend on the connections before it, or on which of them a build has. So:

  • api-test-sansio records and checks it only in a build with fault injection (LWS_WITH_SYS_FAULT_INJECTION), which can seed lws' random, and says so in other builds; - a port either draws the same bytes from the same stream in the same order (xoshiro256** seeded by splitmix64, each 64-bit result little-endian, the vector in api-test-random-prng), or its replay treats the drawn values as free, taking them from its own tx and checking what follows from them (the accept value, the masking) instead of the bytes.

The tls library's own random, inside its handshakes, is not lws' and is not covered: no transcript here is under tls yet.

The connections and their applications

A replay needs the same application on lws' side. In each connection it does only this:

casesidethe application
h1-ws-serverserverone vhost with protocols http and echo. http: answers any request 200, text/plain, content length 10, body sansio ok\n, then completes the transaction (the connection is kept). echo: sends each ws message that came in one frame back, as text; it sends nothing for a fragmented one. The peer does an h1 GET, then a ws upgrade to echo on the same connection, then sends the masked text frame Hello of RFC 6455 5.7
h1-client-getclientconnects to sansio, port 80, h1, GET /x, and pulls the response body as it comes
h1-client-cl-junk, h1-client-cl-twice, h1-client-te-listclientas h1-client-get. The peer answers 200 with Content-Length: 10abc; with Content-Length: 10 and a second Content-Length: 5; or with Transfer-Encoding: chunked and a second Transfer-Encoding: gzip, which makes the codings a list. lws cannot know where such a body ends: it gives the app none of it, fails the connection and releases it
h1-client-head-chunked, h1-client-head-cl, h1-client-304-clclientas h1-client-get, asking with HEAD for the first two. The peer answers 200 with Transfer-Encoding: chunked; 200 with Content-Length: 10; or, to the GET, 304 with Content-Length: 10. None of these has a body (RFC 9112 6.3): lws completes the transaction at the end of the headers and gives the app nothing
ws-clientclientconnects to sansio, port 80, GET /echo, ws subprotocol echo; once established, sends one text message Hello, and takes the messages it receives
ws-server-version-8, ws-server-no-version, ws-server-conn-no-upgrade, ws-server-no-subprotocolserveras h1-ws-server. The peer asks for a ws upgrade saying Sec-WebSocket-Version: 8; or no version at all; or Connection: up, which is not the upgrade token; or only the subprotocol chat, which the vhost does not have. lws answers the first with 426 and sec-websocket-version: 13, the others with 400, each with the usual status page, then shuts the connection down (a half-close, waiting for the peer's; no close step follows)
ws-client-rsv1-no-ext, ws-client-rsv2, ws-client-huge-frameclientas ws-client, no extension offered. After the 101, the peer sends a frame with RSV1, with RSV2, or whose length is 256MiB + 1: lws gives the app nothing of it and sends a close, status 1002 rsv bits for the RSV ones and 1009 huge frame for the length; the peer answers the close and lws releases the connection
ws-client-ping-closeclientas ws-client, sending nothing once established. The peer sends a ping p and then a close with status 1000 in one read: lws sends the masked pong, then the masked answer to the close with the peer's 1000, and releases the connection
ws-client-interimclientas ws-client. The peer answers the upgrade with an interim 100 Continue, then in the same read the 101: lws skips the interim response, goes on waiting, and is established by the 101
ws-client-pmd-rsv2, ws-client-pmd-rsv1-continuation, ws-client-pmd-rsv1-pingclientas ws-client, on a vhost offering permessage-deflate (no parameters), sending nothing once established, and the peer's 101 accepts permessage-deflate. The peer then sends a frame with RSV2; or an uncompressed, unfinished text frame He (given to the app) and then a continuation with RSV1; or a ping with RSV1. As above, lws fails the connection, with 1002 rsv bits
h1-uri-dotdot-args, h1-uri-dot-args, h1-uri-plus, h1-uri-at-limitservera third vhost, sansio-uri, whose only protocol, http, answers any request 200, text/plain, with the path lws decoded, a newline, then the args, and completes the transaction. The context limits the request URI to 33 bytes and User-Agent to 16 (token_limits). The peer asks for /x/..?a=b (path /, args a=b), /x/.?a=b (path /x/), /a+b?c+d=e+f (path /a+b, args c d=e f: + is only a space in the args) and a 33-byte path, which is served whole
h1-uri-past-limit, h1-header-past-limitserveras above. The peer asks for a 34-byte path, or sends a 17-byte User-Agent: rather than serving the request with the value cut short, lws answers 414, or 431, with the usual status page saying Oversized request URI or Oversized headers, and shuts the connection down
h1-reqline-http09, h1-reqline-no-method, h1-reqline-version-2, h1-reqline-version-junk, h1-reqline-version-longserveras above. The peer sends a request line with no version, GET /x (HTTP/0.9); a head with no request line, only Host: sansio-uri; or asks for /x as HTTP/2.0, HTTP/1.x or HTTP/1.10. A request line is method, target and HTTP/ digit . digit: lws answers 400 with the usual status page, except 505 for the major version it does not speak, and shuts the connection down
h1-reqline-unknown-method, h1-reqline-unknown-header-firstserveras above. The peer asks for /x with the method FOO, which lws does not implement: 501 (RFC 9110 9.1); or starts its head with X-Foo: bar, a header lws does not know, rather than a request line: 400. Either way with the usual status page, and the connection is shut down
h1-reqline-leading-empty, h1-reqline-leading-empty-manyserveras above. The peer sends two empty lines, then its request for /x: they are ignored (RFC 9112 2.2) and it is served; or nine: past eight, no request is coming, and lws answers 400 and shuts the connection down
h1-reqline-version-1-2serveras above. The peer asks for /x as HTTP/1.2: a later minor version is served as the highest lws speaks, and the response says HTTP/1.1
h2-oversized-headersservera fourth vhost, sansio-h2, taking h2 with prior knowledge, whose only protocol is the http of sansio-uri. The peer sends the preface, an empty SETTINGS and the ack of the server's, then three GET / requests. sid 1: :authority entered in the dynamic table, then a 3000 byte user-agent and a 2000 byte referer, not indexed, that the 4096 byte ah cannot hold, then accept: x entered in the dynamic table after it filled: answered 431 Oversized headers, the connection kept. sid 3: :authority and accept from the dynamic table: accept could not be kept, so it is answered 431 too, rather than served as if the peer had not sent it. sid 5: only :authority from the dynamic table, which was kept: served
h1-404-keepaliveservera fifth vhost, sansio-404, whose 404 document is /404.html. /cb is mounted on its only protocol, http; everything else is a file mount on a directory that does not exist, falling back to http for what it cannot open. http answers whatever reaches it with lws_return_http_status() 404, then completes the transaction. On one kept-alive connection the peer asks for /x: lws redirects it to the 404 document with a 302; then for /404.html, which is not there either: the 404 status page; then for /cb/y: redirected with a 302 again, as /x was
ws-server-ping-close, ws-server-close-partialserveras h1-ws-server: after the upgrade, the peer sends, masked, in one read, a ping p and then a close with status 1000; or a text frame Hello and then the close, while the connection takes at most 4 bytes a write. lws answers the ping with its pong before it answers the close, with the peer's own 1000; the echo of Hello, left partly unwritten, goes out whole before the answer to the close, rather than the connection being dropped. After its answer lws reads nothing more, and shuts the connection down
ws-server-huge-frameserveras h1-ws-server: after the upgrade, the peer sends a masked frame whose length is 256MiB + 1. lws closes with status 1009, huge frame
ws-server-pmd-rsv1-continuationserveras h1-ws-server, on a second vhost, sansio-pmd, with permessage-deflate (no parameters). The peer asks for a ws upgrade to it offering permessage-deflate, which is accepted, then sends an unfinished text frame He and a continuation with RSV1: lws closes with status 1002, rsv bits, and once the peer answers the close, shuts the connection down
h2-ws-peer-closeservera sixth vhost, sansio-h2ws, taking h2 with prior knowledge, with the ws protocols of sansio. The peer sends the preface and SETTINGS, then an extended CONNECT (RFC 8441) for a ws stream on sid 1 speaking echo, which is accepted with 200; then one DATA frame holding, masked, a close with status 1000 and after it a ping p. lws answers the close with the peer's own 1000 and END_STREAM, reads nothing after it, so the ping gets no pong, and resets the stream with NO_ERROR, since the peer has not ended its side: not REFUSED_STREAM, which is only for a refused upgrade
h1-post-no-lengthserveras h1-uri-*, on sansio-uri. The peer sends, in one read, POST /x?a=1 with neither Content-Length nor Transfer-Encoding, and behind its head GET /y?b=2. The POST has no body (RFC 9112 6.3): lws answers it (path /x, args a=1), then serves the GET as the next request on the kept-alive connection, rather than taking it as the POST's body and reading until the close
h1-connect-rejected-uaserveras h1-reqline-*, on sansio-uri, in a context that turns away a user agent containing badbot with 403 Go away. The peer sends a CONNECT for example.com:443 saying it is badbot/1: it is refused like any other request of its, 403 with the status page, and the connection shut down, rather than handed to the fallback role first

Recording

 $ ./bin/lws-api-test-sansio --record ../minimal-examples-lowlevel/api-tests/api-test-sansio/transcripts

from a build with fault injection, so the seeded connections are recorded too. ctest runs lws-api-test-sansio --transcripts transcripts in this directory's parent, which fails if what lws does differs from what is here, and says at which line.

A transcript is behaviour: a change to one is a change to what lws puts on the wire, and the commit making it says why. A new case adds a file here, its row above, and its connection's application to the test.