@amritk/nish-x86_64-linux 0.27.2 → 0.28.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 +13 -8
- package/bin/nish +0 -0
- package/docs/AI.md +19 -2
- package/package.json +1 -1
- package/runtime/core.d.ts +47 -0
- package/runtime/core.mjs +67 -0
- package/runtime/nish.d.ts +49 -0
- package/runtime/nish.h +50 -5
- package/runtime/nish.mjs +6 -0
- package/runtime/runtime-checksum.c +270 -0
- package/runtime/runtime-parallel.c +21 -0
- package/runtime/runtime-sandbox.c +470 -0
- package/runtime/runtime-sha256.c +239 -0
- package/scripts/build.sh +44 -23
- package/std/README.md +15 -10
- 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/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-value.ts +510 -17
- package/std/net/http-compress.ts +259 -16
- package/std/net/http1-client.ts +104 -19
- package/std/net/http1-server.ts +61 -12
- package/std/net/postgres.ts +405 -0
- package/std/net/websocket.ts +295 -14
- package/std/regex.ts +613 -52
- package/std/term-io.ts +35 -8
- package/std/yaml-json.ts +841 -0
package/std/net/http-compress.ts
CHANGED
|
@@ -23,6 +23,30 @@
|
|
|
23
23
|
* its `minLength`, longer than its `maxLength`, or that gzip would not make
|
|
24
24
|
* smaller, answering -1 so the program sends the bytes as they are.
|
|
25
25
|
*
|
|
26
|
+
* **Compression that pays for itself.** A body is sent compressed only when
|
|
27
|
+
* it saves at least `minSaving` bytes and `minSavingPercent` percent of the
|
|
28
|
+
* body; otherwise the compressed bytes are dropped and the body goes as it
|
|
29
|
+
* is. With `segmentMode` set, `respondEncoded` also sends it compressed only
|
|
30
|
+
* when the whole response, head included, takes fewer TCP segments of `mss`
|
|
31
|
+
* bytes (the connection's `mss` if set, else the compressor's, 1448 by
|
|
32
|
+
* default): a 1300-byte body that gzips to 700 still fits in one segment
|
|
33
|
+
* either way. After each call `decision` says why (`HTTP_COMPRESS_SENT`,
|
|
34
|
+
* `_NOT_ACCEPTED`, `_TOO_SMALL`, `_TOO_LARGE`, `_TOO_LITTLE_SAVED`,
|
|
35
|
+
* `_NO_FEWER_SEGMENTS`), for a program to count.
|
|
36
|
+
*
|
|
37
|
+
* **Two encoders.** Bodies up to `fixedLimit` (16 KB) go through
|
|
38
|
+
* `GzipFixedCompressor`: one fixed-Huffman block, a 16 KB window, no state
|
|
39
|
+
* between calls, two to four times faster than `DEFLATE_FAST` on 0.5 to
|
|
40
|
+
* 16 KB of JSON for output about 1.35 times larger. Larger bodies go through
|
|
41
|
+
* the `GzipCompressor` at the level given; `encoder` forces either one.
|
|
42
|
+
*
|
|
43
|
+
* **`Vary: Accept-Encoding`** (with `vary`, on by default) goes on every
|
|
44
|
+
* response whose coding depended on the request: one sent compressed, and
|
|
45
|
+
* one sent as it is because the request did not accept gzip. A body that is
|
|
46
|
+
* too small, too large or did not pay is sent the same to every client, so
|
|
47
|
+
* it carries no `Vary`. A request that refuses identity (`identity;q=0`)
|
|
48
|
+
* and accepts gzip is sent gzip whatever the policy (`compressAnyway`).
|
|
49
|
+
*
|
|
26
50
|
* **Negotiation** (`httpAcceptEncoding`) follows §12.5.3: each listed coding
|
|
27
51
|
* with its q-value, `x-gzip` as `gzip`, `*` for every coding not listed, and
|
|
28
52
|
* `identity` acceptable unless it is given q=0, directly or through `*`. Of
|
|
@@ -35,15 +59,16 @@
|
|
|
35
59
|
* and identity is refused: a server may answer 406 or send identity anyway.
|
|
36
60
|
* A malformed element is skipped. The parser allocates nothing.
|
|
37
61
|
*
|
|
38
|
-
* **Compressing allocates nothing
|
|
39
|
-
* `
|
|
62
|
+
* **Compressing allocates nothing** and caches nothing between responses:
|
|
63
|
+
* `HttpCompressor` holds a `GzipCompressor`, a `GzipFixedCompressor`, an
|
|
64
|
+
* output buffer of `compressBound(maxLength)` bytes and
|
|
40
65
|
* the field arrays `addFields` fills, all built once.
|
|
41
66
|
* `HTTP_CODING_DEFLATE` (zlib framing) is negotiated but not produced: a
|
|
42
67
|
* client that accepts only `deflate` is sent identity, which every client
|
|
43
68
|
* reads.
|
|
44
69
|
*/
|
|
45
70
|
import { DEFLATE_FAST, compressBound } from "nish/compress/deflate"
|
|
46
|
-
import { GzipCompressor } from "nish/compress/gzip"
|
|
71
|
+
import { GzipCompressor, GzipFixedCompressor } from "nish/compress/gzip"
|
|
47
72
|
|
|
48
73
|
/** No content coding: the body as it is. */
|
|
49
74
|
export const HTTP_CODING_IDENTITY: i32 = 0
|
|
@@ -61,6 +86,62 @@ export const HTTP_CODING_NONE: i32 = -1
|
|
|
61
86
|
export const HTTP_OFFER_GZIP: i32 = 1
|
|
62
87
|
export const HTTP_OFFER_DEFLATE: i32 = 2
|
|
63
88
|
|
|
89
|
+
/**
|
|
90
|
+
* What `HttpCompressor.compress` (and `respondEncoded`'s segment check) last
|
|
91
|
+
* decided, in `HttpCompressor.decision`, for a program to count: the body
|
|
92
|
+
* went out compressed; the client did not take gzip; it was shorter than
|
|
93
|
+
* `minLength`; longer than `maxLength`; gzip did not save `minSaving` bytes
|
|
94
|
+
* and `minSavingPercent` percent; or the compressed response would not have
|
|
95
|
+
* taken fewer TCP segments (`segmentMode`).
|
|
96
|
+
*/
|
|
97
|
+
export const HTTP_COMPRESS_SENT: i32 = 0
|
|
98
|
+
export const HTTP_COMPRESS_NOT_ACCEPTED: i32 = 1
|
|
99
|
+
export const HTTP_COMPRESS_TOO_SMALL: i32 = 2
|
|
100
|
+
export const HTTP_COMPRESS_TOO_LARGE: i32 = 3
|
|
101
|
+
export const HTTP_COMPRESS_TOO_LITTLE_SAVED: i32 = 4
|
|
102
|
+
export const HTTP_COMPRESS_NO_FEWER_SEGMENTS: i32 = 5
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `HttpCompressor.encoder`: by the body's length (the fixed-Huffman encoder up
|
|
106
|
+
* to `fixedLimit` bytes, the constructor's level above), the fixed-Huffman
|
|
107
|
+
* encoder always, or the constructor's level always.
|
|
108
|
+
*/
|
|
109
|
+
export const HTTP_ENCODER_AUTO: i32 = 0
|
|
110
|
+
export const HTTP_ENCODER_FIXED: i32 = 1
|
|
111
|
+
export const HTTP_ENCODER_LEVEL: i32 = 2
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The maximum segment size `segmentMode` counts with unless told the
|
|
115
|
+
* connection's: a 1500-byte Ethernet MTU less 20 bytes of IPv4 header, 20 of
|
|
116
|
+
* TCP header and 12 of the timestamp option.
|
|
117
|
+
*/
|
|
118
|
+
export const HTTP_DEFAULT_MSS: i32 = 1448
|
|
119
|
+
|
|
120
|
+
/** The bytes `content-encoding: gzip` adds to an HTTP/1.1 head: name, value, ": " and CRLF. */
|
|
121
|
+
export const HTTP1_GZIP_FIELD_BYTES: i32 = 24
|
|
122
|
+
|
|
123
|
+
/** The number of TCP segments `bytes` bytes take at `mss` bytes a segment. */
|
|
124
|
+
export const httpSegments = (bytes: i32, mss: i32): i32 => {
|
|
125
|
+
if (mss <= 0) {
|
|
126
|
+
panic("httpSegments: a segment size of zero or less")
|
|
127
|
+
}
|
|
128
|
+
if (bytes <= 0) {
|
|
129
|
+
return 0
|
|
130
|
+
}
|
|
131
|
+
return (bytes - 1) / mss + 1
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The digits of `n` in decimal, `n` 0 or more. */
|
|
135
|
+
const httpDecimalDigits = (n: i32): i32 => {
|
|
136
|
+
let digits: i32 = 1
|
|
137
|
+
let rest: i32 = n
|
|
138
|
+
while (rest >= 10) {
|
|
139
|
+
rest = rest / 10
|
|
140
|
+
digits += 1
|
|
141
|
+
}
|
|
142
|
+
return digits
|
|
143
|
+
}
|
|
144
|
+
|
|
64
145
|
/** A q-value of 1, in thousandths. */
|
|
65
146
|
const HTTP_Q_ONE: i32 = 1000
|
|
66
147
|
|
|
@@ -255,6 +336,15 @@ export const httpAcceptEncoding = (buf: u8[], start: i32, end: i32, offered: i32
|
|
|
255
336
|
return HTTP_CODING_NONE
|
|
256
337
|
}
|
|
257
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Whether the `Accept-Encoding` value in `buf[start .. end)` refuses the
|
|
341
|
+
* body as it is: `identity;q=0`, or `*;q=0` with no `identity` listed. A
|
|
342
|
+
* server should then send a coding the client takes even where it would not
|
|
343
|
+
* bother (`HttpCompressor.compressAnyway`).
|
|
344
|
+
*/
|
|
345
|
+
export const httpIdentityRefused = (buf: u8[], start: i32, end: i32): boolean =>
|
|
346
|
+
httpAcceptEncoding(buf, start, end, 0) === HTTP_CODING_NONE
|
|
347
|
+
|
|
258
348
|
/** The bytes of an ASCII string. */
|
|
259
349
|
const httpCompressBytes = (text: string): u8[] => {
|
|
260
350
|
const out: u8[] = new Array<u8>(toI32(text.length))
|
|
@@ -270,6 +360,8 @@ const httpCompressBytes = (text: string): u8[] => {
|
|
|
270
360
|
*/
|
|
271
361
|
export class HttpCompressor {
|
|
272
362
|
readonly gzip: GzipCompressor
|
|
363
|
+
/** The small-body encoder: one fixed-Huffman block, nothing kept between calls. */
|
|
364
|
+
readonly fixed: GzipFixedCompressor
|
|
273
365
|
/** Where `compress` writes; valid until the next call. */
|
|
274
366
|
readonly output: u8[]
|
|
275
367
|
/** The fields `addFields` built: the program's, then `Content-Encoding` and `Vary`. */
|
|
@@ -288,6 +380,27 @@ export class HttpCompressor {
|
|
|
288
380
|
* default; turn it off when the program sets its own `Vary`.
|
|
289
381
|
*/
|
|
290
382
|
vary: boolean
|
|
383
|
+
/**
|
|
384
|
+
* The least a compressed body must save to be sent: `minSaving` bytes and
|
|
385
|
+
* `minSavingPercent` percent of the body, whichever is more. 1 and 0 by
|
|
386
|
+
* default (any saving at all); a value below those counts as them.
|
|
387
|
+
*/
|
|
388
|
+
minSaving: i32
|
|
389
|
+
minSavingPercent: i32
|
|
390
|
+
/**
|
|
391
|
+
* Whether `respondEncoded` also sends the compressed response only when,
|
|
392
|
+
* head included, it takes fewer TCP segments of `mss` bytes than the body
|
|
393
|
+
* as it is; off by default. `mss` is `HTTP_DEFAULT_MSS` unless the program
|
|
394
|
+
* sets it, or a connection's own `mss` overrides it.
|
|
395
|
+
*/
|
|
396
|
+
segmentMode: boolean
|
|
397
|
+
mss: i32
|
|
398
|
+
/** `HTTP_ENCODER_AUTO` (the default), `HTTP_ENCODER_FIXED` or `HTTP_ENCODER_LEVEL`. */
|
|
399
|
+
encoder: i32
|
|
400
|
+
/** The longest body `HTTP_ENCODER_AUTO` gives the fixed-Huffman encoder: 16384 by default. */
|
|
401
|
+
fixedLimit: i32
|
|
402
|
+
/** The last decision, an `HTTP_COMPRESS_*`; `HTTP_COMPRESS_NOT_ACCEPTED` before any. */
|
|
403
|
+
decision: i32
|
|
291
404
|
|
|
292
405
|
/**
|
|
293
406
|
* A compressor at `level` (`DEFLATE_FAST` suits a server) for bodies of
|
|
@@ -299,6 +412,7 @@ export class HttpCompressor {
|
|
|
299
412
|
panic("HttpCompressor: the lengths are out of order")
|
|
300
413
|
}
|
|
301
414
|
this.gzip = new GzipCompressor(level)
|
|
415
|
+
this.fixed = new GzipFixedCompressor()
|
|
302
416
|
this.output = new Array<u8>(compressBound(maxLength))
|
|
303
417
|
this.names = []
|
|
304
418
|
this.values = []
|
|
@@ -309,28 +423,151 @@ export class HttpCompressor {
|
|
|
309
423
|
this.minLength = minLength
|
|
310
424
|
this.maxLength = maxLength
|
|
311
425
|
this.vary = true
|
|
426
|
+
this.minSaving = 1
|
|
427
|
+
this.minSavingPercent = 0
|
|
428
|
+
this.segmentMode = false
|
|
429
|
+
this.mss = HTTP_DEFAULT_MSS
|
|
430
|
+
this.encoder = HTTP_ENCODER_AUTO
|
|
431
|
+
this.fixedLimit = 16384
|
|
432
|
+
this.decision = HTTP_COMPRESS_NOT_ACCEPTED
|
|
312
433
|
}
|
|
313
434
|
|
|
314
435
|
/**
|
|
315
|
-
* Compresses `body[off .. off + len)` into `output` when
|
|
316
|
-
* `
|
|
317
|
-
*
|
|
318
|
-
* as it is
|
|
436
|
+
* Compresses `body[off .. off + len)` into `output` when the length is in
|
|
437
|
+
* `[minLength, maxLength]`, `coding` is `HTTP_CODING_GZIP` and gzip saves
|
|
438
|
+
* what `minSaving` and `minSavingPercent` ask; answers the compressed
|
|
439
|
+
* length, or -1 to send the body as it is, and sets `decision` either way.
|
|
440
|
+
* `encoder` picks the encoder; the fixed-Huffman one stops as soon as its
|
|
441
|
+
* output passes what would be kept. A window outside `body` panics.
|
|
319
442
|
*/
|
|
320
443
|
compress(coding: i32, body: u8[], off: i32, len: i32): i32 {
|
|
321
444
|
if (off < 0 || len < 0 || off > toI32(body.length) - len) {
|
|
322
445
|
panic("HttpCompressor.compress: the window lies outside its body")
|
|
323
446
|
}
|
|
324
|
-
if (
|
|
447
|
+
if (len < this.minLength) {
|
|
448
|
+
this.decision = HTTP_COMPRESS_TOO_SMALL
|
|
449
|
+
return -1
|
|
450
|
+
}
|
|
451
|
+
if (len > this.maxLength) {
|
|
452
|
+
this.decision = HTTP_COMPRESS_TOO_LARGE
|
|
453
|
+
return -1
|
|
454
|
+
}
|
|
455
|
+
if (coding !== HTTP_CODING_GZIP) {
|
|
456
|
+
this.decision = HTTP_COMPRESS_NOT_ACCEPTED
|
|
457
|
+
return -1
|
|
458
|
+
}
|
|
459
|
+
// The longest output kept; `output` holds `compressBound(maxLength)`, more.
|
|
460
|
+
const keep: i32 = len - this.leastSaving(len)
|
|
461
|
+
let n: i32 = -1
|
|
462
|
+
if (keep > 0) {
|
|
463
|
+
const fixed: boolean =
|
|
464
|
+
this.encoder === HTTP_ENCODER_FIXED || (this.encoder !== HTTP_ENCODER_LEVEL && len <= this.fixedLimit)
|
|
465
|
+
n = fixed
|
|
466
|
+
? this.fixed.compress(body, off, len, this.output, toI32(0), keep)
|
|
467
|
+
: this.gzip.compress(body, off, len, this.output, toI32(0))
|
|
468
|
+
}
|
|
469
|
+
if (n < 0 || n > keep) {
|
|
470
|
+
this.decision = HTTP_COMPRESS_TOO_LITTLE_SAVED
|
|
471
|
+
return -1
|
|
472
|
+
}
|
|
473
|
+
this.decision = HTTP_COMPRESS_SENT
|
|
474
|
+
return n
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* `compress` for a client that refuses identity (`httpIdentityRefused`):
|
|
479
|
+
* gzip whatever the body's length, the saving or the
|
|
480
|
+
* segments, so long as it is no longer than `maxLength`. Answers the
|
|
481
|
+
* compressed length, or -1 when `coding` is not gzip or the body is too
|
|
482
|
+
* long (sent as it is, which RFC 9110 §12.5.3 allows).
|
|
483
|
+
*/
|
|
484
|
+
compressAnyway(coding: i32, body: u8[], off: i32, len: i32): i32 {
|
|
485
|
+
if (off < 0 || len < 0 || off > toI32(body.length) - len) {
|
|
486
|
+
panic("HttpCompressor.compressAnyway: the window lies outside its body")
|
|
487
|
+
}
|
|
488
|
+
if (len > this.maxLength) {
|
|
489
|
+
this.decision = HTTP_COMPRESS_TOO_LARGE
|
|
325
490
|
return -1
|
|
326
491
|
}
|
|
327
|
-
|
|
328
|
-
|
|
492
|
+
if (coding !== HTTP_CODING_GZIP) {
|
|
493
|
+
this.decision = HTTP_COMPRESS_NOT_ACCEPTED
|
|
329
494
|
return -1
|
|
330
495
|
}
|
|
496
|
+
let n: i32 = -1
|
|
497
|
+
if (
|
|
498
|
+
this.encoder === HTTP_ENCODER_FIXED ||
|
|
499
|
+
(this.encoder !== HTTP_ENCODER_LEVEL && len <= this.fixedLimit)
|
|
500
|
+
) {
|
|
501
|
+
n = this.fixed.compress(body, off, len, this.output, toI32(0), toI32(this.output.length))
|
|
502
|
+
}
|
|
503
|
+
if (n < 0) {
|
|
504
|
+
n = this.gzip.compress(body, off, len, this.output, toI32(0))
|
|
505
|
+
}
|
|
506
|
+
this.decision = HTTP_COMPRESS_SENT
|
|
331
507
|
return n
|
|
332
508
|
}
|
|
333
509
|
|
|
510
|
+
/** The bytes a compressed `len`-byte body must save: `minSaving` and `minSavingPercent`, at least 1. */
|
|
511
|
+
leastSaving(len: i32): i32 {
|
|
512
|
+
let least: i32 = this.minSaving > 1 ? this.minSaving : 1
|
|
513
|
+
let pct: i32 = this.minSavingPercent
|
|
514
|
+
if (pct < 0) {
|
|
515
|
+
pct = 0
|
|
516
|
+
} else if (pct > 100) {
|
|
517
|
+
pct = 100
|
|
518
|
+
}
|
|
519
|
+
const share: i32 = toI32((toI64(len) * toI64(pct) + toI64(99)) / toI64(100))
|
|
520
|
+
if (share > least) {
|
|
521
|
+
least = share
|
|
522
|
+
}
|
|
523
|
+
return least
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* `segmentMode`'s rule, for a response of `identityBytes` bytes sent as it
|
|
528
|
+
* is or `encodedBytes` compressed, heads included, at `mss` bytes a segment
|
|
529
|
+
* (`this.mss` when 0 or less): whether the compressed one takes fewer
|
|
530
|
+
* segments. When it does not, `decision` becomes
|
|
531
|
+
* `HTTP_COMPRESS_NO_FEWER_SEGMENTS`. `respondEncoded` calls it; a program
|
|
532
|
+
* driving HTTP/2 or HTTP/3 by hand may with its own head sizes.
|
|
533
|
+
*/
|
|
534
|
+
fewerSegments(identityBytes: i32, encodedBytes: i32, mss: i32): boolean {
|
|
535
|
+
let size: i32 = mss
|
|
536
|
+
if (size <= 0) {
|
|
537
|
+
size = this.mss > 0 ? this.mss : HTTP_DEFAULT_MSS
|
|
538
|
+
}
|
|
539
|
+
if (httpSegments(encodedBytes, size) < httpSegments(identityBytes, size)) {
|
|
540
|
+
return true
|
|
541
|
+
}
|
|
542
|
+
this.decision = HTTP_COMPRESS_NO_FEWER_SEGMENTS
|
|
543
|
+
return false
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* The bytes of the HTTP/1.1 head a `length`-byte body sent as it is would
|
|
548
|
+
* get, from `encodedHead`, that of the same response sent compressed as
|
|
549
|
+
* `encodedLength` bytes with the fields `addFields` built: less
|
|
550
|
+
* `Content-Encoding: gzip`, less `Vary` (a body the segment count kept as
|
|
551
|
+
* it is would go out so to any client), and the other `Content-Length`.
|
|
552
|
+
*/
|
|
553
|
+
http1IdentityHead(encodedHead: i32, encodedLength: i32, length: i32): i32 {
|
|
554
|
+
let head: i32 = encodedHead - HTTP1_GZIP_FIELD_BYTES
|
|
555
|
+
if (this.vary) {
|
|
556
|
+
head -= toI32(this.varyName.length) + toI32(this.acceptEncodingValue.length) + 4
|
|
557
|
+
}
|
|
558
|
+
return head - httpDecimalDigits(encodedLength) + httpDecimalDigits(length)
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Whether the last decision depended on the request's `Accept-Encoding`, so
|
|
563
|
+
* the response needs `Vary: Accept-Encoding`: a body sent compressed, or
|
|
564
|
+
* one sent as it is because the client did not take gzip. A body the
|
|
565
|
+
* lengths or the saving kept as it is would go out the same to any client.
|
|
566
|
+
*/
|
|
567
|
+
varies(): boolean {
|
|
568
|
+
return this.decision === HTTP_COMPRESS_SENT || this.decision === HTTP_COMPRESS_NOT_ACCEPTED
|
|
569
|
+
}
|
|
570
|
+
|
|
334
571
|
/**
|
|
335
572
|
* Sets `names` / `values` to the program's fields followed by
|
|
336
573
|
* `Content-Encoding: gzip` and, when `vary`, `Vary: Accept-Encoding`. The
|
|
@@ -354,21 +591,27 @@ export class HttpCompressor {
|
|
|
354
591
|
}
|
|
355
592
|
this.names.push(this.contentEncodingName)
|
|
356
593
|
this.values.push(this.gzipValue)
|
|
357
|
-
this.
|
|
594
|
+
if (this.vary) {
|
|
595
|
+
this.names.push(this.varyName)
|
|
596
|
+
this.values.push(this.acceptEncodingValue)
|
|
597
|
+
}
|
|
358
598
|
}
|
|
359
599
|
|
|
360
|
-
/**
|
|
600
|
+
/**
|
|
601
|
+
* Appends `Vary: Accept-Encoding` to the arrays `identityFields` built when
|
|
602
|
+
* `vary` is set and the last decision `varies`.
|
|
603
|
+
*/
|
|
361
604
|
addVary(): void {
|
|
362
|
-
if (this.vary) {
|
|
605
|
+
if (this.vary && this.varies()) {
|
|
363
606
|
this.names.push(this.varyName)
|
|
364
607
|
this.values.push(this.acceptEncodingValue)
|
|
365
608
|
}
|
|
366
609
|
}
|
|
367
610
|
|
|
368
611
|
/**
|
|
369
|
-
* Sets `names` / `values` to the program's fields and, when `vary
|
|
370
|
-
* `Vary: Accept-Encoding`: the fields of a body
|
|
371
|
-
* resource that is sometimes compressed.
|
|
612
|
+
* Sets `names` / `values` to the program's fields and, when `vary` and the
|
|
613
|
+
* last decision `varies`, `Vary: Accept-Encoding`: the fields of a body
|
|
614
|
+
* sent as it is from a resource that is sometimes compressed.
|
|
372
615
|
*/
|
|
373
616
|
identityFields(names: u8[][], values: u8[][]): void {
|
|
374
617
|
while (this.names.length > 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
|
}
|