quico 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1768 -155
- package/index.js +55 -36
- package/package.json +3 -3
- 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 +770 -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 +624 -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 +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +467 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6517 -657
- package/src/quic_server.js +768 -73
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +361 -35
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +474 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
package/src/h2_server.js
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
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
|
+
import { EventEmitter } from 'node:events';
|
|
16
|
+
import { Duplex } from 'node:stream';
|
|
17
|
+
|
|
18
|
+
import { H2Connection, SOCKET_HIGH_WATER } from './h2.js';
|
|
19
|
+
import * as F from './h2_frames.js';
|
|
20
|
+
import { buildResponsePseudoHeaders, corkForTick, pseudoHeadersToRequest, runRequestHandler, STATUS_TEXT, maxRequestsPerSocket,
|
|
21
|
+
DEFAULT_HEADERS_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, startDeadline, responsePriority, trailerFields, statusHasNoBody, httpDate, streamResetLimiter } from './http_common.js';
|
|
22
|
+
import { observeH2 } from './h2_fingerprint.js';
|
|
23
|
+
import { IncomingMessage, ServerResponse } from './streams.js';
|
|
24
|
+
import { createTcpListener } from './tcp_listener.js';
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// ============================================================
|
|
28
|
+
// HTTP/2 server.
|
|
29
|
+
//
|
|
30
|
+
// The same three layers as everywhere else in this stack: tcp_listener owns
|
|
31
|
+
// the socket and ALPN, h2.js turns bytes into stream events, and streams.js
|
|
32
|
+
// wraps those in the IncomingMessage / ServerResponse pair every framework
|
|
33
|
+
// expects. This file is only the wiring between them — which is why it is a
|
|
34
|
+
// fraction of h2.js's size despite being the thing applications touch.
|
|
35
|
+
//
|
|
36
|
+
// It is also why an Express app runs unchanged over HTTP/2: the req and res
|
|
37
|
+
// it receives are the same objects h1_server and h3_server hand it, and
|
|
38
|
+
// nothing in the middle knows which protocol produced them.
|
|
39
|
+
// ============================================================
|
|
40
|
+
|
|
41
|
+
var DEFAULT_KEEPALIVE_TIMEOUT = 0; // 0 = no idle limit; h2 connections are long-lived
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// ============================================================
|
|
45
|
+
// Protocol-independent refusal codes.
|
|
46
|
+
//
|
|
47
|
+
// HTTP/2 and HTTP/3 both let a server refuse a stream, and both use entirely
|
|
48
|
+
// different numbers for it. Code that limits request rates should not have to
|
|
49
|
+
// know which protocol carried the request, so it names an INTENT and each
|
|
50
|
+
// layer maps it to its own registry.
|
|
51
|
+
//
|
|
52
|
+
// REFUSED specifically, and not CANCELLED, is what a rate limiter wants:
|
|
53
|
+
// RFC 9113 §8.1 lets a client safely retry a REFUSED_STREAM on another
|
|
54
|
+
// connection, because it states the request was never processed. Cancelling
|
|
55
|
+
// says the opposite, and a client that retries after it may duplicate work.
|
|
56
|
+
// ============================================================
|
|
57
|
+
|
|
58
|
+
var STREAM_REFUSED = 'refused';
|
|
59
|
+
var STREAM_CANCELLED = 'cancelled';
|
|
60
|
+
var CONN_OVERLOADED = 'overloaded';
|
|
61
|
+
|
|
62
|
+
function translateStreamError(code) {
|
|
63
|
+
if (typeof code === 'number') return code; // caller wants a raw code
|
|
64
|
+
if (code === STREAM_CANCELLED) return F.ERROR.CANCEL;
|
|
65
|
+
return F.ERROR.REFUSED_STREAM;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function translateConnError(code) {
|
|
69
|
+
if (typeof code === 'number') return code;
|
|
70
|
+
if (code === CONN_OVERLOADED) return F.ERROR.ENHANCE_YOUR_CALM;
|
|
71
|
+
return F.ERROR.NO_ERROR;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
function createServer(options, handler) {
|
|
76
|
+
if (typeof options === 'function') { handler = options; options = {}; }
|
|
77
|
+
options = options || {};
|
|
78
|
+
|
|
79
|
+
var ev = new EventEmitter();
|
|
80
|
+
var listener = null;
|
|
81
|
+
var connections = new Set();
|
|
82
|
+
var closing = false;
|
|
83
|
+
|
|
84
|
+
var altSvc = options.altSvc || null;
|
|
85
|
+
// How long a request may take to arrive (see http_common). headersTimeout
|
|
86
|
+
// bounds a header block left open, which holds the whole connection;
|
|
87
|
+
// requestTimeout each request, from its head to its last byte.
|
|
88
|
+
var headersTimeout = options.headersTimeout != null ? options.headersTimeout : DEFAULT_HEADERS_TIMEOUT;
|
|
89
|
+
var requestTimeout = options.requestTimeout != null ? options.requestTimeout : DEFAULT_REQUEST_TIMEOUT;
|
|
90
|
+
// Rapid Reset (http_common streamResetLimiter): the options checked now.
|
|
91
|
+
streamResetLimiter(options);
|
|
92
|
+
// node:http2's maxSessionMemory: megabytes a connection may have queued.
|
|
93
|
+
var maxSessionMemory = (options.maxSessionMemory > 0 ? options.maxSessionMemory : 10) * 1024 * 1024;
|
|
94
|
+
// node:http's maxRequestsPerSocket, as HTTP/2 can honour it: after the Nth
|
|
95
|
+
// request, GOAWAY — those in flight finish, the client opens a new
|
|
96
|
+
// connection for the next (nginx's keepalive_requests, the same way).
|
|
97
|
+
var maxRequests = maxRequestsPerSocket(options.maxRequestsPerSocket);
|
|
98
|
+
|
|
99
|
+
// node:http's shouldUpgradeCallback, for extended CONNECT (RFC 8441): taken
|
|
100
|
+
// up as an 'upgrade' (true) or handled as an ordinary 'request' (false).
|
|
101
|
+
// Default: true when something listens for 'upgrade'.
|
|
102
|
+
var shouldUpgradeCallback = typeof options.shouldUpgradeCallback === 'function'
|
|
103
|
+
? options.shouldUpgradeCallback
|
|
104
|
+
: function () { return ev.listenerCount('upgrade') > 0; };
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
function handleConnection(socket) {
|
|
108
|
+
if (closing) { try { socket.destroy(); } catch (e) {} return; }
|
|
109
|
+
|
|
110
|
+
var h2 = new H2Connection({
|
|
111
|
+
isServer: true,
|
|
112
|
+
stats: options.stats || null,
|
|
113
|
+
maxConcurrentStreams: options.maxConcurrentStreams,
|
|
114
|
+
initialWindowSize: options.initialWindowSize,
|
|
115
|
+
connectionWindowSize: options.connectionWindowSize,
|
|
116
|
+
maxFrameSize: options.maxFrameSize,
|
|
117
|
+
maxHeaderListSize: options.maxHeaderListSize,
|
|
118
|
+
headerTableSize: options.headerTableSize,
|
|
119
|
+
// RFC 8441. On by default: a server that can accept WebSocket over
|
|
120
|
+
// HTTP/2 should say so, or browsers open a second HTTP/1.1 connection
|
|
121
|
+
// instead. Passing false turns it off for a server that has no
|
|
122
|
+
// WebSocket endpoint and no reason to invite extended CONNECT.
|
|
123
|
+
enableConnectProtocol: options.enableConnectProtocol,
|
|
124
|
+
// DATA is held while the socket's unsent bytes are over this mark — the
|
|
125
|
+
// flow-control windows bound what the peer may receive, this what we
|
|
126
|
+
// queue for it (see flushStream in h2.js).
|
|
127
|
+
isWriteBlocked: function () { return socket.writableLength >= SOCKET_HIGH_WATER; },
|
|
128
|
+
// Credit is returned when the application reads, not when the frame
|
|
129
|
+
// arrives. Without this the receive window never closes and a client can
|
|
130
|
+
// push a body far faster than the handler consumes it — the bytes pile
|
|
131
|
+
// up in our heap and flow control does nothing at all.
|
|
132
|
+
autoWindowUpdate: false
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
var streams = {}; // id -> { req, res, deadline }
|
|
136
|
+
var served = 0; // requests on this connection (maxRequestsPerSocket)
|
|
137
|
+
var destroyed = false;
|
|
138
|
+
|
|
139
|
+
// ---- How long a request may take to arrive ----
|
|
140
|
+
//
|
|
141
|
+
// A header block left open (HEADERS without END_HEADERS) holds the
|
|
142
|
+
// connection: no other frame may come until its CONTINUATIONs end it
|
|
143
|
+
// (§6.10). Trickled, it held every other request on the connection with
|
|
144
|
+
// it, and nothing timed it. Past headersTimeout the connection goes —
|
|
145
|
+
// there is no stream to give up instead.
|
|
146
|
+
var headerBlockTimer = null;
|
|
147
|
+
h2.on('header_block_open', function () {
|
|
148
|
+
if (headerBlockTimer) clearTimeout(headerBlockTimer);
|
|
149
|
+
headerBlockTimer = startDeadline(headersTimeout, function () {
|
|
150
|
+
headerBlockTimer = null;
|
|
151
|
+
if (destroyed) return;
|
|
152
|
+
h2.sendGoaway(F.ERROR.NO_ERROR);
|
|
153
|
+
destroy(new Error('timeout: header block not completed within ' + headersTimeout + ' ms'), true);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
h2.on('header_block_closed', function () {
|
|
157
|
+
if (headerBlockTimer) { clearTimeout(headerBlockTimer); headerBlockTimer = null; }
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// A request whose body has not all arrived within requestTimeout of its
|
|
161
|
+
// head (CONNECT excepted — a tunnel): the stream is reset (CANCEL), both ways, and the request and its
|
|
162
|
+
// response end as they do when the client cancels — 'aborted', and a
|
|
163
|
+
// 'streamError' for the log. Only this stream: the connection serves on.
|
|
164
|
+
// (HTTP/1.1 answers 408 and closes; a stream of its own needs no more
|
|
165
|
+
// than its reset.)
|
|
166
|
+
function armRequestDeadline(streamId, st) {
|
|
167
|
+
st.deadline = startDeadline(requestTimeout, function () {
|
|
168
|
+
st.deadline = null;
|
|
169
|
+
if (streams[streamId] !== st || st.req.complete) return;
|
|
170
|
+
delete streams[streamId];
|
|
171
|
+
h2.sendRstStream(streamId, F.ERROR.CANCEL);
|
|
172
|
+
abortStream(st, 'request not received in full within ' + requestTimeout + ' ms', 'ERR_HTTP_REQUEST_TIMEOUT');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function clearRequestDeadline(st) {
|
|
176
|
+
if (st && st.deadline) { clearTimeout(st.deadline); st.deadline = null; }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Accumulates the connection's HTTP/2 fingerprint from the opening frames.
|
|
180
|
+
// Attached before execute() is ever called, because everything it needs
|
|
181
|
+
// arrives in the peer's first flight — the fingerprint exists before a
|
|
182
|
+
// single byte of content has been served, which is what makes it useful
|
|
183
|
+
// for deciding whether to serve any.
|
|
184
|
+
var getFingerprint = observeH2(h2);
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What a rate limiter is handed: enough to build a key from, and the two
|
|
188
|
+
* ways to refuse.
|
|
189
|
+
*
|
|
190
|
+
* The methods are named identically to the HTTP/3 equivalents even though
|
|
191
|
+
* the wire mechanisms differ — RST_STREAM on a frame here, RESET_STREAM on
|
|
192
|
+
* a QUIC stream there — so that one piece of limiting code works against
|
|
193
|
+
* both without branching on version.
|
|
194
|
+
*/
|
|
195
|
+
var connHandle = {
|
|
196
|
+
httpVersion: '2.0',
|
|
197
|
+
get remoteAddress() { return socket.remoteAddress; },
|
|
198
|
+
get remotePort() { return socket.remotePort; },
|
|
199
|
+
get encrypted() { return !!socket.encrypted; },
|
|
200
|
+
// The session ticket: { data, resumed, offered } (see lemon-tls
|
|
201
|
+
// getTicketInfo); null over cleartext.
|
|
202
|
+
getTicketInfo: function () { return socket.getTicketInfo ? socket.getTicketInfo() : null; },
|
|
203
|
+
isSessionReused: function () { return !!(socket.isSessionReused && socket.isSessionReused()); },
|
|
204
|
+
get activeStreams() { return Object.keys(streams).length; },
|
|
205
|
+
fingerprint: getFingerprint,
|
|
206
|
+
|
|
207
|
+
sendRstStream: function (streamId, code) {
|
|
208
|
+
h2.sendRstStream(streamId, translateStreamError(code));
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// Stops new streams; in-flight ones finish. See the note in h2.js.
|
|
212
|
+
goaway: function (code) { h2.goaway(translateConnError(code)); },
|
|
213
|
+
|
|
214
|
+
// For when stopping is not enough.
|
|
215
|
+
destroy: function () { destroy(null); }
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
h2.on('stream_open', function (streamId) {
|
|
219
|
+
ev.emit('stream_open', connHandle, streamId);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Rapid Reset: past streamResetBurst / streamResetRate, GOAWAY
|
|
223
|
+
// ENHANCE_YOUR_CALM and the connection closed, as node:http2 does.
|
|
224
|
+
var resetTooMany = streamResetLimiter(options);
|
|
225
|
+
h2.on('stream_reset', function (streamId, code) {
|
|
226
|
+
ev.emit('stream_reset', connHandle, streamId, code);
|
|
227
|
+
if (destroyed || !resetTooMany()) return;
|
|
228
|
+
var rr = new Error('too many stream resets (streamResetBurst, streamResetRate)');
|
|
229
|
+
rr.code = 'ERR_HTTP2_STREAM_RESET_RATE';
|
|
230
|
+
try { h2.goaway(F.ERROR.ENHANCE_YOUR_CALM); } catch (e) {}
|
|
231
|
+
ev.emit('sessionError', rr, connHandle);
|
|
232
|
+
destroy(rr, true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// The response is complete — END_STREAM written — and the request is
|
|
236
|
+
// not (RFC 9113 §8.1): a 413, a 401, a refusal answered before the upload
|
|
237
|
+
// is read. Unless the application is reading the body, the client is
|
|
238
|
+
// told to stop sending it: RST_STREAM with NO_ERROR, after the response,
|
|
239
|
+
// which a client must not take as a failure of it. node:http2 decides it
|
|
240
|
+
// the same way (the stream is closed if nothing has read from it). The
|
|
241
|
+
// stream used to stay open: the client went on uploading into a window
|
|
242
|
+
// nobody would open again, and held one of its concurrent streams until
|
|
243
|
+
// it gave up.
|
|
244
|
+
h2.on('local_end', function (streamId) {
|
|
245
|
+
var st = streams[streamId]; // taken now: the response's end removes the entry
|
|
246
|
+
if (!st || !st.req || st.duplex) return;
|
|
247
|
+
process.nextTick(function () {
|
|
248
|
+
var req = st.req;
|
|
249
|
+
if (req.complete || req.aborted || destroyed) return;
|
|
250
|
+
if (!req.destroyed && req.readableFlowing !== null) return; // still being read
|
|
251
|
+
if (streams[streamId] === st) delete streams[streamId];
|
|
252
|
+
clearRequestDeadline(st);
|
|
253
|
+
h2.sendRstStream(streamId, F.ERROR.NO_ERROR);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
connections.add(socket);
|
|
258
|
+
|
|
259
|
+
// `linger`: end the socket rather than destroy it — what was written last
|
|
260
|
+
// (the GOAWAY saying why) leaves first — and destroy it if the peer has
|
|
261
|
+
// not closed its side in time. destroy() right after end() dropped the
|
|
262
|
+
// GOAWAY the comment below meant to deliver.
|
|
263
|
+
function destroy(err, linger) {
|
|
264
|
+
if (destroyed) return;
|
|
265
|
+
destroyed = true;
|
|
266
|
+
connections.delete(socket);
|
|
267
|
+
if (headerBlockTimer) { clearTimeout(headerBlockTimer); headerBlockTimer = null; }
|
|
268
|
+
for (var id in streams) {
|
|
269
|
+
var st = streams[id];
|
|
270
|
+
clearRequestDeadline(st);
|
|
271
|
+
// An extended-CONNECT tunnel (a WebSocket, CONNECT-UDP) goes with the
|
|
272
|
+
// connection: its Duplex is destroyed. Its request had ended at the
|
|
273
|
+
// accept, so the line below never reached it — the tunnel's user
|
|
274
|
+
// waited for an end that could not come.
|
|
275
|
+
if (st.duplex) { try { st.duplex.destroy(); } catch (e) {} continue; }
|
|
276
|
+
// 'aborted', an Error only to a listener, 'close' (see _abort); the
|
|
277
|
+
// response, mid-write, closed (_abortResponse).
|
|
278
|
+
if (st.req && !st.req.complete) st.req._abort(err || null);
|
|
279
|
+
if (st.res) st.res._abortResponse();
|
|
280
|
+
}
|
|
281
|
+
streams = {};
|
|
282
|
+
if (!linger) { try { socket.destroy(); } catch (e) {} return; }
|
|
283
|
+
try { socket.end(); } catch (e) {}
|
|
284
|
+
var t = setTimeout(function () { try { socket.destroy(); } catch (e) {} }, 1000);
|
|
285
|
+
if (t.unref) t.unref();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// An HTTP/2 connection's errors are 'sessionError' (err, session), as
|
|
289
|
+
// node:http2 reports them — not 'clientError' (err, socket), which is
|
|
290
|
+
// HTTP/1.1's, and whose listeners answer by writing an HTTP/1.1 status
|
|
291
|
+
// line onto the socket: on an HTTP/2 connection, after its GOAWAY, that is
|
|
292
|
+
// bytes the client cannot parse.
|
|
293
|
+
h2.on('send', function (data) {
|
|
294
|
+
if (destroyed || !socket.writable) return;
|
|
295
|
+
corkForTick(socket);
|
|
296
|
+
try { socket.write(data); } catch (e) { destroy(e); return; }
|
|
297
|
+
// What is queued on the socket and not yet sent, bounded per connection
|
|
298
|
+
// (node's maxSessionMemory, in MB, default 10). DATA waits for the
|
|
299
|
+
// socket (isWriteBlocked), so what can pile up here is everything else —
|
|
300
|
+
// replies the peer provokes (PING and SETTINGS acknowledgements, resets)
|
|
301
|
+
// while it does not read. Past the bound the connection goes.
|
|
302
|
+
if (socket.writableLength > maxSessionMemory) {
|
|
303
|
+
var e = new Error('peer is not reading: ' + socket.writableLength + ' bytes queued');
|
|
304
|
+
e.code = 'ERR_HTTP2_SESSION_MEMORY';
|
|
305
|
+
ev.emit('sessionError', e, connHandle);
|
|
306
|
+
destroy(e);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// DATA was waiting for the socket; it has room again.
|
|
311
|
+
socket.on('drain', function () { if (!destroyed) h2.flushAll(); });
|
|
312
|
+
|
|
313
|
+
h2.on('error', function (err) {
|
|
314
|
+
ev.emit('sessionError', err, connHandle);
|
|
315
|
+
// GOAWAY has already been queued by the connection layer; let it reach
|
|
316
|
+
// the peer before the socket goes, so the client learns why.
|
|
317
|
+
destroy(err, true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
socket.on('data', function (chunk) {
|
|
321
|
+
if (destroyed) return;
|
|
322
|
+
try { h2.execute(chunk); }
|
|
323
|
+
catch (err) { ev.emit('sessionError', err, connHandle); destroy(err); }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
socket.on('error', function (err) {
|
|
327
|
+
ev.emit('sessionError', err, connHandle);
|
|
328
|
+
destroy(err);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
socket.on('close', function () { destroy(null); });
|
|
332
|
+
|
|
333
|
+
// The client half-closes (TCP FIN, or TLS close_notify): it will send
|
|
334
|
+
// nothing more, and still reads (RFC 9293 §3.6; RFC 9113 has nothing
|
|
335
|
+
// against it). The connection was destroyed on the spot, and every
|
|
336
|
+
// response in progress with it. Now: a request whose body was still
|
|
337
|
+
// arriving cannot complete — reset (CANCEL), the request 'aborted';
|
|
338
|
+
// the ones fully received are answered; and when the last stream closes,
|
|
339
|
+
// GOAWAY and our own end. The socket is half-open for this connection
|
|
340
|
+
// only (HTTP/1.1 ends its socket with the client's, as node:http does).
|
|
341
|
+
socket.allowHalfOpen = true;
|
|
342
|
+
var peerEnded = false;
|
|
343
|
+
function finishIfDone() {
|
|
344
|
+
if (destroyed || !peerEnded || h2.activeStreams > 0) return;
|
|
345
|
+
try { h2.sendGoaway(F.ERROR.NO_ERROR); } catch (e) {}
|
|
346
|
+
destroy(null, true);
|
|
347
|
+
}
|
|
348
|
+
socket.on('end', function () {
|
|
349
|
+
if (destroyed) return;
|
|
350
|
+
peerEnded = true;
|
|
351
|
+
if (h2.windowBlocked()) { destroy(null); return; } // see 'window_blocked' below
|
|
352
|
+
for (var id in streams) {
|
|
353
|
+
var st = streams[id];
|
|
354
|
+
// A tunnel (WebSocket, CONNECT-UDP) cannot go on: nothing more can
|
|
355
|
+
// come from the client on it. Reset and destroyed — left open, it
|
|
356
|
+
// held the connection half-open for good (finishIfDone waits for it).
|
|
357
|
+
if (st.duplex) {
|
|
358
|
+
delete streams[id];
|
|
359
|
+
h2.sendRstStream(Number(id), F.ERROR.CANCEL);
|
|
360
|
+
try { st.duplex.destroy(); } catch (e) {}
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (st.req && !st.req.complete && !st.req.aborted) {
|
|
364
|
+
delete streams[id];
|
|
365
|
+
clearRequestDeadline(st);
|
|
366
|
+
h2.sendRstStream(Number(id), F.ERROR.CANCEL);
|
|
367
|
+
st.req._abort(null);
|
|
368
|
+
if (st.res) st.res._abortResponse();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
finishIfDone();
|
|
372
|
+
});
|
|
373
|
+
h2.on('stream_close', function (id) {
|
|
374
|
+
// A tunnel closed both ways: its entry goes (nothing else removes it —
|
|
375
|
+
// a long-lived connection kept every WebSocket it ever carried).
|
|
376
|
+
var st = streams[id];
|
|
377
|
+
if (st && st.duplex) delete streams[id];
|
|
378
|
+
if (peerEnded) process.nextTick(finishIfDone);
|
|
379
|
+
});
|
|
380
|
+
// A response that waits for the client to open a window, from a client
|
|
381
|
+
// that has half-closed: it sends nothing more — no WINDOW_UPDATE either —
|
|
382
|
+
// so the response can never finish, and the connection would be held
|
|
383
|
+
// open for good (a client that destroyed its socket looks the same until
|
|
384
|
+
// a write reaches it, and none would). Over, as before half-close.
|
|
385
|
+
h2.on('window_blocked', function () {
|
|
386
|
+
if (!peerEnded || destroyed) return;
|
|
387
|
+
process.nextTick(function () { if (!destroyed) destroy(null); });
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
// ---- Requests ----
|
|
392
|
+
|
|
393
|
+
h2.on('headers', function (streamId, headers, endStream, connectProtocol) {
|
|
394
|
+
|
|
395
|
+
// ---- Extended CONNECT (RFC 8441) ----
|
|
396
|
+
//
|
|
397
|
+
// A CONNECT carrying `:protocol` is not a request/response exchange but
|
|
398
|
+
// a bidirectional channel — this is how WebSocket runs over HTTP/2. The
|
|
399
|
+
// application gets it through the SAME 'upgrade' event that HTTP/1.1
|
|
400
|
+
// uses, so a WebSocketServer attached to this server works over both
|
|
401
|
+
// without knowing which carried the connection.
|
|
402
|
+
//
|
|
403
|
+
// What it gets is a Duplex over the stream rather than a socket: reads
|
|
404
|
+
// are DATA frames, writes are DATA frames, and the HTTP/2 connection
|
|
405
|
+
// underneath keeps serving every other stream. That is the whole
|
|
406
|
+
// advantage over HTTP/1.1, where an upgrade consumes the connection.
|
|
407
|
+
if (connectProtocol) {
|
|
408
|
+
var parsedC = pseudoHeadersToRequest(headers);
|
|
409
|
+
var creq = new IncomingMessage({
|
|
410
|
+
httpVersion: '2.0',
|
|
411
|
+
method: 'CONNECT',
|
|
412
|
+
url: parsedC.url,
|
|
413
|
+
headers: parsedC.headers,
|
|
414
|
+
streamId: streamId,
|
|
415
|
+
socket: socket
|
|
416
|
+
});
|
|
417
|
+
creq.rawHeaders = [];
|
|
418
|
+
creq.headers[':protocol'] = connectProtocol;
|
|
419
|
+
}
|
|
420
|
+
// Not taken up: an ordinary request, answered by the handler — as
|
|
421
|
+
// node:http does with an upgrade nobody asked for.
|
|
422
|
+
if (connectProtocol && !shouldUpgradeCallback(creq)) connectProtocol = null;
|
|
423
|
+
if (connectProtocol) {
|
|
424
|
+
creq._endData();
|
|
425
|
+
|
|
426
|
+
var stream = makeStreamDuplex(streamId);
|
|
427
|
+
streams[streamId] = { req: creq, res: null, duplex: stream };
|
|
428
|
+
|
|
429
|
+
if (ev.listenerCount('upgrade') === 0) {
|
|
430
|
+
// Accepted by shouldUpgradeCallback, yet nobody listens. Where
|
|
431
|
+
// node:http destroys the socket, here only this stream is lost: §4
|
|
432
|
+
// says to answer with an error status rather than leave the client
|
|
433
|
+
// waiting for a 200 that will never come.
|
|
434
|
+
h2.sendHeaders(streamId, [[':status', '501']], true);
|
|
435
|
+
delete streams[streamId];
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
ev.emit('upgrade', creq, stream, Buffer.alloc(0));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
var parsed = pseudoHeadersToRequest(headers);
|
|
443
|
+
|
|
444
|
+
var req = new IncomingMessage({
|
|
445
|
+
httpVersion: '2.0',
|
|
446
|
+
method: parsed.method,
|
|
447
|
+
url: parsed.url,
|
|
448
|
+
headers: parsed.headers,
|
|
449
|
+
streamId: streamId,
|
|
450
|
+
socket: socket
|
|
451
|
+
});
|
|
452
|
+
req.scheme = headers[':scheme'];
|
|
453
|
+
req.authority = headers[':authority'];
|
|
454
|
+
// Its HEADERS read from 0-RTT data (TLS early data): see h1_server.
|
|
455
|
+
req.earlyData = socket.isEarlyData === true;
|
|
456
|
+
// Per-connection, not per-request: the same function on every request of
|
|
457
|
+
// a connection returns the same fingerprint, because it describes how
|
|
458
|
+
// the connection was set up.
|
|
459
|
+
req.h2Fingerprint = getFingerprint;
|
|
460
|
+
|
|
461
|
+
var res = new ServerResponse({ streamId: streamId, socket: socket });
|
|
462
|
+
res.req = req;
|
|
463
|
+
streams[streamId] = { req: req, res: res };
|
|
464
|
+
|
|
465
|
+
// Back-pressure: a Readable that nobody is reading must slow the peer
|
|
466
|
+
// down, and the only lever h2 has is the flow-control window. Credit
|
|
467
|
+
// goes back as the application READS: from _read, which the stream
|
|
468
|
+
// calls when its consumer wants more. Bytes that arrived are counted in
|
|
469
|
+
// (see 'data' below) and returned there.
|
|
470
|
+
//
|
|
471
|
+
// This was a 'data' listener, which says the same in its comment and
|
|
472
|
+
// the opposite in effect: a 'data' listener switches a Readable to
|
|
473
|
+
// flowing, so every body chunk was emitted the moment it arrived — with
|
|
474
|
+
// nobody listening yet if the handler did anything asynchronous before
|
|
475
|
+
// reading the body (an auth check, a lookup). The chunks and the 'end'
|
|
476
|
+
// were lost, and the request waited forever for a body it had already
|
|
477
|
+
// been given. node:http keeps a request paused until someone reads it;
|
|
478
|
+
// so does this now.
|
|
479
|
+
var uncredited = 0;
|
|
480
|
+
req._h2Arrived = function (n) { uncredited += n; };
|
|
481
|
+
req._read = function () {
|
|
482
|
+
if (uncredited > 0) { h2.consume(streamId, uncredited); uncredited = 0; }
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
wireResponse(streamId, req, res);
|
|
486
|
+
|
|
487
|
+
if (endStream) req._endData();
|
|
488
|
+
else if (req.method !== 'CONNECT') armRequestDeadline(streamId, streams[streamId]); // a tunnel stays open
|
|
489
|
+
|
|
490
|
+
// A handler that throws loses its own stream, reset INTERNAL_ERROR —
|
|
491
|
+
// not the connection, and not the other requests on it (see
|
|
492
|
+
// runRequestHandler). A response it had already ended is left to finish.
|
|
493
|
+
if (maxRequests > 0 && ++served === maxRequests) h2.goaway(F.ERROR.NO_ERROR);
|
|
494
|
+
runRequestHandler(function () {
|
|
495
|
+
if (typeof handler === 'function') handler(req, res);
|
|
496
|
+
ev.emit('request', req, res);
|
|
497
|
+
}, function () {
|
|
498
|
+
if (res.writableEnded || streams[streamId] === undefined) return;
|
|
499
|
+
clearRequestDeadline(streams[streamId]);
|
|
500
|
+
delete streams[streamId];
|
|
501
|
+
h2.sendRstStream(streamId, F.ERROR.INTERNAL_ERROR);
|
|
502
|
+
req._abort(null);
|
|
503
|
+
res._abortResponse();
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
h2.on('data', function (streamId, chunk) {
|
|
508
|
+
var st = streams[streamId];
|
|
509
|
+
if (!st) return;
|
|
510
|
+
if (st.duplex) { st.duplex.push(chunk); h2.consume(streamId, chunk.length); return; }
|
|
511
|
+
if (st.req) {
|
|
512
|
+
// Counted before the push: a consumer already reading pulls
|
|
513
|
+
// synchronously from inside it, and _read hands the credit back.
|
|
514
|
+
st.req._h2Arrived(chunk.length);
|
|
515
|
+
st.req._pushData(chunk);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
h2.on('trailers', function (streamId, trailers) {
|
|
520
|
+
var st = streams[streamId];
|
|
521
|
+
if (st && st.req) st.req.trailers = trailers;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
h2.on('end', function (streamId) {
|
|
525
|
+
var st = streams[streamId];
|
|
526
|
+
if (!st) return;
|
|
527
|
+
if (st.duplex) { st.duplex.push(null); return; }
|
|
528
|
+
clearRequestDeadline(st);
|
|
529
|
+
if (st.req) st.req._endData();
|
|
530
|
+
if (st.responseDone) delete streams[streamId];
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
h2.on('rst', function (streamId, code) {
|
|
534
|
+
var st = streams[streamId];
|
|
535
|
+
if (!st) return;
|
|
536
|
+
delete streams[streamId];
|
|
537
|
+
clearRequestDeadline(st);
|
|
538
|
+
if (st.duplex) { st.duplex.destroy(); return; }
|
|
539
|
+
abortStream(st, 'stream reset: ' + (F.ERROR_NAME[code] || code),
|
|
540
|
+
'ERR_HTTP2_STREAM_ERROR');
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* End a request that the network killed under it.
|
|
545
|
+
*
|
|
546
|
+
* A reset arrives because the PEER cancelled, which is ordinary traffic —
|
|
547
|
+
* a browser navigating away resets every in-flight stream. Destroying the
|
|
548
|
+
* IncomingMessage with an Error means an application that never registered
|
|
549
|
+
* an 'error' handler takes the whole process down with it, and almost none
|
|
550
|
+
* do: node:http teaches people to listen for 'aborted', not 'error'.
|
|
551
|
+
*
|
|
552
|
+
* So: 'aborted' always, an Error only to a listener that asked for one, and
|
|
553
|
+
* the server's own 'streamError' for logging either way. h2spec found this
|
|
554
|
+
* on its fourth test — it resets a stream as a matter of course, and the
|
|
555
|
+
* server died.
|
|
556
|
+
*/
|
|
557
|
+
function abortStream(st, message, code) {
|
|
558
|
+
var err = new Error(message);
|
|
559
|
+
err.code = code;
|
|
560
|
+
|
|
561
|
+
if (st.req) st.req._abort(err); // 'aborted', an Error only to a listener, 'close'
|
|
562
|
+
// The response gets 'close' only, as node:http's and node:http2's do
|
|
563
|
+
// when the client goes. An 'error' here was guarded by "a listener
|
|
564
|
+
// asked for it" — but pipe() is such a listener, and rethrows when it
|
|
565
|
+
// is the only one: a client cancelling a piped response took the
|
|
566
|
+
// server down.
|
|
567
|
+
if (st.res) st.res._abortResponse();
|
|
568
|
+
ev.emit('streamError', err, st.req, st.res);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
h2.on('goaway', function () {
|
|
572
|
+
// The client will not open new streams; existing ones run to completion
|
|
573
|
+
// and the socket closes when they do.
|
|
574
|
+
closingConnection = true;
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
var closingConnection = false;
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* A Duplex over one HTTP/2 stream, for extended CONNECT.
|
|
582
|
+
*
|
|
583
|
+
* ws_server was written against a socket: it calls write(), listens for
|
|
584
|
+
* 'data', and destroys when finished. Giving it this instead of a socket
|
|
585
|
+
* is the entire integration — the WebSocket framing code never learns
|
|
586
|
+
* whether its bytes are TCP or DATA frames.
|
|
587
|
+
*
|
|
588
|
+
* The `_h2Accept` hook is the one thing a socket does not have: an HTTP/2
|
|
589
|
+
* extended CONNECT is accepted with a 200 response rather than a 101, so
|
|
590
|
+
* the layer above needs a way to say "accepted" that is not writing bytes.
|
|
591
|
+
*/
|
|
592
|
+
function makeStreamDuplex(streamId) {
|
|
593
|
+
var stream = new Duplex({
|
|
594
|
+
read: function () {},
|
|
595
|
+
write: function (chunk, encoding, callback) {
|
|
596
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
|
|
597
|
+
var more = h2.sendData(streamId, chunk, false);
|
|
598
|
+
if (more) { callback(); return; }
|
|
599
|
+
// Send window closed: hold the callback so back-pressure reaches
|
|
600
|
+
// whoever is writing, exactly as a socket would.
|
|
601
|
+
var released = false;
|
|
602
|
+
function onDrain(id) {
|
|
603
|
+
if (id !== streamId || released) return;
|
|
604
|
+
released = true;
|
|
605
|
+
h2.off('drain', onDrain);
|
|
606
|
+
callback();
|
|
607
|
+
}
|
|
608
|
+
h2.on('drain', onDrain);
|
|
609
|
+
},
|
|
610
|
+
final: function (callback) {
|
|
611
|
+
h2.sendData(streamId, null, true);
|
|
612
|
+
callback();
|
|
613
|
+
},
|
|
614
|
+
// Destroyed by its user while the stream is open: RST_STREAM CANCEL,
|
|
615
|
+
// so the peer hears the tunnel is gone. (Destroyed by a reset or the
|
|
616
|
+
// connection's end, the entry is already gone and nothing is sent.)
|
|
617
|
+
destroy: function (err, callback) {
|
|
618
|
+
var st = streams[streamId];
|
|
619
|
+
if (st && st.duplex === stream && !destroyed) {
|
|
620
|
+
delete streams[streamId];
|
|
621
|
+
try { h2.sendRstStream(streamId, F.ERROR.CANCEL); } catch (e) {}
|
|
622
|
+
}
|
|
623
|
+
callback(err);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
stream._streamId = streamId;
|
|
628
|
+
stream.encrypted = !!socket.encrypted;
|
|
629
|
+
stream.remoteAddress = socket.remoteAddress;
|
|
630
|
+
stream.remotePort = socket.remotePort;
|
|
631
|
+
stream.setNoDelay = function () { return stream; };
|
|
632
|
+
stream.setKeepAlive = function () { return stream; };
|
|
633
|
+
stream.setTimeout = function () { return stream; };
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Accept the extended CONNECT.
|
|
637
|
+
*
|
|
638
|
+
* HTTP/2 has no 101: RFC 8441 §4 says a 2xx response accepts the channel
|
|
639
|
+
* and anything else refuses it. ws_server calls this instead of writing
|
|
640
|
+
* an HTTP/1.1 status line — which is why the WebSocket handshake code
|
|
641
|
+
* has to know which transport it is on for exactly one line, and no more.
|
|
642
|
+
*/
|
|
643
|
+
stream._h2Accept = function (extraHeaders) {
|
|
644
|
+
var list = [[':status', '200']];
|
|
645
|
+
for (var n in (extraHeaders || {})) list.push([String(n).toLowerCase(), extraHeaders[n]]);
|
|
646
|
+
h2.sendHeaders(streamId, list, false);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/** Refuse it, with a status (and fields: a Proxy-Status, say). */
|
|
650
|
+
stream._h2Reject = function (status, message, extraHeaders) {
|
|
651
|
+
var list = [[':status', String(status)]];
|
|
652
|
+
for (var n in (extraHeaders || {})) list.push([String(n).toLowerCase(), String(extraHeaders[n])]);
|
|
653
|
+
h2.sendHeaders(streamId, list, !message);
|
|
654
|
+
if (message) h2.sendData(streamId, Buffer.from(String(message)), true);
|
|
655
|
+
delete streams[streamId];
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
return stream;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
// ============================================================
|
|
663
|
+
// Response wiring
|
|
664
|
+
//
|
|
665
|
+
// ServerResponse is transport-agnostic — h1_server overrides these same
|
|
666
|
+
// three hooks to write HTTP/1.1 bytes, h3_server to write H3 frames.
|
|
667
|
+
// ============================================================
|
|
668
|
+
|
|
669
|
+
function wireResponse(streamId, req, res) {
|
|
670
|
+
var bodyAllowed = true;
|
|
671
|
+
var finished = false;
|
|
672
|
+
|
|
673
|
+
res._flushHeaders = function () {
|
|
674
|
+
if (res.headersSent || res._aborted || destroyed) return;
|
|
675
|
+
var headers = res.getHeaders();
|
|
676
|
+
var code = res.statusCode;
|
|
677
|
+
|
|
678
|
+
// HTTP/2 has no reason phrase and no connection-specific fields.
|
|
679
|
+
// buildResponsePseudoHeaders drops the latter; sending them would make
|
|
680
|
+
// the message malformed for the peer and, worse, would be forwarded by
|
|
681
|
+
// a proxy into an HTTP/1.1 hop where they change the framing.
|
|
682
|
+
delete headers['connection'];
|
|
683
|
+
delete headers['keep-alive'];
|
|
684
|
+
delete headers['transfer-encoding'];
|
|
685
|
+
delete headers['upgrade'];
|
|
686
|
+
|
|
687
|
+
bodyAllowed = !statusHasNoBody(code) && req.method !== 'HEAD';
|
|
688
|
+
|
|
689
|
+
if (!('date' in headers)) headers['date'] = httpDate();
|
|
690
|
+
// A function when the server was built before its port was known —
|
|
691
|
+
// the unified server builds its layers at creation and learns the
|
|
692
|
+
// port from listen(), or from options.port when fed from outside.
|
|
693
|
+
var altSvcNow = typeof altSvc === 'function' ? altSvc() : altSvc;
|
|
694
|
+
if (altSvcNow && !('alt-svc' in headers)) headers['alt-svc'] = altSvcNow;
|
|
695
|
+
|
|
696
|
+
var list = buildResponsePseudoHeaders(code, headers);
|
|
697
|
+
try { h2.sendHeaders(streamId, list, false); }
|
|
698
|
+
catch (e) { destroy(e); return; }
|
|
699
|
+
res.headersSent = true;
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
res._write = function (chunk, encoding, callback) {
|
|
703
|
+
// The peer reset this stream while the handler was still producing.
|
|
704
|
+
// Discard rather than error: the handler is mid-flight and cannot be
|
|
705
|
+
// expected to check on every write.
|
|
706
|
+
if (res._aborted || destroyed) { callback(); return; }
|
|
707
|
+
if (!res.headersSent) res._flushHeaders();
|
|
708
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
|
|
709
|
+
|
|
710
|
+
// A HEAD response, or a bodiless status: the application still wrote
|
|
711
|
+
// the body it WOULD have sent, and the headers already describe it.
|
|
712
|
+
if (!bodyAllowed) { callback(); return; }
|
|
713
|
+
|
|
714
|
+
var more = h2.sendData(streamId, chunk, false);
|
|
715
|
+
if (more) { callback(); return; }
|
|
716
|
+
|
|
717
|
+
// The send window is closed. Hold the callback until it reopens —
|
|
718
|
+
// that is what makes res.write() return false and a piped Readable
|
|
719
|
+
// pause, instead of queueing the whole body in memory.
|
|
720
|
+
var released = false;
|
|
721
|
+
function onDrain(id) {
|
|
722
|
+
if (id !== streamId || released) return;
|
|
723
|
+
released = true;
|
|
724
|
+
h2.off('drain', onDrain);
|
|
725
|
+
callback();
|
|
726
|
+
}
|
|
727
|
+
h2.on('drain', onDrain);
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
res._final = function (callback) {
|
|
731
|
+
if (res._aborted || destroyed) { callback(); return; }
|
|
732
|
+
if (finished) { callback(); return; }
|
|
733
|
+
finished = true;
|
|
734
|
+
if (!res.headersSent) res._flushHeaders();
|
|
735
|
+
var trailers = trailerFields(res.trailers);
|
|
736
|
+
if (trailers) {
|
|
737
|
+
h2.sendTrailers(streamId, trailers);
|
|
738
|
+
} else {
|
|
739
|
+
h2.sendData(streamId, null, true);
|
|
740
|
+
}
|
|
741
|
+
// The entry goes when the request is over too. Removed here, with the
|
|
742
|
+
// request body still arriving — an application reading it after
|
|
743
|
+
// answering — every DATA frame after the response was dropped, its
|
|
744
|
+
// flow-control credit never returned, and the client's upload stalled.
|
|
745
|
+
var st = streams[streamId];
|
|
746
|
+
if (st) {
|
|
747
|
+
st.responseDone = true;
|
|
748
|
+
if (req.complete || req.aborted) delete streams[streamId];
|
|
749
|
+
}
|
|
750
|
+
callback();
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* 103 Early Hints (RFC 8297): a HEADERS frame with :status 103, the
|
|
755
|
+
* stream left open — the final response follows on it. node:http2's
|
|
756
|
+
* signature: (hints, callback). Nothing once the head has gone.
|
|
757
|
+
*/
|
|
758
|
+
res.writeEarlyHints = function (hints, callback) {
|
|
759
|
+
if (!res.headersSent && !res._aborted && !destroyed) {
|
|
760
|
+
var h = {};
|
|
761
|
+
for (var n in (hints || {})) {
|
|
762
|
+
if (Object.prototype.hasOwnProperty.call(hints, n) && hints[n] != null) h[String(n).toLowerCase()] = hints[n];
|
|
763
|
+
}
|
|
764
|
+
try { h2.sendHeaders(streamId, buildResponsePseudoHeaders(103, h), false); }
|
|
765
|
+
catch (e) { /* the stream went; the final response will find out */ }
|
|
766
|
+
}
|
|
767
|
+
if (typeof callback === 'function') process.nextTick(callback);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* The response's priority, as the server decides it: { urgency (0..7,
|
|
772
|
+
* 0 first), incremental } — RFC 9218, as h3_server's. It replaces what
|
|
773
|
+
* the client asked for, and later requests from it do not override it.
|
|
774
|
+
*/
|
|
775
|
+
res.setPriority = function (p) { h2.setStreamPriority(streamId, responsePriority(p)); };
|
|
776
|
+
|
|
777
|
+
/** Reset the stream — the HTTP/2 way to abandon a response. */
|
|
778
|
+
res.destroyStream = function (code) {
|
|
779
|
+
h2.sendRstStream(streamId, code === undefined ? F.ERROR.CANCEL : code);
|
|
780
|
+
clearRequestDeadline(streams[streamId]);
|
|
781
|
+
delete streams[streamId];
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// The application giving the exchange up — res.destroy([err]),
|
|
785
|
+
// req.destroy([err]), either of which ends the stream on node:http2:
|
|
786
|
+
// the response unfinished, RST_STREAM CANCEL (INTERNAL_ERROR for a
|
|
787
|
+
// failure of ours); complete, RST_STREAM NO_ERROR for the rest of the
|
|
788
|
+
// request body (RFC 9113 §8.1). It was local only: the client waited
|
|
789
|
+
// for a response that would not come, the stream open until the
|
|
790
|
+
// connection closed.
|
|
791
|
+
var giveUp = function (err) {
|
|
792
|
+
if (destroyed || !streams[streamId]) return;
|
|
793
|
+
if (!finished) res.destroyStream(err ? F.ERROR.INTERNAL_ERROR : F.ERROR.CANCEL);
|
|
794
|
+
else if (!req.complete) res.destroyStream(F.ERROR.NO_ERROR);
|
|
795
|
+
};
|
|
796
|
+
res._onDestroy(giveUp);
|
|
797
|
+
req._onDestroy(function (err) { if (!res.writableFinished && !res.destroyed) res.destroy(err); else giveUp(err); });
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// The client speaks first, but the server's SETTINGS must go out
|
|
801
|
+
// immediately — a client may not send anything until it has ours.
|
|
802
|
+
h2.sendPreface();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
// ============================================================
|
|
807
|
+
// Listening
|
|
808
|
+
//
|
|
809
|
+
// Convenience only, for an h2-only server. The unified server builds one
|
|
810
|
+
// tcp_listener and registers this handleConnection under 'h2' alongside
|
|
811
|
+
// h1_server's under 'http/1.1'.
|
|
812
|
+
// ============================================================
|
|
813
|
+
|
|
814
|
+
function listen(port, host, callback) {
|
|
815
|
+
if (typeof host === 'function') { callback = host; host = undefined; }
|
|
816
|
+
|
|
817
|
+
listener = createTcpListener({
|
|
818
|
+
tls: options.tls,
|
|
819
|
+
key: options.key,
|
|
820
|
+
cert: options.cert,
|
|
821
|
+
ca: options.ca,
|
|
822
|
+
SNICallback: options.SNICallback
|
|
823
|
+
});
|
|
824
|
+
listener.register('h2', handleConnection);
|
|
825
|
+
listener.on('error', function (err) { ev.emit('error', err); });
|
|
826
|
+
listener.on('clientError', function (err, sock) { ev.emit('clientError', err, sock); });
|
|
827
|
+
listener.on('tlsClientError', function (err, sock) { ev.emit('tlsClientError', err, sock); });
|
|
828
|
+
listener.listen(port, host, function () {
|
|
829
|
+
ev.emit('listening');
|
|
830
|
+
if (typeof callback === 'function') callback();
|
|
831
|
+
});
|
|
832
|
+
return server;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function close(callback) {
|
|
836
|
+
closing = true;
|
|
837
|
+
if (!listener) {
|
|
838
|
+
for (var s of connections) { try { s.end(); } catch (e) {} }
|
|
839
|
+
if (callback) callback();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
listener.close(callback);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
var server = {
|
|
847
|
+
listen: listen,
|
|
848
|
+
close: close,
|
|
849
|
+
address: function () { return listener && listener.address ? listener.address() : null; },
|
|
850
|
+
handleConnection: handleConnection,
|
|
851
|
+
on: function (n, f) { ev.on(n, f); return server; },
|
|
852
|
+
once: function (n, f) { ev.once(n, f); return server; },
|
|
853
|
+
off: function (n, f) { ev.off(n, f); return server; },
|
|
854
|
+
emit: function () { ev.emit.apply(ev, arguments); return server; },
|
|
855
|
+
listenerCount: function (n) { return ev.listenerCount(n); }
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
return server;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
export { createServer, STREAM_REFUSED, STREAM_CANCELLED, CONN_OVERLOADED };
|