@amritk/nish-x86_64-linux 0.29.0 → 0.31.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.
@@ -1,12 +1,12 @@
1
1
  /**
2
- * `nish/net/http1` — an incremental HTTP/1.1 request parser and a response
2
+ * `nish/net/http1-codec` — an incremental HTTP/1.1 request parser and a response
3
3
  * writer, sans-IO: bytes in, events and bytes out, and no socket anywhere.
4
4
  *
5
5
  * **The parser** is a push-pull machine. A caller `feed`s whatever slice the
6
6
  * socket gave it, of any length and cut anywhere, and then calls `next()`
7
7
  * until it answers `HTTP1_NEED_MORE`:
8
8
  *
9
- * import { Http1Parser, HTTP1_HEAD, HTTP1_BODY, HTTP1_END, HTTP1_ERROR } from "nish/net/http1";
9
+ * import { Http1Parser, HTTP1_HEAD, HTTP1_BODY, HTTP1_END, HTTP1_ERROR } from "nish/net/http1-codec";
10
10
  *
11
11
  * const p = new Http1Parser(maxTarget, maxHeaderBytes, maxHeaders, maxBody);
12
12
  * p.feed(received, 0, n);
@@ -81,6 +81,7 @@
81
81
  * Written from RFC 9112 and RFC 9110, not ported from another implementation.
82
82
  */
83
83
  import { copyBytes } from "nish/bytes"
84
+ import { extract, loadU8x16, u8x16 } from "nish:simd"
84
85
 
85
86
  /** `next()`: the input so far holds no further event; feed more. */
86
87
  export const HTTP1_NEED_MORE: i32 = 0
@@ -218,7 +219,13 @@ export class Http1Parser {
218
219
  /** The line `http1TakeLine` found is `buf[lineStart .. lineEnd)`, its CRLF dropped. */
219
220
  lineStart: i32 = 0
220
221
  lineEnd: i32 = 0
221
-
222
+ /**
223
+ * Whether the line being scanned holds a byte a field value may not: a
224
+ * control byte other than HTAB, or DEL (RFC 9110 §5.5). `http1TakeLine`
225
+ * sets it as it looks for the line's end, so a field line's bytes are read
226
+ * once, not once for the LF and again for the check.
227
+ */
228
+ lineControl: boolean = false
222
229
  chunked: boolean = false
223
230
  keepAlive: boolean = false
224
231
  /** Whether the request asked to switch protocols (`upgrade` is its value when `keepText`). */
@@ -498,6 +505,60 @@ const http1StartRequest = (p: Http1Parser): void => {
498
505
  p.headHeld = false
499
506
  }
500
507
 
508
+ /**
509
+ * Whether any of the eight bytes of `x` is below 32 or is 127: a control
510
+ * byte or DEL, which `http1TakeLine` stops at. Two of the bit tricks of
511
+ * "Bit Twiddling Hacks" (S. E. Anderson, public domain): a byte below `n`
512
+ * leaves its high bit set in `(x - n * ones) & ~x`, for `n` up to 128, and
513
+ * `127` is the byte that is zero in `x ^ 127 * ones`. A byte from 128 up
514
+ * has its high bit set in `x`, so `~x` clears it: obs-text never matches.
515
+ * The answer is exact for the word, though not for which byte. Each
516
+ * subtraction is masked to 64 bits because a `u64` is a BigInt under Node,
517
+ * which does not wrap; natively the mask is a no-op.
518
+ */
519
+ const http1WordHasControl = (x: u64): boolean => {
520
+ const ones32: u64 = toU64(16843009)
521
+ const ones: u64 = (ones32 << toU64(32)) | ones32
522
+ const low32: u64 = 0xffffffff
523
+ const all: u64 = (low32 << toU64(32)) | low32
524
+ const high: u64 = ones * toU64(128)
525
+ const below: u64 = (x - ones * toU64(32)) & all & ~x & high
526
+ const y: u64 = x ^ (ones * toU64(127))
527
+ const del: u64 = (y - ones) & all & ~y & high
528
+ return (below | del) !== toU64(0)
529
+ }
530
+
531
+ /**
532
+ * Lanes `at` to `at + 7` of `v`, the first the lowest byte: eight bytes of a
533
+ * sixteen-byte load as one word, which `opt` folds to a bitcast of the
534
+ * vector's half. One load is one bounds check for sixteen bytes, where eight
535
+ * reads of `buf[i + k]` would be eight.
536
+ */
537
+ const http1Lanes = (v: u8x16, at: i32): u64 => {
538
+ if (at === 0) {
539
+ return (
540
+ toU64(extract(v, 0)) |
541
+ (toU64(extract(v, 1)) << toU64(8)) |
542
+ (toU64(extract(v, 2)) << toU64(16)) |
543
+ (toU64(extract(v, 3)) << toU64(24)) |
544
+ (toU64(extract(v, 4)) << toU64(32)) |
545
+ (toU64(extract(v, 5)) << toU64(40)) |
546
+ (toU64(extract(v, 6)) << toU64(48)) |
547
+ (toU64(extract(v, 7)) << toU64(56))
548
+ )
549
+ }
550
+ return (
551
+ toU64(extract(v, 8)) |
552
+ (toU64(extract(v, 9)) << toU64(8)) |
553
+ (toU64(extract(v, 10)) << toU64(16)) |
554
+ (toU64(extract(v, 11)) << toU64(24)) |
555
+ (toU64(extract(v, 12)) << toU64(32)) |
556
+ (toU64(extract(v, 13)) << toU64(40)) |
557
+ (toU64(extract(v, 14)) << toU64(48)) |
558
+ (toU64(extract(v, 15)) << toU64(56))
559
+ )
560
+ }
561
+
501
562
  /** `http1TakeLine`'s answer when a whole line is ready; never an event. */
502
563
  const HTTP1_LINE: i32 = -1
503
564
 
@@ -505,14 +566,42 @@ const HTTP1_LINE: i32 = -1
505
566
  * Finds the next line: answers `HTTP1_LINE` with it in `lineStart ..
506
567
  * lineEnd`, `HTTP1_NEED_MORE` when no LF has arrived yet, or the refusal. A line ends in CRLF and in
507
568
  * nothing else; a LF without its CR is a 400. A line that has not ended by
508
- * `limit` bytes is refused with `overStatus`.
569
+ * `limit` bytes is refused with `overStatus`. On the way it sets
570
+ * `lineControl` when the line holds a byte no field value may, which
571
+ * `http1FieldLine` reads instead of scanning the value again.
509
572
  */
510
573
  const http1TakeLine = (p: Http1Parser, limit: i32, overStatus: i32): i32 => {
511
574
  const buf: u8[] = p.buf
512
575
  const end: i32 = p.end
513
- let i: i32 = p.scan > p.start ? p.scan : p.start
514
- while (i >= 0 && i < end && i < toI32(buf.length)) {
515
- if (buf[i] === 10) {
576
+ let i: i32 = p.start
577
+ if (p.scan > p.start) {
578
+ i = p.scan
579
+ } else {
580
+ p.lineControl = false
581
+ }
582
+ const stop: i32 = end < toI32(buf.length) ? end : toI32(buf.length)
583
+ if (i < 0) {
584
+ i = 0
585
+ }
586
+ while (i < stop) {
587
+ // Most bytes are printable: sixteen at a time while a whole vector is,
588
+ // then one at a time, with no test in either loop but the bytes' and the
589
+ // bound's.
590
+ while (i >= 0 && i <= stop - 16 && i <= toI32(buf.length) - 16) {
591
+ const v: u8x16 = loadU8x16(buf, i)
592
+ if (http1WordHasControl(http1Lanes(v, 0)) || http1WordHasControl(http1Lanes(v, 8))) {
593
+ break
594
+ }
595
+ i += 16
596
+ }
597
+ while (i >= 0 && i < stop && i < toI32(buf.length) && buf[i] >= 32 && buf[i] !== 127) {
598
+ i += 1
599
+ }
600
+ if (i < 0 || i >= stop || i >= toI32(buf.length)) {
601
+ break
602
+ }
603
+ const c: i32 = toI32(buf[i])
604
+ if (c === 10) {
516
605
  if (i === p.start || buf[i - 1] !== 13) {
517
606
  return http1Fail(p, 400, "a line ended in a bare LF")
518
607
  }
@@ -525,31 +614,49 @@ const http1TakeLine = (p: Http1Parser, limit: i32, overStatus: i32): i32 => {
525
614
  p.scan = i + 1
526
615
  return HTTP1_LINE
527
616
  }
617
+ if (c === 13 && i + 1 >= stop) {
618
+ // A CR at the end of the bytes so far: whether it ends the line is
619
+ // the next byte's to say, so the next call looks at it again.
620
+ break
621
+ }
622
+ if (c !== 9 && !(c === 13 && i + 1 < toI32(buf.length) && buf[i + 1] === 10)) {
623
+ p.lineControl = true
624
+ }
528
625
  i += 1
529
626
  }
530
- p.scan = end
627
+ p.scan = i
531
628
  if (end - p.start > limit + 1) {
532
629
  return http1Fail(p, overStatus, "a line over its limit")
533
630
  }
534
631
  return HTTP1_NEED_MORE
535
632
  }
536
633
 
634
+ /**
635
+ * The tchars of RFC 9110 §5.6.2 as three 32-bit masks, bit `c - 32`,
636
+ * `c - 64` and `c - 96` of each: `!` `#`-`'` `*` `+` `-` `.` and the digits
637
+ * in the first (0x03ff6cfa); the upper-case letters, `^` and `_` in the
638
+ * second (0xc7fffffe); `` ` ``, the lower-case letters, `|` and `~` in the
639
+ * third (0x57ffffff). No byte below 33 or above 126 is a tchar. One shift
640
+ * and mask replaces a chain of fourteen comparisons on the parser's hottest
641
+ * path (`bench/techempower`).
642
+ */
643
+ const HTTP1_TCHAR_32: i32 = 67071226
644
+ const HTTP1_TCHAR_64: i32 = -939524098
645
+ const HTTP1_TCHAR_96: i32 = 1476395007
646
+
537
647
  /** Whether `c` is a tchar of RFC 9110 §5.6.2, the bytes a token is made of. */
538
- export const http1IsTokenByte = (c: i32): boolean =>
539
- (c >= 97 && c <= 122) ||
540
- (c >= 65 && c <= 90) ||
541
- (c >= 48 && c <= 57) ||
542
- c === 33 ||
543
- (c >= 35 && c <= 39) ||
544
- c === 42 ||
545
- c === 43 ||
546
- c === 45 ||
547
- c === 46 ||
548
- c === 94 ||
549
- c === 95 ||
550
- c === 96 ||
551
- c === 124 ||
552
- c === 126
648
+ export const http1IsTokenByte = (c: i32): boolean => {
649
+ if (c < 32 || c > 127) {
650
+ return false
651
+ }
652
+ let mask: i32 = HTTP1_TCHAR_96
653
+ if (c < 64) {
654
+ mask = HTTP1_TCHAR_32
655
+ } else if (c < 96) {
656
+ mask = HTTP1_TCHAR_64
657
+ }
658
+ return ((mask >> (c & 31)) & 1) !== 0
659
+ }
553
660
 
554
661
  /** Whether `c` may appear in a field value (RFC 9110 §5.5): HTAB, SP, VCHAR or obs-text. */
555
662
  export const http1IsValueByte = (c: i32): boolean => c === 9 || (c >= 32 && c !== 127)
@@ -720,6 +827,57 @@ const http1Tally = (p: Http1Parser, name: string, word: string): Http1ListTally
720
827
  return tally
721
828
  }
722
829
 
830
+ /** `http1ConnectionOptions`: the Connection list names `close`. */
831
+ const HTTP1_CONNECTION_CLOSE: i32 = 1
832
+ /** `http1ConnectionOptions`: the Connection list names `keep-alive`. */
833
+ const HTTP1_CONNECTION_KEEP_ALIVE: i32 = 2
834
+ /** `http1ConnectionOptions`: the Connection list names `upgrade`. */
835
+ const HTTP1_CONNECTION_UPGRADE: i32 = 4
836
+
837
+ /**
838
+ * Which of `close`, `keep-alive` and `upgrade` the Connection fields name,
839
+ * in any case, as `HTTP1_CONNECTION_` bits: one walk of every Connection
840
+ * field's list, where three `http1Tally` walks would each read it all. An
841
+ * element is trimmed of OWS and compared whole, as `http1TallyRun` compares.
842
+ */
843
+ const http1ConnectionOptions = (p: Http1Parser): i32 => {
844
+ const spans: i32[] = p.spans
845
+ const head: u8[] = p.head
846
+ let found: i32 = 0
847
+ for (let i: i32 = 0; i < p.headerCount && 4 * i + 3 < toI32(spans.length); i += 1) {
848
+ if (!http1HeadIs(head, spans[4 * i], spans[4 * i + 1], "connection", false)) {
849
+ continue
850
+ }
851
+ const to: i32 = spans[4 * i + 3]
852
+ let start: i32 = spans[4 * i + 2]
853
+ for (let k: i32 = start; k <= to; k += 1) {
854
+ const c: i32 = k < to && k >= 0 && k < toI32(head.length) ? toI32(head[k]) : 44
855
+ if (c !== 44) {
856
+ continue
857
+ }
858
+ let a: i32 = start
859
+ let b: i32 = k
860
+ while (a >= 0 && a < b && a < toI32(head.length) && http1IsOws(toI32(head[a]))) {
861
+ a += 1
862
+ }
863
+ while (b > a && b - 1 >= 0 && b - 1 < toI32(head.length) && http1IsOws(toI32(head[b - 1]))) {
864
+ b -= 1
865
+ }
866
+ // The length picks the one word an element could be.
867
+ const n: i32 = b - a
868
+ if (n === 5 && http1HeadIs(head, a, b, "close", true)) {
869
+ found = found | HTTP1_CONNECTION_CLOSE
870
+ } else if (n === 10 && http1HeadIs(head, a, b, "keep-alive", true)) {
871
+ found = found | HTTP1_CONNECTION_KEEP_ALIVE
872
+ } else if (n === 7 && http1HeadIs(head, a, b, "upgrade", true)) {
873
+ found = found | HTTP1_CONNECTION_UPGRADE
874
+ }
875
+ start = k + 1
876
+ }
877
+ }
878
+ return found
879
+ }
880
+
723
881
  /**
724
882
  * Whether the buffered start of a request line holds more than
725
883
  * `HTTP1_MAX_METHOD` bytes before its first space (or line end).
@@ -828,10 +986,10 @@ const http1FieldLine = (p: Http1Parser, keep: boolean): i32 => {
828
986
  return http1Fail(p, 400, "a malformed field name")
829
987
  }
830
988
  const nameEnd: i32 = i
831
- for (let k: i32 = nameEnd + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
832
- if (!http1IsValueByte(toI32(buf[k]))) {
833
- return http1Fail(p, 400, "a control character in a field value")
834
- }
989
+ // The name is tchars and the colon is field text, so a byte no value may
990
+ // hold anywhere on the line is one in the value.
991
+ if (p.lineControl) {
992
+ return http1Fail(p, 400, "a control character in a field value")
835
993
  }
836
994
  p.fieldCount += 1
837
995
  if (p.fieldCount > p.maxHeaders) {
@@ -856,9 +1014,13 @@ const http1FieldLine = (p: Http1Parser, keep: boolean): i32 => {
856
1014
  }
857
1015
  const base: i32 = 4 * p.headerCount
858
1016
  http1SpanSet(p, base + 3, at + (valueEnd - a))
859
- http1SpanSet(p, base, at)
860
- http1SpanSet(p, base + 1, at + (nameEnd - a))
861
- http1SpanSet(p, base + 2, at + (valueStart - a))
1017
+ // The last of the four is set first, so the array has room for them all.
1018
+ const spans: i32[] = p.spans
1019
+ if (base >= 0 && base + 2 < toI32(spans.length)) {
1020
+ spans[base] = at
1021
+ spans[base + 1] = at + (nameEnd - a)
1022
+ spans[base + 2] = at + (valueStart - a)
1023
+ }
862
1024
  p.headerCount += 1
863
1025
  if (p.keepText) {
864
1026
  p.names.push(http1Text(buf, a, nameEnd, true))
@@ -938,12 +1100,12 @@ const http1EndOfHead = (p: Http1Parser): i32 => {
938
1100
  }
939
1101
  p.contentLength = size
940
1102
  }
941
- // Each tally is read before the next one reuses it, and none is walked
942
- // for a request without a Connection field, which is most of them.
943
- const listed: boolean = connectionFields > 0
944
- const close: boolean = listed && http1Tally(p, "connection", "close").matches > 0
945
- const keep: boolean = listed && http1Tally(p, "connection", "keep-alive").matches > 0
946
- const upgradeAsked: boolean = listed && http1Tally(p, "connection", "upgrade").matches > 0
1103
+ // One walk of the Connection list finds all three options, and none is
1104
+ // made for a request without a Connection field.
1105
+ const options: i32 = connectionFields > 0 ? http1ConnectionOptions(p) : 0
1106
+ const close: boolean = (options & HTTP1_CONNECTION_CLOSE) !== 0
1107
+ const keep: boolean = (options & HTTP1_CONNECTION_KEEP_ALIVE) !== 0
1108
+ const upgradeAsked: boolean = (options & HTTP1_CONNECTION_UPGRADE) !== 0
947
1109
  p.keepAlive = p.minor === 1 ? !close : keep && !close
948
1110
  // The fields of one name joined with ", " are empty only when there is one
949
1111
  // field and it is empty: the same test, without the join.
@@ -1405,9 +1567,18 @@ export const http1BytesAre = (bytes: u8[], word: string): boolean =>
1405
1567
 
1406
1568
  /** How many decimal digits `n`, which is not negative, takes. */
1407
1569
  const http1DecimalDigits = (n: i32): i32 => {
1408
- let digits: i32 = 1
1409
- let v: i32 = n
1410
- while (v >= 10) {
1570
+ if (n < 10) {
1571
+ return 1
1572
+ }
1573
+ if (n < 100) {
1574
+ return 2
1575
+ }
1576
+ if (n < 1000) {
1577
+ return 3
1578
+ }
1579
+ let digits: i32 = 4
1580
+ let v: i32 = n / 10000
1581
+ while (v > 0) {
1411
1582
  v = v / 10
1412
1583
  digits += 1
1413
1584
  }
@@ -1425,9 +1596,31 @@ export const http1PutText = (out: u8[], at: i32, text: string): i32 => {
1425
1596
  return at + n
1426
1597
  }
1427
1598
 
1428
- /** Writes `bytes` at `out[at ..)`, likewise. */
1599
+ /** Writes CR LF at `out[at ..)`, likewise: two stores, where `http1PutText` walks a string. */
1600
+ const http1PutCrlf = (out: u8[], at: i32): i32 => {
1601
+ if (at >= 0 && at < toI32(out.length) - 1) {
1602
+ out[at] = 13
1603
+ out[at + 1] = 10
1604
+ }
1605
+ return at + 2
1606
+ }
1607
+
1608
+ /** Writes `: ` at `out[at ..)`, likewise. */
1609
+ const http1PutColonSpace = (out: u8[], at: i32): i32 => {
1610
+ if (at >= 0 && at < toI32(out.length) - 1) {
1611
+ out[at] = 58
1612
+ out[at + 1] = 32
1613
+ }
1614
+ return at + 2
1615
+ }
1616
+
1617
+ /** Writes `bytes` at `out[at ..)`, likewise, as one copy when it fits. */
1429
1618
  const http1PutBytes = (out: u8[], at: i32, bytes: u8[]): i32 => {
1430
1619
  const n: i32 = toI32(bytes.length)
1620
+ if (at >= 0 && at <= toI32(out.length) - n) {
1621
+ copyBytes(out, at, bytes, 0, n)
1622
+ return at + n
1623
+ }
1431
1624
  for (let k: i32 = 0; k < n && at + k < toI32(out.length); k += 1) {
1432
1625
  if (at + k >= 0) {
1433
1626
  out[at + k] = bytes[k]
@@ -1525,32 +1718,42 @@ export const http1WriteResponseHead = (
1525
1718
  return HTTP1_NO_ROOM
1526
1719
  }
1527
1720
  let end: i32 = http1PutText(out, at, "HTTP/1.1 ")
1528
- end = http1PutDecimal(out, end, status)
1529
- end = http1PutText(out, end, " ")
1721
+ // A status is three digits, checked above: no division loop for them.
1722
+ if (end >= 0 && end < toI32(out.length) - 3) {
1723
+ const hundreds: i32 = status / 100
1724
+ const tens: i32 = status / 10
1725
+ const tensDigit: i32 = tens % 10
1726
+ const ones: i32 = status % 10
1727
+ out[end] = toU8(48 + hundreds)
1728
+ out[end + 1] = toU8(48 + tensDigit)
1729
+ out[end + 2] = toU8(48 + ones)
1730
+ out[end + 3] = 32
1731
+ }
1732
+ end = end + 4
1530
1733
  end = http1PutText(out, end, reason)
1531
- end = http1PutText(out, end, "\r\n")
1734
+ end = http1PutCrlf(out, end)
1532
1735
  for (let i: i32 = 0; i < count && i < toI32(values.length); i += 1) {
1533
1736
  end = http1PutBytes(out, end, names[i])
1534
- end = http1PutText(out, end, ": ")
1737
+ end = http1PutColonSpace(out, end)
1535
1738
  end = http1PutBytes(out, end, values[i])
1536
- end = http1PutText(out, end, "\r\n")
1739
+ end = http1PutCrlf(out, end)
1537
1740
  }
1538
1741
  if (sendDate) {
1539
1742
  end = http1PutText(out, end, "Date: ")
1540
1743
  end = http1PutBytes(out, end, date)
1541
- end = http1PutText(out, end, "\r\n")
1744
+ end = http1PutCrlf(out, end)
1542
1745
  }
1543
1746
  if (bodyLength >= 0) {
1544
1747
  end = http1PutText(out, end, "Content-Length: ")
1545
1748
  end = http1PutDecimal(out, end, bodyLength)
1546
- end = http1PutText(out, end, "\r\n")
1749
+ end = http1PutCrlf(out, end)
1547
1750
  } else if (bodyLength === HTTP1_CHUNKED) {
1548
1751
  end = http1PutText(out, end, "Transfer-Encoding: chunked\r\n")
1549
1752
  }
1550
1753
  if (toI32(connection.length) > 0) {
1551
1754
  end = http1PutText(out, end, "Connection: ")
1552
1755
  end = http1PutText(out, end, connection)
1553
- end = http1PutText(out, end, "\r\n")
1756
+ end = http1PutCrlf(out, end)
1554
1757
  }
1555
- return http1PutText(out, end, "\r\n")
1758
+ return http1PutCrlf(out, end)
1556
1759
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `nish/net/http1-server` — the server side of HTTP/1.1 (RFC 9112): a
3
- * sans-IO connection that drives `nish/net/http1`'s parser, and two pools of
3
+ * sans-IO connection that drives `nish/net/http1-codec`'s parser, and two pools of
4
4
  * them for a program that owns its readiness loop, one on `nish:net`'s plain
5
5
  * TCP and one on `nish/net/tls-tcp` with ALPN `http/1.1`.
6
6
  *
@@ -70,14 +70,18 @@
70
70
  * most once a second; a 1xx carries none. The pool's slots share one, so a
71
71
  * busy server formats a second once, not once a slot. A program that sets its
72
72
  * own `date` field sends that one instead, so a response has one `Date`.
73
- * `date.pin(unixMs)` fixes it, for a test that compares bytes.
73
+ * `Http1Server` reads the clock once per serve call — from `readable` or
74
+ * `writable` to the `flush` that ends it — and every response written in
75
+ * between carries that reading; one written outside a serve call, an answer
76
+ * given later, reads the clock then. `date.pin(unixMs)` fixes it, for a test
77
+ * that compares bytes.
74
78
  *
75
- * **Refusals.** Every refusal of `nish/net/http1`'s parser — the smuggling
79
+ * **Refusals.** Every refusal of `nish/net/http1-codec`'s parser — the smuggling
76
80
  * shapes, a `Content-Length` with a `Transfer-Encoding`, two that disagree,
77
81
  * a malformed chunk size — is answered with its status (400, 413, 414, 431,
78
82
  * 501, 505), `Content-Length: 0` and `Connection: close`, and ends the
79
83
  * connection; so does a malformed WebSocket frame, with its close code.
80
- * `docs/security/http1.md` is the record.
84
+ * `docs/security/http1-codec.md` is the record.
81
85
  *
82
86
  * **WebSocket.** A request that asked to upgrade reaches the program like any
83
87
  * other; `acceptWebSocket(protocol)` answers it with the 101 of
@@ -141,7 +145,7 @@ import {
141
145
  http1WriteChunk,
142
146
  http1WriteLastChunk,
143
147
  http1WriteResponseHead,
144
- } from "nish/net/http1"
148
+ } from "nish/net/http1-codec"
145
149
  import {
146
150
  WS_CLOSE,
147
151
  WS_ERROR,
@@ -275,7 +279,8 @@ const http1ServerCopy = (out: u8[], at: i32, src: u8[], off: i32, n: i32): i32 =
275
279
  * it calls, `httpDateWrite`, stays in `nish/net/http-fields`, which compiles
276
280
  * for wasm32 (the split `nish/crypto/x509` and `x509-random` make). `pin`
277
281
  * replaces the clock with one instant, which is how a test asserts a response
278
- * byte for byte.
282
+ * byte for byte. `hold` reads the clock once for a run of responses, a serve
283
+ * call's, until `unhold`; `pin` wins over it.
279
284
  */
280
285
  export class HttpDate {
281
286
  /** The IMF-fixdate of `second`: what `current` answers, made once. */
@@ -284,9 +289,14 @@ export class HttpDate {
284
289
  second: i64 = -1
285
290
  /** The instant `pin` fixed, in milliseconds since 1970, while `pinned`. */
286
291
  pinnedMs: i64 = 0
292
+ /** The clock reading `hold` took, in milliseconds since 1970, while `held`. */
293
+ heldMs: i64 = 0
287
294
  /** How many times `bytes` has been rewritten. */
288
295
  rewrites: i64 = 0
296
+ /** How many times the wall clock has been read. */
297
+ reads: i64 = 0
289
298
  pinned: boolean = false
299
+ held: boolean = false
290
300
 
291
301
  /** A date on the wall clock, with nothing formatted yet. */
292
302
  constructor() {
@@ -301,7 +311,10 @@ export class HttpDate {
301
311
  * or past 9999 answers the nearer of the two ends.
302
312
  */
303
313
  current(): u8[] {
304
- const ms: i64 = this.pinned ? this.pinnedMs : toI64(Date.now())
314
+ let ms: i64 = this.pinnedMs
315
+ if (!this.pinned) {
316
+ ms = this.held ? this.heldMs : this.now()
317
+ }
305
318
  let second: i64 = ms / 1000
306
319
  if (ms < 0) {
307
320
  second = 0
@@ -335,6 +348,34 @@ export class HttpDate {
335
348
  this.pinned = false
336
349
  }
337
350
 
351
+ /**
352
+ * Reads the wall clock once and makes `current` answer that reading until
353
+ * `unhold`, so the responses of one serve call cost one read. A pinned date
354
+ * reads nothing. A second `hold` takes a fresh reading.
355
+ */
356
+ hold(): void {
357
+ if (!this.pinned) {
358
+ this.heldMs = this.now()
359
+ }
360
+ this.held = true
361
+ }
362
+
363
+ /** Puts `current` back on a clock read per call, for a response written outside a serve call. */
364
+ unhold(): void {
365
+ this.held = false
366
+ }
367
+
368
+ /** The wall clock, in milliseconds since 1970. */
369
+ now(): i64 {
370
+ this.reads += 1
371
+ return toI64(Date.now())
372
+ }
373
+
374
+ /** How many times the wall clock has been read: once per `hold`, and once per `current` outside one. */
375
+ clockReads(): i64 {
376
+ return this.reads
377
+ }
378
+
338
379
  /** How many times the bytes have been rewritten: one per second that `current` was asked for, at most. */
339
380
  formats(): i64 {
340
381
  return this.rewrites
@@ -1573,7 +1614,11 @@ export class Http1Server {
1573
1614
  * accepted into, and slot 0's once `connection` has asked for it.
1574
1615
  */
1575
1616
  connections: Http1Connection[]
1576
- /** The `Date` every slot's responses carry, formatted once a second for all of them. `date.pin` fixes its clock. */
1617
+ /**
1618
+ * The `Date` every slot's responses carry, formatted once a second for all
1619
+ * of them and read from the clock once per serve call: `readable` and
1620
+ * `writable` hold a reading and `flush` lets it go. `date.pin` fixes its clock.
1621
+ */
1577
1622
  date: HttpDate
1578
1623
  /**
1579
1624
  * Each slot's descriptor, or -1 while it is free, for the slots accepted
@@ -1729,12 +1774,15 @@ export class Http1Server {
1729
1774
 
1730
1775
  /**
1731
1776
  * The socket of `slot` is readable: reads it while the connection has room
1732
- * and the socket has more, then sends. Answers the interest.
1777
+ * and the socket has more, then sends. Answers the interest. It begins a
1778
+ * serve call, which the program's `flush` of the slot ends: the responses
1779
+ * written in between carry one clock reading as their `Date`.
1733
1780
  */
1734
1781
  readable(slot: i32): i32 {
1735
1782
  if (!this.holds(slot)) {
1736
1783
  return H1_DONE
1737
1784
  }
1785
+ this.date.hold()
1738
1786
  const conn: Http1Connection = this.connections[slot]
1739
1787
  const fd: i32 = this.fds[slot]
1740
1788
  let moved: boolean = false
@@ -1768,12 +1816,16 @@ export class Http1Server {
1768
1816
  conn.lastActive = now
1769
1817
  conn.headArrived(now)
1770
1818
  }
1771
- return this.flush(slot)
1819
+ return this.settle(slot)
1772
1820
  }
1773
1821
 
1774
- /** The socket of `slot` is writable. Answers the interest. */
1822
+ /** The socket of `slot` is writable: sends, and begins a serve call as `readable` does. Answers the interest. */
1775
1823
  writable(slot: i32): i32 {
1776
- return this.flush(slot)
1824
+ if (!this.holds(slot)) {
1825
+ return H1_DONE
1826
+ }
1827
+ this.date.hold()
1828
+ return this.settle(slot)
1777
1829
  }
1778
1830
 
1779
1831
  /**
@@ -1797,9 +1849,16 @@ export class Http1Server {
1797
1849
  /**
1798
1850
  * Sends what `slot`'s connection holds until the socket would block, and
1799
1851
  * answers the interest: `H1_DONE` once the connection is over and sent,
1800
- * otherwise read while it has room and write while it holds bytes.
1852
+ * otherwise read while it has room and write while it holds bytes. It ends
1853
+ * the serve call, so a response written after it reads the clock anew.
1801
1854
  */
1802
1855
  flush(slot: i32): i32 {
1856
+ this.date.unhold()
1857
+ return this.settle(slot)
1858
+ }
1859
+
1860
+ /** `flush` within a serve call: sends, answers the interest, and keeps the clock reading. */
1861
+ settle(slot: i32): i32 {
1803
1862
  if (!this.holds(slot)) {
1804
1863
  return H1_DONE
1805
1864
  }
@@ -7,13 +7,13 @@
7
7
  * reaches services that trust the program's address.
8
8
  *
9
9
  * import { OutboundPolicy } from "nish/net/outbound";
10
- * import { HttpOptions } from "nish/net/http-client";
10
+ * import { HttpOptions } from "nish/net/fetch";
11
11
  *
12
12
  * const options = new HttpOptions();
13
13
  * options.outbound = new OutboundPolicy();
14
14
  *
15
15
  * **The check is on the address, after the lookup.** A name is never judged
16
- * by its text: `nish/net/http-client` resolves it, drops every address
16
+ * by its text: `nish/net/fetch` resolves it, drops every address
17
17
  * `outboundAddressAllowed` refuses, and connects only to the bytes it kept,
18
18
  * with no second lookup, so a name whose answer changes between a check and
19
19
  * a connect (DNS rebinding) cannot move the connection. Every redirect hop is