@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.
Files changed (54) hide show
  1. package/INSTALL.md +16 -8
  2. package/bin/nish +0 -0
  3. package/docs/AI.md +118 -26
  4. package/package.json +1 -1
  5. package/runtime/core/host.d.ts +2 -1
  6. package/runtime/core/host.mjs +4 -0
  7. package/runtime/core.d.ts +47 -0
  8. package/runtime/core.mjs +67 -0
  9. package/runtime/nish.d.ts +176 -0
  10. package/runtime/nish.h +120 -6
  11. package/runtime/nish.mjs +24 -4
  12. package/runtime/runtime-checksum.c +270 -0
  13. package/runtime/runtime-net.c +386 -5
  14. package/runtime/runtime-os.c +59 -8
  15. package/runtime/runtime-parallel.c +81 -5
  16. package/runtime/runtime-sandbox.c +479 -0
  17. package/runtime/runtime-sha256.c +239 -0
  18. package/runtime/runtime-shm.c +541 -0
  19. package/runtime/runtime.c +827 -791
  20. package/scripts/build.sh +79 -28
  21. package/std/README.md +25 -13
  22. package/std/collections.ts +5 -5
  23. package/std/compress/checksum.ts +88 -75
  24. package/std/compress/deflate.ts +224 -0
  25. package/std/compress/gzip.ts +52 -0
  26. package/std/crypto/bcrypt.ts +816 -280
  27. package/std/crypto/hmac.ts +305 -2
  28. package/std/crypto/jwt.ts +1661 -0
  29. package/std/json-equal.ts +335 -0
  30. package/std/json-format.ts +1070 -0
  31. package/std/json-number.ts +428 -0
  32. package/std/json-pointer.ts +245 -0
  33. package/std/json-typed.ts +34 -0
  34. package/std/json-value.ts +510 -17
  35. package/std/json.ts +35 -6
  36. package/std/net/handler.ts +1348 -0
  37. package/std/net/http-client.ts +97 -16
  38. package/std/net/http-compress.ts +301 -16
  39. package/std/net/http1-client.ts +104 -19
  40. package/std/net/http1-server.ts +127 -24
  41. package/std/net/http1.ts +24 -4
  42. package/std/net/outbound.ts +409 -0
  43. package/std/net/postgres.ts +405 -0
  44. package/std/net/prefork.ts +344 -20
  45. package/std/net/websocket.ts +295 -14
  46. package/std/regex.ts +613 -52
  47. package/std/secret.ts +1 -1
  48. package/std/sync/ring.ts +587 -0
  49. package/std/sync/shm.ts +284 -0
  50. package/std/term-io.ts +35 -8
  51. package/std/term-rate.ts +13 -1
  52. package/std/threads.ts +35 -3
  53. package/std/yaml-json.ts +841 -0
  54. /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -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
  }
@@ -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
- /** The request parser: its head (`methodIs`, `targetIs`, `header`, spans) is the last `H1_REQUEST`'s. */
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, HTTP_OFFER_GZIP)
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` either way while
1244
- * `enc.vary` is set. The program's fields may not name `Content-Encoding`
1245
- * or, with `enc.vary`, `Vary`. The whole response is written or none of
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 packed: i32 = enc.compress(coding, body, off, len)
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
- const r: i32 = this.respond(status, enc.names, enc.values, n)
1282
- if (r !== 0) {
1283
- return r
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 = 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
- /** `next()`: the request's body is complete. */
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 request's -----
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
- http1StartRequest(p)
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
  }