@amritk/nish-x86_64-linux 0.23.1 → 0.25.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 +50 -23
- package/package.json +1 -1
- package/runtime/browser.mjs +4 -3
- package/runtime/core/arrays.d.ts +0 -2
- package/runtime/core/arrays.mjs +0 -2
- package/runtime/core/host.d.ts +8 -0
- package/runtime/core/host.mjs +50 -2
- package/runtime/core/numbers.mjs +2 -3
- package/runtime/nish.d.ts +100 -4
- package/runtime/nish.h +34 -0
- package/runtime/nish.mjs +23 -5
- package/runtime/node.mjs +57 -6
- package/runtime/runtime-net.c +45 -4
- package/runtime/runtime-os.c +115 -12
- package/runtime/runtime-term.c +2 -2
- package/runtime/runtime.c +1 -1
- package/scripts/build.sh +13 -3
- package/std/README.md +44 -7
- package/std/cli-args.ts +1216 -0
- package/std/cli-flags.ts +585 -0
- package/std/cli.ts +308 -19
- package/std/compress/checksum.ts +290 -0
- package/std/compress/deflate.ts +2790 -0
- package/std/compress/gzip.ts +761 -0
- package/std/compress/zlib.ts +168 -0
- package/std/crypto/LICENSE-fiat-crypto-p384 +21 -0
- package/std/crypto/p256.ts +63 -1
- package/std/crypto/p384.ts +6044 -0
- package/std/crypto/pki-roots.ts +322 -0
- package/std/crypto/pki.ts +1207 -0
- package/std/crypto/rsa.ts +663 -0
- package/std/crypto/sha256.ts +2 -3
- package/std/crypto/x25519.ts +3 -4
- package/std/crypto/x509.ts +1054 -58
- package/std/net/dns-config.ts +734 -0
- package/std/net/dns-message.ts +517 -0
- package/std/net/dns-resolver.ts +1015 -0
- package/std/net/dns.ts +478 -0
- package/std/net/http-compress.ts +393 -0
- package/std/net/http1-client.ts +1227 -0
- package/std/net/http1-server.ts +101 -1
- package/std/net/http1.ts +9 -9
- package/std/net/tls/client.ts +1094 -0
- package/std/net/tls/codec.ts +585 -3
- package/std/net/tls/record-client.ts +1192 -0
- package/std/net/tls/verify.ts +221 -0
- package/std/net/tls-tcp.ts +642 -11
- package/std/net/tls.ts +2 -2
- package/std/net/url.ts +559 -4
- package/std/yaml-write.ts +415 -0
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nish/net/http1-client` — the client side of HTTP/1.1, sans-IO: a request
|
|
3
|
+
* writer and an incremental response parser, bytes in, events and bytes out,
|
|
4
|
+
* and no socket anywhere. `nish/net/http1` is the server side; the event
|
|
5
|
+
* constants and the chunk writers are shared and imported from there.
|
|
6
|
+
*
|
|
7
|
+
* **The writer** is `http1RequestHead`, which answers a request line and
|
|
8
|
+
* header section with the framing field the body needs, and
|
|
9
|
+
* `http1WriteRequestHead`, which writes the same head into a caller's buffer
|
|
10
|
+
* with nothing allocated. A chunked body is written with `http1Chunk` and
|
|
11
|
+
* `http1LastChunk` from `nish/net/http1`. The writer refuses anything that
|
|
12
|
+
* would let a caller's string end a line early or frame the body itself — the
|
|
13
|
+
* request-splitting and request-smuggling shapes.
|
|
14
|
+
*
|
|
15
|
+
* **The parser** is a push-pull machine, like the server's. A caller tells it
|
|
16
|
+
* what it asked (`expect`), `feed`s whatever slice the socket gave it, cut
|
|
17
|
+
* anywhere, and calls `next()` until it answers `HTTP1_NEED_MORE`:
|
|
18
|
+
*
|
|
19
|
+
* import { HTTP1_NEED_MORE, HTTP1_HEAD, HTTP1_BODY, HTTP1_END, HTTP1_ERROR } from "nish/net/http1";
|
|
20
|
+
* import { Http1ResponseParser } from "nish/net/http1-client";
|
|
21
|
+
*
|
|
22
|
+
* const p = new Http1ResponseParser(maxStatusLine, maxHeaderBytes, maxHeaders, maxBody, maxTrailerBytes);
|
|
23
|
+
* p.expect(false, false);
|
|
24
|
+
* p.feed(received, 0, n); // and p.finish() when the server closes
|
|
25
|
+
* let event: i32 = p.next();
|
|
26
|
+
* while (event !== HTTP1_NEED_MORE) {
|
|
27
|
+
* // HTTP1_HEAD: p.status, p.reason, p.header("content-type"), ...
|
|
28
|
+
* // HTTP1_BODY: p.body[p.bodyOff .. p.bodyOff + p.bodyLen)
|
|
29
|
+
* // HTTP1_ERROR: p.error and p.errorText say why; close
|
|
30
|
+
* event = p.next();
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* The events are `HTTP1_INFORMATIONAL` (an interim 1xx response was read; the
|
|
34
|
+
* final one follows), `HTTP1_HEAD` (the final response's status line and
|
|
35
|
+
* header section are in), `HTTP1_BODY` (some body bytes, de-chunked),
|
|
36
|
+
* `HTTP1_END` (the response is complete; with `keepAlive()` the next one
|
|
37
|
+
* follows), `HTTP1_UPGRADE` (a CONNECT was answered with 2xx and the
|
|
38
|
+
* connection is a tunnel now: `tunnelBytes()` answers what was fed past the
|
|
39
|
+
* head), `HTTP1_CLOSED` (the connection is done: the server closed it
|
|
40
|
+
* between responses, or the last response was not kept alive) and
|
|
41
|
+
* `HTTP1_ERROR`. The last three repeat on every later call. Only complete
|
|
42
|
+
* lines are acted on, body bytes are counted, never guessed, and every
|
|
43
|
+
* refusal is decided by the same byte wherever the input was cut, so the
|
|
44
|
+
* sequence of events, the concatenated body and the error code do not depend
|
|
45
|
+
* on the split —
|
|
46
|
+
* `tests/link/net_http1_client` feeds a corpus at every split point.
|
|
47
|
+
*
|
|
48
|
+
* **How a response ends** (RFC 9112 §6.3): a response to HEAD, a 1xx, 204 or
|
|
49
|
+
* 304 has no body whatever its fields say; a 2xx to CONNECT is a tunnel; a
|
|
50
|
+
* `Transfer-Encoding: chunked` response is de-chunked; a `Content-Length`
|
|
51
|
+
* response is that many bytes; anything else is read until the server closes,
|
|
52
|
+
* which `finish()` reports. A connection closed anywhere else is
|
|
53
|
+
* `HTTP1_E_TRUNCATED`, so a cut-short body is never taken for a whole one.
|
|
54
|
+
*
|
|
55
|
+
* **What it refuses**, each with its own code in `error`: a malformed status
|
|
56
|
+
* line, an HTTP major version other than 1, a bare LF, a malformed field line
|
|
57
|
+
* (an obs-fold, whitespace before the colon, a control byte in a value), a 101
|
|
58
|
+
* (this client never asks to switch protocols), `Transfer-Encoding` together
|
|
59
|
+
* with `Content-Length`, a transfer coding other than one `chunked` or one in
|
|
60
|
+
* an HTTP/1.0 response, a `Content-Length` that is not a decimal or whose
|
|
61
|
+
* repeats disagree, a malformed chunk, a status line, header section, field
|
|
62
|
+
* count, body or trailer section over the caller's limit, a chunk-size line
|
|
63
|
+
* over `HTTP1_MAX_CHUNK_LINE` and more than `HTTP1_MAX_INFORMATIONAL` interim
|
|
64
|
+
* responses. Each of these is a response-smuggling or a resource shape, and
|
|
65
|
+
* each is refused rather than repaired: a client that repairs one differently
|
|
66
|
+
* from a proxy disagrees with it about where the next response starts. An
|
|
67
|
+
* error is final.
|
|
68
|
+
*
|
|
69
|
+
* Private helpers share the importing program's flat symbol namespace
|
|
70
|
+
* (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
|
|
71
|
+
*
|
|
72
|
+
* Written from RFC 9112 and RFC 9110, not ported from another implementation.
|
|
73
|
+
*/
|
|
74
|
+
import { copyBytes } from "nish/bytes"
|
|
75
|
+
import {
|
|
76
|
+
HTTP1_BODY,
|
|
77
|
+
HTTP1_CLOSED,
|
|
78
|
+
HTTP1_END,
|
|
79
|
+
HTTP1_ERROR,
|
|
80
|
+
HTTP1_HEAD,
|
|
81
|
+
HTTP1_NEED_MORE,
|
|
82
|
+
HTTP1_NO_ROOM,
|
|
83
|
+
HTTP1_REFUSED,
|
|
84
|
+
HTTP1_UPGRADE,
|
|
85
|
+
HTTP1_FIRST_INPUT,
|
|
86
|
+
http1HexValue,
|
|
87
|
+
http1IsFieldText,
|
|
88
|
+
http1IsOws,
|
|
89
|
+
http1IsToken,
|
|
90
|
+
http1IsTokenByte,
|
|
91
|
+
http1IsValueByte,
|
|
92
|
+
http1PutText,
|
|
93
|
+
http1SameWord,
|
|
94
|
+
http1Text,
|
|
95
|
+
} from "nish/net/http1"
|
|
96
|
+
|
|
97
|
+
/** `next()`: an interim 1xx response was read; `status` and the fields are its. */
|
|
98
|
+
export const HTTP1_INFORMATIONAL: i32 = 7
|
|
99
|
+
|
|
100
|
+
/** The longest chunk-size line, extension included, the parser reads. */
|
|
101
|
+
export const HTTP1_MAX_CHUNK_LINE: i32 = 4096
|
|
102
|
+
/** The most interim 1xx responses the parser reads before a final one. */
|
|
103
|
+
export const HTTP1_MAX_INFORMATIONAL: i32 = 32
|
|
104
|
+
|
|
105
|
+
/** `error`: nothing is wrong. */
|
|
106
|
+
export const HTTP1_E_NONE: i32 = 0
|
|
107
|
+
/** `error`: the status line is not `HTTP/1.x`, a three-digit status from 100 to 599 and an optional reason. */
|
|
108
|
+
export const HTTP1_E_STATUS_LINE: i32 = 1
|
|
109
|
+
/** `error`: the response's HTTP major version is not 1. */
|
|
110
|
+
export const HTTP1_E_VERSION: i32 = 2
|
|
111
|
+
/** `error`: a line ended in a LF without its CR (RFC 9112 §2.2). */
|
|
112
|
+
export const HTTP1_E_BARE_LF: i32 = 3
|
|
113
|
+
/** `error`: a malformed field line: an obs-fold, a bad name, whitespace before the colon, a control byte. */
|
|
114
|
+
export const HTTP1_E_FIELD: i32 = 4
|
|
115
|
+
/** `error`: a 101, which this client never asks for. */
|
|
116
|
+
export const HTTP1_E_SWITCH: i32 = 5
|
|
117
|
+
/** `error`: both `Transfer-Encoding` and `Content-Length` (RFC 9112 §6.1). */
|
|
118
|
+
export const HTTP1_E_FRAMING: i32 = 6
|
|
119
|
+
/** `error`: a transfer coding list that is not one `chunked`, or one in an HTTP/1.0 response. */
|
|
120
|
+
export const HTTP1_E_CODING: i32 = 7
|
|
121
|
+
/** `error`: a `Content-Length` that is not a run of digits, or values that differ (a value past 2^53 differs from any). */
|
|
122
|
+
export const HTTP1_E_LENGTH: i32 = 8
|
|
123
|
+
/** `error`: a malformed chunk size, chunk extension or chunk terminator (RFC 9112 §7.1). */
|
|
124
|
+
export const HTTP1_E_CHUNK: i32 = 9
|
|
125
|
+
/** `error`: the connection closed before the response ended. */
|
|
126
|
+
export const HTTP1_E_TRUNCATED: i32 = 10
|
|
127
|
+
/** `error`: a status line longer than `maxStatusLine`. */
|
|
128
|
+
export const HTTP1_E_STATUS_LINE_LIMIT: i32 = 11
|
|
129
|
+
/** `error`: a header section longer than `maxHeaderBytes`. */
|
|
130
|
+
export const HTTP1_E_HEADER_LIMIT: i32 = 12
|
|
131
|
+
/** `error`: more fields, trailers included, than `maxHeaders`. */
|
|
132
|
+
export const HTTP1_E_COUNT_LIMIT: i32 = 13
|
|
133
|
+
/** `error`: a declared or a received body longer than `maxBody`. */
|
|
134
|
+
export const HTTP1_E_BODY_LIMIT: i32 = 14
|
|
135
|
+
/** `error`: a trailer section longer than `maxTrailerBytes`. */
|
|
136
|
+
export const HTTP1_E_TRAILER_LIMIT: i32 = 15
|
|
137
|
+
/** `error`: a chunk-size line longer than `HTTP1_MAX_CHUNK_LINE`. */
|
|
138
|
+
export const HTTP1_E_CHUNK_LINE_LIMIT: i32 = 16
|
|
139
|
+
/** `error`: more than `HTTP1_MAX_INFORMATIONAL` interim responses. */
|
|
140
|
+
export const HTTP1_E_INFORMATIONAL_LIMIT: i32 = 17
|
|
141
|
+
|
|
142
|
+
// --- The writer -------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/** How many decimal digits `n`, which is not negative, takes. */
|
|
145
|
+
const http1ClientDigits = (n: i64): i32 => {
|
|
146
|
+
let digits: i32 = 1
|
|
147
|
+
let v: i64 = n
|
|
148
|
+
while (v >= toI64(10)) {
|
|
149
|
+
v = v / toI64(10)
|
|
150
|
+
digits += 1
|
|
151
|
+
}
|
|
152
|
+
return digits
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Writes the decimal digits of `n`, which is not negative, at `out[at ..)`, which holds them, and answers where they end. */
|
|
156
|
+
const http1ClientPutDecimal = (out: u8[], at: i32, n: i64): i32 => {
|
|
157
|
+
const digits: i32 = http1ClientDigits(n)
|
|
158
|
+
let v: i64 = n
|
|
159
|
+
for (let k: i32 = at + digits - 1; k >= at && k >= 0 && k < toI32(out.length); k -= 1) {
|
|
160
|
+
out[k] = toU8(toI32(v % toI64(10)) + 48)
|
|
161
|
+
v = v / toI64(10)
|
|
162
|
+
}
|
|
163
|
+
return at + digits
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Whether `c` may follow the first letter of a URI scheme (RFC 3986 §3.1). */
|
|
167
|
+
const http1ClientIsSchemeByte = (c: i32): boolean =>
|
|
168
|
+
(c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c === 43 || c === 45 || c === 46
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether `target` is a request target `method` may send (RFC 9112 §3.2):
|
|
172
|
+
* authority-form (`host:port`) for CONNECT and for nothing else, `*` for
|
|
173
|
+
* OPTIONS only, and otherwise origin-form (`/path?query`) or absolute-form
|
|
174
|
+
* (`scheme:...`). A `host:port` sent with another method is well-formed
|
|
175
|
+
* absolute-form, the host its scheme, so it is not refused. No form holds a
|
|
176
|
+
* byte at or below SP, DEL, a non-ASCII byte or a `#`: a fragment is never
|
|
177
|
+
* sent, and the others would end the target or the line early.
|
|
178
|
+
*/
|
|
179
|
+
const http1ClientTargetOk = (method: string, target: string): boolean => {
|
|
180
|
+
const n: i32 = toI32(target.length)
|
|
181
|
+
if (n === 0) {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
for (let k: i32 = 0; k < n; k += 1) {
|
|
185
|
+
const c: i32 = toI32(target.charCodeAt(k))
|
|
186
|
+
if (c <= 32 || c >= 127 || c === 35) {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (method === "CONNECT") {
|
|
191
|
+
// uri-host ":" port, with no path, query or userinfo; an IPv6 host is
|
|
192
|
+
// bracketed, so the port follows the last colon.
|
|
193
|
+
let colon: i32 = -1
|
|
194
|
+
for (let k: i32 = 0; k < n; k += 1) {
|
|
195
|
+
const c: i32 = toI32(target.charCodeAt(k))
|
|
196
|
+
if (c === 47 || c === 63 || c === 64) {
|
|
197
|
+
return false
|
|
198
|
+
}
|
|
199
|
+
if (c === 58) {
|
|
200
|
+
colon = k
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (colon < 1 || n - colon - 1 < 1 || n - colon - 1 > 5) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
for (let k: i32 = colon + 1; k < n; k += 1) {
|
|
207
|
+
const c: i32 = toI32(target.charCodeAt(k))
|
|
208
|
+
if (c < 48 || c > 57) {
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
const first: i32 = toI32(target.charCodeAt(0))
|
|
215
|
+
if (first === 42) {
|
|
216
|
+
return n === 1 && method === "OPTIONS"
|
|
217
|
+
}
|
|
218
|
+
if (first === 47) {
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
// absolute-form: a scheme, a letter then scheme bytes, and its colon.
|
|
222
|
+
if (!((first >= 97 && first <= 122) || (first >= 65 && first <= 90))) {
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
for (let k: i32 = 1; k < n; k += 1) {
|
|
226
|
+
const c: i32 = toI32(target.charCodeAt(k))
|
|
227
|
+
if (c === 58) {
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
if (!http1ClientIsSchemeByte(c)) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return false
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Whether `value[k]` is a comma, the list separator of RFC 9110 §5.6.1. */
|
|
238
|
+
const http1ClientIsComma = (value: string, k: i32): boolean =>
|
|
239
|
+
k >= 0 && k < toI32(value.length) && toI32(value.charCodeAt(k)) === 44
|
|
240
|
+
|
|
241
|
+
/** Where `value[a .. b)` starts once its leading OWS is dropped. */
|
|
242
|
+
const http1ClientTrimStart = (value: string, a: i32, b: i32): i32 => {
|
|
243
|
+
let i: i32 = a
|
|
244
|
+
while (i >= 0 && i < b && i < toI32(value.length) && http1IsOws(toI32(value.charCodeAt(i)))) {
|
|
245
|
+
i += 1
|
|
246
|
+
}
|
|
247
|
+
return i
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Where `value[a .. b)` ends once its trailing OWS is dropped. */
|
|
251
|
+
const http1ClientTrimEnd = (value: string, a: i32, b: i32): i32 => {
|
|
252
|
+
let i: i32 = b
|
|
253
|
+
while (i > a && i > 0 && i <= toI32(value.length)) {
|
|
254
|
+
const j: i32 = i - 1
|
|
255
|
+
if (j < 0 || j >= toI32(value.length) || !http1IsOws(toI32(value.charCodeAt(j)))) {
|
|
256
|
+
return i
|
|
257
|
+
}
|
|
258
|
+
i = j
|
|
259
|
+
}
|
|
260
|
+
return i
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Whether the comma-separated list in `value` (RFC 9110 §5.6.1) holds `word`,
|
|
265
|
+
* which is lowercase, in any case, each element trimmed of OWS.
|
|
266
|
+
*/
|
|
267
|
+
const http1ClientListHas = (value: string, word: string): boolean => {
|
|
268
|
+
const n: i32 = toI32(value.length)
|
|
269
|
+
let start: i32 = 0
|
|
270
|
+
for (let k: i32 = 0; k <= n; k += 1) {
|
|
271
|
+
if (k === n || http1ClientIsComma(value, k)) {
|
|
272
|
+
const a: i32 = http1ClientTrimStart(value, start, k)
|
|
273
|
+
const b: i32 = http1ClientTrimEnd(value, a, k)
|
|
274
|
+
if (http1SameWord(value, a, b, word)) {
|
|
275
|
+
return true
|
|
276
|
+
}
|
|
277
|
+
start = k + 1
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The bytes `http1WriteRequestHead` writes for these arguments, counted in 64
|
|
285
|
+
* bits so that no caller's lengths can wrap the sum, or -1 for a head it
|
|
286
|
+
* refuses. Every check is made here, before anything is written.
|
|
287
|
+
*/
|
|
288
|
+
const http1ClientRequestNeed = (
|
|
289
|
+
method: string,
|
|
290
|
+
target: string,
|
|
291
|
+
names: string[],
|
|
292
|
+
values: string[],
|
|
293
|
+
bodyLength: i64
|
|
294
|
+
): i64 => {
|
|
295
|
+
if (!http1IsToken(method) || !http1ClientTargetOk(method, target)) {
|
|
296
|
+
return toI64(-1)
|
|
297
|
+
}
|
|
298
|
+
if (bodyLength < toI64(-2)) {
|
|
299
|
+
return toI64(-1)
|
|
300
|
+
}
|
|
301
|
+
const count: i32 = toI32(names.length)
|
|
302
|
+
if (toI32(values.length) !== count) {
|
|
303
|
+
return toI64(-1)
|
|
304
|
+
}
|
|
305
|
+
// method SP target SP "HTTP/1.1" CRLF, the fields, the framing field and
|
|
306
|
+
// the empty line.
|
|
307
|
+
let need: i64 = toI64(method.length) + toI64(target.length) + toI64(14)
|
|
308
|
+
let hosts: i32 = 0
|
|
309
|
+
for (let i: i32 = 0; i < count && i < toI32(values.length); i += 1) {
|
|
310
|
+
const name: string = names[i]
|
|
311
|
+
const value: string = values[i]
|
|
312
|
+
const nameLength: i32 = toI32(name.length)
|
|
313
|
+
if (!http1IsToken(name) || !http1IsFieldText(value)) {
|
|
314
|
+
return toI64(-1)
|
|
315
|
+
}
|
|
316
|
+
if (
|
|
317
|
+
http1SameWord(name, 0, nameLength, "content-length") ||
|
|
318
|
+
http1SameWord(name, 0, nameLength, "transfer-encoding")
|
|
319
|
+
) {
|
|
320
|
+
return toI64(-1)
|
|
321
|
+
}
|
|
322
|
+
if (http1SameWord(name, 0, nameLength, "connection") && http1ClientListHas(value, "upgrade")) {
|
|
323
|
+
return toI64(-1)
|
|
324
|
+
}
|
|
325
|
+
if (http1SameWord(name, 0, nameLength, "host")) {
|
|
326
|
+
hosts += 1
|
|
327
|
+
}
|
|
328
|
+
need = need + toI64(nameLength) + toI64(value.length) + toI64(4)
|
|
329
|
+
}
|
|
330
|
+
if (hosts !== 1) {
|
|
331
|
+
return toI64(-1)
|
|
332
|
+
}
|
|
333
|
+
if (bodyLength >= toI64(0)) {
|
|
334
|
+
need = need + toI64(18 + http1ClientDigits(bodyLength))
|
|
335
|
+
} else if (bodyLength === toI64(-1)) {
|
|
336
|
+
need = need + toI64(28)
|
|
337
|
+
}
|
|
338
|
+
return need
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Writes field `i` at `out[at ..)`, which holds it, and answers where it ends. */
|
|
342
|
+
const http1ClientPutField = (out: u8[], at: i32, names: string[], values: string[], i: i32): i32 => {
|
|
343
|
+
if (i < 0 || i >= toI32(names.length) || i >= toI32(values.length)) {
|
|
344
|
+
return at
|
|
345
|
+
}
|
|
346
|
+
let end: i32 = http1PutText(out, at, names[i])
|
|
347
|
+
end = http1PutText(out, end, ": ")
|
|
348
|
+
end = http1PutText(out, end, values[i])
|
|
349
|
+
return http1PutText(out, end, "\r\n")
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* An HTTP/1.1 request line and header section (RFC 9112 §3, §5), ending in
|
|
354
|
+
* the empty line, written into `out` at `at` with nothing allocated, and
|
|
355
|
+
* answers where it ends. `Host` goes first, as RFC 9110 §7.2 asks, then the
|
|
356
|
+
* caller's other fields in order, then the framing field `bodyLength` asks
|
|
357
|
+
* for: `Content-Length: n` for `n >= 0`, `Transfer-Encoding: chunked` for -1
|
|
358
|
+
* (`HTTP1_CHUNKED`), and neither for -2 (`HTTP1_NO_BODY`).
|
|
359
|
+
*
|
|
360
|
+
* Answers `HTTP1_REFUSED` for a method that is not a token; a target that is
|
|
361
|
+
* not a form the method may send (see the guide) or that holds a space, a
|
|
362
|
+
* control byte, a non-ASCII byte or a `#`; a field name that is not a token or
|
|
363
|
+
* a value holding CR, LF or another control byte (which would end the line
|
|
364
|
+
* early and let the rest be read as a field or a second request); `names` and
|
|
365
|
+
* `values` of different lengths; not exactly one `Host`; a `Content-Length` or
|
|
366
|
+
* `Transfer-Encoding` among the caller's fields, since the framing is decided
|
|
367
|
+
* here; a `Connection` that lists `upgrade`, which the parser refuses the
|
|
368
|
+
* answer to; and a `bodyLength` below -2. Answers `HTTP1_NO_ROOM` when it is
|
|
369
|
+
* well formed but does not fit. Either way `out` is untouched.
|
|
370
|
+
*/
|
|
371
|
+
export const http1WriteRequestHead = (
|
|
372
|
+
out: u8[],
|
|
373
|
+
at: i32,
|
|
374
|
+
method: string,
|
|
375
|
+
target: string,
|
|
376
|
+
names: string[],
|
|
377
|
+
values: string[],
|
|
378
|
+
bodyLength: i64
|
|
379
|
+
): i32 => {
|
|
380
|
+
const need: i64 = http1ClientRequestNeed(method, target, names, values, bodyLength)
|
|
381
|
+
if (need < toI64(0)) {
|
|
382
|
+
return HTTP1_REFUSED
|
|
383
|
+
}
|
|
384
|
+
if (at < 0 || need > toI64(toI32(out.length) - at)) {
|
|
385
|
+
return HTTP1_NO_ROOM
|
|
386
|
+
}
|
|
387
|
+
return http1ClientPutHead(out, at, method, target, names, values, bodyLength)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Writes a head `http1ClientRequestNeed` accepted into room it counted; answers the end. */
|
|
391
|
+
const http1ClientPutHead = (
|
|
392
|
+
out: u8[],
|
|
393
|
+
at: i32,
|
|
394
|
+
method: string,
|
|
395
|
+
target: string,
|
|
396
|
+
names: string[],
|
|
397
|
+
values: string[],
|
|
398
|
+
bodyLength: i64
|
|
399
|
+
): i32 => {
|
|
400
|
+
let end: i32 = http1PutText(out, at, method)
|
|
401
|
+
end = http1PutText(out, end, " ")
|
|
402
|
+
end = http1PutText(out, end, target)
|
|
403
|
+
end = http1PutText(out, end, " HTTP/1.1\r\n")
|
|
404
|
+
const count: i32 = toI32(names.length)
|
|
405
|
+
for (let i: i32 = 0; i < count; i += 1) {
|
|
406
|
+
if (http1SameWord(names[i], 0, toI32(names[i].length), "host")) {
|
|
407
|
+
end = http1ClientPutField(out, end, names, values, i)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
for (let i: i32 = 0; i < count; i += 1) {
|
|
411
|
+
if (!http1SameWord(names[i], 0, toI32(names[i].length), "host")) {
|
|
412
|
+
end = http1ClientPutField(out, end, names, values, i)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (bodyLength >= toI64(0)) {
|
|
416
|
+
end = http1PutText(out, end, "Content-Length: ")
|
|
417
|
+
end = http1ClientPutDecimal(out, end, bodyLength)
|
|
418
|
+
end = http1PutText(out, end, "\r\n")
|
|
419
|
+
} else if (bodyLength === toI64(-1)) {
|
|
420
|
+
end = http1PutText(out, end, "Transfer-Encoding: chunked\r\n")
|
|
421
|
+
}
|
|
422
|
+
return http1PutText(out, end, "\r\n")
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* `http1WriteRequestHead`'s head in an array of its own, or `null` for
|
|
427
|
+
* everything it refuses.
|
|
428
|
+
*/
|
|
429
|
+
export const http1RequestHead = (
|
|
430
|
+
method: string,
|
|
431
|
+
target: string,
|
|
432
|
+
names: string[],
|
|
433
|
+
values: string[],
|
|
434
|
+
bodyLength: i64
|
|
435
|
+
): u8[] | null => {
|
|
436
|
+
const need: i64 = http1ClientRequestNeed(method, target, names, values, bodyLength)
|
|
437
|
+
if (need < toI64(0) || need > toI64(1073741824)) {
|
|
438
|
+
return null
|
|
439
|
+
}
|
|
440
|
+
const out: u8[] = new Array<u8>(toI32(need))
|
|
441
|
+
http1ClientPutHead(out, 0, method, target, names, values, bodyLength)
|
|
442
|
+
return out
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// --- The parser -------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
// The parser's states, one per kind of thing it is waiting for.
|
|
448
|
+
const HTTP1_CLIENT_S_STATUS_LINE: i32 = 0
|
|
449
|
+
const HTTP1_CLIENT_S_FIELDS: i32 = 1
|
|
450
|
+
const HTTP1_CLIENT_S_LENGTH_BODY: i32 = 2
|
|
451
|
+
const HTTP1_CLIENT_S_CLOSE_BODY: i32 = 3
|
|
452
|
+
const HTTP1_CLIENT_S_CHUNK_SIZE: i32 = 4
|
|
453
|
+
const HTTP1_CLIENT_S_CHUNK_DATA: i32 = 5
|
|
454
|
+
const HTTP1_CLIENT_S_CHUNK_CRLF: i32 = 6
|
|
455
|
+
const HTTP1_CLIENT_S_TRAILERS: i32 = 7
|
|
456
|
+
const HTTP1_CLIENT_S_MESSAGE_END: i32 = 8
|
|
457
|
+
const HTTP1_CLIENT_S_TUNNEL: i32 = 9
|
|
458
|
+
const HTTP1_CLIENT_S_CLOSED: i32 = 10
|
|
459
|
+
const HTTP1_CLIENT_S_ERROR: i32 = 11
|
|
460
|
+
|
|
461
|
+
/** `http1ClientTakeLine`'s answer when a whole line is ready; never an event. */
|
|
462
|
+
const HTTP1_CLIENT_LINE: i32 = -1
|
|
463
|
+
|
|
464
|
+
/** The largest `maxBody`: 2^53, so that sums of it cannot overflow an `i64`. */
|
|
465
|
+
const HTTP1_CLIENT_MAX_BODY: i64 = 9007199254740992
|
|
466
|
+
|
|
467
|
+
/** `limit` clamped into 0 to 2^30. */
|
|
468
|
+
const http1ClientClamp = (limit: i32): i32 => {
|
|
469
|
+
if (limit < 0) {
|
|
470
|
+
return 0
|
|
471
|
+
}
|
|
472
|
+
return limit > 1073741824 ? 1073741824 : limit
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* One connection's response parser: responses on a kept-alive connection
|
|
477
|
+
* follow one another through the same parser, each after its `expect`.
|
|
478
|
+
*/
|
|
479
|
+
export class Http1ResponseParser {
|
|
480
|
+
// --- the response: valid from its HEAD or INFORMATIONAL until the next ----
|
|
481
|
+
/** The reason phrase, which RFC 9112 §4 says a client ignores; for a log. */
|
|
482
|
+
reason: string = ""
|
|
483
|
+
/** The field names, in lowercase, in the order they arrived; `values` beside them, OWS trimmed. */
|
|
484
|
+
names: string[]
|
|
485
|
+
values: string[]
|
|
486
|
+
/** The buffer the last `HTTP1_BODY`'s bytes are in, at `bodyOff`, `bodyLen` long. */
|
|
487
|
+
body: u8[]
|
|
488
|
+
/** What was wrong, once `next()` said `HTTP1_ERROR`; for a log. */
|
|
489
|
+
errorText: string = ""
|
|
490
|
+
/** The bytes fed and not yet consumed are `buf[start .. end)`. */
|
|
491
|
+
buf: u8[]
|
|
492
|
+
|
|
493
|
+
// --- limits, the caller's --------------------------------------------------
|
|
494
|
+
maxBody: i64 = 0
|
|
495
|
+
/** The declared body length, or -1 when the response has none or is chunked or read until close. */
|
|
496
|
+
contentLength: i64 = -1
|
|
497
|
+
/** Bytes left in a length-delimited body or in the current chunk. */
|
|
498
|
+
left: i64 = 0
|
|
499
|
+
/** De-chunked body bytes so far, against `maxBody`. */
|
|
500
|
+
bodyTotal: i64 = 0
|
|
501
|
+
maxStatusLine: i32 = 0
|
|
502
|
+
maxHeaderBytes: i32 = 0
|
|
503
|
+
maxHeaders: i32 = 0
|
|
504
|
+
maxTrailerBytes: i32 = 0
|
|
505
|
+
|
|
506
|
+
/** The status code of the response last read. */
|
|
507
|
+
status: i32 = 0
|
|
508
|
+
/** The minor version: 0 for HTTP/1.0, 1 for HTTP/1.1 and anything above it. */
|
|
509
|
+
minor: i32 = 1
|
|
510
|
+
bodyOff: i32 = 0
|
|
511
|
+
bodyLen: i32 = 0
|
|
512
|
+
/** Why `next()` said `HTTP1_ERROR`: one of the `HTTP1_E_` codes. */
|
|
513
|
+
error: i32 = 0
|
|
514
|
+
|
|
515
|
+
// --- internal --------------------------------------------------------------
|
|
516
|
+
start: i32 = 0
|
|
517
|
+
end: i32 = 0
|
|
518
|
+
/** Where the search for the next LF resumes, so a line fed a byte at a time is scanned once. */
|
|
519
|
+
scan: i32 = 0
|
|
520
|
+
state: i32 = 0
|
|
521
|
+
/** Bytes of the header or trailer section so far, against its limit. */
|
|
522
|
+
sectionBytes: i32 = 0
|
|
523
|
+
/** Field lines of the response so far, its trailers included, against `maxHeaders`. */
|
|
524
|
+
fieldCount: i32 = 0
|
|
525
|
+
/** Interim responses before this request's final one, against `HTTP1_MAX_INFORMATIONAL`. */
|
|
526
|
+
informational: i32 = 0
|
|
527
|
+
/** The line `http1ClientTakeLine` found is `buf[lineStart .. lineEnd)`, its CRLF dropped. */
|
|
528
|
+
lineStart: i32 = 0
|
|
529
|
+
lineEnd: i32 = 0
|
|
530
|
+
|
|
531
|
+
chunked: boolean = false
|
|
532
|
+
persistent: boolean = false
|
|
533
|
+
/** What `expect` said of the request this response answers. */
|
|
534
|
+
wasHead: boolean = false
|
|
535
|
+
wasConnect: boolean = false
|
|
536
|
+
/** Whether `finish()` said the server closed the connection. */
|
|
537
|
+
closed: boolean = false
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Each `i32` limit is clamped into 0 to 2^30 and `maxBody` into 0 to 2^53,
|
|
541
|
+
* so that the sums the parser makes of them cannot overflow.
|
|
542
|
+
*/
|
|
543
|
+
constructor(maxStatusLine: i32, maxHeaderBytes: i32, maxHeaders: i32, maxBody: i64, maxTrailerBytes: i32) {
|
|
544
|
+
this.maxStatusLine = http1ClientClamp(maxStatusLine)
|
|
545
|
+
this.maxHeaderBytes = http1ClientClamp(maxHeaderBytes)
|
|
546
|
+
this.maxHeaders = http1ClientClamp(maxHeaders)
|
|
547
|
+
this.maxTrailerBytes = http1ClientClamp(maxTrailerBytes)
|
|
548
|
+
let body: i64 = maxBody
|
|
549
|
+
if (body < toI64(0)) {
|
|
550
|
+
body = toI64(0)
|
|
551
|
+
}
|
|
552
|
+
this.maxBody = body > HTTP1_CLIENT_MAX_BODY ? HTTP1_CLIENT_MAX_BODY : body
|
|
553
|
+
this.buf = []
|
|
554
|
+
this.body = this.buf
|
|
555
|
+
this.names = []
|
|
556
|
+
this.values = []
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Says what the request this response answers was: a response to HEAD has
|
|
561
|
+
* no body, and a 2xx to CONNECT is a tunnel. It takes effect only while the
|
|
562
|
+
* parser waits for a response's first status line, and the parser forgets it
|
|
563
|
+
* after that response's `HTTP1_END`; anywhere else it does nothing, so a
|
|
564
|
+
* pipelining caller says it once per response, after the last one's end.
|
|
565
|
+
*/
|
|
566
|
+
expect(methodWasHead: boolean, wasConnect: boolean): void {
|
|
567
|
+
if (this.state === HTTP1_CLIENT_S_STATUS_LINE && this.informational === 0) {
|
|
568
|
+
this.wasHead = methodWasHead
|
|
569
|
+
this.wasConnect = wasConnect
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Appends `data[off .. off + len)` to the input. A window outside `data`
|
|
575
|
+
* panics. After `finish()`, or once the parser has closed or refused, the
|
|
576
|
+
* input is dropped; in a tunnel it is kept for `tunnelBytes()`.
|
|
577
|
+
*/
|
|
578
|
+
feed(data: u8[], off: i32, len: i32): void {
|
|
579
|
+
const size: i32 = toI32(data.length)
|
|
580
|
+
if (off < 0 || len < 0 || off > size || len > size - off) {
|
|
581
|
+
panic("Http1ResponseParser: the window is outside the buffer")
|
|
582
|
+
}
|
|
583
|
+
if (this.closed || this.state === HTTP1_CLIENT_S_CLOSED || this.state === HTTP1_CLIENT_S_ERROR) {
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
http1ClientReserve(this, len)
|
|
587
|
+
const at: i32 = this.end
|
|
588
|
+
copyBytes(this.buf, at, data, off, len)
|
|
589
|
+
this.end = at + len
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Says the server closed the connection: a body read until close ends
|
|
594
|
+
* there, a parser waiting for a response with nothing fed is closed, and
|
|
595
|
+
* anything else is `HTTP1_E_TRUNCATED` once the bytes fed before it are read.
|
|
596
|
+
*/
|
|
597
|
+
finish(): void {
|
|
598
|
+
this.closed = true
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** The next event the input holds; see the module comment. */
|
|
602
|
+
next(): i32 {
|
|
603
|
+
const event: i32 = http1ClientNext(this)
|
|
604
|
+
if (event !== HTTP1_NEED_MORE || !this.closed) {
|
|
605
|
+
return event
|
|
606
|
+
}
|
|
607
|
+
if (this.state === HTTP1_CLIENT_S_CLOSE_BODY) {
|
|
608
|
+
this.state = HTTP1_CLIENT_S_MESSAGE_END
|
|
609
|
+
return http1ClientNext(this)
|
|
610
|
+
}
|
|
611
|
+
if (this.state === HTTP1_CLIENT_S_STATUS_LINE && this.informational === 0 && this.end === this.start) {
|
|
612
|
+
return HTTP1_CLOSED
|
|
613
|
+
}
|
|
614
|
+
return http1ClientFail(this, HTTP1_E_TRUNCATED, "the connection closed before the response ended")
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Whether the connection may carry the next request once this response has
|
|
619
|
+
* ended (RFC 9112 §9.3): a 1.1 response unless `Connection` lists `close`,
|
|
620
|
+
* a 1.0 response only when it lists `keep-alive`, and never one read until
|
|
621
|
+
* close, a tunnel or an error.
|
|
622
|
+
*/
|
|
623
|
+
keepAlive(): boolean {
|
|
624
|
+
return this.persistent
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** The value of the first field named `name` (any case), or `null`. */
|
|
628
|
+
header(name: string): string | null {
|
|
629
|
+
const names: string[] = this.names
|
|
630
|
+
const n: i32 = toI32(name.length)
|
|
631
|
+
for (let i: i32 = 0; i < toI32(names.length) && i < toI32(this.values.length); i += 1) {
|
|
632
|
+
if (http1SameWord(name, 0, n, names[i])) {
|
|
633
|
+
return this.values[i]
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return null
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** How many bytes were fed and not consumed yet. */
|
|
640
|
+
buffered(): i32 {
|
|
641
|
+
return this.end - this.start
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** In a tunnel: every byte fed past the response head, as a fresh array. */
|
|
645
|
+
tunnelBytes(): u8[] {
|
|
646
|
+
let n: i32 = this.end - this.start
|
|
647
|
+
if (n < 0 || this.state !== HTTP1_CLIENT_S_TUNNEL) {
|
|
648
|
+
n = 0
|
|
649
|
+
}
|
|
650
|
+
const out: u8[] = new Array<u8>(n)
|
|
651
|
+
if (n > 0) {
|
|
652
|
+
copyBytes(out, 0, this.buf, this.start, n)
|
|
653
|
+
}
|
|
654
|
+
return out
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** Makes room for `len` more bytes at `p.end`, moving the live bytes down first. */
|
|
659
|
+
const http1ClientReserve = (p: Http1ResponseParser, len: i32): void => {
|
|
660
|
+
const old: u8[] = p.buf
|
|
661
|
+
const capacity: i32 = toI32(old.length)
|
|
662
|
+
if (len <= capacity - p.end) {
|
|
663
|
+
return
|
|
664
|
+
}
|
|
665
|
+
const live: i32 = p.end - p.start
|
|
666
|
+
let size: i32 = capacity < HTTP1_FIRST_INPUT ? HTTP1_FIRST_INPUT : capacity
|
|
667
|
+
while (size - live < len) {
|
|
668
|
+
if (size > 1073741823) {
|
|
669
|
+
panic("Http1ResponseParser: more than 2^30 bytes buffered")
|
|
670
|
+
}
|
|
671
|
+
size = size * 2
|
|
672
|
+
}
|
|
673
|
+
// A fresh array when it has to grow, and the same one otherwise: either way
|
|
674
|
+
// the live bytes move to the front, and every index into them moves too.
|
|
675
|
+
const buf: u8[] = size === capacity ? old : new Array<u8>(size)
|
|
676
|
+
const from: i32 = p.start
|
|
677
|
+
if (live > 0) {
|
|
678
|
+
copyBytes(buf, 0, old, from, live)
|
|
679
|
+
}
|
|
680
|
+
p.buf = buf
|
|
681
|
+
p.scan -= from
|
|
682
|
+
p.start = 0
|
|
683
|
+
p.end = live
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Refuses the input with `code` and drops what is buffered: nothing after a
|
|
688
|
+
* refusal is read. Answers `HTTP1_ERROR`, so a caller can `return` it.
|
|
689
|
+
*/
|
|
690
|
+
const http1ClientFail = (p: Http1ResponseParser, code: i32, text: string): i32 => {
|
|
691
|
+
p.state = HTTP1_CLIENT_S_ERROR
|
|
692
|
+
p.start = p.end
|
|
693
|
+
p.error = code
|
|
694
|
+
p.errorText = text
|
|
695
|
+
p.persistent = false
|
|
696
|
+
return HTTP1_ERROR
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Clears the per-response fields for the status line just taken. */
|
|
700
|
+
const http1ClientStartHead = (p: Http1ResponseParser): void => {
|
|
701
|
+
p.status = 0
|
|
702
|
+
p.reason = ""
|
|
703
|
+
p.minor = 1
|
|
704
|
+
p.names = []
|
|
705
|
+
p.values = []
|
|
706
|
+
p.contentLength = toI64(-1)
|
|
707
|
+
p.chunked = false
|
|
708
|
+
p.persistent = false
|
|
709
|
+
p.bodyLen = 0
|
|
710
|
+
p.bodyTotal = toI64(0)
|
|
711
|
+
p.sectionBytes = 0
|
|
712
|
+
p.fieldCount = 0
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Finds the next line: answers `HTTP1_CLIENT_LINE` with it in `lineStart ..
|
|
717
|
+
* lineEnd`, `HTTP1_NEED_MORE` when no LF has arrived yet, or the refusal. A
|
|
718
|
+
* line ends in CRLF and in nothing else. A line longer than `limit` bytes is
|
|
719
|
+
* refused with `overCode`, and that is decided before the bare-LF check, so
|
|
720
|
+
* that a long line ending in a bare LF is refused with the same code whether
|
|
721
|
+
* its LF arrived with it or after the limit was already passed.
|
|
722
|
+
*/
|
|
723
|
+
const http1ClientTakeLine = (p: Http1ResponseParser, limit: i32, overCode: i32, overText: string): i32 => {
|
|
724
|
+
const buf: u8[] = p.buf
|
|
725
|
+
const end: i32 = p.end
|
|
726
|
+
let i: i32 = p.scan > p.start ? p.scan : p.start
|
|
727
|
+
while (i >= 0 && i < end && i < toI32(buf.length)) {
|
|
728
|
+
if (buf[i] === 10) {
|
|
729
|
+
if (i - p.start > limit + 1) {
|
|
730
|
+
return http1ClientFail(p, overCode, overText)
|
|
731
|
+
}
|
|
732
|
+
if (i === p.start || buf[i - 1] !== 13) {
|
|
733
|
+
return http1ClientFail(p, HTTP1_E_BARE_LF, "a line ended in a bare LF")
|
|
734
|
+
}
|
|
735
|
+
p.lineStart = p.start
|
|
736
|
+
p.lineEnd = i - 1
|
|
737
|
+
p.start = i + 1
|
|
738
|
+
p.scan = i + 1
|
|
739
|
+
return HTTP1_CLIENT_LINE
|
|
740
|
+
}
|
|
741
|
+
i += 1
|
|
742
|
+
}
|
|
743
|
+
p.scan = end
|
|
744
|
+
if (end - p.start > limit + 1) {
|
|
745
|
+
return http1ClientFail(p, overCode, overText)
|
|
746
|
+
}
|
|
747
|
+
return HTTP1_NEED_MORE
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Whether `buf[i]` is an ASCII digit. */
|
|
751
|
+
const http1ClientDigitAt = (buf: u8[], i: i32): boolean =>
|
|
752
|
+
i >= 0 && i < toI32(buf.length) && buf[i] >= 48 && buf[i] <= 57
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Reads the status line in `lineStart .. lineEnd` (RFC 9112 §4):
|
|
756
|
+
* `HTTP/` DIGIT `.` DIGIT SP 3DIGIT SP reason-phrase. A missing reason and its
|
|
757
|
+
* space are taken as an empty reason, since the reason means nothing.
|
|
758
|
+
*/
|
|
759
|
+
const http1ClientStatusLine = (p: Http1ResponseParser): i32 => {
|
|
760
|
+
const buf: u8[] = p.buf
|
|
761
|
+
const a: i32 = p.lineStart
|
|
762
|
+
const b: i32 = p.lineEnd < toI32(buf.length) ? p.lineEnd : toI32(buf.length)
|
|
763
|
+
if (
|
|
764
|
+
b - a < 12 ||
|
|
765
|
+
a < 0 ||
|
|
766
|
+
buf[a] !== 72 ||
|
|
767
|
+
buf[a + 1] !== 84 ||
|
|
768
|
+
buf[a + 2] !== 84 ||
|
|
769
|
+
buf[a + 3] !== 80 ||
|
|
770
|
+
buf[a + 4] !== 47 ||
|
|
771
|
+
!http1ClientDigitAt(buf, a + 5) ||
|
|
772
|
+
buf[a + 6] !== 46 ||
|
|
773
|
+
!http1ClientDigitAt(buf, a + 7) ||
|
|
774
|
+
buf[a + 8] !== 32 ||
|
|
775
|
+
!http1ClientDigitAt(buf, a + 9) ||
|
|
776
|
+
!http1ClientDigitAt(buf, a + 10) ||
|
|
777
|
+
!http1ClientDigitAt(buf, a + 11) ||
|
|
778
|
+
(b - a > 12 && buf[a + 12] !== 32)
|
|
779
|
+
) {
|
|
780
|
+
return http1ClientFail(p, HTTP1_E_STATUS_LINE, "a malformed status line")
|
|
781
|
+
}
|
|
782
|
+
if (buf[a + 5] !== 49) {
|
|
783
|
+
return http1ClientFail(p, HTTP1_E_VERSION, "an HTTP major version other than 1")
|
|
784
|
+
}
|
|
785
|
+
const status: i32 =
|
|
786
|
+
(toI32(buf[a + 9]) - 48) * 100 + (toI32(buf[a + 10]) - 48) * 10 + (toI32(buf[a + 11]) - 48)
|
|
787
|
+
if (status < 100 || status > 599) {
|
|
788
|
+
return http1ClientFail(p, HTTP1_E_STATUS_LINE, "a status code outside 100 to 599")
|
|
789
|
+
}
|
|
790
|
+
for (let k: i32 = a + 13; k < b && k < toI32(buf.length); k += 1) {
|
|
791
|
+
if (!http1IsValueByte(toI32(buf[k]))) {
|
|
792
|
+
return http1ClientFail(p, HTTP1_E_STATUS_LINE, "a control character in the reason phrase")
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
p.minor = buf[a + 7] === 48 ? 0 : 1
|
|
796
|
+
p.status = status
|
|
797
|
+
p.reason = b - a > 13 ? http1Text(buf, a + 13, b, false) : ""
|
|
798
|
+
p.state = HTTP1_CLIENT_S_FIELDS
|
|
799
|
+
return HTTP1_NEED_MORE
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Reads one field line in `lineStart .. lineEnd` (RFC 9112 §5) into `names`
|
|
804
|
+
* and `values`, or, for a trailer, only checks it. Answers `HTTP1_NEED_MORE`
|
|
805
|
+
* or the refusal.
|
|
806
|
+
*/
|
|
807
|
+
const http1ClientFieldLine = (p: Http1ResponseParser, keep: boolean): i32 => {
|
|
808
|
+
const buf: u8[] = p.buf
|
|
809
|
+
const a: i32 = p.lineStart
|
|
810
|
+
const b: i32 = p.lineEnd < toI32(buf.length) ? p.lineEnd : toI32(buf.length)
|
|
811
|
+
if (a >= 0 && a < b && http1IsOws(toI32(buf[a]))) {
|
|
812
|
+
return http1ClientFail(p, HTTP1_E_FIELD, "a field line that starts with whitespace (obs-fold)")
|
|
813
|
+
}
|
|
814
|
+
let i: i32 = a
|
|
815
|
+
while (i >= 0 && i < b && i < toI32(buf.length) && http1IsTokenByte(toI32(buf[i]))) {
|
|
816
|
+
i += 1
|
|
817
|
+
}
|
|
818
|
+
if (i === a || i >= b || buf[i] !== 58) {
|
|
819
|
+
return http1ClientFail(p, HTTP1_E_FIELD, "a malformed field name")
|
|
820
|
+
}
|
|
821
|
+
const nameEnd: i32 = i
|
|
822
|
+
for (let k: i32 = nameEnd + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
|
|
823
|
+
if (!http1IsValueByte(toI32(buf[k]))) {
|
|
824
|
+
return http1ClientFail(p, HTTP1_E_FIELD, "a control character in a field value")
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
p.fieldCount += 1
|
|
828
|
+
if (p.fieldCount > p.maxHeaders) {
|
|
829
|
+
return http1ClientFail(p, HTTP1_E_COUNT_LIMIT, "more fields than the limit")
|
|
830
|
+
}
|
|
831
|
+
// A trailer is checked and counted, but not kept: nothing here reads one.
|
|
832
|
+
if (keep) {
|
|
833
|
+
let valueStart: i32 = nameEnd + 1
|
|
834
|
+
let valueEnd: i32 = b
|
|
835
|
+
while (
|
|
836
|
+
valueStart >= 0 &&
|
|
837
|
+
valueStart < valueEnd &&
|
|
838
|
+
valueStart < toI32(buf.length) &&
|
|
839
|
+
http1IsOws(toI32(buf[valueStart]))
|
|
840
|
+
) {
|
|
841
|
+
valueStart += 1
|
|
842
|
+
}
|
|
843
|
+
while (
|
|
844
|
+
valueEnd > valueStart &&
|
|
845
|
+
valueEnd - 1 < toI32(buf.length) &&
|
|
846
|
+
http1IsOws(toI32(buf[valueEnd - 1]))
|
|
847
|
+
) {
|
|
848
|
+
valueEnd -= 1
|
|
849
|
+
}
|
|
850
|
+
p.names.push(http1Text(buf, a, nameEnd, true))
|
|
851
|
+
p.values.push(http1Text(buf, valueStart, valueEnd, false))
|
|
852
|
+
}
|
|
853
|
+
return HTTP1_NEED_MORE
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Reads one `Content-Length` value: a comma-separated list (RFC 9110 §8.6
|
|
858
|
+
* lets a recipient take a list of one repeated value as that value) whose
|
|
859
|
+
* elements are each a run of digits and all the same as each other and as
|
|
860
|
+
* `seen`, the value of an earlier field (-1 for none). Answers the value, -1
|
|
861
|
+
* when an element is empty or not digits, or -2 when two values differ. A
|
|
862
|
+
* value past 2^53 is saturated to 2^53 + 1: over any `maxBody`. Saturated
|
|
863
|
+
* values cannot be told apart, so one seen with any other value is -2.
|
|
864
|
+
*/
|
|
865
|
+
const http1ClientLengthList = (value: string, seen: i64): i64 => {
|
|
866
|
+
const n: i32 = toI32(value.length)
|
|
867
|
+
let found: i64 = seen
|
|
868
|
+
let start: i32 = 0
|
|
869
|
+
for (let k: i32 = 0; k <= n; k += 1) {
|
|
870
|
+
if (k === n || http1ClientIsComma(value, k)) {
|
|
871
|
+
const a: i32 = http1ClientTrimStart(value, start, k)
|
|
872
|
+
const b: i32 = http1ClientTrimEnd(value, a, k)
|
|
873
|
+
if (a === b) {
|
|
874
|
+
return toI64(-1)
|
|
875
|
+
}
|
|
876
|
+
let v: i64 = toI64(0)
|
|
877
|
+
for (let j: i32 = a; j >= 0 && j < b && j < n; j += 1) {
|
|
878
|
+
const c: i32 = toI32(value.charCodeAt(j))
|
|
879
|
+
if (c < 48 || c > 57) {
|
|
880
|
+
return toI64(-1)
|
|
881
|
+
}
|
|
882
|
+
if (v <= (HTTP1_CLIENT_MAX_BODY - toI64(c - 48)) / toI64(10)) {
|
|
883
|
+
v = v * toI64(10) + toI64(c - 48)
|
|
884
|
+
} else {
|
|
885
|
+
v = HTTP1_CLIENT_MAX_BODY + toI64(1)
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (found >= toI64(0) && (found !== v || v > HTTP1_CLIENT_MAX_BODY)) {
|
|
889
|
+
return toI64(-2)
|
|
890
|
+
}
|
|
891
|
+
found = v
|
|
892
|
+
start = k + 1
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return found
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* The fields of `name` together as one comma-separated list (RFC 9110 §5.3),
|
|
900
|
+
* tallied for `word`: answers the element count times 4, plus 2 when `word`
|
|
901
|
+
* appears more than once and 1 when it is the last element.
|
|
902
|
+
*/
|
|
903
|
+
const http1ClientTally = (p: Http1ResponseParser, name: string, word: string): i32 => {
|
|
904
|
+
let elements: i32 = 0
|
|
905
|
+
let matches: i32 = 0
|
|
906
|
+
let last: boolean = false
|
|
907
|
+
const names: string[] = p.names
|
|
908
|
+
const values: string[] = p.values
|
|
909
|
+
for (let i: i32 = 0; i < toI32(names.length) && i < toI32(values.length); i += 1) {
|
|
910
|
+
if (names[i] === name) {
|
|
911
|
+
const value: string = values[i]
|
|
912
|
+
const n: i32 = toI32(value.length)
|
|
913
|
+
let start: i32 = 0
|
|
914
|
+
for (let k: i32 = 0; k <= n; k += 1) {
|
|
915
|
+
if (k === n || http1ClientIsComma(value, k)) {
|
|
916
|
+
const a: i32 = http1ClientTrimStart(value, start, k)
|
|
917
|
+
const b: i32 = http1ClientTrimEnd(value, a, k)
|
|
918
|
+
if (b > a) {
|
|
919
|
+
elements += 1
|
|
920
|
+
last = http1SameWord(value, a, b, word)
|
|
921
|
+
if (last) {
|
|
922
|
+
matches += 1
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
start = k + 1
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return elements * 4 + (matches > 1 ? 2 : 0) + (last ? 1 : 0)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/** Whether a `Connection` field of the response lists `word`. */
|
|
934
|
+
const http1ClientConnectionHas = (p: Http1ResponseParser, word: string): boolean => {
|
|
935
|
+
const names: string[] = p.names
|
|
936
|
+
for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
|
|
937
|
+
if (names[i] === "connection" && http1ClientListHas(p.values[i], word)) {
|
|
938
|
+
return true
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return false
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* A header section has ended: answer `HTTP1_INFORMATIONAL` for a 1xx, or
|
|
946
|
+
* decide the final response's framing and the connection's fate (RFC 9112
|
|
947
|
+
* §6.3, §9.3) and answer `HTTP1_HEAD`, or the refusal.
|
|
948
|
+
*/
|
|
949
|
+
const http1ClientEndOfHead = (p: Http1ResponseParser): i32 => {
|
|
950
|
+
const status: i32 = p.status
|
|
951
|
+
if (status === 101) {
|
|
952
|
+
return http1ClientFail(p, HTTP1_E_SWITCH, "a 101 Switching Protocols this client never asks for")
|
|
953
|
+
}
|
|
954
|
+
if (status < 200) {
|
|
955
|
+
if (p.informational >= HTTP1_MAX_INFORMATIONAL) {
|
|
956
|
+
return http1ClientFail(p, HTTP1_E_INFORMATIONAL_LIMIT, "more interim responses than the limit")
|
|
957
|
+
}
|
|
958
|
+
// The 1xx's status and fields stay readable until the next status line.
|
|
959
|
+
p.informational += 1
|
|
960
|
+
p.state = HTTP1_CLIENT_S_STATUS_LINE
|
|
961
|
+
return HTTP1_INFORMATIONAL
|
|
962
|
+
}
|
|
963
|
+
if (p.wasConnect && status < 300) {
|
|
964
|
+
// RFC 9110 §9.3.6: a 2xx to CONNECT has no content, and its framing
|
|
965
|
+
// fields are ignored; what follows the head is the tunnel's.
|
|
966
|
+
p.state = HTTP1_CLIENT_S_MESSAGE_END
|
|
967
|
+
return HTTP1_HEAD
|
|
968
|
+
}
|
|
969
|
+
let codings: boolean = false
|
|
970
|
+
let length: i64 = toI64(-1)
|
|
971
|
+
const names: string[] = p.names
|
|
972
|
+
for (let i: i32 = 0; i < toI32(names.length) && i < toI32(p.values.length); i += 1) {
|
|
973
|
+
if (names[i] === "transfer-encoding") {
|
|
974
|
+
codings = true
|
|
975
|
+
} else if (names[i] === "content-length") {
|
|
976
|
+
length = http1ClientLengthList(p.values[i], length)
|
|
977
|
+
if (length === toI64(-2)) {
|
|
978
|
+
return http1ClientFail(
|
|
979
|
+
p,
|
|
980
|
+
HTTP1_E_LENGTH,
|
|
981
|
+
"Content-Length values that differ, or one past 2^53 given twice"
|
|
982
|
+
)
|
|
983
|
+
}
|
|
984
|
+
if (length < toI64(0)) {
|
|
985
|
+
return http1ClientFail(p, HTTP1_E_LENGTH, "a Content-Length that is not one run of digits")
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
if (codings) {
|
|
990
|
+
if (length >= toI64(0)) {
|
|
991
|
+
return http1ClientFail(p, HTTP1_E_FRAMING, "both Content-Length and Transfer-Encoding")
|
|
992
|
+
}
|
|
993
|
+
if (p.minor === 0) {
|
|
994
|
+
return http1ClientFail(p, HTTP1_E_CODING, "Transfer-Encoding in an HTTP/1.0 response")
|
|
995
|
+
}
|
|
996
|
+
const tally: i32 = http1ClientTally(p, "transfer-encoding", "chunked")
|
|
997
|
+
if ((tally & 1) === 0) {
|
|
998
|
+
return http1ClientFail(p, HTTP1_E_CODING, "a transfer coding list that does not end in chunked")
|
|
999
|
+
}
|
|
1000
|
+
if ((tally & 2) !== 0) {
|
|
1001
|
+
return http1ClientFail(p, HTTP1_E_CODING, "chunked applied twice")
|
|
1002
|
+
}
|
|
1003
|
+
if (tally >> 2 > 1) {
|
|
1004
|
+
return http1ClientFail(p, HTTP1_E_CODING, "a transfer coding other than chunked")
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const close: boolean = http1ClientConnectionHas(p, "close")
|
|
1008
|
+
p.persistent = p.minor === 1 ? !close : !close && http1ClientConnectionHas(p, "keep-alive")
|
|
1009
|
+
if (p.wasHead || status === 204 || status === 304) {
|
|
1010
|
+
// RFC 9112 §6.3 rules 1 and 2: no content, whatever the fields say,
|
|
1011
|
+
// and their syntax has been checked so that a smuggled one still fails.
|
|
1012
|
+
p.state = HTTP1_CLIENT_S_MESSAGE_END
|
|
1013
|
+
return HTTP1_HEAD
|
|
1014
|
+
}
|
|
1015
|
+
if (codings) {
|
|
1016
|
+
p.chunked = true
|
|
1017
|
+
p.state = HTTP1_CLIENT_S_CHUNK_SIZE
|
|
1018
|
+
} else if (length >= toI64(0)) {
|
|
1019
|
+
if (length > p.maxBody) {
|
|
1020
|
+
return http1ClientFail(p, HTTP1_E_BODY_LIMIT, "a Content-Length over the limit")
|
|
1021
|
+
}
|
|
1022
|
+
p.contentLength = length
|
|
1023
|
+
p.left = length
|
|
1024
|
+
p.state = length > toI64(0) ? HTTP1_CLIENT_S_LENGTH_BODY : HTTP1_CLIENT_S_MESSAGE_END
|
|
1025
|
+
} else {
|
|
1026
|
+
p.persistent = false
|
|
1027
|
+
p.state = HTTP1_CLIENT_S_CLOSE_BODY
|
|
1028
|
+
}
|
|
1029
|
+
return HTTP1_HEAD
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Reads a chunk-size line (RFC 9112 §7.1): 1*HEXDIG, then optional
|
|
1034
|
+
* whitespace and a `;` extension that is read past but not interpreted. Sets
|
|
1035
|
+
* `left` and answers `HTTP1_NEED_MORE`, or the refusal.
|
|
1036
|
+
*/
|
|
1037
|
+
const http1ClientChunkSize = (p: Http1ResponseParser): i32 => {
|
|
1038
|
+
const buf: u8[] = p.buf
|
|
1039
|
+
const a: i32 = p.lineStart
|
|
1040
|
+
const b: i32 = p.lineEnd < toI32(buf.length) ? p.lineEnd : toI32(buf.length)
|
|
1041
|
+
const room: i64 = p.maxBody - p.bodyTotal
|
|
1042
|
+
let size: i64 = toI64(0)
|
|
1043
|
+
let over: boolean = false
|
|
1044
|
+
let i: i32 = a
|
|
1045
|
+
while (i >= 0 && i < b && i < toI32(buf.length)) {
|
|
1046
|
+
const digit: i32 = http1HexValue(toI32(buf[i]))
|
|
1047
|
+
if (digit < 0) {
|
|
1048
|
+
break
|
|
1049
|
+
}
|
|
1050
|
+
if (!over) {
|
|
1051
|
+
if (toI64(digit) > room || size > (room - toI64(digit)) / toI64(16)) {
|
|
1052
|
+
over = true
|
|
1053
|
+
} else {
|
|
1054
|
+
size = size * toI64(16) + toI64(digit)
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
i += 1
|
|
1058
|
+
}
|
|
1059
|
+
if (i === a) {
|
|
1060
|
+
return http1ClientFail(p, HTTP1_E_CHUNK, "a chunk size with no hex digits")
|
|
1061
|
+
}
|
|
1062
|
+
// Whitespace may follow the size only as the BWS before a chunk extension's
|
|
1063
|
+
// `;`, so whitespace that runs to the end of the line is refused.
|
|
1064
|
+
const digitsEnd: i32 = i
|
|
1065
|
+
while (i >= 0 && i < b && i < toI32(buf.length) && http1IsOws(toI32(buf[i]))) {
|
|
1066
|
+
i += 1
|
|
1067
|
+
}
|
|
1068
|
+
if (i >= b && i > digitsEnd) {
|
|
1069
|
+
return http1ClientFail(p, HTTP1_E_CHUNK, "a malformed chunk size")
|
|
1070
|
+
}
|
|
1071
|
+
if (i >= 0 && i < b) {
|
|
1072
|
+
if (buf[i] !== 59) {
|
|
1073
|
+
return http1ClientFail(p, HTTP1_E_CHUNK, "a malformed chunk size")
|
|
1074
|
+
}
|
|
1075
|
+
for (let k: i32 = i + 1; k >= 0 && k < b && k < toI32(buf.length); k += 1) {
|
|
1076
|
+
if (!http1IsValueByte(toI32(buf[k]))) {
|
|
1077
|
+
return http1ClientFail(p, HTTP1_E_CHUNK, "a control character in a chunk extension")
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
if (over) {
|
|
1082
|
+
return http1ClientFail(p, HTTP1_E_BODY_LIMIT, "a chunked body over the limit")
|
|
1083
|
+
}
|
|
1084
|
+
p.left = size
|
|
1085
|
+
p.state = size === toI64(0) ? HTTP1_CLIENT_S_TRAILERS : HTTP1_CLIENT_S_CHUNK_DATA
|
|
1086
|
+
p.sectionBytes = 0
|
|
1087
|
+
return HTTP1_NEED_MORE
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Hands the caller up to `p.left` of the buffered body bytes, and moves to
|
|
1092
|
+
* `after` once the last of them is handed out.
|
|
1093
|
+
*/
|
|
1094
|
+
const http1ClientBodyWindow = (p: Http1ResponseParser, after: i32): i32 => {
|
|
1095
|
+
const ready: i32 = p.end - p.start
|
|
1096
|
+
if (ready <= 0) {
|
|
1097
|
+
return HTTP1_NEED_MORE
|
|
1098
|
+
}
|
|
1099
|
+
const take: i32 = toI64(ready) < p.left ? ready : toI32(p.left)
|
|
1100
|
+
p.body = p.buf
|
|
1101
|
+
p.bodyOff = p.start
|
|
1102
|
+
p.bodyLen = take
|
|
1103
|
+
p.start += take
|
|
1104
|
+
p.scan = p.start
|
|
1105
|
+
p.left = p.left - toI64(take)
|
|
1106
|
+
p.bodyTotal = p.bodyTotal + toI64(take)
|
|
1107
|
+
if (p.left === toI64(0)) {
|
|
1108
|
+
p.state = after
|
|
1109
|
+
}
|
|
1110
|
+
return HTTP1_BODY
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/** A body read until the server closes: every byte fed, up to `maxBody`. */
|
|
1114
|
+
const http1ClientCloseBody = (p: Http1ResponseParser): i32 => {
|
|
1115
|
+
const ready: i32 = p.end - p.start
|
|
1116
|
+
if (ready <= 0) {
|
|
1117
|
+
return HTTP1_NEED_MORE
|
|
1118
|
+
}
|
|
1119
|
+
const room: i64 = p.maxBody - p.bodyTotal
|
|
1120
|
+
if (room <= toI64(0)) {
|
|
1121
|
+
return http1ClientFail(p, HTTP1_E_BODY_LIMIT, "a body read until close over the limit")
|
|
1122
|
+
}
|
|
1123
|
+
// `left` is the room, and the state stays: only the close ends this body.
|
|
1124
|
+
p.left = room
|
|
1125
|
+
return http1ClientBodyWindow(p, HTTP1_CLIENT_S_CLOSE_BODY)
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/** The state machine behind `next()`. */
|
|
1129
|
+
const http1ClientNext = (p: Http1ResponseParser): i32 => {
|
|
1130
|
+
while (true) {
|
|
1131
|
+
switch (p.state) {
|
|
1132
|
+
case HTTP1_CLIENT_S_STATUS_LINE: {
|
|
1133
|
+
const got: i32 = http1ClientTakeLine(
|
|
1134
|
+
p,
|
|
1135
|
+
p.maxStatusLine,
|
|
1136
|
+
HTTP1_E_STATUS_LINE_LIMIT,
|
|
1137
|
+
"a status line over the limit"
|
|
1138
|
+
)
|
|
1139
|
+
if (got !== HTTP1_CLIENT_LINE) {
|
|
1140
|
+
return got
|
|
1141
|
+
}
|
|
1142
|
+
http1ClientStartHead(p)
|
|
1143
|
+
if (http1ClientStatusLine(p) === HTTP1_ERROR) {
|
|
1144
|
+
return HTTP1_ERROR
|
|
1145
|
+
}
|
|
1146
|
+
break
|
|
1147
|
+
}
|
|
1148
|
+
case HTTP1_CLIENT_S_FIELDS:
|
|
1149
|
+
case HTTP1_CLIENT_S_TRAILERS: {
|
|
1150
|
+
const trailer: boolean = p.state === HTTP1_CLIENT_S_TRAILERS
|
|
1151
|
+
const limit: i32 = trailer ? p.maxTrailerBytes : p.maxHeaderBytes
|
|
1152
|
+
const code: i32 = trailer ? HTTP1_E_TRAILER_LIMIT : HTTP1_E_HEADER_LIMIT
|
|
1153
|
+
const text: string = trailer ? "a trailer section over the limit" : "a header section over the limit"
|
|
1154
|
+
const got: i32 = http1ClientTakeLine(p, limit - p.sectionBytes, code, text)
|
|
1155
|
+
if (got !== HTTP1_CLIENT_LINE) {
|
|
1156
|
+
return got
|
|
1157
|
+
}
|
|
1158
|
+
p.sectionBytes += p.lineEnd - p.lineStart + 2
|
|
1159
|
+
if (p.sectionBytes > limit) {
|
|
1160
|
+
return http1ClientFail(p, code, text)
|
|
1161
|
+
}
|
|
1162
|
+
if (p.lineEnd === p.lineStart) {
|
|
1163
|
+
if (trailer) {
|
|
1164
|
+
p.state = HTTP1_CLIENT_S_MESSAGE_END
|
|
1165
|
+
break
|
|
1166
|
+
}
|
|
1167
|
+
return http1ClientEndOfHead(p)
|
|
1168
|
+
}
|
|
1169
|
+
if (http1ClientFieldLine(p, !trailer) === HTTP1_ERROR) {
|
|
1170
|
+
return HTTP1_ERROR
|
|
1171
|
+
}
|
|
1172
|
+
break
|
|
1173
|
+
}
|
|
1174
|
+
case HTTP1_CLIENT_S_LENGTH_BODY:
|
|
1175
|
+
return http1ClientBodyWindow(p, HTTP1_CLIENT_S_MESSAGE_END)
|
|
1176
|
+
case HTTP1_CLIENT_S_CLOSE_BODY:
|
|
1177
|
+
return http1ClientCloseBody(p)
|
|
1178
|
+
case HTTP1_CLIENT_S_CHUNK_SIZE: {
|
|
1179
|
+
const got: i32 = http1ClientTakeLine(
|
|
1180
|
+
p,
|
|
1181
|
+
HTTP1_MAX_CHUNK_LINE,
|
|
1182
|
+
HTTP1_E_CHUNK_LINE_LIMIT,
|
|
1183
|
+
"a chunk-size line over the limit"
|
|
1184
|
+
)
|
|
1185
|
+
if (got !== HTTP1_CLIENT_LINE) {
|
|
1186
|
+
return got
|
|
1187
|
+
}
|
|
1188
|
+
if (http1ClientChunkSize(p) === HTTP1_ERROR) {
|
|
1189
|
+
return HTTP1_ERROR
|
|
1190
|
+
}
|
|
1191
|
+
break
|
|
1192
|
+
}
|
|
1193
|
+
case HTTP1_CLIENT_S_CHUNK_DATA:
|
|
1194
|
+
return http1ClientBodyWindow(p, HTTP1_CLIENT_S_CHUNK_CRLF)
|
|
1195
|
+
case HTTP1_CLIENT_S_CHUNK_CRLF: {
|
|
1196
|
+
// The CRLF after chunk data is an empty line, so a limit of zero
|
|
1197
|
+
// refuses anything else in its place.
|
|
1198
|
+
const got: i32 = http1ClientTakeLine(p, 0, HTTP1_E_CHUNK, "chunk data not followed by CRLF")
|
|
1199
|
+
if (got !== HTTP1_CLIENT_LINE) {
|
|
1200
|
+
return got
|
|
1201
|
+
}
|
|
1202
|
+
p.state = HTTP1_CLIENT_S_CHUNK_SIZE
|
|
1203
|
+
break
|
|
1204
|
+
}
|
|
1205
|
+
case HTTP1_CLIENT_S_MESSAGE_END: {
|
|
1206
|
+
if (p.wasConnect && p.status < 300) {
|
|
1207
|
+
p.state = HTTP1_CLIENT_S_TUNNEL
|
|
1208
|
+
} else if (!p.persistent) {
|
|
1209
|
+
p.state = HTTP1_CLIENT_S_CLOSED
|
|
1210
|
+
} else {
|
|
1211
|
+
// The fields stay readable until the next status line arrives.
|
|
1212
|
+
p.state = HTTP1_CLIENT_S_STATUS_LINE
|
|
1213
|
+
p.informational = 0
|
|
1214
|
+
p.wasHead = false
|
|
1215
|
+
p.wasConnect = false
|
|
1216
|
+
}
|
|
1217
|
+
return HTTP1_END
|
|
1218
|
+
}
|
|
1219
|
+
case HTTP1_CLIENT_S_TUNNEL:
|
|
1220
|
+
return HTTP1_UPGRADE
|
|
1221
|
+
case HTTP1_CLIENT_S_CLOSED:
|
|
1222
|
+
return HTTP1_CLOSED
|
|
1223
|
+
default:
|
|
1224
|
+
return HTTP1_ERROR
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|