@amritk/nish-x86_64-linux 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +43 -34
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.d.ts +117 -8
- package/runtime/nish.h +50 -13
- package/runtime/nish.mjs +71 -7
- package/runtime/runtime-host.c +43 -24
- package/runtime/runtime-net.c +57 -10
- package/runtime/runtime-os.c +62 -20
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +12 -0
- package/runtime/runtime.c +66 -18
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +14 -12
- package/std/README.md +168 -36
- package/std/crypto/aes.ts +51 -8
- package/std/crypto/base64url.ts +25 -12
- package/std/crypto/chacha20poly1305.ts +43 -29
- package/std/crypto/ct.ts +6 -1
- package/std/crypto/hkdf.ts +364 -3
- package/std/crypto/hmac.ts +307 -7
- package/std/crypto/p256.ts +618 -34
- package/std/crypto/sha1.ts +147 -0
- package/std/crypto/sha256.ts +3 -2
- package/std/crypto/x25519.ts +88 -36
- package/std/crypto/x509.ts +130 -47
- package/std/json.ts +297 -41
- package/std/net/hpack.ts +1094 -0
- package/std/net/http-fields.ts +604 -0
- package/std/net/http1-server.ts +1389 -0
- package/std/net/http1.ts +1492 -0
- package/std/net/http2-frame.ts +745 -0
- package/std/net/http2-tls.ts +216 -0
- package/std/net/http2.ts +1433 -0
- package/std/net/http3-frame.ts +457 -0
- package/std/net/http3-server.ts +911 -0
- package/std/net/http3.ts +2145 -0
- package/std/net/qpack.ts +1163 -0
- package/std/net/quic-conn-ack.ts +206 -0
- package/std/net/quic-conn-cid.ts +425 -0
- package/std/net/quic-conn-params.ts +462 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +929 -0
- package/std/net/quic-packet.ts +1698 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1609 -0
- package/std/net/quic.ts +3578 -0
- package/std/net/tls/codec.ts +1089 -0
- package/std/net/tls/record-server.ts +1173 -0
- package/std/net/tls/record.ts +621 -0
- package/std/net/tls/schedule.ts +429 -0
- package/std/net/tls-tcp.ts +414 -0
- package/std/net/tls.ts +1437 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
- package/std/text.ts +77 -0
|
@@ -0,0 +1,1389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/http1-server` — the server side of HTTP/1.1 (RFC 9112): a
|
|
3
|
+
* sans-IO connection that drives `nish/net/http1`'s parser, and two pools of
|
|
4
|
+
* them for a program that owns its readiness loop, one on `nish:net`'s plain
|
|
5
|
+
* TCP and one on `nish/net/tls-tcp` with ALPN `http/1.1`.
|
|
6
|
+
*
|
|
7
|
+
* const listener = tcpListen("0.0.0.0", 8080, 128);
|
|
8
|
+
* const server = new Http1Server(new Http1Config(), listener, 64);
|
|
9
|
+
* // when the listener is readable:
|
|
10
|
+
* const slot: i32 = server.accept();
|
|
11
|
+
* if (slot >= 0) { pollAdd(loop, server.fd(slot), 1, slot); }
|
|
12
|
+
* // when a connection's token comes back:
|
|
13
|
+
* let wants: i32 = (events & 5) !== 0 ? server.readable(slot) : server.writable(slot);
|
|
14
|
+
* let event: i32 = server.next(slot);
|
|
15
|
+
* while (event !== H1_NEED_MORE && event !== H1_ERROR) {
|
|
16
|
+
* const conn: Http1Connection = server.connection(slot);
|
|
17
|
+
* // H1_REQUEST: conn.parser.methodIs("GET"), conn.parser.targetIs("/"), ...
|
|
18
|
+
* // conn.respond(200, names, values, HTTP1_CHUNKED)
|
|
19
|
+
* // H1_BODY: conn.data[conn.dataStart .. conn.dataStart + conn.dataLength)
|
|
20
|
+
* // H1_END: conn.write(body, 0, n); conn.end()
|
|
21
|
+
* event = server.next(slot);
|
|
22
|
+
* }
|
|
23
|
+
* wants = server.flush(slot);
|
|
24
|
+
* if ((wants & H1_DONE) !== 0) { server.close(slot); }
|
|
25
|
+
* else { pollModify(loop, server.fd(slot), wants & 3, slot); }
|
|
26
|
+
*
|
|
27
|
+
* **Events.** `next()` answers `H1_REQUEST` (a request's head: the parser's
|
|
28
|
+
* spans and `...Is` methods read it), `H1_BODY` (some of its body, de-chunked,
|
|
29
|
+
* a window that stays valid until the next `next()`), `H1_END` (the request
|
|
30
|
+
* is complete), `H1_WRITE` (a write that was held back may be tried again),
|
|
31
|
+
* `H1_WS_MESSAGE` and `H1_WS_CLOSE` once the slot has switched to WebSocket,
|
|
32
|
+
* and `H1_ERROR`, final: the request broke a rule, the response that says so
|
|
33
|
+
* is queued, and the connection closes once it is sent. `H1_NEED_MORE` asks
|
|
34
|
+
* for more input, for the output to drain, or for the program to finish the
|
|
35
|
+
* response it owes.
|
|
36
|
+
*
|
|
37
|
+
* **One exchange at a time.** A request is answered before the next one on
|
|
38
|
+
* the connection is read: `respond`, any number of `write`s, `end`. The
|
|
39
|
+
* program may answer at once (before the body has arrived) or after
|
|
40
|
+
* `H1_END`; either way the request's body is read to its end before the next
|
|
41
|
+
* request starts, which is what keeps pipelined requests in order (§9.3.2).
|
|
42
|
+
* Pipelined bytes wait in the parser's buffer, and once it holds
|
|
43
|
+
* `inputLimit()` bytes the connection stops asking for input, so the client's
|
|
44
|
+
* TCP window closes: back-pressure on the way in.
|
|
45
|
+
*
|
|
46
|
+
* **Streaming both ways.** A request body arrives a chunk at a time, at most
|
|
47
|
+
* `chunkSize` bytes in each `H1_BODY`, whether it was sent with
|
|
48
|
+
* `Content-Length` or chunked. A response is written with a length or
|
|
49
|
+
* chunked, a `write` at a time into the output buffer; a `write` takes what
|
|
50
|
+
* fits and answers how much, and when it could not take everything the
|
|
51
|
+
* connection answers `H1_NEED_MORE` until the socket has drained half the
|
|
52
|
+
* output, then `H1_WRITE`. Until then it reads nothing, so the window the
|
|
53
|
+
* program is writing from stays valid: back-pressure on the way out. Nothing
|
|
54
|
+
* buffers a whole body.
|
|
55
|
+
*
|
|
56
|
+
* **What the server decides.** The framing field (`Content-Length` or
|
|
57
|
+
* `Transfer-Encoding: chunked`) and the `Connection` field are the server's;
|
|
58
|
+
* the program's fields may not hold either, nor `Upgrade`, `Keep-Alive` or
|
|
59
|
+
* `Content-Length`. A request that did not ask to keep the connection open
|
|
60
|
+
* gets `Connection: close` and the connection closes after it; an HTTP/1.0
|
|
61
|
+
* request that asked for keep-alive gets `Connection: keep-alive`; a chunked
|
|
62
|
+
* response to an HTTP/1.0 request is sent unframed and ends with the
|
|
63
|
+
* connection, since chunked is HTTP/1.1's. A response to HEAD, and a 204 or
|
|
64
|
+
* 304, carries no body whatever the program writes (RFC 9110 §9.3.2, §15.3.5,
|
|
65
|
+
* §15.4.5). A 1xx before the final response (100 Continue) is allowed; 101 is
|
|
66
|
+
* `acceptWebSocket`'s.
|
|
67
|
+
*
|
|
68
|
+
* **Refusals.** Every refusal of `nish/net/http1`'s parser — the smuggling
|
|
69
|
+
* shapes, a `Content-Length` with a `Transfer-Encoding`, two that disagree,
|
|
70
|
+
* a malformed chunk size — is answered with its status (400, 413, 414, 431,
|
|
71
|
+
* 501, 505), `Content-Length: 0` and `Connection: close`, and ends the
|
|
72
|
+
* connection; so does a malformed WebSocket frame, with its close code.
|
|
73
|
+
* `docs/security/http1.md` is the record.
|
|
74
|
+
*
|
|
75
|
+
* **WebSocket.** A request that asked to upgrade reaches the program like any
|
|
76
|
+
* other; `acceptWebSocket(protocol)` answers it with the 101 of
|
|
77
|
+
* `nish/net/websocket` when it is a valid opening handshake (RFC 6455 §4.2),
|
|
78
|
+
* and once its exchange is over the slot reads frames with a `WsDecoder`
|
|
79
|
+
* instead of requests, the bytes that followed the request included. A ping
|
|
80
|
+
* is answered with a pong and a close with a close, by the connection.
|
|
81
|
+
*
|
|
82
|
+
* **Caps are start-up numbers**, all in `Http1Config`: the request target,
|
|
83
|
+
* the header section and its field count, a request body, the chunk handed
|
|
84
|
+
* over, the output buffer, a WebSocket message and the idle timeout; the
|
|
85
|
+
* slots are the pool's size. Every buffer is allocated by the constructor or
|
|
86
|
+
* grown to its cap on first use and kept, so a warmed slot serves request
|
|
87
|
+
* after request with `Arena.used()` flat. Accepting a WebSocket allocates
|
|
88
|
+
* (the accept key), once a connection.
|
|
89
|
+
*
|
|
90
|
+
* Private helpers share the importing program's flat symbol namespace
|
|
91
|
+
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
92
|
+
*
|
|
93
|
+
* Written from RFC 9112, RFC 9110 and RFC 6455, not ported from another
|
|
94
|
+
* implementation.
|
|
95
|
+
*/
|
|
96
|
+
import { Secret } from "nish:secret"
|
|
97
|
+
import { netClose, netRead, netWrite, tcpAccept } from "nish:net"
|
|
98
|
+
import {
|
|
99
|
+
HTTP1_BODY,
|
|
100
|
+
HTTP1_CHUNKED,
|
|
101
|
+
HTTP1_CLOSED,
|
|
102
|
+
HTTP1_END,
|
|
103
|
+
HTTP1_ERROR,
|
|
104
|
+
HTTP1_HEAD,
|
|
105
|
+
HTTP1_NO_BODY,
|
|
106
|
+
HTTP1_REFUSED,
|
|
107
|
+
HTTP1_UPGRADE,
|
|
108
|
+
Http1Parser,
|
|
109
|
+
http1BytesAre,
|
|
110
|
+
http1WriteChunk,
|
|
111
|
+
http1WriteLastChunk,
|
|
112
|
+
http1WriteResponseHead,
|
|
113
|
+
} from "nish/net/http1"
|
|
114
|
+
import {
|
|
115
|
+
WS_CLOSE,
|
|
116
|
+
WS_ERROR,
|
|
117
|
+
WS_MAX_CONTROL,
|
|
118
|
+
WS_MESSAGE,
|
|
119
|
+
WS_NEED_MORE,
|
|
120
|
+
WS_OP_CLOSE,
|
|
121
|
+
WS_OP_PONG,
|
|
122
|
+
WS_PING,
|
|
123
|
+
WsDecoder,
|
|
124
|
+
websocketClosePayload,
|
|
125
|
+
websocketFrameSize,
|
|
126
|
+
websocketRequestKey,
|
|
127
|
+
websocketUpgradeResponse,
|
|
128
|
+
websocketWriteFrame,
|
|
129
|
+
} from "nish/net/websocket"
|
|
130
|
+
import { TlsServerConfig } from "nish/net/tls"
|
|
131
|
+
import { TLS_RECORD_DONE, TLS_RECORD_STATE_OPEN, TLS_RECORD_WANT_WRITE } from "nish/net/tls/record-server"
|
|
132
|
+
import { TlsTcpServer } from "nish/net/tls-tcp"
|
|
133
|
+
|
|
134
|
+
/** `next`: nothing to hand over until more input arrives, the output drains, or the program answers. */
|
|
135
|
+
export const H1_NEED_MORE: i32 = 0
|
|
136
|
+
/** A request's head was read: `parser` holds it. */
|
|
137
|
+
export const H1_REQUEST: i32 = 1
|
|
138
|
+
/** Request body bytes: `data[dataStart .. dataStart + dataLength)`, valid until the next `next()`. */
|
|
139
|
+
export const H1_BODY: i32 = 2
|
|
140
|
+
/** The request is complete. */
|
|
141
|
+
export const H1_END: i32 = 3
|
|
142
|
+
/** A `write`, `respond`, `end` or `sendFrame` that answered `H1_AGAIN` may be tried again. */
|
|
143
|
+
export const H1_WRITE: i32 = 4
|
|
144
|
+
/** A WebSocket message: `opcode`, `data[dataStart .. dataStart + dataLength)`. */
|
|
145
|
+
export const H1_WS_MESSAGE: i32 = 5
|
|
146
|
+
/** The WebSocket peer closed with `status` (1005 for none); the close is answered. Final. */
|
|
147
|
+
export const H1_WS_CLOSE: i32 = 6
|
|
148
|
+
/** The input broke a rule; `status` is the HTTP status or WebSocket close code sent. Final. */
|
|
149
|
+
export const H1_ERROR: i32 = 7
|
|
150
|
+
|
|
151
|
+
/** Nothing fits yet: Linux's EAGAIN, as `nish:net` spells it. */
|
|
152
|
+
export const H1_AGAIN: i32 = -11
|
|
153
|
+
/** Something the server may not send: Linux's EINVAL. */
|
|
154
|
+
export const H1_INVALID: i32 = -22
|
|
155
|
+
/** No exchange the call belongs to: Linux's EPIPE. */
|
|
156
|
+
export const H1_CLOSED: i32 = -32
|
|
157
|
+
/** `accept`: every slot was busy, so the connection was accepted and closed. Linux's ENOBUFS. */
|
|
158
|
+
export const H1_POOL_FULL: i32 = -105
|
|
159
|
+
|
|
160
|
+
/** Interest bits, the same as `nish/net/tls/record-server`'s: the low two are `pollModify`'s events. */
|
|
161
|
+
export const H1_WANT_READ: i32 = 1
|
|
162
|
+
export const H1_WANT_WRITE: i32 = 2
|
|
163
|
+
/** The slot's connection is over and every byte is sent: `close` it. */
|
|
164
|
+
export const H1_DONE: i32 = 16
|
|
165
|
+
|
|
166
|
+
/** The ALPN protocol identifier of HTTP/1.1 (RFC 7301 §6). */
|
|
167
|
+
export const H1_ALPN: string = "http/1.1"
|
|
168
|
+
|
|
169
|
+
/** A typed zero: a bare literal is an `f64` under `--number-mode f64`. */
|
|
170
|
+
const H1_ZERO: i32 = 0
|
|
171
|
+
|
|
172
|
+
/** A free slot's descriptor. */
|
|
173
|
+
const H1_FREE: i32 = -1
|
|
174
|
+
|
|
175
|
+
/** The address form's length, from `nish:net`. */
|
|
176
|
+
const H1_ADDRESS_SIZE: i32 = 18
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Output kept free of response bytes and frames, so that what the connection
|
|
180
|
+
* writes on its own — an error response, a pong, a close frame — always fits.
|
|
181
|
+
*/
|
|
182
|
+
const H1_CONTROL_RESERVE: i32 = 256
|
|
183
|
+
|
|
184
|
+
/** The most a chunk's framing adds: eight hex digits and two CRLFs. */
|
|
185
|
+
const H1_CHUNK_OVERHEAD: i32 = 12
|
|
186
|
+
|
|
187
|
+
/** The longest request line a head can need beyond the target: the method, two spaces, the version, CRLF. */
|
|
188
|
+
const H1_LINE_OVERHEAD: i32 = 44
|
|
189
|
+
|
|
190
|
+
/** The longest WebSocket frame header, which `WsDecoder` buffers with its whole frame: 2, 8 of length, 4 of mask. */
|
|
191
|
+
const H1_FRAME_HEADER: i32 = 14
|
|
192
|
+
|
|
193
|
+
// The connection's states.
|
|
194
|
+
const H1_S_HTTP: i32 = 0
|
|
195
|
+
const H1_S_WEBSOCKET: i32 = 1
|
|
196
|
+
/** Sending what is left, then done. */
|
|
197
|
+
const H1_S_CLOSING: i32 = 2
|
|
198
|
+
/** `H1_ERROR` was answered; sending what is left, then done. */
|
|
199
|
+
const H1_S_FAILED: i32 = 3
|
|
200
|
+
|
|
201
|
+
// Where the current exchange's response stands.
|
|
202
|
+
const H1_R_NONE: i32 = 0
|
|
203
|
+
const H1_R_BODY: i32 = 1
|
|
204
|
+
const H1_R_DONE: i32 = 2
|
|
205
|
+
|
|
206
|
+
/** Panics unless `value` is in `low .. high`: a cap no connection could work with is the program's mistake. */
|
|
207
|
+
const http1ServerCheckCap = (name: string, value: i32, low: i32, high: i32): void => {
|
|
208
|
+
if (value < low || value > high) {
|
|
209
|
+
panic(`Http1Config.${name}: ${value} is outside ${low} to ${high}`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Panics unless `buf[off .. off + len)` is inside `buf`: a window past its buffer is the caller's mistake. */
|
|
214
|
+
const http1ServerCheckWindow = (what: string, buf: u8[], off: i32, len: i32): void => {
|
|
215
|
+
const size: i32 = toI32(buf.length)
|
|
216
|
+
if (off < 0 || len < 0 || off > size || len > size - off) {
|
|
217
|
+
panic(`${what}: the window is outside the buffer`)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Copies `src[off .. off + n)` to `out` at `at`, which the caller has made sure holds it, and answers where it ends. */
|
|
222
|
+
const http1ServerCopy = (out: u8[], at: i32, src: u8[], off: i32, n: i32): i32 => {
|
|
223
|
+
for (let k: i32 = 0; k < n && at + k < toI32(out.length) && off + k < toI32(src.length); k++) {
|
|
224
|
+
if (at + k >= 0 && off + k >= 0) {
|
|
225
|
+
out[at + k] = src[off + k]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return at + n
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The caps of every connection made from it, fixed when the connection is
|
|
233
|
+
* made. Each is a number the program sets once, at start-up.
|
|
234
|
+
*/
|
|
235
|
+
export class Http1Config {
|
|
236
|
+
/** The longest request target; past it, 414. With `maxHeaderBytes`, at most 2^30 − 44, the most a head buffer holds. */
|
|
237
|
+
maxTarget: i32 = 8192
|
|
238
|
+
/** The longest header section, and trailer section; past it, 431. */
|
|
239
|
+
maxHeaderBytes: i32 = 16384
|
|
240
|
+
/** The most fields in a header section, trailers counted; past it, 431. */
|
|
241
|
+
maxHeaders: i32 = 100
|
|
242
|
+
/** The longest request body, declared or de-chunked; past it, 413. */
|
|
243
|
+
maxBody: i32 = 1073741824
|
|
244
|
+
/** The most body bytes one `H1_BODY` hands over, and how much is read from the socket at once. */
|
|
245
|
+
chunkSize: i32 = 16384
|
|
246
|
+
/** The output buffer, in bytes: the most of a response waiting for the socket. */
|
|
247
|
+
outputSize: i32 = 65536
|
|
248
|
+
/** The longest WebSocket message, its fragments together; past it, close 1009. At most 2^30 − 14, so a whole frame fits the decoder's input. */
|
|
249
|
+
maxMessage: i32 = 1048576
|
|
250
|
+
/** Milliseconds without a byte read or written after which `expire` closes a slot. */
|
|
251
|
+
idleTimeout: i32 = 60000
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The reason phrase a status is sent with; RFC 9112 §4 lets a client ignore it. */
|
|
255
|
+
const http1ServerReason = (status: i32): string => {
|
|
256
|
+
switch (status) {
|
|
257
|
+
case 100:
|
|
258
|
+
return "Continue"
|
|
259
|
+
case 101:
|
|
260
|
+
return "Switching Protocols"
|
|
261
|
+
case 200:
|
|
262
|
+
return "OK"
|
|
263
|
+
case 201:
|
|
264
|
+
return "Created"
|
|
265
|
+
case 204:
|
|
266
|
+
return "No Content"
|
|
267
|
+
case 206:
|
|
268
|
+
return "Partial Content"
|
|
269
|
+
case 301:
|
|
270
|
+
return "Moved Permanently"
|
|
271
|
+
case 302:
|
|
272
|
+
return "Found"
|
|
273
|
+
case 304:
|
|
274
|
+
return "Not Modified"
|
|
275
|
+
case 400:
|
|
276
|
+
return "Bad Request"
|
|
277
|
+
case 403:
|
|
278
|
+
return "Forbidden"
|
|
279
|
+
case 404:
|
|
280
|
+
return "Not Found"
|
|
281
|
+
case 405:
|
|
282
|
+
return "Method Not Allowed"
|
|
283
|
+
case 408:
|
|
284
|
+
return "Request Timeout"
|
|
285
|
+
case 413:
|
|
286
|
+
return "Content Too Large"
|
|
287
|
+
case 414:
|
|
288
|
+
return "URI Too Long"
|
|
289
|
+
case 426:
|
|
290
|
+
return "Upgrade Required"
|
|
291
|
+
case 431:
|
|
292
|
+
return "Request Header Fields Too Large"
|
|
293
|
+
case 500:
|
|
294
|
+
return "Internal Server Error"
|
|
295
|
+
case 501:
|
|
296
|
+
return "Not Implemented"
|
|
297
|
+
case 503:
|
|
298
|
+
return "Service Unavailable"
|
|
299
|
+
case 505:
|
|
300
|
+
return "HTTP Version Not Supported"
|
|
301
|
+
default:
|
|
302
|
+
return "Status"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Whether `name` is one of the fields the server decides and a program may not send. */
|
|
307
|
+
const http1ServerReserved = (name: u8[]): boolean =>
|
|
308
|
+
http1BytesAre(name, "connection") ||
|
|
309
|
+
http1BytesAre(name, "upgrade") ||
|
|
310
|
+
http1BytesAre(name, "keep-alive") ||
|
|
311
|
+
http1BytesAre(name, "transfer-encoding") ||
|
|
312
|
+
http1BytesAre(name, "content-length")
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* One connection's HTTP/1.1 server, sans-IO: bytes in through `feed`, events
|
|
316
|
+
* out of `next`, and the response the program writes in
|
|
317
|
+
* `output[outputStart .. outputEnd)` for its carrier to send.
|
|
318
|
+
*/
|
|
319
|
+
export class Http1Connection {
|
|
320
|
+
// References, then 64-bit fields, then 32-bit ones, then flags: the order
|
|
321
|
+
// the layout packs without padding.
|
|
322
|
+
config: Http1Config
|
|
323
|
+
/** The request parser: its head (`methodIs`, `targetIs`, `header`, spans) is the last `H1_REQUEST`'s. */
|
|
324
|
+
parser: Http1Parser
|
|
325
|
+
/** The frame decoder, once the slot has switched to WebSocket. */
|
|
326
|
+
ws: WsDecoder
|
|
327
|
+
/** Bytes to send are `output[outputStart .. outputEnd)`. */
|
|
328
|
+
output: u8[]
|
|
329
|
+
/** `H1_BODY`'s and `H1_WS_MESSAGE`'s bytes. */
|
|
330
|
+
data: u8[]
|
|
331
|
+
/** A ping's payload, kept until its pong fits. */
|
|
332
|
+
pong: u8[]
|
|
333
|
+
/** No fields, for the responses the connection writes itself: made once. */
|
|
334
|
+
noFields: u8[][]
|
|
335
|
+
/** When the carrier last moved a byte for the connection, in `monotonicNanos`. */
|
|
336
|
+
lastActive: i64 = 0
|
|
337
|
+
outputStart: i32 = 0
|
|
338
|
+
outputEnd: i32 = 0
|
|
339
|
+
dataStart: i32 = 0
|
|
340
|
+
dataLength: i32 = 0
|
|
341
|
+
/** `H1_WS_MESSAGE`'s opcode: text or binary. */
|
|
342
|
+
opcode: i32 = 0
|
|
343
|
+
/** `H1_ERROR`'s HTTP status or WebSocket close code, or `H1_WS_CLOSE`'s close code. */
|
|
344
|
+
status: i32 = 0
|
|
345
|
+
/** The most input the connection buffers, unread: the cap `inputRoom` holds it to. */
|
|
346
|
+
bufferLimit: i32 = 0
|
|
347
|
+
pongLength: i32 = 0
|
|
348
|
+
state: i32 = 0
|
|
349
|
+
response: i32 = 0
|
|
350
|
+
/**
|
|
351
|
+
* How the response's body is framed: a length (0 or more), `HTTP1_CHUNKED`,
|
|
352
|
+
* or `HTTP1_NO_BODY` for one that ends with the connection.
|
|
353
|
+
*/
|
|
354
|
+
responseFraming: i32 = 0
|
|
355
|
+
/** A response with a length: the bytes it still owes. */
|
|
356
|
+
responseLeft: i32 = 0
|
|
357
|
+
/** The response carries no body (HEAD, 204, 304): what is written is dropped. */
|
|
358
|
+
responseEmpty: boolean = false
|
|
359
|
+
/** A request's head has been read and its exchange is not over. */
|
|
360
|
+
requestOpen: boolean = false
|
|
361
|
+
/** The request's body has been read to its end. */
|
|
362
|
+
requestEnded: boolean = false
|
|
363
|
+
/** The connection closes once this exchange is over. */
|
|
364
|
+
closeAfter: boolean = false
|
|
365
|
+
/** The program accepted the request's WebSocket handshake. */
|
|
366
|
+
upgraded: boolean = false
|
|
367
|
+
/** A write was held back; `next` answers nothing but `H1_WRITE` until there is room. */
|
|
368
|
+
writeHeld: boolean = false
|
|
369
|
+
pongPending: boolean = false
|
|
370
|
+
/** The peer's byte stream has ended. */
|
|
371
|
+
inputEnded: boolean = false
|
|
372
|
+
|
|
373
|
+
/** A connection under `config`. A cap outside what the connection can work with panics. */
|
|
374
|
+
constructor(config: Http1Config) {
|
|
375
|
+
const big: i32 = 1073741824
|
|
376
|
+
const one: i32 = 1
|
|
377
|
+
const smallest: i32 = 64
|
|
378
|
+
const output: i32 = 1024
|
|
379
|
+
http1ServerCheckCap("maxTarget", config.maxTarget, one, big)
|
|
380
|
+
http1ServerCheckCap("maxHeaderBytes", config.maxHeaderBytes, smallest, big)
|
|
381
|
+
http1ServerCheckCap("maxHeaders", config.maxHeaders, one, big)
|
|
382
|
+
http1ServerCheckCap("maxBody", config.maxBody, H1_ZERO, big)
|
|
383
|
+
http1ServerCheckCap("chunkSize", config.chunkSize, one, big)
|
|
384
|
+
http1ServerCheckCap("outputSize", config.outputSize, output, big)
|
|
385
|
+
http1ServerCheckCap("maxMessage", config.maxMessage, H1_ZERO, big - H1_FRAME_HEADER)
|
|
386
|
+
http1ServerCheckCap("idleTimeout", config.idleTimeout, one, big)
|
|
387
|
+
// The buffers the peer's bytes fill grow by doubling and stop at 2^30
|
|
388
|
+
// (`Http1Parser`'s input and head, `WsDecoder`'s input), so a cap whose
|
|
389
|
+
// buffer could need more is refused here rather than reached by a peer:
|
|
390
|
+
// the head holds the request line and the header section together, and
|
|
391
|
+
// the input never holds more than the larger of the two lines or a chunk.
|
|
392
|
+
http1ServerCheckCap(
|
|
393
|
+
"maxTarget + maxHeaderBytes",
|
|
394
|
+
config.maxTarget + config.maxHeaderBytes,
|
|
395
|
+
one,
|
|
396
|
+
big - H1_LINE_OVERHEAD
|
|
397
|
+
)
|
|
398
|
+
this.config = config
|
|
399
|
+
this.parser = new Http1Parser(config.maxTarget, config.maxHeaderBytes, config.maxHeaders, config.maxBody)
|
|
400
|
+
this.parser.keepText = false
|
|
401
|
+
this.parser.maxChunk = config.chunkSize
|
|
402
|
+
this.ws = new WsDecoder(true, config.maxMessage)
|
|
403
|
+
this.output = new Array<u8>(config.outputSize)
|
|
404
|
+
this.data = this.output
|
|
405
|
+
this.pong = new Array<u8>(WS_MAX_CONTROL)
|
|
406
|
+
this.noFields = []
|
|
407
|
+
// A line must fit whole before the parser can read it, so the buffer
|
|
408
|
+
// holds the longest request line or field line however small a chunk is.
|
|
409
|
+
let limit: i32 = config.chunkSize
|
|
410
|
+
if (config.maxHeaderBytes + 2 > limit) {
|
|
411
|
+
limit = config.maxHeaderBytes + 2
|
|
412
|
+
}
|
|
413
|
+
if (config.maxTarget + H1_LINE_OVERHEAD > limit) {
|
|
414
|
+
limit = config.maxTarget + H1_LINE_OVERHEAD
|
|
415
|
+
}
|
|
416
|
+
this.bufferLimit = limit
|
|
417
|
+
this.restart()
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Puts the connection back as the constructor left it, for the next peer. It allocates nothing. */
|
|
421
|
+
restart(): void {
|
|
422
|
+
this.parser.restart()
|
|
423
|
+
this.ws.reset()
|
|
424
|
+
this.outputStart = 0
|
|
425
|
+
this.outputEnd = 0
|
|
426
|
+
this.data = this.output
|
|
427
|
+
this.dataStart = 0
|
|
428
|
+
this.dataLength = 0
|
|
429
|
+
this.opcode = 0
|
|
430
|
+
this.status = 0
|
|
431
|
+
this.pongLength = 0
|
|
432
|
+
this.state = H1_S_HTTP
|
|
433
|
+
this.response = H1_R_NONE
|
|
434
|
+
this.responseFraming = 0
|
|
435
|
+
this.responseLeft = 0
|
|
436
|
+
this.responseEmpty = false
|
|
437
|
+
this.requestOpen = false
|
|
438
|
+
this.requestEnded = false
|
|
439
|
+
this.closeAfter = false
|
|
440
|
+
this.upgraded = false
|
|
441
|
+
this.writeHeld = false
|
|
442
|
+
this.pongPending = false
|
|
443
|
+
this.inputEnded = false
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ---- Bytes in and out ---------------------------------------------------------
|
|
447
|
+
|
|
448
|
+
/** The most input the connection holds unread: what `inputRoom` counts against. */
|
|
449
|
+
inputLimit(): i32 {
|
|
450
|
+
return this.state === H1_S_WEBSOCKET ? this.config.maxMessage + 14 : this.bufferLimit
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** How many bytes `feed` would take now: none while a write is held back or once the connection is closing. */
|
|
454
|
+
inputRoom(): i32 {
|
|
455
|
+
if (this.writeHeld || this.inputEnded || this.state === H1_S_CLOSING || this.state === H1_S_FAILED) {
|
|
456
|
+
return 0
|
|
457
|
+
}
|
|
458
|
+
const held: i32 = this.state === H1_S_WEBSOCKET ? this.ws.end - this.ws.start : this.parser.buffered()
|
|
459
|
+
const room: i32 = this.inputLimit() - held
|
|
460
|
+
return room > 0 ? room : 0
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Takes as much of `buf[off .. off + len)` as `inputRoom` allows and
|
|
465
|
+
* answers how much; the rest is the caller's to feed again later. A window
|
|
466
|
+
* outside `buf` panics.
|
|
467
|
+
*/
|
|
468
|
+
feed(buf: u8[], off: i32, len: i32): i32 {
|
|
469
|
+
http1ServerCheckWindow("Http1Connection.feed", buf, off, len)
|
|
470
|
+
const room: i32 = this.inputRoom()
|
|
471
|
+
const n: i32 = len < room ? len : room
|
|
472
|
+
if (n <= 0) {
|
|
473
|
+
return 0
|
|
474
|
+
}
|
|
475
|
+
if (this.state === H1_S_WEBSOCKET) {
|
|
476
|
+
this.ws.feed(buf, off, n)
|
|
477
|
+
} else {
|
|
478
|
+
this.parser.feed(buf, off, n)
|
|
479
|
+
}
|
|
480
|
+
return n
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** The peer's byte stream has ended: once what it sent is answered, the connection is done. */
|
|
484
|
+
endInput(): void {
|
|
485
|
+
this.inputEnded = true
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** The socket failed: nothing more is sent or read, and the connection is done. */
|
|
489
|
+
abort(): void {
|
|
490
|
+
this.state = H1_S_FAILED
|
|
491
|
+
this.outputStart = 0
|
|
492
|
+
this.outputEnd = 0
|
|
493
|
+
this.pongPending = false
|
|
494
|
+
this.writeHeld = false
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Marks the first `n` bytes of the output sent. */
|
|
498
|
+
consume(n: i32): void {
|
|
499
|
+
const pending: i32 = this.outputEnd - this.outputStart
|
|
500
|
+
this.outputStart = this.outputStart + (n < pending ? n : pending)
|
|
501
|
+
if (this.outputStart === this.outputEnd) {
|
|
502
|
+
this.outputStart = 0
|
|
503
|
+
this.outputEnd = 0
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Whether the output holds bytes to send. */
|
|
508
|
+
wantsWrite(): boolean {
|
|
509
|
+
return this.outputEnd > this.outputStart
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Whether the connection is over and everything is sent: the carrier closes it. */
|
|
513
|
+
isDone(): boolean {
|
|
514
|
+
return (
|
|
515
|
+
(this.state === H1_S_CLOSING || this.state === H1_S_FAILED) && !this.wantsWrite() && !this.pongPending
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** The room at the end of the output, once what was sent is moved out of the way. */
|
|
520
|
+
room(): i32 {
|
|
521
|
+
const output: u8[] = this.output
|
|
522
|
+
const live: i32 = this.outputEnd - this.outputStart
|
|
523
|
+
if (this.outputStart > 0) {
|
|
524
|
+
for (
|
|
525
|
+
let k: i32 = 0;
|
|
526
|
+
k < live && k < toI32(output.length) && this.outputStart + k < toI32(output.length);
|
|
527
|
+
k++
|
|
528
|
+
) {
|
|
529
|
+
output[k] = output[this.outputStart + k]
|
|
530
|
+
}
|
|
531
|
+
this.outputStart = 0
|
|
532
|
+
this.outputEnd = live
|
|
533
|
+
}
|
|
534
|
+
return toI32(output.length) - this.outputEnd
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** The room a response or a frame may use: all but the control reserve. */
|
|
538
|
+
bodyRoom(): i32 {
|
|
539
|
+
const r: i32 = this.room() - H1_CONTROL_RESERVE
|
|
540
|
+
return r > 0 ? r : 0
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ---- Events -------------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
/** The next event; see the module comment. */
|
|
546
|
+
next(): i32 {
|
|
547
|
+
if (this.state === H1_S_FAILED) {
|
|
548
|
+
return H1_ERROR
|
|
549
|
+
}
|
|
550
|
+
if (this.writeHeld) {
|
|
551
|
+
// Half the output free before the program is asked to write again, so
|
|
552
|
+
// that a slow reader is not answered one small write at a time.
|
|
553
|
+
if (this.bodyRoom() * 2 < toI32(this.output.length) - H1_CONTROL_RESERVE) {
|
|
554
|
+
return H1_NEED_MORE
|
|
555
|
+
}
|
|
556
|
+
this.writeHeld = false
|
|
557
|
+
return H1_WRITE
|
|
558
|
+
}
|
|
559
|
+
if (this.state === H1_S_CLOSING) {
|
|
560
|
+
return H1_NEED_MORE
|
|
561
|
+
}
|
|
562
|
+
if (this.state === H1_S_WEBSOCKET) {
|
|
563
|
+
return this.nextFrame()
|
|
564
|
+
}
|
|
565
|
+
return this.nextRequest()
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** `next` while the connection speaks HTTP. */
|
|
569
|
+
nextRequest(): i32 {
|
|
570
|
+
while (true) {
|
|
571
|
+
if (this.requestEnded) {
|
|
572
|
+
if (this.response !== H1_R_DONE) {
|
|
573
|
+
return H1_NEED_MORE
|
|
574
|
+
}
|
|
575
|
+
// The exchange is over: the next request, a WebSocket, or the close.
|
|
576
|
+
this.requestOpen = false
|
|
577
|
+
this.requestEnded = false
|
|
578
|
+
this.response = H1_R_NONE
|
|
579
|
+
if (this.upgraded) {
|
|
580
|
+
const p: Http1Parser = this.parser
|
|
581
|
+
this.ws.reset()
|
|
582
|
+
this.ws.feed(p.buf, p.start, p.buffered())
|
|
583
|
+
this.state = H1_S_WEBSOCKET
|
|
584
|
+
return this.nextFrame()
|
|
585
|
+
}
|
|
586
|
+
if (this.closeAfter) {
|
|
587
|
+
this.state = H1_S_CLOSING
|
|
588
|
+
return H1_NEED_MORE
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
const event: i32 = this.parser.next()
|
|
592
|
+
if (event === HTTP1_HEAD) {
|
|
593
|
+
this.requestOpen = true
|
|
594
|
+
this.response = H1_R_NONE
|
|
595
|
+
this.closeAfter = !this.parser.keepAlive
|
|
596
|
+
this.upgraded = false
|
|
597
|
+
return H1_REQUEST
|
|
598
|
+
}
|
|
599
|
+
if (event === HTTP1_BODY) {
|
|
600
|
+
this.data = this.parser.body
|
|
601
|
+
this.dataStart = this.parser.bodyOff
|
|
602
|
+
this.dataLength = this.parser.bodyLen
|
|
603
|
+
return H1_BODY
|
|
604
|
+
}
|
|
605
|
+
if (event === HTTP1_END) {
|
|
606
|
+
this.requestEnded = true
|
|
607
|
+
return H1_END
|
|
608
|
+
}
|
|
609
|
+
if (event === HTTP1_UPGRADE) {
|
|
610
|
+
// Reached only once a response that did not switch is over.
|
|
611
|
+
this.parser.declineUpgrade()
|
|
612
|
+
} else if (event === HTTP1_ERROR) {
|
|
613
|
+
return this.fail(this.parser.status)
|
|
614
|
+
} else if (event === HTTP1_CLOSED) {
|
|
615
|
+
this.state = H1_S_CLOSING
|
|
616
|
+
return H1_NEED_MORE
|
|
617
|
+
} else {
|
|
618
|
+
// HTTP1_NEED_MORE. A request cut off by the end of the stream is
|
|
619
|
+
// over: there is no one left to answer.
|
|
620
|
+
if (this.inputEnded) {
|
|
621
|
+
this.state = H1_S_CLOSING
|
|
622
|
+
}
|
|
623
|
+
return H1_NEED_MORE
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Refuses the request with `status`: the response that says so, when none
|
|
630
|
+
* has started, and the close after it. Answers `H1_ERROR`.
|
|
631
|
+
*/
|
|
632
|
+
fail(status: i32): i32 {
|
|
633
|
+
this.status = status
|
|
634
|
+
if (this.response === H1_R_NONE) {
|
|
635
|
+
// The control reserve keeps room for this whatever the output holds.
|
|
636
|
+
this.room()
|
|
637
|
+
const end: i32 = http1WriteResponseHead(
|
|
638
|
+
this.output,
|
|
639
|
+
this.outputEnd,
|
|
640
|
+
status,
|
|
641
|
+
http1ServerReason(status),
|
|
642
|
+
this.noFields,
|
|
643
|
+
this.noFields,
|
|
644
|
+
H1_ZERO,
|
|
645
|
+
"close"
|
|
646
|
+
)
|
|
647
|
+
if (end >= 0) {
|
|
648
|
+
this.outputEnd = end
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
this.state = H1_S_FAILED
|
|
652
|
+
this.writeHeld = false
|
|
653
|
+
return H1_ERROR
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** `next` once the slot has switched to WebSocket. */
|
|
657
|
+
nextFrame(): i32 {
|
|
658
|
+
while (true) {
|
|
659
|
+
if (this.pongPending && !this.sendPong()) {
|
|
660
|
+
return H1_NEED_MORE
|
|
661
|
+
}
|
|
662
|
+
const event: i32 = this.ws.next()
|
|
663
|
+
if (event === WS_MESSAGE) {
|
|
664
|
+
this.opcode = this.ws.opcode
|
|
665
|
+
this.data = this.ws.data
|
|
666
|
+
this.dataStart = 0
|
|
667
|
+
this.dataLength = this.ws.dataLen
|
|
668
|
+
return H1_WS_MESSAGE
|
|
669
|
+
}
|
|
670
|
+
if (event === WS_PING) {
|
|
671
|
+
const n: i32 = this.ws.dataLen
|
|
672
|
+
for (let k: i32 = 0; k < n && k < toI32(this.pong.length) && k < toI32(this.ws.data.length); k++) {
|
|
673
|
+
this.pong[k] = this.ws.data[k]
|
|
674
|
+
}
|
|
675
|
+
this.pongLength = n
|
|
676
|
+
this.pongPending = true
|
|
677
|
+
} else if (event === WS_CLOSE) {
|
|
678
|
+
this.status = this.ws.closeCode
|
|
679
|
+
this.sendClose(this.ws.closeCode)
|
|
680
|
+
this.state = H1_S_CLOSING
|
|
681
|
+
return H1_WS_CLOSE
|
|
682
|
+
} else if (event === WS_ERROR) {
|
|
683
|
+
this.status = this.ws.closeCode
|
|
684
|
+
this.sendClose(this.ws.closeCode)
|
|
685
|
+
this.state = H1_S_FAILED
|
|
686
|
+
return H1_ERROR
|
|
687
|
+
} else if (event === WS_NEED_MORE) {
|
|
688
|
+
if (this.inputEnded) {
|
|
689
|
+
this.state = H1_S_CLOSING
|
|
690
|
+
}
|
|
691
|
+
return H1_NEED_MORE
|
|
692
|
+
}
|
|
693
|
+
// A pong needs no answer: read on.
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Writes the pending pong if it fits, from the control reserve; answers whether it went. */
|
|
698
|
+
sendPong(): boolean {
|
|
699
|
+
this.room()
|
|
700
|
+
const end: i32 = websocketWriteFrame(
|
|
701
|
+
this.output,
|
|
702
|
+
this.outputEnd,
|
|
703
|
+
true,
|
|
704
|
+
WS_OP_PONG,
|
|
705
|
+
this.pong,
|
|
706
|
+
H1_ZERO,
|
|
707
|
+
this.pongLength,
|
|
708
|
+
null
|
|
709
|
+
)
|
|
710
|
+
if (end < 0) {
|
|
711
|
+
return false
|
|
712
|
+
}
|
|
713
|
+
this.outputEnd = end
|
|
714
|
+
this.pongPending = false
|
|
715
|
+
return true
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Writes a close frame with `code` (none for 1005), from the control reserve. */
|
|
719
|
+
sendClose(code: i32): void {
|
|
720
|
+
this.room()
|
|
721
|
+
const payload: u8[] | null = websocketClosePayload(code, "")
|
|
722
|
+
// A code that may not be sent (1005, for a close that had none) is
|
|
723
|
+
// answered with an empty close.
|
|
724
|
+
const body: u8[] = payload === null ? this.pong : payload
|
|
725
|
+
const n: i32 = payload === null ? H1_ZERO : toI32(body.length)
|
|
726
|
+
const end: i32 = websocketWriteFrame(
|
|
727
|
+
this.output,
|
|
728
|
+
this.outputEnd,
|
|
729
|
+
true,
|
|
730
|
+
WS_OP_CLOSE,
|
|
731
|
+
body,
|
|
732
|
+
H1_ZERO,
|
|
733
|
+
n,
|
|
734
|
+
null
|
|
735
|
+
)
|
|
736
|
+
if (end >= 0) {
|
|
737
|
+
this.outputEnd = end
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// ---- The response -------------------------------------------------------------
|
|
742
|
+
|
|
743
|
+
/** Whether the program may answer the current request now. */
|
|
744
|
+
answerable(): boolean {
|
|
745
|
+
return this.state === H1_S_HTTP && this.requestOpen && this.response === H1_R_NONE
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Answers the current request with `status` and the fields `names[k]`:
|
|
750
|
+
* `values[k]`, and the framing `bodyLength` asks for: a length, or
|
|
751
|
+
* `HTTP1_CHUNKED`, or `HTTP1_NO_BODY` for a 1xx, a 204 or a 304. A 1xx
|
|
752
|
+
* (but 101) is interim and the final status follows it. Answers 0;
|
|
753
|
+
* `H1_INVALID` for a status outside 100 to 599 or 101, a reserved field, a
|
|
754
|
+
* name or value `http1WriteResponseHead` refuses, or a head larger than the
|
|
755
|
+
* output; `H1_AGAIN` when the output has no room for it yet (`H1_WRITE`
|
|
756
|
+
* says when to try again); `H1_CLOSED` with no request to answer.
|
|
757
|
+
*/
|
|
758
|
+
respond(status: i32, names: u8[][], values: u8[][], bodyLength: i32): i32 {
|
|
759
|
+
if (!this.answerable()) {
|
|
760
|
+
return H1_CLOSED
|
|
761
|
+
}
|
|
762
|
+
if (status < 100 || status > 599 || status === 101) {
|
|
763
|
+
return H1_INVALID
|
|
764
|
+
}
|
|
765
|
+
for (const name of names) {
|
|
766
|
+
if (http1ServerReserved(name)) {
|
|
767
|
+
return H1_INVALID
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const interim: boolean = status < 200
|
|
771
|
+
const empty: boolean = interim || status === 204 || status === 304 || this.parser.methodIs("HEAD")
|
|
772
|
+
if (!empty && bodyLength === HTTP1_NO_BODY) {
|
|
773
|
+
return H1_INVALID
|
|
774
|
+
}
|
|
775
|
+
let framing: i32 = bodyLength
|
|
776
|
+
let close: boolean = this.closeAfter
|
|
777
|
+
// Chunked is HTTP/1.1's: an HTTP/1.0 client reads the body to the close.
|
|
778
|
+
if (!empty && bodyLength === HTTP1_CHUNKED && this.parser.minor === 0) {
|
|
779
|
+
framing = HTTP1_NO_BODY
|
|
780
|
+
close = true
|
|
781
|
+
}
|
|
782
|
+
let connection: string = ""
|
|
783
|
+
if (!interim && close) {
|
|
784
|
+
connection = "close"
|
|
785
|
+
} else if (!interim && this.parser.minor === 0) {
|
|
786
|
+
connection = "keep-alive"
|
|
787
|
+
}
|
|
788
|
+
const room: i32 = this.bodyRoom()
|
|
789
|
+
const end: i32 = http1WriteResponseHead(
|
|
790
|
+
this.output,
|
|
791
|
+
this.outputEnd,
|
|
792
|
+
status,
|
|
793
|
+
http1ServerReason(status),
|
|
794
|
+
names,
|
|
795
|
+
values,
|
|
796
|
+
framing,
|
|
797
|
+
connection
|
|
798
|
+
)
|
|
799
|
+
if (end === HTTP1_REFUSED) {
|
|
800
|
+
return H1_INVALID
|
|
801
|
+
}
|
|
802
|
+
if (end < 0 || end - this.outputEnd > room) {
|
|
803
|
+
// The head was not written, or ate into the reserve: take it back.
|
|
804
|
+
if (end - this.outputEnd > toI32(this.output.length) - H1_CONTROL_RESERVE) {
|
|
805
|
+
return H1_INVALID
|
|
806
|
+
}
|
|
807
|
+
if (this.outputEnd === 0) {
|
|
808
|
+
return H1_INVALID
|
|
809
|
+
}
|
|
810
|
+
this.writeHeld = true
|
|
811
|
+
return H1_AGAIN
|
|
812
|
+
}
|
|
813
|
+
this.outputEnd = end
|
|
814
|
+
if (interim) {
|
|
815
|
+
return 0
|
|
816
|
+
}
|
|
817
|
+
this.closeAfter = close
|
|
818
|
+
this.response = H1_R_BODY
|
|
819
|
+
this.responseFraming = framing
|
|
820
|
+
this.responseLeft = framing >= 0 ? framing : 0
|
|
821
|
+
this.responseEmpty = empty
|
|
822
|
+
return 0
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Writes `buf[off .. off + len)` of the response body, as much as the output
|
|
827
|
+
* has room for, and answers how much: a chunk of a chunked body, or bytes
|
|
828
|
+
* of one with a length (more than it still owes is `H1_INVALID`). When not
|
|
829
|
+
* everything was taken, `H1_WRITE` says when to try the rest; `H1_AGAIN`
|
|
830
|
+
* when nothing was. A response with no body takes everything and sends
|
|
831
|
+
* nothing. `H1_CLOSED` with no response under way. A window outside `buf`
|
|
832
|
+
* panics.
|
|
833
|
+
*/
|
|
834
|
+
write(buf: u8[], off: i32, len: i32): i32 {
|
|
835
|
+
http1ServerCheckWindow("Http1Connection.write", buf, off, len)
|
|
836
|
+
if (this.response !== H1_R_BODY || this.state !== H1_S_HTTP) {
|
|
837
|
+
return H1_CLOSED
|
|
838
|
+
}
|
|
839
|
+
if (this.responseEmpty || len === 0) {
|
|
840
|
+
return len
|
|
841
|
+
}
|
|
842
|
+
const chunked: boolean = this.responseFraming === HTTP1_CHUNKED
|
|
843
|
+
if (this.responseFraming >= 0 && len > this.responseLeft) {
|
|
844
|
+
return H1_INVALID
|
|
845
|
+
}
|
|
846
|
+
const room: i32 = this.bodyRoom()
|
|
847
|
+
let take: i32 = len
|
|
848
|
+
if (chunked) {
|
|
849
|
+
if (take > room - H1_CHUNK_OVERHEAD) {
|
|
850
|
+
take = room - H1_CHUNK_OVERHEAD
|
|
851
|
+
}
|
|
852
|
+
} else if (take > room) {
|
|
853
|
+
take = room
|
|
854
|
+
}
|
|
855
|
+
if (take <= 0) {
|
|
856
|
+
this.writeHeld = true
|
|
857
|
+
return H1_AGAIN
|
|
858
|
+
}
|
|
859
|
+
if (chunked) {
|
|
860
|
+
this.outputEnd = http1WriteChunk(this.output, this.outputEnd, buf, off, take)
|
|
861
|
+
} else {
|
|
862
|
+
this.outputEnd = http1ServerCopy(this.output, this.outputEnd, buf, off, take)
|
|
863
|
+
if (this.responseFraming >= 0) {
|
|
864
|
+
this.responseLeft = this.responseLeft - take
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (take < len) {
|
|
868
|
+
this.writeHeld = true
|
|
869
|
+
}
|
|
870
|
+
return take
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Ends the response: the last chunk of a chunked body. Answers 0;
|
|
875
|
+
* `H1_INVALID` for a response with a length that still owes bytes;
|
|
876
|
+
* `H1_AGAIN` when the last chunk does not fit yet; `H1_CLOSED` with no
|
|
877
|
+
* response under way.
|
|
878
|
+
*/
|
|
879
|
+
end(): i32 {
|
|
880
|
+
if (this.response !== H1_R_BODY || this.state !== H1_S_HTTP) {
|
|
881
|
+
return H1_CLOSED
|
|
882
|
+
}
|
|
883
|
+
if (this.responseEmpty) {
|
|
884
|
+
this.response = H1_R_DONE
|
|
885
|
+
return 0
|
|
886
|
+
}
|
|
887
|
+
if (this.responseFraming >= 0 && this.responseLeft > 0) {
|
|
888
|
+
return H1_INVALID
|
|
889
|
+
}
|
|
890
|
+
if (this.responseFraming === HTTP1_CHUNKED) {
|
|
891
|
+
// The last chunk comes out of the body's room, not the reserve.
|
|
892
|
+
if (this.bodyRoom() < 5) {
|
|
893
|
+
this.writeHeld = true
|
|
894
|
+
return H1_AGAIN
|
|
895
|
+
}
|
|
896
|
+
this.outputEnd = http1WriteLastChunk(this.output, this.outputEnd)
|
|
897
|
+
}
|
|
898
|
+
this.response = H1_R_DONE
|
|
899
|
+
// A response with neither a length nor chunks ends with the connection.
|
|
900
|
+
if (this.responseFraming === HTTP1_NO_BODY) {
|
|
901
|
+
this.closeAfter = true
|
|
902
|
+
}
|
|
903
|
+
return 0
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ---- WebSocket ----------------------------------------------------------------
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Accepts the current request's WebSocket opening handshake (RFC 6455
|
|
910
|
+
* §4.2.2) with `protocol`, empty for none: the 101 is queued, and once the
|
|
911
|
+
* request has ended the slot reads frames. Answers 0; `H1_INVALID` when the
|
|
912
|
+
* request is not a valid opening handshake (the program answers 400, or 426
|
|
913
|
+
* for the wrong version) or `protocol` is not a token; `H1_AGAIN` when the
|
|
914
|
+
* output has no room yet; `H1_CLOSED` with no request to answer.
|
|
915
|
+
*/
|
|
916
|
+
acceptWebSocket(protocol: string): i32 {
|
|
917
|
+
if (!this.answerable()) {
|
|
918
|
+
return H1_CLOSED
|
|
919
|
+
}
|
|
920
|
+
const key: string | null = websocketRequestKey(this.parser)
|
|
921
|
+
if (key === null) {
|
|
922
|
+
return H1_INVALID
|
|
923
|
+
}
|
|
924
|
+
const head: u8[] | null = websocketUpgradeResponse(key, protocol)
|
|
925
|
+
if (head === null) {
|
|
926
|
+
return H1_INVALID
|
|
927
|
+
}
|
|
928
|
+
const n: i32 = toI32(head.length)
|
|
929
|
+
if (n > this.bodyRoom()) {
|
|
930
|
+
this.writeHeld = true
|
|
931
|
+
return H1_AGAIN
|
|
932
|
+
}
|
|
933
|
+
this.outputEnd = http1ServerCopy(this.output, this.outputEnd, head, H1_ZERO, n)
|
|
934
|
+
this.response = H1_R_DONE
|
|
935
|
+
this.upgraded = true
|
|
936
|
+
this.closeAfter = false
|
|
937
|
+
return 0
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Sends one WebSocket frame, unmasked as a server's are (§5.1): a whole
|
|
942
|
+
* text or binary message, a fragment of one (`fin` false, then
|
|
943
|
+
* continuations), a ping or a pong. Answers 0; `H1_INVALID` for a frame
|
|
944
|
+
* `websocketWriteFrame` refuses, a close (that is `closeWebSocket`), or one
|
|
945
|
+
* larger than the output; `H1_AGAIN` when it does not fit yet; `H1_CLOSED`
|
|
946
|
+
* when the slot is not an open WebSocket. A window outside `buf` panics.
|
|
947
|
+
*/
|
|
948
|
+
sendFrame(fin: boolean, opcode: i32, buf: u8[], off: i32, len: i32): i32 {
|
|
949
|
+
http1ServerCheckWindow("Http1Connection.sendFrame", buf, off, len)
|
|
950
|
+
if (this.state !== H1_S_WEBSOCKET) {
|
|
951
|
+
return H1_CLOSED
|
|
952
|
+
}
|
|
953
|
+
if (opcode === WS_OP_CLOSE || len > toI32(this.output.length) - H1_CONTROL_RESERVE - 14) {
|
|
954
|
+
return H1_INVALID
|
|
955
|
+
}
|
|
956
|
+
if (websocketFrameSize(len, false) > this.bodyRoom()) {
|
|
957
|
+
this.writeHeld = true
|
|
958
|
+
return H1_AGAIN
|
|
959
|
+
}
|
|
960
|
+
const end: i32 = websocketWriteFrame(this.output, this.outputEnd, fin, opcode, buf, off, len, null)
|
|
961
|
+
if (end < 0) {
|
|
962
|
+
return H1_INVALID
|
|
963
|
+
}
|
|
964
|
+
this.outputEnd = end
|
|
965
|
+
return 0
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Starts the closing handshake (§5.5.1, §7.1.2) with `code`: a close frame,
|
|
970
|
+
* then the connection is done once it is sent. Answers 0, `H1_INVALID` for a
|
|
971
|
+
* code that may not be sent, or `H1_CLOSED` when the slot is not an open
|
|
972
|
+
* WebSocket.
|
|
973
|
+
*/
|
|
974
|
+
closeWebSocket(code: i32): i32 {
|
|
975
|
+
if (this.state !== H1_S_WEBSOCKET) {
|
|
976
|
+
return H1_CLOSED
|
|
977
|
+
}
|
|
978
|
+
if (websocketClosePayload(code, "") === null) {
|
|
979
|
+
return H1_INVALID
|
|
980
|
+
}
|
|
981
|
+
this.sendClose(code)
|
|
982
|
+
this.state = H1_S_CLOSING
|
|
983
|
+
return 0
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** Whether the slot has switched to WebSocket and is open. */
|
|
987
|
+
isWebSocket(): boolean {
|
|
988
|
+
return this.state === H1_S_WEBSOCKET
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** Whether nothing has moved for longer than the idle timeout, at `now` (`monotonicNanos`). */
|
|
992
|
+
idle(now: i64): boolean {
|
|
993
|
+
return now - this.lastActive > toI64(this.config.idleTimeout) * toI64(1000000)
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// ---- Carriers -------------------------------------------------------------------
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* A pool of HTTP/1.1 connections over plain TCP on one listening socket.
|
|
1001
|
+
* `slot`s are the indices `accept` answers; a slot is the program's from
|
|
1002
|
+
* `accept` until it calls `close`, and its descriptor is `fd(slot)`.
|
|
1003
|
+
*/
|
|
1004
|
+
export class Http1Server {
|
|
1005
|
+
config: Http1Config
|
|
1006
|
+
connections: Http1Connection[]
|
|
1007
|
+
/** Each slot's descriptor, or -1 while it is free. */
|
|
1008
|
+
fds: i32[]
|
|
1009
|
+
/** Where the socket is read into before the connection takes the bytes. */
|
|
1010
|
+
scratch: u8[]
|
|
1011
|
+
/** The last accepted peer's address, in `nish:net`'s 18-byte form. */
|
|
1012
|
+
peer: u8[]
|
|
1013
|
+
/** The listening socket, which the program made with `tcpListen` and still owns. */
|
|
1014
|
+
listener: i32 = -1
|
|
1015
|
+
|
|
1016
|
+
/** A pool of `size` slots, at least one, serving `config` on `listener`. Every slot's buffers are allocated here. */
|
|
1017
|
+
constructor(config: Http1Config, listener: i32, size: i32) {
|
|
1018
|
+
this.config = config
|
|
1019
|
+
this.listener = listener
|
|
1020
|
+
this.connections = []
|
|
1021
|
+
this.fds = []
|
|
1022
|
+
const slots: i32 = size < 1 ? 1 : size
|
|
1023
|
+
for (let k: i32 = 0; k < slots; k++) {
|
|
1024
|
+
this.connections.push(new Http1Connection(config))
|
|
1025
|
+
this.fds.push(H1_FREE)
|
|
1026
|
+
}
|
|
1027
|
+
this.scratch = new Array<u8>(config.chunkSize)
|
|
1028
|
+
this.peer = new Array<u8>(H1_ADDRESS_SIZE)
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** How many slots the pool has. */
|
|
1032
|
+
size(): i32 {
|
|
1033
|
+
return toI32(this.fds.length)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** How many slots hold a connection. */
|
|
1037
|
+
busy(): i32 {
|
|
1038
|
+
let n: i32 = 0
|
|
1039
|
+
for (const fd of this.fds) {
|
|
1040
|
+
if (fd >= 0) {
|
|
1041
|
+
n = n + 1
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return n
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/** Whether `slot` is a slot of this pool that holds a connection. */
|
|
1048
|
+
holds(slot: i32): boolean {
|
|
1049
|
+
return slot >= 0 && slot < toI32(this.fds.length) && this.fds[slot] >= 0
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/** The descriptor of `slot`, for `pollAdd`; -1 for a free or unknown one. */
|
|
1053
|
+
fd(slot: i32): i32 {
|
|
1054
|
+
return this.holds(slot) ? this.fds[slot] : -1
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** The connection of `slot`; a slot out of range names the first. */
|
|
1058
|
+
connection(slot: i32): Http1Connection {
|
|
1059
|
+
const at: i32 = slot >= 0 && slot < toI32(this.connections.length) ? slot : H1_ZERO
|
|
1060
|
+
return this.connections[at]
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Accepts the next waiting connection into a free slot and answers the
|
|
1065
|
+
* slot; -11 when none is waiting, `H1_POOL_FULL` when every slot is busy
|
|
1066
|
+
* (the connection is closed), or `tcpAccept`'s own failure.
|
|
1067
|
+
*/
|
|
1068
|
+
accept(): i32 {
|
|
1069
|
+
const fd: i32 = tcpAccept(this.listener, this.peer)
|
|
1070
|
+
if (fd < 0) {
|
|
1071
|
+
return fd
|
|
1072
|
+
}
|
|
1073
|
+
for (let k: i32 = 0; k < toI32(this.fds.length) && k < toI32(this.connections.length); k++) {
|
|
1074
|
+
if (this.fds[k] < 0) {
|
|
1075
|
+
this.fds[k] = fd
|
|
1076
|
+
const conn: Http1Connection = this.connections[k]
|
|
1077
|
+
conn.restart()
|
|
1078
|
+
conn.lastActive = monotonicNanos()
|
|
1079
|
+
return k
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
netClose(fd)
|
|
1083
|
+
return H1_POOL_FULL
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/** The socket of `slot` is readable: reads it while the connection has room, then sends. Answers the interest. */
|
|
1087
|
+
readable(slot: i32): i32 {
|
|
1088
|
+
if (!this.holds(slot)) {
|
|
1089
|
+
return H1_DONE
|
|
1090
|
+
}
|
|
1091
|
+
const conn: Http1Connection = this.connections[slot]
|
|
1092
|
+
const fd: i32 = this.fds[slot]
|
|
1093
|
+
let moved: boolean = false
|
|
1094
|
+
while (true) {
|
|
1095
|
+
const room: i32 = conn.inputRoom()
|
|
1096
|
+
const want: i32 = room < toI32(this.scratch.length) ? room : toI32(this.scratch.length)
|
|
1097
|
+
if (want <= 0) {
|
|
1098
|
+
break
|
|
1099
|
+
}
|
|
1100
|
+
const n: i32 = netRead(fd, this.scratch, H1_ZERO, want)
|
|
1101
|
+
if (n <= 0) {
|
|
1102
|
+
if (n === 0) {
|
|
1103
|
+
conn.endInput()
|
|
1104
|
+
} else if (n !== H1_AGAIN) {
|
|
1105
|
+
conn.abort()
|
|
1106
|
+
}
|
|
1107
|
+
break
|
|
1108
|
+
}
|
|
1109
|
+
moved = true
|
|
1110
|
+
conn.feed(this.scratch, H1_ZERO, n)
|
|
1111
|
+
}
|
|
1112
|
+
if (moved) {
|
|
1113
|
+
conn.lastActive = monotonicNanos()
|
|
1114
|
+
}
|
|
1115
|
+
return this.flush(slot)
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** The socket of `slot` is writable. Answers the interest. */
|
|
1119
|
+
writable(slot: i32): i32 {
|
|
1120
|
+
return this.flush(slot)
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* `slot`'s connection's next event; `H1_ERROR` for a free slot. While a
|
|
1125
|
+
* write is held back the output is sent first, so that the room it waits
|
|
1126
|
+
* for is made here and `H1_WRITE` comes in the same pass; otherwise what
|
|
1127
|
+
* the program writes in answer to one event and the next goes out together
|
|
1128
|
+
* at `flush`, as one segment rather than several.
|
|
1129
|
+
*/
|
|
1130
|
+
next(slot: i32): i32 {
|
|
1131
|
+
if (!this.holds(slot)) {
|
|
1132
|
+
return H1_ERROR
|
|
1133
|
+
}
|
|
1134
|
+
const conn: Http1Connection = this.connections[slot]
|
|
1135
|
+
if (conn.writeHeld) {
|
|
1136
|
+
this.send(slot)
|
|
1137
|
+
}
|
|
1138
|
+
return conn.next()
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Sends what `slot`'s connection holds until the socket would block, and
|
|
1143
|
+
* answers the interest: `H1_DONE` once the connection is over and sent,
|
|
1144
|
+
* otherwise read while it has room and write while it holds bytes.
|
|
1145
|
+
*/
|
|
1146
|
+
flush(slot: i32): i32 {
|
|
1147
|
+
if (!this.holds(slot)) {
|
|
1148
|
+
return H1_DONE
|
|
1149
|
+
}
|
|
1150
|
+
const conn: Http1Connection = this.connections[slot]
|
|
1151
|
+
this.send(slot)
|
|
1152
|
+
if (conn.isDone()) {
|
|
1153
|
+
return H1_DONE
|
|
1154
|
+
}
|
|
1155
|
+
let wants: i32 = 0
|
|
1156
|
+
if (conn.inputRoom() > 0) {
|
|
1157
|
+
wants = wants | H1_WANT_READ
|
|
1158
|
+
}
|
|
1159
|
+
if (conn.wantsWrite()) {
|
|
1160
|
+
wants = wants | H1_WANT_WRITE
|
|
1161
|
+
}
|
|
1162
|
+
return wants
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/** Writes what `slot`'s connection holds, which it holds, until the socket would block. */
|
|
1166
|
+
send(slot: i32): void {
|
|
1167
|
+
const conn: Http1Connection = this.connections[slot]
|
|
1168
|
+
const fd: i32 = this.fds[slot]
|
|
1169
|
+
const pending: i32 = conn.outputEnd - conn.outputStart
|
|
1170
|
+
while (conn.wantsWrite()) {
|
|
1171
|
+
const n: i32 = netWrite(fd, conn.output, conn.outputStart, conn.outputEnd - conn.outputStart)
|
|
1172
|
+
if (n <= 0) {
|
|
1173
|
+
if (n !== H1_AGAIN) {
|
|
1174
|
+
conn.abort()
|
|
1175
|
+
}
|
|
1176
|
+
break
|
|
1177
|
+
}
|
|
1178
|
+
conn.consume(n)
|
|
1179
|
+
}
|
|
1180
|
+
if (conn.outputEnd - conn.outputStart < pending) {
|
|
1181
|
+
conn.lastActive = monotonicNanos()
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** Closes `slot`'s socket, whatever is left unsent, and frees the slot. */
|
|
1186
|
+
close(slot: i32): void {
|
|
1187
|
+
if (!this.holds(slot)) {
|
|
1188
|
+
return
|
|
1189
|
+
}
|
|
1190
|
+
netClose(this.fds[slot])
|
|
1191
|
+
this.fds[slot] = H1_FREE
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/** Closes every slot that has been idle past `idleTimeout` at `now` (`monotonicNanos`); answers how many. */
|
|
1195
|
+
expire(now: i64): i32 {
|
|
1196
|
+
let n: i32 = 0
|
|
1197
|
+
for (let k: i32 = 0; k < toI32(this.fds.length) && k < toI32(this.connections.length); k++) {
|
|
1198
|
+
if (this.fds[k] >= 0 && this.connections[k].idle(now)) {
|
|
1199
|
+
this.close(k)
|
|
1200
|
+
n = n + 1
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return n
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* A pool of HTTP/1.1 connections over TLS 1.3 on one listening socket: one
|
|
1209
|
+
* `Http1Connection` beside each slot of a `TlsTcpServer`, run the way
|
|
1210
|
+
* `nish/net/http2-tls` runs HTTP/2. A handshake that chose ALPN `http/1.1`,
|
|
1211
|
+
* or none (a client that does not send ALPN speaks HTTP/1.1), reaches its
|
|
1212
|
+
* connection; any other protocol is shut down with `close_notify`.
|
|
1213
|
+
*/
|
|
1214
|
+
export class Http1TlsServer {
|
|
1215
|
+
tls: TlsTcpServer
|
|
1216
|
+
connections: Http1Connection[]
|
|
1217
|
+
/** Where TLS decrypts into before the connection takes the bytes. */
|
|
1218
|
+
scratch: u8[]
|
|
1219
|
+
|
|
1220
|
+
/** A pool of `size` slots, at least one, serving `config` over TLS under `tlsConfig` on `listener`. */
|
|
1221
|
+
constructor(tlsConfig: TlsServerConfig, config: Http1Config, listener: i32, size: i32) {
|
|
1222
|
+
this.tls = new TlsTcpServer(tlsConfig, listener, size)
|
|
1223
|
+
this.connections = []
|
|
1224
|
+
for (let k: i32 = 0; k < this.tls.size(); k++) {
|
|
1225
|
+
this.connections.push(new Http1Connection(config))
|
|
1226
|
+
}
|
|
1227
|
+
this.scratch = new Array<u8>(config.chunkSize)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/** How many slots the pool has. */
|
|
1231
|
+
size(): i32 {
|
|
1232
|
+
return this.tls.size()
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** How many slots hold a connection. */
|
|
1236
|
+
busy(): i32 {
|
|
1237
|
+
return this.tls.busy()
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/** Whether `slot` holds a connection. */
|
|
1241
|
+
holds(slot: i32): boolean {
|
|
1242
|
+
return this.tls.holds(slot)
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/** The descriptor of `slot`, for `pollAdd`; -1 for a free one. */
|
|
1246
|
+
fd(slot: i32): i32 {
|
|
1247
|
+
return this.tls.fd(slot)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** The HTTP/1.1 connection of `slot`; a slot out of range names the first. */
|
|
1251
|
+
connection(slot: i32): Http1Connection {
|
|
1252
|
+
const at: i32 = slot >= 0 && slot < toI32(this.connections.length) ? slot : H1_ZERO
|
|
1253
|
+
return this.connections[at]
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
/** The ALPN protocol `slot`'s handshake chose, or empty. */
|
|
1257
|
+
alpn(slot: i32): string {
|
|
1258
|
+
return this.tls.connection(slot).tls.alpn
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/** Whether `slot`'s handshake is done and chose a protocol other than `http/1.1`. */
|
|
1262
|
+
refused(slot: i32): boolean {
|
|
1263
|
+
const chosen: string = this.alpn(slot)
|
|
1264
|
+
return this.tls.connection(slot).state === TLS_RECORD_STATE_OPEN && chosen !== "" && chosen !== H1_ALPN
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Accepts the next waiting connection as `TlsTcpServer.accept` does, with
|
|
1269
|
+
* the caller's 32-byte server random and x25519 key, and gives it a fresh
|
|
1270
|
+
* connection. Answers the slot, or what `TlsTcpServer.accept` answers.
|
|
1271
|
+
*/
|
|
1272
|
+
accept(serverRandom: u8[], ephemeralPrivate: u8[]): i32 {
|
|
1273
|
+
const slot: i32 = this.tls.accept(serverRandom, ephemeralPrivate)
|
|
1274
|
+
if (slot >= 0 && slot < toI32(this.connections.length)) {
|
|
1275
|
+
const conn: Http1Connection = this.connections[slot]
|
|
1276
|
+
conn.restart()
|
|
1277
|
+
conn.lastActive = monotonicNanos()
|
|
1278
|
+
}
|
|
1279
|
+
return slot
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/** The socket of `slot` is readable: TLS reads it, and what the connection has to send goes after. Answers the interest. */
|
|
1283
|
+
readable(slot: i32): i32 {
|
|
1284
|
+
if (this.holds(slot)) {
|
|
1285
|
+
this.connections[slot].lastActive = monotonicNanos()
|
|
1286
|
+
}
|
|
1287
|
+
this.tls.readable(slot)
|
|
1288
|
+
return this.flush(slot)
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/** The socket of `slot` is writable. Answers the interest. */
|
|
1292
|
+
writable(slot: i32): i32 {
|
|
1293
|
+
this.tls.writable(slot)
|
|
1294
|
+
return this.flush(slot)
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
/** Signs `slot`'s CertificateVerify with `key`, as `TlsTcpServer.signP256` does. Answers the interest. */
|
|
1298
|
+
signP256(slot: i32, key: Secret<u8[]>): i32 {
|
|
1299
|
+
this.tls.signP256(slot, key)
|
|
1300
|
+
return this.flush(slot)
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Moves what TLS has decrypted for `slot` into its connection, as far as
|
|
1305
|
+
* the connection has room, and answers the connection's next event. A free
|
|
1306
|
+
* slot, or one whose ALPN was another protocol, answers `H1_ERROR`.
|
|
1307
|
+
*/
|
|
1308
|
+
next(slot: i32): i32 {
|
|
1309
|
+
if (!this.holds(slot) || this.refused(slot)) {
|
|
1310
|
+
return H1_ERROR
|
|
1311
|
+
}
|
|
1312
|
+
const conn: Http1Connection = this.connections[slot]
|
|
1313
|
+
while (true) {
|
|
1314
|
+
const room: i32 = conn.inputRoom()
|
|
1315
|
+
const want: i32 = room < toI32(this.scratch.length) ? room : toI32(this.scratch.length)
|
|
1316
|
+
if (want <= 0) {
|
|
1317
|
+
break
|
|
1318
|
+
}
|
|
1319
|
+
const n: i32 = this.tls.read(slot, this.scratch, H1_ZERO, want)
|
|
1320
|
+
if (n === 0) {
|
|
1321
|
+
conn.endInput()
|
|
1322
|
+
}
|
|
1323
|
+
if (n <= 0) {
|
|
1324
|
+
break
|
|
1325
|
+
}
|
|
1326
|
+
conn.feed(this.scratch, H1_ZERO, n)
|
|
1327
|
+
}
|
|
1328
|
+
// A held-back write finds its room here, as in `Http1Server.next`.
|
|
1329
|
+
if (conn.writeHeld) {
|
|
1330
|
+
this.send(slot)
|
|
1331
|
+
}
|
|
1332
|
+
return conn.next()
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Writes what `slot`'s connection holds into TLS, as far as TLS takes it,
|
|
1337
|
+
* and sends it; shuts TLS down once the connection is over or ALPN chose
|
|
1338
|
+
* another protocol. Answers TLS's interest, with the write bit while the
|
|
1339
|
+
* connection still holds bytes.
|
|
1340
|
+
*/
|
|
1341
|
+
flush(slot: i32): i32 {
|
|
1342
|
+
if (!this.holds(slot)) {
|
|
1343
|
+
return TLS_RECORD_DONE
|
|
1344
|
+
}
|
|
1345
|
+
if (this.refused(slot)) {
|
|
1346
|
+
return this.tls.shutdown(slot)
|
|
1347
|
+
}
|
|
1348
|
+
const conn: Http1Connection = this.connections[slot]
|
|
1349
|
+
this.send(slot)
|
|
1350
|
+
if (conn.isDone()) {
|
|
1351
|
+
return this.tls.shutdown(slot)
|
|
1352
|
+
}
|
|
1353
|
+
const wants: i32 = this.tls.interest(slot)
|
|
1354
|
+
return conn.wantsWrite() && (wants & TLS_RECORD_DONE) === 0 ? wants | TLS_RECORD_WANT_WRITE : wants
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/** Writes what `slot`'s connection holds into TLS, as far as TLS takes it. */
|
|
1358
|
+
send(slot: i32): void {
|
|
1359
|
+
const conn: Http1Connection = this.connections[slot]
|
|
1360
|
+
const pending: i32 = conn.outputEnd - conn.outputStart
|
|
1361
|
+
while (conn.wantsWrite()) {
|
|
1362
|
+
const n: i32 = this.tls.write(slot, conn.output, conn.outputStart, conn.outputEnd - conn.outputStart)
|
|
1363
|
+
if (n <= 0) {
|
|
1364
|
+
break
|
|
1365
|
+
}
|
|
1366
|
+
conn.consume(n)
|
|
1367
|
+
}
|
|
1368
|
+
if (conn.outputEnd - conn.outputStart < pending) {
|
|
1369
|
+
conn.lastActive = monotonicNanos()
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/** Ends `slot`'s connection and frees the slot at once, as `TlsTcpServer.close` does. */
|
|
1374
|
+
close(slot: i32): void {
|
|
1375
|
+
this.tls.close(slot)
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/** Closes every slot that has been idle past `idleTimeout` at `now` (`monotonicNanos`); answers how many. */
|
|
1379
|
+
expire(now: i64): i32 {
|
|
1380
|
+
let n: i32 = 0
|
|
1381
|
+
for (let k: i32 = 0; k < this.tls.size() && k < toI32(this.connections.length); k++) {
|
|
1382
|
+
if (this.tls.holds(k) && this.connections[k].idle(now)) {
|
|
1383
|
+
this.tls.close(k)
|
|
1384
|
+
n = n + 1
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return n
|
|
1388
|
+
}
|
|
1389
|
+
}
|