@amritk/nish-x86_64-linux 0.27.2 → 0.29.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 +16 -8
- package/bin/nish +0 -0
- package/docs/AI.md +118 -26
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -1
- package/runtime/core/host.mjs +4 -0
- package/runtime/core.d.ts +47 -0
- package/runtime/core.mjs +67 -0
- package/runtime/nish.d.ts +176 -0
- package/runtime/nish.h +120 -6
- package/runtime/nish.mjs +24 -4
- package/runtime/runtime-checksum.c +270 -0
- package/runtime/runtime-net.c +386 -5
- package/runtime/runtime-os.c +59 -8
- package/runtime/runtime-parallel.c +81 -5
- package/runtime/runtime-sandbox.c +479 -0
- package/runtime/runtime-sha256.c +239 -0
- package/runtime/runtime-shm.c +541 -0
- package/runtime/runtime.c +827 -791
- package/scripts/build.sh +79 -28
- package/std/README.md +25 -13
- package/std/collections.ts +5 -5
- package/std/compress/checksum.ts +88 -75
- package/std/compress/deflate.ts +224 -0
- package/std/compress/gzip.ts +52 -0
- package/std/crypto/bcrypt.ts +816 -280
- package/std/crypto/hmac.ts +305 -2
- package/std/crypto/jwt.ts +1661 -0
- package/std/json-equal.ts +335 -0
- package/std/json-format.ts +1070 -0
- package/std/json-number.ts +428 -0
- package/std/json-pointer.ts +245 -0
- package/std/json-typed.ts +34 -0
- package/std/json-value.ts +510 -17
- package/std/json.ts +35 -6
- package/std/net/handler.ts +1348 -0
- package/std/net/http-client.ts +97 -16
- package/std/net/http-compress.ts +301 -16
- package/std/net/http1-client.ts +104 -19
- package/std/net/http1-server.ts +127 -24
- package/std/net/http1.ts +24 -4
- package/std/net/outbound.ts +409 -0
- package/std/net/postgres.ts +405 -0
- package/std/net/prefork.ts +344 -20
- package/std/net/websocket.ts +295 -14
- package/std/regex.ts +613 -52
- package/std/secret.ts +1 -1
- package/std/sync/ring.ts +587 -0
- package/std/sync/shm.ts +284 -0
- package/std/term-io.ts +35 -8
- package/std/term-rate.ts +13 -1
- package/std/threads.ts +35 -3
- package/std/yaml-json.ts +841 -0
- /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
package/std/net/http1-client.ts
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
* with nothing allocated. A chunked body is written with `http1Chunk` and
|
|
11
11
|
* `http1LastChunk` from `nish/net/http1`. The writer refuses anything that
|
|
12
12
|
* would let a caller's string end a line early or frame the body itself — the
|
|
13
|
-
* request-splitting and request-smuggling shapes.
|
|
13
|
+
* request-splitting and request-smuggling shapes. `http1UpgradeRequestHead`
|
|
14
|
+
* is the one way to ask the server to switch protocols: it writes `Upgrade`
|
|
15
|
+
* and `Connection: Upgrade` itself, and every other writer refuses a
|
|
16
|
+
* `Connection` that lists `upgrade`.
|
|
14
17
|
*
|
|
15
18
|
* **The parser** is a push-pull machine, like the server's. A caller tells it
|
|
16
19
|
* what it asked (`expect`), `feed`s whatever slice the socket gave it, cut
|
|
@@ -34,10 +37,11 @@
|
|
|
34
37
|
* final one follows), `HTTP1_HEAD` (the final response's status line and
|
|
35
38
|
* header section are in), `HTTP1_BODY` (some body bytes, de-chunked),
|
|
36
39
|
* `HTTP1_END` (the response is complete; with `keepAlive()` the next one
|
|
37
|
-
* follows), `HTTP1_UPGRADE` (a CONNECT was answered with 2xx
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
40
|
+
* follows), `HTTP1_UPGRADE` (a CONNECT was answered with 2xx, or a request
|
|
41
|
+
* that `expectUpgrade` named was answered with 101, and the connection is a
|
|
42
|
+
* tunnel now: `tunnelBytes()` answers what was fed past the head),
|
|
43
|
+
* `HTTP1_CLOSED` (the connection is done: the server closed it between
|
|
44
|
+
* responses, or the last response was not kept alive) and
|
|
41
45
|
* `HTTP1_ERROR`. The last three repeat on every later call. Only complete
|
|
42
46
|
* lines are acted on, body bytes are counted, never guessed, and every
|
|
43
47
|
* refusal is decided by the same byte wherever the input was cut, so the
|
|
@@ -55,7 +59,7 @@
|
|
|
55
59
|
* **What it refuses**, each with its own code in `error`: a malformed status
|
|
56
60
|
* line, an HTTP major version other than 1, a bare LF, a malformed field line
|
|
57
61
|
* (an obs-fold, whitespace before the colon, a control byte in a value), a 101
|
|
58
|
-
*
|
|
62
|
+
* to a request that did not ask to switch protocols, `Transfer-Encoding` together
|
|
59
63
|
* with `Content-Length`, a transfer coding other than one `chunked` or one in
|
|
60
64
|
* an HTTP/1.0 response, a `Content-Length` that is not a decimal or whose
|
|
61
65
|
* repeats disagree, a malformed chunk, a status line, header section, field
|
|
@@ -112,7 +116,7 @@ export const HTTP1_E_VERSION: i32 = 2
|
|
|
112
116
|
export const HTTP1_E_BARE_LF: i32 = 3
|
|
113
117
|
/** `error`: a malformed field line: an obs-fold, a bad name, whitespace before the colon, a control byte. */
|
|
114
118
|
export const HTTP1_E_FIELD: i32 = 4
|
|
115
|
-
/** `error`: a 101
|
|
119
|
+
/** `error`: a 101 to a request that did not ask to switch protocols (see `expectUpgrade`). */
|
|
116
120
|
export const HTTP1_E_SWITCH: i32 = 5
|
|
117
121
|
/** `error`: both `Transfer-Encoding` and `Content-Length` (RFC 9112 §6.1). */
|
|
118
122
|
export const HTTP1_E_FRAMING: i32 = 6
|
|
@@ -290,11 +294,16 @@ const http1ClientRequestNeed = (
|
|
|
290
294
|
target: string,
|
|
291
295
|
names: string[],
|
|
292
296
|
values: string[],
|
|
293
|
-
bodyLength: i64
|
|
297
|
+
bodyLength: i64,
|
|
298
|
+
upgrade: string
|
|
294
299
|
): i64 => {
|
|
295
300
|
if (!http1IsToken(method) || !http1ClientTargetOk(method, target)) {
|
|
296
301
|
return toI64(-1)
|
|
297
302
|
}
|
|
303
|
+
const upgrading: boolean = toI32(upgrade.length) > 0
|
|
304
|
+
if (upgrading && (!http1IsToken(upgrade) || bodyLength !== toI64(-2))) {
|
|
305
|
+
return toI64(-1)
|
|
306
|
+
}
|
|
298
307
|
if (bodyLength < toI64(-2)) {
|
|
299
308
|
return toI64(-1)
|
|
300
309
|
}
|
|
@@ -319,7 +328,13 @@ const http1ClientRequestNeed = (
|
|
|
319
328
|
) {
|
|
320
329
|
return toI64(-1)
|
|
321
330
|
}
|
|
322
|
-
if (
|
|
331
|
+
if (
|
|
332
|
+
http1SameWord(name, 0, nameLength, "connection") &&
|
|
333
|
+
(upgrading || http1ClientListHas(value, "upgrade"))
|
|
334
|
+
) {
|
|
335
|
+
return toI64(-1)
|
|
336
|
+
}
|
|
337
|
+
if (upgrading && http1SameWord(name, 0, nameLength, "upgrade")) {
|
|
323
338
|
return toI64(-1)
|
|
324
339
|
}
|
|
325
340
|
if (http1SameWord(name, 0, nameLength, "host")) {
|
|
@@ -335,6 +350,10 @@ const http1ClientRequestNeed = (
|
|
|
335
350
|
} else if (bodyLength === toI64(-1)) {
|
|
336
351
|
need = need + toI64(28)
|
|
337
352
|
}
|
|
353
|
+
if (upgrading) {
|
|
354
|
+
// "Upgrade: " value CRLF "Connection: Upgrade" CRLF
|
|
355
|
+
need = need + toI64(upgrade.length) + toI64(32)
|
|
356
|
+
}
|
|
338
357
|
return need
|
|
339
358
|
}
|
|
340
359
|
|
|
@@ -364,9 +383,9 @@ const http1ClientPutField = (out: u8[], at: i32, names: string[], values: string
|
|
|
364
383
|
* early and let the rest be read as a field or a second request); `names` and
|
|
365
384
|
* `values` of different lengths; not exactly one `Host`; a `Content-Length` or
|
|
366
385
|
* `Transfer-Encoding` among the caller's fields, since the framing is decided
|
|
367
|
-
* here; a `Connection` that lists `upgrade`, which
|
|
368
|
-
*
|
|
369
|
-
* well formed but does not fit. Either way `out` is untouched.
|
|
386
|
+
* here; a `Connection` that lists `upgrade`, which only
|
|
387
|
+
* `http1UpgradeRequestHead` may write; and a `bodyLength` below -2. Answers
|
|
388
|
+
* `HTTP1_NO_ROOM` when it is well formed but does not fit. Either way `out` is untouched.
|
|
370
389
|
*/
|
|
371
390
|
export const http1WriteRequestHead = (
|
|
372
391
|
out: u8[],
|
|
@@ -377,14 +396,14 @@ export const http1WriteRequestHead = (
|
|
|
377
396
|
values: string[],
|
|
378
397
|
bodyLength: i64
|
|
379
398
|
): i32 => {
|
|
380
|
-
const need: i64 = http1ClientRequestNeed(method, target, names, values, bodyLength)
|
|
399
|
+
const need: i64 = http1ClientRequestNeed(method, target, names, values, bodyLength, "")
|
|
381
400
|
if (need < toI64(0)) {
|
|
382
401
|
return HTTP1_REFUSED
|
|
383
402
|
}
|
|
384
403
|
if (at < 0 || need > toI64(toI32(out.length) - at)) {
|
|
385
404
|
return HTTP1_NO_ROOM
|
|
386
405
|
}
|
|
387
|
-
return http1ClientPutHead(out, at, method, target, names, values, bodyLength)
|
|
406
|
+
return http1ClientPutHead(out, at, method, target, names, values, bodyLength, "")
|
|
388
407
|
}
|
|
389
408
|
|
|
390
409
|
/** Writes a head `http1ClientRequestNeed` accepted into room it counted; answers the end. */
|
|
@@ -395,7 +414,8 @@ const http1ClientPutHead = (
|
|
|
395
414
|
target: string,
|
|
396
415
|
names: string[],
|
|
397
416
|
values: string[],
|
|
398
|
-
bodyLength: i64
|
|
417
|
+
bodyLength: i64,
|
|
418
|
+
upgrade: string
|
|
399
419
|
): i32 => {
|
|
400
420
|
let end: i32 = http1PutText(out, at, method)
|
|
401
421
|
end = http1PutText(out, end, " ")
|
|
@@ -419,6 +439,11 @@ const http1ClientPutHead = (
|
|
|
419
439
|
} else if (bodyLength === toI64(-1)) {
|
|
420
440
|
end = http1PutText(out, end, "Transfer-Encoding: chunked\r\n")
|
|
421
441
|
}
|
|
442
|
+
if (toI32(upgrade.length) > 0) {
|
|
443
|
+
end = http1PutText(out, end, "Upgrade: ")
|
|
444
|
+
end = http1PutText(out, end, upgrade)
|
|
445
|
+
end = http1PutText(out, end, "\r\nConnection: Upgrade\r\n")
|
|
446
|
+
}
|
|
422
447
|
return http1PutText(out, end, "\r\n")
|
|
423
448
|
}
|
|
424
449
|
|
|
@@ -432,13 +457,46 @@ export const http1RequestHead = (
|
|
|
432
457
|
names: string[],
|
|
433
458
|
values: string[],
|
|
434
459
|
bodyLength: i64
|
|
460
|
+
): u8[] | null => http1ClientHeadArray(method, target, names, values, bodyLength, "")
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* A request that asks the server to switch the connection to `upgrade` (RFC
|
|
464
|
+
* 9110 §7.8), such as a WebSocket opening handshake: `http1RequestHead`'s head
|
|
465
|
+
* with no body, and `Upgrade: <upgrade>` and `Connection: Upgrade` written
|
|
466
|
+
* after the caller's fields. Answers `null` for everything `http1RequestHead`
|
|
467
|
+
* refuses, an `upgrade` that is not one token, and an `Upgrade` or
|
|
468
|
+
* `Connection` among the caller's fields, which would contradict the two this
|
|
469
|
+
* writes. A response to it is read after `expect(false, false)` and then
|
|
470
|
+
* `expectUpgrade()`, so that its 101 answers `HTTP1_UPGRADE`.
|
|
471
|
+
*/
|
|
472
|
+
export const http1UpgradeRequestHead = (
|
|
473
|
+
method: string,
|
|
474
|
+
target: string,
|
|
475
|
+
names: string[],
|
|
476
|
+
values: string[],
|
|
477
|
+
upgrade: string
|
|
435
478
|
): u8[] | null => {
|
|
436
|
-
|
|
479
|
+
if (toI32(upgrade.length) === 0) {
|
|
480
|
+
return null
|
|
481
|
+
}
|
|
482
|
+
return http1ClientHeadArray(method, target, names, values, toI64(-2), upgrade)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** The head `http1ClientRequestNeed` accepts in an array of its own, or `null`. */
|
|
486
|
+
const http1ClientHeadArray = (
|
|
487
|
+
method: string,
|
|
488
|
+
target: string,
|
|
489
|
+
names: string[],
|
|
490
|
+
values: string[],
|
|
491
|
+
bodyLength: i64,
|
|
492
|
+
upgrade: string
|
|
493
|
+
): u8[] | null => {
|
|
494
|
+
const need: i64 = http1ClientRequestNeed(method, target, names, values, bodyLength, upgrade)
|
|
437
495
|
if (need < toI64(0) || need > toI64(1073741824)) {
|
|
438
496
|
return null
|
|
439
497
|
}
|
|
440
498
|
const out: u8[] = new Array<u8>(toI32(need))
|
|
441
|
-
http1ClientPutHead(out, 0, method, target, names, values, bodyLength)
|
|
499
|
+
http1ClientPutHead(out, 0, method, target, names, values, bodyLength, upgrade)
|
|
442
500
|
return out
|
|
443
501
|
}
|
|
444
502
|
|
|
@@ -533,6 +591,8 @@ export class Http1ResponseParser {
|
|
|
533
591
|
/** What `expect` said of the request this response answers. */
|
|
534
592
|
wasHead: boolean = false
|
|
535
593
|
wasConnect: boolean = false
|
|
594
|
+
/** Whether `expectUpgrade` said the request asked to switch protocols. */
|
|
595
|
+
wasUpgrade: boolean = false
|
|
536
596
|
/** Whether `finish()` said the server closed the connection. */
|
|
537
597
|
closed: boolean = false
|
|
538
598
|
|
|
@@ -567,6 +627,24 @@ export class Http1ResponseParser {
|
|
|
567
627
|
if (this.state === HTTP1_CLIENT_S_STATUS_LINE && this.informational === 0) {
|
|
568
628
|
this.wasHead = methodWasHead
|
|
569
629
|
this.wasConnect = wasConnect
|
|
630
|
+
this.wasUpgrade = false
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Says the request this response answers asked to switch protocols (it was
|
|
636
|
+
* written by `http1UpgradeRequestHead`), so a 101 is that switch: the parser
|
|
637
|
+
* answers `HTTP1_HEAD` with its status and fields readable, `HTTP1_END`, and
|
|
638
|
+
* then `HTTP1_UPGRADE`, and `tunnelBytes()` answers what was fed past the
|
|
639
|
+
* head. Any other status is read as it would be without it. It is said after
|
|
640
|
+
* `expect`, which clears it, and like `expect` it takes effect only while the
|
|
641
|
+
* parser waits for a response's first status line. Without it a 101 is
|
|
642
|
+
* refused with `HTTP1_E_SWITCH`: a server may not switch a connection whose
|
|
643
|
+
* client never asked.
|
|
644
|
+
*/
|
|
645
|
+
expectUpgrade(): void {
|
|
646
|
+
if (this.state === HTTP1_CLIENT_S_STATUS_LINE && this.informational === 0) {
|
|
647
|
+
this.wasUpgrade = true
|
|
570
648
|
}
|
|
571
649
|
}
|
|
572
650
|
|
|
@@ -949,7 +1027,13 @@ const http1ClientConnectionHas = (p: Http1ResponseParser, word: string): boolean
|
|
|
949
1027
|
const http1ClientEndOfHead = (p: Http1ResponseParser): i32 => {
|
|
950
1028
|
const status: i32 = p.status
|
|
951
1029
|
if (status === 101) {
|
|
952
|
-
|
|
1030
|
+
if (!p.wasUpgrade) {
|
|
1031
|
+
return http1ClientFail(p, HTTP1_E_SWITCH, "a 101 Switching Protocols the request did not ask for")
|
|
1032
|
+
}
|
|
1033
|
+
// RFC 9110 §15.2.2: the connection switches right after the 101's empty
|
|
1034
|
+
// line, so what follows the head is the new protocol's.
|
|
1035
|
+
p.state = HTTP1_CLIENT_S_MESSAGE_END
|
|
1036
|
+
return HTTP1_HEAD
|
|
953
1037
|
}
|
|
954
1038
|
if (status < 200) {
|
|
955
1039
|
if (p.informational >= HTTP1_MAX_INFORMATIONAL) {
|
|
@@ -1203,7 +1287,7 @@ const http1ClientNext = (p: Http1ResponseParser): i32 => {
|
|
|
1203
1287
|
break
|
|
1204
1288
|
}
|
|
1205
1289
|
case HTTP1_CLIENT_S_MESSAGE_END: {
|
|
1206
|
-
if (p.wasConnect && p.status < 300) {
|
|
1290
|
+
if ((p.wasConnect && p.status < 300) || (p.wasUpgrade && p.status === 101)) {
|
|
1207
1291
|
p.state = HTTP1_CLIENT_S_TUNNEL
|
|
1208
1292
|
} else if (!p.persistent) {
|
|
1209
1293
|
p.state = HTTP1_CLIENT_S_CLOSED
|
|
@@ -1213,6 +1297,7 @@ const http1ClientNext = (p: Http1ResponseParser): i32 => {
|
|
|
1213
1297
|
p.informational = 0
|
|
1214
1298
|
p.wasHead = false
|
|
1215
1299
|
p.wasConnect = false
|
|
1300
|
+
p.wasUpgrade = false
|
|
1216
1301
|
}
|
|
1217
1302
|
return HTTP1_END
|
|
1218
1303
|
}
|
package/std/net/http1-server.ts
CHANGED
|
@@ -115,12 +115,15 @@
|
|
|
115
115
|
import { Secret } from "nish:secret"
|
|
116
116
|
import { copyBytes } from "nish/bytes"
|
|
117
117
|
import {
|
|
118
|
+
HTTP_CODING_GZIP,
|
|
118
119
|
HTTP_CODING_IDENTITY,
|
|
120
|
+
HTTP_CODING_NONE,
|
|
121
|
+
HTTP_DEFAULT_MSS,
|
|
119
122
|
HTTP_OFFER_GZIP,
|
|
120
123
|
HttpCompressor,
|
|
121
124
|
httpAcceptEncoding,
|
|
122
125
|
} from "nish/net/http-compress"
|
|
123
|
-
import { netClose, netRead, netWrite, tcpAccept } from "nish:net"
|
|
126
|
+
import { netClose, netMss, netRead, netWrite, tcpAccept } from "nish:net"
|
|
124
127
|
import {
|
|
125
128
|
HTTP1_BODY,
|
|
126
129
|
HTTP1_CHUNKED,
|
|
@@ -491,7 +494,11 @@ export class Http1Connection {
|
|
|
491
494
|
* put another, to share it or to pin its clock.
|
|
492
495
|
*/
|
|
493
496
|
date: HttpDate
|
|
494
|
-
/**
|
|
497
|
+
/**
|
|
498
|
+
* The request parser: its head (`methodIs`, `targetIs`, `header`, spans) is
|
|
499
|
+
* the current request's from its `H1_REQUEST` until its response is done,
|
|
500
|
+
* `H1_END` included, however much of a pipelined request is fed behind it.
|
|
501
|
+
*/
|
|
495
502
|
parser: Http1Parser
|
|
496
503
|
/**
|
|
497
504
|
* The frame decoder: made when the slot first switches to WebSocket, so a
|
|
@@ -559,6 +566,15 @@ export class Http1Connection {
|
|
|
559
566
|
inputEnded: boolean = false
|
|
560
567
|
/** A head has begun to arrive and is not read whole yet: `headSince` is its deadline's start. */
|
|
561
568
|
headTimed: boolean = false
|
|
569
|
+
/**
|
|
570
|
+
* The TCP maximum segment size the program sets for this connection, for
|
|
571
|
+
* `respondEncoded` under `HttpCompressor.segmentMode`; 0, the default,
|
|
572
|
+
* leaves it to the compressor and the socket (`segmentMss`). Each accept
|
|
573
|
+
* puts it back to 0.
|
|
574
|
+
*/
|
|
575
|
+
mss: i32 = 0
|
|
576
|
+
/** The MSS `Http1Server.accept` read from this connection's socket (`netMss`), or 0 when it reports none. */
|
|
577
|
+
socketMss: i32 = 0
|
|
562
578
|
|
|
563
579
|
/** A connection under `config`. A cap outside what the connection can work with panics. */
|
|
564
580
|
constructor(config: Http1Config) {
|
|
@@ -642,6 +658,8 @@ export class Http1Connection {
|
|
|
642
658
|
this.inputEnded = false
|
|
643
659
|
this.headTimed = false
|
|
644
660
|
this.headSince = 0
|
|
661
|
+
this.mss = 0
|
|
662
|
+
this.socketMss = 0
|
|
645
663
|
this.release()
|
|
646
664
|
this.park()
|
|
647
665
|
}
|
|
@@ -1218,9 +1236,23 @@ export class Http1Connection {
|
|
|
1218
1236
|
* `Accept-Encoding` with gzip offered (`httpAcceptEncoding`):
|
|
1219
1237
|
* `HTTP_CODING_GZIP`, `HTTP_CODING_IDENTITY` (also with no such field), or
|
|
1220
1238
|
* `HTTP_CODING_NONE` when the client refuses identity and does not take
|
|
1221
|
-
* gzip. Allocates nothing.
|
|
1239
|
+
* gzip. Allocates nothing. Read at `H1_REQUEST`, `H1_BODY` or `H1_END`
|
|
1240
|
+
* alike: the head stays the request's until its response is done.
|
|
1222
1241
|
*/
|
|
1223
1242
|
acceptEncoding(): i32 {
|
|
1243
|
+
return this.acceptEncodingOf(HTTP_OFFER_GZIP)
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Whether the current request's `Accept-Encoding` refuses the body as it
|
|
1248
|
+
* is (`httpIdentityRefused`); `respondEncoded` then gzips any body it can.
|
|
1249
|
+
*/
|
|
1250
|
+
identityRefused(): boolean {
|
|
1251
|
+
return this.acceptEncodingOf(toI32(0)) === HTTP_CODING_NONE
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
/** `httpAcceptEncoding` of the current request's `Accept-Encoding`, with `offered`. */
|
|
1255
|
+
acceptEncodingOf(offered: i32): i32 {
|
|
1224
1256
|
const parser: Http1Parser = this.parser
|
|
1225
1257
|
const i: i32 = parser.headerIndex("accept-encoding")
|
|
1226
1258
|
const spans: i32[] = parser.spans
|
|
@@ -1232,7 +1264,23 @@ export class Http1Connection {
|
|
|
1232
1264
|
if (start < 0 || start > end || end > toI32(parser.head.length)) {
|
|
1233
1265
|
return HTTP_CODING_IDENTITY
|
|
1234
1266
|
}
|
|
1235
|
-
return httpAcceptEncoding(parser.head, start, end,
|
|
1267
|
+
return httpAcceptEncoding(parser.head, start, end, offered)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* The segment `respondEncoded` counts in under `enc.segmentMode`: this
|
|
1272
|
+
* connection's `mss` when the program set it, else `enc.mss` when the
|
|
1273
|
+
* program set it to other than `HTTP_DEFAULT_MSS`, else the socket's
|
|
1274
|
+
* (`socketMss`), else `HTTP_DEFAULT_MSS`.
|
|
1275
|
+
*/
|
|
1276
|
+
segmentMss(enc: HttpCompressor): i32 {
|
|
1277
|
+
if (this.mss > 0) {
|
|
1278
|
+
return this.mss
|
|
1279
|
+
}
|
|
1280
|
+
if (enc.mss > 0 && enc.mss !== HTTP_DEFAULT_MSS) {
|
|
1281
|
+
return enc.mss
|
|
1282
|
+
}
|
|
1283
|
+
return this.socketMss > 0 ? this.socketMss : HTTP_DEFAULT_MSS
|
|
1236
1284
|
}
|
|
1237
1285
|
|
|
1238
1286
|
/**
|
|
@@ -1240,9 +1288,17 @@ export class Http1Connection {
|
|
|
1240
1288
|
* call: `respond`, `write` and `end`, the body compressed by `enc` when
|
|
1241
1289
|
* `coding` (from `acceptEncoding`) is gzip and `enc.compress` takes it,
|
|
1242
1290
|
* with `Content-Encoding: gzip` and the compressed `Content-Length`, and
|
|
1243
|
-
* otherwise sent as it is; `Vary: Accept-Encoding`
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1291
|
+
* otherwise sent as it is; `Vary: Accept-Encoding` while `enc.vary` is set
|
|
1292
|
+
* and the choice turned on the request (`HttpCompressor.varies`). Under
|
|
1293
|
+
* `enc.segmentMode` the compressed response goes out only when, head
|
|
1294
|
+
* included, it takes fewer segments of `segmentMss(enc)` bytes than the
|
|
1295
|
+
* body as it is, and a response that fits one segment as it is goes so
|
|
1296
|
+
* without compressing (`enc.oneSegment`); `enc.decision`
|
|
1297
|
+
* says what was chosen. A request that refuses identity (`identityRefused`)
|
|
1298
|
+
* is sent gzip whatever the length, the saving or the segments
|
|
1299
|
+
* (`enc.compressAnyway`).
|
|
1300
|
+
* The program's fields may not name `Content-Encoding` or, with
|
|
1301
|
+
* `enc.vary`, `Vary`. The whole response is written or none of
|
|
1246
1302
|
* it: `H1_AGAIN` when the output has no room for it yet (`H1_WRITE` says
|
|
1247
1303
|
* when to try again, compressing again), `H1_INVALID` for a response
|
|
1248
1304
|
* larger than the output could ever hold or one `respond` refuses,
|
|
@@ -1263,14 +1319,7 @@ export class Http1Connection {
|
|
|
1263
1319
|
if (!this.answerable()) {
|
|
1264
1320
|
return H1_CLOSED
|
|
1265
1321
|
}
|
|
1266
|
-
const
|
|
1267
|
-
let n: i32 = len
|
|
1268
|
-
if (packed >= 0) {
|
|
1269
|
-
enc.addFields(names, values)
|
|
1270
|
-
n = packed
|
|
1271
|
-
} else {
|
|
1272
|
-
enc.identityFields(names, values)
|
|
1273
|
-
}
|
|
1322
|
+
const required: boolean = coding === HTTP_CODING_GZIP && this.identityRefused()
|
|
1274
1323
|
// `respond` and the body together, or neither: what `respond` sets is
|
|
1275
1324
|
// put back when the body does not fit behind the head. `room` first, so
|
|
1276
1325
|
// that what was sent is already moved out of the way and the `outputEnd`
|
|
@@ -1278,18 +1327,56 @@ export class Http1Connection {
|
|
|
1278
1327
|
this.room()
|
|
1279
1328
|
const outputEnd: i32 = this.outputEnd
|
|
1280
1329
|
const closeAfter: boolean = this.closeAfter
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1330
|
+
let packed: i32 = -1
|
|
1331
|
+
let n: i32 = len
|
|
1332
|
+
let r: i32 = 0
|
|
1333
|
+
let queued: boolean = false
|
|
1334
|
+
const mss: i32 = this.segmentMss(enc)
|
|
1335
|
+
if (!required && enc.segmentMode && enc.oneSegment(coding, len, mss)) {
|
|
1336
|
+
// A response that fits one segment as it is cannot take fewer
|
|
1337
|
+
// compressed, so it goes as it is with no encoder run, unless its head
|
|
1338
|
+
// pushes it past the segment: then it is taken back and decided below.
|
|
1339
|
+
enc.identityFields(names, values)
|
|
1340
|
+
r = this.respond(status, enc.names, enc.values, len)
|
|
1341
|
+
if (r !== 0) {
|
|
1342
|
+
return r
|
|
1343
|
+
}
|
|
1344
|
+
queued = this.outputEnd - outputEnd + len <= enc.segmentSize(mss)
|
|
1345
|
+
if (!queued) {
|
|
1346
|
+
this.unrespond(outputEnd, closeAfter)
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
if (!queued) {
|
|
1350
|
+
packed = required ? enc.compressAnyway(coding, body, off, len) : enc.compress(coding, body, off, len)
|
|
1351
|
+
if (packed >= 0) {
|
|
1352
|
+
enc.addFields(names, values)
|
|
1353
|
+
n = packed
|
|
1354
|
+
} else {
|
|
1355
|
+
enc.identityFields(names, values)
|
|
1356
|
+
}
|
|
1357
|
+
r = this.respond(status, enc.names, enc.values, n)
|
|
1358
|
+
if (r !== 0) {
|
|
1359
|
+
return r
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (packed >= 0 && enc.segmentMode && !required) {
|
|
1363
|
+
// The head just written, and the one the body as it is would get.
|
|
1364
|
+
const head: i32 = this.outputEnd - outputEnd
|
|
1365
|
+
const plain: i32 = enc.http1IdentityHead(head, packed, len)
|
|
1366
|
+
if (!enc.fewerSegments(plain + len, head + packed, mss)) {
|
|
1367
|
+
this.unrespond(outputEnd, closeAfter)
|
|
1368
|
+
enc.identityFields(names, values)
|
|
1369
|
+
packed = -1
|
|
1370
|
+
n = len
|
|
1371
|
+
r = this.respond(status, enc.names, enc.values, n)
|
|
1372
|
+
if (r !== 0) {
|
|
1373
|
+
return r
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1284
1376
|
}
|
|
1285
1377
|
if (!this.responseEmpty && n > this.bodyRoom()) {
|
|
1286
1378
|
const fits: boolean = this.outputEnd - outputEnd + n <= this.config.outputSize - H1_CONTROL_RESERVE
|
|
1287
|
-
this.outputEnd
|
|
1288
|
-
this.closeAfter = closeAfter
|
|
1289
|
-
this.response = H1_R_NONE
|
|
1290
|
-
this.responseFraming = 0
|
|
1291
|
-
this.responseLeft = 0
|
|
1292
|
-
this.responseEmpty = false
|
|
1379
|
+
this.unrespond(outputEnd, closeAfter)
|
|
1293
1380
|
if (!fits) {
|
|
1294
1381
|
return H1_INVALID
|
|
1295
1382
|
}
|
|
@@ -1303,6 +1390,16 @@ export class Http1Connection {
|
|
|
1303
1390
|
return this.end()
|
|
1304
1391
|
}
|
|
1305
1392
|
|
|
1393
|
+
/** Takes back a head `respond` just queued: `outputEnd` and `closeAfter` as they were before it. */
|
|
1394
|
+
unrespond(outputEnd: i32, closeAfter: boolean): void {
|
|
1395
|
+
this.outputEnd = outputEnd
|
|
1396
|
+
this.closeAfter = closeAfter
|
|
1397
|
+
this.response = H1_R_NONE
|
|
1398
|
+
this.responseFraming = 0
|
|
1399
|
+
this.responseLeft = 0
|
|
1400
|
+
this.responseEmpty = false
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1306
1403
|
// ---- WebSocket ----------------------------------------------------------------
|
|
1307
1404
|
|
|
1308
1405
|
/**
|
|
@@ -1620,6 +1717,12 @@ export class Http1Server {
|
|
|
1620
1717
|
this.fds[k] = fd
|
|
1621
1718
|
this.armed[k] = H1_WANT_READ
|
|
1622
1719
|
conn.restart()
|
|
1720
|
+
// Once a connection, not a request, so a keep-alive request costs no
|
|
1721
|
+
// syscall: a path whose MSS moves after the handshake is not followed.
|
|
1722
|
+
const mss: i32 = netMss(fd)
|
|
1723
|
+
if (mss > 0) {
|
|
1724
|
+
conn.socketMss = mss
|
|
1725
|
+
}
|
|
1623
1726
|
conn.lastActive = monotonicNanos()
|
|
1624
1727
|
return k
|
|
1625
1728
|
}
|
package/std/net/http1.ts
CHANGED
|
@@ -88,7 +88,12 @@ export const HTTP1_NEED_MORE: i32 = 0
|
|
|
88
88
|
export const HTTP1_HEAD: i32 = 1
|
|
89
89
|
/** `next()`: `bodyLen` body bytes are at `body[bodyOff ..]`, valid until the next `feed`. */
|
|
90
90
|
export const HTTP1_BODY: i32 = 2
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* `next()`: the request's body is complete. Its head (method, target,
|
|
93
|
+
* version, fields, `keepAlive`) still reads as this request's until the next
|
|
94
|
+
* call to `next()`, so a response written at `HTTP1_END` sees the request it
|
|
95
|
+
* answers and never the pipelined one behind it.
|
|
96
|
+
*/
|
|
92
97
|
export const HTTP1_END: i32 = 3
|
|
93
98
|
/** `next()`, from now on: the request switched protocols; see `upgradeBytes()`. */
|
|
94
99
|
export const HTTP1_UPGRADE: i32 = 4
|
|
@@ -144,7 +149,7 @@ export class Http1Parser {
|
|
|
144
149
|
// The fields are declared widest first, references before numbers before
|
|
145
150
|
// flags, so the struct carries no padding; the groups say what each is.
|
|
146
151
|
|
|
147
|
-
// --- the request: valid from its `HTTP1_HEAD` until the next
|
|
152
|
+
// --- the request: valid from its `HTTP1_HEAD` until the `next()` after its `HTTP1_END`
|
|
148
153
|
method: string = ""
|
|
149
154
|
target: string = ""
|
|
150
155
|
/** The field names, in lowercase, in the order they arrived; `values` beside them. */
|
|
@@ -218,6 +223,11 @@ export class Http1Parser {
|
|
|
218
223
|
keepAlive: boolean = false
|
|
219
224
|
/** Whether the request asked to switch protocols (`upgrade` is its value when `keepText`). */
|
|
220
225
|
upgrading: boolean = false
|
|
226
|
+
/**
|
|
227
|
+
* Whether a kept-alive request ended and its head is still held for the
|
|
228
|
+
* response: the next `next()` clears it before it reads a byte more.
|
|
229
|
+
*/
|
|
230
|
+
headHeld: boolean = false
|
|
221
231
|
/**
|
|
222
232
|
* Whether `method`, `target`, `names`, `values` and `upgrade` are filled as
|
|
223
233
|
* strings. A server that must allocate nothing per request turns it off and
|
|
@@ -361,7 +371,7 @@ export class Http1Parser {
|
|
|
361
371
|
* next feed and head make or the caller puts back first.
|
|
362
372
|
*/
|
|
363
373
|
resting(): boolean {
|
|
364
|
-
return this.state === HTTP1_S_REQUEST_LINE && this.end === this.start
|
|
374
|
+
return this.state === HTTP1_S_REQUEST_LINE && this.end === this.start && !this.headHeld
|
|
365
375
|
}
|
|
366
376
|
|
|
367
377
|
/** How many bytes were fed and not consumed yet: what a caller bounds its reads by. */
|
|
@@ -485,6 +495,7 @@ const http1StartRequest = (p: Http1Parser): void => {
|
|
|
485
495
|
p.bodyTotal = 0
|
|
486
496
|
p.sectionBytes = 0
|
|
487
497
|
p.fieldCount = 0
|
|
498
|
+
p.headHeld = false
|
|
488
499
|
}
|
|
489
500
|
|
|
490
501
|
/** `http1TakeLine`'s answer when a whole line is ready; never an event. */
|
|
@@ -1092,6 +1103,11 @@ const http1Next = (p: Http1Parser): i32 => {
|
|
|
1092
1103
|
while (true) {
|
|
1093
1104
|
switch (p.state) {
|
|
1094
1105
|
case HTTP1_S_REQUEST_LINE: {
|
|
1106
|
+
// The request that ended was answered while its head was held; the
|
|
1107
|
+
// pipelined bytes behind it are only read once it is cleared.
|
|
1108
|
+
if (p.headHeld) {
|
|
1109
|
+
http1StartRequest(p)
|
|
1110
|
+
}
|
|
1095
1111
|
// A method too long is a 400, decided before the line's length can
|
|
1096
1112
|
// call it a long target and answer 414.
|
|
1097
1113
|
if (http1MethodOverrun(p)) {
|
|
@@ -1162,7 +1178,11 @@ const http1Next = (p: Http1Parser): i32 => {
|
|
|
1162
1178
|
} else if (!p.keepAlive) {
|
|
1163
1179
|
p.state = HTTP1_S_CLOSED
|
|
1164
1180
|
} else {
|
|
1165
|
-
|
|
1181
|
+
// The head stays this request's until the next call, which clears
|
|
1182
|
+
// it before reading on: a response written at HTTP1_END reads its
|
|
1183
|
+
// own method, version and fields, and no copy is made.
|
|
1184
|
+
p.state = HTTP1_S_REQUEST_LINE
|
|
1185
|
+
p.headHeld = true
|
|
1166
1186
|
}
|
|
1167
1187
|
return HTTP1_END
|
|
1168
1188
|
}
|