@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.
- package/INSTALL.md +8 -5
- package/bin/nish +0 -0
- package/docs/AI.md +99 -24
- package/package.json +1 -1
- package/runtime/core/host.d.ts +2 -1
- package/runtime/core/host.mjs +4 -0
- package/runtime/nish.d.ts +127 -0
- package/runtime/nish.h +72 -3
- package/runtime/nish.mjs +18 -4
- package/runtime/runtime-net.c +386 -5
- package/runtime/runtime-os.c +59 -8
- package/runtime/runtime-parallel.c +60 -5
- package/runtime/runtime-sandbox.c +43 -34
- package/runtime/runtime-shm.c +541 -0
- package/runtime/runtime.c +827 -791
- package/scripts/build.sh +42 -12
- package/std/README.md +10 -3
- package/std/collections.ts +5 -5
- package/std/crypto/hmac.ts +305 -2
- package/std/crypto/jwt.ts +1661 -0
- package/std/json-typed.ts +34 -0
- package/std/json.ts +35 -6
- package/std/net/handler.ts +1348 -0
- package/std/net/http-client.ts +97 -16
- package/std/net/http-compress.ts +54 -12
- package/std/net/http1-server.ts +77 -23
- package/std/net/http1.ts +24 -4
- package/std/net/outbound.ts +409 -0
- package/std/net/prefork.ts +344 -20
- package/std/secret.ts +1 -1
- package/std/sync/ring.ts +587 -0
- package/std/sync/shm.ts +284 -0
- package/std/term-rate.ts +13 -1
- package/std/threads.ts +35 -3
- /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
package/std/net/http-client.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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 (
|
|
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
|
|
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 <
|
|
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
|
|
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
|
|
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
|
package/std/net/http-compress.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/std/net/http1-server.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
*
|
|
566
|
-
* `HttpCompressor.segmentMode`; 0, the default,
|
|
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 `
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
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
|
|
1311
|
-
|
|
1312
|
-
|
|
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,
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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
|
}
|