@amritk/nish-x86_64-linux 0.28.0 → 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.
@@ -83,6 +83,7 @@ import { Url, urlDecode, urlDefaultPort, urlOrigin, urlParse, urlResolve } from
83
83
  import { resolveHost } from "nish/net/dns"
84
84
  import { DNS_ADDRESS_BYTES } from "nish/net/dns-message"
85
85
  import { DNS_RESOLVE_MAX_ADDRESSES, DNS_RESOLVE_TIMEOUT } from "nish/net/dns-resolver"
86
+ import { OutboundPolicy, outboundAddressText, outboundFilter, outboundHostAllowed } from "nish/net/outbound"
86
87
  import {
87
88
  connectResult,
88
89
  netClose,
@@ -136,6 +137,8 @@ export const HTTP_E_REQUEST: i32 = 14
136
137
  export const HTTP_E_IO: i32 = 15
137
138
  /** A proxy setting that is not an `http://` URL with a host. */
138
139
  export const HTTP_E_PROXY: i32 = 16
140
+ /** The outbound policy refused the URL's host or every address it resolved to. */
141
+ export const HTTP_E_BLOCKED: i32 = 17
139
142
 
140
143
  /** A typed zero: a bare literal is an `f64` under `--number-mode f64`. */
141
144
  const HTTP_CLIENT_ZERO: i32 = 0
@@ -185,6 +188,7 @@ export const httpErrorName = (code: i32): string => {
185
188
  "request",
186
189
  "io",
187
190
  "proxy",
191
+ "blocked",
188
192
  ]
189
193
  return code >= 0 && code < toI32(names.length) ? names[code] : "unknown"
190
194
  }
@@ -230,6 +234,12 @@ export class HttpOptions {
230
234
  maxBodyBytes: i64 = 268435456
231
235
  /** A PEM file of the roots an https server's chain must end at; null for the system's. */
232
236
  rootsPemPath: string | null = null
237
+ /**
238
+ * For a URL the program did not choose: only the addresses and hosts the
239
+ * policy allows are reached, no proxy is used, and its caps apply (see
240
+ * `nish/net/outbound`). Null for none.
241
+ */
242
+ outbound: OutboundPolicy | null = null
233
243
  /** The budget for the lookup and the connect together; 0 or less for none but the total. */
234
244
  connectTimeoutMs: i32 = 10000
235
245
  /** The longest wait for the server to send or take a byte; 0 or less for none. */
@@ -371,6 +381,8 @@ export class HttpClient {
371
381
  noProxy: string = ""
372
382
  /** Where `fd` leads: an origin, or the proxy an http request went through. */
373
383
  route: string = ""
384
+ /** The outbound policy `fd` was dialled under; a connection is reused only under the same one. */
385
+ routePolicy: OutboundPolicy | null = null
374
386
  parser: Http1ResponseParser
375
387
  /** The TLS connection over `fd` to an https origin; null over plain http. */
376
388
  tls: TlsRecordClient | null = null
@@ -442,13 +454,18 @@ export class HttpClient {
442
454
  this.drop()
443
455
  }
444
456
  this.generation += 1
445
- const total: i32 = this.options.totalTimeoutMs
457
+ const total: i32 = httpClientTotalMs(this.options)
446
458
  this.deadline = total > 0 ? monotonicNanos() + toI64(total) * HTTP_CLIENT_NS_PER_MS : HTTP_CLIENT_NEVER
447
459
  let url: Url | null = urlParse(req.url)
448
460
  if (url === null || (url.scheme !== "http" && url.scheme !== "https")) {
449
461
  this.fail(HTTP_E_URL, `not an absolute http or https URL: ${httpClientRedact(url)}`)
450
462
  return null
451
463
  }
464
+ const refusal: string = this.hostRefusal(url)
465
+ if (refusal !== "") {
466
+ this.fail(HTTP_E_BLOCKED, refusal)
467
+ return null
468
+ }
452
469
  let method: string = req.method
453
470
  let body: u8[] | null = req.body
454
471
  const names: string[] = []
@@ -468,10 +485,11 @@ export class HttpClient {
468
485
  return res
469
486
  }
470
487
  const next: Url | null = urlResolve(url, location)
471
- if (hops >= this.options.maxRedirects) {
488
+ const maxRedirects: i32 = httpClientMaxRedirects(this.options)
489
+ if (hops >= maxRedirects) {
472
490
  return this.refuse(
473
491
  HTTP_E_TOO_MANY_REDIRECTS,
474
- `more than ${this.options.maxRedirects} redirects, the last to ${httpClientRedact(next)}`
492
+ `more than ${maxRedirects} redirects, the last to ${httpClientRedact(next)}`
475
493
  )
476
494
  }
477
495
  if (next === null || (next.scheme !== "http" && next.scheme !== "https")) {
@@ -486,6 +504,10 @@ export class HttpClient {
486
504
  `a redirect from https to http (${httpClientRedact(next)})`
487
505
  )
488
506
  }
507
+ const hopRefusal: string = this.hostRefusal(next)
508
+ if (hopRefusal !== "") {
509
+ return this.refuse(HTTP_E_BLOCKED, hopRefusal)
510
+ }
489
511
  const from: string | null = urlOrigin(url)
490
512
  const to: string | null = urlOrigin(next)
491
513
  if (from === null || to === null || from !== to) {
@@ -517,6 +539,15 @@ export class HttpClient {
517
539
  return null
518
540
  }
519
541
 
542
+ /** Why the outbound policy refuses to let a URL name `url`'s host; "" when there is no policy or it allows the host. */
543
+ hostRefusal(url: Url): string {
544
+ const policy: OutboundPolicy | null = this.options.outbound
545
+ if (policy === null || outboundHostAllowed(policy, url.host)) {
546
+ return ""
547
+ }
548
+ return `${httpClientRedact(url)} names a host the outbound policy does not allow`
549
+ }
550
+
520
551
  /** Closes the connection; the client can still fetch, on a new one. */
521
552
  close(): void {
522
553
  this.drop()
@@ -564,14 +595,11 @@ export class HttpClient {
564
595
  */
565
596
  open(url: Url): boolean {
566
597
  const port: i32 = url.port >= 0 ? url.port : urlDefaultPort(url.scheme)
567
- const proxyText: string = httpProxyFor(
568
- url.scheme,
569
- url.host,
570
- port,
571
- this.httpProxy,
572
- this.httpsProxy,
573
- this.noProxy
574
- )
598
+ // A proxy would look the name up itself, past the policy's address check.
599
+ const proxyText: string =
600
+ this.options.outbound !== null
601
+ ? ""
602
+ : httpProxyFor(url.scheme, url.host, port, this.httpProxy, this.httpsProxy, this.noProxy)
575
603
  let proxy: Url | null = null
576
604
  if (proxyText !== "") {
577
605
  proxy = httpClientProxyUrl(proxyText)
@@ -592,7 +620,12 @@ export class HttpClient {
592
620
  } else if (proxy !== null) {
593
621
  route = `${origin} via ${httpClientHostText(proxy.host)}:${proxy.port}`
594
622
  }
595
- if (this.fd >= 0 && this.route === route && this.quiet()) {
623
+ if (
624
+ this.fd >= 0 &&
625
+ this.route === route &&
626
+ httpClientSamePolicy(this.routePolicy, this.options.outbound) &&
627
+ this.quiet()
628
+ ) {
596
629
  this.reused = true
597
630
  return true
598
631
  }
@@ -610,6 +643,7 @@ export class HttpClient {
610
643
  }
611
644
  this.fd = fd
612
645
  this.route = route
646
+ this.routePolicy = this.options.outbound
613
647
  this.parser = httpClientParser(this.options)
614
648
  if (url.scheme === "https") {
615
649
  let early: u8[] = []
@@ -863,7 +897,7 @@ export class HttpClient {
863
897
  if (code === HTTP1_E_BODY_LIMIT) {
864
898
  this.fail(
865
899
  HTTP_E_BODY_TOO_LARGE,
866
- `the body is larger than maxBodyBytes (${this.options.maxBodyBytes})`
900
+ `the body is larger than maxBodyBytes (${httpClientBodyCap(this.options)})`
867
901
  )
868
902
  } else {
869
903
  // A response the server closed part way through is a connection lost,
@@ -1080,9 +1114,22 @@ export class HttpClient {
1080
1114
  )
1081
1115
  return -1
1082
1116
  }
1117
+ let usable: i32 = count < DNS_RESOLVE_MAX_ADDRESSES ? count : DNS_RESOLVE_MAX_ADDRESSES
1118
+ const policy: OutboundPolicy | null = this.options.outbound
1119
+ if (policy !== null) {
1120
+ // Only the bytes checked here are connected to: no second lookup a rebinding answer could change.
1121
+ usable = outboundFilter(policy, addresses, usable)
1122
+ if (usable === 0) {
1123
+ this.fail(
1124
+ HTTP_E_BLOCKED,
1125
+ `${host} resolves to ${outboundAddressText(addresses, 0)}, which the outbound policy does not allow`
1126
+ )
1127
+ return -1
1128
+ }
1129
+ }
1083
1130
  const one: u8[] = new Array<u8>(DNS_ADDRESS_BYTES)
1084
1131
  let last: i32 = 0
1085
- for (let i: i32 = 0; i < count && i < DNS_RESOLVE_MAX_ADDRESSES; i += 1) {
1132
+ for (let i: i32 = 0; i < usable && i < DNS_RESOLVE_MAX_ADDRESSES; i += 1) {
1086
1133
  copyBytes(one, 0, addresses, i * DNS_ADDRESS_BYTES, DNS_ADDRESS_BYTES)
1087
1134
  const fd: i32 = tcpConnect(one)
1088
1135
  if (fd < 0) {
@@ -1320,7 +1367,7 @@ export class HttpClient {
1320
1367
  /** Sets the error for a wait that ended with `code`, `text` saying what timed out. */
1321
1368
  failWait(code: i32, text: string): void {
1322
1369
  if (code === HTTP_E_TOTAL_TIMEOUT) {
1323
- this.fail(code, `the fetch ran past ${this.options.totalTimeoutMs} ms`)
1370
+ this.fail(code, `the fetch ran past ${httpClientTotalMs(this.options)} ms`)
1324
1371
  } else if (code === HTTP_E_IO) {
1325
1372
  this.fail(code, "the poll loop failed")
1326
1373
  } else {
@@ -1468,10 +1515,44 @@ const httpClientParser = (options: HttpOptions): Http1ResponseParser =>
1468
1515
  options.maxHeaderBytes,
1469
1516
  options.maxHeaderBytes,
1470
1517
  HTTP_CLIENT_MAX_HEADERS,
1471
- options.maxBodyBytes,
1518
+ httpClientBodyCap(options),
1472
1519
  options.maxHeaderBytes
1473
1520
  )
1474
1521
 
1522
+ /** Whether `a` and `b` are the same outbound policy, or both none. */
1523
+ const httpClientSamePolicy = (a: OutboundPolicy | null, b: OutboundPolicy | null): boolean => {
1524
+ if (a === null || b === null) {
1525
+ return a === null && b === null
1526
+ }
1527
+ return a === b
1528
+ }
1529
+
1530
+ /** `maxBodyBytes`, or the outbound policy's cap when it is lower. */
1531
+ const httpClientBodyCap = (options: HttpOptions): i64 => {
1532
+ const policy: OutboundPolicy | null = options.outbound
1533
+ return policy !== null && policy.maxBodyBytes < options.maxBodyBytes
1534
+ ? policy.maxBodyBytes
1535
+ : options.maxBodyBytes
1536
+ }
1537
+
1538
+ /** `totalTimeoutMs`, or the outbound policy's budget when it is set and the options' is not, or is longer. */
1539
+ const httpClientTotalMs = (options: HttpOptions): i32 => {
1540
+ const policy: OutboundPolicy | null = options.outbound
1541
+ const own: i32 = options.totalTimeoutMs
1542
+ if (policy === null || policy.totalTimeoutMs <= 0) {
1543
+ return own
1544
+ }
1545
+ return own > 0 && own < policy.totalTimeoutMs ? own : policy.totalTimeoutMs
1546
+ }
1547
+
1548
+ /** `maxRedirects`, or the outbound policy's limit when it is lower. */
1549
+ const httpClientMaxRedirects = (options: HttpOptions): i32 => {
1550
+ const policy: OutboundPolicy | null = options.outbound
1551
+ return policy !== null && policy.maxRedirects < options.maxRedirects
1552
+ ? policy.maxRedirects
1553
+ : options.maxRedirects
1554
+ }
1555
+
1475
1556
  /** The statuses a redirect is followed for: 301, 302, 303, 307 and 308. */
1476
1557
  const httpClientIsRedirect = (status: i32): boolean =>
1477
1558
  status === 301 || status === 302 || status === 303 || status === 307 || status === 308
@@ -32,7 +32,9 @@
32
32
  * default): a 1300-byte body that gzips to 700 still fits in one segment
33
33
  * either way. After each call `decision` says why (`HTTP_COMPRESS_SENT`,
34
34
  * `_NOT_ACCEPTED`, `_TOO_SMALL`, `_TOO_LARGE`, `_TOO_LITTLE_SAVED`,
35
- * `_NO_FEWER_SEGMENTS`), for a program to count.
35
+ * `_NO_FEWER_SEGMENTS`), for a program to count. A response whose identity
36
+ * form already fits one segment is `_NO_FEWER_SEGMENTS` without running gzip
37
+ * at all, as no compression could take fewer.
36
38
  *
37
39
  * **Two encoders.** Bodies up to `fixedLimit` (16 KB) go through
38
40
  * `GzipFixedCompressor`: one fixed-Huffman block, a 16 KB window, no state
@@ -69,6 +71,7 @@
69
71
  */
70
72
  import { DEFLATE_FAST, compressBound } from "nish/compress/deflate"
71
73
  import { GzipCompressor, GzipFixedCompressor } from "nish/compress/gzip"
74
+ import { wrappingAdd } from "nish:unsafe"
72
75
 
73
76
  /** No content coding: the body as it is. */
74
77
  export const HTTP_CODING_IDENTITY: i32 = 0
@@ -92,7 +95,8 @@ export const HTTP_OFFER_DEFLATE: i32 = 2
92
95
  * went out compressed; the client did not take gzip; it was shorter than
93
96
  * `minLength`; longer than `maxLength`; gzip did not save `minSaving` bytes
94
97
  * and `minSavingPercent` percent; or the compressed response would not have
95
- * taken fewer TCP segments (`segmentMode`).
98
+ * taken fewer TCP segments (`segmentMode`), which includes an identity
99
+ * response that already fits one segment, decided without compressing.
96
100
  */
97
101
  export const HTTP_COMPRESS_SENT: i32 = 0
98
102
  export const HTTP_COMPRESS_NOT_ACCEPTED: i32 = 1
@@ -374,12 +378,6 @@ export class HttpCompressor {
374
378
  /** The shortest body worth compressing, and the longest the output can hold. */
375
379
  readonly minLength: i32
376
380
  readonly maxLength: i32
377
- /**
378
- * Whether `addFields` adds `Vary: Accept-Encoding` (RFC 9110 §12.5.5), which
379
- * a cache needs whenever the same resource may be sent either way. On by
380
- * default; turn it off when the program sets its own `Vary`.
381
- */
382
- vary: boolean
383
381
  /**
384
382
  * The least a compressed body must save to be sent: `minSaving` bytes and
385
383
  * `minSavingPercent` percent of the body, whichever is more. 1 and 0 by
@@ -387,6 +385,12 @@ export class HttpCompressor {
387
385
  */
388
386
  minSaving: i32
389
387
  minSavingPercent: i32
388
+ /**
389
+ * Whether `addFields` adds `Vary: Accept-Encoding` (RFC 9110 §12.5.5), which
390
+ * a cache needs whenever the same resource may be sent either way. On by
391
+ * default; turn it off when the program sets its own `Vary`.
392
+ */
393
+ vary: boolean
390
394
  /**
391
395
  * Whether `respondEncoded` also sends the compressed response only when,
392
396
  * head included, it takes fewer TCP segments of `mss` bytes than the body
@@ -401,6 +405,13 @@ export class HttpCompressor {
401
405
  fixedLimit: i32
402
406
  /** The last decision, an `HTTP_COMPRESS_*`; `HTTP_COMPRESS_NOT_ACCEPTED` before any. */
403
407
  decision: i32
408
+ /**
409
+ * How many times `compress` and `compressAnyway` ran an encoder, from 0: a
410
+ * count for a test or a profile to read, never consulted by a decision. It
411
+ * wraps past 2^31 - 1 to -2^31 rather than panic, as a server can run that
412
+ * many; read it as a difference between two reads.
413
+ */
414
+ encoderRuns: i32
404
415
 
405
416
  /**
406
417
  * A compressor at `level` (`DEFLATE_FAST` suits a server) for bodies of
@@ -430,6 +441,7 @@ export class HttpCompressor {
430
441
  this.encoder = HTTP_ENCODER_AUTO
431
442
  this.fixedLimit = 16384
432
443
  this.decision = HTTP_COMPRESS_NOT_ACCEPTED
444
+ this.encoderRuns = 0
433
445
  }
434
446
 
435
447
  /**
@@ -462,6 +474,7 @@ export class HttpCompressor {
462
474
  if (keep > 0) {
463
475
  const fixed: boolean =
464
476
  this.encoder === HTTP_ENCODER_FIXED || (this.encoder !== HTTP_ENCODER_LEVEL && len <= this.fixedLimit)
477
+ this.encoderRuns = wrappingAdd(this.encoderRuns, 1)
465
478
  n = fixed
466
479
  ? this.fixed.compress(body, off, len, this.output, toI32(0), keep)
467
480
  : this.gzip.compress(body, off, len, this.output, toI32(0))
@@ -493,6 +506,7 @@ export class HttpCompressor {
493
506
  this.decision = HTTP_COMPRESS_NOT_ACCEPTED
494
507
  return -1
495
508
  }
509
+ this.encoderRuns = wrappingAdd(this.encoderRuns, 1)
496
510
  let n: i32 = -1
497
511
  if (
498
512
  this.encoder === HTTP_ENCODER_FIXED ||
@@ -532,10 +546,7 @@ export class HttpCompressor {
532
546
  * driving HTTP/2 or HTTP/3 by hand may with its own head sizes.
533
547
  */
534
548
  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
- }
549
+ const size: i32 = this.segmentSize(mss)
539
550
  if (httpSegments(encodedBytes, size) < httpSegments(identityBytes, size)) {
540
551
  return true
541
552
  }
@@ -543,6 +554,37 @@ export class HttpCompressor {
543
554
  return false
544
555
  }
545
556
 
557
+ /** The segment `segmentMode` counts in: `mss` when above 0, else `this.mss` when above 0, else `HTTP_DEFAULT_MSS`. */
558
+ segmentSize(mss: i32): i32 {
559
+ if (mss > 0) {
560
+ return mss
561
+ }
562
+ return this.mss > 0 ? this.mss : HTTP_DEFAULT_MSS
563
+ }
564
+
565
+ /**
566
+ * `segmentMode`'s shortcut, before any encoder runs: whether a `len`-byte
567
+ * body that `compress` would try (its length in `[minLength, maxLength]`
568
+ * and `coding` gzip) is no longer than one segment of `mss` bytes (as
569
+ * `fewerSegments` reads it). Sent as it is, such a response takes one
570
+ * segment once its head fits beside it, and no compression takes fewer, so
571
+ * `decision` becomes `HTTP_COMPRESS_NO_FEWER_SEGMENTS` and the caller checks
572
+ * the head: when it does not fit, the caller goes on to `compress`, which
573
+ * decides again. Anything else answers false and leaves `decision` alone.
574
+ */
575
+ oneSegment(coding: i32, len: i32, mss: i32): boolean {
576
+ if (
577
+ coding !== HTTP_CODING_GZIP ||
578
+ len < this.minLength ||
579
+ len > this.maxLength ||
580
+ len > this.segmentSize(mss)
581
+ ) {
582
+ return false
583
+ }
584
+ this.decision = HTTP_COMPRESS_NO_FEWER_SEGMENTS
585
+ return true
586
+ }
587
+
546
588
  /**
547
589
  * The bytes of the HTTP/1.1 head a `length`-byte body sent as it is would
548
590
  * get, from `encodedHead`, that of the same response sent compressed as
@@ -118,11 +118,12 @@ import {
118
118
  HTTP_CODING_GZIP,
119
119
  HTTP_CODING_IDENTITY,
120
120
  HTTP_CODING_NONE,
121
+ HTTP_DEFAULT_MSS,
121
122
  HTTP_OFFER_GZIP,
122
123
  HttpCompressor,
123
124
  httpAcceptEncoding,
124
125
  } from "nish/net/http-compress"
125
- import { netClose, netRead, netWrite, tcpAccept } from "nish:net"
126
+ import { netClose, netMss, netRead, netWrite, tcpAccept } from "nish:net"
126
127
  import {
127
128
  HTTP1_BODY,
128
129
  HTTP1_CHUNKED,
@@ -493,7 +494,11 @@ export class Http1Connection {
493
494
  * put another, to share it or to pin its clock.
494
495
  */
495
496
  date: HttpDate
496
- /** 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
+ */
497
502
  parser: Http1Parser
498
503
  /**
499
504
  * The frame decoder: made when the slot first switches to WebSocket, so a
@@ -562,10 +567,14 @@ export class Http1Connection {
562
567
  /** A head has begun to arrive and is not read whole yet: `headSince` is its deadline's start. */
563
568
  headTimed: boolean = false
564
569
  /**
565
- * This connection's TCP maximum segment size, for `respondEncoded` under
566
- * `HttpCompressor.segmentMode`; 0, the default, uses the compressor's `mss`.
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.
567
574
  */
568
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
569
578
 
570
579
  /** A connection under `config`. A cap outside what the connection can work with panics. */
571
580
  constructor(config: Http1Config) {
@@ -649,6 +658,8 @@ export class Http1Connection {
649
658
  this.inputEnded = false
650
659
  this.headTimed = false
651
660
  this.headSince = 0
661
+ this.mss = 0
662
+ this.socketMss = 0
652
663
  this.release()
653
664
  this.park()
654
665
  }
@@ -1225,7 +1236,8 @@ export class Http1Connection {
1225
1236
  * `Accept-Encoding` with gzip offered (`httpAcceptEncoding`):
1226
1237
  * `HTTP_CODING_GZIP`, `HTTP_CODING_IDENTITY` (also with no such field), or
1227
1238
  * `HTTP_CODING_NONE` when the client refuses identity and does not take
1228
- * 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.
1229
1241
  */
1230
1242
  acceptEncoding(): i32 {
1231
1243
  return this.acceptEncodingOf(HTTP_OFFER_GZIP)
@@ -1255,6 +1267,22 @@ export class Http1Connection {
1255
1267
  return httpAcceptEncoding(parser.head, start, end, offered)
1256
1268
  }
1257
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
1284
+ }
1285
+
1258
1286
  /**
1259
1287
  * Answers the current request with all of `body[off .. off + len)` in one
1260
1288
  * call: `respond`, `write` and `end`, the body compressed by `enc` when
@@ -1263,10 +1291,12 @@ export class Http1Connection {
1263
1291
  * otherwise sent as it is; `Vary: Accept-Encoding` while `enc.vary` is set
1264
1292
  * and the choice turned on the request (`HttpCompressor.varies`). Under
1265
1293
  * `enc.segmentMode` the compressed response goes out only when, head
1266
- * included, it takes fewer segments of `mss` bytes (this connection's, or
1267
- * `enc.mss`) than the body as it is; `enc.decision` says what was chosen.
1268
- * A request that refuses identity (`identityRefused`) is sent gzip whatever
1269
- * the length, the saving or the segments (`enc.compressAnyway`).
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`).
1270
1300
  * The program's fields may not name `Content-Encoding` or, with
1271
1301
  * `enc.vary`, `Vary`. The whole response is written or none of
1272
1302
  * it: `H1_AGAIN` when the output has no room for it yet (`H1_WRITE` says
@@ -1290,16 +1320,6 @@ export class Http1Connection {
1290
1320
  return H1_CLOSED
1291
1321
  }
1292
1322
  const required: boolean = coding === HTTP_CODING_GZIP && this.identityRefused()
1293
- let packed: i32 = required
1294
- ? enc.compressAnyway(coding, body, off, len)
1295
- : enc.compress(coding, body, off, len)
1296
- let n: i32 = len
1297
- if (packed >= 0) {
1298
- enc.addFields(names, values)
1299
- n = packed
1300
- } else {
1301
- enc.identityFields(names, values)
1302
- }
1303
1323
  // `respond` and the body together, or neither: what `respond` sets is
1304
1324
  // put back when the body does not fit behind the head. `room` first, so
1305
1325
  // that what was sent is already moved out of the way and the `outputEnd`
@@ -1307,15 +1327,43 @@ export class Http1Connection {
1307
1327
  this.room()
1308
1328
  const outputEnd: i32 = this.outputEnd
1309
1329
  const closeAfter: boolean = this.closeAfter
1310
- let r: i32 = this.respond(status, enc.names, enc.values, n)
1311
- if (r !== 0) {
1312
- 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
+ }
1313
1361
  }
1314
1362
  if (packed >= 0 && enc.segmentMode && !required) {
1315
1363
  // The head just written, and the one the body as it is would get.
1316
1364
  const head: i32 = this.outputEnd - outputEnd
1317
1365
  const plain: i32 = enc.http1IdentityHead(head, packed, len)
1318
- if (!enc.fewerSegments(plain + len, head + packed, this.mss)) {
1366
+ if (!enc.fewerSegments(plain + len, head + packed, mss)) {
1319
1367
  this.unrespond(outputEnd, closeAfter)
1320
1368
  enc.identityFields(names, values)
1321
1369
  packed = -1
@@ -1669,6 +1717,12 @@ export class Http1Server {
1669
1717
  this.fds[k] = fd
1670
1718
  this.armed[k] = H1_WANT_READ
1671
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
+ }
1672
1726
  conn.lastActive = monotonicNanos()
1673
1727
  return k
1674
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
  }