quico 0.4.0 → 0.8.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/README.md +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
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
|
+
};
|