@amritk/nish-x86_64-linux 0.18.0 → 0.20.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 -25
- package/bin/nish +0 -0
- package/package.json +2 -2
- package/runtime/nish.d.ts +55 -5
- package/runtime/nish.h +215 -18
- package/runtime/nish.mjs +16 -0
- package/runtime/runtime-aes.c +490 -0
- package/runtime/runtime-cross.c +366 -0
- package/runtime/runtime-host.c +7 -6
- package/runtime/runtime-net.c +93 -34
- package/runtime/runtime-os.c +213 -15
- package/runtime/runtime-parallel.c +590 -107
- package/runtime/runtime-simd.c +4 -4
- package/runtime/runtime-wasm.c +5 -3
- package/runtime/runtime.c +69 -13
- package/runtime/shim.mjs +71 -1
- package/scripts/build.sh +223 -22
- package/std/README.md +81 -51
- package/std/bytes.ts +163 -0
- package/std/crypto/LICENSE-libsecp256k1 +19 -0
- package/std/crypto/aes.ts +238 -76
- package/std/crypto/chacha20poly1305.ts +43 -9
- package/std/crypto/ed25519.ts +840 -0
- package/std/crypto/hkdf.ts +304 -12
- package/std/crypto/hmac.ts +245 -53
- package/std/crypto/p256.ts +932 -61
- package/std/crypto/x25519.ts +55 -37
- package/std/crypto/x509-random.ts +71 -0
- package/std/crypto/x509.ts +9 -9
- package/std/json-tokenizer.ts +1296 -0
- package/std/json-typed.ts +1014 -0
- package/std/json-value.ts +1163 -0
- package/std/json.ts +1120 -34
- package/std/net/buffer-pool.ts +151 -0
- package/std/net/hpack.ts +416 -112
- package/std/net/http-fields.ts +93 -0
- package/std/net/http1-server.ts +417 -55
- package/std/net/http1.ts +31 -6
- package/std/net/http2-tls.ts +112 -24
- package/std/net/http2.ts +333 -75
- package/std/net/http3-server.ts +211 -31
- package/std/net/http3.ts +187 -28
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-listener.ts +88 -7
- package/std/net/quic-packet.ts +243 -82
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +424 -24
- package/std/net/quic.ts +519 -167
- package/std/net/router.ts +676 -0
- package/std/net/tls/codec.ts +426 -27
- package/std/net/tls/record-server.ts +478 -132
- package/std/net/tls/record.ts +133 -42
- package/std/net/tls/schedule.ts +464 -55
- package/std/net/tls/ticket.ts +610 -0
- package/std/net/tls-tcp.ts +196 -53
- package/std/net/tls.ts +1018 -260
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/threads.ts +438 -14
package/std/net/http1-server.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* }
|
|
23
23
|
* wants = server.flush(slot);
|
|
24
24
|
* if ((wants & H1_DONE) !== 0) { server.close(slot); }
|
|
25
|
-
* else { pollModify(loop, server.fd(slot), wants & 3, slot); }
|
|
25
|
+
* else if (server.rearm(slot, wants)) { pollModify(loop, server.fd(slot), wants & 3, slot); }
|
|
26
26
|
*
|
|
27
27
|
* **Events.** `next()` answers `H1_REQUEST` (a request's head: the parser's
|
|
28
28
|
* spans and `...Is` methods read it), `H1_BODY` (some of its body, de-chunked,
|
|
@@ -65,6 +65,13 @@
|
|
|
65
65
|
* §15.4.5). A 1xx before the final response (100 Continue) is allowed; 101 is
|
|
66
66
|
* `acceptWebSocket`'s.
|
|
67
67
|
*
|
|
68
|
+
* **The Date.** Every final response, an error included, carries a `Date`
|
|
69
|
+
* field (RFC 9110 §6.6.1) from `HttpDate`, which formats the IMF-fixdate at
|
|
70
|
+
* most once a second; a 1xx carries none. The pool's slots share one, so a
|
|
71
|
+
* busy server formats a second once, not once a slot. A program that sets its
|
|
72
|
+
* own `date` field sends that one instead, so a response has one `Date`.
|
|
73
|
+
* `date.pin(unixMs)` fixes it, for a test that compares bytes.
|
|
74
|
+
*
|
|
68
75
|
* **Refusals.** Every refusal of `nish/net/http1`'s parser — the smuggling
|
|
69
76
|
* shapes, a `Content-Length` with a `Transfer-Encoding`, two that disagree,
|
|
70
77
|
* a malformed chunk size — is answered with its status (400, 413, 414, 431,
|
|
@@ -82,10 +89,21 @@
|
|
|
82
89
|
* **Caps are start-up numbers**, all in `Http1Config`: the request target,
|
|
83
90
|
* the header section and its field count, a request body, the chunk handed
|
|
84
91
|
* over, the output buffer, a WebSocket message and the idle timeout; the
|
|
85
|
-
* slots are the pool's size.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
92
|
+
* slots are the pool's size. `Http1Server` builds a slot's connection on the
|
|
93
|
+
* first accept into it and keeps it, so a pool sized for its peak costs at
|
|
94
|
+
* start-up only what does not depend on the size, and holds at most `size`
|
|
95
|
+
* connections once the peak has come; `Http1TlsServer` does the same for a
|
|
96
|
+
* slot's connection and, through `TlsTcpServer`, its TLS state. Every buffer
|
|
97
|
+
* is allocated with its slot or grown to its cap on first use and kept, so a
|
|
98
|
+
* warmed slot serves request after request with `Arena.used()` flat.
|
|
99
|
+
* Accepting a WebSocket allocates (the accept key), once a connection.
|
|
100
|
+
*
|
|
101
|
+
* **Output is borrowed.** The pools' slots share an `Http1OutputPool`: a
|
|
102
|
+
* connection takes an `outputSize` buffer when it first needs room to write
|
|
103
|
+
* and gives it back once its carrier has sent everything, so the output
|
|
104
|
+
* memory a pool holds follows the connections the socket is holding back,
|
|
105
|
+
* not its size. A pool that ever had `n` connections writing at once keeps
|
|
106
|
+
* `n` buffers and allocates no more.
|
|
89
107
|
*
|
|
90
108
|
* Private helpers share the importing program's flat symbol namespace
|
|
91
109
|
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
@@ -128,6 +146,7 @@ import {
|
|
|
128
146
|
websocketWriteFrame,
|
|
129
147
|
} from "nish/net/websocket"
|
|
130
148
|
import { TlsServerConfig } from "nish/net/tls"
|
|
149
|
+
import { HTTP_DATE_LAST_SECOND, HTTP_DATE_SIZE, httpDateWrite } from "nish/net/http-fields"
|
|
131
150
|
import { TLS_RECORD_DONE, TLS_RECORD_STATE_OPEN, TLS_RECORD_WANT_WRITE } from "nish/net/tls/record-server"
|
|
132
151
|
import { TlsTcpServer } from "nish/net/tls-tcp"
|
|
133
152
|
|
|
@@ -228,6 +247,82 @@ const http1ServerCopy = (out: u8[], at: i32, src: u8[], off: i32, n: i32): i32 =
|
|
|
228
247
|
return at + n
|
|
229
248
|
}
|
|
230
249
|
|
|
250
|
+
/**
|
|
251
|
+
* The `Date` field an origin server with a clock sends on every final
|
|
252
|
+
* response (RFC 9110 §6.6.1): the IMF-fixdate of now, in 29 bytes the object
|
|
253
|
+
* owns and rewrites in place, at most once a second.
|
|
254
|
+
*
|
|
255
|
+
* It reads the wall clock, `Date.now()`, which a wasm32 build has no host for,
|
|
256
|
+
* so it lives here, in a module that is native already, while the formatting
|
|
257
|
+
* it calls, `httpDateWrite`, stays in `nish/net/http-fields`, which compiles
|
|
258
|
+
* for wasm32 (the split `nish/crypto/x509` and `x509-random` make). `pin`
|
|
259
|
+
* replaces the clock with one instant, which is how a test asserts a response
|
|
260
|
+
* byte for byte.
|
|
261
|
+
*/
|
|
262
|
+
export class HttpDate {
|
|
263
|
+
/** The IMF-fixdate of `second`: what `current` answers, made once. */
|
|
264
|
+
bytes: u8[]
|
|
265
|
+
/** The second `bytes` holds, since 1970; -1 before the first `current`. */
|
|
266
|
+
second: i64 = -1
|
|
267
|
+
/** The instant `pin` fixed, in milliseconds since 1970, while `pinned`. */
|
|
268
|
+
pinnedMs: i64 = 0
|
|
269
|
+
/** How many times `bytes` has been rewritten. */
|
|
270
|
+
rewrites: i64 = 0
|
|
271
|
+
pinned: boolean = false
|
|
272
|
+
|
|
273
|
+
/** A date on the wall clock, with nothing formatted yet. */
|
|
274
|
+
constructor() {
|
|
275
|
+
this.bytes = new Array<u8>(HTTP_DATE_SIZE)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The 29 bytes for now, which are rewritten only when the second differs
|
|
280
|
+
* from the one they hold — earlier as well as later, since a corrected clock
|
|
281
|
+
* steps back. It allocates nothing, and the array is the same one each time,
|
|
282
|
+
* so a caller copies it out rather than keeping it. A wall clock before 1970
|
|
283
|
+
* or past 9999 answers the nearer of the two ends.
|
|
284
|
+
*/
|
|
285
|
+
current(): u8[] {
|
|
286
|
+
const ms: i64 = this.pinned ? this.pinnedMs : toI64(Date.now())
|
|
287
|
+
let second: i64 = ms / 1000
|
|
288
|
+
if (ms < 0) {
|
|
289
|
+
second = 0
|
|
290
|
+
} else if (second > HTTP_DATE_LAST_SECOND) {
|
|
291
|
+
second = HTTP_DATE_LAST_SECOND
|
|
292
|
+
}
|
|
293
|
+
if (second !== this.second) {
|
|
294
|
+
httpDateWrite(this.bytes, 0, second)
|
|
295
|
+
this.second = second
|
|
296
|
+
this.rewrites += 1
|
|
297
|
+
}
|
|
298
|
+
return this.bytes
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Makes `current` answer `unixMs`, milliseconds since 1970, until `unpin`.
|
|
303
|
+
* False, and nothing changed, for an instant before 1970-01-01T00:00:00Z or
|
|
304
|
+
* after 9999-12-31T23:59:59.999Z, which an IMF-fixdate cannot name.
|
|
305
|
+
*/
|
|
306
|
+
pin(unixMs: i64): boolean {
|
|
307
|
+
if (unixMs < 0 || unixMs / 1000 > HTTP_DATE_LAST_SECOND) {
|
|
308
|
+
return false
|
|
309
|
+
}
|
|
310
|
+
this.pinnedMs = unixMs
|
|
311
|
+
this.pinned = true
|
|
312
|
+
return true
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Puts `current` back on the wall clock. */
|
|
316
|
+
unpin(): void {
|
|
317
|
+
this.pinned = false
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** How many times the bytes have been rewritten: one per second that `current` was asked for, at most. */
|
|
321
|
+
formats(): i64 {
|
|
322
|
+
return this.rewrites
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
231
326
|
/**
|
|
232
327
|
* The caps of every connection made from it, fixed when the connection is
|
|
233
328
|
* made. Each is a number the program sets once, at start-up.
|
|
@@ -311,6 +406,44 @@ const http1ServerReserved = (name: u8[]): boolean =>
|
|
|
311
406
|
http1BytesAre(name, "transfer-encoding") ||
|
|
312
407
|
http1BytesAre(name, "content-length")
|
|
313
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Output buffers shared by the connections of one pool, each `size` bytes. A
|
|
411
|
+
* connection borrows one when it has something to write and gives it back
|
|
412
|
+
* once its carrier has sent everything, so a pool's output memory follows the
|
|
413
|
+
* connections the socket is holding back, not the slots: Go's `net/http`
|
|
414
|
+
* pools its writers the same way, and uWebSockets shares one per loop. A
|
|
415
|
+
* buffer is made only when none is free, so the pool grows to the most
|
|
416
|
+
* connections ever writing at once and then allocates nothing.
|
|
417
|
+
*/
|
|
418
|
+
export class Http1OutputPool {
|
|
419
|
+
// Widest first, so the layout packs without padding.
|
|
420
|
+
free: u8[][]
|
|
421
|
+
size: i32 = 0
|
|
422
|
+
/** How many buffers the pool has made. */
|
|
423
|
+
made: i32 = 0
|
|
424
|
+
|
|
425
|
+
constructor(size: i32) {
|
|
426
|
+
this.size = size
|
|
427
|
+
this.free = []
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** A buffer of `size` bytes: the one given back last, or a new one. */
|
|
431
|
+
take(): u8[] {
|
|
432
|
+
if (this.free.length > 0) {
|
|
433
|
+
const buf: u8[] = this.free[this.free.length - 1]
|
|
434
|
+
this.free.pop()
|
|
435
|
+
return buf
|
|
436
|
+
}
|
|
437
|
+
this.made = this.made + 1
|
|
438
|
+
return new Array<u8>(this.size)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Takes back a buffer `take` gave. */
|
|
442
|
+
give(buf: u8[]): void {
|
|
443
|
+
this.free.push(buf)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
314
447
|
/**
|
|
315
448
|
* One connection's HTTP/1.1 server, sans-IO: bytes in through `feed`, events
|
|
316
449
|
* out of `next`, and the response the program writes in
|
|
@@ -320,12 +453,27 @@ export class Http1Connection {
|
|
|
320
453
|
// References, then 64-bit fields, then 32-bit ones, then flags: the order
|
|
321
454
|
// the layout packs without padding.
|
|
322
455
|
config: Http1Config
|
|
456
|
+
/**
|
|
457
|
+
* The `Date` every final response carries. The connection makes its own;
|
|
458
|
+
* `Http1Server` and `Http1TlsServer` put theirs here, so that every slot of a
|
|
459
|
+
* pool shares one and the pool formats at most once a second. A program may
|
|
460
|
+
* put another, to share it or to pin its clock.
|
|
461
|
+
*/
|
|
462
|
+
date: HttpDate
|
|
323
463
|
/** The request parser: its head (`methodIs`, `targetIs`, `header`, spans) is the last `H1_REQUEST`'s. */
|
|
324
464
|
parser: Http1Parser
|
|
325
465
|
/** The frame decoder, once the slot has switched to WebSocket. */
|
|
326
466
|
ws: WsDecoder
|
|
327
|
-
/**
|
|
467
|
+
/**
|
|
468
|
+
* Bytes to send are `output[outputStart .. outputEnd)`. Empty until the
|
|
469
|
+
* connection first needs room to write: then its own buffer, made once, or
|
|
470
|
+
* one borrowed from `pool` and given back by `release` once sent.
|
|
471
|
+
*/
|
|
328
472
|
output: u8[]
|
|
473
|
+
/** The empty array `output` is while the connection holds no buffer. */
|
|
474
|
+
noOutput: u8[]
|
|
475
|
+
/** Where `output` is borrowed from, for a connection of a pool; null for one that keeps its own. */
|
|
476
|
+
pool: Http1OutputPool | null
|
|
329
477
|
/** `H1_BODY`'s and `H1_WS_MESSAGE`'s bytes. */
|
|
330
478
|
data: u8[]
|
|
331
479
|
/** A ping's payload, kept until its pong fits. */
|
|
@@ -396,12 +544,15 @@ export class Http1Connection {
|
|
|
396
544
|
big - H1_LINE_OVERHEAD
|
|
397
545
|
)
|
|
398
546
|
this.config = config
|
|
547
|
+
this.date = new HttpDate()
|
|
399
548
|
this.parser = new Http1Parser(config.maxTarget, config.maxHeaderBytes, config.maxHeaders, config.maxBody)
|
|
400
549
|
this.parser.keepText = false
|
|
401
550
|
this.parser.maxChunk = config.chunkSize
|
|
402
551
|
this.ws = new WsDecoder(true, config.maxMessage)
|
|
403
|
-
this.
|
|
404
|
-
this.
|
|
552
|
+
this.noOutput = []
|
|
553
|
+
this.output = this.noOutput
|
|
554
|
+
this.pool = null
|
|
555
|
+
this.data = this.noOutput
|
|
405
556
|
this.pong = new Array<u8>(WS_MAX_CONTROL)
|
|
406
557
|
this.noFields = []
|
|
407
558
|
// A line must fit whole before the parser can read it, so the buffer
|
|
@@ -423,7 +574,7 @@ export class Http1Connection {
|
|
|
423
574
|
this.ws.reset()
|
|
424
575
|
this.outputStart = 0
|
|
425
576
|
this.outputEnd = 0
|
|
426
|
-
this.data = this.
|
|
577
|
+
this.data = this.noOutput
|
|
427
578
|
this.dataStart = 0
|
|
428
579
|
this.dataLength = 0
|
|
429
580
|
this.opcode = 0
|
|
@@ -441,6 +592,7 @@ export class Http1Connection {
|
|
|
441
592
|
this.writeHeld = false
|
|
442
593
|
this.pongPending = false
|
|
443
594
|
this.inputEnded = false
|
|
595
|
+
this.release()
|
|
444
596
|
}
|
|
445
597
|
|
|
446
598
|
// ---- Bytes in and out ---------------------------------------------------------
|
|
@@ -492,6 +644,44 @@ export class Http1Connection {
|
|
|
492
644
|
this.outputEnd = 0
|
|
493
645
|
this.pongPending = false
|
|
494
646
|
this.writeHeld = false
|
|
647
|
+
this.release()
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Borrows `output` from the pool between connections of one, for a
|
|
652
|
+
* connection that keeps its own makes it the first time: before anything
|
|
653
|
+
* is written into it, which is why `room` calls it. It allocates only when
|
|
654
|
+
* the pool has no free buffer, or for a connection's own, once.
|
|
655
|
+
*/
|
|
656
|
+
acquire(): void {
|
|
657
|
+
if (toI32(this.output.length) > 0) {
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
const pool: Http1OutputPool | null = this.pool
|
|
661
|
+
this.output = pool !== null ? pool.take() : new Array<u8>(this.config.outputSize)
|
|
662
|
+
this.outputStart = 0
|
|
663
|
+
this.outputEnd = 0
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Gives `output` back to the pool once nothing in it is waiting: no bytes
|
|
668
|
+
* to send, no write held back for room. The carrier calls it after each
|
|
669
|
+
* send; a connection that keeps its own buffer keeps it.
|
|
670
|
+
*/
|
|
671
|
+
release(): void {
|
|
672
|
+
const pool: Http1OutputPool | null = this.pool
|
|
673
|
+
if (
|
|
674
|
+
pool === null ||
|
|
675
|
+
toI32(this.output.length) === 0 ||
|
|
676
|
+
this.outputEnd > this.outputStart ||
|
|
677
|
+
this.writeHeld
|
|
678
|
+
) {
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
pool.give(this.output)
|
|
682
|
+
this.output = this.noOutput
|
|
683
|
+
this.outputStart = 0
|
|
684
|
+
this.outputEnd = 0
|
|
495
685
|
}
|
|
496
686
|
|
|
497
687
|
/** Marks the first `n` bytes of the output sent. */
|
|
@@ -518,6 +708,7 @@ export class Http1Connection {
|
|
|
518
708
|
|
|
519
709
|
/** The room at the end of the output, once what was sent is moved out of the way. */
|
|
520
710
|
room(): i32 {
|
|
711
|
+
this.acquire()
|
|
521
712
|
const output: u8[] = this.output
|
|
522
713
|
const live: i32 = this.outputEnd - this.outputStart
|
|
523
714
|
if (this.outputStart > 0) {
|
|
@@ -550,7 +741,7 @@ export class Http1Connection {
|
|
|
550
741
|
if (this.writeHeld) {
|
|
551
742
|
// Half the output free before the program is asked to write again, so
|
|
552
743
|
// that a slow reader is not answered one small write at a time.
|
|
553
|
-
if (this.bodyRoom() * 2 <
|
|
744
|
+
if (this.bodyRoom() * 2 < this.config.outputSize - H1_CONTROL_RESERVE) {
|
|
554
745
|
return H1_NEED_MORE
|
|
555
746
|
}
|
|
556
747
|
this.writeHeld = false
|
|
@@ -642,7 +833,8 @@ export class Http1Connection {
|
|
|
642
833
|
this.noFields,
|
|
643
834
|
this.noFields,
|
|
644
835
|
H1_ZERO,
|
|
645
|
-
"close"
|
|
836
|
+
"close",
|
|
837
|
+
this.date.current()
|
|
646
838
|
)
|
|
647
839
|
if (end >= 0) {
|
|
648
840
|
this.outputEnd = end
|
|
@@ -794,14 +986,15 @@ export class Http1Connection {
|
|
|
794
986
|
names,
|
|
795
987
|
values,
|
|
796
988
|
framing,
|
|
797
|
-
connection
|
|
989
|
+
connection,
|
|
990
|
+
this.date.current()
|
|
798
991
|
)
|
|
799
992
|
if (end === HTTP1_REFUSED) {
|
|
800
993
|
return H1_INVALID
|
|
801
994
|
}
|
|
802
995
|
if (end < 0 || end - this.outputEnd > room) {
|
|
803
996
|
// The head was not written, or ate into the reserve: take it back.
|
|
804
|
-
if (end - this.outputEnd >
|
|
997
|
+
if (end - this.outputEnd > this.config.outputSize - H1_CONTROL_RESERVE) {
|
|
805
998
|
return H1_INVALID
|
|
806
999
|
}
|
|
807
1000
|
if (this.outputEnd === 0) {
|
|
@@ -950,7 +1143,7 @@ export class Http1Connection {
|
|
|
950
1143
|
if (this.state !== H1_S_WEBSOCKET) {
|
|
951
1144
|
return H1_CLOSED
|
|
952
1145
|
}
|
|
953
|
-
if (opcode === WS_OP_CLOSE || len >
|
|
1146
|
+
if (opcode === WS_OP_CLOSE || len > this.config.outputSize - H1_CONTROL_RESERVE - 14) {
|
|
954
1147
|
return H1_INVALID
|
|
955
1148
|
}
|
|
956
1149
|
if (websocketFrameSize(len, false) > this.bodyRoom()) {
|
|
@@ -1003,34 +1196,72 @@ export class Http1Connection {
|
|
|
1003
1196
|
*/
|
|
1004
1197
|
export class Http1Server {
|
|
1005
1198
|
config: Http1Config
|
|
1199
|
+
/**
|
|
1200
|
+
* The connections built so far, slot `k`'s at `k`: one per slot ever
|
|
1201
|
+
* accepted into, and slot 0's once `connection` has asked for it.
|
|
1202
|
+
*/
|
|
1006
1203
|
connections: Http1Connection[]
|
|
1007
|
-
/**
|
|
1204
|
+
/** The `Date` every slot's responses carry, formatted once a second for all of them. `date.pin` fixes its clock. */
|
|
1205
|
+
date: HttpDate
|
|
1206
|
+
/**
|
|
1207
|
+
* Each slot's descriptor, or -1 while it is free, for the slots accepted
|
|
1208
|
+
* into so far: its length is the high-water mark, and a slot past it has
|
|
1209
|
+
* never been used.
|
|
1210
|
+
*/
|
|
1008
1211
|
fds: i32[]
|
|
1212
|
+
/** Each used slot's interest as the loop last armed it, for `rearm`. */
|
|
1213
|
+
armed: i32[]
|
|
1214
|
+
/** The output buffers the slots borrow while they write. */
|
|
1215
|
+
outputs: Http1OutputPool
|
|
1216
|
+
/**
|
|
1217
|
+
* The slots closed since they were used, a stack whose top `accept` takes
|
|
1218
|
+
* before it builds a slot past the high-water mark: finding one costs the
|
|
1219
|
+
* same with every slot free as with all but one busy, where a scan of
|
|
1220
|
+
* `fds` walked the busy ones.
|
|
1221
|
+
*/
|
|
1222
|
+
free: i32[]
|
|
1009
1223
|
/** Where the socket is read into before the connection takes the bytes. */
|
|
1010
1224
|
scratch: u8[]
|
|
1011
1225
|
/** The last accepted peer's address, in `nish:net`'s 18-byte form. */
|
|
1012
1226
|
peer: u8[]
|
|
1227
|
+
/** How many slots the pool may hold, which `size` answers. */
|
|
1228
|
+
slots: i32 = 1
|
|
1013
1229
|
/** The listening socket, which the program made with `tcpListen` and still owns. */
|
|
1014
1230
|
listener: i32 = -1
|
|
1015
1231
|
|
|
1016
|
-
/**
|
|
1232
|
+
/**
|
|
1233
|
+
* A pool of `size` slots, at least one, serving `config` on `listener`.
|
|
1234
|
+
* No slot is built here: a slot's connection and its buffers are
|
|
1235
|
+
* allocated by the first `accept` into it and kept, so a pool sized for a
|
|
1236
|
+
* peak costs at start-up only the read scratch, the shared `Date` and the
|
|
1237
|
+
* output pool, and at most `size` connections once the peak has come.
|
|
1238
|
+
*/
|
|
1017
1239
|
constructor(config: Http1Config, listener: i32, size: i32) {
|
|
1018
1240
|
this.config = config
|
|
1019
1241
|
this.listener = listener
|
|
1020
1242
|
this.connections = []
|
|
1021
1243
|
this.fds = []
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1244
|
+
this.armed = []
|
|
1245
|
+
this.free = []
|
|
1246
|
+
this.slots = size < 1 ? 1 : size
|
|
1247
|
+
this.outputs = new Http1OutputPool(config.outputSize)
|
|
1248
|
+
this.date = new HttpDate()
|
|
1027
1249
|
this.scratch = new Array<u8>(config.chunkSize)
|
|
1028
1250
|
this.peer = new Array<u8>(H1_ADDRESS_SIZE)
|
|
1029
1251
|
}
|
|
1030
1252
|
|
|
1031
|
-
/**
|
|
1253
|
+
/** A new connection on this pool's outputs and `Date`, for the next slot built. */
|
|
1254
|
+
build(): Http1Connection {
|
|
1255
|
+
const conn: Http1Connection = new Http1Connection(this.config)
|
|
1256
|
+
conn.pool = this.outputs
|
|
1257
|
+
conn.date = this.date
|
|
1258
|
+
this.connections.push(conn)
|
|
1259
|
+
return conn
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/** How many slots the pool has, built or not. */
|
|
1032
1263
|
size(): i32 {
|
|
1033
|
-
return
|
|
1264
|
+
return this.slots
|
|
1034
1265
|
}
|
|
1035
1266
|
|
|
1036
1267
|
/** How many slots hold a connection. */
|
|
@@ -1054,36 +1285,57 @@ export class Http1Server {
|
|
|
1054
1285
|
return this.holds(slot) ? this.fds[slot] : -1
|
|
1055
1286
|
}
|
|
1056
1287
|
|
|
1057
|
-
/**
|
|
1288
|
+
/**
|
|
1289
|
+
* The connection of `slot`. A slot never accepted into, or out of range,
|
|
1290
|
+
* names slot 0's connection, which this builds when no slot has been yet
|
|
1291
|
+
* (the first `accept` then takes it, restarted).
|
|
1292
|
+
*/
|
|
1058
1293
|
connection(slot: i32): Http1Connection {
|
|
1059
|
-
|
|
1060
|
-
|
|
1294
|
+
if (slot >= 0 && slot < toI32(this.connections.length)) {
|
|
1295
|
+
return this.connections[slot]
|
|
1296
|
+
}
|
|
1297
|
+
return this.connections.length > 0 ? this.connections[0] : this.build()
|
|
1061
1298
|
}
|
|
1062
1299
|
|
|
1063
1300
|
/**
|
|
1064
1301
|
* Accepts the next waiting connection into a free slot and answers the
|
|
1065
|
-
* slot
|
|
1066
|
-
*
|
|
1302
|
+
* slot, which the program adds to its loop with `H1_WANT_READ`; -11 when
|
|
1303
|
+
* none is waiting, `H1_POOL_FULL` when every slot is busy (the connection
|
|
1304
|
+
* is closed), or `tcpAccept`'s own failure.
|
|
1067
1305
|
*/
|
|
1068
1306
|
accept(): i32 {
|
|
1069
1307
|
const fd: i32 = tcpAccept(this.listener, this.peer)
|
|
1070
1308
|
if (fd < 0) {
|
|
1071
1309
|
return fd
|
|
1072
1310
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1311
|
+
// A slot closed before is taken first, then the next never used, so the
|
|
1312
|
+
// first accepts take slots 0, 1, 2 ... and a pool that never fills never
|
|
1313
|
+
// builds its last slots.
|
|
1314
|
+
let k: i32 = H1_POOL_FULL
|
|
1315
|
+
if (this.free.length > 0) {
|
|
1316
|
+
k = this.free[this.free.length - 1]
|
|
1317
|
+
this.free.pop()
|
|
1318
|
+
} else if (toI32(this.fds.length) < this.slots) {
|
|
1319
|
+
k = toI32(this.fds.length)
|
|
1320
|
+
this.fds.push(H1_FREE)
|
|
1321
|
+
this.armed.push(H1_WANT_READ)
|
|
1322
|
+
}
|
|
1323
|
+
if (k < 0 || k >= toI32(this.fds.length) || k >= toI32(this.armed.length)) {
|
|
1324
|
+
netClose(fd)
|
|
1325
|
+
return H1_POOL_FULL
|
|
1326
|
+
}
|
|
1327
|
+
const conn: Http1Connection = k < toI32(this.connections.length) ? this.connections[k] : this.build()
|
|
1328
|
+
this.fds[k] = fd
|
|
1329
|
+
this.armed[k] = H1_WANT_READ
|
|
1330
|
+
conn.restart()
|
|
1331
|
+
conn.lastActive = monotonicNanos()
|
|
1332
|
+
return k
|
|
1084
1333
|
}
|
|
1085
1334
|
|
|
1086
|
-
/**
|
|
1335
|
+
/**
|
|
1336
|
+
* The socket of `slot` is readable: reads it while the connection has room
|
|
1337
|
+
* and the socket has more, then sends. Answers the interest.
|
|
1338
|
+
*/
|
|
1087
1339
|
readable(slot: i32): i32 {
|
|
1088
1340
|
if (!this.holds(slot)) {
|
|
1089
1341
|
return H1_DONE
|
|
@@ -1108,6 +1360,13 @@ export class Http1Server {
|
|
|
1108
1360
|
}
|
|
1109
1361
|
moved = true
|
|
1110
1362
|
conn.feed(this.scratch, H1_ZERO, n)
|
|
1363
|
+
// A short read emptied the socket. The loop's readiness is
|
|
1364
|
+
// level-triggered, so more input, or the peer's close, wakes it again;
|
|
1365
|
+
// reading once more here would only answer -11, at the price of a
|
|
1366
|
+
// system call on every request.
|
|
1367
|
+
if (n < want) {
|
|
1368
|
+
break
|
|
1369
|
+
}
|
|
1111
1370
|
}
|
|
1112
1371
|
if (moved) {
|
|
1113
1372
|
conn.lastActive = monotonicNanos()
|
|
@@ -1180,6 +1439,23 @@ export class Http1Server {
|
|
|
1180
1439
|
if (conn.outputEnd - conn.outputStart < pending) {
|
|
1181
1440
|
conn.lastActive = monotonicNanos()
|
|
1182
1441
|
}
|
|
1442
|
+
conn.release()
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* Whether the program must `pollModify` `slot`'s descriptor to `wants`, the
|
|
1447
|
+
* interest `readable`, `writable` or `flush` answered, and records it as
|
|
1448
|
+
* armed when it must. A keep-alive connection answers the same interest
|
|
1449
|
+
* request after request, and each `pollModify` is a system call
|
|
1450
|
+
* (`epoll_ctl`) that changes nothing.
|
|
1451
|
+
*/
|
|
1452
|
+
rearm(slot: i32, wants: i32): boolean {
|
|
1453
|
+
const interest: i32 = wants & (H1_WANT_READ | H1_WANT_WRITE)
|
|
1454
|
+
if (!this.holds(slot) || slot >= toI32(this.armed.length) || this.armed[slot] === interest) {
|
|
1455
|
+
return false
|
|
1456
|
+
}
|
|
1457
|
+
this.armed[slot] = interest
|
|
1458
|
+
return true
|
|
1183
1459
|
}
|
|
1184
1460
|
|
|
1185
1461
|
/** Closes `slot`'s socket, whatever is left unsent, and frees the slot. */
|
|
@@ -1189,6 +1465,10 @@ export class Http1Server {
|
|
|
1189
1465
|
}
|
|
1190
1466
|
netClose(this.fds[slot])
|
|
1191
1467
|
this.fds[slot] = H1_FREE
|
|
1468
|
+
this.free.push(slot)
|
|
1469
|
+
if (slot < toI32(this.connections.length)) {
|
|
1470
|
+
this.connections[slot].abort()
|
|
1471
|
+
}
|
|
1192
1472
|
}
|
|
1193
1473
|
|
|
1194
1474
|
/** Closes every slot that has been idle past `idleTimeout` at `now` (`monotonicNanos`); answers how many. */
|
|
@@ -1209,25 +1489,69 @@ export class Http1Server {
|
|
|
1209
1489
|
* `Http1Connection` beside each slot of a `TlsTcpServer`, run the way
|
|
1210
1490
|
* `nish/net/http2-tls` runs HTTP/2. A handshake that chose ALPN `http/1.1`,
|
|
1211
1491
|
* 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`.
|
|
1492
|
+
* connection; any other protocol is shut down with `close_notify`. A slot's
|
|
1493
|
+
* connection is built on the first accept into it and kept, as
|
|
1494
|
+
* `Http1Server`'s is.
|
|
1213
1495
|
*/
|
|
1214
1496
|
export class Http1TlsServer {
|
|
1497
|
+
config: Http1Config
|
|
1215
1498
|
tls: TlsTcpServer
|
|
1499
|
+
/**
|
|
1500
|
+
* The connections built so far, slot `k`'s at `k`: one per slot ever
|
|
1501
|
+
* accepted into. `connection` never adds to it; a slot never built reads
|
|
1502
|
+
* `vacant` instead.
|
|
1503
|
+
*/
|
|
1216
1504
|
connections: Http1Connection[]
|
|
1505
|
+
/**
|
|
1506
|
+
* What `connection` answers for every slot never built: one connection no
|
|
1507
|
+
* slot holds and every such slot shares, so the slot never reads as
|
|
1508
|
+
* another slot's live request. Its first read allocates it, once per
|
|
1509
|
+
* server, and a caller must not change it.
|
|
1510
|
+
*/
|
|
1511
|
+
vacant: Http1Connection | null = null
|
|
1512
|
+
/** The `Date` every slot's responses carry, as `Http1Server.date` is. */
|
|
1513
|
+
date: HttpDate
|
|
1217
1514
|
/** Where TLS decrypts into before the connection takes the bytes. */
|
|
1218
1515
|
scratch: u8[]
|
|
1516
|
+
/** The output buffers the slots borrow while they write. */
|
|
1517
|
+
outputs: Http1OutputPool
|
|
1219
1518
|
|
|
1220
|
-
/**
|
|
1519
|
+
/**
|
|
1520
|
+
* A pool of `size` slots, at least one, serving `config` over TLS under
|
|
1521
|
+
* `tlsConfig` on `listener`. No slot is built here: the first `accept` into
|
|
1522
|
+
* a slot builds its connection and, in `tls`, its handshake state, and
|
|
1523
|
+
* keeps them, so a pool sized for a peak costs at start-up only the read
|
|
1524
|
+
* scratches, the shared `Date` and the empty pools.
|
|
1525
|
+
*/
|
|
1221
1526
|
constructor(tlsConfig: TlsServerConfig, config: Http1Config, listener: i32, size: i32) {
|
|
1527
|
+
this.config = config
|
|
1222
1528
|
this.tls = new TlsTcpServer(tlsConfig, listener, size)
|
|
1223
1529
|
this.connections = []
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
}
|
|
1530
|
+
this.outputs = new Http1OutputPool(config.outputSize)
|
|
1531
|
+
this.date = new HttpDate()
|
|
1227
1532
|
this.scratch = new Array<u8>(config.chunkSize)
|
|
1228
1533
|
}
|
|
1229
1534
|
|
|
1230
|
-
/**
|
|
1535
|
+
/**
|
|
1536
|
+
* A new connection on this pool's outputs and `Date`, for the next slot
|
|
1537
|
+
* built. Called by `accept` alone, as `tls` builds slots: slot `k`'s
|
|
1538
|
+
* connection must sit at index `k` of `connections`.
|
|
1539
|
+
*/
|
|
1540
|
+
build(): Http1Connection {
|
|
1541
|
+
const conn: Http1Connection = this.make()
|
|
1542
|
+
this.connections.push(conn)
|
|
1543
|
+
return conn
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
/** A connection on this pool's outputs and `Date` that no slot holds yet. */
|
|
1547
|
+
make(): Http1Connection {
|
|
1548
|
+
const conn: Http1Connection = new Http1Connection(this.config)
|
|
1549
|
+
conn.pool = this.outputs
|
|
1550
|
+
conn.date = this.date
|
|
1551
|
+
return conn
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
/** How many slots the pool has, built or not. */
|
|
1231
1555
|
size(): i32 {
|
|
1232
1556
|
return this.tls.size()
|
|
1233
1557
|
}
|
|
@@ -1247,10 +1571,32 @@ export class Http1TlsServer {
|
|
|
1247
1571
|
return this.tls.fd(slot)
|
|
1248
1572
|
}
|
|
1249
1573
|
|
|
1250
|
-
/**
|
|
1574
|
+
/**
|
|
1575
|
+
* Whether `slot` holds a connection and has its HTTP/1.1 connection built,
|
|
1576
|
+
* as `Http2TlsServer.live` asks: a slot accepted on `tls` directly holds a
|
|
1577
|
+
* TLS session and no `Http1Connection`, and reads as free here.
|
|
1578
|
+
*/
|
|
1579
|
+
live(slot: i32): boolean {
|
|
1580
|
+
return this.tls.holds(slot) && slot < toI32(this.connections.length)
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* The HTTP/1.1 connection of `slot`. A slot never accepted into answers
|
|
1585
|
+
* `vacant`, as a free slot answers a connection that holds nothing, and
|
|
1586
|
+
* builds no slot; a slot out of range names slot 0's.
|
|
1587
|
+
*/
|
|
1251
1588
|
connection(slot: i32): Http1Connection {
|
|
1252
|
-
const
|
|
1253
|
-
|
|
1589
|
+
const index: i32 = slot >= 0 && slot < this.tls.size() ? slot : 0
|
|
1590
|
+
if (index >= 0 && index < toI32(this.connections.length)) {
|
|
1591
|
+
return this.connections[index]
|
|
1592
|
+
}
|
|
1593
|
+
const vacant: Http1Connection | null = this.vacant
|
|
1594
|
+
if (vacant !== null) {
|
|
1595
|
+
return vacant
|
|
1596
|
+
}
|
|
1597
|
+
const made: Http1Connection = this.make()
|
|
1598
|
+
this.vacant = made
|
|
1599
|
+
return made
|
|
1254
1600
|
}
|
|
1255
1601
|
|
|
1256
1602
|
/** The ALPN protocol `slot`'s handshake chose, or empty. */
|
|
@@ -1267,11 +1613,17 @@ export class Http1TlsServer {
|
|
|
1267
1613
|
/**
|
|
1268
1614
|
* Accepts the next waiting connection as `TlsTcpServer.accept` does, with
|
|
1269
1615
|
* the caller's 32-byte server random and x25519 key, and gives it a fresh
|
|
1270
|
-
* connection. Answers the slot, or
|
|
1616
|
+
* connection, building the slot's on its first accept. Answers the slot, or
|
|
1617
|
+
* what `TlsTcpServer.accept` answers.
|
|
1271
1618
|
*/
|
|
1272
1619
|
accept(serverRandom: u8[], ephemeralPrivate: u8[]): i32 {
|
|
1273
1620
|
const slot: i32 = this.tls.accept(serverRandom, ephemeralPrivate)
|
|
1274
|
-
if (slot >= 0
|
|
1621
|
+
if (slot >= 0) {
|
|
1622
|
+
// `tls` builds a slot past its high-water mark one at a time, so this
|
|
1623
|
+
// builds at most the one connection, keeping slot `k`'s at `k`.
|
|
1624
|
+
while (toI32(this.connections.length) <= slot) {
|
|
1625
|
+
this.build()
|
|
1626
|
+
}
|
|
1275
1627
|
const conn: Http1Connection = this.connections[slot]
|
|
1276
1628
|
conn.restart()
|
|
1277
1629
|
conn.lastActive = monotonicNanos()
|
|
@@ -1281,7 +1633,7 @@ export class Http1TlsServer {
|
|
|
1281
1633
|
|
|
1282
1634
|
/** The socket of `slot` is readable: TLS reads it, and what the connection has to send goes after. Answers the interest. */
|
|
1283
1635
|
readable(slot: i32): i32 {
|
|
1284
|
-
if (this.
|
|
1636
|
+
if (this.live(slot)) {
|
|
1285
1637
|
this.connections[slot].lastActive = monotonicNanos()
|
|
1286
1638
|
}
|
|
1287
1639
|
this.tls.readable(slot)
|
|
@@ -1306,7 +1658,7 @@ export class Http1TlsServer {
|
|
|
1306
1658
|
* slot, or one whose ALPN was another protocol, answers `H1_ERROR`.
|
|
1307
1659
|
*/
|
|
1308
1660
|
next(slot: i32): i32 {
|
|
1309
|
-
if (!this.
|
|
1661
|
+
if (!this.live(slot) || this.refused(slot)) {
|
|
1310
1662
|
return H1_ERROR
|
|
1311
1663
|
}
|
|
1312
1664
|
const conn: Http1Connection = this.connections[slot]
|
|
@@ -1339,7 +1691,7 @@ export class Http1TlsServer {
|
|
|
1339
1691
|
* connection still holds bytes.
|
|
1340
1692
|
*/
|
|
1341
1693
|
flush(slot: i32): i32 {
|
|
1342
|
-
if (!this.
|
|
1694
|
+
if (!this.live(slot)) {
|
|
1343
1695
|
return TLS_RECORD_DONE
|
|
1344
1696
|
}
|
|
1345
1697
|
if (this.refused(slot)) {
|
|
@@ -1354,8 +1706,14 @@ export class Http1TlsServer {
|
|
|
1354
1706
|
return conn.wantsWrite() && (wants & TLS_RECORD_DONE) === 0 ? wants | TLS_RECORD_WANT_WRITE : wants
|
|
1355
1707
|
}
|
|
1356
1708
|
|
|
1357
|
-
/**
|
|
1709
|
+
/**
|
|
1710
|
+
* Writes what `slot`'s connection holds into TLS, as far as TLS takes it;
|
|
1711
|
+
* a slot never built has nothing to write.
|
|
1712
|
+
*/
|
|
1358
1713
|
send(slot: i32): void {
|
|
1714
|
+
if (slot < 0 || slot >= toI32(this.connections.length)) {
|
|
1715
|
+
return
|
|
1716
|
+
}
|
|
1359
1717
|
const conn: Http1Connection = this.connections[slot]
|
|
1360
1718
|
const pending: i32 = conn.outputEnd - conn.outputStart
|
|
1361
1719
|
while (conn.wantsWrite()) {
|
|
@@ -1368,11 +1726,15 @@ export class Http1TlsServer {
|
|
|
1368
1726
|
if (conn.outputEnd - conn.outputStart < pending) {
|
|
1369
1727
|
conn.lastActive = monotonicNanos()
|
|
1370
1728
|
}
|
|
1729
|
+
conn.release()
|
|
1371
1730
|
}
|
|
1372
1731
|
|
|
1373
1732
|
/** Ends `slot`'s connection and frees the slot at once, as `TlsTcpServer.close` does. */
|
|
1374
1733
|
close(slot: i32): void {
|
|
1375
1734
|
this.tls.close(slot)
|
|
1735
|
+
if (slot >= 0 && slot < toI32(this.connections.length)) {
|
|
1736
|
+
this.connections[slot].abort()
|
|
1737
|
+
}
|
|
1376
1738
|
}
|
|
1377
1739
|
|
|
1378
1740
|
/** Closes every slot that has been idle past `idleTimeout` at `now` (`monotonicNanos`); answers how many. */
|