quico 0.4.0 → 0.7.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/src/h1.js ADDED
@@ -0,0 +1,953 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+
16
+ // ============================================================
17
+ // HTTP/1.1 message parsing and serialization (RFC 9110 / RFC 9112).
18
+ //
19
+ // No I/O and no sockets: feed bytes in with execute(), get events out. The
20
+ // same shape as h3.js — which is handed already-ordered stream bytes by QUIC
21
+ // and turns them into http_headers / http_body / http_end — so h1_client and
22
+ // h1_server can stay as thin as h3_client and h3_server.
23
+ //
24
+ // What makes HTTP/1.1 harder than it looks is not the grammar, which is
25
+ // trivial, but that the grammar is ambiguous unless you refuse the ambiguous
26
+ // cases. Where a message can be read two ways, an intermediary may read it
27
+ // one way and the origin the other, and the difference is a smuggled request
28
+ // (RFC 9112 §11.2). Every rejection below exists for that reason, and every
29
+ // one of them is a deliberate refusal to guess. A parser that is "lenient"
30
+ // here is not being kind to anyone.
31
+ // ============================================================
32
+
33
+
34
+ import {
35
+ Emitter, isToken, isValidFieldValue, isValidAuthority, addHeader, parseContentLength,
36
+ responseHasBody, STATUS_TEXT, TRAILER_FORBIDDEN
37
+ } from './http_common.js';
38
+
39
+
40
+ // Head = request/status line plus all field lines. Bounded because it arrives
41
+ // before we know anything about the sender: an unbounded head is a trivial
42
+ // memory exhaustion, and the peer can hold the connection open while sending
43
+ // it one byte at a time.
44
+ //
45
+ // The defaults are node:http's: 16 KB for the whole head (its maxHeaderSize),
46
+ // 2000 field lines (its maxHeadersCount — which drops the extra lines; here
47
+ // they are refused, as a dropped header is a header two hops disagree about).
48
+ // The head was allowed 80 KB and only 128 lines: a 20 KB request line that
49
+ // node answers with 431 was served, and a request with 200 headers that node
50
+ // serves was refused.
51
+ var MAX_HEAD_SIZE = 16 * 1024;
52
+ var MAX_HEADER_COUNT = 2000;
53
+
54
+ // A chunk size is a hex number. 16 digits already exceeds any real body and
55
+ // Number.MAX_SAFE_INTEGER; more than that is an attack or a bug.
56
+ var MAX_CHUNK_SIZE_DIGITS = 16;
57
+
58
+ var EMPTY = Buffer.alloc(0);
59
+
60
+ // chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] )
61
+ // chunk-ext-name = token; chunk-ext-val = token / quoted-string (RFC 9112 §7.1.1)
62
+ var TOKEN_RE = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]+";
63
+ var QSTRING_RE = '"(?:[\\t \\x21\\x23-\\x5b\\x5d-\\x7e\\x80-\\xff]|\\\\[\\t \\x21-\\x7e\\x80-\\xff])*"';
64
+ var CHUNK_EXT = new RegExp('^(?:[ \\t]*;[ \\t]*' + TOKEN_RE + '(?:[ \\t]*=[ \\t]*(?:' + TOKEN_RE + '|' + QSTRING_RE + '))?)*$');
65
+ var CRLF = Buffer.from('\r\n');
66
+ var CRLFCRLF = Buffer.from('\r\n\r\n');
67
+
68
+
69
+ // Token and field-value validation now live in http_common.js: the rules are
70
+ // RFC 9110's, not RFC 9112's, and h2/h3 need exactly the same checks before
71
+ // putting a caller-supplied header on the wire.
72
+
73
+
74
+ // ============================================================
75
+ // Errors
76
+ //
77
+ // Every parse failure carries the status the server should answer with, so
78
+ // h1_server does not have to re-derive it. `close` marks the failures after
79
+ // which the connection cannot be reused: once framing is ambiguous we no
80
+ // longer know where the next message starts, so keep-alive is over.
81
+ // ============================================================
82
+
83
+ function h1Error(message, statusCode, mustClose) {
84
+ var err = new Error(message);
85
+ err.code = 'H1_PARSE_ERROR';
86
+ err.statusCode = statusCode || 400;
87
+ err.mustClose = mustClose !== false;
88
+ return err;
89
+ }
90
+
91
+
92
+ // ============================================================
93
+ // H1Parser
94
+ //
95
+ // Events:
96
+ // 'headers' (info) — head complete. info = { method, url,
97
+ // statusCode, statusMessage, httpVersion,
98
+ // headers, rawHeaders, keepAlive, upgrade,
99
+ // hasBody }
100
+ // 'body' (chunk) — body bytes (de-chunked)
101
+ // 'trailers' (obj) — trailer fields, chunked bodies only
102
+ // 'end' () — message complete; another may follow
103
+ // 'upgrade' (info, head) — protocol switch. `head` is the bytes already
104
+ // received that belong to the NEW protocol.
105
+ // The parser stops after this.
106
+ //
107
+ // Options:
108
+ // isServer parse requests (true) or responses (false)
109
+ // maxHeadSize, maxHeaderCount
110
+ // ============================================================
111
+
112
+ function H1Parser(options) {
113
+ if (!(this instanceof H1Parser)) return new H1Parser(options);
114
+ options = options || {};
115
+
116
+ var ev = Emitter();
117
+ var isServer = options.isServer !== false;
118
+ var maxHeadSize = options.maxHeadSize || MAX_HEAD_SIZE;
119
+ var maxHeaderCount = options.maxHeaderCount || MAX_HEADER_COUNT;
120
+ // Server side: whether an upgrade request is taken up (true) or handled as
121
+ // an ordinary request (false) — node:http's shouldUpgradeCallback. Absent,
122
+ // every upgrade request switches, as before.
123
+ var shouldUpgrade = typeof options.shouldUpgrade === 'function' ? options.shouldUpgrade : null;
124
+
125
+ // Line endings. A request must end its lines with CRLF (see splitLines):
126
+ // a server that took a bare LF where a hop in front of it did not would
127
+ // read another message than that hop did — request smuggling. A response
128
+ // is read by its final recipient, the client, which has no one behind it
129
+ // to disagree with; RFC 9112 §2.2 lets it take a bare LF as a line end,
130
+ // and browsers and curl do, so a server that ends its lines with LF alone
131
+ // — old ones, hand-written ones — can be talked to. Only here: the
132
+ // request parser's rule is unchanged, and so is its code path.
133
+ var lenientLf = !isServer;
134
+
135
+ var buf = EMPTY;
136
+ var lfScanned = 0; // how far into buf the head has been checked for bare LF
137
+ var paused = false;
138
+ var state = 'head';
139
+ var msg = null;
140
+ var bodyRemaining = 0;
141
+ var chunkRemaining = 0;
142
+ var trailerBytes = 0;
143
+
144
+ // Response framing depends on the REQUEST that produced it: a HEAD response
145
+ // carries the Content-Length of the body it is not sending, and a 2xx to
146
+ // CONNECT becomes a tunnel. A response parser that does not know the method
147
+ // will read the next response's head as this one's body and desynchronise —
148
+ // which is exactly the desync a smuggler wants. Clients push the method
149
+ // here as they send it.
150
+ var pendingMethods = [];
151
+
152
+ function fail(message, statusCode, mustClose) {
153
+ var err = h1Error(message, statusCode, mustClose);
154
+ state = 'error';
155
+ ev.emit('error', err);
156
+ return err;
157
+ }
158
+
159
+
160
+ // ---- Head parsing ----
161
+
162
+ /**
163
+ * Split a head block into its start line and field lines.
164
+ *
165
+ * Line terminators must be CRLF. RFC 9112 §2.2 permits a recipient to
166
+ * accept a bare LF, and that permission is precisely how CL.0 and LF-based
167
+ * smuggling work: two hops disagreeing about whether a lone LF ended a line
168
+ * see two different messages in one byte stream. Nothing in practice sends
169
+ * bare LF — every client, proxy and curl sends CRLF — so accepting it buys
170
+ * no compatibility and costs the guarantee.
171
+ */
172
+ // Where the line starting at `from` ends: { idx, len } — its terminator's
173
+ // position and length (CRLF 2; a bare LF 1, in a response) — or null when
174
+ // it has not arrived yet.
175
+ function lineEnd(b, from) {
176
+ if (!lenientLf) {
177
+ var c = b.indexOf(CRLF, from);
178
+ return c === -1 ? null : { idx: c, len: 2 };
179
+ }
180
+ var lf = b.indexOf(0x0a, from);
181
+ if (lf === -1) return null;
182
+ return (lf > from && b[lf - 1] === 0x0d) ? { idx: lf - 1, len: 2 } : { idx: lf, len: 1 };
183
+ }
184
+
185
+ // The end of a field section — the empty line after it, in a response
186
+ // with either terminator: { head, rest } (head: up to and including its
187
+ // last line's terminator; rest: after the empty line), or null.
188
+ function sectionEnd(b) {
189
+ if (!lenientLf) {
190
+ var c = b.indexOf(CRLFCRLF);
191
+ return c === -1 ? null : { head: c + 2, rest: c + 4 };
192
+ }
193
+ var at = 0;
194
+ while (true) {
195
+ var e = lineEnd(b, at);
196
+ if (!e) return null;
197
+ if (e.idx === at) return { head: at, rest: at + e.len };
198
+ at = e.idx + e.len;
199
+ }
200
+ }
201
+
202
+ // A response's head, split on LF with an optional CR before it. A CR
203
+ // anywhere else — a bare CR — is refused (§2.2: "MUST consider that
204
+ // element to be invalid"): some parsers end the line there.
205
+ function splitLinesLenient(head) {
206
+ var lines = [];
207
+ var start = 0;
208
+ while (start < head.length) {
209
+ var e = lineEnd(head, start);
210
+ if (!e) throw h1Error('malformed line terminator', 400);
211
+ var cr = head.indexOf(0x0d, start);
212
+ if (cr !== -1 && cr < e.idx) throw h1Error('bare CR in message head', 400);
213
+ lines.push(head.toString('latin1', start, e.idx));
214
+ start = e.idx + e.len;
215
+ }
216
+ return lines;
217
+ }
218
+
219
+ function splitLines(head) {
220
+ if (lenientLf) return splitLinesLenient(head);
221
+ // Reject a bare LF explicitly, before splitting.
222
+ //
223
+ // This check is deliberately redundant: a bare LF inside a field line also
224
+ // trips isValidFieldValue below, because the line it produces still
225
+ // contains the LF. But relying on that is relying on an accident of where
226
+ // the split happened to land, and a later refactor of the field parser
227
+ // would remove the protection without any test noticing. State the rule
228
+ // where the rule lives.
229
+ //
230
+ // The rule itself: line terminators are CRLF. RFC 9112 §2.2 permits a
231
+ // recipient to accept a bare LF, and that permission is precisely how
232
+ // LF-based smuggling works — two hops disagreeing about whether a lone LF
233
+ // ended a line see two different messages in one byte stream. Nothing in
234
+ // practice sends bare LF, so accepting it buys no compatibility.
235
+ for (var i = 0; i < head.length; i++) {
236
+ if (head[i] === 0x0a && (i === 0 || head[i - 1] !== 0x0d)) {
237
+ throw h1Error('bare LF in message head (CRLF required)', 400);
238
+ }
239
+ }
240
+
241
+ var lines = [];
242
+ var start = 0;
243
+ while (start < head.length) {
244
+ var idx = head.indexOf(CRLF, start);
245
+ // Unreachable in practice — the caller only passes a block that ends in
246
+ // CRLF — but a future caller might not, and silently dropping a trailing
247
+ // fragment is exactly the kind of leniency this file exists to avoid.
248
+ if (idx === -1) {
249
+ throw h1Error('malformed line terminator (CRLF required)', 400);
250
+ }
251
+ lines.push(head.toString('latin1', start, idx));
252
+ start = idx + 2;
253
+ }
254
+ return lines;
255
+ }
256
+
257
+ /**
258
+ * Parse field lines into a lowercased map plus the original-case rawHeaders
259
+ * array, the way node:http surfaces them.
260
+ */
261
+ function parseFields(lines, from) {
262
+ var headers = {};
263
+ var rawHeaders = [];
264
+ var count = 0;
265
+
266
+ for (var i = from; i < lines.length; i++) {
267
+ var line = lines[i];
268
+ if (line.length === 0) continue;
269
+
270
+ // obs-fold: a field line continued by starting the next with SP/HTAB.
271
+ // Deprecated by RFC 9112 §5.2, which tells a server to reject it with
272
+ // 400. Unfolding instead is another two-readings-of-one-message hazard.
273
+ if (line.charCodeAt(0) === 0x20 || line.charCodeAt(0) === 0x09) {
274
+ throw h1Error('obsolete line folding is not permitted', 400);
275
+ }
276
+
277
+ if (++count > maxHeaderCount) {
278
+ throw h1Error('too many header fields', 431);
279
+ }
280
+
281
+ var colon = line.indexOf(':');
282
+ if (colon < 1) throw h1Error('malformed header field', 400);
283
+
284
+ var name = line.slice(0, colon);
285
+
286
+ // "No whitespace is allowed between the field name and colon" — RFC 9112
287
+ // §5.1, which then requires a server to reject the message outright.
288
+ // Some intermediaries strip the space and forward; if we accepted it and
289
+ // they stripped it, "Content-Length : 5" would be a header to one hop and
290
+ // not the other.
291
+ if (!isToken(name)) {
292
+ throw h1Error('invalid header field name: ' + JSON.stringify(name), 400);
293
+ }
294
+
295
+ // OWS around the value is stripped and is not part of it (§5.1).
296
+ var value = line.slice(colon + 1).replace(/^[ \t]+|[ \t]+$/g, '');
297
+ if (!isValidFieldValue(value)) {
298
+ throw h1Error('invalid header field value', 400);
299
+ }
300
+
301
+ rawHeaders.push(name, value);
302
+ // Repeat-combining rules are RFC 9110 §5.3 and differ per field
303
+ // (set-cookie never joins, cookie joins with "; "), so they live in
304
+ // http_common.js where h2 uses the same ones.
305
+ addHeader(headers, name.toLowerCase(), value);
306
+ }
307
+
308
+ return { headers: headers, rawHeaders: rawHeaders };
309
+ }
310
+
311
+ function parseRequestLine(line) {
312
+ // METHOD SP request-target SP HTTP-version — exactly one SP each, and no
313
+ // tolerance for runs of spaces: a target containing a space would then
314
+ // parse differently here than at the next hop.
315
+ var sp1 = line.indexOf(' ');
316
+ if (sp1 < 1) throw h1Error('malformed request line', 400);
317
+ var sp2 = line.indexOf(' ', sp1 + 1);
318
+ if (sp2 < 0) throw h1Error('malformed request line', 400);
319
+
320
+ var method = line.slice(0, sp1);
321
+ var target = line.slice(sp1 + 1, sp2);
322
+ var version = line.slice(sp2 + 1);
323
+
324
+ if (!isToken(method)) throw h1Error('invalid method', 400);
325
+ if (target.length === 0) throw h1Error('empty request target', 400);
326
+ // No control characters in the target (RFC 9112 §3.2, RFC 3986): a NUL
327
+ // reached the handler in req.url, where a file path built from it is cut
328
+ // short by the system call that reads it. node:http answers 400.
329
+ if (/[\x00-\x1f\x7f]/.test(target)) throw h1Error('invalid character in request target', 400);
330
+ if (line.indexOf(' ', sp2 + 1) !== -1) throw h1Error('malformed request line', 400);
331
+
332
+ var v = parseVersion(version);
333
+ return { method: method, url: target, httpVersion: v };
334
+ }
335
+
336
+ function parseStatusLine(line) {
337
+ var sp1 = line.indexOf(' ');
338
+ if (sp1 < 1) throw h1Error('malformed status line', 502);
339
+
340
+ var version = line.slice(0, sp1);
341
+ var rest = line.slice(sp1 + 1);
342
+ var sp2 = rest.indexOf(' ');
343
+ var codeStr = sp2 === -1 ? rest : rest.slice(0, sp2);
344
+ var reason = sp2 === -1 ? '' : rest.slice(sp2 + 1);
345
+
346
+ if (!/^[0-9]{3}$/.test(codeStr)) throw h1Error('invalid status code', 502);
347
+
348
+ return {
349
+ httpVersion: parseVersion(version),
350
+ statusCode: parseInt(codeStr, 10),
351
+ statusMessage: reason
352
+ };
353
+ }
354
+
355
+ function parseVersion(str) {
356
+ var m = /^HTTP\/(\d)\.(\d)$/.exec(str);
357
+ if (!m) throw h1Error('unsupported HTTP version: ' + JSON.stringify(str), 505);
358
+ var major = parseInt(m[1], 10);
359
+ if (major !== 1) throw h1Error('unsupported HTTP major version', 505);
360
+ return m[1] + '.' + m[2];
361
+ }
362
+
363
+
364
+ /**
365
+ * Decide how the body is delimited (RFC 9112 §6.1–§6.3). This is where the
366
+ * smuggling defences live, because "how long is the body" IS the question a
367
+ * smuggled request answers differently at two hops.
368
+ */
369
+ function resolveFraming(info) {
370
+ var h = info.headers;
371
+ var hasTE = 'transfer-encoding' in h;
372
+ var hasCL = 'content-length' in h;
373
+
374
+ // RFC 9112 §6.1: an HTTP/1.0 message with Transfer-Encoding is to be
375
+ // treated as having faulty framing — 1.0 has no chunked coding, and a hop
376
+ // that speaks 1.0 reads the body by Content-Length or to the close, not
377
+ // by the chunks this one would. Refused, as nginx does since 1.21.2.
378
+ if (hasTE && isServer && info.httpVersion === '1.0') {
379
+ throw h1Error('Transfer-Encoding in an HTTP/1.0 request', 400);
380
+ }
381
+
382
+ if (hasTE && hasCL) {
383
+ // §6.1: a message with both is an attack or a broken sender, and the RFC
384
+ // says to reject it rather than pick one — picking one is the whole
385
+ // vulnerability, since the other hop may pick the other.
386
+ throw h1Error('both Transfer-Encoding and Content-Length present', 400);
387
+ }
388
+
389
+ if (hasTE) {
390
+ var te = String(h['transfer-encoding']).toLowerCase();
391
+ var codings = te.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
392
+
393
+ // §6.1: chunked must be the FINAL coding. Anything else and we cannot
394
+ // find the end of the body at all.
395
+ if (codings[codings.length - 1] !== 'chunked') {
396
+ throw h1Error('Transfer-Encoding must end with chunked', 400);
397
+ }
398
+ // "chunked, chunked" is not double-chunking, it is a sender trying to
399
+ // get two hops to decode a different number of times.
400
+ for (var i = 0; i < codings.length - 1; i++) {
401
+ if (codings[i] === 'chunked') {
402
+ throw h1Error('chunked appears more than once in Transfer-Encoding', 400);
403
+ }
404
+ }
405
+ // Any coding BEFORE chunked (gzip, deflate, compress) is applied to the
406
+ // body underneath the chunking, and we do not undo it. Accepting the
407
+ // message would mean de-chunking and then handing the application
408
+ // compressed bytes it believes are plaintext — a Content-Length that
409
+ // does not match, a JSON parse that fails on binary, or worse if the
410
+ // application acts on the bytes. RFC 9112 §7.1 says to answer an
411
+ // unsupported transfer coding with 501, which is the honest reply.
412
+ //
413
+ // Note this is transfer-coding, NOT Content-Encoding: `Content-Encoding:
414
+ // gzip` is untouched by this and still passes straight through, which is
415
+ // the header real-world compression actually uses.
416
+ if (codings.length > 1) {
417
+ throw h1Error('unsupported transfer coding: ' + codings.slice(0, -1).join(', '), 501);
418
+ }
419
+ info.framing = 'chunked';
420
+ return;
421
+ }
422
+
423
+ if (hasCL) {
424
+ var len;
425
+ try {
426
+ len = parseContentLength(h['content-length']);
427
+ } catch (e) {
428
+ // The strictness lives in http_common.js because h2 needs the same
429
+ // rules; the status to answer with is HTTP/1.1's business.
430
+ throw h1Error(e.message, 400);
431
+ }
432
+ info.framing = 'length';
433
+ info.contentLength = len;
434
+ return;
435
+ }
436
+
437
+ // No framing headers.
438
+ if (isServer) {
439
+ // A request without either has no body. It cannot be read-until-close:
440
+ // the client is not going to close, it is waiting for a response.
441
+ info.framing = 'none';
442
+ } else {
443
+ // A response without either runs to end of connection (§6.3 case 7).
444
+ info.framing = 'eof';
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Does this message carry a body at all? For responses the status and the
450
+ * originating method override the framing headers entirely (§6.3).
451
+ */
452
+ function resolveHasBody(info) {
453
+ if (isServer) return info.framing !== 'none';
454
+
455
+ var method = pendingMethods.length ? pendingMethods.shift() : 'GET';
456
+ info.requestMethod = method;
457
+
458
+ // Which responses carry a body is RFC 9110 §6.4 and identical in every
459
+ // version, so the rule lives in http_common.js. Getting it wrong is a
460
+ // desync, not a cosmetic bug: a 304 usually carries the Content-Length of
461
+ // the cached representation, and believing it consumes the NEXT response
462
+ // as this one's body.
463
+ if (!responseHasBody(method, info.statusCode)) {
464
+ info.framing = (method === 'CONNECT' && info.statusCode >= 200 && info.statusCode < 300)
465
+ ? 'tunnel' : 'none';
466
+ return false;
467
+ }
468
+
469
+ return info.framing !== 'none';
470
+ }
471
+
472
+ function resolveKeepAlive(info) {
473
+ var conn = String(info.headers['connection'] || '').toLowerCase();
474
+ var tokens = conn.split(',').map(function (s) { return s.trim(); });
475
+
476
+ if (tokens.indexOf('close') !== -1) return false;
477
+ if (info.httpVersion === '1.0') return tokens.indexOf('keep-alive') !== -1;
478
+
479
+ // A response with no way to delimit its body ends when the connection
480
+ // does, so it can never be followed by another message.
481
+ if (info.framing === 'eof') return false;
482
+
483
+ return true;
484
+ }
485
+
486
+ function resolveUpgrade(info) {
487
+ var conn = String(info.headers['connection'] || '').toLowerCase();
488
+ var hasUpgradeToken = conn.split(',').some(function (s) { return s.trim() === 'upgrade'; });
489
+ if (!hasUpgradeToken) return null;
490
+ var up = info.headers['upgrade'];
491
+ if (!up) return null;
492
+ // HTTP/1.0 has no Upgrade mechanism.
493
+ if (info.httpVersion === '1.0') return null;
494
+ return String(up);
495
+ }
496
+
497
+
498
+ // ---- The state machine ----
499
+
500
+ function processHead() {
501
+ // A bare LF is refused as it arrives (see splitLines for the rule). It was
502
+ // checked only once a CRLFCRLF had been found — and a head written with LF
503
+ // alone has none, so the connection waited, unanswered, for the head
504
+ // limit or the timeout. node:http answers 400 at once. Checked from where
505
+ // the last pass stopped, so a head arriving in pieces is scanned once.
506
+ // (A request only: a response may end its lines with LF — see lenientLf.)
507
+ if (!lenientLf) {
508
+ for (var li = Math.max(lfScanned, 0); li < buf.length; li++) {
509
+ if (buf[li] === 0x0a && (li === 0 || buf[li - 1] !== 0x0d)) {
510
+ fail('bare LF in message head (CRLF required)', 400);
511
+ return false;
512
+ }
513
+ }
514
+ lfScanned = buf.length;
515
+ }
516
+
517
+ var end = sectionEnd(buf);
518
+ var idx = end ? end.head - 2 : -1;
519
+
520
+ if (!end) {
521
+ // A leading CRLF before a request line is tolerated (RFC 9112 §2.2):
522
+ // buggy clients append one after a body, and it is unambiguous.
523
+ if (isServer && buf.length >= 2 && buf[0] === 0x0d && buf[1] === 0x0a) {
524
+ buf = buf.subarray(2);
525
+ lfScanned = Math.max(0, lfScanned - 2);
526
+ return true;
527
+ }
528
+ if (buf.length > maxHeadSize) {
529
+ fail('header section exceeds ' + maxHeadSize + ' bytes', 431);
530
+ return false;
531
+ }
532
+ return false; // need more
533
+ }
534
+
535
+ if (idx > maxHeadSize) { fail('header section too large', 431); return false; }
536
+
537
+ var head = buf.subarray(0, end.head); // include the final field line's CRLF
538
+ var rest = buf.subarray(end.rest);
539
+
540
+ var info;
541
+ try {
542
+ var lines = splitLines(head);
543
+ if (lines.length === 0) throw h1Error('empty message head', 400);
544
+
545
+ info = isServer ? parseRequestLine(lines[0]) : parseStatusLine(lines[0]);
546
+
547
+ var fields = parseFields(lines, 1);
548
+ info.headers = fields.headers;
549
+ info.rawHeaders = fields.rawHeaders;
550
+
551
+ // RFC 9112 §3.2: an HTTP/1.1 request MUST carry Host, and exactly one.
552
+ // Two Host headers let two hops route to two different origins.
553
+ if (isServer && info.httpVersion === '1.1') {
554
+ if (!('host' in info.headers)) throw h1Error('missing Host header', 400);
555
+ }
556
+ // One Host, and a well-formed one (see isValidAuthority) — on HTTP/1.0
557
+ // too, which need not send it but must not send two.
558
+ if (isServer && 'host' in info.headers) {
559
+ if (String(info.headers['host']).indexOf(',') !== -1) {
560
+ throw h1Error('multiple Host headers', 400);
561
+ }
562
+ if (!isValidAuthority(String(info.headers['host']))) throw h1Error('invalid Host header', 400);
563
+ }
564
+
565
+ resolveFraming(info);
566
+ info.hasBody = resolveHasBody(info);
567
+ info.keepAlive = resolveKeepAlive(info);
568
+ info.upgrade = resolveUpgrade(info);
569
+
570
+ } catch (err) {
571
+ state = 'error';
572
+ ev.emit('error', err.code === 'H1_PARSE_ERROR' ? err
573
+ : h1Error(err.message, 400));
574
+ return false;
575
+ }
576
+
577
+ buf = rest;
578
+ lfScanned = 0;
579
+ msg = info;
580
+
581
+ // ---- Upgrade / tunnel: hand the socket over ----
582
+ //
583
+ // The bytes already sitting in `buf` belong to the NEW protocol. A client
584
+ // may legitimately put its first WebSocket frame in the same TCP segment
585
+ // as the handshake request, and if we drop these bytes that frame is gone
586
+ // — a bug that only shows under load, when segments happen to coalesce.
587
+ // node:http calls this parameter `head` and so do we.
588
+ var switching = (!isServer && info.statusCode === 101) ||
589
+ (!isServer && info.framing === 'tunnel') ||
590
+ (isServer && info.upgrade && info.framing === 'none' &&
591
+ (!shouldUpgrade || !!shouldUpgrade(info)));
592
+
593
+ if (switching) {
594
+ state = 'upgraded';
595
+ var head2 = buf;
596
+ buf = EMPTY;
597
+ // ONLY 'upgrade' — not 'headers' as well. The same `info` is the first
598
+ // argument, so emitting both says nothing extra and forces every
599
+ // consumer to recognise and discard the duplicate. The client did not:
600
+ // it built a response object from 'headers' and reported "unexpected
601
+ // server response: 101" for a handshake that had in fact succeeded.
602
+ ev.emit('upgrade', info, head2);
603
+ return false;
604
+ }
605
+
606
+ ev.emit('headers', info);
607
+
608
+ if (!info.hasBody) { finishMessage(); return true; }
609
+
610
+ if (info.framing === 'chunked') { state = 'chunk-size'; return true; }
611
+ if (info.framing === 'eof') { state = 'body-eof'; return true; }
612
+
613
+ bodyRemaining = info.contentLength;
614
+ if (bodyRemaining === 0) { finishMessage(); return true; }
615
+ state = 'body-length';
616
+ return true;
617
+ }
618
+
619
+ function processBodyLength() {
620
+ if (buf.length === 0) return false;
621
+ var take = Math.min(buf.length, bodyRemaining);
622
+ var chunk = buf.subarray(0, take);
623
+ buf = buf.subarray(take);
624
+ bodyRemaining -= take;
625
+ ev.emit('body', chunk);
626
+ if (bodyRemaining === 0) { finishMessage(); return true; }
627
+ return buf.length > 0;
628
+ }
629
+
630
+ function processBodyEof() {
631
+ if (buf.length === 0) return false;
632
+ var chunk = buf;
633
+ buf = EMPTY;
634
+ ev.emit('body', chunk);
635
+ return false;
636
+ }
637
+
638
+ function processChunkSize() {
639
+ var le = lineEnd(buf, 0);
640
+ var idx = le ? le.idx : -1;
641
+ if (idx === -1) {
642
+ if (buf.length > 1024) { fail('chunk size line too long', 400); return false; }
643
+ return false;
644
+ }
645
+
646
+ var line = buf.toString('latin1', 0, idx);
647
+
648
+ // chunk-ext (";name=value") is permitted and ignored, but the size itself
649
+ // must be bare hex with no sign, no whitespace and no 0x prefix. Every
650
+ // tolerance here is a place where two parsers can read two sizes.
651
+ var semi = line.indexOf(';');
652
+ var sizeStr = semi === -1 ? line : line.slice(0, semi);
653
+
654
+ // The extensions themselves are ignored, but they must be well formed
655
+ // (RFC 9112 §7.1.1): ;name[=value], a token or a quoted string. They were
656
+ // skipped unread — so a control character or a bare CR in them, which
657
+ // another parser on the path may treat as the end of the line, passed
658
+ // here (the class of node's CVE-2021-22960).
659
+ if (semi !== -1 && !CHUNK_EXT.test(line.slice(semi))) {
660
+ fail('invalid chunk extension', 400);
661
+ return false;
662
+ }
663
+
664
+ if (sizeStr.length === 0 || sizeStr.length > MAX_CHUNK_SIZE_DIGITS ||
665
+ !/^[0-9a-fA-F]+$/.test(sizeStr)) {
666
+ fail('invalid chunk size: ' + JSON.stringify(sizeStr), 400);
667
+ return false;
668
+ }
669
+
670
+ var size = parseInt(sizeStr, 16);
671
+ if (!isFinite(size)) { fail('chunk size out of range', 400); return false; }
672
+
673
+ buf = buf.subarray(idx + le.len);
674
+
675
+ if (size === 0) { state = 'trailers'; trailerBytes = 0; return true; }
676
+
677
+ chunkRemaining = size;
678
+ state = 'chunk-data';
679
+ return true;
680
+ }
681
+
682
+ function processChunkData() {
683
+ if (buf.length === 0) return false;
684
+ var take = Math.min(buf.length, chunkRemaining);
685
+ var chunk = buf.subarray(0, take);
686
+ buf = buf.subarray(take);
687
+ chunkRemaining -= take;
688
+ ev.emit('body', chunk);
689
+ if (chunkRemaining === 0) state = 'chunk-crlf';
690
+ return buf.length > 0;
691
+ }
692
+
693
+ function processChunkCrlf() {
694
+ // A response's chunk may end in LF alone (see lenientLf).
695
+ if (lenientLf && buf.length >= 1 && buf[0] === 0x0a) {
696
+ buf = buf.subarray(1);
697
+ state = 'chunk-size';
698
+ return true;
699
+ }
700
+ if (buf.length < 2) return false;
701
+ if (buf[0] !== 0x0d || buf[1] !== 0x0a) {
702
+ fail('chunk data not terminated by CRLF', 400);
703
+ return false;
704
+ }
705
+ buf = buf.subarray(2);
706
+ state = 'chunk-size';
707
+ return true;
708
+ }
709
+
710
+ function processTrailers() {
711
+ var le = lineEnd(buf, 0);
712
+ var idx = le ? le.idx : -1;
713
+ if (idx === -1) {
714
+ trailerBytes += buf.length;
715
+ if (trailerBytes > maxHeadSize) { fail('trailer section too large', 431); return false; }
716
+ return false;
717
+ }
718
+
719
+ if (idx === 0) { // blank line — trailer section done
720
+ buf = buf.subarray(le.len);
721
+ finishMessage();
722
+ return true;
723
+ }
724
+
725
+ // Collect the whole trailer section before parsing it.
726
+ var tend = sectionEnd(buf);
727
+ if (!tend) {
728
+ trailerBytes = buf.length;
729
+ if (trailerBytes > maxHeadSize) { fail('trailer section too large', 431); return false; }
730
+ return false;
731
+ }
732
+
733
+ var section = buf.subarray(0, tend.head);
734
+ buf = buf.subarray(tend.rest);
735
+
736
+ try {
737
+ var parsed = parseFields(splitLines(section), 0);
738
+ // RFC 9110 §6.5.1: framing and routing fields are forbidden in trailers.
739
+ // A Content-Length or Transfer-Encoding that arrives AFTER the body has
740
+ // been read is a re-framing attempt against whoever reads it late.
741
+ for (var fname in parsed.headers) {
742
+ if (TRAILER_FORBIDDEN[fname]) throw h1Error('field not permitted in trailers: ' + fname, 400);
743
+ }
744
+ ev.emit('trailers', parsed.headers);
745
+ } catch (err) {
746
+ state = 'error';
747
+ ev.emit('error', err.code === 'H1_PARSE_ERROR' ? err : h1Error(err.message, 400));
748
+ return false;
749
+ }
750
+
751
+ finishMessage();
752
+ return true;
753
+ }
754
+
755
+ function finishMessage() {
756
+ var wasKeepAlive = msg ? msg.keepAlive : false;
757
+ msg = null;
758
+ bodyRemaining = 0;
759
+ chunkRemaining = 0;
760
+ state = wasKeepAlive ? 'head' : 'done';
761
+ ev.emit('end');
762
+ }
763
+
764
+
765
+ /**
766
+ * Feed bytes. Returns the number of bytes consumed from `chunk`; the
767
+ * remainder (non-zero only after an upgrade) belongs to the next protocol.
768
+ *
769
+ * Never throws: failures arrive as an 'error' event carrying the status code
770
+ * the server should reply with.
771
+ */
772
+ function execute(chunk) {
773
+ if (state === 'error' || state === 'upgraded' || state === 'done') return 0;
774
+
775
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, 'latin1');
776
+ else if (!Buffer.isBuffer(chunk)) chunk = Buffer.from(chunk.buffer || chunk,
777
+ chunk.byteOffset || 0,
778
+ chunk.byteLength);
779
+
780
+ var before = buf.length;
781
+ buf = buf.length === 0 ? chunk : Buffer.concat([buf, chunk]);
782
+
783
+ var keepGoing = true;
784
+ while (keepGoing) {
785
+ // A paused parser stops between messages but keeps accepting bytes: they
786
+ // accumulate in `buf` and TCP back-pressures the peer. This is how a
787
+ // server answers pipelined requests in order (RFC 9112 §9.3) without an
788
+ // ordered write queue — it simply does not look at request N+1 until
789
+ // response N is out. Pausing must NOT stop mid-message, or a body would
790
+ // stall; hence the flag is only honoured at the top of the loop, and
791
+ // callers set it from the 'end' event.
792
+ if (paused) break;
793
+ switch (state) {
794
+ case 'head': keepGoing = processHead(); break;
795
+ case 'body-length': keepGoing = processBodyLength(); break;
796
+ case 'body-eof': keepGoing = processBodyEof(); break;
797
+ case 'chunk-size': keepGoing = processChunkSize(); break;
798
+ case 'chunk-data': keepGoing = processChunkData(); break;
799
+ case 'chunk-crlf': keepGoing = processChunkCrlf(); break;
800
+ case 'trailers': keepGoing = processTrailers(); break;
801
+ default: keepGoing = false; break;
802
+ }
803
+ }
804
+
805
+ var consumed = before + chunk.length - buf.length;
806
+ return consumed < 0 ? 0 : consumed;
807
+ }
808
+
809
+ /**
810
+ * Tell the parser the connection ended.
811
+ *
812
+ * For an 'eof'-framed response this is the legitimate end of the body. In
813
+ * the middle of any other body it is a truncated message, and the caller
814
+ * must NOT treat what it has as complete.
815
+ */
816
+ function eof() {
817
+ if (state === 'body-eof') { finishMessage(); return; }
818
+ if (state === 'head' && buf.length === 0) { state = 'done'; return; }
819
+ if (state === 'done' || state === 'upgraded' || state === 'error') return;
820
+ fail('connection closed mid-message', 400);
821
+ }
822
+
823
+ this.on = ev.on;
824
+ this.off = ev.off;
825
+ this.execute = execute;
826
+ this.eof = eof;
827
+
828
+ /** Stop consuming buffered bytes. Bytes already handed to execute() stay
829
+ * buffered; new ones are still accepted. Safe to call from any event. */
830
+ this.pause = function () { paused = true; };
831
+
832
+ /** Resume, draining whatever accumulated while paused. */
833
+ this.resume = function () {
834
+ if (!paused) return;
835
+ paused = false;
836
+ if (buf.length > 0) execute(EMPTY);
837
+ };
838
+
839
+ /** Client-side: record the method of a request as it is sent, so the
840
+ * response parser can frame the reply (HEAD / CONNECT). */
841
+ this.pushRequestMethod = function (method) {
842
+ pendingMethods.push(String(method).toUpperCase());
843
+ };
844
+
845
+ Object.defineProperty(this, 'state', { get: function () { return state; } });
846
+ Object.defineProperty(this, 'paused', { get: function () { return paused; } });
847
+ /** Bytes held back awaiting more input — the `head` after an upgrade. */
848
+ Object.defineProperty(this, 'buffered', { get: function () { return buf; } });
849
+
850
+ return this;
851
+ }
852
+
853
+
854
+ // ============================================================
855
+ // Serialization
856
+ // ============================================================
857
+
858
+ /**
859
+ * Reject anything that would let a header inject a line break.
860
+ *
861
+ * Called on the way OUT as well as in. Applications set headers from user
862
+ * input constantly — a filename in Content-Disposition, a URL in Location —
863
+ * and a CR or LF there splits our own response. Validating only inbound
864
+ * traffic leaves the more damaging direction open.
865
+ */
866
+ function assertHeaderSafe(name, value) {
867
+ if (!isToken(name)) {
868
+ throw h1Error('invalid header name: ' + JSON.stringify(name), 500);
869
+ }
870
+ if (!isValidFieldValue(value)) {
871
+ throw h1Error('invalid characters in value of header ' + name, 500);
872
+ }
873
+ }
874
+
875
+ function serializeHeaders(headers, out) {
876
+ for (var name in headers) {
877
+ if (!Object.prototype.hasOwnProperty.call(headers, name)) continue;
878
+ var value = headers[name];
879
+ if (value === undefined || value === null) continue;
880
+
881
+ if (Array.isArray(value)) {
882
+ for (var i = 0; i < value.length; i++) {
883
+ var v = String(value[i]);
884
+ assertHeaderSafe(name, v);
885
+ out.push(name + ': ' + v);
886
+ }
887
+ } else {
888
+ var s = String(value);
889
+ assertHeaderSafe(name, s);
890
+ out.push(name + ': ' + s);
891
+ }
892
+ }
893
+ }
894
+
895
+ /** Serialize a request head. Returns a Buffer ending in CRLFCRLF. */
896
+ function buildRequestHead(method, target, headers) {
897
+ var m = String(method).toUpperCase();
898
+ if (!isToken(m)) throw h1Error('invalid method', 500);
899
+ if (!target || /[ \r\n]/.test(target)) throw h1Error('invalid request target', 500);
900
+
901
+ var lines = [m + ' ' + target + ' HTTP/1.1'];
902
+ serializeHeaders(headers || {}, lines);
903
+ return Buffer.from(lines.join('\r\n') + '\r\n\r\n', 'latin1');
904
+ }
905
+
906
+ /** Serialize a response head. Returns a Buffer ending in CRLFCRLF. */
907
+ function buildResponseHead(statusCode, statusMessage, headers) {
908
+ var code = parseInt(statusCode, 10);
909
+ if (!(code >= 100 && code <= 599)) throw h1Error('invalid status code', 500);
910
+
911
+ var reason = statusMessage != null ? String(statusMessage)
912
+ : (STATUS_TEXT[code] || '');
913
+ if (!isValidFieldValue(reason)) throw h1Error('invalid reason phrase', 500);
914
+
915
+ var lines = ['HTTP/1.1 ' + code + (reason ? ' ' + reason : '')];
916
+ serializeHeaders(headers || {}, lines);
917
+ return Buffer.from(lines.join('\r\n') + '\r\n\r\n', 'latin1');
918
+ }
919
+
920
+ /**
921
+ * One chunk of a chunked body — hex size, CRLF, data, CRLF — as the three
922
+ * pieces to write, the data among them as given: not copied into one buffer
923
+ * with its framing, which for a large body was a second copy of all of it.
924
+ * Written in the same tick to a corked socket, they still leave as one.
925
+ * Nothing for empty data: a zero-size chunk would end the body.
926
+ */
927
+ function encodeChunk(data) {
928
+ if (typeof data === 'string') data = Buffer.from(data);
929
+ if (data.length === 0) return [];
930
+ return [Buffer.from(data.length.toString(16) + '\r\n', 'latin1'), data, CRLF];
931
+ }
932
+
933
+ /** Terminating chunk, with optional trailers. */
934
+ function encodeLastChunk(trailers) {
935
+ if (!trailers) return Buffer.from('0\r\n\r\n', 'latin1');
936
+ var lines = [];
937
+ serializeHeaders(trailers, lines);
938
+ if (lines.length === 0) return Buffer.from('0\r\n\r\n', 'latin1');
939
+ return Buffer.from('0\r\n' + lines.join('\r\n') + '\r\n\r\n', 'latin1');
940
+ }
941
+
942
+
943
+ export {
944
+ H1Parser,
945
+ buildRequestHead,
946
+ buildResponseHead,
947
+ encodeChunk,
948
+ encodeLastChunk,
949
+ STATUS_TEXT,
950
+ isToken,
951
+ isValidFieldValue,
952
+ h1Error
953
+ };