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,779 @@
|
|
|
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
|
+
// quico.connectUdp(proxy, target[, options][, callback]) — CONNECT-UDP's
|
|
17
|
+
// client (RFC 9298)
|
|
18
|
+
//
|
|
19
|
+
// A UDP "socket" to one target, through a proxy: what it returns behaves as
|
|
20
|
+
// a connected dgram.Socket — 'connect', send(msg[, cb]), 'message' (msg,
|
|
21
|
+
// rinfo), close(), 'close', 'error', remoteAddress() — so code written for
|
|
22
|
+
// dgram moves to a proxied path by changing the line that makes the socket.
|
|
23
|
+
//
|
|
24
|
+
// HTTP/3 first: each UDP payload a QUIC DATAGRAM frame (Quarter Stream ID,
|
|
25
|
+
// Context ID 0, the payload) — unreliable, as UDP. If the proxy has not
|
|
26
|
+
// answered QUIC within h3Timeout (UDP blocked on the way, a proxy without
|
|
27
|
+
// HTTP/3), TCP: HTTP/2 extended CONNECT or HTTP/1.1 Upgrade, as ALPN
|
|
28
|
+
// decides, with DATAGRAM capsules on the stream. The capsule format, and the
|
|
29
|
+
// template, are the server's (connect_udp.js).
|
|
30
|
+
//
|
|
31
|
+
// Over HTTP/3 the tunnels to one proxy share a connection (see below);
|
|
32
|
+
// shareConnection: false gives a tunnel one of its own.
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
import { EventEmitter } from 'node:events';
|
|
36
|
+
import dns from 'node:dns';
|
|
37
|
+
import net from 'node:net';
|
|
38
|
+
import lemonTls from 'lemon-tls';
|
|
39
|
+
import { readVarInt, writeVarInt } from './utils.js';
|
|
40
|
+
import { tlsIdentity, globalAgent } from './agent.js';
|
|
41
|
+
import { H3Connection, H3_ERR_NAME } from './h3.js';
|
|
42
|
+
import { H2Connection } from './h2.js';
|
|
43
|
+
import { createQuicClientSocket } from './quic_socket.js';
|
|
44
|
+
import { createCapsuleParser, datagramCapsule, udpPayloadOf, MAX_UDP_PAYLOAD, STREAM_QUEUE_MAX } from './connect_udp.js';
|
|
45
|
+
|
|
46
|
+
var DEFAULT_TEMPLATE_PATH = '/.well-known/masque/udp/{target_host}/{target_port}/';
|
|
47
|
+
var QUEUE_MAX = 64; // payloads sent before the tunnel opened, held
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
// The request's path: the template with the target in it — the host
|
|
51
|
+
// percent-encoded (an IPv6 literal's colons too, §3), the port as a number.
|
|
52
|
+
// A query expansion ({?target_host,target_port}) is filled in as one.
|
|
53
|
+
function expandTemplate(template, host, port) {
|
|
54
|
+
var vals = { target_host: encodeURIComponent(host), target_port: String(port) };
|
|
55
|
+
return template.replace(/\{(\?)?([^}]*)\}/g, function (m, q, names) {
|
|
56
|
+
var list = names.split(',').map(function (s) { return s.trim(); });
|
|
57
|
+
if (q) return '?' + list.map(function (n) { return n + '=' + (vals[n] != null ? vals[n] : ''); }).join('&');
|
|
58
|
+
return list.map(function (n) { return vals[n] != null ? vals[n] : ''; }).join(',');
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The proxy: a URL — its origin, and, when it has {target_host}, the
|
|
63
|
+
// template too (as RFC 9298 clients are configured) — or { hostname, port,
|
|
64
|
+
// template }.
|
|
65
|
+
function parseProxy(proxy, options) {
|
|
66
|
+
var p = {};
|
|
67
|
+
if (typeof proxy === 'string' || proxy instanceof URL) {
|
|
68
|
+
var s = String(proxy);
|
|
69
|
+
var m = /^(https?):\/\/([^/?#]+)(.*)$/i.exec(s);
|
|
70
|
+
if (!m) throw new TypeError('connectUdp: the proxy must be an https:// URL');
|
|
71
|
+
var auth = m[2], rest = m[3] || '';
|
|
72
|
+
var hp = /^\[([^\]]+)\](?::(\d+))?$/.exec(auth) || /^([^:]+)(?::(\d+))?$/.exec(auth);
|
|
73
|
+
if (!hp) throw new TypeError('connectUdp: bad proxy authority ' + JSON.stringify(auth));
|
|
74
|
+
p.hostname = hp[1]; p.port = hp[2] ? Number(hp[2]) : 443;
|
|
75
|
+
p.template = rest.indexOf('{') >= 0 ? rest : (options.template || DEFAULT_TEMPLATE_PATH);
|
|
76
|
+
} else if (proxy && typeof proxy === 'object') {
|
|
77
|
+
p.hostname = proxy.hostname || proxy.host;
|
|
78
|
+
p.port = Number(proxy.port) || 443;
|
|
79
|
+
p.template = proxy.template || options.template || DEFAULT_TEMPLATE_PATH;
|
|
80
|
+
} else throw new TypeError('connectUdp: a proxy URL or { hostname, port } is needed');
|
|
81
|
+
if (!p.hostname) throw new TypeError('connectUdp: the proxy has no host');
|
|
82
|
+
if (p.template.indexOf('{target_host}') < 0 && !/\{\?[^}]*target_host/.test(p.template)) {
|
|
83
|
+
throw new TypeError('connectUdp: the template must contain {target_host} and {target_port}');
|
|
84
|
+
}
|
|
85
|
+
return p;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function connectUdp(proxy, target, options, callback) {
|
|
89
|
+
if (typeof options === 'function') { callback = options; options = null; }
|
|
90
|
+
options = options || {};
|
|
91
|
+
var px = parseProxy(proxy, options);
|
|
92
|
+
if (!target || !target.host) throw new TypeError('connectUdp: target { host, port } is needed');
|
|
93
|
+
var port = Number(target.port);
|
|
94
|
+
if (!(port >= 1 && port <= 65535)) throw new TypeError('connectUdp: target.port must be in 1..65535');
|
|
95
|
+
var protocol = options.protocol || 'auto';
|
|
96
|
+
if (['auto', 'h3', 'h2', 'http/1.1'].indexOf(protocol) < 0) throw new TypeError("connectUdp: protocol is 'auto', 'h3', 'h2' or 'http/1.1'");
|
|
97
|
+
var sock = new UdpProxySocket(px, { host: String(target.host), port: port }, options, protocol);
|
|
98
|
+
if (typeof callback === 'function') sock.once('connect', callback);
|
|
99
|
+
return sock;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
// ============================================================
|
|
104
|
+
// The socket
|
|
105
|
+
// ============================================================
|
|
106
|
+
|
|
107
|
+
function UdpProxySocket(proxy, target, options, protocol) {
|
|
108
|
+
EventEmitter.call(this);
|
|
109
|
+
this._proxy = proxy;
|
|
110
|
+
this._target = target;
|
|
111
|
+
this._options = options;
|
|
112
|
+
// The key-exchange groups to offer (tls.connect's ecdhCurve): a name not
|
|
113
|
+
// implemented throws now, as tls.connect's does.
|
|
114
|
+
if (options && options.ecdhCurve != null) lemonTls.parseEcdhCurve(options.ecdhCurve);
|
|
115
|
+
if (options && options.sigalgs != null) lemonTls.parseSigalgs(options.sigalgs);
|
|
116
|
+
this._wanted = protocol;
|
|
117
|
+
this.protocol = null; // 'h3', 'h2', 'http/1.1' — once the tunnel is open
|
|
118
|
+
this.closeReason = null; // 'local', 'proxy' (it ended the tunnel), 'error'
|
|
119
|
+
this.statusCode = null; // the proxy's answer
|
|
120
|
+
this.responseHeaders = null;
|
|
121
|
+
this._state = 'connecting'; // → 'open' → 'closed'
|
|
122
|
+
this._t = null; // the transport: { send(u8) → 'sent'|'dropped'|'too-big', mode(), maxPayload(), close(), destroy(), address() }
|
|
123
|
+
this._queue = [];
|
|
124
|
+
this._stats = { packetsSent: 0, bytesSent: 0, packetsReceived: 0, bytesReceived: 0, dropped: 0, openedAt: null, closedAt: null };
|
|
125
|
+
this._path = expandTemplate(proxy.template, target.host, target.port);
|
|
126
|
+
// Encrypted Client Hello to the proxy: options.ech, or GREASE ECH unless
|
|
127
|
+
// echGrease is false. Rejected with new keys: once more, with them.
|
|
128
|
+
// The keys the proxy sent on rejecting these before (retry_configs) are
|
|
129
|
+
// kept by the agent and tried first, as quico.request's (client.js).
|
|
130
|
+
this._echOriginal = options.ech || null;
|
|
131
|
+
var echAgent = !this._echOriginal || options.agent === false ? null
|
|
132
|
+
: (options.agent && typeof options.agent._getEchRetry === 'function' ? options.agent : globalAgent);
|
|
133
|
+
this._echAgent = echAgent;
|
|
134
|
+
this._echKey = echAgent ? echAgent._sessionKey('ech', proxy.hostname, proxy.port, {
|
|
135
|
+
servername: options.servername, rejectUnauthorized: options.rejectUnauthorized, ca: options.ca, cert: options.cert, key: options.key,
|
|
136
|
+
ech: options.ech, ecdhCurve: options.ecdhCurve, sigalgs: options.sigalgs
|
|
137
|
+
}) : null;
|
|
138
|
+
this._echKept = this._echKey ? echAgent._getEchRetry(this._echKey) : null;
|
|
139
|
+
this._ech = this._echKept || this._echOriginal;
|
|
140
|
+
this._echRetried = false;
|
|
141
|
+
var self = this;
|
|
142
|
+
process.nextTick(function () { self._start(); });
|
|
143
|
+
}
|
|
144
|
+
Object.setPrototypeOf(UdpProxySocket.prototype, EventEmitter.prototype);
|
|
145
|
+
|
|
146
|
+
// ECH rejected with the proxy's current keys: true when another attempt,
|
|
147
|
+
// with them, has been made.
|
|
148
|
+
UdpProxySocket.prototype._echRetry = function (err, again) {
|
|
149
|
+
if (!err || err.code !== 'ERR_ECH_REJECTED' || this._state !== 'connecting') return false;
|
|
150
|
+
// The kept keys rejected too: forgotten, and the caller's tried again.
|
|
151
|
+
if (this._echKept) {
|
|
152
|
+
this._echAgent._evictEchRetry(this._echKey);
|
|
153
|
+
this._echKept = null;
|
|
154
|
+
this._ech = this._echOriginal;
|
|
155
|
+
again();
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (!err.retryConfigs || this._echRetried) return false;
|
|
159
|
+
this._echRetried = true;
|
|
160
|
+
this._ech = err.retryConfigs;
|
|
161
|
+
again();
|
|
162
|
+
return true;
|
|
163
|
+
};
|
|
164
|
+
UdpProxySocket.prototype._echOptions = function () {
|
|
165
|
+
return { ech: this._ech || undefined, echGrease: !this._ech && this._options.echGrease !== false };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
UdpProxySocket.prototype._start = function () {
|
|
169
|
+
if (this._state !== 'connecting') return;
|
|
170
|
+
if (this._wanted === 'h2' || this._wanted === 'http/1.1') this._startTcp([this._wanted]);
|
|
171
|
+
else this._startH3();
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// The request's own fields: the extended CONNECT's pseudo-headers, the
|
|
175
|
+
// application's (a Proxy-Authorization, say), capsule-protocol.
|
|
176
|
+
UdpProxySocket.prototype._authority = function () {
|
|
177
|
+
var h = this._proxy.hostname;
|
|
178
|
+
if (net.isIPv6(h)) h = '[' + h + ']';
|
|
179
|
+
return h + (this._proxy.port !== 443 ? ':' + this._proxy.port : '');
|
|
180
|
+
};
|
|
181
|
+
UdpProxySocket.prototype._extraHeaders = function () {
|
|
182
|
+
var out = {}, h = this._options.headers || {};
|
|
183
|
+
for (var n in h) out[String(n).toLowerCase()] = String(h[n]);
|
|
184
|
+
if (out['capsule-protocol'] == null) out['capsule-protocol'] = '?1';
|
|
185
|
+
return out;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ---- the proxy answered ----
|
|
189
|
+
|
|
190
|
+
UdpProxySocket.prototype._answered = function (status, headers, protocol) {
|
|
191
|
+
if (this._state !== 'connecting') return;
|
|
192
|
+
this.statusCode = status;
|
|
193
|
+
this.responseHeaders = headers;
|
|
194
|
+
var ok = protocol === 'http/1.1' ? status === 101 : (status >= 200 && status < 300);
|
|
195
|
+
if (!ok) {
|
|
196
|
+
var err = new Error('CONNECT-UDP refused by the proxy: ' + status + (headers['proxy-status'] ? ' (' + headers['proxy-status'] + ')' : ''));
|
|
197
|
+
err.code = 'ERR_CONNECT_UDP_REFUSED';
|
|
198
|
+
err.statusCode = status;
|
|
199
|
+
err.proxyStatus = headers['proxy-status'] || null;
|
|
200
|
+
this._fail(err);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.protocol = protocol;
|
|
204
|
+
this._state = 'open';
|
|
205
|
+
if (this._echRetried && this._echKey) this._echAgent._cacheEchRetry(this._echKey, this._ech);
|
|
206
|
+
this._stats.openedAt = Date.now();
|
|
207
|
+
var q = this._queue; this._queue = null;
|
|
208
|
+
for (var i = 0; i < q.length; i++) this._sendNow(q[i].data, q[i].cb);
|
|
209
|
+
this.emit('connect');
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
UdpProxySocket.prototype._payload = function (u8) {
|
|
213
|
+
if (this._state === 'closed' || !u8) return;
|
|
214
|
+
this._stats.packetsReceived++;
|
|
215
|
+
this._stats.bytesReceived += u8.byteLength;
|
|
216
|
+
var buf = Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength);
|
|
217
|
+
this.emit('message', buf, { address: this._target.host, family: net.isIPv6(this._target.host) ? 'IPv6' : net.isIPv4(this._target.host) ? 'IPv4' : '',
|
|
218
|
+
port: this._target.port, size: buf.length });
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
UdpProxySocket.prototype._fail = function (err) {
|
|
222
|
+
if (this._state === 'closed') return;
|
|
223
|
+
this._finish('error', err);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// The tunnel's end: its stream ended by the proxy, reset, or the connection
|
|
227
|
+
// gone.
|
|
228
|
+
UdpProxySocket.prototype._ended = function (err) {
|
|
229
|
+
if (this._state === 'closed') return;
|
|
230
|
+
if (this._state === 'connecting' && !err) {
|
|
231
|
+
err = new Error('CONNECT-UDP: the proxy closed before answering'); err.code = 'ERR_CONNECT_UDP_CLOSED';
|
|
232
|
+
}
|
|
233
|
+
this._finish(err ? 'error' : 'proxy', err);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
UdpProxySocket.prototype._finish = function (reason, err) {
|
|
237
|
+
this._state = 'closed';
|
|
238
|
+
this.closeReason = reason;
|
|
239
|
+
this._stats.closedAt = Date.now();
|
|
240
|
+
var q = this._queue; this._queue = null;
|
|
241
|
+
for (var i = 0; q && i < q.length; i++) if (q[i].cb) process.nextTick(q[i].cb, notOpen());
|
|
242
|
+
var t = this._t;
|
|
243
|
+
if (t) { if (reason === 'local') t.close(); else t.destroy(); }
|
|
244
|
+
if (err) this.emit('error', err);
|
|
245
|
+
var self = this;
|
|
246
|
+
process.nextTick(function () { self.emit('close'); }); // as a socket's: after 'error', a tick later
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
function notOpen() {
|
|
250
|
+
var e = new Error('Not running'); e.code = 'ERR_SOCKET_DGRAM_NOT_RUNNING'; return e;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---- dgram.Socket's surface ----
|
|
254
|
+
|
|
255
|
+
// send(msg[, offset, length][, callback]) — a connected socket's forms.
|
|
256
|
+
// msg: a Buffer, a TypedArray, a DataView, a string, or an array of them
|
|
257
|
+
// (sent as one payload). Before the tunnel opens, held — as many as
|
|
258
|
+
// QUEUE_MAX — and sent when it does.
|
|
259
|
+
UdpProxySocket.prototype.send = function (msg, offset, length, port, address, callback) {
|
|
260
|
+
var args = Array.prototype.slice.call(arguments);
|
|
261
|
+
var cb = typeof args[args.length - 1] === 'function' ? args.pop() : null;
|
|
262
|
+
// A port (and an address) — told from (offset, length) as dgram tells
|
|
263
|
+
// them: by the address being a string. Not for a connected socket.
|
|
264
|
+
if (args.length === 2 || args.length > 3 || (args.length === 3 && typeof args[2] === 'string')) {
|
|
265
|
+
var ce = new Error('Already connected'); ce.code = 'ERR_SOCKET_DGRAM_IS_CONNECTED'; throw ce;
|
|
266
|
+
}
|
|
267
|
+
var data = toBytes(msg);
|
|
268
|
+
if (args.length === 3) data = data.subarray(Number(args[1]) || 0, (Number(args[1]) || 0) + Number(args[2]));
|
|
269
|
+
if (this._state === 'closed') { if (cb) process.nextTick(cb, notOpen()); return; }
|
|
270
|
+
if (this._state === 'connecting') {
|
|
271
|
+
if (this._queue.length >= QUEUE_MAX) { this._stats.dropped++; if (cb) process.nextTick(cb, null, data.byteLength); return; }
|
|
272
|
+
this._queue.push({ data: Uint8Array.prototype.slice.call(data), cb: cb });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
this._sendNow(data, cb);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
UdpProxySocket.prototype._sendNow = function (data, cb) {
|
|
279
|
+
if (data.byteLength > MAX_UDP_PAYLOAD) { if (cb) process.nextTick(cb, emsgsize()); return; }
|
|
280
|
+
var r = this._t.send(data);
|
|
281
|
+
if (r === 'too-big') { this._stats.dropped++; if (cb) process.nextTick(cb, emsgsize()); return; }
|
|
282
|
+
if (r === 'dropped') this._stats.dropped++; // as the kernel drops on a full queue: no error
|
|
283
|
+
else { this._stats.packetsSent++; this._stats.bytesSent += data.byteLength; }
|
|
284
|
+
if (cb) process.nextTick(cb, null, data.byteLength);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
function emsgsize() {
|
|
288
|
+
var e = new Error('send EMSGSIZE: larger than the tunnel carries (see maxPayloadSize)'); e.code = 'EMSGSIZE'; return e;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function toBytes(msg) {
|
|
292
|
+
if (typeof msg === 'string') return Buffer.from(msg);
|
|
293
|
+
if (Array.isArray(msg)) return Buffer.concat(msg.map(toBytes));
|
|
294
|
+
if (msg instanceof Uint8Array) return msg;
|
|
295
|
+
if (ArrayBuffer.isView(msg)) return new Uint8Array(msg.buffer, msg.byteOffset, msg.byteLength);
|
|
296
|
+
if (msg instanceof ArrayBuffer) return new Uint8Array(msg);
|
|
297
|
+
throw new TypeError('The "msg" argument must be a Buffer, TypedArray, DataView, string or array');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Ends the tunnel: the request stream's end, then the connection's.
|
|
301
|
+
UdpProxySocket.prototype.close = function (callback) {
|
|
302
|
+
if (typeof callback === 'function') this.once('close', callback);
|
|
303
|
+
if (this._state === 'closed') return this;
|
|
304
|
+
this._finish('local', null);
|
|
305
|
+
return this;
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// The target, as a connected dgram socket names its peer.
|
|
309
|
+
UdpProxySocket.prototype.remoteAddress = function () {
|
|
310
|
+
if (this._state !== 'open') { var e = new Error('Not connected'); e.code = 'ERR_SOCKET_DGRAM_NOT_CONNECTED'; throw e; }
|
|
311
|
+
var h = this._target.host;
|
|
312
|
+
return { address: h, family: net.isIPv6(h) ? 'IPv6' : net.isIPv4(h) ? 'IPv4' : '', port: this._target.port };
|
|
313
|
+
};
|
|
314
|
+
// The local end of the connection to the proxy.
|
|
315
|
+
UdpProxySocket.prototype.address = function () {
|
|
316
|
+
return this._t && this._t.address ? this._t.address() : {};
|
|
317
|
+
};
|
|
318
|
+
UdpProxySocket.prototype.ref = function () { return this; };
|
|
319
|
+
UdpProxySocket.prototype.unref = function () { return this; };
|
|
320
|
+
|
|
321
|
+
// The largest payload send() takes now: over HTTP/3 datagrams, what a QUIC
|
|
322
|
+
// DATAGRAM frame can carry on this path; over capsules, a UDP payload's.
|
|
323
|
+
Object.defineProperty(UdpProxySocket.prototype, 'maxPayloadSize', { get: function () {
|
|
324
|
+
return this._t ? this._t.maxPayload() : 0;
|
|
325
|
+
} });
|
|
326
|
+
Object.defineProperty(UdpProxySocket.prototype, 'stats', { get: function () {
|
|
327
|
+
var s = Object.assign({ protocol: this.protocol, transport: this._t ? this._t.mode() : null,
|
|
328
|
+
target: { host: this._target.host, port: this._target.port } }, this._stats);
|
|
329
|
+
s.durationMs = s.openedAt ? (s.closedAt || Date.now()) - s.openedAt : 0;
|
|
330
|
+
return s;
|
|
331
|
+
} });
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
// ============================================================
|
|
335
|
+
// HTTP/3: one connection per proxy, shared by its tunnels
|
|
336
|
+
// ============================================================
|
|
337
|
+
//
|
|
338
|
+
// RFC 9298's tunnels are requests: as many as the proxy allows run over one
|
|
339
|
+
// HTTP/3 connection, each its own request stream, their datagrams told apart
|
|
340
|
+
// by Quarter Stream ID (RFC 9297 §2.1). A socket per connection — as this
|
|
341
|
+
// was — cost each tunnel a handshake (a round trip, two before 0.5 RTT of
|
|
342
|
+
// data on a lossy path), a congestion controller starting from nothing, and
|
|
343
|
+
// a UDP port; a VPN or a game client opening a tunnel per flow paid it per
|
|
344
|
+
// flow. Now a tunnel joins the connection its proxy already has — opened
|
|
345
|
+
// with the same TLS settings (agent.js tlsIdentity) — or waits for the one
|
|
346
|
+
// being made; the connection ends with its last tunnel.
|
|
347
|
+
// shareConnection: false keeps a tunnel on a connection of its own.
|
|
348
|
+
|
|
349
|
+
var h3Connections = new Map(); // pool key → ProxyH3Connection
|
|
350
|
+
var unsharedSeq = 0;
|
|
351
|
+
|
|
352
|
+
function h3PoolKey(proxy, o, ech) {
|
|
353
|
+
if (o.shareConnection === false) return 'unshared#' + (++unsharedSeq);
|
|
354
|
+
return [proxy.hostname, proxy.port, (o.quicVersions || []).join(','), tlsIdentity({
|
|
355
|
+
servername: o.servername, rejectUnauthorized: o.rejectUnauthorized, ca: o.ca, cert: o.cert, key: o.key,
|
|
356
|
+
ech: ech.ech, ecdhCurve: o.ecdhCurve, sigalgs: o.sigalgs
|
|
357
|
+
}), ech.echGrease ? 'grease' : ''].join('|');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// TLS session resumption to the proxy, as quico.request has it: a session
|
|
361
|
+
// from the agent's cache (options.agent, or the global one; agent: false —
|
|
362
|
+
// none), each used once, and each the proxy sends kept there. An explicit
|
|
363
|
+
// options.session wins. Every tunnel began with a full handshake.
|
|
364
|
+
// transport: 'quic', or 'tcp:<alpn>'. A session the handshake then failed
|
|
365
|
+
// with is dropped.
|
|
366
|
+
function proxySessions(o, transport, proxy, ech) {
|
|
367
|
+
var agent = o.agent === false ? null : (o.agent && typeof o.agent._sessionKey === 'function' ? o.agent : globalAgent);
|
|
368
|
+
var key = agent ? agent._sessionKey(transport, proxy.hostname, proxy.port, {
|
|
369
|
+
servername: o.servername, rejectUnauthorized: o.rejectUnauthorized, ca: o.ca, cert: o.cert, key: o.key,
|
|
370
|
+
ech: ech.ech, ecdhCurve: o.ecdhCurve, sigalgs: o.sigalgs
|
|
371
|
+
}) : null;
|
|
372
|
+
var taken = false, offered;
|
|
373
|
+
return {
|
|
374
|
+
take: function () {
|
|
375
|
+
if (o.session) return o.session;
|
|
376
|
+
if (!taken) { taken = true; offered = key ? agent._takeSession(key) : undefined; }
|
|
377
|
+
return offered;
|
|
378
|
+
},
|
|
379
|
+
keep: function (s) { if (key) agent._cacheSession(key, s); },
|
|
380
|
+
failed: function () { if (key && taken && offered && !o.session) agent._evictSession(key); }
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function ProxyH3Connection(key, proxy, o, ech) {
|
|
385
|
+
this.key = key;
|
|
386
|
+
this.state = 'connecting'; // → 'open' → 'closing' | 'closed'
|
|
387
|
+
this.quic = null; this.h3 = null; this.udp = null;
|
|
388
|
+
this.waiters = []; // (err, conn), until the handshake ends
|
|
389
|
+
this.tunnels = new Map(); // request stream ID → the tunnel's handlers
|
|
390
|
+
this.nextSid = 0; // client bidirectional: 0, 4, 8, …
|
|
391
|
+
this.sessions = proxySessions(o, 'quic', proxy, ech);
|
|
392
|
+
var self = this;
|
|
393
|
+
resolve(proxy.hostname, function (err, ip) {
|
|
394
|
+
if (err) { self._failed(err); return; }
|
|
395
|
+
if (self.state !== 'connecting') return;
|
|
396
|
+
self.udp = createQuicClientSocket({
|
|
397
|
+
remoteIp: ip, remotePort: proxy.port, hostname: o.servername || proxy.hostname,
|
|
398
|
+
rejectUnauthorized: o.rejectUnauthorized !== false, ca: o.ca || null,
|
|
399
|
+
key: o.key || null, cert: o.cert || null, passphrase: o.passphrase,
|
|
400
|
+
quicVersions: o.quicVersions,
|
|
401
|
+
startupCap: o.startupCap, pmtuDiscovery: o.pmtuDiscovery, maxUdpPayloadSize: o.maxUdpPayloadSize,
|
|
402
|
+
initialRtt: o.initialRtt, reorderWindow: o.reorderWindow,
|
|
403
|
+
ech: ech.ech, echGrease: ech.echGrease,
|
|
404
|
+
ecdhCurve: o.ecdhCurve,
|
|
405
|
+
sigalgs: o.sigalgs,
|
|
406
|
+
session: self.sessions.take() || null,
|
|
407
|
+
// Each ticket the proxy sends: the agent's, and each tunnel's 'session'.
|
|
408
|
+
onSocket: function (q) {
|
|
409
|
+
self.quic = q;
|
|
410
|
+
// The connection is over the moment either side's CONNECTION_CLOSE is
|
|
411
|
+
// known ('closing'), not when its draining period (RFC 9000 §10.2,
|
|
412
|
+
// some 3 s) ends and 'close' comes: each tunnel on it is told at
|
|
413
|
+
// once, and the connection leaves the pool at once — a tunnel opened
|
|
414
|
+
// meanwhile joined a connection already closed, and failed. As the
|
|
415
|
+
// WebTransport client hears it (r69). Only once open: a handshake
|
|
416
|
+
// that ends is onError's to report — an ECH rejection is retried
|
|
417
|
+
// there with the server's new keys, not failed.
|
|
418
|
+
q.on('closing', function () { if (self.quic === q && self.state === 'open') self._gone(); });
|
|
419
|
+
q.on('session', function (s) {
|
|
420
|
+
self.sessions.keep(s);
|
|
421
|
+
self.tunnels.forEach(function (t) { if (t.session) t.session(s); });
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
onError: function (e) { if (self.state === 'connecting') { self.sessions.failed(); self._failed(e); } },
|
|
425
|
+
onConnect: function (q, s) { self._connected(q, s); },
|
|
426
|
+
// The connection went: each tunnel on it with it.
|
|
427
|
+
onClose: function () { self._gone(); }
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
ProxyH3Connection.prototype._failed = function (err) {
|
|
433
|
+
if (this.state !== 'connecting') return;
|
|
434
|
+
this.state = 'closed';
|
|
435
|
+
if (h3Connections.get(this.key) === this) h3Connections.delete(this.key);
|
|
436
|
+
var w = this.waiters; this.waiters = [];
|
|
437
|
+
for (var i = 0; i < w.length; i++) w[i](err, null);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
ProxyH3Connection.prototype._connected = function (q, s) {
|
|
441
|
+
var self = this;
|
|
442
|
+
this.quic = q; this.udp = s;
|
|
443
|
+
if (this.state !== 'connecting') { try { q.close(0x100, 'done', true); } catch (e) {} return; }
|
|
444
|
+
// Every tunnel that waited gave up on HTTP/3 meanwhile (h3Timeout).
|
|
445
|
+
if (this.waiters.length === 0) { this.state = 'closed'; h3Connections.delete(this.key); try { q.close(0x100, 'done', true); } catch (e) {} return; }
|
|
446
|
+
this.state = 'open';
|
|
447
|
+
var h3c = this.h3 = new H3Connection({ quicConnection: q, isServer: false, enableDatagrams: true });
|
|
448
|
+
h3c.on('error', function () {});
|
|
449
|
+
// The connection's events, each to the tunnel whose stream it names.
|
|
450
|
+
function to(name) { return function (id) { var t = self.tunnels.get(id); if (t) t[name].apply(null, Array.prototype.slice.call(arguments, 1)); }; }
|
|
451
|
+
h3c.on('http_headers', to('headers'));
|
|
452
|
+
h3c.on('http_body', to('body'));
|
|
453
|
+
h3c.on('http_end', to('end'));
|
|
454
|
+
h3c.on('stream_reset', to('reset'));
|
|
455
|
+
h3c.on('stream_error', to('error'));
|
|
456
|
+
q.on('datagram', function (raw) {
|
|
457
|
+
if (!raw || raw.byteLength === 0) return;
|
|
458
|
+
var qid = readVarInt(raw, 0);
|
|
459
|
+
if (!qid) return;
|
|
460
|
+
var t = self.tunnels.get(qid.value * 4);
|
|
461
|
+
if (t) t.datagram(raw.subarray(qid.byteLength));
|
|
462
|
+
});
|
|
463
|
+
h3c.sendControlStreams();
|
|
464
|
+
var w = this.waiters; this.waiters = [];
|
|
465
|
+
for (var i = 0; i < w.length; i++) w[i](null, this);
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
ProxyH3Connection.prototype._gone = function () {
|
|
469
|
+
var was = this.state;
|
|
470
|
+
this.state = 'closed';
|
|
471
|
+
if (h3Connections.get(this.key) === this) h3Connections.delete(this.key);
|
|
472
|
+
if (was === 'connecting') { this._failed(connLost()); return; }
|
|
473
|
+
var ts = Array.from(this.tunnels.values()); this.tunnels.clear();
|
|
474
|
+
for (var i = 0; i < ts.length; i++) ts[i].lost();
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
// A tunnel's stream, and its handlers.
|
|
478
|
+
ProxyH3Connection.prototype.open = function (handlers) {
|
|
479
|
+
var sid = this.nextSid; this.nextSid += 4;
|
|
480
|
+
this.tunnels.set(sid, handlers);
|
|
481
|
+
return sid;
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
// A tunnel done with its stream. The last one closes the connection — once
|
|
485
|
+
// what was sent has been acknowledged (or a second has passed); a tunnel
|
|
486
|
+
// made after that opens another.
|
|
487
|
+
ProxyH3Connection.prototype.release = function (sid) {
|
|
488
|
+
if (!this.tunnels.delete(sid) || this.tunnels.size > 0 || this.state !== 'open') return;
|
|
489
|
+
this.state = 'closing';
|
|
490
|
+
if (h3Connections.get(this.key) === this) h3Connections.delete(this.key);
|
|
491
|
+
var q = this.quic;
|
|
492
|
+
var t = setTimeout(done, 1000);
|
|
493
|
+
q.whenAllSent(function () { clearTimeout(t); done(); });
|
|
494
|
+
function done() { try { q.close(0x100, 'done', true); } catch (e) {} }
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
// The connection for this proxy: the open one, the one being made, or a new
|
|
498
|
+
// one. cb(err, conn) — once; cancel() takes the tunnel out of the wait.
|
|
499
|
+
function acquireH3(proxy, o, ech, cb) {
|
|
500
|
+
var key = h3PoolKey(proxy, o, ech);
|
|
501
|
+
var c = h3Connections.get(key);
|
|
502
|
+
if (!c || c.state === 'closing' || c.state === 'closed') { c = new ProxyH3Connection(key, proxy, o, ech); h3Connections.set(key, c); }
|
|
503
|
+
if (c.state === 'open') { var conn = c; process.nextTick(function () { cb(null, conn); }); return function () {}; }
|
|
504
|
+
c.waiters.push(cb);
|
|
505
|
+
return function cancel() {
|
|
506
|
+
var i = c.waiters.indexOf(cb);
|
|
507
|
+
if (i >= 0) c.waiters.splice(i, 1);
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
UdpProxySocket.prototype._startH3 = function () {
|
|
512
|
+
var self = this, o = this._options;
|
|
513
|
+
var fallback = this._wanted === 'auto' && o.fallback !== false;
|
|
514
|
+
var given = false, timer = null, cancel = null;
|
|
515
|
+
|
|
516
|
+
// HTTP/3 given up on: this tunnel out of the wait, and TCP tried — or,
|
|
517
|
+
// with no fallback, the socket fails.
|
|
518
|
+
function giveUp(err) {
|
|
519
|
+
if (given || self._state !== 'connecting') return;
|
|
520
|
+
given = true;
|
|
521
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
522
|
+
if (cancel) cancel();
|
|
523
|
+
if (self._echRetry(err, function () { self._startH3(); })) return;
|
|
524
|
+
if (fallback) self._startTcp(['h2', 'http/1.1']);
|
|
525
|
+
else self._fail(err);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
var h3Timeout = o.h3Timeout != null ? o.h3Timeout : 3000;
|
|
529
|
+
if (fallback && h3Timeout > 0) {
|
|
530
|
+
timer = setTimeout(function () {
|
|
531
|
+
timer = null;
|
|
532
|
+
var e = new Error('HTTP/3: no answer within h3Timeout'); e.code = 'ERR_HTTP3_TIMEOUT';
|
|
533
|
+
giveUp(e);
|
|
534
|
+
}, h3Timeout);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
cancel = acquireH3(this._proxy, o, this._echOptions(), function (err, conn) {
|
|
538
|
+
if (given) return;
|
|
539
|
+
if (err) { giveUp(err); return; }
|
|
540
|
+
given = true;
|
|
541
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
542
|
+
if (self._state !== 'connecting') return; // closed meanwhile
|
|
543
|
+
self._overH3(conn);
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
UdpProxySocket.prototype._overH3 = function (conn) {
|
|
548
|
+
var self = this, q = conn.quic, h3c = conn.h3;
|
|
549
|
+
var read = createCapsuleParser(function (u8) { self._payload(udpPayloadOf(u8)); }, function () { self._stats.dropped++; });
|
|
550
|
+
function datagramsOn() { return h3c.context.remote_datagram_support === true && q.maxDatagramSize() > 0; }
|
|
551
|
+
var busy = false, done = false;
|
|
552
|
+
var sid = conn.open({
|
|
553
|
+
headers: function (hdrs) {
|
|
554
|
+
var status = parseInt(hdrs[':status'], 10) || 0;
|
|
555
|
+
if (status >= 100 && status < 200) return; // 1xx: the answer follows
|
|
556
|
+
if (self._state !== 'connecting') return; // trailers
|
|
557
|
+
var fields = {};
|
|
558
|
+
for (var n in hdrs) if (n.charAt(0) !== ':') fields[n] = hdrs[n];
|
|
559
|
+
self._answered(status, fields, 'h3');
|
|
560
|
+
},
|
|
561
|
+
body: function (data) { if (self._state === 'open') read(data); },
|
|
562
|
+
end: function () { release(); self._ended(null); },
|
|
563
|
+
reset: function (code) {
|
|
564
|
+
release();
|
|
565
|
+
var e = new Error('CONNECT-UDP stream reset by the proxy: ' + (H3_ERR_NAME[code] || code)); e.code = 'ERR_CONNECT_UDP_RESET'; e.errorCode = code;
|
|
566
|
+
self._ended(e);
|
|
567
|
+
},
|
|
568
|
+
error: function (code, reason) {
|
|
569
|
+
release();
|
|
570
|
+
var e = new Error('CONNECT-UDP stream error: ' + reason); e.code = 'ERR_HTTP3_STREAM_ERROR'; e.errorCode = code;
|
|
571
|
+
self._ended(e);
|
|
572
|
+
},
|
|
573
|
+
datagram: function (u8) { self._payload(udpPayloadOf(u8)); },
|
|
574
|
+
// The connection went: before the answer, the tunnel was never made;
|
|
575
|
+
// after it, it was lost (a tunnel the proxy ended had its FIN first).
|
|
576
|
+
session: function (s) { self.emit('session', s); },
|
|
577
|
+
lost: function () { done = true; self._ended(self._state === 'open' ? connLost() : null); }
|
|
578
|
+
});
|
|
579
|
+
var qsid = writeVarInt(sid / 4);
|
|
580
|
+
function release() { if (done) return; done = true; conn.release(sid); }
|
|
581
|
+
|
|
582
|
+
this._t = {
|
|
583
|
+
mode: function () { return datagramsOn() ? 'datagram' : 'capsule'; },
|
|
584
|
+
maxPayload: function () {
|
|
585
|
+
if (!datagramsOn()) return MAX_UDP_PAYLOAD;
|
|
586
|
+
return Math.max(0, q.maxDatagramSize() - qsid.length - 1); // Quarter Stream ID, Context ID 0
|
|
587
|
+
},
|
|
588
|
+
send: function (u8) {
|
|
589
|
+
if (datagramsOn()) {
|
|
590
|
+
var dg = new Uint8Array(qsid.length + 1 + u8.byteLength);
|
|
591
|
+
dg.set(qsid, 0); dg[qsid.length] = 0; // Quarter Stream ID, Context ID 0
|
|
592
|
+
dg.set(u8, qsid.length + 1);
|
|
593
|
+
if (dg.byteLength > q.maxDatagramSize()) return 'too-big';
|
|
594
|
+
return q.sendDatagram(dg, null, null) ? 'sent' : 'dropped';
|
|
595
|
+
}
|
|
596
|
+
if (busy) return 'dropped';
|
|
597
|
+
h3c.sendBody(sid, datagramCapsule(u8), false);
|
|
598
|
+
busy = true;
|
|
599
|
+
q.whenStreamSent(sid, STREAM_QUEUE_MAX, function () { busy = false; });
|
|
600
|
+
return 'sent';
|
|
601
|
+
},
|
|
602
|
+
// Ours ends: FIN on the stream; the connection's end is the last
|
|
603
|
+
// tunnel's (release).
|
|
604
|
+
close: function () {
|
|
605
|
+
if (done) return;
|
|
606
|
+
try { h3c.sendBody(sid, null, true); } catch (e) {}
|
|
607
|
+
release();
|
|
608
|
+
},
|
|
609
|
+
// Given up on: the stream reset (H3_REQUEST_CANCELLED), the others on
|
|
610
|
+
// the connection untouched.
|
|
611
|
+
destroy: function () {
|
|
612
|
+
if (done) return;
|
|
613
|
+
try { h3c.abortStream(sid, 0x10c); } catch (e) {}
|
|
614
|
+
release();
|
|
615
|
+
},
|
|
616
|
+
address: function () { try { return conn.udp.address(); } catch (e) { return {}; } }
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
var h = { ':method': 'CONNECT', ':protocol': 'connect-udp', ':scheme': 'https', ':authority': this._authority(), ':path': this._path };
|
|
620
|
+
var extra = this._extraHeaders();
|
|
621
|
+
for (var k in extra) h[k] = extra[k];
|
|
622
|
+
h3c.sendHeaders(sid, h, false);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
// ============================================================
|
|
627
|
+
// TCP: HTTP/2 (extended CONNECT, RFC 8441) or HTTP/1.1 (Upgrade)
|
|
628
|
+
// ============================================================
|
|
629
|
+
|
|
630
|
+
UdpProxySocket.prototype._startTcp = function (alpn) {
|
|
631
|
+
var self = this, o = this._options;
|
|
632
|
+
var socket;
|
|
633
|
+
var sessions = proxySessions(o, 'tcp:' + alpn[0], this._proxy, this._echOptions());
|
|
634
|
+
try {
|
|
635
|
+
socket = lemonTls.connect({
|
|
636
|
+
session: sessions.take() || undefined,
|
|
637
|
+
host: this._proxy.hostname, port: this._proxy.port,
|
|
638
|
+
servername: o.servername || (net.isIP(this._proxy.hostname) ? undefined : this._proxy.hostname),
|
|
639
|
+
ALPNProtocols: alpn,
|
|
640
|
+
rejectUnauthorized: o.rejectUnauthorized !== false, ca: o.ca,
|
|
641
|
+
key: o.key, cert: o.cert, passphrase: o.passphrase,
|
|
642
|
+
ech: this._echOptions().ech, echGrease: this._echOptions().echGrease,
|
|
643
|
+
ecdhCurve: o.ecdhCurve,
|
|
644
|
+
sigalgs: o.sigalgs
|
|
645
|
+
}, function () {
|
|
646
|
+
if (self._state !== 'connecting') { try { socket.destroy(); } catch (e) {} return; } // closed meanwhile
|
|
647
|
+
var p = socket.alpnProtocol;
|
|
648
|
+
if (p === 'h2' && alpn.indexOf('h2') >= 0) self._overH2(socket);
|
|
649
|
+
else if ((p === 'http/1.1' || !p) && alpn.indexOf('http/1.1') >= 0) self._overH1(socket);
|
|
650
|
+
else {
|
|
651
|
+
try { socket.destroy(); } catch (e) {}
|
|
652
|
+
var e = new Error('the proxy negotiated ' + JSON.stringify(p) + ', not ' + alpn.join(' or ')); e.code = 'ERR_CONNECT_UDP_ALPN';
|
|
653
|
+
self._fail(e);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
} catch (err) { this._fail(err); return; }
|
|
657
|
+
socket.on('session', function (s) { sessions.keep(s); self.emit('session', s); });
|
|
658
|
+
socket.on('error', function (err) {
|
|
659
|
+
if (self._state === 'connecting') sessions.failed();
|
|
660
|
+
if (self._echRetry(err, function () { self._startTcp(alpn); })) return;
|
|
661
|
+
if (self._state === 'connecting') self._fail(err);
|
|
662
|
+
else self._ended(err);
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// Capsules on a byte stream: what both TCP versions write, and how much may
|
|
667
|
+
// wait — past STREAM_QUEUE_MAX, a payload is dropped, as UDP drops.
|
|
668
|
+
function streamTransport(writable, write, close, destroy, address) {
|
|
669
|
+
return {
|
|
670
|
+
mode: function () { return 'capsule'; },
|
|
671
|
+
maxPayload: function () { return MAX_UDP_PAYLOAD; },
|
|
672
|
+
send: function (u8) {
|
|
673
|
+
if (writable.destroyed || writable.writableLength > STREAM_QUEUE_MAX) return 'dropped';
|
|
674
|
+
write(datagramCapsule(u8));
|
|
675
|
+
return 'sent';
|
|
676
|
+
},
|
|
677
|
+
close: close, destroy: destroy, address: address
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
UdpProxySocket.prototype._overH2 = function (socket) {
|
|
682
|
+
var self = this;
|
|
683
|
+
var h2 = new H2Connection({ isServer: false });
|
|
684
|
+
var id = 1, opened = false, ended = false;
|
|
685
|
+
var read = createCapsuleParser(function (u8) { self._payload(udpPayloadOf(u8)); }, function () { self._stats.dropped++; });
|
|
686
|
+
h2.on('send', function (b) { if (!socket.destroyed) socket.write(b); });
|
|
687
|
+
h2.on('error', function (err) { self._ended(err); try { socket.destroy(); } catch (e) {} });
|
|
688
|
+
socket.on('data', function (d) { try { h2.execute(d); } catch (e) { self._ended(e); try { socket.destroy(); } catch (e2) {} } });
|
|
689
|
+
socket.on('close', function () { self._ended(self._state === 'open' && !ended ? connLost() : null); });
|
|
690
|
+
|
|
691
|
+
self._t = streamTransport(socket, function (bytes) { h2.sendData(id, Buffer.from(bytes), false); },
|
|
692
|
+
function () { try { h2.sendData(id, null, true); } catch (e) {} setTimeout(function () { try { socket.end(); } catch (e) {} }, 50); },
|
|
693
|
+
function () { try { socket.destroy(); } catch (e) {} },
|
|
694
|
+
function () { try { return socket.address(); } catch (e) { return {}; } });
|
|
695
|
+
|
|
696
|
+
// RFC 8441 §3: :protocol only once the server has said it takes it
|
|
697
|
+
// (SETTINGS_ENABLE_CONNECT_PROTOCOL).
|
|
698
|
+
h2.on('settings', function (o, list) {
|
|
699
|
+
if (opened) return;
|
|
700
|
+
opened = true;
|
|
701
|
+
var ok = (list || []).some(function (s) { return s.id === 8 && s.value === 1; });
|
|
702
|
+
if (!ok) {
|
|
703
|
+
var e = new Error('the proxy does not take extended CONNECT over HTTP/2'); e.code = 'ERR_CONNECT_UDP_NOT_SUPPORTED';
|
|
704
|
+
self._fail(e);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
var list2 = [[':method', 'CONNECT'], [':protocol', 'connect-udp'], [':scheme', 'https'], [':authority', self._authority()], [':path', self._path]];
|
|
708
|
+
var extra = self._extraHeaders();
|
|
709
|
+
for (var k in extra) list2.push([k, extra[k]]);
|
|
710
|
+
h2.sendHeaders(id, list2, false);
|
|
711
|
+
});
|
|
712
|
+
h2.on('headers', function (sid, hd) {
|
|
713
|
+
if (sid !== id || self._state !== 'connecting') return;
|
|
714
|
+
var fields = {}, status = 0;
|
|
715
|
+
var pairs = Array.isArray(hd) ? hd : Object.keys(hd).map(function (k) { return [k, hd[k]]; });
|
|
716
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
717
|
+
if (pairs[i][0] === ':status') status = parseInt(pairs[i][1], 10) || 0;
|
|
718
|
+
else if (pairs[i][0].charAt(0) !== ':') fields[pairs[i][0]] = pairs[i][1];
|
|
719
|
+
}
|
|
720
|
+
if (status >= 100 && status < 200) return;
|
|
721
|
+
self._answered(status, fields, 'h2');
|
|
722
|
+
});
|
|
723
|
+
h2.on('data', function (sid, d) { if (sid === id && self._state === 'open') read(d); });
|
|
724
|
+
h2.on('end', function (sid) { if (sid === id) { ended = true; self._ended(null); } });
|
|
725
|
+
h2.on('rst', function (sid, code) {
|
|
726
|
+
if (sid !== id) return;
|
|
727
|
+
var e = new Error('CONNECT-UDP stream reset by the proxy (HTTP/2 error ' + code + ')'); e.code = 'ERR_CONNECT_UDP_RESET'; e.errorCode = code;
|
|
728
|
+
self._ended(e);
|
|
729
|
+
});
|
|
730
|
+
h2.sendPreface();
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
UdpProxySocket.prototype._overH1 = function (socket) {
|
|
734
|
+
var self = this;
|
|
735
|
+
var head = Buffer.alloc(0), inHead = true;
|
|
736
|
+
var read = createCapsuleParser(function (u8) { self._payload(udpPayloadOf(u8)); }, function () { self._stats.dropped++; });
|
|
737
|
+
self._t = streamTransport(socket, function (bytes) { socket.write(bytes); },
|
|
738
|
+
function () { try { socket.end(); } catch (e) {} },
|
|
739
|
+
function () { try { socket.destroy(); } catch (e) {} },
|
|
740
|
+
function () { try { return socket.address(); } catch (e) { return {}; } });
|
|
741
|
+
socket.on('data', function (d) {
|
|
742
|
+
if (!inHead) { if (self._state === 'open') read(d); return; }
|
|
743
|
+
head = Buffer.concat([head, d]);
|
|
744
|
+
var end = head.indexOf('\r\n\r\n');
|
|
745
|
+
if (end < 0) { if (head.length > 65536) self._fail(new Error('CONNECT-UDP: response head too large')); return; }
|
|
746
|
+
inHead = false;
|
|
747
|
+
var lines = head.subarray(0, end).toString('latin1').split('\r\n');
|
|
748
|
+
var m = /^HTTP\/1\.[01] (\d{3})/.exec(lines[0]);
|
|
749
|
+
var fields = {};
|
|
750
|
+
for (var i = 1; i < lines.length; i++) {
|
|
751
|
+
var c = lines[i].indexOf(':');
|
|
752
|
+
if (c > 0) fields[lines[i].slice(0, c).trim().toLowerCase()] = lines[i].slice(c + 1).trim();
|
|
753
|
+
}
|
|
754
|
+
var status = m ? Number(m[1]) : 0;
|
|
755
|
+
if (status === 101 && String(fields.upgrade || '').toLowerCase() !== 'connect-udp') status = 0;
|
|
756
|
+
self._answered(status, fields, 'http/1.1');
|
|
757
|
+
if (self._state === 'open' && head.length > end + 4) read(head.subarray(end + 4));
|
|
758
|
+
head = null;
|
|
759
|
+
});
|
|
760
|
+
socket.on('end', function () { self._ended(null); });
|
|
761
|
+
socket.on('close', function () { self._ended(null); });
|
|
762
|
+
var req = 'GET ' + self._path + ' HTTP/1.1\r\nHost: ' + self._authority() + '\r\nConnection: Upgrade\r\nUpgrade: connect-udp\r\n';
|
|
763
|
+
var extra = self._extraHeaders();
|
|
764
|
+
for (var k in extra) req += k + ': ' + extra[k].replace(/[\r\n]/g, '') + '\r\n';
|
|
765
|
+
socket.write(req + '\r\n');
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
function connLost() {
|
|
769
|
+
var e = new Error('CONNECT-UDP: the connection to the proxy was lost'); e.code = 'ERR_CONNECT_UDP_CLOSED'; return e;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function resolve(host, cb) {
|
|
773
|
+
if (net.isIP(host)) { cb(null, host); return; }
|
|
774
|
+
if (host === 'localhost') { cb(null, '127.0.0.1'); return; }
|
|
775
|
+
dns.lookup(host, function (err, address) { cb(err, address); });
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
export { connectUdp, UdpProxySocket, expandTemplate };
|