@amritk/nish-x86_64-linux 0.26.0 → 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.
@@ -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**: `HttpCompressor` holds a
39
- * `GzipCompressor`, an output buffer of `compressBound(maxLength)` bytes and
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 `coding` is
316
- * `HTTP_CODING_GZIP`, the length is in `[minLength, maxLength]` and gzip
317
- * makes it smaller; answers the compressed length, or -1 to send the body
318
- * as it is. A window outside `body` panics.
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 (coding !== HTTP_CODING_GZIP || len < this.minLength || len > this.maxLength) {
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
- const n: i32 = this.gzip.compress(body, off, len, this.output, toI32(0))
328
- if (n < 0 || n >= len) {
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.addVary()
594
+ if (this.vary) {
595
+ this.names.push(this.varyName)
596
+ this.values.push(this.acceptEncodingValue)
597
+ }
358
598
  }
359
599
 
360
- /** Appends `Vary: Accept-Encoding` to the arrays `addFields` built when `vary` is set. */
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 sent as it is from a
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) {
@@ -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 and the
38
- * connection is a tunnel now: `tunnelBytes()` answers what was fed past the
39
- * head), `HTTP1_CLOSED` (the connection is done: the server closed it
40
- * between responses, or the last response was not kept alive) and
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
- * (this client never asks to switch protocols), `Transfer-Encoding` together
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, which this client never asks for. */
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 (http1SameWord(name, 0, nameLength, "connection") && http1ClientListHas(value, "upgrade")) {
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 the parser refuses the
368
- * answer to; and a `bodyLength` below -2. Answers `HTTP1_NO_ROOM` when it is
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
- const need: i64 = http1ClientRequestNeed(method, target, names, values, bodyLength)
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
- return http1ClientFail(p, HTTP1_E_SWITCH, "a 101 Switching Protocols this client never asks for")
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
  }