@amritk/nish-x86_64-linux 0.28.0 → 0.30.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.
@@ -0,0 +1,759 @@
1
+ /**
2
+ * `nish/net/upstream` — outbound HTTP/1.1 calls from a route handler that
3
+ * never block its worker: a pool of keep-alive connections to one origin,
4
+ * registered on the `HttpHandler`'s own poll loop and read through the
5
+ * `nish/net/http1-client` codec.
6
+ *
7
+ * const h = new HttpHandler(new HandlerLimits(), listener, router)
8
+ * const users = new Upstream(h, "http://127.0.0.1:9000", new UpstreamLimits())
9
+ * while (true) {
10
+ * h.wait(100) // one turn: the handler's sockets and the upstream's
11
+ * while (h.ready()) {
12
+ * const req = h.take()
13
+ * switch (req.route) {
14
+ * case USER: if (users.get(req, "/v1/users/7") !== UPSTREAM_E_NONE) { h.problem(req, 503, "Busy", "") } break
15
+ * default: h.refuse(req)
16
+ * }
17
+ * }
18
+ * while (users.ready()) { // the answers that arrived this turn
19
+ * const r = users.take() // r.req is the request that asked
20
+ * if (r.ok) { h.send(r.req, r.status, JSON, r.body, 0, r.length) } else { h.problem(r.req, 502, "Bad Gateway", "") }
21
+ * users.release(r)
22
+ * }
23
+ * }
24
+ *
25
+ * **`get` returns at once.** It writes the request head into a call slot
26
+ * taken from a pool, then onto a free connection, or queues it until one is
27
+ * free, and the program goes on serving. The asking request stays held, as
28
+ * `nish/net/handler` allows. Each connection is watched on the handler's loop
29
+ * under a watch id (`UpstreamLimits.firstId` and up), so the program's next
30
+ * `h.wait` wakes for the upstream as well as for its clients; `ready` then
31
+ * reads what arrived, once per turn, and hands back every finished call.
32
+ *
33
+ * **Every call ends exactly once, as a call.** A response the codec reads to
34
+ * its end is `ok` with its `status` and body, whatever the status. Anything
35
+ * else is `ok === false` with an `error`: one of the codec's `HTTP1_E_`
36
+ * refusals (a malformed status line, a bare LF, conflicting framing, a body
37
+ * past `maxBody`, a response cut short), or one of the `UPSTREAM_E_` codes
38
+ * below (the origin was unusable, the connection failed or closed, the
39
+ * deadline passed, the path could not be written). The program answers its
40
+ * own client either way and gives the call back with `release`.
41
+ *
42
+ * **A kept-alive connection is checked before it is reused.** One the origin
43
+ * closed or sent bytes on while it was idle is closed instead, so no call is
44
+ * written to a dead socket or handed bytes it did not ask for. A call the
45
+ * origin drops unanswered on a reused connection anyway (it closed as the
46
+ * request crossed) is made once more, on a new connection; a `GET` is safe
47
+ * to repeat. Two `Upstream`s on one handler must be given watch ids that do
48
+ * not overlap: nothing checks it, and two with the default limits would both
49
+ * take ids 8 to 11, each taking the other's wake-ups for its own.
50
+ *
51
+ * **The origin is fixed at start-up.** The constructor parses it, looks its
52
+ * host up (blocking, once, as a program's start-up may), and keeps one
53
+ * address; nothing is looked up again, so no later answer can move the pool
54
+ * somewhere else. When the origin comes from input — configuration, an
55
+ * environment variable, a request — set `UpstreamLimits.outbound`: the
56
+ * `nish/net/outbound` policy then checks the host name before the lookup and
57
+ * the address after it, and a refused origin fails every call with
58
+ * `UPSTREAM_E_BLOCKED`. Only `http:` is spoken; TLS is not, so `https:` is
59
+ * `UPSTREAM_E_ORIGIN`.
60
+ *
61
+ * **Every cap is set at start-up.** The connection count, the call slots
62
+ * (calls queued, in flight and finished but not yet released), the request
63
+ * head's room, the response limits the codec enforces, and a deadline per
64
+ * call. The slots, their head buffers and the connections' read buffers are
65
+ * made by the constructor; a call's body buffer grows to the largest body it
66
+ * has held and is kept. Each connection keeps one codec for its life and
67
+ * the next connection's, reset between them, in its spans mode: a response's
68
+ * fields are read as spans of a buffer the codec reuses, so a warm pool
69
+ * allocates nothing per call.
70
+ *
71
+ * `docs/security/upstream.md` is the record. Private helpers share the
72
+ * importing program's flat symbol namespace (`docs/wp26-stdlib.md` §3e),
73
+ * which is why each one carries the module's name.
74
+ *
75
+ * Written from RFC 9110 and RFC 9112, not ported from another implementation.
76
+ */
77
+ import { copyBytes } from "nish/bytes"
78
+ import { DNS_ADDRESS_BYTES } from "nish/net/dns-message"
79
+ import { DNS_RESOLVE_MAX_ADDRESSES } from "nish/net/dns-resolver"
80
+ import { resolveHost } from "nish/net/dns"
81
+ import { HANDLER_MAX_WATCHED, HttpHandler, HttpRequest } from "nish/net/handler"
82
+ import {
83
+ HTTP1_BODY,
84
+ HTTP1_CLOSED,
85
+ HTTP1_END,
86
+ HTTP1_ERROR,
87
+ HTTP1_HEAD,
88
+ HTTP1_NEED_MORE,
89
+ HTTP1_NO_BODY,
90
+ } from "nish/net/http1"
91
+ import { HTTP1_INFORMATIONAL, Http1ResponseParser, http1WriteRequestHead } from "nish/net/http1-client"
92
+ import { OutboundPolicy, outboundAddressText, outboundFilter, outboundHostAllowed } from "nish/net/outbound"
93
+ import { Url, urlParse } from "nish/net/url"
94
+ import {
95
+ connectResult,
96
+ netClose,
97
+ netRead,
98
+ netWrite,
99
+ pollAdd,
100
+ pollModify,
101
+ pollRemove,
102
+ tcpConnect,
103
+ } from "nish:net"
104
+ import { monotonicNanos } from "nish:process"
105
+
106
+ /** The call was read to its end: `ok`. */
107
+ export const UPSTREAM_E_NONE: i32 = 0
108
+ // 1 to 17 are the codec's own `HTTP1_E_` refusals, passed through as they are.
109
+ /** The origin is not an `http:` URL with a host and nothing past its authority but `/`. */
110
+ export const UPSTREAM_E_ORIGIN: i32 = 32
111
+ /** The origin's host could not be looked up at start-up. */
112
+ export const UPSTREAM_E_LOOKUP: i32 = 33
113
+ /** The outbound policy refused the origin's host name or every address it has. */
114
+ export const UPSTREAM_E_BLOCKED: i32 = 34
115
+ /** The connection to the origin could not be made. */
116
+ export const UPSTREAM_E_CONNECT: i32 = 35
117
+ /** The call's deadline passed before its response ended. */
118
+ export const UPSTREAM_E_TIMEOUT: i32 = 36
119
+ /** The connection failed or closed before a response began. */
120
+ export const UPSTREAM_E_CLOSED: i32 = 37
121
+ /**
122
+ * The path does not start with `/` (an absolute-form or asterisk target is
123
+ * not this pool's to send), the codec refused to write it, or the head did
124
+ * not fit `maxHead`.
125
+ */
126
+ export const UPSTREAM_E_REQUEST: i32 = 38
127
+ /** Every call slot is taken: `get` answers this and holds nothing. */
128
+ export const UPSTREAM_E_FULL: i32 = 39
129
+ /** The connection failed (it was reset, say) while the response was being read: what came is not the whole of it. */
130
+ export const UPSTREAM_E_RESET: i32 = 40
131
+
132
+ const UPSTREAM_READ: i32 = 1
133
+ const UPSTREAM_WRITE: i32 = 2
134
+ const UPSTREAM_WOULD_BLOCK: i32 = -11
135
+ const UPSTREAM_NS_PER_MS: i64 = 1000000
136
+ const UPSTREAM_READ_CHUNK: i32 = 16384
137
+ /** The largest body a call takes, whatever `maxBody` says: doubling the buffer up to it stays in i32's range. */
138
+ const UPSTREAM_MAX_BODY: i64 = 1073741824
139
+
140
+ /** A connection with no socket. */
141
+ const UPSTREAM_C_CLOSED: i32 = 0
142
+ /** Connecting, with a call waiting on it. */
143
+ const UPSTREAM_C_CONNECTING: i32 = 1
144
+ /** Carrying a call: its head being written, or its response being read. */
145
+ const UPSTREAM_C_BUSY: i32 = 2
146
+ /** Open and kept alive, with no call on it. */
147
+ const UPSTREAM_C_IDLE: i32 = 3
148
+
149
+ /** The caps of one `Upstream`, all read once by its constructor. */
150
+ export class UpstreamLimits {
151
+ /** Checks the origin's host and address when set; set it whenever the origin comes from input. */
152
+ outbound: OutboundPolicy | null = null
153
+ /** The largest response body the codec accepts, at most 1 GiB; past it the call fails `HTTP1_E_BODY_LIMIT`. */
154
+ maxBody: i64 = 1048576
155
+ /** Connections to the origin, at most; each takes one handler watch id. */
156
+ connections: i32 = 4
157
+ /** Call slots: calls queued, in flight, and finished but not yet released, at most. */
158
+ calls: i32 = 256
159
+ /** The first handler watch id the connections take: they use `firstId` to `firstId + connections - 1`. */
160
+ firstId: i32 = 8
161
+ /** A call fails `UPSTREAM_E_TIMEOUT` this long after `get` if its response has not ended. */
162
+ timeoutMs: i32 = 10000
163
+ /** How long the constructor's one lookup of the origin's host may take. */
164
+ lookupMs: i32 = 5000
165
+ /** Room for one request head: the request line, `Host` and the empty line. */
166
+ maxHead: i32 = 8192
167
+ maxStatusLine: i32 = 8192
168
+ maxHeaderBytes: i32 = 65536
169
+ maxHeaders: i32 = 100
170
+ maxTrailerBytes: i32 = 8192
171
+ }
172
+
173
+ /**
174
+ * One call: the request that asked, then, once `ready` hands it back, how it
175
+ * ended. The object is the pool's again after `release`.
176
+ */
177
+ export class UpstreamResponse {
178
+ /** The handler's request this call answers. */
179
+ req: HttpRequest
180
+ /** The body, in `body[0 .. length)`; the buffer is reused, so read `length`, not `body.length`. */
181
+ body: u8[]
182
+ /** The request head, written by `get`. */
183
+ head: u8[]
184
+ /** When the call fails `UPSTREAM_E_TIMEOUT`, on `monotonicNanos`'s clock. */
185
+ deadline: i64 = 0
186
+ /** The final status code, once the response's head has arrived. */
187
+ status: i32 = 0
188
+ /** `UPSTREAM_E_NONE`, an `HTTP1_E_` refusal of the codec, or an `UPSTREAM_E_` code. */
189
+ error: i32 = 0
190
+ length: i32 = 0
191
+ headLength: i32 = 0
192
+ index: i32 = 0
193
+ /** Whether the response was read to its end; `error` says why not. */
194
+ ok: boolean = false
195
+ /** Taken by the program and not yet released. */
196
+ taken: boolean = false
197
+ /** Made once more already, after a reused connection dropped it unanswered. */
198
+ retried: boolean = false
199
+
200
+ constructor(index: i32, headRoom: i32, placeholder: HttpRequest) {
201
+ this.index = index
202
+ this.req = placeholder
203
+ this.head = new Array<u8>(headRoom)
204
+ this.body = new Array<u8>(0)
205
+ }
206
+ }
207
+
208
+ /** One connection of the pool. */
209
+ class UpstreamConnection {
210
+ parser: Http1ResponseParser
211
+ input: u8[]
212
+ fd: i32 = -1
213
+ /** Its handler watch id. */
214
+ id: i32 = 0
215
+ state: i32 = 0
216
+ /** The slot of the call it carries, or -1. */
217
+ call: i32 = -1
218
+ /** How much of the call's head has been written. */
219
+ sent: i32 = 0
220
+ /** The poll interest it is registered with, 0 for none. */
221
+ events: i32 = 0
222
+ /** Whether the call it carries went out on a connection kept alive from an earlier call. */
223
+ reused: boolean = false
224
+ /** Whether any byte of the call's response has arrived. */
225
+ answered: boolean = false
226
+
227
+ constructor(id: i32, parser: Http1ResponseParser) {
228
+ this.id = id
229
+ this.parser = parser
230
+ this.input = new Array<u8>(UPSTREAM_READ_CHUNK)
231
+ }
232
+ }
233
+
234
+ /** A keep-alive pool to one origin, on an `HttpHandler`'s loop. */
235
+ export class Upstream {
236
+ h: HttpHandler
237
+ limits: UpstreamLimits
238
+ /** The one address the pool connects to, checked against the policy if there is one. */
239
+ address: u8[]
240
+ hostNames: string[]
241
+ hostValues: string[]
242
+ connections: UpstreamConnection[]
243
+ calls: UpstreamResponse[]
244
+ free: i32[]
245
+ /** Calls written into their slot and waiting for a connection, oldest first. */
246
+ waiting: i32[]
247
+ /** Calls ended and not yet taken, in the order they ended. */
248
+ done: i32[]
249
+ /** Why the origin is unusable; empty when it is not. For a log. */
250
+ errorText: string = ""
251
+ /** The handler turn `pump` last read. */
252
+ turn: i64 = -1
253
+ /** Not `UPSTREAM_E_NONE` when the origin is unusable: every call then fails with it. */
254
+ error: i32 = 0
255
+ freeCount: i32 = 0
256
+ waitingStart: i32 = 0
257
+ waitingCount: i32 = 0
258
+ doneStart: i32 = 0
259
+ doneCount: i32 = 0
260
+
261
+ /**
262
+ * Parses `origin`, looks its host up once (this blocks for at most
263
+ * `limits.lookupMs`), and makes every slot and connection buffer. Panics
264
+ * when a cap is below 1 or the watch ids run past the handler's. An
265
+ * unusable origin is not a panic: `error` says why, and every call fails
266
+ * with it.
267
+ */
268
+ constructor(h: HttpHandler, origin: string, limits: UpstreamLimits) {
269
+ if (limits.connections < 1 || limits.calls < 1 || limits.maxHead < 1 || limits.timeoutMs < 1) {
270
+ panic("Upstream: connections, calls, maxHead and timeoutMs must each be at least 1")
271
+ }
272
+ // Each operand on its own: their sum could pass i32's range.
273
+ if (
274
+ limits.firstId < 0 ||
275
+ limits.connections > HANDLER_MAX_WATCHED ||
276
+ limits.firstId > HANDLER_MAX_WATCHED - limits.connections
277
+ ) {
278
+ panic(
279
+ `Upstream: watch ids ${limits.firstId} to ${limits.firstId + limits.connections - 1} are outside 0 to ${HANDLER_MAX_WATCHED - 1}`
280
+ )
281
+ }
282
+ this.h = h
283
+ this.limits = limits
284
+ this.address = new Array<u8>(DNS_ADDRESS_BYTES)
285
+ this.hostNames = ["host"]
286
+ this.hostValues = [""]
287
+ this.connections = []
288
+ for (let k: i32 = 0; k < limits.connections; k += 1) {
289
+ this.connections.push(new UpstreamConnection(limits.firstId + k, upstreamParser(limits)))
290
+ }
291
+ const placeholder: HttpRequest = new HttpRequest(-1)
292
+ this.calls = []
293
+ this.free = new Array<i32>(limits.calls)
294
+ this.waiting = new Array<i32>(limits.calls)
295
+ this.done = new Array<i32>(limits.calls)
296
+ for (let k: i32 = 0; k < limits.calls; k += 1) {
297
+ this.calls.push(new UpstreamResponse(k, limits.maxHead, placeholder))
298
+ this.free[k] = limits.calls - 1 - k
299
+ }
300
+ this.freeCount = limits.calls
301
+ this.settle(origin)
302
+ }
303
+
304
+ /** Settles the origin: its `Host` value and the one address, or `error`. */
305
+ settle(origin: string): void {
306
+ const url: Url | null = urlParse(origin)
307
+ if (
308
+ url === null ||
309
+ url.scheme !== "http" ||
310
+ url.host === "" ||
311
+ url.username !== "" ||
312
+ url.password !== "" ||
313
+ url.query !== null ||
314
+ url.fragment !== null ||
315
+ (url.path !== "" && url.path !== "/")
316
+ ) {
317
+ this.fail(UPSTREAM_E_ORIGIN, `${origin} is not an http: origin`)
318
+ return
319
+ }
320
+ const host: string = url.host
321
+ const port: i32 = url.port >= 0 ? url.port : 80
322
+ const named: string = host.indexOf(":") >= 0 ? `[${host}]` : host
323
+ this.hostValues[0] = url.port >= 0 ? `${named}:${port}` : named
324
+ const policy: OutboundPolicy | null = this.limits.outbound
325
+ if (policy !== null && !outboundHostAllowed(policy, host)) {
326
+ this.fail(UPSTREAM_E_BLOCKED, `${host} is not a host the outbound policy allows`)
327
+ return
328
+ }
329
+ const addresses: u8[] = new Array<u8>(DNS_RESOLVE_MAX_ADDRESSES * DNS_ADDRESS_BYTES)
330
+ const ttls: i32[] = new Array<i32>(DNS_RESOLVE_MAX_ADDRESSES)
331
+ const count: i32 = resolveHost(host, this.limits.lookupMs, port, addresses, ttls)
332
+ if (count <= 0) {
333
+ this.fail(
334
+ UPSTREAM_E_LOOKUP,
335
+ `${host} could not be looked up (${count === 0 ? "no address" : `code ${count}`})`
336
+ )
337
+ return
338
+ }
339
+ let usable: i32 = count < DNS_RESOLVE_MAX_ADDRESSES ? count : DNS_RESOLVE_MAX_ADDRESSES
340
+ if (policy !== null) {
341
+ // Only the bytes checked here are connected to: there is no second lookup.
342
+ usable = outboundFilter(policy, addresses, usable)
343
+ if (usable === 0) {
344
+ this.fail(
345
+ UPSTREAM_E_BLOCKED,
346
+ `${host} resolves to ${outboundAddressText(addresses, 0)}, which the outbound policy does not allow`
347
+ )
348
+ return
349
+ }
350
+ }
351
+ copyBytes(this.address, 0, addresses, 0, DNS_ADDRESS_BYTES)
352
+ }
353
+
354
+ fail(code: i32, text: string): void {
355
+ this.error = code
356
+ this.errorText = text
357
+ }
358
+
359
+ // ---- Calls ------------------------------------------------------------------
360
+
361
+ /**
362
+ * Starts `GET path` for `req` and returns at once: `UPSTREAM_E_NONE` when
363
+ * the call is the pool's, which hands it back through `ready` and `take`
364
+ * however it ends; `UPSTREAM_E_FULL` when every slot is taken, and then
365
+ * nothing is held and `req` is still the program's to answer. `path` is
366
+ * copied out before this returns.
367
+ */
368
+ get(req: HttpRequest, path: string): i32 {
369
+ if (this.freeCount === 0) {
370
+ return UPSTREAM_E_FULL
371
+ }
372
+ this.freeCount -= 1
373
+ const r: UpstreamResponse = this.calls[this.free[this.freeCount]]
374
+ r.req = req
375
+ r.ok = false
376
+ r.status = 0
377
+ r.error = UPSTREAM_E_NONE
378
+ r.length = 0
379
+ r.retried = false
380
+ r.deadline = monotonicNanos() + toI64(this.limits.timeoutMs) * UPSTREAM_NS_PER_MS
381
+ if (this.error !== UPSTREAM_E_NONE) {
382
+ this.end(r, this.error)
383
+ return UPSTREAM_E_NONE
384
+ }
385
+ // An origin-form target only: an absolute-form one would name another host on this pool's connection.
386
+ if (path.length === 0 || path.charCodeAt(0) !== 47) {
387
+ this.end(r, UPSTREAM_E_REQUEST)
388
+ return UPSTREAM_E_NONE
389
+ }
390
+ const n: i32 = http1WriteRequestHead(
391
+ r.head,
392
+ 0,
393
+ "GET",
394
+ path,
395
+ this.hostNames,
396
+ this.hostValues,
397
+ toI64(HTTP1_NO_BODY)
398
+ )
399
+ if (n < 0) {
400
+ this.end(r, UPSTREAM_E_REQUEST)
401
+ return UPSTREAM_E_NONE
402
+ }
403
+ r.headLength = n
404
+ const at: i32 = (this.waitingStart + this.waitingCount) % toI32(this.waiting.length)
405
+ this.waiting[at] = r.index
406
+ this.waitingCount += 1
407
+ this.dispatch()
408
+ return UPSTREAM_E_NONE
409
+ }
410
+
411
+ /** Whether a finished call is waiting for `take`; reads the upstream's sockets once per handler turn. */
412
+ ready(): boolean {
413
+ this.pump()
414
+ return this.doneCount > 0
415
+ }
416
+
417
+ /** The next finished call, which the program gives back with `release`. Panics when none is `ready`. */
418
+ take(): UpstreamResponse {
419
+ if (!this.ready()) {
420
+ panic("Upstream.take: no call is ready")
421
+ }
422
+ const r: UpstreamResponse = this.calls[this.done[this.doneStart]]
423
+ this.doneStart = (this.doneStart + 1) % toI32(this.done.length)
424
+ this.doneCount -= 1
425
+ r.taken = true
426
+ return r
427
+ }
428
+
429
+ /**
430
+ * Gives a taken call's slot back to the pool. Panics for a call not taken
431
+ * or already released: two calls sharing a slot would hand one request's
432
+ * answer to another.
433
+ */
434
+ release(r: UpstreamResponse): void {
435
+ if (!r.taken) {
436
+ panic("Upstream.release: the call is not taken, or was released already")
437
+ }
438
+ r.taken = false
439
+ this.free[this.freeCount] = r.index
440
+ this.freeCount += 1
441
+ }
442
+
443
+ /** How many calls hold a slot: queued, in flight, or finished and not yet released. */
444
+ pending(): i32 {
445
+ return toI32(this.free.length) - this.freeCount
446
+ }
447
+
448
+ /** How many connections to the origin are open. */
449
+ openConnections(): i32 {
450
+ let n: i32 = 0
451
+ for (let k: i32 = 0; k < toI32(this.connections.length); k += 1) {
452
+ if (this.connections[k].fd >= 0) {
453
+ n += 1
454
+ }
455
+ }
456
+ return n
457
+ }
458
+
459
+ // ---- The loop ---------------------------------------------------------------
460
+
461
+ /** Once per handler turn: reads and writes the connections that fired, then fails what is past its deadline. */
462
+ pump(): void {
463
+ if (this.h.turnAt === this.turn) {
464
+ return
465
+ }
466
+ this.turn = this.h.turnAt
467
+ for (let k: i32 = 0; k < toI32(this.connections.length); k += 1) {
468
+ const c: UpstreamConnection = this.connections[k]
469
+ if (c.fd >= 0 && this.h.fired(c.id)) {
470
+ this.service(c)
471
+ }
472
+ }
473
+ const now: i64 = monotonicNanos()
474
+ for (let k: i32 = 0; k < toI32(this.connections.length); k += 1) {
475
+ const c: UpstreamConnection = this.connections[k]
476
+ if (c.call >= 0 && this.calls[c.call].deadline <= now) {
477
+ this.finish(c, UPSTREAM_E_TIMEOUT)
478
+ }
479
+ }
480
+ // Every call has the same timeout, so the oldest waiting call is the first to expire.
481
+ while (this.waitingCount > 0 && this.calls[this.waiting[this.waitingStart]].deadline <= now) {
482
+ const r: UpstreamResponse = this.calls[this.waiting[this.waitingStart]]
483
+ this.waitingStart = (this.waitingStart + 1) % toI32(this.waiting.length)
484
+ this.waitingCount -= 1
485
+ this.end(r, UPSTREAM_E_TIMEOUT)
486
+ }
487
+ this.dispatch()
488
+ }
489
+
490
+ /** Puts waiting calls on connections: a kept-alive one first, then a new one. */
491
+ dispatch(): void {
492
+ while (this.waitingCount > 0) {
493
+ const c: UpstreamConnection | null = this.idle()
494
+ if (c === null) {
495
+ return
496
+ }
497
+ const index: i32 = this.waiting[this.waitingStart]
498
+ this.waitingStart = (this.waitingStart + 1) % toI32(this.waiting.length)
499
+ this.waitingCount -= 1
500
+ c.call = index
501
+ c.sent = 0
502
+ c.answered = false
503
+ c.reused = c.state === UPSTREAM_C_IDLE
504
+ if (c.reused) {
505
+ c.state = UPSTREAM_C_BUSY
506
+ c.parser.expect(false, false)
507
+ this.send(c)
508
+ } else {
509
+ this.connect(c)
510
+ }
511
+ }
512
+ }
513
+
514
+ /**
515
+ * A connection free for the next call: open, idle and still usable if one
516
+ * is, else closed, else null. An idle connection the origin has closed or
517
+ * sent bytes on is closed here, before a call is put on it.
518
+ */
519
+ idle(): UpstreamConnection | null {
520
+ let closed: UpstreamConnection | null = null
521
+ for (let k: i32 = 0; k < toI32(this.connections.length); k += 1) {
522
+ const c: UpstreamConnection = this.connections[k]
523
+ if (c.state === UPSTREAM_C_IDLE && this.usable(c)) {
524
+ return c
525
+ }
526
+ if (c.state === UPSTREAM_C_CLOSED && closed === null) {
527
+ closed = c
528
+ }
529
+ }
530
+ return closed
531
+ }
532
+
533
+ /**
534
+ * Whether the idle `c` has nothing to read. Anything else closes it: an end
535
+ * of stream or an error means the origin has given it up, and bytes no call
536
+ * asked for would be read as the next call's response.
537
+ */
538
+ usable(c: UpstreamConnection): boolean {
539
+ const start: i32 = 0
540
+ if (netRead(c.fd, c.input, start, toI32(c.input.length)) === UPSTREAM_WOULD_BLOCK) {
541
+ return true
542
+ }
543
+ this.close(c)
544
+ return false
545
+ }
546
+
547
+ /** Starts a connection for the call `c` holds; a failure ends the call. */
548
+ connect(c: UpstreamConnection): void {
549
+ const fd: i32 = tcpConnect(this.address)
550
+ if (fd < 0) {
551
+ this.finish(c, UPSTREAM_E_CONNECT)
552
+ return
553
+ }
554
+ if (pollAdd(this.h.loop, fd, UPSTREAM_WRITE, -2 - c.id) < 0) {
555
+ netClose(fd)
556
+ this.finish(c, UPSTREAM_E_CONNECT)
557
+ return
558
+ }
559
+ c.fd = fd
560
+ c.events = UPSTREAM_WRITE
561
+ c.state = UPSTREAM_C_CONNECTING
562
+ // The slot's parser is kept and starts over with the new connection.
563
+ c.parser.reset()
564
+ c.parser.expect(false, false)
565
+ }
566
+
567
+ /** What a connection's turn found ready. */
568
+ service(c: UpstreamConnection): void {
569
+ if (c.state === UPSTREAM_C_CONNECTING) {
570
+ if (connectResult(c.fd) !== 0) {
571
+ this.finish(c, UPSTREAM_E_CONNECT)
572
+ return
573
+ }
574
+ c.state = UPSTREAM_C_BUSY
575
+ }
576
+ if (c.state === UPSTREAM_C_BUSY && c.call >= 0 && c.sent < this.calls[c.call].headLength) {
577
+ this.send(c)
578
+ return
579
+ }
580
+ this.receive(c)
581
+ }
582
+
583
+ /** Writes what the socket takes of the call's head, then waits to read. */
584
+ send(c: UpstreamConnection): void {
585
+ const r: UpstreamResponse = this.calls[c.call]
586
+ while (c.sent < r.headLength) {
587
+ const n: i32 = netWrite(c.fd, r.head, c.sent, r.headLength - c.sent)
588
+ if (n === UPSTREAM_WOULD_BLOCK) {
589
+ this.interest(c, UPSTREAM_WRITE)
590
+ return
591
+ }
592
+ if (n <= 0) {
593
+ this.finish(c, UPSTREAM_E_CLOSED)
594
+ return
595
+ }
596
+ c.sent += n
597
+ }
598
+ this.interest(c, UPSTREAM_READ)
599
+ }
600
+
601
+ /** Reads what arrived and runs the codec over it. */
602
+ receive(c: UpstreamConnection): void {
603
+ while (c.fd >= 0) {
604
+ const start: i32 = 0
605
+ const n: i32 = netRead(c.fd, c.input, start, toI32(c.input.length))
606
+ if (n === UPSTREAM_WOULD_BLOCK) {
607
+ return
608
+ }
609
+ if (c.call < 0) {
610
+ // Nothing was asked: the server closed a kept-alive connection, or spoke out of turn.
611
+ this.close(c)
612
+ return
613
+ }
614
+ if (n < 0) {
615
+ // Not the origin's close: a close-delimited body cut off here must not end as whole.
616
+ this.finish(c, UPSTREAM_E_RESET)
617
+ return
618
+ }
619
+ if (n > 0) {
620
+ c.answered = true
621
+ c.parser.feed(c.input, start, n)
622
+ } else {
623
+ c.parser.finish()
624
+ }
625
+ this.parse(c)
626
+ if (n === 0 && c.fd >= 0) {
627
+ this.close(c)
628
+ }
629
+ }
630
+ }
631
+
632
+ /** Takes every event the codec has for the call `c` carries. */
633
+ parse(c: UpstreamConnection): void {
634
+ const p: Http1ResponseParser = c.parser
635
+ while (c.call >= 0) {
636
+ const event: i32 = p.next()
637
+ const r: UpstreamResponse = this.calls[c.call]
638
+ if (event === HTTP1_NEED_MORE) {
639
+ return
640
+ }
641
+ if (event === HTTP1_HEAD) {
642
+ r.status = p.status
643
+ } else if (event === HTTP1_BODY) {
644
+ upstreamAppend(r, p.body, p.bodyOff, p.bodyLen)
645
+ } else if (event === HTTP1_END) {
646
+ const again: boolean = p.keepAlive() && p.buffered() === 0
647
+ this.finish(c, UPSTREAM_E_NONE)
648
+ if (again) {
649
+ c.state = UPSTREAM_C_IDLE
650
+ } else {
651
+ this.close(c)
652
+ }
653
+ } else if (event === HTTP1_ERROR) {
654
+ this.finish(c, p.error)
655
+ } else if (event === HTTP1_CLOSED) {
656
+ this.finish(c, UPSTREAM_E_CLOSED)
657
+ } else if (event !== HTTP1_INFORMATIONAL) {
658
+ // Nothing here asks to switch protocols, so the codec has no other event for it.
659
+ this.finish(c, UPSTREAM_E_CLOSED)
660
+ }
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Ends the call `c` carries with `code`. A connection whose call ended any
666
+ * way but `UPSTREAM_E_NONE` is closed: what is left on it cannot be trusted
667
+ * to start the next response.
668
+ *
669
+ * A call that failed on a reused connection before any byte of its response
670
+ * came is not ended but put back at the front of the queue, once: the
671
+ * origin closed the connection as the request crossed it, and a `GET` is
672
+ * safe to send again. Past its deadline it is not.
673
+ */
674
+ finish(c: UpstreamConnection, code: i32): void {
675
+ if (c.call >= 0) {
676
+ const r: UpstreamResponse = this.calls[c.call]
677
+ c.call = -1
678
+ if (code !== UPSTREAM_E_NONE && code !== UPSTREAM_E_TIMEOUT && c.reused && !c.answered && !r.retried) {
679
+ r.retried = true
680
+ const length: i32 = toI32(this.waiting.length)
681
+ this.waitingStart = (this.waitingStart + length - 1) % length
682
+ this.waiting[this.waitingStart] = r.index
683
+ this.waitingCount += 1
684
+ } else {
685
+ this.end(r, code)
686
+ }
687
+ }
688
+ if (code !== UPSTREAM_E_NONE) {
689
+ this.close(c)
690
+ }
691
+ }
692
+
693
+ /** Hands `r` to `ready` with `code`. */
694
+ end(r: UpstreamResponse, code: i32): void {
695
+ r.error = code
696
+ r.ok = code === UPSTREAM_E_NONE
697
+ if (!r.ok) {
698
+ r.length = 0
699
+ }
700
+ const at: i32 = (this.doneStart + this.doneCount) % toI32(this.done.length)
701
+ this.done[at] = r.index
702
+ this.doneCount += 1
703
+ }
704
+
705
+ /** Watches `c` for `events` alone. */
706
+ interest(c: UpstreamConnection, events: i32): void {
707
+ if (c.events !== events) {
708
+ pollModify(this.h.loop, c.fd, events, -2 - c.id)
709
+ c.events = events
710
+ }
711
+ }
712
+
713
+ close(c: UpstreamConnection): void {
714
+ if (c.fd >= 0) {
715
+ pollRemove(this.h.loop, c.fd)
716
+ netClose(c.fd)
717
+ }
718
+ c.fd = -1
719
+ c.events = 0
720
+ c.state = UPSTREAM_C_CLOSED
721
+ if (c.call >= 0) {
722
+ this.finish(c, UPSTREAM_E_CLOSED)
723
+ }
724
+ }
725
+ }
726
+
727
+ /** A connection slot's codec, made once, reading fields as spans so that a response allocates no strings. */
728
+ const upstreamParser = (limits: UpstreamLimits): Http1ResponseParser => {
729
+ const p: Http1ResponseParser = new Http1ResponseParser(
730
+ limits.maxStatusLine,
731
+ limits.maxHeaderBytes,
732
+ limits.maxHeaders,
733
+ // A body buffer is an i32-indexed array, so past this the codec refuses the body rather than the buffer overflowing.
734
+ limits.maxBody < UPSTREAM_MAX_BODY ? limits.maxBody : UPSTREAM_MAX_BODY,
735
+ limits.maxTrailerBytes
736
+ )
737
+ p.fieldsAsSpans()
738
+ return p
739
+ }
740
+
741
+ /**
742
+ * Appends `data[off .. off + len)` to `r`'s body, growing its buffer by
743
+ * doubling when it is full: to under twice the body, and at least 1 KiB.
744
+ */
745
+ const upstreamAppend = (r: UpstreamResponse, data: u8[], off: i32, len: i32): void => {
746
+ const need: i32 = r.length + len
747
+ if (need > toI32(r.body.length)) {
748
+ let size: i32 = toI32(r.body.length) < 1024 ? 1024 : toI32(r.body.length)
749
+ // `need` is at most UPSTREAM_MAX_BODY, 2^30, so `size` doubles to 2^30 at most.
750
+ while (size < need) {
751
+ size = size * 2
752
+ }
753
+ const grown: u8[] = new Array<u8>(size)
754
+ copyBytes(grown, 0, r.body, 0, r.length)
755
+ r.body = grown
756
+ }
757
+ copyBytes(r.body, r.length, data, off, len)
758
+ r.length = need
759
+ }