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
|
@@ -0,0 +1,908 @@
|
|
|
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
|
+
// What every HTTP version agrees on.
|
|
18
|
+
//
|
|
19
|
+
// The three versions differ in how a message is put on the wire and in
|
|
20
|
+
// nothing else: the same methods, the same status codes, the same rules about
|
|
21
|
+
// which field may repeat and how repeats combine, the same meaning for
|
|
22
|
+
// Content-Length. Those rules live in RFC 9110, which is deliberately
|
|
23
|
+
// version-independent, and so does this file.
|
|
24
|
+
//
|
|
25
|
+
// What does NOT belong here is anything a version does its own way: framing,
|
|
26
|
+
// compression, stream state, flow control. h1.js, h2.js and h3.js keep those.
|
|
27
|
+
// The test is simple — if a rule cites RFC 9110 it is shared, and if it cites
|
|
28
|
+
// 9112 / 9113 / 9114 it is not.
|
|
29
|
+
// ============================================================
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
import crypto from 'node:crypto';
|
|
33
|
+
import { createEmitter } from './utils.js';
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// ============================================================
|
|
37
|
+
// Emitter
|
|
38
|
+
//
|
|
39
|
+
// One implementation, because there were four: h1.js, h2.js and ws.js each
|
|
40
|
+
// grew a private copy, and utils.js has a fifth that differs in the one way
|
|
41
|
+
// that matters — it wraps every listener in try/catch and discards what it
|
|
42
|
+
// catches. That is how a QPACK failure reached nobody at all: it threw, and
|
|
43
|
+
// the emitter ate it.
|
|
44
|
+
//
|
|
45
|
+
// This one does NOT swallow. A listener that throws is a bug in the listener,
|
|
46
|
+
// and the caller that pumped bytes in is the right place to see it — every
|
|
47
|
+
// such caller in this codebase already wraps execute() and turns a throw into
|
|
48
|
+
// a protocol error with the correct code.
|
|
49
|
+
// ============================================================
|
|
50
|
+
|
|
51
|
+
function Emitter() { return createEmitter(false); } // one implementation: utils.js
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
// ============================================================
|
|
55
|
+
// Tokens and field values (RFC 9110 §5.1, §5.5)
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
// tchar, per RFC 9110 §5.6.2. A table because these run over every byte of
|
|
59
|
+
// every field name.
|
|
60
|
+
var IS_TCHAR = (function () {
|
|
61
|
+
var t = new Uint8Array(256);
|
|
62
|
+
var s = "!#$%&'*+-.^_`|~0123456789" +
|
|
63
|
+
'abcdefghijklmnopqrstuvwxyz' +
|
|
64
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
65
|
+
for (var i = 0; i < s.length; i++) t[s.charCodeAt(i)] = 1;
|
|
66
|
+
return t;
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
function isToken(str) {
|
|
70
|
+
if (typeof str !== 'string' || str.length === 0) return false;
|
|
71
|
+
for (var i = 0; i < str.length; i++) {
|
|
72
|
+
if (!IS_TCHAR[str.charCodeAt(i)]) return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A field value may hold VCHAR, SP, HTAB and obs-text. It may NOT hold CR, LF
|
|
79
|
+
* or NUL.
|
|
80
|
+
*
|
|
81
|
+
* The most important check in the HTTP stack, and it applies to every version.
|
|
82
|
+
* In HTTP/1.1 a CR or LF in a value injects a field line or a whole message —
|
|
83
|
+
* response splitting in any application that reflects user input into a
|
|
84
|
+
* header. HTTP/2 and HTTP/3 do not frame by line, so this cannot split a
|
|
85
|
+
* message there; but the value is very often forwarded to an HTTP/1.1 origin
|
|
86
|
+
* behind a proxy, where it can. Validating only in h1.js would leave that door
|
|
87
|
+
* open, so it is validated everywhere.
|
|
88
|
+
*/
|
|
89
|
+
function isValidFieldValue(str) {
|
|
90
|
+
for (var i = 0; i < str.length; i++) {
|
|
91
|
+
var c = str.charCodeAt(i);
|
|
92
|
+
if (c === 0x0d || c === 0x0a || c === 0x00) return false;
|
|
93
|
+
if (c < 0x20 && c !== 0x09) return false;
|
|
94
|
+
if (c === 0x7f) return false;
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A field value may not begin or end with whitespace (RFC 9110 §5.5). */
|
|
100
|
+
function isTrimmedFieldValue(str) {
|
|
101
|
+
if (str.length === 0) return true;
|
|
102
|
+
var first = str.charCodeAt(0), last = str.charCodeAt(str.length - 1);
|
|
103
|
+
return first !== 0x20 && first !== 0x09 && last !== 0x20 && last !== 0x09;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
// ============================================================
|
|
108
|
+
// Repeated fields (RFC 9110 §5.3)
|
|
109
|
+
// ============================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Add a field to a header object, combining repeats the way each field
|
|
113
|
+
* requires.
|
|
114
|
+
*
|
|
115
|
+
* Three different rules, and using the wrong one corrupts data rather than
|
|
116
|
+
* merely looking untidy:
|
|
117
|
+
*
|
|
118
|
+
* set-cookie — never combined. Its own grammar contains commas (in Expires
|
|
119
|
+
* dates), so joining with ", " makes every cookie after the
|
|
120
|
+
* first unparseable. RFC 9110 §5.3 carves it out by name.
|
|
121
|
+
* cookie — combined with "; ", not ",". RFC 9113 §8.2.3 permits an
|
|
122
|
+
* HTTP/2 sender to split it across fields precisely so it
|
|
123
|
+
* compresses better, and the receiver must rejoin it in the
|
|
124
|
+
* form the Cookie grammar uses.
|
|
125
|
+
* everything — combined with ", ", the general list rule.
|
|
126
|
+
*
|
|
127
|
+
* h1.js and h2.js had two separate copies of this, already differing: h1
|
|
128
|
+
* handled set-cookie and h2 handled set-cookie and cookie.
|
|
129
|
+
*/
|
|
130
|
+
// "Already present" means an own property. `name in headers` also found the
|
|
131
|
+
// object's prototype: a field named `constructor` was taken
|
|
132
|
+
// for a repeat and joined onto the inherited function's text, and one named
|
|
133
|
+
// `__proto__` replaced the object's prototype instead of being stored.
|
|
134
|
+
var hasOwn = Object.prototype.hasOwnProperty;
|
|
135
|
+
function addHeader(headers, name, value) {
|
|
136
|
+
if (!hasOwn.call(headers, name)) {
|
|
137
|
+
var v = name === 'set-cookie' ? [value] : value;
|
|
138
|
+
if (name === '__proto__') Object.defineProperty(headers, name, { value: v, enumerable: true, writable: true, configurable: true });
|
|
139
|
+
else headers[name] = v;
|
|
140
|
+
return headers;
|
|
141
|
+
}
|
|
142
|
+
if (name === 'set-cookie') {
|
|
143
|
+
if (!Array.isArray(headers[name])) headers[name] = [headers[name]];
|
|
144
|
+
headers[name].push(value);
|
|
145
|
+
} else if (name === 'cookie') {
|
|
146
|
+
headers[name] += '; ' + value;
|
|
147
|
+
} else {
|
|
148
|
+
headers[name] += ', ' + value;
|
|
149
|
+
}
|
|
150
|
+
return headers;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
// ============================================================
|
|
155
|
+
// Content-Length (RFC 9110 §8.6)
|
|
156
|
+
// ============================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse a Content-Length value, which may already have been combined from
|
|
160
|
+
* repeats into "5, 5".
|
|
161
|
+
*
|
|
162
|
+
* Returns the number, or null when the field is absent. Throws when it is
|
|
163
|
+
* unusable — and "unusable" is strict on purpose. "+5", " 5", "0x5" and "5\v"
|
|
164
|
+
* are each read as a number by some parser somewhere and as zero by another,
|
|
165
|
+
* and a length two hops disagree about is exactly what request smuggling is
|
|
166
|
+
* built from. Repeats with the SAME value are permitted (§8.6): that is one
|
|
167
|
+
* length stated twice. Differing values are the classic desync.
|
|
168
|
+
*/
|
|
169
|
+
function parseContentLength(raw) {
|
|
170
|
+
if (raw === undefined || raw === null) return null;
|
|
171
|
+
var parts = String(raw).split(',');
|
|
172
|
+
var first = null;
|
|
173
|
+
for (var i = 0; i < parts.length; i++) {
|
|
174
|
+
var p = parts[i].trim();
|
|
175
|
+
if (!/^[0-9]+$/.test(p)) throw new Error('invalid Content-Length: ' + JSON.stringify(raw));
|
|
176
|
+
if (first === null) first = p;
|
|
177
|
+
else if (p !== first) throw new Error('conflicting Content-Length values: ' + raw);
|
|
178
|
+
}
|
|
179
|
+
var n = parseInt(first, 10);
|
|
180
|
+
if (!isFinite(n) || n > Number.MAX_SAFE_INTEGER) {
|
|
181
|
+
throw new Error('Content-Length out of range: ' + raw);
|
|
182
|
+
}
|
|
183
|
+
return n;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
// ============================================================
|
|
188
|
+
// Method and status semantics (RFC 9110 §9, §15)
|
|
189
|
+
// ============================================================
|
|
190
|
+
|
|
191
|
+
/** A response to these carries no body regardless of what its headers claim. */
|
|
192
|
+
function statusHasNoBody(statusCode) {
|
|
193
|
+
return (statusCode >= 100 && statusCode < 200) || statusCode === 204 || statusCode === 304;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Does a response to `method` with `statusCode` carry a body?
|
|
198
|
+
*
|
|
199
|
+
* Version-independent, and each clause is a real desync if omitted: a 304
|
|
200
|
+
* usually carries the Content-Length of the cached representation, and a HEAD
|
|
201
|
+
* response carries the length of the body it is not sending. Believing either
|
|
202
|
+
* makes an HTTP/1.1 client read the next response as this one's body, and
|
|
203
|
+
* makes an HTTP/2 client wait for DATA that never comes.
|
|
204
|
+
*/
|
|
205
|
+
function responseHasBody(method, statusCode) {
|
|
206
|
+
if (statusHasNoBody(statusCode)) return false;
|
|
207
|
+
if (method === 'HEAD') return false;
|
|
208
|
+
if (method === 'CONNECT' && statusCode >= 200 && statusCode < 300) return false;
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
var STATUS_TEXT = {
|
|
213
|
+
100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', 103: 'Early Hints',
|
|
214
|
+
200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information',
|
|
215
|
+
204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi-Status',
|
|
216
|
+
300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other',
|
|
217
|
+
304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect',
|
|
218
|
+
400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden',
|
|
219
|
+
404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable',
|
|
220
|
+
407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict',
|
|
221
|
+
410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed',
|
|
222
|
+
413: 'Content Too Large', 414: 'URI Too Long', 415: 'Unsupported Media Type',
|
|
223
|
+
416: 'Range Not Satisfiable', 417: 'Expectation Failed', 418: "I'm a Teapot",
|
|
224
|
+
421: 'Misdirected Request', 422: 'Unprocessable Content', 425: 'Too Early',
|
|
225
|
+
426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests',
|
|
226
|
+
431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons',
|
|
227
|
+
500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway',
|
|
228
|
+
503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported',
|
|
229
|
+
507: 'Insufficient Storage', 508: 'Loop Detected', 511: 'Network Authentication Required'
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
// ============================================================
|
|
234
|
+
// Connection-specific fields (RFC 9113 §8.2.2, RFC 9114 §4.2)
|
|
235
|
+
//
|
|
236
|
+
// Meaningless in HTTP/2 and HTTP/3, and their presence is how a message that
|
|
237
|
+
// means one thing over h2 becomes two requests after an HTTP/1.1 hop. Both
|
|
238
|
+
// versions require a receiver to treat them as malformed rather than ignore
|
|
239
|
+
// them, and both name the same set — so it lives here rather than being
|
|
240
|
+
// written out twice.
|
|
241
|
+
// ============================================================
|
|
242
|
+
|
|
243
|
+
var CONNECTION_SPECIFIC = {
|
|
244
|
+
'connection': 1, 'proxy-connection': 1, 'keep-alive': 1,
|
|
245
|
+
'transfer-encoding': 1, 'upgrade': 1
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Fields no trailer section may carry, whatever the version (RFC 9110
|
|
250
|
+
* §6.5.1): framing and routing (content-length, transfer-encoding, host),
|
|
251
|
+
* request modifiers (te, expect), the trailer list itself, authentication.
|
|
252
|
+
* One list for the parser that refuses them (h1.js) and for trailerFields,
|
|
253
|
+
* which does not send them.
|
|
254
|
+
*/
|
|
255
|
+
var TRAILER_FORBIDDEN = { 'content-length': true, 'transfer-encoding': true, 'host': true, 'te': true,
|
|
256
|
+
'trailer': true, 'connection': true, 'expect': true, 'authorization': true };
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* A message's trailers (res.addTrailers) as they may go on the wire, every
|
|
260
|
+
* version: lower-case names; no pseudo-header, no connection-specific field
|
|
261
|
+
* (RFC 9113 §8.2.2, RFC 9114 §4.2) and none of TRAILER_FORBIDDEN; a list
|
|
262
|
+
* value joined, as one field. Sent as given, a `connection` trailer made the
|
|
263
|
+
* peer refuse the whole response as malformed — over HTTP/1.1 too, where
|
|
264
|
+
* QUICO's own parser rightly refuses it. null when nothing is left.
|
|
265
|
+
*/
|
|
266
|
+
function trailerFields(trailers) {
|
|
267
|
+
if (!trailers) return null;
|
|
268
|
+
var out = {}, any = false;
|
|
269
|
+
for (var name in trailers) {
|
|
270
|
+
if (!Object.prototype.hasOwnProperty.call(trailers, name)) continue;
|
|
271
|
+
var lower = String(name).toLowerCase();
|
|
272
|
+
if (lower.charCodeAt(0) === 58 || isConnectionSpecific(lower) || TRAILER_FORBIDDEN[lower]) continue;
|
|
273
|
+
var value = trailers[name];
|
|
274
|
+
if (value === undefined || value === null) continue;
|
|
275
|
+
out[lower] = Array.isArray(value) ? value.map(String).join(', ') : String(value);
|
|
276
|
+
any = true;
|
|
277
|
+
}
|
|
278
|
+
return any ? out : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isConnectionSpecific(name) {
|
|
282
|
+
return CONNECTION_SPECIFIC[name] === true || CONNECTION_SPECIFIC[name] === 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
// ============================================================
|
|
287
|
+
// Pseudo-headers ↔ HTTP/1.1 request line
|
|
288
|
+
//
|
|
289
|
+
// HTTP/2 and HTTP/3 encode the request line as four pseudo-headers and the
|
|
290
|
+
// Host header as :authority. The translation is identical in both, and was
|
|
291
|
+
// being written by hand in h3_client, in the H2 branch of client.js, and
|
|
292
|
+
// would have been written a third time in h2_client.
|
|
293
|
+
// ============================================================
|
|
294
|
+
|
|
295
|
+
// `:protocol` is the extended CONNECT pseudo-header of RFC 8441, which is what
|
|
296
|
+
// carries WebSocket over HTTP/2 and HTTP/3. It is only legal on a CONNECT
|
|
297
|
+
// request, and only once the peer has advertised ENABLE_CONNECT_PROTOCOL — a
|
|
298
|
+
// receiver that has not enabled it must treat the request as malformed, which
|
|
299
|
+
// is checked where the settings are known rather than here.
|
|
300
|
+
//
|
|
301
|
+
// The same mechanism, with a different value, is how WebTransport rides on
|
|
302
|
+
// HTTP/3: h3.js already sends SETTINGS_ENABLE_CONNECT_PROTOCOL for it.
|
|
303
|
+
var REQUEST_PSEUDO = { ':method': 1, ':scheme': 1, ':authority': 1, ':path': 1, ':protocol': 1 };
|
|
304
|
+
var RESPONSE_PSEUDO = { ':status': 1 };
|
|
305
|
+
|
|
306
|
+
function isPseudoHeader(name) {
|
|
307
|
+
return typeof name === 'string' && name.charCodeAt(0) === 0x3a;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Build the header list for an HTTP/2 or HTTP/3 request.
|
|
312
|
+
*
|
|
313
|
+
* Returns [ [name, value], ... ] with the pseudo-headers first, because both
|
|
314
|
+
* versions require that ordering and a receiver treats a pseudo-header after a
|
|
315
|
+
* regular field as malformed.
|
|
316
|
+
*
|
|
317
|
+
* `Host` is dropped in favour of `:authority` — sending both is permitted only
|
|
318
|
+
* when they agree, and there is no reason to risk them disagreeing. The
|
|
319
|
+
* default port is omitted from the authority, as every client does: including
|
|
320
|
+
* it changes the origin for virtual-host routing and for CORS.
|
|
321
|
+
*/
|
|
322
|
+
function buildRequestPseudoHeaders(options) {
|
|
323
|
+
var method = String(options.method || 'GET').toUpperCase();
|
|
324
|
+
var scheme = options.scheme || 'https';
|
|
325
|
+
var port = options.port;
|
|
326
|
+
var defaultPort = scheme === 'http' ? 80 : 443;
|
|
327
|
+
var authority = options.authority ||
|
|
328
|
+
(options.hostname + (port && port !== defaultPort ? ':' + port : ''));
|
|
329
|
+
|
|
330
|
+
var list = [
|
|
331
|
+
[':method', method],
|
|
332
|
+
[':scheme', scheme],
|
|
333
|
+
[':authority', authority],
|
|
334
|
+
[':path', options.path || '/']
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
var headers = options.headers || {};
|
|
338
|
+
for (var name in headers) {
|
|
339
|
+
if (!Object.prototype.hasOwnProperty.call(headers, name)) continue;
|
|
340
|
+
var lower = String(name).toLowerCase();
|
|
341
|
+
if (isPseudoHeader(lower)) continue; // never set by the caller
|
|
342
|
+
if (lower === 'host') continue; // :authority carries it
|
|
343
|
+
if (isConnectionSpecific(lower)) continue; // malformed in h2/h3
|
|
344
|
+
|
|
345
|
+
var value = headers[name];
|
|
346
|
+
if (value === undefined || value === null) continue;
|
|
347
|
+
if (Array.isArray(value)) {
|
|
348
|
+
for (var i = 0; i < value.length; i++) list.push([lower, String(value[i])]);
|
|
349
|
+
} else {
|
|
350
|
+
list.push([lower, String(value)]);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return list;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Turn a decoded h2/h3 request header set into the shape a Node-style
|
|
358
|
+
* IncomingMessage wants: a method, a url, and the headers.
|
|
359
|
+
*
|
|
360
|
+
* The pseudo-headers stay in `headers`, as node:http2 keeps them
|
|
361
|
+
* (req.headers[':path'], [':authority'], [':scheme']) — code written against
|
|
362
|
+
* node reads them there, and HTTP/3 requests always carried them. They were
|
|
363
|
+
* dropped here, so the same application saw them on HTTP/3 and not on HTTP/2.
|
|
364
|
+
*/
|
|
365
|
+
function pseudoHeadersToRequest(headers) {
|
|
366
|
+
var out = { method: headers[':method'], url: headers[':path'], headers: {} };
|
|
367
|
+
for (var name in headers) out.headers[name] = headers[name];
|
|
368
|
+
// Frameworks read req.headers.host; :authority is where it lives now.
|
|
369
|
+
if (headers[':authority'] !== undefined && out.headers.host === undefined) {
|
|
370
|
+
out.headers.host = headers[':authority'];
|
|
371
|
+
}
|
|
372
|
+
return out;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Build the header list for an h2/h3 response. */
|
|
376
|
+
function buildResponsePseudoHeaders(statusCode, headers) {
|
|
377
|
+
var list = [[':status', String(statusCode)]];
|
|
378
|
+
headers = headers || {};
|
|
379
|
+
for (var name in headers) {
|
|
380
|
+
if (!Object.prototype.hasOwnProperty.call(headers, name)) continue;
|
|
381
|
+
var lower = String(name).toLowerCase();
|
|
382
|
+
if (isPseudoHeader(lower)) continue;
|
|
383
|
+
if (isConnectionSpecific(lower)) continue;
|
|
384
|
+
var value = headers[name];
|
|
385
|
+
if (value === undefined || value === null) continue;
|
|
386
|
+
if (Array.isArray(value)) {
|
|
387
|
+
for (var i = 0; i < value.length; i++) list.push([lower, String(value[i])]);
|
|
388
|
+
} else {
|
|
389
|
+
list.push([lower, String(value)]);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return list;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* A truncated SHA-256, the hash every JA4-family fingerprint uses.
|
|
398
|
+
*
|
|
399
|
+
* Twelve hex characters is what the published formats specify, and it is
|
|
400
|
+
* enough: these identify a client population, not a person, so collisions
|
|
401
|
+
* between two unrelated TLS stacks are a curiosity rather than a failure.
|
|
402
|
+
*/
|
|
403
|
+
function sha12(str) {
|
|
404
|
+
return crypto.createHash('sha256').update(str, 'utf8').digest('hex').slice(0, 12);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
// ============================================================
|
|
409
|
+
// A received field section, HTTP/2 or HTTP/3 (RFC 9113 §8.1–8.3, RFC 9114
|
|
410
|
+
// §4.1–4.3 — the same rules in both): one function, so the two versions
|
|
411
|
+
// cannot drift. h2 checked them and h3 checked none — a value with CR LF, a
|
|
412
|
+
// connection-specific field, a request with no :path all reached an HTTP/3
|
|
413
|
+
// handler as they came.
|
|
414
|
+
//
|
|
415
|
+
// list: [[name, value], ...] in the order received.
|
|
416
|
+
// opts: isServer — a request (else a response);
|
|
417
|
+
// trailers — a trailing section (no pseudo-headers, no request rules);
|
|
418
|
+
// connectProtocol — extended CONNECT is permitted (we advertised it).
|
|
419
|
+
// Returns { headers, pseudoOrder, protocol }. Throws an Error whose message
|
|
420
|
+
// says what was malformed; the caller answers with its own code.
|
|
421
|
+
// ============================================================
|
|
422
|
+
// ============================================================
|
|
423
|
+
// Authority — Host, :authority (RFC 9110 §7.2, RFC 3986 §3.2)
|
|
424
|
+
// ============================================================
|
|
425
|
+
//
|
|
426
|
+
// host [":" port]: a bracketed IP literal, or a reg-name (letters, digits,
|
|
427
|
+
// - . _ ~, sub-delims, %-escapes) — nothing else: no space, no control
|
|
428
|
+
// character, no "@" (userinfo is not part of Host, and deprecated in
|
|
429
|
+
// :authority, RFC 9113 §8.3.1), no "/" "?" "#". The name is what requests are
|
|
430
|
+
// routed by — virtual hosts, certificates, access rules — and a value that
|
|
431
|
+
// one component reads differently from another (a space, a "user@" before
|
|
432
|
+
// it, a path after it) is where they disagree about which site was asked
|
|
433
|
+
// for. nginx checks the same since 1.29.4.
|
|
434
|
+
var AUTHORITY_RE = /^(?:\[[0-9A-Fa-f:.]+\]|\[v[0-9A-Fa-f]+\.[A-Za-z0-9\-._~!$&'()*+,;=:]+\]|[A-Za-z0-9\-._~%!$&'()*+,;=]*)(?::[0-9]*)?$/;
|
|
435
|
+
function isValidAuthority(value) {
|
|
436
|
+
return typeof value === 'string' && AUTHORITY_RE.test(value);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// The site an authority names, for comparing two of them: the host in
|
|
440
|
+
// lowercase, the port without the scheme's default (example.com and
|
|
441
|
+
// EXAMPLE.com:443 are one site over https).
|
|
442
|
+
function authorityKey(value, scheme) {
|
|
443
|
+
var v = String(value).toLowerCase();
|
|
444
|
+
var def = scheme === 'http' ? ':80' : scheme === 'https' ? ':443' : null;
|
|
445
|
+
if (def && v.slice(-def.length) === def) v = v.slice(0, -def.length);
|
|
446
|
+
if (v.charAt(v.length - 1) === ':') v = v.slice(0, -1);
|
|
447
|
+
return v;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function validateFieldSection(list, opts) {
|
|
451
|
+
var isServer = !!opts.isServer, trailers = !!opts.trailers;
|
|
452
|
+
var headers = {};
|
|
453
|
+
var seenRegular = false;
|
|
454
|
+
var pseudo = isServer ? REQUEST_PSEUDO : RESPONSE_PSEUDO;
|
|
455
|
+
// The order pseudo-headers arrive in. The RFCs fix that they come before
|
|
456
|
+
// regular fields and say nothing about their order among themselves, so
|
|
457
|
+
// each implementation chose one and kept it — Chrome m,a,s,p, Firefox
|
|
458
|
+
// m,p,a,s, Safari m,s,p,a. See h2_fingerprint.js. Collected here because
|
|
459
|
+
// this loop is the only place that sees the list in its original sequence.
|
|
460
|
+
var pseudoOrder = [];
|
|
461
|
+
// The fields as they came, one name and value per field line, pseudo-headers
|
|
462
|
+
// first: for rawHeaders, which node:http2 keeps that way (the map combines
|
|
463
|
+
// repeated ones).
|
|
464
|
+
var raw = [];
|
|
465
|
+
var bad = function (m) { var e = new Error(m); e.malformed = true; return e; };
|
|
466
|
+
|
|
467
|
+
for (var i = 0; i < list.length; i++) {
|
|
468
|
+
var name = list[i][0];
|
|
469
|
+
var value = String(list[i][1]);
|
|
470
|
+
|
|
471
|
+
// A field value with CR, LF or NUL, or starting or ending with whitespace,
|
|
472
|
+
// makes the message malformed. Passed through, an application proxying to
|
|
473
|
+
// an HTTP/1.1 upstream writes it where CR LF ends a line: one request
|
|
474
|
+
// becomes a second, injected, one.
|
|
475
|
+
if (!isValidFieldValue(value) || !isTrimmedFieldValue(value)) throw bad('invalid value for field ' + name);
|
|
476
|
+
|
|
477
|
+
if (isPseudoHeader(name)) {
|
|
478
|
+
pseudoOrder.push(name);
|
|
479
|
+
if (trailers) throw bad('pseudo-header in trailers');
|
|
480
|
+
if (seenRegular) throw bad('pseudo-header after a regular field');
|
|
481
|
+
if (!pseudo[name]) throw bad('unknown or misdirected pseudo-header ' + name);
|
|
482
|
+
if (hasOwn.call(headers, name)) throw bad('duplicate pseudo-header ' + name);
|
|
483
|
+
headers[name] = value;
|
|
484
|
+
raw.push(name, value); // node:http2 keeps them in rawHeaders too
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
seenRegular = true;
|
|
488
|
+
|
|
489
|
+
// A field name is a token, in lowercase.
|
|
490
|
+
if (!isToken(name) || name !== name.toLowerCase()) throw bad('invalid field name ' + JSON.stringify(name));
|
|
491
|
+
// Carrying these across is how a request that means one thing here
|
|
492
|
+
// becomes two requests after an HTTP/1.1 hop.
|
|
493
|
+
if (isConnectionSpecific(name)) throw bad('connection-specific field ' + name + ' is not permitted');
|
|
494
|
+
if (name === 'te' && value.toLowerCase() !== 'trailers') throw bad('te may only be "trailers"');
|
|
495
|
+
|
|
496
|
+
// Repeat-combining is RFC 9110 §5.3: set-cookie never joins, cookie joins
|
|
497
|
+
// with "; " (which h2 and h3 rely on: a sender may split it to compress).
|
|
498
|
+
addHeader(headers, name, value);
|
|
499
|
+
raw.push(name, value);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
var protocol = null;
|
|
503
|
+
if (isServer && !trailers) {
|
|
504
|
+
var isConnect = headers[':method'] === 'CONNECT';
|
|
505
|
+
if (headers[':protocol'] !== undefined) {
|
|
506
|
+
// Extended CONNECT (RFC 8441 §4, RFC 9220): only on CONNECT, only if we
|
|
507
|
+
// said it could be used — and, unlike a plain CONNECT, with :scheme and
|
|
508
|
+
// :path, which is what lets a WebSocket handshake name an endpoint.
|
|
509
|
+
if (!opts.connectProtocol) throw bad('extended CONNECT used but not enabled');
|
|
510
|
+
if (!isConnect) throw bad(':protocol is only valid on a CONNECT request');
|
|
511
|
+
if (!headers[':scheme'] || !headers[':path']) throw bad('extended CONNECT requires :scheme and :path');
|
|
512
|
+
protocol = headers[':protocol'].toLowerCase();
|
|
513
|
+
} else if (isConnect) {
|
|
514
|
+
// A plain CONNECT is a tunnel: no :scheme or :path; :authority is the target.
|
|
515
|
+
if (headers[':scheme'] !== undefined || headers[':path'] !== undefined) throw bad('CONNECT must omit :scheme and :path');
|
|
516
|
+
if (!headers[':authority']) throw bad('CONNECT requires :authority');
|
|
517
|
+
} else if (!headers[':method'] || !headers[':scheme'] || !headers[':path']) {
|
|
518
|
+
throw bad('request is missing a mandatory pseudo-header');
|
|
519
|
+
}
|
|
520
|
+
if (headers[':path'] !== undefined) {
|
|
521
|
+
// The path of an http(s) request is absolute ("/…"), or "*" for
|
|
522
|
+
// OPTIONS — and, as in a request line, holds no space or control
|
|
523
|
+
// character (the HTTP/1.1 parser refuses the same).
|
|
524
|
+
var p = headers[':path'];
|
|
525
|
+
if (!(p.charCodeAt(0) === 0x2f || (p === '*' && headers[':method'] === 'OPTIONS')) || /[\x00-\x20\x7f]/.test(p)) {
|
|
526
|
+
throw bad('invalid :path');
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (headers[':method'] !== undefined && !isToken(headers[':method'])) throw bad('invalid :method');
|
|
530
|
+
|
|
531
|
+
// The site asked for (RFC 9113 §8.3.1, RFC 9114 §4.3.1): :authority or
|
|
532
|
+
// Host, well formed — and, when both are present, the same site. A
|
|
533
|
+
// request naming one site in :authority and another in Host is routed by
|
|
534
|
+
// whichever a component reads: the certificate check and the virtual host
|
|
535
|
+
// by one, a framework's req.hostname by the other. "A server SHOULD treat
|
|
536
|
+
// a request as malformed if it contains a Host header field that
|
|
537
|
+
// identifies an entity that differs from the entity in the :authority".
|
|
538
|
+
var auth = headers[':authority'], host = headers['host'];
|
|
539
|
+
if (auth !== undefined && (auth === '' || !isValidAuthority(auth))) throw bad('invalid :authority');
|
|
540
|
+
if (host !== undefined && !isValidAuthority(host)) throw bad('invalid host');
|
|
541
|
+
if (auth !== undefined && host !== undefined &&
|
|
542
|
+
authorityKey(auth, headers[':scheme']) !== authorityKey(host, headers[':scheme'])) {
|
|
543
|
+
throw bad(':authority and host name different sites');
|
|
544
|
+
}
|
|
545
|
+
if (auth === undefined && host === undefined && !isConnect &&
|
|
546
|
+
(headers[':scheme'] === 'http' || headers[':scheme'] === 'https')) {
|
|
547
|
+
throw bad('request names no authority (:authority or host)');
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (!isServer && !trailers && !headers[':status']) throw bad('response is missing :status');
|
|
551
|
+
|
|
552
|
+
Object.defineProperty(headers, '_rawHeaders', { value: raw, enumerable: false });
|
|
553
|
+
return { headers: headers, pseudoOrder: pseudoOrder, protocol: protocol };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
// ============================================================
|
|
558
|
+
// Validation of what an APPLICATION hands us — node:http's rules, codes and
|
|
559
|
+
// messages, thrown at the call, where node throws them.
|
|
560
|
+
//
|
|
561
|
+
// Values often come from user input; a CR/LF in one is a header injected or
|
|
562
|
+
// a request split on the wire (node's CVE-2018-12116 is the client-path
|
|
563
|
+
// case). Nothing reached the wire before — but the check came late (at
|
|
564
|
+
// writeHead/end, with an internal error code) or not at all (client: an
|
|
565
|
+
// 'error' event later), so `try { res.setHeader(...) } catch` written for
|
|
566
|
+
// node caught nothing. node:http exports the same two functions.
|
|
567
|
+
// ============================================================
|
|
568
|
+
|
|
569
|
+
// node's checkInvalidHeaderChar: anything but HTAB, SP–~ and obs-text.
|
|
570
|
+
var INVALID_HEADER_CHAR = /[^\t\x20-\x7e\x80-\xff]/;
|
|
571
|
+
// node's INVALID_PATH_REGEX: a request path is ! … ÿ, nothing else.
|
|
572
|
+
var INVALID_PATH_CHAR = /[^\u0021-\u00ff]/;
|
|
573
|
+
|
|
574
|
+
function nodeError(Ctor, code, message) {
|
|
575
|
+
var e = new Ctor(message);
|
|
576
|
+
e.code = code;
|
|
577
|
+
return e;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function validateHeaderName(name, label) {
|
|
581
|
+
if (typeof name !== 'string' || !isToken(name)) {
|
|
582
|
+
throw nodeError(TypeError, 'ERR_INVALID_HTTP_TOKEN', (label || 'Header name') + ' must be a valid HTTP token ["' + name + '"]');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function validateHeaderValue(name, value) {
|
|
587
|
+
if (value === undefined) {
|
|
588
|
+
throw nodeError(TypeError, 'ERR_HTTP_INVALID_HEADER_VALUE', 'Invalid value "' + value + '" for header "' + name + '"');
|
|
589
|
+
}
|
|
590
|
+
var list = Array.isArray(value) ? value : [value];
|
|
591
|
+
for (var i = 0; i < list.length; i++) {
|
|
592
|
+
if (INVALID_HEADER_CHAR.test(String(list[i]))) {
|
|
593
|
+
throw nodeError(TypeError, 'ERR_INVALID_CHAR', 'Invalid character in header content ["' + name + '"]');
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// writeHead: a status code of three digits, a reason phrase without CR/LF.
|
|
599
|
+
function validateStatus(statusCode, statusMessage) {
|
|
600
|
+
var n = statusCode | 0;
|
|
601
|
+
if (n < 100 || n > 999 || n !== Number(statusCode)) {
|
|
602
|
+
throw nodeError(RangeError, 'ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: ' + statusCode);
|
|
603
|
+
}
|
|
604
|
+
if (statusMessage !== undefined && INVALID_HEADER_CHAR.test(String(statusMessage))) {
|
|
605
|
+
throw nodeError(TypeError, 'ERR_INVALID_CHAR', 'Invalid character in statusMessage');
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// A client request's path and method.
|
|
610
|
+
function validateRequestTarget(path, method) {
|
|
611
|
+
if (path !== undefined && INVALID_PATH_CHAR.test(String(path))) {
|
|
612
|
+
throw nodeError(TypeError, 'ERR_UNESCAPED_CHARACTERS', 'Request path contains unescaped characters');
|
|
613
|
+
}
|
|
614
|
+
if (method !== undefined) validateHeaderName(String(method), 'Method');
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Run the application's request handler (and the 'request' listeners).
|
|
619
|
+
*
|
|
620
|
+
* node:http calls it straight from the socket's 'data' — a throw goes up to
|
|
621
|
+
* process 'uncaughtException', and the request is left as it was: open, never
|
|
622
|
+
* answered. Here the throw is caught long enough to end that one exchange —
|
|
623
|
+
* `fail(err)`: HTTP/1.1 answers 500 and closes, HTTP/2 and HTTP/3 reset only
|
|
624
|
+
* that stream — and then thrown again, on the next tick, so it still reaches
|
|
625
|
+
* 'uncaughtException' (or ends the process) exactly as in node. It is not
|
|
626
|
+
* swallowed: a bug in a handler must stay visible.
|
|
627
|
+
*
|
|
628
|
+
* Before, the throw went wherever the caller's frame took it: on HTTP/1.1 into
|
|
629
|
+
* the parser's catch, reported as 'clientError' — the client blamed, the
|
|
630
|
+
* connection dropped, the exception hidden; on HTTP/2 into the frame loop, a
|
|
631
|
+
* GOAWAY that took every other stream of the connection with it.
|
|
632
|
+
*/
|
|
633
|
+
function runRequestHandler(call, fail) {
|
|
634
|
+
try {
|
|
635
|
+
call();
|
|
636
|
+
} catch (err) {
|
|
637
|
+
try { fail(err); } catch (e) { /* the exchange is gone either way */ }
|
|
638
|
+
process.nextTick(function () { throw err; });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Hold a socket's writes until the end of this tick, then send them as one.
|
|
644
|
+
*
|
|
645
|
+
* A response is written in pieces — the head, a frame header, a body chunk,
|
|
646
|
+
* the next frame — each its own socket.write(), and on an idle socket each
|
|
647
|
+
* of those was a system call and, with Nagle off, a TCP segment of its own.
|
|
648
|
+
* Corked, everything written in the same tick goes out in one writev. The
|
|
649
|
+
* tick ends before any I/O is waited on, so nothing is delayed.
|
|
650
|
+
*/
|
|
651
|
+
function corkForTick(socket) {
|
|
652
|
+
if (socket.writableCorked) return;
|
|
653
|
+
socket.cork();
|
|
654
|
+
process.nextTick(uncork, socket);
|
|
655
|
+
}
|
|
656
|
+
function uncork(socket) { socket.uncork(); }
|
|
657
|
+
|
|
658
|
+
// ---- Extensible priorities (RFC 9218), HTTP/2 and HTTP/3 alike ----
|
|
659
|
+
//
|
|
660
|
+
// A request's `priority` field, or a PRIORITY_UPDATE frame's value: a
|
|
661
|
+
// Structured Field Dictionary (RFC 8941) of which two members mean anything —
|
|
662
|
+
// u urgency, an integer 0 (most urgent) to 7; default 3
|
|
663
|
+
// i incremental, a boolean: served a share at a time alongside others of
|
|
664
|
+
// its urgency (true) or one after another, whole (false, the default)
|
|
665
|
+
// Unknown members, and members of the wrong type, are ignored (§4.1); a value
|
|
666
|
+
// that does not parse is the defaults (§4). Returned as { urgency,
|
|
667
|
+
// incremental } — what the senders' schedulers take (quic_connection's
|
|
668
|
+
// setStreamPriority, h2.js).
|
|
669
|
+
function parsePriority(value) {
|
|
670
|
+
var out = { urgency: 3, incremental: false };
|
|
671
|
+
if (value == null) return out;
|
|
672
|
+
var str = Array.isArray(value) ? value.join(',') : String(value);
|
|
673
|
+
var members = str.split(',');
|
|
674
|
+
for (var k = 0; k < members.length; k++) {
|
|
675
|
+
var m = members[k].trim();
|
|
676
|
+
if (!m) continue;
|
|
677
|
+
var semi = m.indexOf(';'); // parameters: not used here
|
|
678
|
+
if (semi >= 0) m = m.slice(0, semi).trim();
|
|
679
|
+
var eq = m.indexOf('=');
|
|
680
|
+
var key = (eq >= 0 ? m.slice(0, eq) : m).trim();
|
|
681
|
+
var val = eq >= 0 ? m.slice(eq + 1).trim() : '?1'; // a bare key is true
|
|
682
|
+
if (key === 'u') {
|
|
683
|
+
if (/^[0-7]$/.test(val)) out.urgency = Number(val);
|
|
684
|
+
} else if (key === 'i') {
|
|
685
|
+
if (val === '?1') out.incremental = true;
|
|
686
|
+
else if (val === '?0') out.incremental = false;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// What an application passes to res.setPriority, as the senders take it:
|
|
693
|
+
// the fields given, urgency within RFC 9218's 0..7 (below it is the
|
|
694
|
+
// transport's own: HTTP/3's control streams).
|
|
695
|
+
function responsePriority(p) {
|
|
696
|
+
var q = {};
|
|
697
|
+
if (!p) return q;
|
|
698
|
+
if (p.urgency != null) q.urgency = Math.max(0, Math.min(7, Math.floor(Number(p.urgency)) || 0));
|
|
699
|
+
if (p.incremental != null) q.incremental = !!p.incremental;
|
|
700
|
+
return q;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// The other way: { urgency, incremental } as a Priority Field Value, for a
|
|
704
|
+
// `priority` header or a PRIORITY_UPDATE. Defaults are left out (§4.1, §4.2),
|
|
705
|
+
// so the defaults are the empty string.
|
|
706
|
+
function formatPriority(p) {
|
|
707
|
+
var parts = [];
|
|
708
|
+
var u = p && p.urgency != null ? Math.floor(Number(p.urgency)) : 3;
|
|
709
|
+
if (u >= 0 && u <= 7 && u !== 3) parts.push('u=' + u);
|
|
710
|
+
if (p && p.incremental) parts.push('i');
|
|
711
|
+
return parts.join(', ');
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ---- Structured-field strings (RFC 8941 §3.3.3) ----
|
|
715
|
+
//
|
|
716
|
+
// WebTransport's subprotocol negotiation names its protocols as these:
|
|
717
|
+
// WT-Available-Protocols, a list of strings (the client's offer), and
|
|
718
|
+
// WT-Protocol, one string (the server's choice). Both sides use these.
|
|
719
|
+
function sfString(v) { return '"' + String(v).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"'; }
|
|
720
|
+
|
|
721
|
+
// One string, its parameters ignored; null if the value is not one.
|
|
722
|
+
function parseSfString(v) {
|
|
723
|
+
if (typeof v !== 'string') return null;
|
|
724
|
+
var m = /^\s*"((?:[^"\\]|\\.)*)"\s*(?:;.*)?$/.exec(v);
|
|
725
|
+
return m ? m[1].replace(/\\(.)/g, '$1') : null;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// A list of strings, in order. Members that are not strings are skipped; a
|
|
729
|
+
// list that does not parse is none at all (§4.2: the field is ignored).
|
|
730
|
+
function parseSfStringList(v) {
|
|
731
|
+
if (Array.isArray(v)) v = v.join(',');
|
|
732
|
+
if (typeof v !== 'string') return [];
|
|
733
|
+
var out = [], i = 0, n = v.length;
|
|
734
|
+
while (i < n) {
|
|
735
|
+
while (i < n && (v[i] === ' ' || v[i] === '\t')) i++;
|
|
736
|
+
if (i >= n) break;
|
|
737
|
+
var start = i, inStr = false;
|
|
738
|
+
for (; i < n; i++) {
|
|
739
|
+
if (inStr) { if (v[i] === '\\') i++; else if (v[i] === '"') inStr = false; }
|
|
740
|
+
else if (v[i] === '"') inStr = true;
|
|
741
|
+
else if (v[i] === ',') break;
|
|
742
|
+
}
|
|
743
|
+
if (inStr) return [];
|
|
744
|
+
var member = v.slice(start, i).trim();
|
|
745
|
+
if (member === '') return []; // an empty member: not a list
|
|
746
|
+
var s = parseSfString(member);
|
|
747
|
+
if (s !== null) out.push(s);
|
|
748
|
+
i++; // past the comma
|
|
749
|
+
}
|
|
750
|
+
return out;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// ---- How long a request may take to arrive ----
|
|
754
|
+
//
|
|
755
|
+
// A peer that sends a request slowly — its head a byte at a time, its body a
|
|
756
|
+
// byte now and then — holds what the request holds for as long as it is let.
|
|
757
|
+
// Every version bounds it with the same two options, node:http's:
|
|
758
|
+
// headersTimeout until the head (the field section) is complete;
|
|
759
|
+
// requestTimeout until the whole request, body and all, has arrived —
|
|
760
|
+
// counted from the end of the head, as h1_server does.
|
|
761
|
+
// The response is not timed: a long download or an event stream runs as long
|
|
762
|
+
// as the application keeps writing. 0 turns either off.
|
|
763
|
+
var DEFAULT_HEADERS_TIMEOUT = 60 * 1000;
|
|
764
|
+
var DEFAULT_REQUEST_TIMEOUT = 300000; // node:http's: 5 min
|
|
765
|
+
|
|
766
|
+
// ---- 0-RTT: may a request read from early data be handled now? ----
|
|
767
|
+
//
|
|
768
|
+
// RFC 8470: the application's earlyDataFilter(req, callback(err, allow)), or
|
|
769
|
+
// by default GET, HEAD and OPTIONS. decide(true) once allowed; decide(false)
|
|
770
|
+
// for anything else — refused, an error, a throw, or no answer within
|
|
771
|
+
// EARLY_FILTER_TIMEOUT_MS — and the caller answers 425 Too Early, which a
|
|
772
|
+
// client sends again after its handshake. Once only. HTTP/3 (h3_server) and
|
|
773
|
+
// HTTP/2 / HTTP/1.1 (server.js) each kept their own copy.
|
|
774
|
+
var EARLY_FILTER_TIMEOUT_MS = 100;
|
|
775
|
+
function defaultEarlyDataFilter(req, cb) {
|
|
776
|
+
cb(null, req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS');
|
|
777
|
+
}
|
|
778
|
+
function runEarlyDataFilter(filter, req, decide) {
|
|
779
|
+
var done = false;
|
|
780
|
+
var timer = setTimeout(function () { finish(false); }, EARLY_FILTER_TIMEOUT_MS);
|
|
781
|
+
if (timer.unref) timer.unref();
|
|
782
|
+
function finish(allow) {
|
|
783
|
+
if (done) return;
|
|
784
|
+
done = true;
|
|
785
|
+
clearTimeout(timer);
|
|
786
|
+
decide(allow);
|
|
787
|
+
}
|
|
788
|
+
try { (typeof filter === 'function' ? filter : defaultEarlyDataFilter)(req, function (err, allow) { finish(!err && allow === true); }); }
|
|
789
|
+
catch (e) { finish(false); }
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ---- The Date header (RFC 9110 §6.6.1) ----
|
|
793
|
+
//
|
|
794
|
+
// An origin server with a clock sends it on every response. The string
|
|
795
|
+
// changes once a second; it was built — a Date and its formatting — for
|
|
796
|
+
// every response.
|
|
797
|
+
var dateCache = { second: -1, value: '' };
|
|
798
|
+
function httpDate() {
|
|
799
|
+
var now = Date.now(), s = Math.floor(now / 1000);
|
|
800
|
+
if (s !== dateCache.second) { dateCache.second = s; dateCache.value = new Date(now).toUTCString(); }
|
|
801
|
+
return dateCache.value;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// A deadline that does not keep the process alive; none for 0 or less.
|
|
805
|
+
function startDeadline(ms, fn) {
|
|
806
|
+
if (!(ms > 0)) return null;
|
|
807
|
+
var t = setTimeout(fn, ms);
|
|
808
|
+
if (t.unref) t.unref();
|
|
809
|
+
return t;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// node names it per version: ERR_HTTP2_HEADERS_SENT on an HTTP/2 response.
|
|
813
|
+
function headersSentError(what, httpVersion) {
|
|
814
|
+
return nodeError(Error, httpVersion === '2.0' ? 'ERR_HTTP2_HEADERS_SENT' : 'ERR_HTTP_HEADERS_SENT',
|
|
815
|
+
'Cannot ' + what + ' headers after they are sent to the client');
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
// node:http's maxRequestsPerSocket: requests one connection may carry
|
|
820
|
+
// (0, the default: no limit). Read by every version's server (h1, h2, h3).
|
|
821
|
+
function maxRequestsPerSocket(v) {
|
|
822
|
+
if (v === undefined || v === null) return 0;
|
|
823
|
+
if (typeof v !== 'number' || !Number.isInteger(v) || v < 0)
|
|
824
|
+
throw new TypeError('maxRequestsPerSocket must be a non-negative integer, got ' + v);
|
|
825
|
+
return v;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Rapid Reset (CVE-2023-44487): a client opens a request stream and cancels
|
|
829
|
+
// it at once, over and over. No stream stays open, so the concurrency limit
|
|
830
|
+
// never applies, and each request still reaches the handler. node:http2
|
|
831
|
+
// stops it by default (nghttp2's rate limit on RST_STREAM) and so does every
|
|
832
|
+
// version here now: a token bucket of streamResetBurst cancellations (1000),
|
|
833
|
+
// refilled at streamResetRate a second (33) — node:http2's option names and
|
|
834
|
+
// values. Past it, the connection is closed: HTTP/2 GOAWAY
|
|
835
|
+
// ENHANCE_YOUR_CALM, HTTP/3 H3_EXCESSIVE_LOAD. HTTP/2 counts RST_STREAM
|
|
836
|
+
// frames, HTTP/3 cancelled request streams (RESET_STREAM or STOP_SENDING,
|
|
837
|
+
// once per stream: either cancels a request; a WebTransport session's own
|
|
838
|
+
// streams are not counted). Infinity for either turns it off.
|
|
839
|
+
// Returns a counter for one connection: true when a cancellation is one too
|
|
840
|
+
// many. The 'stream_reset' event stays, for a policy of one's own.
|
|
841
|
+
var DEFAULT_STREAM_RESET_BURST = 1000;
|
|
842
|
+
var DEFAULT_STREAM_RESET_RATE = 33;
|
|
843
|
+
function streamResetOption(name, v, def) {
|
|
844
|
+
if (v === undefined || v === null) return def;
|
|
845
|
+
if (typeof v !== 'number' || !(v > 0))
|
|
846
|
+
throw new TypeError(name + ' must be a positive number (Infinity: no limit), got ' + v);
|
|
847
|
+
return v;
|
|
848
|
+
}
|
|
849
|
+
function streamResetLimiter(options) {
|
|
850
|
+
var burst = streamResetOption('streamResetBurst', options && options.streamResetBurst, DEFAULT_STREAM_RESET_BURST);
|
|
851
|
+
var rate = streamResetOption('streamResetRate', options && options.streamResetRate, DEFAULT_STREAM_RESET_RATE);
|
|
852
|
+
if (burst === Infinity || rate === Infinity) return function () { return false; };
|
|
853
|
+
var tokens = burst, at = Date.now();
|
|
854
|
+
return function () {
|
|
855
|
+
var now = Date.now();
|
|
856
|
+
tokens = Math.min(burst, tokens + (now - at) / 1000 * rate);
|
|
857
|
+
at = now;
|
|
858
|
+
if (tokens < 1) return true;
|
|
859
|
+
tokens -= 1;
|
|
860
|
+
return false;
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
export {
|
|
865
|
+
streamResetLimiter,
|
|
866
|
+
runEarlyDataFilter,
|
|
867
|
+
httpDate,
|
|
868
|
+
maxRequestsPerSocket,
|
|
869
|
+
validateFieldSection,
|
|
870
|
+
isValidAuthority,
|
|
871
|
+
validateHeaderName,
|
|
872
|
+
validateHeaderValue,
|
|
873
|
+
validateStatus,
|
|
874
|
+
validateRequestTarget,
|
|
875
|
+
headersSentError,
|
|
876
|
+
runRequestHandler,
|
|
877
|
+
corkForTick,
|
|
878
|
+
DEFAULT_HEADERS_TIMEOUT,
|
|
879
|
+
DEFAULT_REQUEST_TIMEOUT,
|
|
880
|
+
startDeadline,
|
|
881
|
+
parsePriority,
|
|
882
|
+
formatPriority,
|
|
883
|
+
sfString,
|
|
884
|
+
parseSfString,
|
|
885
|
+
parseSfStringList,
|
|
886
|
+
responsePriority,
|
|
887
|
+
sha12,
|
|
888
|
+
Emitter,
|
|
889
|
+
isToken,
|
|
890
|
+
isValidFieldValue,
|
|
891
|
+
isTrimmedFieldValue,
|
|
892
|
+
IS_TCHAR,
|
|
893
|
+
addHeader,
|
|
894
|
+
parseContentLength,
|
|
895
|
+
statusHasNoBody,
|
|
896
|
+
responseHasBody,
|
|
897
|
+
STATUS_TEXT,
|
|
898
|
+
CONNECTION_SPECIFIC,
|
|
899
|
+
isConnectionSpecific,
|
|
900
|
+
trailerFields,
|
|
901
|
+
TRAILER_FORBIDDEN,
|
|
902
|
+
REQUEST_PSEUDO,
|
|
903
|
+
RESPONSE_PSEUDO,
|
|
904
|
+
isPseudoHeader,
|
|
905
|
+
buildRequestPseudoHeaders,
|
|
906
|
+
pseudoHeadersToRequest,
|
|
907
|
+
buildResponsePseudoHeaders
|
|
908
|
+
};
|