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.
@@ -0,0 +1,578 @@
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
+ // Proxying UDP in HTTP — CONNECT-UDP (RFC 9298), the server's side
17
+ //
18
+ // A client asks the proxy for a UDP "socket" to one target — a host and a
19
+ // port, named in the request's path by a URI template — and from then on
20
+ // sends it UDP payloads, and receives the target's, as HTTP Datagrams
21
+ // (RFC 9297): over HTTP/3 a QUIC DATAGRAM frame each (unreliable, as UDP
22
+ // is), and otherwise — HTTP/2, HTTP/1.1, or an HTTP/3 peer that did not
23
+ // negotiate datagrams — DATAGRAM capsules on the request stream.
24
+ //
25
+ // This file is the protocol and the policy; it never touches a frame. Each
26
+ // HTTP version hands it a carrier (h3_server, server.js for HTTP/2 and
27
+ // HTTP/1.1) with five things it can do — accept, reject, send a payload to
28
+ // the client, end, abort — and three it reports: a datagram, stream bytes
29
+ // (capsules), the client's end or a reset. So the parsing, the relay, the
30
+ // limits and the figures are written once for all three.
31
+ //
32
+ // The application decides (server.on('connectUdp', (req, tunnel) => …)):
33
+ //
34
+ // tunnel.accept() the proxy relays, to the target
35
+ // the request names
36
+ // tunnel.accept({ target: {host, port} }) …to another one
37
+ // tunnel.accept({ socket }) …through the application's own
38
+ // dgram socket
39
+ // tunnel.accept({ relay: false }) no relay: the application gets
40
+ // the client's payloads ('message')
41
+ // and answers with tunnel.send()
42
+ // tunnel.reject(status)
43
+ //
44
+ // Defaults that protect the network the proxy sits in (RFC 9298 §7): a
45
+ // target that resolves only to a private, loopback, link-local, multicast
46
+ // or otherwise special address is refused (403, destination_ip_prohibited)
47
+ // unless allowed; a connection may hold a bounded number of tunnels; a
48
+ // tunnel with nothing passing for idleTimeout is closed (never under two
49
+ // minutes, §3.1).
50
+ // ============================================================
51
+
52
+ import { EventEmitter } from 'node:events';
53
+ import dgram from 'node:dgram';
54
+ import dns from 'node:dns';
55
+ import net from 'node:net';
56
+ import { readVarInt, writeVarInt } from './utils.js';
57
+
58
+ var DEFAULT_TEMPLATE = '/.well-known/masque/udp/{target_host}/{target_port}/';
59
+ var MIN_IDLE_MS = 120000; // §3.1: "SHOULD NOT use a period lower than two minutes"
60
+ var DEFAULT_MAX_PER_CONNECTION = 16;
61
+ var MAX_UDP_PAYLOAD = 65527; // §5: the largest a UDP payload can be
62
+ var CAPSULE_DATAGRAM = 0x00; // RFC 9297 §3.5
63
+ var EARLY_PAYLOADS_MAX = 32; // payloads the client sent before the tunnel opened
64
+ var STREAM_QUEUE_MAX = 256 * 1024; // capsules waiting to be sent before we drop (UDP's way)
65
+
66
+
67
+ // ============================================================
68
+ // The target, from the request's path — the URI template (RFC 6570)
69
+ //
70
+ // The proxy's template is what its clients are configured with; the
71
+ // default is the one RFC 9298 §3 suggests. Two forms are read: variables in
72
+ // the path ('{target_host}'), and a query expansion ('{?target_host,
73
+ // target_port}'), whose parameters may come in any order. Values are
74
+ // percent-decoded; an IPv6 literal arrives with its colons encoded (§3).
75
+ // ============================================================
76
+
77
+ function escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
78
+
79
+ function compileTemplate(template) {
80
+ template = String(template || DEFAULT_TEMPLATE);
81
+ // An absolute template names the proxy's origin too; only its path and
82
+ // query are matched here (the authority is the connection's).
83
+ var m = /^[a-z][a-z0-9+.-]*:\/\/[^/]*(.*)$/i.exec(template);
84
+ if (m) template = m[1] || '/';
85
+ var query = null;
86
+ var qm = /\{\?([^}]*)\}$/.exec(template);
87
+ if (qm) { query = qm[1].split(',').map(function (s) { return s.trim(); }); template = template.slice(0, qm.index); }
88
+ var names = [], re = '';
89
+ var parts = template.split(/(\{[^}]*\})/);
90
+ for (var i = 0; i < parts.length; i++) {
91
+ var p = parts[i];
92
+ if (!p) continue;
93
+ if (p.charAt(0) === '{') {
94
+ var name = p.slice(1, -1).trim();
95
+ names.push(name);
96
+ re += '([^/?#&]*)';
97
+ } else re += escapeRe(p);
98
+ }
99
+ // A trailing slash is the template's — a client that left it off means the
100
+ // same resource.
101
+ if (/\/$/.test(re)) re += '?';
102
+ var vars = names.concat(query || []);
103
+ if (vars.indexOf('target_host') < 0 || vars.indexOf('target_port') < 0) {
104
+ throw new TypeError('connectUdp.template must contain {target_host} and {target_port}');
105
+ }
106
+ return { path: new RegExp('^' + re + (query ? '(?:\\?(.*))?' : '(?:\\?.*)?') + '$'), names: names, query: query };
107
+ }
108
+
109
+ function decode(s) {
110
+ try { return decodeURIComponent(s); } catch (e) { return null; }
111
+ }
112
+
113
+ // { host, port } — or { error: 404 } when the path is not this proxy's
114
+ // resource, { error: 400 } when it is, with a target that cannot be (§3:
115
+ // empty, a port outside 1..65535).
116
+ function parseTarget(compiled, url) {
117
+ var m = compiled.path.exec(String(url || ''));
118
+ if (!m) return { error: 404 };
119
+ var vars = {};
120
+ for (var i = 0; i < compiled.names.length; i++) vars[compiled.names[i]] = m[i + 1];
121
+ if (compiled.query) {
122
+ var qs = m[compiled.names.length + 1] || '';
123
+ qs.split('&').forEach(function (kv) {
124
+ var eq = kv.indexOf('=');
125
+ if (eq > 0) vars[kv.slice(0, eq)] = kv.slice(eq + 1);
126
+ });
127
+ }
128
+ var host = vars.target_host != null ? decode(vars.target_host) : null;
129
+ var portStr = vars.target_port != null ? decode(vars.target_port) : null;
130
+ if (!host || !portStr || !/^[0-9]{1,5}$/.test(portStr)) return { error: 400 };
131
+ var port = Number(portStr);
132
+ if (port < 1 || port > 65535) return { error: 400 };
133
+ if (host.charAt(0) === '[' && host.charAt(host.length - 1) === ']') host = host.slice(1, -1);
134
+ if (host.indexOf('%') >= 0 && net.isIPv6(host.split('%')[0])) return { error: 400 }; // §3: no zone identifiers
135
+ return { host: host, port: port };
136
+ }
137
+
138
+
139
+ // ============================================================
140
+ // Addresses a proxy should not reach for a stranger (RFC 9298 §7)
141
+ //
142
+ // Its own and its network's: loopback, private ranges, link-local,
143
+ // carrier-grade NAT, multicast and broadcast, the unspecified address, and
144
+ // the IPv6 forms of all of these (unique-local, an IPv4-mapped address).
145
+ // ============================================================
146
+
147
+ var blocked = new net.BlockList();
148
+ [['0.0.0.0', 8], ['10.0.0.0', 8], ['100.64.0.0', 10], ['127.0.0.0', 8], ['169.254.0.0', 16],
149
+ ['172.16.0.0', 12], ['192.0.0.0', 24], ['192.168.0.0', 16], ['198.18.0.0', 15],
150
+ ['224.0.0.0', 4], ['240.0.0.0', 4]].forEach(function (r) { blocked.addSubnet(r[0], r[1], 'ipv4'); });
151
+ [['::', 128], ['::1', 128], ['fc00::', 7], ['fe80::', 10], ['ff00::', 8]].forEach(function (r) { blocked.addSubnet(r[0], r[1], 'ipv6'); });
152
+
153
+ function isPrivateAddress(ip) {
154
+ if (net.isIPv4(ip)) return blocked.check(ip, 'ipv4');
155
+ if (!net.isIPv6(ip)) return true;
156
+ // An IPv4 address in IPv6 clothing (::ffff:10.0.0.1, or in hex) is that
157
+ // IPv4 address.
158
+ var low = ip.toLowerCase();
159
+ var mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(low);
160
+ if (mapped) return isPrivateAddress(mapped[1]);
161
+ var hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(low);
162
+ if (hex) {
163
+ var a = parseInt(hex[1], 16), b = parseInt(hex[2], 16);
164
+ return isPrivateAddress([a >> 8, a & 255, b >> 8, b & 255].join('.'));
165
+ }
166
+ return blocked.check(ip, 'ipv6');
167
+ }
168
+
169
+
170
+ // ============================================================
171
+ // HTTP Datagrams and capsules (RFC 9297)
172
+ // ============================================================
173
+
174
+ // A UDP payload as an HTTP Datagram's payload: Context ID 0, then the bytes
175
+ // (§4 of RFC 9298).
176
+ function contextZero(payload) {
177
+ var out = new Uint8Array(1 + payload.byteLength);
178
+ out[0] = 0;
179
+ out.set(payload, 1);
180
+ return out;
181
+ }
182
+
183
+ // …and as a DATAGRAM capsule: type, length, value.
184
+ function datagramCapsule(payload) {
185
+ var len = writeVarInt(1 + payload.byteLength);
186
+ var out = new Uint8Array(1 + len.byteLength + 1 + payload.byteLength);
187
+ out[0] = CAPSULE_DATAGRAM;
188
+ out.set(len, 1);
189
+ out[1 + len.byteLength] = 0;
190
+ out.set(payload, 2 + len.byteLength);
191
+ return out;
192
+ }
193
+
194
+ // An HTTP Datagram's payload → the UDP payload in it, or null: a Context ID
195
+ // other than 0 is one nobody registered here (§4: dropped).
196
+ function udpPayloadOf(u8) {
197
+ var c = readVarInt(u8, 0);
198
+ if (!c || c.value !== 0) return null;
199
+ return u8.subarray(c.byteLength);
200
+ }
201
+
202
+ // Capsules from the request stream, however the bytes are split. DATAGRAM
203
+ // capsules go to onDatagram (their value: an HTTP Datagram payload); a
204
+ // capsule of another type is skipped (RFC 9297 §3.2: unknown types are
205
+ // ignored), and so is a DATAGRAM too large to carry a UDP payload — without
206
+ // buffering it (§5 of RFC 9298), so neither costs memory.
207
+ function createCapsuleParser(onDatagram, onDropped) {
208
+ var pending = null, skip = 0;
209
+ var MAX_VALUE = MAX_UDP_PAYLOAD + 8;
210
+ return function feed(chunk) {
211
+ if (!chunk || chunk.byteLength === 0) return;
212
+ var u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
213
+ while (u8.byteLength > 0) {
214
+ if (skip > 0) {
215
+ var n = Math.min(skip, u8.byteLength);
216
+ skip -= n;
217
+ u8 = u8.subarray(n);
218
+ continue;
219
+ }
220
+ if (pending) {
221
+ var joined = new Uint8Array(pending.byteLength + u8.byteLength);
222
+ joined.set(pending, 0); joined.set(u8, pending.byteLength);
223
+ u8 = joined; pending = null;
224
+ }
225
+ for (;;) {
226
+ var t = readVarInt(u8, 0);
227
+ var l = t && readVarInt(u8, t.byteLength);
228
+ if (!t || !l) { pending = u8.byteLength ? u8 : null; u8 = new Uint8Array(0); break; }
229
+ var hdr = t.byteLength + l.byteLength, len = l.value;
230
+ if (t.value !== CAPSULE_DATAGRAM || len > MAX_VALUE) {
231
+ if (t.value === CAPSULE_DATAGRAM && onDropped) onDropped();
232
+ var avail = u8.byteLength - hdr;
233
+ if (avail >= len) { u8 = u8.subarray(hdr + len); continue; }
234
+ skip = len - avail;
235
+ u8 = new Uint8Array(0);
236
+ break;
237
+ }
238
+ if (u8.byteLength < hdr + len) { pending = u8; u8 = new Uint8Array(0); break; }
239
+ onDatagram(u8.subarray(hdr, hdr + len));
240
+ u8 = u8.subarray(hdr + len);
241
+ if (u8.byteLength === 0) break;
242
+ }
243
+ }
244
+ };
245
+ }
246
+
247
+
248
+ // ============================================================
249
+ // The manager: one per server
250
+ //
251
+ // Holds the policy (options.connectUdp) and the figures across tunnels;
252
+ // handle(req, carrier, emit) is what each HTTP version calls with a
253
+ // CONNECT-UDP request.
254
+ // ============================================================
255
+
256
+ function createConnectUdp(options) {
257
+ options = options || {};
258
+ var compiled = compileTemplate(options.template);
259
+ var allowPrivateDefault = options.allowPrivate === true;
260
+ var maxPerConnection = options.maxTunnelsPerConnection > 0 ? Math.floor(options.maxTunnelsPerConnection) : DEFAULT_MAX_PER_CONNECTION;
261
+ var idleTimeout = options.idleTimeout === 0 ? 0 : Math.max(MIN_IDLE_MS, Number(options.idleTimeout) || MIN_IDLE_MS);
262
+ var lookup = typeof options.lookup === 'function' ? options.lookup : dns.lookup;
263
+ var proxyName = options.proxyName || 'quico';
264
+
265
+ var perConnection = new WeakMap(); // carrier.connKey → tunnels open on it
266
+ var active = new Set();
267
+ var totals = { accepted: 0, rejected: 0, closed: 0,
268
+ packetsFromClient: 0, bytesFromClient: 0, packetsToClient: 0, bytesToClient: 0,
269
+ droppedFromClient: 0, droppedToClient: 0 };
270
+ var COUNTERS = ['packetsFromClient', 'bytesFromClient', 'packetsToClient', 'bytesToClient', 'droppedFromClient', 'droppedToClient'];
271
+
272
+ function proxyStatus(error, details) {
273
+ return proxyName + '; error=' + error + (details ? '; details="' + String(details).replace(/["\\]/g, '') + '"' : '');
274
+ }
275
+
276
+ // A CONNECT-UDP request (HTTP/2, HTTP/3) or Upgrade (HTTP/1.1). `emit`
277
+ // hands the tunnel to the application; false when nothing listens.
278
+ function handle(req, carrier, emit) {
279
+ var target = parseTarget(compiled, req.url);
280
+ if (carrier.httpVersion === '1.1' && String(req.method).toUpperCase() !== 'GET') target = { error: 400 }; // §3.2
281
+ if (target.error) {
282
+ totals.rejected++;
283
+ carrier.reject(target.error, null);
284
+ return;
285
+ }
286
+ var key = carrier.connKey;
287
+ var count = (key && perConnection.get(key)) || 0;
288
+ if (count >= maxPerConnection) {
289
+ totals.rejected++;
290
+ carrier.reject(429, null);
291
+ return;
292
+ }
293
+ if (key) perConnection.set(key, count + 1);
294
+ var tunnel = new UdpTunnel(req, carrier, target);
295
+ active.add(tunnel);
296
+ tunnel.once('close', function () {
297
+ active.delete(tunnel);
298
+ if (key) { var c = perConnection.get(key) || 1; if (c <= 1) perConnection.delete(key); else perConnection.set(key, c - 1); }
299
+ totals.closed++;
300
+ if (tunnel._wasOpen) totals.accepted++; else totals.rejected++;
301
+ for (var i = 0; i < COUNTERS.length; i++) totals[COUNTERS[i]] += tunnel._stats[COUNTERS[i]];
302
+ });
303
+ if (!emit(tunnel)) tunnel.reject(501);
304
+ }
305
+
306
+ // The figures: tunnels open now, and totals — those of open tunnels
307
+ // included.
308
+ function stats() {
309
+ var out = { active: active.size, accepted: totals.accepted, rejected: totals.rejected, closed: totals.closed };
310
+ for (var i = 0; i < COUNTERS.length; i++) out[COUNTERS[i]] = totals[COUNTERS[i]];
311
+ active.forEach(function (t) {
312
+ if (t._wasOpen && t.state === 'open') out.accepted++;
313
+ for (var i = 0; i < COUNTERS.length; i++) out[COUNTERS[i]] += t._stats[COUNTERS[i]];
314
+ });
315
+ return out;
316
+ }
317
+
318
+
319
+ // ============================================================
320
+ // A tunnel
321
+ // ============================================================
322
+
323
+ function UdpTunnel(req, carrier, target) {
324
+ EventEmitter.call(this);
325
+ var self = this;
326
+ this.req = req;
327
+ this.target = { host: target.host, port: target.port };
328
+ this.httpVersion = carrier.httpVersion;
329
+ this.state = 'pending'; // → 'open' → 'closed'
330
+ this.error = null; // why it was refused or ended, when it was not the client
331
+ this.closeReason = null; // 'client', 'aborted', 'idle', 'local', 'socket', 'rejected'
332
+ this._carrier = carrier;
333
+ this._wasOpen = false;
334
+ this._relay = null; // { socket, own, address, port, onMessage, onError }
335
+ this._early = []; // payloads from the client before 'open'
336
+ this._last = Date.now();
337
+ this._idleTimer = null;
338
+ this._stats = { openedAt: null, closedAt: null, address: null, port: null,
339
+ packetsFromClient: 0, bytesFromClient: 0, packetsToClient: 0, bytesToClient: 0,
340
+ droppedFromClient: 0, droppedToClient: 0 };
341
+ carrier.sink = {
342
+ datagram: function (u8) { self._fromClient(udpPayloadOf(u8)); },
343
+ data: createCapsuleParser(function (u8) { self._fromClient(udpPayloadOf(u8)); },
344
+ function () { self._stats.droppedFromClient++; }),
345
+ end: function () { self._ended('client'); },
346
+ abort: function () { self._ended('aborted'); }
347
+ };
348
+ }
349
+ Object.setPrototypeOf(UdpTunnel.prototype, EventEmitter.prototype);
350
+
351
+ // A copy of the figures, with how the datagrams travel to the client now:
352
+ // 'datagram' (QUIC DATAGRAM frames) or 'capsule' (the request stream).
353
+ Object.defineProperty(UdpTunnel.prototype, 'stats', { get: function () {
354
+ var s = Object.assign({ target: { host: this.target.host, port: this.target.port }, httpVersion: this.httpVersion,
355
+ state: this.state, transport: this._carrier.mode ? this._carrier.mode() : 'capsule' }, this._stats);
356
+ s.durationMs = s.openedAt ? (s.closedAt || Date.now()) - s.openedAt : 0;
357
+ return s;
358
+ } });
359
+
360
+ UdpTunnel.prototype.accept = function (opts) {
361
+ if (this.state !== 'pending' || this._accepting) return this;
362
+ opts = opts || {};
363
+ var self = this;
364
+ this._accepting = true;
365
+ if (opts.target) {
366
+ var p = Number(opts.target.port);
367
+ if (!opts.target.host || !(p >= 1 && p <= 65535)) throw new TypeError('accept({ target }) needs a host and a port in 1..65535');
368
+ this.target = { host: String(opts.target.host), port: p };
369
+ }
370
+ var headers = opts.headers || null;
371
+ if (opts.relay === false) { this._open(headers); return this; }
372
+
373
+ var allowPrivate = opts.allowPrivate != null ? !!opts.allowPrivate : allowPrivateDefault;
374
+ var host = this.target.host;
375
+ lookup(host, { all: true }, function (err, list) {
376
+ if (self.state !== 'pending') return;
377
+ if (err || !list || !list.length) {
378
+ self._refuse(502, proxyStatus('dns_error', err && err.code), err || new Error('no address for ' + host));
379
+ return;
380
+ }
381
+ if (!Array.isArray(list)) list = [{ address: list, family: net.isIPv6(list) ? 6 : 4 }];
382
+ var pick = null;
383
+ for (var i = 0; i < list.length && !pick; i++) if (allowPrivate || !isPrivateAddress(list[i].address)) pick = list[i];
384
+ if (!pick) {
385
+ var e = new Error('target ' + host + ' resolves only to addresses not allowed'); e.code = 'ERR_DESTINATION_PROHIBITED';
386
+ self._refuse(403, proxyStatus('destination_ip_prohibited'), e);
387
+ return;
388
+ }
389
+ var family = pick.family === 6 || pick.family === 'IPv6' ? 6 : 4;
390
+ if (opts.socket) self._useSocket(opts.socket, false, pick.address, headers);
391
+ else {
392
+ var sock;
393
+ try { sock = dgram.createSocket(family === 6 ? 'udp6' : 'udp4'); }
394
+ catch (e2) { self._refuse(502, proxyStatus('proxy_internal_error'), e2); return; }
395
+ sock.once('error', function (e3) {
396
+ if (self.state === 'pending') self._refuse(502, proxyStatus('destination_ip_unroutable', e3.code), e3);
397
+ });
398
+ sock.connect(self.target.port, pick.address, function () {
399
+ if (self.state !== 'pending') { try { sock.close(); } catch (e4) {} return; }
400
+ self._useSocket(sock, true, pick.address, headers);
401
+ });
402
+ }
403
+ });
404
+ return this;
405
+ };
406
+
407
+ // The relay's socket: ours (connected to the target — the kernel keeps
408
+ // other senders out), or the application's, whose senders are checked
409
+ // here (§5: "MUST validate the IP source address and UDP source port").
410
+ UdpTunnel.prototype._useSocket = function (sock, own, address, headers) {
411
+ var self = this, port = this.target.port;
412
+ var connected = own;
413
+ if (!own) { try { connected = !!sock.remoteAddress(); } catch (e) { connected = false; } }
414
+ var relay = { socket: sock, own: own, connected: connected, address: address, port: port };
415
+ relay.onMessage = function (msg, rinfo) {
416
+ if (!relay.connected && (rinfo.port !== port || rinfo.address !== address)) return;
417
+ self._toClient(msg);
418
+ };
419
+ // The socket can no longer be used — an ICMP port unreachable, on a
420
+ // connected socket: the request stream closes with it (§3.1).
421
+ relay.onError = function (err) {
422
+ if (self.state === 'closed') return;
423
+ self.error = err;
424
+ self._close('socket');
425
+ };
426
+ sock.on('message', relay.onMessage);
427
+ sock.on('error', relay.onError);
428
+ this._relay = relay;
429
+ this._stats.address = address; this._stats.port = port;
430
+ this._open(headers);
431
+ };
432
+
433
+ UdpTunnel.prototype._open = function (headers) {
434
+ var h = { 'capsule-protocol': '?1' };
435
+ for (var n in (headers || {})) h[String(n).toLowerCase()] = headers[n];
436
+ this.state = 'open';
437
+ this._wasOpen = true;
438
+ this._stats.openedAt = Date.now();
439
+ this._carrier.accept(h);
440
+ this._touch();
441
+ this._armIdle();
442
+ this.emit('open');
443
+ var early = this._early; this._early = null;
444
+ for (var i = 0; early && i < early.length && this.state === 'open'; i++) this._deliver(early[i]);
445
+ };
446
+
447
+ UdpTunnel.prototype._refuse = function (status, ps, err) {
448
+ if (this.state !== 'pending') return;
449
+ this.error = err || null;
450
+ this._carrier.reject(status, ps ? { 'proxy-status': ps } : null);
451
+ this._close('rejected');
452
+ };
453
+
454
+ UdpTunnel.prototype.reject = function (status, headers) {
455
+ if (this.state !== 'pending') return this;
456
+ this._carrier.reject(status || 403, headers || null);
457
+ this._close('rejected');
458
+ return this;
459
+ };
460
+
461
+ // A UDP payload to the client — the target's (the relay), or the
462
+ // application's (tunnel.send). false: not sent (closed, too large for the
463
+ // path, or the stream already holds too much — UDP's answer to a full
464
+ // queue is to drop).
465
+ UdpTunnel.prototype.send = function (data) {
466
+ if (typeof data === 'string') data = Buffer.from(data);
467
+ if (!(data instanceof Uint8Array)) throw new TypeError('tunnel.send() takes a Buffer, a Uint8Array or a string');
468
+ return this._toClient(data);
469
+ };
470
+
471
+ UdpTunnel.prototype._toClient = function (u8) {
472
+ if (this.state !== 'open') return false;
473
+ if (u8.byteLength > MAX_UDP_PAYLOAD || !this._carrier.sendPayload(u8)) { this._stats.droppedToClient++; return false; }
474
+ this._stats.packetsToClient++;
475
+ this._stats.bytesToClient += u8.byteLength;
476
+ this._touch();
477
+ return true;
478
+ };
479
+
480
+ UdpTunnel.prototype._fromClient = function (payload) {
481
+ if (this.state === 'closed') return;
482
+ if (!payload || payload.byteLength > MAX_UDP_PAYLOAD) { this._stats.droppedFromClient++; return; }
483
+ if (this.state === 'pending') {
484
+ // §3.4: a client may send before it has the 2xx. Kept, a few — they go
485
+ // once the tunnel opens.
486
+ if (this._early.length >= EARLY_PAYLOADS_MAX) { this._stats.droppedFromClient++; return; }
487
+ this._early.push(Uint8Array.prototype.slice.call(payload));
488
+ return;
489
+ }
490
+ this._deliver(payload);
491
+ };
492
+
493
+ UdpTunnel.prototype._deliver = function (payload) {
494
+ this._stats.packetsFromClient++;
495
+ this._stats.bytesFromClient += payload.byteLength;
496
+ this._touch();
497
+ var relay = this._relay;
498
+ if (!relay) { this.emit('message', Buffer.from(payload.buffer, payload.byteOffset, payload.byteLength)); return; }
499
+ var self = this;
500
+ var cb = function (err) { if (err) self._stats.droppedFromClient++; };
501
+ try {
502
+ if (relay.connected) relay.socket.send(payload, cb);
503
+ else relay.socket.send(payload, relay.port, relay.address, cb);
504
+ } catch (e) { this._stats.droppedFromClient++; }
505
+ };
506
+
507
+ UdpTunnel.prototype._touch = function () { this._last = Date.now(); };
508
+
509
+ // One timer for the tunnel's life, not one per packet: when it fires it
510
+ // looks at the last activity, and sleeps for what is left.
511
+ UdpTunnel.prototype._armIdle = function () {
512
+ if (!idleTimeout || this.state !== 'open') return;
513
+ var self = this, wait = Math.max(1000, this._last + idleTimeout - Date.now());
514
+ this._idleTimer = setTimeout(function () {
515
+ self._idleTimer = null;
516
+ if (self.state !== 'open') return;
517
+ if (Date.now() - self._last >= idleTimeout) self._close('idle');
518
+ else self._armIdle();
519
+ }, wait);
520
+ if (this._idleTimer.unref) this._idleTimer.unref();
521
+ };
522
+
523
+ UdpTunnel.prototype.close = function () {
524
+ if (this.state === 'closed') return this;
525
+ if (this.state === 'pending') return this.reject(503);
526
+ this._close('local');
527
+ return this;
528
+ };
529
+
530
+ // The client ended its side, or the stream went.
531
+ UdpTunnel.prototype._ended = function (reason) {
532
+ if (this.state === 'closed') return;
533
+ this._close(reason);
534
+ };
535
+
536
+ UdpTunnel.prototype._close = function (reason) {
537
+ if (this.state === 'closed') return;
538
+ var wasPending = this.state === 'pending';
539
+ this.state = 'closed';
540
+ this.closeReason = reason;
541
+ this._stats.closedAt = Date.now();
542
+ this._early = null;
543
+ if (this._idleTimer) { clearTimeout(this._idleTimer); this._idleTimer = null; }
544
+ var relay = this._relay;
545
+ this._relay = null;
546
+ if (relay) {
547
+ relay.socket.removeListener('message', relay.onMessage);
548
+ relay.socket.removeListener('error', relay.onError);
549
+ if (relay.own) { relay.socket.on('error', function () {}); try { relay.socket.close(); } catch (e) {} }
550
+ }
551
+ // The request stream with it (§3.1): ended (FIN) after the client's
552
+ // end, ours (close()) or an idle one; reset when it broke — the socket
553
+ // failed, the stream was reset, or the client left before any answer.
554
+ // A refusal has sent its answer already.
555
+ if (reason !== 'rejected') {
556
+ if (wasPending || reason === 'aborted' || reason === 'socket') this._carrier.abort();
557
+ else this._carrier.end();
558
+ }
559
+ this.emit('close', this.stats);
560
+ };
561
+
562
+ return { handle: handle, stats: stats, compiled: compiled };
563
+ }
564
+
565
+
566
+ // Is this request a CONNECT-UDP? Over HTTP/2 and HTTP/3, a CONNECT with
567
+ // :protocol connect-udp; over HTTP/1.1, an Upgrade to it (§3.2).
568
+ function isConnectUdp(req) {
569
+ var h = (req && req.headers) || {};
570
+ if (h[':protocol'] != null) return String(h[':protocol']).toLowerCase() === 'connect-udp';
571
+ var up = h.upgrade;
572
+ if (up == null) return false;
573
+ return String(up).split(',').some(function (t) { return t.trim().toLowerCase() === 'connect-udp'; });
574
+ }
575
+
576
+
577
+ export { createConnectUdp, isConnectUdp, parseTarget, compileTemplate, isPrivateAddress,
578
+ createCapsuleParser, datagramCapsule, contextZero, udpPayloadOf, MAX_UDP_PAYLOAD, STREAM_QUEUE_MAX };