@amritk/nish-x86_64-linux 0.23.1 → 0.24.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/bin/nish +0 -0
- package/docs/AI.md +26 -19
- package/package.json +1 -1
- package/runtime/core/arrays.d.ts +0 -2
- package/runtime/core/arrays.mjs +0 -2
- package/runtime/core/numbers.mjs +2 -3
- package/runtime/nish.d.ts +0 -4
- package/runtime/nish.mjs +2 -5
- package/runtime/runtime-net.c +1 -1
- package/runtime/runtime-term.c +2 -2
- package/runtime/runtime.c +1 -1
- package/std/README.md +20 -0
- package/std/compress/checksum.ts +178 -0
- package/std/compress/deflate.ts +2400 -0
- package/std/compress/gzip.ts +747 -0
- package/std/compress/zlib.ts +168 -0
- package/std/crypto/sha256.ts +2 -3
- package/std/crypto/x25519.ts +3 -4
- package/std/net/http-compress.ts +393 -0
- package/std/net/http1-server.ts +95 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/compress/zlib` — the zlib format (RFC 1950) around
|
|
3
|
+
* `nish/compress/deflate`: a two-byte header, the DEFLATE stream and the
|
|
4
|
+
* Adler-32 of the data, big-endian. What HTTP's `Content-Encoding: deflate`
|
|
5
|
+
* means (RFC 9110 §8.4.1.2), and what PNG and many protocols carry.
|
|
6
|
+
*
|
|
7
|
+
* import { DEFLATE_DEFAULT } from "nish/compress/deflate";
|
|
8
|
+
* import { ZlibCompressor, ZlibDecompressor } from "nish/compress/zlib";
|
|
9
|
+
*
|
|
10
|
+
* const z = new ZlibCompressor(DEFLATE_DEFAULT);
|
|
11
|
+
* const end: i32 = z.compress(src, 0, n, out, 0); // -1: no room
|
|
12
|
+
* const got: i32 = new ZlibDecompressor().decompress(out, 0, end, back, 0, n); // < 0: an error
|
|
13
|
+
*
|
|
14
|
+
* The objects allocate their tables once and nothing per call; the free
|
|
15
|
+
* functions build one for a single call. The header written is `0x78` and
|
|
16
|
+
* the `FLEVEL` of the level, with no preset dictionary. Reading refuses a
|
|
17
|
+
* method other than 8, a window over 32 KB, a header whose check fails
|
|
18
|
+
* (`ZLIB_ERR_HEADER`) and a preset dictionary (`ZLIB_ERR_DICTIONARY`, since
|
|
19
|
+
* none can be supplied); a wrong Adler-32 is `ZLIB_ERR_ADLER`, and bytes
|
|
20
|
+
* after the trailer are `DEFLATE_ERR_TRAILING`.
|
|
21
|
+
*
|
|
22
|
+
* Written from RFC 1950.
|
|
23
|
+
*/
|
|
24
|
+
import { adler32 } from "nish/compress/checksum"
|
|
25
|
+
import {
|
|
26
|
+
DEFLATE_ERR_OUTPUT_LIMIT,
|
|
27
|
+
DEFLATE_ERR_TRAILING,
|
|
28
|
+
DEFLATE_ERR_TRUNCATED,
|
|
29
|
+
DEFLATE_NO_ROOM,
|
|
30
|
+
Deflater,
|
|
31
|
+
INFLATE_DONE,
|
|
32
|
+
INFLATE_NEED_OUTPUT,
|
|
33
|
+
Inflater,
|
|
34
|
+
} from "nish/compress/deflate"
|
|
35
|
+
|
|
36
|
+
/** A zlib header that is not one: the wrong method, a window over 32 KB, or a failed check. */
|
|
37
|
+
export const ZLIB_ERR_HEADER: i32 = -12
|
|
38
|
+
|
|
39
|
+
/** The trailer's Adler-32 is not the Adler-32 of the data. */
|
|
40
|
+
export const ZLIB_ERR_ADLER: i32 = -13
|
|
41
|
+
|
|
42
|
+
/** The stream needs a preset dictionary (`FDICT`), which this reader has no way to be given. */
|
|
43
|
+
export const ZLIB_ERR_DICTIONARY: i32 = -14
|
|
44
|
+
|
|
45
|
+
/** Panics unless `buf[off .. off + len)` lies inside `buf`. */
|
|
46
|
+
const zlibWindow = (what: string, buf: u8[], off: i32, len: i32): void => {
|
|
47
|
+
if (off < 0 || len < 0 || off > toI32(buf.length) - len) {
|
|
48
|
+
panic(`${what}: the window [${off}, ${off} + ${len}) is outside a buffer of ${toI32(buf.length)} bytes`)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A zlib compressor at one level, allocated once. Not shared between threads. */
|
|
53
|
+
export class ZlibCompressor {
|
|
54
|
+
readonly deflater: Deflater
|
|
55
|
+
|
|
56
|
+
/** A compressor at `level`, 0 to 9; another level panics. */
|
|
57
|
+
constructor(level: i32) {
|
|
58
|
+
this.deflater = new Deflater(level)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compresses `src[off .. off + len)` into `out` from `at` as one zlib
|
|
63
|
+
* stream and answers where it ends, or `DEFLATE_NO_ROOM` (-1) when `out`
|
|
64
|
+
* cannot hold it; `compressBound(len)` bytes always can. A window outside
|
|
65
|
+
* its array panics. Allocates nothing.
|
|
66
|
+
*/
|
|
67
|
+
compress(src: u8[], off: i32, len: i32, out: u8[], at: i32): i32 {
|
|
68
|
+
zlibWindow("ZlibCompressor.compress", src, off, len)
|
|
69
|
+
zlibWindow("ZlibCompressor.compress", out, at, 0)
|
|
70
|
+
if (toI32(out.length) - at < 6) {
|
|
71
|
+
return DEFLATE_NO_ROOM
|
|
72
|
+
}
|
|
73
|
+
const level: i32 = this.deflater.level
|
|
74
|
+
// FLEVEL (§2.2): fastest, fast, default, slowest.
|
|
75
|
+
let flevel: i32 = 3
|
|
76
|
+
if (level < 2) {
|
|
77
|
+
flevel = 0
|
|
78
|
+
} else if (level < 6) {
|
|
79
|
+
flevel = 1
|
|
80
|
+
} else if (level === 6) {
|
|
81
|
+
flevel = 2
|
|
82
|
+
}
|
|
83
|
+
const cmf: i32 = 0x78
|
|
84
|
+
let flg: i32 = flevel << 6
|
|
85
|
+
flg += (toI32(31) - ((cmf * 256 + flg) % 31)) % 31
|
|
86
|
+
out[at] = toU8(cmf)
|
|
87
|
+
out[at + 1] = toU8(flg)
|
|
88
|
+
const end: i32 = this.deflater.compress(src, off, len, out, at + 2)
|
|
89
|
+
if (end < 0 || toI32(out.length) - end < 4) {
|
|
90
|
+
return DEFLATE_NO_ROOM
|
|
91
|
+
}
|
|
92
|
+
const a: u32 = adler32(src, off, len)
|
|
93
|
+
out[end] = toU8(a >>> 24)
|
|
94
|
+
out[end + 1] = toU8((a >>> 16) & 255)
|
|
95
|
+
out[end + 2] = toU8((a >>> 8) & 255)
|
|
96
|
+
out[end + 3] = toU8(a & 255)
|
|
97
|
+
return end + 4
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A zlib decompressor, allocated once. Not shared between threads. */
|
|
102
|
+
export class ZlibDecompressor {
|
|
103
|
+
readonly inflater: Inflater
|
|
104
|
+
|
|
105
|
+
/** Builds the tables. */
|
|
106
|
+
constructor() {
|
|
107
|
+
this.inflater = new Inflater()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Decompresses the zlib stream `src[off .. off + len)` into `out` from
|
|
112
|
+
* `at`, writing at most `limit` bytes and never past `out`'s end, and
|
|
113
|
+
* answers where the output ends, or a negative `ZLIB_ERR_*` or
|
|
114
|
+
* `DEFLATE_ERR_*`. A window outside its array, or a negative `limit`,
|
|
115
|
+
* panics. Allocates nothing.
|
|
116
|
+
*/
|
|
117
|
+
decompress(src: u8[], off: i32, len: i32, out: u8[], at: i32, limit: i32): i32 {
|
|
118
|
+
zlibWindow("ZlibDecompressor.decompress", src, off, len)
|
|
119
|
+
zlibWindow("ZlibDecompressor.decompress", out, at, 0)
|
|
120
|
+
if (limit < 0) {
|
|
121
|
+
panic("ZlibDecompressor.decompress: a negative limit")
|
|
122
|
+
}
|
|
123
|
+
if (len < 2) {
|
|
124
|
+
return DEFLATE_ERR_TRUNCATED
|
|
125
|
+
}
|
|
126
|
+
const cmf: i32 = toI32(src[off])
|
|
127
|
+
const flg: i32 = toI32(src[off + 1])
|
|
128
|
+
if ((cmf & 15) !== 8 || cmf >> 4 > 7 || (cmf * 256 + flg) % 31 !== 0) {
|
|
129
|
+
return ZLIB_ERR_HEADER
|
|
130
|
+
}
|
|
131
|
+
if ((flg & 0x20) !== 0) {
|
|
132
|
+
return ZLIB_ERR_DICTIONARY
|
|
133
|
+
}
|
|
134
|
+
const room: i32 = toI32(out.length) - at
|
|
135
|
+
const outEnd: i32 = at + (limit < room ? limit : room)
|
|
136
|
+
const inf: Inflater = this.inflater
|
|
137
|
+
inf.reset()
|
|
138
|
+
const end: i32 = off + len
|
|
139
|
+
const r: i32 = inf.inflate(src, off + 2, end, false, out, at, outEnd, at)
|
|
140
|
+
if (r === INFLATE_NEED_OUTPUT) {
|
|
141
|
+
return DEFLATE_ERR_OUTPUT_LIMIT
|
|
142
|
+
}
|
|
143
|
+
if (r !== INFLATE_DONE) {
|
|
144
|
+
return r
|
|
145
|
+
}
|
|
146
|
+
const q: i32 = inf.inPos
|
|
147
|
+
if (end - q < 4) {
|
|
148
|
+
return DEFLATE_ERR_TRUNCATED
|
|
149
|
+
}
|
|
150
|
+
const want: u32 =
|
|
151
|
+
(toU32(src[q]) << 24) | (toU32(src[q + 1]) << 16) | (toU32(src[q + 2]) << 8) | toU32(src[q + 3])
|
|
152
|
+
if (adler32(out, at, inf.outPos - at) !== want) {
|
|
153
|
+
return ZLIB_ERR_ADLER
|
|
154
|
+
}
|
|
155
|
+
if (q + 4 !== end) {
|
|
156
|
+
return DEFLATE_ERR_TRAILING
|
|
157
|
+
}
|
|
158
|
+
return inf.outPos
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `ZlibCompressor.compress` at `level`, building the compressor for this call alone. */
|
|
163
|
+
export const zlibCompress = (src: u8[], off: i32, len: i32, out: u8[], at: i32, level: i32): i32 =>
|
|
164
|
+
new ZlibCompressor(level).compress(src, off, len, out, at)
|
|
165
|
+
|
|
166
|
+
/** `ZlibDecompressor.decompress`, building the decompressor for this call alone. */
|
|
167
|
+
export const zlibDecompress = (src: u8[], off: i32, len: i32, out: u8[], at: i32, limit: i32): i32 =>
|
|
168
|
+
new ZlibDecompressor().decompress(src, off, len, out, at, limit)
|
package/std/crypto/sha256.ts
CHANGED
|
@@ -215,9 +215,8 @@ const sha256Compress = (h: u32[], w: u32[], src: u8[], at: i32): void => {
|
|
|
215
215
|
// early exit, because that is the shape that proves the plain `w[t]` and
|
|
216
216
|
// `h[i]` indices in range and drops their bounds checks. The computed ones —
|
|
217
217
|
// the byte reads in `sha256LoadWord` and `w[t - 2]` and its kin — keep a
|
|
218
|
-
// check each; measured, removing every check
|
|
219
|
-
//
|
|
220
|
-
// less than the run-to-run noise.
|
|
218
|
+
// check each; measured, removing every check moved the 1 MiB time by less
|
|
219
|
+
// than the run-to-run noise.
|
|
221
220
|
if (toI32(h.length) >= 8 && toI32(w.length) >= 64 && at >= 0 && at <= toI32(src.length) - SHA256_BLOCK) {
|
|
222
221
|
// Step 1: the message schedule.
|
|
223
222
|
for (let t: i32 = 0; t < 16; t += 1) {
|
package/std/crypto/x25519.ts
CHANGED
|
@@ -21,10 +21,9 @@
|
|
|
21
21
|
* subtraction needs no bias and a carry is a floor division that works the
|
|
22
22
|
* same on a negative limb.
|
|
23
23
|
*
|
|
24
|
-
* Signed `i64` overflow is undefined behaviour in this language
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* site that calls one), so the limb bounds below are not a nicety: each function states what it accepts and what it answers, and
|
|
24
|
+
* Signed `i64` overflow is undefined behaviour in this language (only
|
|
25
|
+
* `wrappingAdd`, `wrappingSub` and `wrappingMul` from `nish:unsafe` define a
|
|
26
|
+
* wrap, at the site that calls one), so the limb bounds below are not a nicety: each function states what it accepts and what it answers, and
|
|
28
27
|
* `f25519Mul` says why its sums fit.
|
|
29
28
|
*
|
|
30
29
|
* **Constant time, by construction, and in part by disassembly.** Nothing
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/http-compress` — content coding for HTTP responses (RFC 9110
|
|
3
|
+
* §8.4 and §12.5.3): reading a request's `Accept-Encoding` and compressing a
|
|
4
|
+
* body with `Content-Encoding: gzip`, for any of the three servers.
|
|
5
|
+
*
|
|
6
|
+
* import { DEFLATE_FAST } from "nish/compress/deflate";
|
|
7
|
+
* import { HTTP_CODING_GZIP, HttpCompressor } from "nish/net/http-compress";
|
|
8
|
+
*
|
|
9
|
+
* const enc = new HttpCompressor(DEFLATE_FAST, 1024, 65536); // once per thread
|
|
10
|
+
* // HTTP/1.1, in one call:
|
|
11
|
+
* conn.respondEncoded(200, names, values, body, 0, n, enc, conn.acceptEncoding());
|
|
12
|
+
* // HTTP/2 or HTTP/3, by hand:
|
|
13
|
+
* const coding: i32 = httpAcceptEncoding(value, 0, toI32(value.length), HTTP_OFFER_GZIP);
|
|
14
|
+
* const m: i32 = enc.compress(coding, body, 0, n);
|
|
15
|
+
* if (m >= 0) {
|
|
16
|
+
* enc.addFields(names, values);
|
|
17
|
+
* h2.respond(id, 200, enc.names, enc.values, false);
|
|
18
|
+
* h2.write(id, enc.output, 0, m);
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* **Opt-in.** Nothing here runs unless the program asks: it decides which
|
|
22
|
+
* responses to compress, and `HttpCompressor` declines any body shorter than
|
|
23
|
+
* its `minLength`, longer than its `maxLength`, or that gzip would not make
|
|
24
|
+
* smaller, answering -1 so the program sends the bytes as they are.
|
|
25
|
+
*
|
|
26
|
+
* **Negotiation** (`httpAcceptEncoding`) follows §12.5.3: each listed coding
|
|
27
|
+
* with its q-value, `x-gzip` as `gzip`, `*` for every coding not listed, and
|
|
28
|
+
* `identity` acceptable unless it is given q=0, directly or through `*`. Of
|
|
29
|
+
* the codings the program offers, the most preferred with q above 0 wins, the
|
|
30
|
+
* program's order (gzip, then deflate) breaking ties; it beats `identity` at
|
|
31
|
+
* an equal q, and beats an `identity` the field does not mention at any q. With no field at all the answer is
|
|
32
|
+
* `HTTP_CODING_IDENTITY` (the RFC allows any coding then, but a client that
|
|
33
|
+
* says nothing is safest sent nothing it may not read); an empty field asks
|
|
34
|
+
* for identity too. `HTTP_CODING_NONE` means nothing offered is acceptable
|
|
35
|
+
* and identity is refused: a server may answer 406 or send identity anyway.
|
|
36
|
+
* A malformed element is skipped. The parser allocates nothing.
|
|
37
|
+
*
|
|
38
|
+
* **Compressing allocates nothing**: `HttpCompressor` holds a
|
|
39
|
+
* `GzipCompressor`, an output buffer of `compressBound(maxLength)` bytes and
|
|
40
|
+
* the field arrays `addFields` fills, all built once.
|
|
41
|
+
* `HTTP_CODING_DEFLATE` (zlib framing) is negotiated but not produced: a
|
|
42
|
+
* client that accepts only `deflate` is sent identity, which every client
|
|
43
|
+
* reads.
|
|
44
|
+
*/
|
|
45
|
+
import { DEFLATE_FAST, compressBound } from "nish/compress/deflate"
|
|
46
|
+
import { GzipCompressor } from "nish/compress/gzip"
|
|
47
|
+
|
|
48
|
+
/** No content coding: the body as it is. */
|
|
49
|
+
export const HTTP_CODING_IDENTITY: i32 = 0
|
|
50
|
+
|
|
51
|
+
/** `gzip` (RFC 9110 §8.4.1.3), or its alias `x-gzip`. */
|
|
52
|
+
export const HTTP_CODING_GZIP: i32 = 1
|
|
53
|
+
|
|
54
|
+
/** `deflate`: the zlib format (RFC 9110 §8.4.1.2). */
|
|
55
|
+
export const HTTP_CODING_DEFLATE: i32 = 2
|
|
56
|
+
|
|
57
|
+
/** Nothing offered is acceptable, identity included (§12.5.3: 406, or identity anyway). */
|
|
58
|
+
export const HTTP_CODING_NONE: i32 = -1
|
|
59
|
+
|
|
60
|
+
/** `httpAcceptEncoding`'s `offered` bits: the program can send gzip, deflate. */
|
|
61
|
+
export const HTTP_OFFER_GZIP: i32 = 1
|
|
62
|
+
export const HTTP_OFFER_DEFLATE: i32 = 2
|
|
63
|
+
|
|
64
|
+
/** A q-value of 1, in thousandths. */
|
|
65
|
+
const HTTP_Q_ONE: i32 = 1000
|
|
66
|
+
|
|
67
|
+
/** Whether `c` is an ASCII letter, digit or one of the token characters a coding uses. */
|
|
68
|
+
const httpCodingChar = (c: i32): boolean =>
|
|
69
|
+
(c >= 97 && c <= 122) ||
|
|
70
|
+
(c >= 65 && c <= 90) ||
|
|
71
|
+
(c >= 48 && c <= 57) ||
|
|
72
|
+
c === 45 ||
|
|
73
|
+
c === 46 ||
|
|
74
|
+
c === 95 ||
|
|
75
|
+
c === 42 ||
|
|
76
|
+
c === 33 ||
|
|
77
|
+
c === 35 ||
|
|
78
|
+
c === 36 ||
|
|
79
|
+
c === 37 ||
|
|
80
|
+
c === 38 ||
|
|
81
|
+
c === 39 ||
|
|
82
|
+
c === 43 ||
|
|
83
|
+
c === 94 ||
|
|
84
|
+
c === 96 ||
|
|
85
|
+
c === 124 ||
|
|
86
|
+
c === 126
|
|
87
|
+
|
|
88
|
+
/** Whether `buf[start .. end)` is `word`, ignoring ASCII case; `word` is lower case. */
|
|
89
|
+
const httpCodingIs = (buf: u8[], start: i32, end: i32, word: string): boolean => {
|
|
90
|
+
if (end - start !== toI32(word.length)) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
for (let i: i32 = 0; i < end - start && i < toI32(word.length); i += 1) {
|
|
94
|
+
let c: i32 = toI32(buf[start + i])
|
|
95
|
+
if (c >= 65 && c <= 90) {
|
|
96
|
+
c += 32
|
|
97
|
+
}
|
|
98
|
+
if (c !== toI32(word.charCodeAt(i))) {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The q-value of `buf[start .. end)`, in thousandths (RFC 9110 §12.4.2:
|
|
107
|
+
* `0`, `0.` and up to three digits, `1`, `1.` and up to three zeros), or -1
|
|
108
|
+
* when it is not one.
|
|
109
|
+
*/
|
|
110
|
+
const httpQValue = (buf: u8[], start: i32, end: i32): i32 => {
|
|
111
|
+
if (start >= end) {
|
|
112
|
+
return -1
|
|
113
|
+
}
|
|
114
|
+
const lead: i32 = toI32(buf[start])
|
|
115
|
+
if (lead !== 48 && lead !== 49) {
|
|
116
|
+
return -1
|
|
117
|
+
}
|
|
118
|
+
let q: i32 = lead === 49 ? HTTP_Q_ONE : 0
|
|
119
|
+
if (start + 1 === end) {
|
|
120
|
+
return q
|
|
121
|
+
}
|
|
122
|
+
if (buf[start + 1] !== 46 || end - start > 5) {
|
|
123
|
+
return -1
|
|
124
|
+
}
|
|
125
|
+
let scale: i32 = 100
|
|
126
|
+
for (let i: i32 = start + 2; i < end && i >= 0 && i < toI32(buf.length); i += 1) {
|
|
127
|
+
const d: i32 = toI32(buf[i]) - 48
|
|
128
|
+
if (d < 0 || d > 9 || (lead === 49 && d !== 0)) {
|
|
129
|
+
return -1
|
|
130
|
+
}
|
|
131
|
+
q += d * scale
|
|
132
|
+
scale = scale / 10
|
|
133
|
+
}
|
|
134
|
+
return q
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A coding's q: its own when listed, else `*`'s when that is, else 0. */
|
|
138
|
+
const httpQOrStar = (own: i32, star: i32): i32 => {
|
|
139
|
+
if (own >= 0) {
|
|
140
|
+
return own
|
|
141
|
+
}
|
|
142
|
+
return star >= 0 ? star : 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Whether `c` is optional whitespace (SP or HTAB). */
|
|
146
|
+
const httpOws = (c: i32): boolean => c === 32 || c === 9
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The content coding to send for the `Accept-Encoding` field value
|
|
150
|
+
* `buf[start .. end)` among the codings `offered` (`HTTP_OFFER_GZIP |
|
|
151
|
+
* HTTP_OFFER_DEFLATE`): `HTTP_CODING_GZIP`, `HTTP_CODING_DEFLATE`,
|
|
152
|
+
* `HTTP_CODING_IDENTITY` or `HTTP_CODING_NONE`, by the rules in the module
|
|
153
|
+
* comment. A window outside `buf` panics.
|
|
154
|
+
*/
|
|
155
|
+
export const httpAcceptEncoding = (buf: u8[], start: i32, end: i32, offered: i32): i32 => {
|
|
156
|
+
if (start < 0 || start > end || end > toI32(buf.length)) {
|
|
157
|
+
panic("httpAcceptEncoding: the window lies outside its buffer")
|
|
158
|
+
}
|
|
159
|
+
let qGzip: i32 = -1
|
|
160
|
+
let qDeflate: i32 = -1
|
|
161
|
+
let qIdentity: i32 = -1
|
|
162
|
+
let qStar: i32 = -1
|
|
163
|
+
let p: i32 = start
|
|
164
|
+
while (p < end) {
|
|
165
|
+
// One element: OWS coding OWS *( ";" OWS name "=" value ) OWS ","
|
|
166
|
+
while (p < end && (httpOws(toI32(buf[p])) || buf[p] === 44)) {
|
|
167
|
+
p += 1
|
|
168
|
+
}
|
|
169
|
+
const codingStart: i32 = p
|
|
170
|
+
while (p < end && httpCodingChar(toI32(buf[p]))) {
|
|
171
|
+
p += 1
|
|
172
|
+
}
|
|
173
|
+
const codingEnd: i32 = p
|
|
174
|
+
let q: i32 = HTTP_Q_ONE
|
|
175
|
+
let valid: boolean = codingEnd > codingStart
|
|
176
|
+
while (p < end && buf[p] !== 44) {
|
|
177
|
+
while (p < end && httpOws(toI32(buf[p]))) {
|
|
178
|
+
p += 1
|
|
179
|
+
}
|
|
180
|
+
if (p >= end || buf[p] === 44) {
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
if (buf[p] !== 59) {
|
|
184
|
+
valid = false
|
|
185
|
+
while (p < end && buf[p] !== 44) {
|
|
186
|
+
p += 1
|
|
187
|
+
}
|
|
188
|
+
break
|
|
189
|
+
}
|
|
190
|
+
p += 1
|
|
191
|
+
while (p < end && httpOws(toI32(buf[p]))) {
|
|
192
|
+
p += 1
|
|
193
|
+
}
|
|
194
|
+
const nameStart: i32 = p
|
|
195
|
+
while (p < end && httpCodingChar(toI32(buf[p]))) {
|
|
196
|
+
p += 1
|
|
197
|
+
}
|
|
198
|
+
const nameEnd: i32 = p
|
|
199
|
+
if (p >= end || buf[p] !== 61) {
|
|
200
|
+
valid = false
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
p += 1
|
|
204
|
+
const valueStart: i32 = p
|
|
205
|
+
while (p < end && httpCodingChar(toI32(buf[p]))) {
|
|
206
|
+
p += 1
|
|
207
|
+
}
|
|
208
|
+
if (httpCodingIs(buf, nameStart, nameEnd, "q")) {
|
|
209
|
+
q = httpQValue(buf, valueStart, p)
|
|
210
|
+
if (q < 0) {
|
|
211
|
+
valid = false
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (valid) {
|
|
216
|
+
if (
|
|
217
|
+
httpCodingIs(buf, codingStart, codingEnd, "gzip") ||
|
|
218
|
+
httpCodingIs(buf, codingStart, codingEnd, "x-gzip")
|
|
219
|
+
) {
|
|
220
|
+
qGzip = q
|
|
221
|
+
} else if (httpCodingIs(buf, codingStart, codingEnd, "deflate")) {
|
|
222
|
+
qDeflate = q
|
|
223
|
+
} else if (httpCodingIs(buf, codingStart, codingEnd, "identity")) {
|
|
224
|
+
qIdentity = q
|
|
225
|
+
} else if (httpCodingIs(buf, codingStart, codingEnd, "*")) {
|
|
226
|
+
qStar = q
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const gzip: i32 = (offered & HTTP_OFFER_GZIP) === 0 ? 0 : httpQOrStar(qGzip, qStar)
|
|
231
|
+
const deflate: i32 = (offered & HTTP_OFFER_DEFLATE) === 0 ? 0 : httpQOrStar(qDeflate, qStar)
|
|
232
|
+
// `identity` unlisted (and not reached by `*`) is acceptable but stated no
|
|
233
|
+
// preference, so any offered coding with q above 0 is sent before it.
|
|
234
|
+
let identity: i32 = -1
|
|
235
|
+
if (qIdentity >= 0) {
|
|
236
|
+
identity = qIdentity
|
|
237
|
+
} else if (qStar >= 0) {
|
|
238
|
+
identity = qStar
|
|
239
|
+
}
|
|
240
|
+
let coding: i32 = HTTP_CODING_NONE
|
|
241
|
+
let codingQ: i32 = 0
|
|
242
|
+
if (deflate > gzip) {
|
|
243
|
+
coding = HTTP_CODING_DEFLATE
|
|
244
|
+
codingQ = deflate
|
|
245
|
+
} else if (gzip > 0) {
|
|
246
|
+
coding = HTTP_CODING_GZIP
|
|
247
|
+
codingQ = gzip
|
|
248
|
+
}
|
|
249
|
+
if (coding !== HTTP_CODING_NONE && codingQ >= identity) {
|
|
250
|
+
return coding
|
|
251
|
+
}
|
|
252
|
+
if (identity !== 0) {
|
|
253
|
+
return HTTP_CODING_IDENTITY
|
|
254
|
+
}
|
|
255
|
+
return HTTP_CODING_NONE
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The bytes of an ASCII string. */
|
|
259
|
+
const httpCompressBytes = (text: string): u8[] => {
|
|
260
|
+
const out: u8[] = new Array<u8>(toI32(text.length))
|
|
261
|
+
for (let i: i32 = 0; i < toI32(text.length) && i < toI32(out.length); i += 1) {
|
|
262
|
+
out[i] = toU8(text.charCodeAt(i))
|
|
263
|
+
}
|
|
264
|
+
return out
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Compresses response bodies with gzip into a buffer of its own, and builds
|
|
269
|
+
* the response fields that say so. One per thread; allocated once.
|
|
270
|
+
*/
|
|
271
|
+
export class HttpCompressor {
|
|
272
|
+
readonly gzip: GzipCompressor
|
|
273
|
+
/** Where `compress` writes; valid until the next call. */
|
|
274
|
+
readonly output: u8[]
|
|
275
|
+
/** The fields `addFields` built: the program's, then `Content-Encoding` and `Vary`. */
|
|
276
|
+
readonly names: u8[][]
|
|
277
|
+
readonly values: u8[][]
|
|
278
|
+
readonly contentEncodingName: u8[]
|
|
279
|
+
readonly gzipValue: u8[]
|
|
280
|
+
readonly varyName: u8[]
|
|
281
|
+
readonly acceptEncodingValue: u8[]
|
|
282
|
+
/** The shortest body worth compressing, and the longest the output can hold. */
|
|
283
|
+
readonly minLength: i32
|
|
284
|
+
readonly maxLength: i32
|
|
285
|
+
/**
|
|
286
|
+
* Whether `addFields` adds `Vary: Accept-Encoding` (RFC 9110 §12.5.5), which
|
|
287
|
+
* a cache needs whenever the same resource may be sent either way. On by
|
|
288
|
+
* default; turn it off when the program sets its own `Vary`.
|
|
289
|
+
*/
|
|
290
|
+
vary: boolean
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* A compressor at `level` (`DEFLATE_FAST` suits a server) for bodies of
|
|
294
|
+
* `minLength` to `maxLength` bytes. A level outside 0 to 9, a negative
|
|
295
|
+
* `minLength` or a `maxLength` below it panics.
|
|
296
|
+
*/
|
|
297
|
+
constructor(level: i32, minLength: i32, maxLength: i32) {
|
|
298
|
+
if (minLength < 0 || maxLength < minLength) {
|
|
299
|
+
panic("HttpCompressor: the lengths are out of order")
|
|
300
|
+
}
|
|
301
|
+
this.gzip = new GzipCompressor(level)
|
|
302
|
+
this.output = new Array<u8>(compressBound(maxLength))
|
|
303
|
+
this.names = []
|
|
304
|
+
this.values = []
|
|
305
|
+
this.contentEncodingName = httpCompressBytes("content-encoding")
|
|
306
|
+
this.gzipValue = httpCompressBytes("gzip")
|
|
307
|
+
this.varyName = httpCompressBytes("vary")
|
|
308
|
+
this.acceptEncodingValue = httpCompressBytes("accept-encoding")
|
|
309
|
+
this.minLength = minLength
|
|
310
|
+
this.maxLength = maxLength
|
|
311
|
+
this.vary = true
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Compresses `body[off .. off + len)` into `output` when `coding` is
|
|
316
|
+
* `HTTP_CODING_GZIP`, the length is in `[minLength, maxLength]` and gzip
|
|
317
|
+
* makes it smaller; answers the compressed length, or -1 to send the body
|
|
318
|
+
* as it is. A window outside `body` panics.
|
|
319
|
+
*/
|
|
320
|
+
compress(coding: i32, body: u8[], off: i32, len: i32): i32 {
|
|
321
|
+
if (off < 0 || len < 0 || off > toI32(body.length) - len) {
|
|
322
|
+
panic("HttpCompressor.compress: the window lies outside its body")
|
|
323
|
+
}
|
|
324
|
+
if (coding !== HTTP_CODING_GZIP || len < this.minLength || len > this.maxLength) {
|
|
325
|
+
return -1
|
|
326
|
+
}
|
|
327
|
+
const n: i32 = this.gzip.compress(body, off, len, this.output, toI32(0))
|
|
328
|
+
if (n < 0 || n >= len) {
|
|
329
|
+
return -1
|
|
330
|
+
}
|
|
331
|
+
return n
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Sets `names` / `values` to the program's fields followed by
|
|
336
|
+
* `Content-Encoding: gzip` and, when `vary`, `Vary: Accept-Encoding`. The
|
|
337
|
+
* arrays are reused, so after the first few calls this allocates nothing;
|
|
338
|
+
* they hold until the next call.
|
|
339
|
+
*/
|
|
340
|
+
addFields(names: u8[][], values: u8[][]): void {
|
|
341
|
+
while (this.names.length > 0) {
|
|
342
|
+
this.names.pop()
|
|
343
|
+
}
|
|
344
|
+
while (this.values.length > 0) {
|
|
345
|
+
this.values.pop()
|
|
346
|
+
}
|
|
347
|
+
for (let i: i32 = 0; i < toI32(values.length); i += 1) {
|
|
348
|
+
if (i < toI32(names.length)) {
|
|
349
|
+
const name: u8[] = names[i]
|
|
350
|
+
const value: u8[] = values[i]
|
|
351
|
+
this.names.push(name)
|
|
352
|
+
this.values.push(value)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
this.names.push(this.contentEncodingName)
|
|
356
|
+
this.values.push(this.gzipValue)
|
|
357
|
+
this.addVary()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Appends `Vary: Accept-Encoding` to the arrays `addFields` built when `vary` is set. */
|
|
361
|
+
addVary(): void {
|
|
362
|
+
if (this.vary) {
|
|
363
|
+
this.names.push(this.varyName)
|
|
364
|
+
this.values.push(this.acceptEncodingValue)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Sets `names` / `values` to the program's fields and, when `vary`,
|
|
370
|
+
* `Vary: Accept-Encoding`: the fields of a body sent as it is from a
|
|
371
|
+
* resource that is sometimes compressed.
|
|
372
|
+
*/
|
|
373
|
+
identityFields(names: u8[][], values: u8[][]): void {
|
|
374
|
+
while (this.names.length > 0) {
|
|
375
|
+
this.names.pop()
|
|
376
|
+
}
|
|
377
|
+
while (this.values.length > 0) {
|
|
378
|
+
this.values.pop()
|
|
379
|
+
}
|
|
380
|
+
for (let i: i32 = 0; i < toI32(values.length); i += 1) {
|
|
381
|
+
if (i < toI32(names.length)) {
|
|
382
|
+
const name: u8[] = names[i]
|
|
383
|
+
const value: u8[] = values[i]
|
|
384
|
+
this.names.push(name)
|
|
385
|
+
this.values.push(value)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
this.addVary()
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** A compressor for the usual server: `DEFLATE_FAST`, bodies of 1 KB to 1 MB. */
|
|
393
|
+
export const httpCompressorDefault = (): HttpCompressor => new HttpCompressor(DEFLATE_FAST, 1024, 1048576)
|
package/std/net/http1-server.ts
CHANGED
|
@@ -114,6 +114,12 @@
|
|
|
114
114
|
*/
|
|
115
115
|
import { Secret } from "nish:secret"
|
|
116
116
|
import { copyBytes } from "nish/bytes"
|
|
117
|
+
import {
|
|
118
|
+
HTTP_CODING_IDENTITY,
|
|
119
|
+
HTTP_OFFER_GZIP,
|
|
120
|
+
HttpCompressor,
|
|
121
|
+
httpAcceptEncoding,
|
|
122
|
+
} from "nish/net/http-compress"
|
|
117
123
|
import { netClose, netRead, netWrite, tcpAccept } from "nish:net"
|
|
118
124
|
import {
|
|
119
125
|
HTTP1_BODY,
|
|
@@ -1205,6 +1211,95 @@ export class Http1Connection {
|
|
|
1205
1211
|
return 0
|
|
1206
1212
|
}
|
|
1207
1213
|
|
|
1214
|
+
// ---- Content coding -----------------------------------------------------------
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* The content coding to answer the current request with, read from its
|
|
1218
|
+
* `Accept-Encoding` with gzip offered (`httpAcceptEncoding`):
|
|
1219
|
+
* `HTTP_CODING_GZIP`, `HTTP_CODING_IDENTITY` (also with no such field), or
|
|
1220
|
+
* `HTTP_CODING_NONE` when the client refuses identity and does not take
|
|
1221
|
+
* gzip. Allocates nothing.
|
|
1222
|
+
*/
|
|
1223
|
+
acceptEncoding(): i32 {
|
|
1224
|
+
const parser: Http1Parser = this.parser
|
|
1225
|
+
const i: i32 = parser.headerIndex("accept-encoding")
|
|
1226
|
+
const spans: i32[] = parser.spans
|
|
1227
|
+
if (i < 0 || 4 * i + 3 >= toI32(spans.length)) {
|
|
1228
|
+
return HTTP_CODING_IDENTITY
|
|
1229
|
+
}
|
|
1230
|
+
const start: i32 = spans[4 * i + 2]
|
|
1231
|
+
const end: i32 = spans[4 * i + 3]
|
|
1232
|
+
if (start < 0 || start > end || end > toI32(parser.head.length)) {
|
|
1233
|
+
return HTTP_CODING_IDENTITY
|
|
1234
|
+
}
|
|
1235
|
+
return httpAcceptEncoding(parser.head, start, end, HTTP_OFFER_GZIP)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Answers the current request with all of `body[off .. off + len)` in one
|
|
1240
|
+
* call: `respond`, `write` and `end`, the body compressed by `enc` when
|
|
1241
|
+
* `coding` (from `acceptEncoding`) is gzip and `enc.compress` takes it,
|
|
1242
|
+
* 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
|
|
1246
|
+
* it: `H1_AGAIN` when the output has no room for it yet (`H1_WRITE` says
|
|
1247
|
+
* when to try again, compressing again), `H1_INVALID` for a response
|
|
1248
|
+
* larger than the output could ever hold or one `respond` refuses,
|
|
1249
|
+
* `H1_CLOSED` with no request to answer; 0 once it is queued. A window
|
|
1250
|
+
* outside `body` panics.
|
|
1251
|
+
*/
|
|
1252
|
+
respondEncoded(
|
|
1253
|
+
status: i32,
|
|
1254
|
+
names: u8[][],
|
|
1255
|
+
values: u8[][],
|
|
1256
|
+
body: u8[],
|
|
1257
|
+
off: i32,
|
|
1258
|
+
len: i32,
|
|
1259
|
+
enc: HttpCompressor,
|
|
1260
|
+
coding: i32
|
|
1261
|
+
): i32 {
|
|
1262
|
+
http1ServerCheckWindow("Http1Connection.respondEncoded", body, off, len)
|
|
1263
|
+
if (!this.answerable()) {
|
|
1264
|
+
return H1_CLOSED
|
|
1265
|
+
}
|
|
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
|
+
}
|
|
1274
|
+
// `respond` and the body together, or neither: what `respond` sets is
|
|
1275
|
+
// put back when the body does not fit behind the head.
|
|
1276
|
+
const outputEnd: i32 = this.outputEnd
|
|
1277
|
+
const closeAfter: boolean = this.closeAfter
|
|
1278
|
+
const r: i32 = this.respond(status, enc.names, enc.values, n)
|
|
1279
|
+
if (r !== 0) {
|
|
1280
|
+
return r
|
|
1281
|
+
}
|
|
1282
|
+
if (!this.responseEmpty && n > this.bodyRoom()) {
|
|
1283
|
+
const fits: boolean = this.outputEnd - outputEnd + n <= this.config.outputSize - H1_CONTROL_RESERVE
|
|
1284
|
+
this.outputEnd = outputEnd
|
|
1285
|
+
this.closeAfter = closeAfter
|
|
1286
|
+
this.response = H1_R_NONE
|
|
1287
|
+
this.responseFraming = 0
|
|
1288
|
+
this.responseLeft = 0
|
|
1289
|
+
this.responseEmpty = false
|
|
1290
|
+
if (!fits) {
|
|
1291
|
+
return H1_INVALID
|
|
1292
|
+
}
|
|
1293
|
+
this.writeHeld = true
|
|
1294
|
+
return H1_AGAIN
|
|
1295
|
+
}
|
|
1296
|
+
const w: i32 = packed >= 0 ? this.write(enc.output, toI32(0), n) : this.write(body, off, n)
|
|
1297
|
+
if (w !== n) {
|
|
1298
|
+
panic("Http1Connection.respondEncoded: a body the room was checked for did not fit")
|
|
1299
|
+
}
|
|
1300
|
+
return this.end()
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1208
1303
|
// ---- WebSocket ----------------------------------------------------------------
|
|
1209
1304
|
|
|
1210
1305
|
/**
|