#!/usr/bin/env node const __import_meta_url = require("node:url").pathToFileURL(__filename).href; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJS = (cb, mod) => function __require() { try { return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; } catch (e) { throw mod = 0, e; } }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // node_modules/ws/lib/constants.js var require_constants = __commonJS({ "node_modules/ws/lib/constants.js"(exports2, module2) { "use strict"; var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"]; var hasBlob = typeof Blob !== "undefined"; if (hasBlob) BINARY_TYPES.push("blob"); module2.exports = { BINARY_TYPES, CLOSE_TIMEOUT: 3e4, EMPTY_BUFFER: Buffer.alloc(0), GUID: "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", hasBlob, kForOnEventAttribute: /* @__PURE__ */ Symbol("kIsForOnEventAttribute"), kListener: /* @__PURE__ */ Symbol("kListener"), kStatusCode: /* @__PURE__ */ Symbol("status-code"), kWebSocket: /* @__PURE__ */ Symbol("websocket"), NOOP: () => { } }; } }); // node_modules/ws/lib/buffer-util.js var require_buffer_util = __commonJS({ "node_modules/ws/lib/buffer-util.js"(exports2, module2) { "use strict"; var { EMPTY_BUFFER } = require_constants(); var FastBuffer = Buffer[Symbol.species]; function concat(list, totalLength) { if (list.length === 0) return EMPTY_BUFFER; if (list.length === 1) return list[0]; const target = Buffer.allocUnsafe(totalLength); let offset = 0; for (let i = 0; i < list.length; i++) { const buf = list[i]; target.set(buf, offset); offset += buf.length; } if (offset < totalLength) { return new FastBuffer(target.buffer, target.byteOffset, offset); } return target; } function _mask(source, mask, output, offset, length) { for (let i = 0; i < length; i++) { output[offset + i] = source[i] ^ mask[i & 3]; } } function _unmask(buffer, mask) { for (let i = 0; i < buffer.length; i++) { buffer[i] ^= mask[i & 3]; } } function toArrayBuffer(buf) { if (buf.length === buf.buffer.byteLength) { return buf.buffer; } return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length); } function toBuffer(data) { toBuffer.readOnly = true; if (Buffer.isBuffer(data)) return data; let buf; if (data instanceof ArrayBuffer) { buf = new FastBuffer(data); } else if (ArrayBuffer.isView(data)) { buf = new FastBuffer(data.buffer, data.byteOffset, data.byteLength); } else { buf = Buffer.from(data); toBuffer.readOnly = false; } return buf; } module2.exports = { concat, mask: _mask, toArrayBuffer, toBuffer, unmask: _unmask }; if (!process.env.WS_NO_BUFFER_UTIL) { try { const bufferUtil = require("bufferutil"); module2.exports.mask = function(source, mask, output, offset, length) { if (length < 48) _mask(source, mask, output, offset, length); else bufferUtil.mask(source, mask, output, offset, length); }; module2.exports.unmask = function(buffer, mask) { if (buffer.length < 32) _unmask(buffer, mask); else bufferUtil.unmask(buffer, mask); }; } catch (e) { } } } }); // node_modules/ws/lib/limiter.js var require_limiter = __commonJS({ "node_modules/ws/lib/limiter.js"(exports2, module2) { "use strict"; var kDone = /* @__PURE__ */ Symbol("kDone"); var kRun = /* @__PURE__ */ Symbol("kRun"); var Limiter = class { /** * Creates a new `Limiter`. * * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed * to run concurrently */ constructor(concurrency) { this[kDone] = () => { this.pending--; this[kRun](); }; this.concurrency = concurrency || Infinity; this.jobs = []; this.pending = 0; } /** * Adds a job to the queue. * * @param {Function} job The job to run * @public */ add(job) { this.jobs.push(job); this[kRun](); } /** * Removes a job from the queue and runs it if possible. * * @private */ [kRun]() { if (this.pending === this.concurrency) return; if (this.jobs.length) { const job = this.jobs.shift(); this.pending++; job(this[kDone]); } } }; module2.exports = Limiter; } }); // node_modules/ws/lib/permessage-deflate.js var require_permessage_deflate = __commonJS({ "node_modules/ws/lib/permessage-deflate.js"(exports2, module2) { "use strict"; var zlib = require("zlib"); var bufferUtil = require_buffer_util(); var Limiter = require_limiter(); var { kStatusCode } = require_constants(); var FastBuffer = Buffer[Symbol.species]; var TRAILER = Buffer.from([0, 0, 255, 255]); var kPerMessageDeflate = /* @__PURE__ */ Symbol("permessage-deflate"); var kTotalLength = /* @__PURE__ */ Symbol("total-length"); var kCallback = /* @__PURE__ */ Symbol("callback"); var kBuffers = /* @__PURE__ */ Symbol("buffers"); var kError = /* @__PURE__ */ Symbol("error"); var zlibLimiter; var PerMessageDeflate2 = class { /** * Creates a PerMessageDeflate instance. * * @param {Object} [options] Configuration options * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support * for, or request, a custom client window size * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/ * acknowledge disabling of client context takeover * @param {Number} [options.concurrencyLimit=10] The number of concurrent * calls to zlib * @param {Boolean} [options.isServer=false] Create the instance in either * server or client mode * @param {Number} [options.maxPayload=0] The maximum allowed message length * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the * use of a custom server window size * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept * disabling of server context takeover * @param {Number} [options.threshold=1024] Size (in bytes) below which * messages should not be compressed if context takeover is disabled * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on * deflate * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on * inflate */ constructor(options) { this._options = options || {}; this._threshold = this._options.threshold !== void 0 ? this._options.threshold : 1024; this._maxPayload = this._options.maxPayload | 0; this._isServer = !!this._options.isServer; this._deflate = null; this._inflate = null; this.params = null; if (!zlibLimiter) { const concurrency = this._options.concurrencyLimit !== void 0 ? this._options.concurrencyLimit : 10; zlibLimiter = new Limiter(concurrency); } } /** * @type {String} */ static get extensionName() { return "permessage-deflate"; } /** * Create an extension negotiation offer. * * @return {Object} Extension parameters * @public */ offer() { const params = {}; if (this._options.serverNoContextTakeover) { params.server_no_context_takeover = true; } if (this._options.clientNoContextTakeover) { params.client_no_context_takeover = true; } if (this._options.serverMaxWindowBits) { params.server_max_window_bits = this._options.serverMaxWindowBits; } if (this._options.clientMaxWindowBits) { params.client_max_window_bits = this._options.clientMaxWindowBits; } else if (this._options.clientMaxWindowBits == null) { params.client_max_window_bits = true; } return params; } /** * Accept an extension negotiation offer/response. * * @param {Array} configurations The extension negotiation offers/reponse * @return {Object} Accepted configuration * @public */ accept(configurations) { configurations = this.normalizeParams(configurations); this.params = this._isServer ? this.acceptAsServer(configurations) : this.acceptAsClient(configurations); return this.params; } /** * Releases all resources used by the extension. * * @public */ cleanup() { if (this._inflate) { this._inflate.close(); this._inflate = null; } if (this._deflate) { const callback = this._deflate[kCallback]; this._deflate.close(); this._deflate = null; if (callback) { callback( new Error( "The deflate stream was closed while data was being processed" ) ); } } } /** * Accept an extension negotiation offer. * * @param {Array} offers The extension negotiation offers * @return {Object} Accepted configuration * @private */ acceptAsServer(offers) { const opts = this._options; const accepted = offers.find((params) => { if (opts.serverNoContextTakeover === false && params.server_no_context_takeover || params.server_max_window_bits && (opts.serverMaxWindowBits === false || typeof opts.serverMaxWindowBits === "number" && opts.serverMaxWindowBits > params.server_max_window_bits) || typeof opts.clientMaxWindowBits === "number" && (typeof params.client_max_window_bits === "number" ? opts.clientMaxWindowBits > params.client_max_window_bits : !params.client_max_window_bits)) { return false; } return true; }); if (!accepted) { throw new Error("None of the extension offers can be accepted"); } if (opts.serverNoContextTakeover) { accepted.server_no_context_takeover = true; } if (opts.clientNoContextTakeover) { accepted.client_no_context_takeover = true; } if (typeof opts.serverMaxWindowBits === "number") { accepted.server_max_window_bits = opts.serverMaxWindowBits; } if (typeof opts.clientMaxWindowBits === "number") { accepted.client_max_window_bits = opts.clientMaxWindowBits; } else if (accepted.client_max_window_bits === true || opts.clientMaxWindowBits === false) { delete accepted.client_max_window_bits; } return accepted; } /** * Accept the extension negotiation response. * * @param {Array} response The extension negotiation response * @return {Object} Accepted configuration * @private */ acceptAsClient(response) { const params = response[0]; if (this._options.clientNoContextTakeover === false && params.client_no_context_takeover) { throw new Error('Unexpected parameter "client_no_context_takeover"'); } if (!params.client_max_window_bits) { if (typeof this._options.clientMaxWindowBits === "number") { params.client_max_window_bits = this._options.clientMaxWindowBits; } } else if (this._options.clientMaxWindowBits === false || typeof this._options.clientMaxWindowBits === "number" && params.client_max_window_bits > this._options.clientMaxWindowBits) { throw new Error( 'Unexpected or invalid parameter "client_max_window_bits"' ); } return params; } /** * Normalize parameters. * * @param {Array} configurations The extension negotiation offers/reponse * @return {Array} The offers/response with normalized parameters * @private */ normalizeParams(configurations) { configurations.forEach((params) => { Object.keys(params).forEach((key) => { let value = params[key]; if (value.length > 1) { throw new Error(`Parameter "${key}" must have only a single value`); } value = value[0]; if (key === "client_max_window_bits") { if (value !== true) { const num = +value; if (!Number.isInteger(num) || num < 8 || num > 15) { throw new TypeError( `Invalid value for parameter "${key}": ${value}` ); } value = num; } else if (!this._isServer) { throw new TypeError( `Invalid value for parameter "${key}": ${value}` ); } } else if (key === "server_max_window_bits") { const num = +value; if (!Number.isInteger(num) || num < 8 || num > 15) { throw new TypeError( `Invalid value for parameter "${key}": ${value}` ); } value = num; } else if (key === "client_no_context_takeover" || key === "server_no_context_takeover") { if (value !== true) { throw new TypeError( `Invalid value for parameter "${key}": ${value}` ); } } else { throw new Error(`Unknown parameter "${key}"`); } params[key] = value; }); }); return configurations; } /** * Decompress data. Concurrency limited. * * @param {Buffer} data Compressed data * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public */ decompress(data, fin, callback) { zlibLimiter.add((done) => { this._decompress(data, fin, (err, result) => { done(); callback(err, result); }); }); } /** * Compress data. Concurrency limited. * * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @public */ compress(data, fin, callback) { zlibLimiter.add((done) => { this._compress(data, fin, (err, result) => { done(); callback(err, result); }); }); } /** * Decompress data. * * @param {Buffer} data Compressed data * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @private */ _decompress(data, fin, callback) { const endpoint = this._isServer ? "client" : "server"; if (!this._inflate) { const key = `${endpoint}_max_window_bits`; const windowBits = typeof this.params[key] !== "number" ? zlib.Z_DEFAULT_WINDOWBITS : this.params[key]; this._inflate = zlib.createInflateRaw({ ...this._options.zlibInflateOptions, windowBits }); this._inflate[kPerMessageDeflate] = this; this._inflate[kTotalLength] = 0; this._inflate[kBuffers] = []; this._inflate.on("error", inflateOnError); this._inflate.on("data", inflateOnData); } this._inflate[kCallback] = callback; this._inflate.write(data); if (fin) this._inflate.write(TRAILER); this._inflate.flush(() => { const err = this._inflate[kError]; if (err) { this._inflate.close(); this._inflate = null; callback(err); return; } const data2 = bufferUtil.concat( this._inflate[kBuffers], this._inflate[kTotalLength] ); if (this._inflate._readableState.endEmitted) { this._inflate.close(); this._inflate = null; } else { this._inflate[kTotalLength] = 0; this._inflate[kBuffers] = []; if (fin && this.params[`${endpoint}_no_context_takeover`]) { this._inflate.reset(); } } callback(null, data2); }); } /** * Compress data. * * @param {(Buffer|String)} data Data to compress * @param {Boolean} fin Specifies whether or not this is the last fragment * @param {Function} callback Callback * @private */ _compress(data, fin, callback) { const endpoint = this._isServer ? "server" : "client"; if (!this._deflate) { const key = `${endpoint}_max_window_bits`; const windowBits = typeof this.params[key] !== "number" ? zlib.Z_DEFAULT_WINDOWBITS : this.params[key]; this._deflate = zlib.createDeflateRaw({ ...this._options.zlibDeflateOptions, windowBits }); this._deflate[kTotalLength] = 0; this._deflate[kBuffers] = []; this._deflate.on("data", deflateOnData); } this._deflate[kCallback] = callback; this._deflate.write(data); this._deflate.flush(zlib.Z_SYNC_FLUSH, () => { if (!this._deflate) { return; } let data2 = bufferUtil.concat( this._deflate[kBuffers], this._deflate[kTotalLength] ); if (fin) { data2 = new FastBuffer(data2.buffer, data2.byteOffset, data2.length - 4); } this._deflate[kCallback] = null; this._deflate[kTotalLength] = 0; this._deflate[kBuffers] = []; if (fin && this.params[`${endpoint}_no_context_takeover`]) { this._deflate.reset(); } callback(null, data2); }); } }; module2.exports = PerMessageDeflate2; function deflateOnData(chunk) { this[kBuffers].push(chunk); this[kTotalLength] += chunk.length; } function inflateOnData(chunk) { this[kTotalLength] += chunk.length; if (this[kPerMessageDeflate]._maxPayload < 1 || this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload) { this[kBuffers].push(chunk); return; } this[kError] = new RangeError("Max payload size exceeded"); this[kError].code = "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH"; this[kError][kStatusCode] = 1009; this.removeListener("data", inflateOnData); this.reset(); } function inflateOnError(err) { this[kPerMessageDeflate]._inflate = null; if (this[kError]) { this[kCallback](this[kError]); return; } err[kStatusCode] = 1007; this[kCallback](err); } } }); // node_modules/ws/lib/validation.js var require_validation = __commonJS({ "node_modules/ws/lib/validation.js"(exports2, module2) { "use strict"; var { isUtf8 } = require("buffer"); var { hasBlob } = require_constants(); var tokenChars = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127 ]; function isValidStatusCode(code) { return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006 || code >= 3e3 && code <= 4999; } function _isValidUTF8(buf) { const len = buf.length; let i = 0; while (i < len) { if ((buf[i] & 128) === 0) { i++; } else if ((buf[i] & 224) === 192) { if (i + 1 === len || (buf[i + 1] & 192) !== 128 || (buf[i] & 254) === 192) { return false; } i += 2; } else if ((buf[i] & 240) === 224) { if (i + 2 >= len || (buf[i + 1] & 192) !== 128 || (buf[i + 2] & 192) !== 128 || buf[i] === 224 && (buf[i + 1] & 224) === 128 || // Overlong buf[i] === 237 && (buf[i + 1] & 224) === 160) { return false; } i += 3; } else if ((buf[i] & 248) === 240) { if (i + 3 >= len || (buf[i + 1] & 192) !== 128 || (buf[i + 2] & 192) !== 128 || (buf[i + 3] & 192) !== 128 || buf[i] === 240 && (buf[i + 1] & 240) === 128 || // Overlong buf[i] === 244 && buf[i + 1] > 143 || buf[i] > 244) { return false; } i += 4; } else { return false; } } return true; } function isBlob(value) { return hasBlob && typeof value === "object" && typeof value.arrayBuffer === "function" && typeof value.type === "string" && typeof value.stream === "function" && (value[Symbol.toStringTag] === "Blob" || value[Symbol.toStringTag] === "File"); } module2.exports = { isBlob, isValidStatusCode, isValidUTF8: _isValidUTF8, tokenChars }; if (isUtf8) { module2.exports.isValidUTF8 = function(buf) { return buf.length < 24 ? _isValidUTF8(buf) : isUtf8(buf); }; } else if (!process.env.WS_NO_UTF_8_VALIDATE) { try { const isValidUTF8 = require("utf-8-validate"); module2.exports.isValidUTF8 = function(buf) { return buf.length < 32 ? _isValidUTF8(buf) : isValidUTF8(buf); }; } catch (e) { } } } }); // node_modules/ws/lib/receiver.js var require_receiver = __commonJS({ "node_modules/ws/lib/receiver.js"(exports2, module2) { "use strict"; var { Writable } = require("stream"); var PerMessageDeflate2 = require_permessage_deflate(); var { BINARY_TYPES, EMPTY_BUFFER, kStatusCode, kWebSocket } = require_constants(); var { concat, toArrayBuffer, unmask } = require_buffer_util(); var { isValidStatusCode, isValidUTF8 } = require_validation(); var FastBuffer = Buffer[Symbol.species]; var GET_INFO = 0; var GET_PAYLOAD_LENGTH_16 = 1; var GET_PAYLOAD_LENGTH_64 = 2; var GET_MASK = 3; var GET_DATA = 4; var INFLATING = 5; var DEFER_EVENT = 6; var Receiver2 = class extends Writable { /** * Creates a Receiver instance. * * @param {Object} [options] Options object * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted * multiple times in the same tick * @param {String} [options.binaryType=nodebuffer] The type for binary data * @param {Object} [options.extensions] An object containing the negotiated * extensions * @param {Boolean} [options.isServer=false] Specifies whether to operate in * client or server mode * @param {Number} [options.maxBufferedChunks=0] The maximum number of * buffered data chunks * @param {Number} [options.maxFragments=0] The maximum number of message * fragments * @param {Number} [options.maxPayload=0] The maximum allowed message length * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or * not to skip UTF-8 validation for text and close messages */ constructor(options = {}) { super(); this._allowSynchronousEvents = options.allowSynchronousEvents !== void 0 ? options.allowSynchronousEvents : true; this._binaryType = options.binaryType || BINARY_TYPES[0]; this._extensions = options.extensions || {}; this._isServer = !!options.isServer; this._maxBufferedChunks = options.maxBufferedChunks | 0; this._maxFragments = options.maxFragments | 0; this._maxPayload = options.maxPayload | 0; this._skipUTF8Validation = !!options.skipUTF8Validation; this[kWebSocket] = void 0; this._bufferedBytes = 0; this._buffers = []; this._compressed = false; this._payloadLength = 0; this._mask = void 0; this._fragmented = 0; this._masked = false; this._fin = false; this._opcode = 0; this._totalPayloadLength = 0; this._messageLength = 0; this._numFragments = 0; this._fragments = []; this._errored = false; this._loop = false; this._state = GET_INFO; } /** * Implements `Writable.prototype._write()`. * * @param {Buffer} chunk The chunk of data to write * @param {String} encoding The character encoding of `chunk` * @param {Function} cb Callback * @private */ _write(chunk, encoding, cb) { if (this._opcode === 8 && this._state == GET_INFO) return cb(); if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) { cb( this.createError( RangeError, "Too many buffered chunks", false, 1008, "WS_ERR_TOO_MANY_BUFFERED_PARTS" ) ); return; } this._bufferedBytes += chunk.length; this._buffers.push(chunk); this.startLoop(cb); } /** * Consumes `n` bytes from the buffered data. * * @param {Number} n The number of bytes to consume * @return {Buffer} The consumed bytes * @private */ consume(n) { this._bufferedBytes -= n; if (n === this._buffers[0].length) return this._buffers.shift(); if (n < this._buffers[0].length) { const buf = this._buffers[0]; this._buffers[0] = new FastBuffer( buf.buffer, buf.byteOffset + n, buf.length - n ); return new FastBuffer(buf.buffer, buf.byteOffset, n); } const dst = Buffer.allocUnsafe(n); do { const buf = this._buffers[0]; const offset = dst.length - n; if (n >= buf.length) { dst.set(this._buffers.shift(), offset); } else { dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset); this._buffers[0] = new FastBuffer( buf.buffer, buf.byteOffset + n, buf.length - n ); } n -= buf.length; } while (n > 0); return dst; } /** * Starts the parsing loop. * * @param {Function} cb Callback * @private */ startLoop(cb) { this._loop = true; do { switch (this._state) { case GET_INFO: this.getInfo(cb); break; case GET_PAYLOAD_LENGTH_16: this.getPayloadLength16(cb); break; case GET_PAYLOAD_LENGTH_64: this.getPayloadLength64(cb); break; case GET_MASK: this.getMask(); break; case GET_DATA: this.getData(cb); break; case INFLATING: case DEFER_EVENT: this._loop = false; return; } } while (this._loop); if (!this._errored) cb(); } /** * Reads the first two bytes of a frame. * * @param {Function} cb Callback * @private */ getInfo(cb) { if (this._bufferedBytes < 2) { this._loop = false; return; } const buf = this.consume(2); if ((buf[0] & 48) !== 0) { const error = this.createError( RangeError, "RSV2 and RSV3 must be clear", true, 1002, "WS_ERR_UNEXPECTED_RSV_2_3" ); cb(error); return; } const compressed = (buf[0] & 64) === 64; if (compressed && !this._extensions[PerMessageDeflate2.extensionName]) { const error = this.createError( RangeError, "RSV1 must be clear", true, 1002, "WS_ERR_UNEXPECTED_RSV_1" ); cb(error); return; } this._fin = (buf[0] & 128) === 128; this._opcode = buf[0] & 15; this._payloadLength = buf[1] & 127; if (this._opcode === 0) { if (compressed) { const error = this.createError( RangeError, "RSV1 must be clear", true, 1002, "WS_ERR_UNEXPECTED_RSV_1" ); cb(error); return; } if (!this._fragmented) { const error = this.createError( RangeError, "invalid opcode 0", true, 1002, "WS_ERR_INVALID_OPCODE" ); cb(error); return; } this._opcode = this._fragmented; } else if (this._opcode === 1 || this._opcode === 2) { if (this._fragmented) { const error = this.createError( RangeError, `invalid opcode ${this._opcode}`, true, 1002, "WS_ERR_INVALID_OPCODE" ); cb(error); return; } this._compressed = compressed; } else if (this._opcode > 7 && this._opcode < 11) { if (!this._fin) { const error = this.createError( RangeError, "FIN must be set", true, 1002, "WS_ERR_EXPECTED_FIN" ); cb(error); return; } if (compressed) { const error = this.createError( RangeError, "RSV1 must be clear", true, 1002, "WS_ERR_UNEXPECTED_RSV_1" ); cb(error); return; } if (this._payloadLength > 125 || this._opcode === 8 && this._payloadLength === 1) { const error = this.createError( RangeError, `invalid payload length ${this._payloadLength}`, true, 1002, "WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH" ); cb(error); return; } } else { const error = this.createError( RangeError, `invalid opcode ${this._opcode}`, true, 1002, "WS_ERR_INVALID_OPCODE" ); cb(error); return; } if (!this._fin && !this._fragmented) this._fragmented = this._opcode; this._masked = (buf[1] & 128) === 128; if (this._isServer) { if (!this._masked) { const error = this.createError( RangeError, "MASK must be set", true, 1002, "WS_ERR_EXPECTED_MASK" ); cb(error); return; } } else if (this._masked) { const error = this.createError( RangeError, "MASK must be clear", true, 1002, "WS_ERR_UNEXPECTED_MASK" ); cb(error); return; } if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16; else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64; else this.haveLength(cb); } /** * Gets extended payload length (7+16). * * @param {Function} cb Callback * @private */ getPayloadLength16(cb) { if (this._bufferedBytes < 2) { this._loop = false; return; } this._payloadLength = this.consume(2).readUInt16BE(0); this.haveLength(cb); } /** * Gets extended payload length (7+64). * * @param {Function} cb Callback * @private */ getPayloadLength64(cb) { if (this._bufferedBytes < 8) { this._loop = false; return; } const buf = this.consume(8); const num = buf.readUInt32BE(0); if (num > Math.pow(2, 53 - 32) - 1) { const error = this.createError( RangeError, "Unsupported WebSocket frame: payload length > 2^53 - 1", false, 1009, "WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH" ); cb(error); return; } this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4); this.haveLength(cb); } /** * Payload length has been read. * * @param {Function} cb Callback * @private */ haveLength(cb) { if (this._payloadLength && this._opcode < 8) { this._totalPayloadLength += this._payloadLength; if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) { const error = this.createError( RangeError, "Max payload size exceeded", false, 1009, "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ); cb(error); return; } } if (this._masked) this._state = GET_MASK; else this._state = GET_DATA; } /** * Reads mask bytes. * * @private */ getMask() { if (this._bufferedBytes < 4) { this._loop = false; return; } this._mask = this.consume(4); this._state = GET_DATA; } /** * Reads data bytes. * * @param {Function} cb Callback * @private */ getData(cb) { let data = EMPTY_BUFFER; if (this._payloadLength) { if (this._bufferedBytes < this._payloadLength) { this._loop = false; return; } data = this.consume(this._payloadLength); if (this._masked && (this._mask[0] | this._mask[1] | this._mask[2] | this._mask[3]) !== 0) { unmask(data, this._mask); } } if (this._opcode > 7) { this.controlMessage(data, cb); return; } if (this._maxFragments > 0 && ++this._numFragments > this._maxFragments) { const error = this.createError( RangeError, "Too many message fragments", false, 1008, "WS_ERR_TOO_MANY_BUFFERED_PARTS" ); cb(error); return; } if (this._compressed) { this._state = INFLATING; this.decompress(data, cb); return; } if (data.length) { this._messageLength = this._totalPayloadLength; this._fragments.push(data); } this.dataMessage(cb); } /** * Decompresses data. * * @param {Buffer} data Compressed data * @param {Function} cb Callback * @private */ decompress(data, cb) { const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName]; perMessageDeflate.decompress(data, this._fin, (err, buf) => { if (err) return cb(err); if (buf.length) { this._messageLength += buf.length; if (this._messageLength > this._maxPayload && this._maxPayload > 0) { const error = this.createError( RangeError, "Max payload size exceeded", false, 1009, "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH" ); cb(error); return; } this._fragments.push(buf); } this.dataMessage(cb); if (this._state === GET_INFO) this.startLoop(cb); }); } /** * Handles a data message. * * @param {Function} cb Callback * @private */ dataMessage(cb) { if (!this._fin) { this._state = GET_INFO; return; } const messageLength = this._messageLength; const fragments = this._fragments; this._totalPayloadLength = 0; this._messageLength = 0; this._fragmented = 0; this._numFragments = 0; this._fragments = []; if (this._opcode === 2) { let data; if (this._binaryType === "nodebuffer") { data = concat(fragments, messageLength); } else if (this._binaryType === "arraybuffer") { data = toArrayBuffer(concat(fragments, messageLength)); } else if (this._binaryType === "blob") { data = new Blob(fragments); } else { data = fragments; } if (this._allowSynchronousEvents) { this.emit("message", data, true); this._state = GET_INFO; } else { this._state = DEFER_EVENT; setImmediate(() => { this.emit("message", data, true); this._state = GET_INFO; this.startLoop(cb); }); } } else { const buf = concat(fragments, messageLength); if (!this._skipUTF8Validation && !isValidUTF8(buf)) { const error = this.createError( Error, "invalid UTF-8 sequence", true, 1007, "WS_ERR_INVALID_UTF8" ); cb(error); return; } if (this._state === INFLATING || this._allowSynchronousEvents) { this.emit("message", buf, false); this._state = GET_INFO; } else { this._state = DEFER_EVENT; setImmediate(() => { this.emit("message", buf, false); this._state = GET_INFO; this.startLoop(cb); }); } } } /** * Handles a control message. * * @param {Buffer} data Data to handle * @return {(Error|RangeError|undefined)} A possible error * @private */ controlMessage(data, cb) { if (this._opcode === 8) { if (data.length === 0) { this._loop = false; this.emit("conclude", 1005, EMPTY_BUFFER); this.end(); } else { const code = data.readUInt16BE(0); if (!isValidStatusCode(code)) { const error = this.createError( RangeError, `invalid status code ${code}`, true, 1002, "WS_ERR_INVALID_CLOSE_CODE" ); cb(error); return; } const buf = new FastBuffer( data.buffer, data.byteOffset + 2, data.length - 2 ); if (!this._skipUTF8Validation && !isValidUTF8(buf)) { const error = this.createError( Error, "invalid UTF-8 sequence", true, 1007, "WS_ERR_INVALID_UTF8" ); cb(error); return; } this._loop = false; this.emit("conclude", code, buf); this.end(); } this._state = GET_INFO; return; } if (this._allowSynchronousEvents) { this.emit(this._opcode === 9 ? "ping" : "pong", data); this._state = GET_INFO; } else { this._state = DEFER_EVENT; setImmediate(() => { this.emit(this._opcode === 9 ? "ping" : "pong", data); this._state = GET_INFO; this.startLoop(cb); }); } } /** * Builds an error object. * * @param {function(new:Error|RangeError)} ErrorCtor The error constructor * @param {String} message The error message * @param {Boolean} prefix Specifies whether or not to add a default prefix to * `message` * @param {Number} statusCode The status code * @param {String} errorCode The exposed error code * @return {(Error|RangeError)} The error * @private */ createError(ErrorCtor, message, prefix, statusCode, errorCode) { this._loop = false; this._errored = true; const err = new ErrorCtor( prefix ? `Invalid WebSocket frame: ${message}` : message ); Error.captureStackTrace(err, this.createError); err.code = errorCode; err[kStatusCode] = statusCode; return err; } }; module2.exports = Receiver2; } }); // node_modules/ws/lib/sender.js var require_sender = __commonJS({ "node_modules/ws/lib/sender.js"(exports2, module2) { "use strict"; var { Duplex: Duplex2 } = require("stream"); var { randomFillSync } = require("crypto"); var { types: { isUint8Array } } = require("util"); var PerMessageDeflate2 = require_permessage_deflate(); var { EMPTY_BUFFER, kWebSocket, NOOP } = require_constants(); var { isBlob, isValidStatusCode } = require_validation(); var { mask: applyMask, toBuffer } = require_buffer_util(); var kByteLength = /* @__PURE__ */ Symbol("kByteLength"); var maskBuffer = Buffer.alloc(4); var RANDOM_POOL_SIZE = 8 * 1024; var randomPool; var randomPoolPointer = RANDOM_POOL_SIZE; var DEFAULT = 0; var DEFLATING = 1; var GET_BLOB_DATA = 2; var Sender2 = class _Sender { /** * Creates a Sender instance. * * @param {Duplex} socket The connection socket * @param {Object} [extensions] An object containing the negotiated extensions * @param {Function} [generateMask] The function used to generate the masking * key */ constructor(socket, extensions, generateMask) { this._extensions = extensions || {}; if (generateMask) { this._generateMask = generateMask; this._maskBuffer = Buffer.alloc(4); } this._socket = socket; this._firstFragment = true; this._compress = false; this._bufferedBytes = 0; this._queue = []; this._state = DEFAULT; this.onerror = NOOP; this[kWebSocket] = void 0; } /** * Frames a piece of data according to the HyBi WebSocket protocol. * * @param {(Buffer|String)} data The data to frame * @param {Object} options Options object * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit * @param {Function} [options.generateMask] The function used to generate the * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` * @param {Buffer} [options.maskBuffer] The buffer used to store the masking * key * @param {Number} options.opcode The opcode * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @return {(Buffer|String)[]} The framed data * @public */ static frame(data, options) { let mask; let merge = false; let offset = 2; let skipMasking = false; if (options.mask) { mask = options.maskBuffer || maskBuffer; if (options.generateMask) { options.generateMask(mask); } else { if (randomPoolPointer === RANDOM_POOL_SIZE) { if (randomPool === void 0) { randomPool = Buffer.alloc(RANDOM_POOL_SIZE); } randomFillSync(randomPool, 0, RANDOM_POOL_SIZE); randomPoolPointer = 0; } mask[0] = randomPool[randomPoolPointer++]; mask[1] = randomPool[randomPoolPointer++]; mask[2] = randomPool[randomPoolPointer++]; mask[3] = randomPool[randomPoolPointer++]; } skipMasking = (mask[0] | mask[1] | mask[2] | mask[3]) === 0; offset = 6; } let dataLength; if (typeof data === "string") { if ((!options.mask || skipMasking) && options[kByteLength] !== void 0) { dataLength = options[kByteLength]; } else { data = Buffer.from(data); dataLength = data.length; } } else { dataLength = data.length; merge = options.mask && options.readOnly && !skipMasking; } let payloadLength = dataLength; if (dataLength >= 65536) { offset += 8; payloadLength = 127; } else if (dataLength > 125) { offset += 2; payloadLength = 126; } const target = Buffer.allocUnsafe(merge ? dataLength + offset : offset); target[0] = options.fin ? options.opcode | 128 : options.opcode; if (options.rsv1) target[0] |= 64; target[1] = payloadLength; if (payloadLength === 126) { target.writeUInt16BE(dataLength, 2); } else if (payloadLength === 127) { target[2] = target[3] = 0; target.writeUIntBE(dataLength, 4, 6); } if (!options.mask) return [target, data]; target[1] |= 128; target[offset - 4] = mask[0]; target[offset - 3] = mask[1]; target[offset - 2] = mask[2]; target[offset - 1] = mask[3]; if (skipMasking) return [target, data]; if (merge) { applyMask(data, mask, target, offset, dataLength); return [target]; } applyMask(data, mask, data, 0, dataLength); return [target, data]; } /** * Sends a close message to the other peer. * * @param {Number} [code] The status code component of the body * @param {(String|Buffer)} [data] The message component of the body * @param {Boolean} [mask=false] Specifies whether or not to mask the message * @param {Function} [cb] Callback * @public */ close(code, data, mask, cb) { let buf; if (code === void 0) { buf = EMPTY_BUFFER; } else if (typeof code !== "number" || !isValidStatusCode(code)) { throw new TypeError("First argument must be a valid error code number"); } else if (data === void 0 || !data.length) { buf = Buffer.allocUnsafe(2); buf.writeUInt16BE(code, 0); } else { const length = Buffer.byteLength(data); if (length > 123) { throw new RangeError("The message must not be greater than 123 bytes"); } buf = Buffer.allocUnsafe(2 + length); buf.writeUInt16BE(code, 0); if (typeof data === "string") { buf.write(data, 2); } else if (isUint8Array(data)) { buf.set(data, 2); } else { throw new TypeError("Second argument must be a string or a Uint8Array"); } } const options = { [kByteLength]: buf.length, fin: true, generateMask: this._generateMask, mask, maskBuffer: this._maskBuffer, opcode: 8, readOnly: false, rsv1: false }; if (this._state !== DEFAULT) { this.enqueue([this.dispatch, buf, false, options, cb]); } else { this.sendFrame(_Sender.frame(buf, options), cb); } } /** * Sends a ping message to the other peer. * * @param {*} data The message to send * @param {Boolean} [mask=false] Specifies whether or not to mask `data` * @param {Function} [cb] Callback * @public */ ping(data, mask, cb) { let byteLength; let readOnly; if (typeof data === "string") { byteLength = Buffer.byteLength(data); readOnly = false; } else if (isBlob(data)) { byteLength = data.size; readOnly = false; } else { data = toBuffer(data); byteLength = data.length; readOnly = toBuffer.readOnly; } if (byteLength > 125) { throw new RangeError("The data size must not be greater than 125 bytes"); } const options = { [kByteLength]: byteLength, fin: true, generateMask: this._generateMask, mask, maskBuffer: this._maskBuffer, opcode: 9, readOnly, rsv1: false }; if (isBlob(data)) { if (this._state !== DEFAULT) { this.enqueue([this.getBlobData, data, false, options, cb]); } else { this.getBlobData(data, false, options, cb); } } else if (this._state !== DEFAULT) { this.enqueue([this.dispatch, data, false, options, cb]); } else { this.sendFrame(_Sender.frame(data, options), cb); } } /** * Sends a pong message to the other peer. * * @param {*} data The message to send * @param {Boolean} [mask=false] Specifies whether or not to mask `data` * @param {Function} [cb] Callback * @public */ pong(data, mask, cb) { let byteLength; let readOnly; if (typeof data === "string") { byteLength = Buffer.byteLength(data); readOnly = false; } else if (isBlob(data)) { byteLength = data.size; readOnly = false; } else { data = toBuffer(data); byteLength = data.length; readOnly = toBuffer.readOnly; } if (byteLength > 125) { throw new RangeError("The data size must not be greater than 125 bytes"); } const options = { [kByteLength]: byteLength, fin: true, generateMask: this._generateMask, mask, maskBuffer: this._maskBuffer, opcode: 10, readOnly, rsv1: false }; if (isBlob(data)) { if (this._state !== DEFAULT) { this.enqueue([this.getBlobData, data, false, options, cb]); } else { this.getBlobData(data, false, options, cb); } } else if (this._state !== DEFAULT) { this.enqueue([this.dispatch, data, false, options, cb]); } else { this.sendFrame(_Sender.frame(data, options), cb); } } /** * Sends a data message to the other peer. * * @param {*} data The message to send * @param {Object} options Options object * @param {Boolean} [options.binary=false] Specifies whether `data` is binary * or text * @param {Boolean} [options.compress=false] Specifies whether or not to * compress `data` * @param {Boolean} [options.fin=false] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` * @param {Function} [cb] Callback * @public */ send(data, options, cb) { const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName]; let opcode = options.binary ? 2 : 1; let rsv1 = options.compress; let byteLength; let readOnly; if (typeof data === "string") { byteLength = Buffer.byteLength(data); readOnly = false; } else if (isBlob(data)) { byteLength = data.size; readOnly = false; } else { data = toBuffer(data); byteLength = data.length; readOnly = toBuffer.readOnly; } if (this._firstFragment) { this._firstFragment = false; if (rsv1 && perMessageDeflate && perMessageDeflate.params[perMessageDeflate._isServer ? "server_no_context_takeover" : "client_no_context_takeover"]) { rsv1 = byteLength >= perMessageDeflate._threshold; } this._compress = rsv1; } else { rsv1 = false; opcode = 0; } if (options.fin) this._firstFragment = true; const opts = { [kByteLength]: byteLength, fin: options.fin, generateMask: this._generateMask, mask: options.mask, maskBuffer: this._maskBuffer, opcode, readOnly, rsv1 }; if (isBlob(data)) { if (this._state !== DEFAULT) { this.enqueue([this.getBlobData, data, this._compress, opts, cb]); } else { this.getBlobData(data, this._compress, opts, cb); } } else if (this._state !== DEFAULT) { this.enqueue([this.dispatch, data, this._compress, opts, cb]); } else { this.dispatch(data, this._compress, opts, cb); } } /** * Gets the contents of a blob as binary data. * * @param {Blob} blob The blob * @param {Boolean} [compress=false] Specifies whether or not to compress * the data * @param {Object} options Options object * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit * @param {Function} [options.generateMask] The function used to generate the * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` * @param {Buffer} [options.maskBuffer] The buffer used to store the masking * key * @param {Number} options.opcode The opcode * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @param {Function} [cb] Callback * @private */ getBlobData(blob, compress, options, cb) { this._bufferedBytes += options[kByteLength]; this._state = GET_BLOB_DATA; blob.arrayBuffer().then((arrayBuffer) => { if (this._socket.destroyed) { const err = new Error( "The socket was closed while the blob was being read" ); process.nextTick(callCallbacks, this, err, cb); return; } this._bufferedBytes -= options[kByteLength]; const data = toBuffer(arrayBuffer); if (!compress) { this._state = DEFAULT; this.sendFrame(_Sender.frame(data, options), cb); this.dequeue(); } else { this.dispatch(data, compress, options, cb); } }).catch((err) => { process.nextTick(onError, this, err, cb); }); } /** * Dispatches a message. * * @param {(Buffer|String)} data The message to send * @param {Boolean} [compress=false] Specifies whether or not to compress * `data` * @param {Object} options Options object * @param {Boolean} [options.fin=false] Specifies whether or not to set the * FIN bit * @param {Function} [options.generateMask] The function used to generate the * masking key * @param {Boolean} [options.mask=false] Specifies whether or not to mask * `data` * @param {Buffer} [options.maskBuffer] The buffer used to store the masking * key * @param {Number} options.opcode The opcode * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be * modified * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the * RSV1 bit * @param {Function} [cb] Callback * @private */ dispatch(data, compress, options, cb) { if (!compress) { this.sendFrame(_Sender.frame(data, options), cb); return; } const perMessageDeflate = this._extensions[PerMessageDeflate2.extensionName]; this._bufferedBytes += options[kByteLength]; this._state = DEFLATING; perMessageDeflate.compress(data, options.fin, (_, buf) => { if (this._socket.destroyed) { const err = new Error( "The socket was closed while data was being compressed" ); callCallbacks(this, err, cb); return; } this._bufferedBytes -= options[kByteLength]; this._state = DEFAULT; options.readOnly = false; this.sendFrame(_Sender.frame(buf, options), cb); this.dequeue(); }); } /** * Executes queued send operations. * * @private */ dequeue() { while (this._state === DEFAULT && this._queue.length) { const params = this._queue.shift(); this._bufferedBytes -= params[3][kByteLength]; Reflect.apply(params[0], this, params.slice(1)); } } /** * Enqueues a send operation. * * @param {Array} params Send operation parameters. * @private */ enqueue(params) { this._bufferedBytes += params[3][kByteLength]; this._queue.push(params); } /** * Sends a frame. * * @param {(Buffer | String)[]} list The frame to send * @param {Function} [cb] Callback * @private */ sendFrame(list, cb) { if (list.length === 2) { this._socket.cork(); this._socket.write(list[0]); this._socket.write(list[1], cb); this._socket.uncork(); } else { this._socket.write(list[0], cb); } } }; module2.exports = Sender2; function callCallbacks(sender, err, cb) { if (typeof cb === "function") cb(err); for (let i = 0; i < sender._queue.length; i++) { const params = sender._queue[i]; const callback = params[params.length - 1]; if (typeof callback === "function") callback(err); } } function onError(sender, err, cb) { callCallbacks(sender, err, cb); sender.onerror(err); } } }); // node_modules/ws/lib/event-target.js var require_event_target = __commonJS({ "node_modules/ws/lib/event-target.js"(exports2, module2) { "use strict"; var { kForOnEventAttribute, kListener } = require_constants(); var kCode = /* @__PURE__ */ Symbol("kCode"); var kData = /* @__PURE__ */ Symbol("kData"); var kError = /* @__PURE__ */ Symbol("kError"); var kMessage = /* @__PURE__ */ Symbol("kMessage"); var kReason = /* @__PURE__ */ Symbol("kReason"); var kTarget = /* @__PURE__ */ Symbol("kTarget"); var kType = /* @__PURE__ */ Symbol("kType"); var kWasClean = /* @__PURE__ */ Symbol("kWasClean"); var Event = class { /** * Create a new `Event`. * * @param {String} type The name of the event * @throws {TypeError} If the `type` argument is not specified */ constructor(type) { this[kTarget] = null; this[kType] = type; } /** * @type {*} */ get target() { return this[kTarget]; } /** * @type {String} */ get type() { return this[kType]; } }; Object.defineProperty(Event.prototype, "target", { enumerable: true }); Object.defineProperty(Event.prototype, "type", { enumerable: true }); var CloseEvent = class extends Event { /** * Create a new `CloseEvent`. * * @param {String} type The name of the event * @param {Object} [options] A dictionary object that allows for setting * attributes via object members of the same name * @param {Number} [options.code=0] The status code explaining why the * connection was closed * @param {String} [options.reason=''] A human-readable string explaining why * the connection was closed * @param {Boolean} [options.wasClean=false] Indicates whether or not the * connection was cleanly closed */ constructor(type, options = {}) { super(type); this[kCode] = options.code === void 0 ? 0 : options.code; this[kReason] = options.reason === void 0 ? "" : options.reason; this[kWasClean] = options.wasClean === void 0 ? false : options.wasClean; } /** * @type {Number} */ get code() { return this[kCode]; } /** * @type {String} */ get reason() { return this[kReason]; } /** * @type {Boolean} */ get wasClean() { return this[kWasClean]; } }; Object.defineProperty(CloseEvent.prototype, "code", { enumerable: true }); Object.defineProperty(CloseEvent.prototype, "reason", { enumerable: true }); Object.defineProperty(CloseEvent.prototype, "wasClean", { enumerable: true }); var ErrorEvent = class extends Event { /** * Create a new `ErrorEvent`. * * @param {String} type The name of the event * @param {Object} [options] A dictionary object that allows for setting * attributes via object members of the same name * @param {*} [options.error=null] The error that generated this event * @param {String} [options.message=''] The error message */ constructor(type, options = {}) { super(type); this[kError] = options.error === void 0 ? null : options.error; this[kMessage] = options.message === void 0 ? "" : options.message; } /** * @type {*} */ get error() { return this[kError]; } /** * @type {String} */ get message() { return this[kMessage]; } }; Object.defineProperty(ErrorEvent.prototype, "error", { enumerable: true }); Object.defineProperty(ErrorEvent.prototype, "message", { enumerable: true }); var MessageEvent = class extends Event { /** * Create a new `MessageEvent`. * * @param {String} type The name of the event * @param {Object} [options] A dictionary object that allows for setting * attributes via object members of the same name * @param {*} [options.data=null] The message content */ constructor(type, options = {}) { super(type); this[kData] = options.data === void 0 ? null : options.data; } /** * @type {*} */ get data() { return this[kData]; } }; Object.defineProperty(MessageEvent.prototype, "data", { enumerable: true }); var EventTarget = { /** * Register an event listener. * * @param {String} type A string representing the event type to listen for * @param {(Function|Object)} handler The listener to add * @param {Object} [options] An options object specifies characteristics about * the event listener * @param {Boolean} [options.once=false] A `Boolean` indicating that the * listener should be invoked at most once after being added. If `true`, * the listener would be automatically removed when invoked. * @public */ addEventListener(type, handler, options = {}) { for (const listener of this.listeners(type)) { if (!options[kForOnEventAttribute] && listener[kListener] === handler && !listener[kForOnEventAttribute]) { return; } } let wrapper; if (type === "message") { wrapper = function onMessage(data, isBinary) { const event = new MessageEvent("message", { data: isBinary ? data : data.toString() }); event[kTarget] = this; callListener(handler, this, event); }; } else if (type === "close") { wrapper = function onClose(code, message) { const event = new CloseEvent("close", { code, reason: message.toString(), wasClean: this._closeFrameReceived && this._closeFrameSent }); event[kTarget] = this; callListener(handler, this, event); }; } else if (type === "error") { wrapper = function onError(error) { const event = new ErrorEvent("error", { error, message: error.message }); event[kTarget] = this; callListener(handler, this, event); }; } else if (type === "open") { wrapper = function onOpen() { const event = new Event("open"); event[kTarget] = this; callListener(handler, this, event); }; } else { return; } wrapper[kForOnEventAttribute] = !!options[kForOnEventAttribute]; wrapper[kListener] = handler; if (options.once) { this.once(type, wrapper); } else { this.on(type, wrapper); } }, /** * Remove an event listener. * * @param {String} type A string representing the event type to remove * @param {(Function|Object)} handler The listener to remove * @public */ removeEventListener(type, handler) { for (const listener of this.listeners(type)) { if (listener[kListener] === handler && !listener[kForOnEventAttribute]) { this.removeListener(type, listener); break; } } } }; module2.exports = { CloseEvent, ErrorEvent, Event, EventTarget, MessageEvent }; function callListener(listener, thisArg, event) { if (typeof listener === "object" && listener.handleEvent) { listener.handleEvent.call(listener, event); } else { listener.call(thisArg, event); } } } }); // node_modules/ws/lib/extension.js var require_extension = __commonJS({ "node_modules/ws/lib/extension.js"(exports2, module2) { "use strict"; var { tokenChars } = require_validation(); function push(dest, name, elem) { if (dest[name] === void 0) dest[name] = [elem]; else dest[name].push(elem); } function parse(header) { const offers = /* @__PURE__ */ Object.create(null); let params = /* @__PURE__ */ Object.create(null); let mustUnescape = false; let isEscaping = false; let inQuotes = false; let extensionName; let paramName; let start = -1; let code = -1; let end = -1; let i = 0; for (; i < header.length; i++) { code = header.charCodeAt(i); if (extensionName === void 0) { if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; } else if (i !== 0 && (code === 32 || code === 9)) { if (end === -1 && start !== -1) end = i; } else if (code === 59 || code === 44) { if (start === -1) { throw new SyntaxError(`Unexpected character at index ${i}`); } if (end === -1) end = i; const name = header.slice(start, end); if (code === 44) { push(offers, name, params); params = /* @__PURE__ */ Object.create(null); } else { extensionName = name; } start = end = -1; } else { throw new SyntaxError(`Unexpected character at index ${i}`); } } else if (paramName === void 0) { if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; } else if (code === 32 || code === 9) { if (end === -1 && start !== -1) end = i; } else if (code === 59 || code === 44) { if (start === -1) { throw new SyntaxError(`Unexpected character at index ${i}`); } if (end === -1) end = i; push(params, header.slice(start, end), true); if (code === 44) { push(offers, extensionName, params); params = /* @__PURE__ */ Object.create(null); extensionName = void 0; } start = end = -1; } else if (code === 61 && start !== -1 && end === -1) { paramName = header.slice(start, i); start = end = -1; } else { throw new SyntaxError(`Unexpected character at index ${i}`); } } else { if (isEscaping) { if (tokenChars[code] !== 1) { throw new SyntaxError(`Unexpected character at index ${i}`); } if (start === -1) start = i; else if (!mustUnescape) mustUnescape = true; isEscaping = false; } else if (inQuotes) { if (tokenChars[code] === 1) { if (start === -1) start = i; } else if (code === 34 && start !== -1) { inQuotes = false; end = i; } else if (code === 92) { isEscaping = true; } else { throw new SyntaxError(`Unexpected character at index ${i}`); } } else if (code === 34 && header.charCodeAt(i - 1) === 61) { inQuotes = true; } else if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; } else if (start !== -1 && (code === 32 || code === 9)) { if (end === -1) end = i; } else if (code === 59 || code === 44) { if (start === -1) { throw new SyntaxError(`Unexpected character at index ${i}`); } if (end === -1) end = i; let value = header.slice(start, end); if (mustUnescape) { value = value.replace(/\\/g, ""); mustUnescape = false; } push(params, paramName, value); if (code === 44) { push(offers, extensionName, params); params = /* @__PURE__ */ Object.create(null); extensionName = void 0; } paramName = void 0; start = end = -1; } else { throw new SyntaxError(`Unexpected character at index ${i}`); } } } if (start === -1 || inQuotes || code === 32 || code === 9) { throw new SyntaxError("Unexpected end of input"); } if (end === -1) end = i; const token = header.slice(start, end); if (extensionName === void 0) { push(offers, token, params); } else { if (paramName === void 0) { push(params, token, true); } else if (mustUnescape) { push(params, paramName, token.replace(/\\/g, "")); } else { push(params, paramName, token); } push(offers, extensionName, params); } return offers; } function format(extensions) { return Object.keys(extensions).map((extension2) => { let configurations = extensions[extension2]; if (!Array.isArray(configurations)) configurations = [configurations]; return configurations.map((params) => { return [extension2].concat( Object.keys(params).map((k) => { let values = params[k]; if (!Array.isArray(values)) values = [values]; return values.map((v) => v === true ? k : `${k}=${v}`).join("; "); }) ).join("; "); }).join(", "); }).join(", "); } module2.exports = { format, parse }; } }); // node_modules/ws/lib/websocket.js var require_websocket = __commonJS({ "node_modules/ws/lib/websocket.js"(exports2, module2) { "use strict"; var EventEmitter3 = require("events"); var https = require("https"); var http2 = require("http"); var net7 = require("net"); var tls = require("tls"); var { randomBytes, createHash } = require("crypto"); var { Duplex: Duplex2, Readable } = require("stream"); var { URL: URL2 } = require("url"); var PerMessageDeflate2 = require_permessage_deflate(); var Receiver2 = require_receiver(); var Sender2 = require_sender(); var { isBlob } = require_validation(); var { BINARY_TYPES, CLOSE_TIMEOUT, EMPTY_BUFFER, GUID, kForOnEventAttribute, kListener, kStatusCode, kWebSocket, NOOP } = require_constants(); var { EventTarget: { addEventListener, removeEventListener } } = require_event_target(); var { format, parse } = require_extension(); var { toBuffer } = require_buffer_util(); var kAborted = /* @__PURE__ */ Symbol("kAborted"); var protocolVersions = [8, 13]; var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"]; var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/; var WebSocket2 = class _WebSocket extends EventEmitter3 { /** * Create a new `WebSocket`. * * @param {(String|URL)} address The URL to which to connect * @param {(String|String[])} [protocols] The subprotocols * @param {Object} [options] Connection options */ constructor(address, protocols, options) { super(); this._binaryType = BINARY_TYPES[0]; this._closeCode = 1006; this._closeFrameReceived = false; this._closeFrameSent = false; this._closeMessage = EMPTY_BUFFER; this._closeTimer = null; this._errorEmitted = false; this._extensions = {}; this._paused = false; this._protocol = ""; this._readyState = _WebSocket.CONNECTING; this._receiver = null; this._sender = null; this._socket = null; if (address !== null) { this._bufferedAmount = 0; this._isServer = false; this._redirects = 0; if (protocols === void 0) { protocols = []; } else if (!Array.isArray(protocols)) { if (typeof protocols === "object" && protocols !== null) { options = protocols; protocols = []; } else { protocols = [protocols]; } } initAsClient(this, address, protocols, options); } else { this._autoPong = options.autoPong; this._closeTimeout = options.closeTimeout; this._isServer = true; } } /** * For historical reasons, the custom "nodebuffer" type is used by the default * instead of "blob". * * @type {String} */ get binaryType() { return this._binaryType; } set binaryType(type) { if (!BINARY_TYPES.includes(type)) return; this._binaryType = type; if (this._receiver) this._receiver._binaryType = type; } /** * @type {Number} */ get bufferedAmount() { if (!this._socket) return this._bufferedAmount; return this._socket._writableState.length + this._sender._bufferedBytes; } /** * @type {String} */ get extensions() { return Object.keys(this._extensions).join(); } /** * @type {Boolean} */ get isPaused() { return this._paused; } /** * @type {Function} */ /* istanbul ignore next */ get onclose() { return null; } /** * @type {Function} */ /* istanbul ignore next */ get onerror() { return null; } /** * @type {Function} */ /* istanbul ignore next */ get onopen() { return null; } /** * @type {Function} */ /* istanbul ignore next */ get onmessage() { return null; } /** * @type {String} */ get protocol() { return this._protocol; } /** * @type {Number} */ get readyState() { return this._readyState; } /** * @type {String} */ get url() { return this._url; } /** * Set up the socket and the internal resources. * * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Object} options Options object * @param {Boolean} [options.allowSynchronousEvents=false] Specifies whether * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted * multiple times in the same tick * @param {Function} [options.generateMask] The function used to generate the * masking key * @param {Number} [options.maxBufferedChunks=0] The maximum number of * buffered data chunks * @param {Number} [options.maxFragments=0] The maximum number of message * fragments * @param {Number} [options.maxPayload=0] The maximum allowed message size * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or * not to skip UTF-8 validation for text and close messages * @private */ setSocket(socket, head, options) { const receiver = new Receiver2({ allowSynchronousEvents: options.allowSynchronousEvents, binaryType: this.binaryType, extensions: this._extensions, isServer: this._isServer, maxBufferedChunks: options.maxBufferedChunks, maxFragments: options.maxFragments, maxPayload: options.maxPayload, skipUTF8Validation: options.skipUTF8Validation }); const sender = new Sender2(socket, this._extensions, options.generateMask); this._receiver = receiver; this._sender = sender; this._socket = socket; receiver[kWebSocket] = this; sender[kWebSocket] = this; socket[kWebSocket] = this; receiver.on("conclude", receiverOnConclude); receiver.on("drain", receiverOnDrain); receiver.on("error", receiverOnError); receiver.on("message", receiverOnMessage); receiver.on("ping", receiverOnPing); receiver.on("pong", receiverOnPong); sender.onerror = senderOnError; if (socket.setTimeout) socket.setTimeout(0); if (socket.setNoDelay) socket.setNoDelay(); if (head.length > 0) socket.unshift(head); socket.on("close", socketOnClose); socket.on("data", socketOnData); socket.on("end", socketOnEnd); socket.on("error", socketOnError); this._readyState = _WebSocket.OPEN; this.emit("open"); } /** * Emit the `'close'` event. * * @private */ emitClose() { if (!this._socket) { this._readyState = _WebSocket.CLOSED; this.emit("close", this._closeCode, this._closeMessage); return; } if (this._extensions[PerMessageDeflate2.extensionName]) { this._extensions[PerMessageDeflate2.extensionName].cleanup(); } this._receiver.removeAllListeners(); this._readyState = _WebSocket.CLOSED; this.emit("close", this._closeCode, this._closeMessage); } /** * Start a closing handshake. * * +----------+ +-----------+ +----------+ * - - -|ws.close()|-->|close frame|-->|ws.close()|- - - * | +----------+ +-----------+ +----------+ | * +----------+ +-----------+ | * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING * +----------+ +-----------+ | * | | | +---+ | * +------------------------+-->|fin| - - - - * | +---+ | +---+ * - - - - -|fin|<---------------------+ * +---+ * * @param {Number} [code] Status code explaining why the connection is closing * @param {(String|Buffer)} [data] The reason why the connection is * closing * @public */ close(code, data) { if (this.readyState === _WebSocket.CLOSED) return; if (this.readyState === _WebSocket.CONNECTING) { const msg = "WebSocket was closed before the connection was established"; abortHandshake(this, this._req, msg); return; } if (this.readyState === _WebSocket.CLOSING) { if (this._closeFrameSent && (this._closeFrameReceived || this._receiver._writableState.errorEmitted)) { this._socket.end(); } return; } this._readyState = _WebSocket.CLOSING; this._sender.close(code, data, !this._isServer, (err) => { if (err) return; this._closeFrameSent = true; if (this._closeFrameReceived || this._receiver._writableState.errorEmitted) { this._socket.end(); } }); setCloseTimer(this); } /** * Pause the socket. * * @public */ pause() { if (this.readyState === _WebSocket.CONNECTING || this.readyState === _WebSocket.CLOSED) { return; } this._paused = true; this._socket.pause(); } /** * Send a ping. * * @param {*} [data] The data to send * @param {Boolean} [mask] Indicates whether or not to mask `data` * @param {Function} [cb] Callback which is executed when the ping is sent * @public */ ping(data, mask, cb) { if (this.readyState === _WebSocket.CONNECTING) { throw new Error("WebSocket is not open: readyState 0 (CONNECTING)"); } if (typeof data === "function") { cb = data; data = mask = void 0; } else if (typeof mask === "function") { cb = mask; mask = void 0; } if (typeof data === "number") data = data.toString(); if (this.readyState !== _WebSocket.OPEN) { sendAfterClose(this, data, cb); return; } if (mask === void 0) mask = !this._isServer; this._sender.ping(data || EMPTY_BUFFER, mask, cb); } /** * Send a pong. * * @param {*} [data] The data to send * @param {Boolean} [mask] Indicates whether or not to mask `data` * @param {Function} [cb] Callback which is executed when the pong is sent * @public */ pong(data, mask, cb) { if (this.readyState === _WebSocket.CONNECTING) { throw new Error("WebSocket is not open: readyState 0 (CONNECTING)"); } if (typeof data === "function") { cb = data; data = mask = void 0; } else if (typeof mask === "function") { cb = mask; mask = void 0; } if (typeof data === "number") data = data.toString(); if (this.readyState !== _WebSocket.OPEN) { sendAfterClose(this, data, cb); return; } if (mask === void 0) mask = !this._isServer; this._sender.pong(data || EMPTY_BUFFER, mask, cb); } /** * Resume the socket. * * @public */ resume() { if (this.readyState === _WebSocket.CONNECTING || this.readyState === _WebSocket.CLOSED) { return; } this._paused = false; if (!this._receiver._writableState.needDrain) this._socket.resume(); } /** * Send a data message. * * @param {*} data The message to send * @param {Object} [options] Options object * @param {Boolean} [options.binary] Specifies whether `data` is binary or * text * @param {Boolean} [options.compress] Specifies whether or not to compress * `data` * @param {Boolean} [options.fin=true] Specifies whether the fragment is the * last one * @param {Boolean} [options.mask] Specifies whether or not to mask `data` * @param {Function} [cb] Callback which is executed when data is written out * @public */ send(data, options, cb) { if (this.readyState === _WebSocket.CONNECTING) { throw new Error("WebSocket is not open: readyState 0 (CONNECTING)"); } if (typeof options === "function") { cb = options; options = {}; } if (typeof data === "number") data = data.toString(); if (this.readyState !== _WebSocket.OPEN) { sendAfterClose(this, data, cb); return; } const opts = { binary: typeof data !== "string", mask: !this._isServer, compress: true, fin: true, ...options }; if (!this._extensions[PerMessageDeflate2.extensionName]) { opts.compress = false; } this._sender.send(data || EMPTY_BUFFER, opts, cb); } /** * Forcibly close the connection. * * @public */ terminate() { if (this.readyState === _WebSocket.CLOSED) return; if (this.readyState === _WebSocket.CONNECTING) { const msg = "WebSocket was closed before the connection was established"; abortHandshake(this, this._req, msg); return; } if (this._socket) { this._readyState = _WebSocket.CLOSING; this._socket.destroy(); } } }; Object.defineProperty(WebSocket2, "CONNECTING", { enumerable: true, value: readyStates.indexOf("CONNECTING") }); Object.defineProperty(WebSocket2.prototype, "CONNECTING", { enumerable: true, value: readyStates.indexOf("CONNECTING") }); Object.defineProperty(WebSocket2, "OPEN", { enumerable: true, value: readyStates.indexOf("OPEN") }); Object.defineProperty(WebSocket2.prototype, "OPEN", { enumerable: true, value: readyStates.indexOf("OPEN") }); Object.defineProperty(WebSocket2, "CLOSING", { enumerable: true, value: readyStates.indexOf("CLOSING") }); Object.defineProperty(WebSocket2.prototype, "CLOSING", { enumerable: true, value: readyStates.indexOf("CLOSING") }); Object.defineProperty(WebSocket2, "CLOSED", { enumerable: true, value: readyStates.indexOf("CLOSED") }); Object.defineProperty(WebSocket2.prototype, "CLOSED", { enumerable: true, value: readyStates.indexOf("CLOSED") }); [ "binaryType", "bufferedAmount", "extensions", "isPaused", "protocol", "readyState", "url" ].forEach((property) => { Object.defineProperty(WebSocket2.prototype, property, { enumerable: true }); }); ["open", "error", "close", "message"].forEach((method) => { Object.defineProperty(WebSocket2.prototype, `on${method}`, { enumerable: true, get() { for (const listener of this.listeners(method)) { if (listener[kForOnEventAttribute]) return listener[kListener]; } return null; }, set(handler) { for (const listener of this.listeners(method)) { if (listener[kForOnEventAttribute]) { this.removeListener(method, listener); break; } } if (typeof handler !== "function") return; this.addEventListener(method, handler, { [kForOnEventAttribute]: true }); } }); }); WebSocket2.prototype.addEventListener = addEventListener; WebSocket2.prototype.removeEventListener = removeEventListener; module2.exports = WebSocket2; function initAsClient(websocket, address, protocols, options) { const opts = { allowSynchronousEvents: true, autoPong: true, closeTimeout: CLOSE_TIMEOUT, protocolVersion: protocolVersions[1], maxBufferedChunks: 256 * 1024, maxFragments: 16 * 1024, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: true, followRedirects: false, maxRedirects: 10, ...options, socketPath: void 0, hostname: void 0, protocol: void 0, timeout: void 0, method: "GET", host: void 0, path: void 0, port: void 0 }; websocket._autoPong = opts.autoPong; websocket._closeTimeout = opts.closeTimeout; if (!protocolVersions.includes(opts.protocolVersion)) { throw new RangeError( `Unsupported protocol version: ${opts.protocolVersion} (supported versions: ${protocolVersions.join(", ")})` ); } let parsedUrl; if (address instanceof URL2) { parsedUrl = address; } else { try { parsedUrl = new URL2(address); } catch { throw new SyntaxError(`Invalid URL: ${address}`); } } if (parsedUrl.protocol === "http:") { parsedUrl.protocol = "ws:"; } else if (parsedUrl.protocol === "https:") { parsedUrl.protocol = "wss:"; } websocket._url = parsedUrl.href; const isSecure = parsedUrl.protocol === "wss:"; const isIpcUrl = parsedUrl.protocol === "ws+unix:"; let invalidUrlMessage; if (parsedUrl.protocol !== "ws:" && !isSecure && !isIpcUrl) { invalidUrlMessage = `The URL's protocol must be one of "ws:", "wss:", "http:", "https:", or "ws+unix:"`; } else if (isIpcUrl && !parsedUrl.pathname) { invalidUrlMessage = "The URL's pathname is empty"; } else if (parsedUrl.hash) { invalidUrlMessage = "The URL contains a fragment identifier"; } if (invalidUrlMessage) { const err = new SyntaxError(invalidUrlMessage); if (websocket._redirects === 0) { throw err; } else { emitErrorAndClose(websocket, err); return; } } const defaultPort = isSecure ? 443 : 80; const key = randomBytes(16).toString("base64"); const request = isSecure ? https.request : http2.request; const protocolSet = /* @__PURE__ */ new Set(); let perMessageDeflate; opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect); opts.defaultPort = opts.defaultPort || defaultPort; opts.port = parsedUrl.port || defaultPort; opts.host = parsedUrl.hostname.startsWith("[") ? parsedUrl.hostname.slice(1, -1) : parsedUrl.hostname; opts.headers = { ...opts.headers, "Sec-WebSocket-Version": opts.protocolVersion, "Sec-WebSocket-Key": key, Connection: "Upgrade", Upgrade: "websocket" }; opts.path = parsedUrl.pathname + parsedUrl.search; opts.timeout = opts.handshakeTimeout; if (opts.perMessageDeflate) { perMessageDeflate = new PerMessageDeflate2({ ...opts.perMessageDeflate, isServer: false, maxPayload: opts.maxPayload }); opts.headers["Sec-WebSocket-Extensions"] = format({ [PerMessageDeflate2.extensionName]: perMessageDeflate.offer() }); } if (protocols.length) { for (const protocol of protocols) { if (typeof protocol !== "string" || !subprotocolRegex.test(protocol) || protocolSet.has(protocol)) { throw new SyntaxError( "An invalid or duplicated subprotocol was specified" ); } protocolSet.add(protocol); } opts.headers["Sec-WebSocket-Protocol"] = protocols.join(","); } if (opts.origin) { if (opts.protocolVersion < 13) { opts.headers["Sec-WebSocket-Origin"] = opts.origin; } else { opts.headers.Origin = opts.origin; } } if (parsedUrl.username || parsedUrl.password) { opts.auth = `${parsedUrl.username}:${parsedUrl.password}`; } if (isIpcUrl) { const parts = opts.path.split(":"); opts.socketPath = parts[0]; opts.path = parts[1]; } let req; if (opts.followRedirects) { if (websocket._redirects === 0) { websocket._originalIpc = isIpcUrl; websocket._originalSecure = isSecure; websocket._originalHostOrSocketPath = isIpcUrl ? opts.socketPath : parsedUrl.host; const headers = options && options.headers; options = { ...options, headers: {} }; if (headers) { for (const [key2, value] of Object.entries(headers)) { options.headers[key2.toLowerCase()] = value; } } } else if (websocket.listenerCount("redirect") === 0) { const isSameHost = isIpcUrl ? websocket._originalIpc ? opts.socketPath === websocket._originalHostOrSocketPath : false : websocket._originalIpc ? false : parsedUrl.host === websocket._originalHostOrSocketPath; if (!isSameHost || websocket._originalSecure && !isSecure) { delete opts.headers.authorization; delete opts.headers.cookie; if (!isSameHost) delete opts.headers.host; opts.auth = void 0; } } if (opts.auth && !options.headers.authorization) { options.headers.authorization = "Basic " + Buffer.from(opts.auth).toString("base64"); } req = websocket._req = request(opts); if (websocket._redirects) { websocket.emit("redirect", websocket.url, req); } } else { req = websocket._req = request(opts); } if (opts.timeout) { req.on("timeout", () => { abortHandshake(websocket, req, "Opening handshake has timed out"); }); } req.on("error", (err) => { if (req === null || req[kAborted]) return; req = websocket._req = null; emitErrorAndClose(websocket, err); }); req.on("response", (res) => { const location = res.headers.location; const statusCode = res.statusCode; if (location && opts.followRedirects && statusCode >= 300 && statusCode < 400) { if (++websocket._redirects > opts.maxRedirects) { abortHandshake(websocket, req, "Maximum redirects exceeded"); return; } req.abort(); let addr; try { addr = new URL2(location, address); } catch (e) { const err = new SyntaxError(`Invalid URL: ${location}`); emitErrorAndClose(websocket, err); return; } initAsClient(websocket, addr, protocols, options); } else if (!websocket.emit("unexpected-response", req, res)) { abortHandshake( websocket, req, `Unexpected server response: ${res.statusCode}` ); } }); req.on("upgrade", (res, socket, head) => { websocket.emit("upgrade", res); if (websocket.readyState !== WebSocket2.CONNECTING) return; req = websocket._req = null; const upgrade = res.headers.upgrade; if (upgrade === void 0 || upgrade.toLowerCase() !== "websocket") { abortHandshake(websocket, socket, "Invalid Upgrade header"); return; } const digest = createHash("sha1").update(key + GUID).digest("base64"); if (res.headers["sec-websocket-accept"] !== digest) { abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header"); return; } const serverProt = res.headers["sec-websocket-protocol"]; let protError; if (serverProt !== void 0) { if (!protocolSet.size) { protError = "Server sent a subprotocol but none was requested"; } else if (!protocolSet.has(serverProt)) { protError = "Server sent an invalid subprotocol"; } } else if (protocolSet.size) { protError = "Server sent no subprotocol"; } if (protError) { abortHandshake(websocket, socket, protError); return; } if (serverProt) websocket._protocol = serverProt; const secWebSocketExtensions = res.headers["sec-websocket-extensions"]; if (secWebSocketExtensions !== void 0) { if (!perMessageDeflate) { const message = "Server sent a Sec-WebSocket-Extensions header but no extension was requested"; abortHandshake(websocket, socket, message); return; } let extensions; try { extensions = parse(secWebSocketExtensions); } catch (err) { const message = "Invalid Sec-WebSocket-Extensions header"; abortHandshake(websocket, socket, message); return; } const extensionNames = Object.keys(extensions); if (extensionNames.length !== 1 || extensionNames[0] !== PerMessageDeflate2.extensionName) { const message = "Server indicated an extension that was not requested"; abortHandshake(websocket, socket, message); return; } try { perMessageDeflate.accept(extensions[PerMessageDeflate2.extensionName]); } catch (err) { const message = "Invalid Sec-WebSocket-Extensions header"; abortHandshake(websocket, socket, message); return; } websocket._extensions[PerMessageDeflate2.extensionName] = perMessageDeflate; } websocket.setSocket(socket, head, { allowSynchronousEvents: opts.allowSynchronousEvents, generateMask: opts.generateMask, maxBufferedChunks: opts.maxBufferedChunks, maxFragments: opts.maxFragments, maxPayload: opts.maxPayload, skipUTF8Validation: opts.skipUTF8Validation }); }); if (opts.finishRequest) { opts.finishRequest(req, websocket); } else { req.end(); } } function emitErrorAndClose(websocket, err) { websocket._readyState = WebSocket2.CLOSING; websocket._errorEmitted = true; websocket.emit("error", err); websocket.emitClose(); } function netConnect(options) { options.path = options.socketPath; return net7.connect(options); } function tlsConnect(options) { options.path = void 0; if (!options.servername && options.servername !== "") { options.servername = net7.isIP(options.host) ? "" : options.host; } return tls.connect(options); } function abortHandshake(websocket, stream, message) { websocket._readyState = WebSocket2.CLOSING; const err = new Error(message); Error.captureStackTrace(err, abortHandshake); if (stream.setHeader) { stream[kAborted] = true; stream.abort(); if (stream.socket && !stream.socket.destroyed) { stream.socket.destroy(); } process.nextTick(emitErrorAndClose, websocket, err); } else { stream.destroy(err); stream.once("error", websocket.emit.bind(websocket, "error")); stream.once("close", websocket.emitClose.bind(websocket)); } } function sendAfterClose(websocket, data, cb) { if (data) { const length = isBlob(data) ? data.size : toBuffer(data).length; if (websocket._socket) websocket._sender._bufferedBytes += length; else websocket._bufferedAmount += length; } if (cb) { const err = new Error( `WebSocket is not open: readyState ${websocket.readyState} (${readyStates[websocket.readyState]})` ); process.nextTick(cb, err); } } function receiverOnConclude(code, reason) { const websocket = this[kWebSocket]; websocket._closeFrameReceived = true; websocket._closeMessage = reason; websocket._closeCode = code; if (websocket._socket[kWebSocket] === void 0) return; websocket._socket.removeListener("data", socketOnData); process.nextTick(resume, websocket._socket); if (code === 1005) websocket.close(); else websocket.close(code, reason); } function receiverOnDrain() { const websocket = this[kWebSocket]; if (!websocket.isPaused) websocket._socket.resume(); } function receiverOnError(err) { const websocket = this[kWebSocket]; if (websocket._socket[kWebSocket] !== void 0) { websocket._socket.removeListener("data", socketOnData); process.nextTick(resume, websocket._socket); websocket.close(err[kStatusCode]); } if (!websocket._errorEmitted) { websocket._errorEmitted = true; websocket.emit("error", err); } } function receiverOnFinish() { this[kWebSocket].emitClose(); } function receiverOnMessage(data, isBinary) { this[kWebSocket].emit("message", data, isBinary); } function receiverOnPing(data) { const websocket = this[kWebSocket]; if (websocket._autoPong) websocket.pong(data, !this._isServer, NOOP); websocket.emit("ping", data); } function receiverOnPong(data) { this[kWebSocket].emit("pong", data); } function resume(stream) { stream.resume(); } function senderOnError(err) { const websocket = this[kWebSocket]; if (websocket.readyState === WebSocket2.CLOSED) return; if (websocket.readyState === WebSocket2.OPEN) { websocket._readyState = WebSocket2.CLOSING; setCloseTimer(websocket); } this._socket.end(); if (!websocket._errorEmitted) { websocket._errorEmitted = true; websocket.emit("error", err); } } function setCloseTimer(websocket) { websocket._closeTimer = setTimeout( websocket._socket.destroy.bind(websocket._socket), websocket._closeTimeout ); } function socketOnClose() { const websocket = this[kWebSocket]; this.removeListener("close", socketOnClose); this.removeListener("data", socketOnData); this.removeListener("end", socketOnEnd); websocket._readyState = WebSocket2.CLOSING; if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) { const chunk = this.read(this._readableState.length); websocket._receiver.write(chunk); } websocket._receiver.end(); this[kWebSocket] = void 0; clearTimeout(websocket._closeTimer); if (websocket._receiver._writableState.finished || websocket._receiver._writableState.errorEmitted) { websocket.emitClose(); } else { websocket._receiver.on("error", receiverOnFinish); websocket._receiver.on("finish", receiverOnFinish); } } function socketOnData(chunk) { if (!this[kWebSocket]._receiver.write(chunk)) { this.pause(); } } function socketOnEnd() { const websocket = this[kWebSocket]; websocket._readyState = WebSocket2.CLOSING; websocket._receiver.end(); this.end(); } function socketOnError() { const websocket = this[kWebSocket]; this.removeListener("error", socketOnError); this.on("error", NOOP); if (websocket) { websocket._readyState = WebSocket2.CLOSING; this.destroy(); } } } }); // node_modules/ws/lib/stream.js var require_stream = __commonJS({ "node_modules/ws/lib/stream.js"(exports2, module2) { "use strict"; var WebSocket2 = require_websocket(); var { Duplex: Duplex2 } = require("stream"); function emitClose(stream) { stream.emit("close"); } function duplexOnEnd() { if (!this.destroyed && this._writableState.finished) { this.destroy(); } } function duplexOnError(err) { this.removeListener("error", duplexOnError); this.destroy(); if (this.listenerCount("error") === 0) { this.emit("error", err); } } function createWebSocketStream2(ws, options) { let terminateOnDestroy = true; const duplex = new Duplex2({ ...options, autoDestroy: false, emitClose: false, objectMode: false, writableObjectMode: false }); ws.on("message", function message(msg, isBinary) { const data = !isBinary && duplex._readableState.objectMode ? msg.toString() : msg; if (!duplex.push(data)) ws.pause(); }); ws.once("error", function error(err) { if (duplex.destroyed) return; terminateOnDestroy = false; duplex.destroy(err); }); ws.once("close", function close() { if (duplex.destroyed) return; duplex.push(null); }); duplex._destroy = function(err, callback) { if (ws.readyState === ws.CLOSED) { callback(err); process.nextTick(emitClose, duplex); return; } let called = false; ws.once("error", function error(err2) { called = true; callback(err2); }); ws.once("close", function close() { if (!called) callback(err); process.nextTick(emitClose, duplex); }); if (terminateOnDestroy) ws.terminate(); }; duplex._final = function(callback) { if (ws.readyState === ws.CONNECTING) { ws.once("open", function open2() { duplex._final(callback); }); return; } if (ws._socket === null) return; if (ws._socket._writableState.finished) { callback(); if (duplex._readableState.endEmitted) duplex.destroy(); } else { ws._socket.once("finish", function finish() { callback(); }); ws.close(); } }; duplex._read = function() { if (ws.isPaused) ws.resume(); }; duplex._write = function(chunk, encoding, callback) { if (ws.readyState === ws.CONNECTING) { ws.once("open", function open2() { duplex._write(chunk, encoding, callback); }); return; } ws.send(chunk, callback); }; duplex.on("end", duplexOnEnd); duplex.on("error", duplexOnError); return duplex; } module2.exports = createWebSocketStream2; } }); // node_modules/ws/lib/subprotocol.js var require_subprotocol = __commonJS({ "node_modules/ws/lib/subprotocol.js"(exports2, module2) { "use strict"; var { tokenChars } = require_validation(); function parse(header) { const protocols = /* @__PURE__ */ new Set(); let start = -1; let end = -1; let i = 0; for (i; i < header.length; i++) { const code = header.charCodeAt(i); if (end === -1 && tokenChars[code] === 1) { if (start === -1) start = i; } else if (i !== 0 && (code === 32 || code === 9)) { if (end === -1 && start !== -1) end = i; } else if (code === 44) { if (start === -1) { throw new SyntaxError(`Unexpected character at index ${i}`); } if (end === -1) end = i; const protocol2 = header.slice(start, end); if (protocols.has(protocol2)) { throw new SyntaxError(`The "${protocol2}" subprotocol is duplicated`); } protocols.add(protocol2); start = end = -1; } else { throw new SyntaxError(`Unexpected character at index ${i}`); } } if (start === -1 || end !== -1) { throw new SyntaxError("Unexpected end of input"); } const protocol = header.slice(start, i); if (protocols.has(protocol)) { throw new SyntaxError(`The "${protocol}" subprotocol is duplicated`); } protocols.add(protocol); return protocols; } module2.exports = { parse }; } }); // node_modules/ws/lib/websocket-server.js var require_websocket_server = __commonJS({ "node_modules/ws/lib/websocket-server.js"(exports2, module2) { "use strict"; var EventEmitter3 = require("events"); var http2 = require("http"); var { Duplex: Duplex2 } = require("stream"); var { createHash } = require("crypto"); var extension2 = require_extension(); var PerMessageDeflate2 = require_permessage_deflate(); var subprotocol2 = require_subprotocol(); var WebSocket2 = require_websocket(); var { CLOSE_TIMEOUT, GUID, kWebSocket } = require_constants(); var keyRegex = /^[+/0-9A-Za-z]{22}==$/; var RUNNING = 0; var CLOSING = 1; var CLOSED = 2; var WebSocketServer2 = class extends EventEmitter3 { /** * Create a `WebSocketServer` instance. * * @param {Object} options Configuration options * @param {Boolean} [options.allowSynchronousEvents=true] Specifies whether * any of the `'message'`, `'ping'`, and `'pong'` events can be emitted * multiple times in the same tick * @param {Boolean} [options.autoPong=true] Specifies whether or not to * automatically send a pong in response to a ping * @param {Number} [options.backlog=511] The maximum length of the queue of * pending connections * @param {Boolean} [options.clientTracking=true] Specifies whether or not to * track clients * @param {Number} [options.closeTimeout=30000] Duration in milliseconds to * wait for the closing handshake to finish after `websocket.close()` is * called * @param {Function} [options.handleProtocols] A hook to handle protocols * @param {String} [options.host] The hostname where to bind the server * @param {Number} [options.maxBufferedChunks=262144] The maximum number of * buffered data chunks * @param {Number} [options.maxFragments=16384] The maximum number of message * fragments * @param {Number} [options.maxPayload=104857600] The maximum allowed message * size * @param {Boolean} [options.noServer=false] Enable no server mode * @param {String} [options.path] Accept only connections matching this path * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable * permessage-deflate * @param {Number} [options.port] The port where to bind the server * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S * server to use * @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or * not to skip UTF-8 validation for text and close messages * @param {Function} [options.verifyClient] A hook to reject connections * @param {Function} [options.WebSocket=WebSocket] Specifies the `WebSocket` * class to use. It must be the `WebSocket` class or class that extends it * @param {Function} [callback] A listener for the `listening` event */ constructor(options, callback) { super(); options = { allowSynchronousEvents: true, autoPong: true, maxBufferedChunks: 256 * 1024, maxFragments: 16 * 1024, maxPayload: 100 * 1024 * 1024, skipUTF8Validation: false, perMessageDeflate: false, handleProtocols: null, clientTracking: true, closeTimeout: CLOSE_TIMEOUT, verifyClient: null, noServer: false, backlog: null, // use default (511 as implemented in net.js) server: null, host: null, path: null, port: null, WebSocket: WebSocket2, ...options }; if (options.port == null && !options.server && !options.noServer || options.port != null && (options.server || options.noServer) || options.server && options.noServer) { throw new TypeError( 'One and only one of the "port", "server", or "noServer" options must be specified' ); } if (options.port != null) { this._server = http2.createServer((req, res) => { const body = http2.STATUS_CODES[426]; res.writeHead(426, { "Content-Length": body.length, "Content-Type": "text/plain" }); res.end(body); }); this._server.listen( options.port, options.host, options.backlog, callback ); } else if (options.server) { this._server = options.server; } if (this._server) { const emitConnection = this.emit.bind(this, "connection"); this._removeListeners = addListeners(this._server, { listening: this.emit.bind(this, "listening"), error: this.emit.bind(this, "error"), upgrade: (req, socket, head) => { this.handleUpgrade(req, socket, head, emitConnection); } }); } if (options.perMessageDeflate === true) options.perMessageDeflate = {}; if (options.clientTracking) { this.clients = /* @__PURE__ */ new Set(); this._shouldEmitClose = false; } this.options = options; this._state = RUNNING; } /** * Returns the bound address, the address family name, and port of the server * as reported by the operating system if listening on an IP socket. * If the server is listening on a pipe or UNIX domain socket, the name is * returned as a string. * * @return {(Object|String|null)} The address of the server * @public */ address() { if (this.options.noServer) { throw new Error('The server is operating in "noServer" mode'); } if (!this._server) return null; return this._server.address(); } /** * Stop the server from accepting new connections and emit the `'close'` event * when all existing connections are closed. * * @param {Function} [cb] A one-time listener for the `'close'` event * @public */ close(cb) { if (this._state === CLOSED) { if (cb) { this.once("close", () => { cb(new Error("The server is not running")); }); } process.nextTick(emitClose, this); return; } if (cb) this.once("close", cb); if (this._state === CLOSING) return; this._state = CLOSING; if (this.options.noServer || this.options.server) { if (this._server) { this._removeListeners(); this._removeListeners = this._server = null; } if (this.clients) { if (!this.clients.size) { process.nextTick(emitClose, this); } else { this._shouldEmitClose = true; } } else { process.nextTick(emitClose, this); } } else { const server = this._server; this._removeListeners(); this._removeListeners = this._server = null; server.close(() => { emitClose(this); }); } } /** * See if a given request should be handled by this server instance. * * @param {http.IncomingMessage} req Request object to inspect * @return {Boolean} `true` if the request is valid, else `false` * @public */ shouldHandle(req) { if (this.options.path) { const index = req.url.indexOf("?"); const pathname = index !== -1 ? req.url.slice(0, index) : req.url; if (pathname !== this.options.path) return false; } return true; } /** * Handle a HTTP Upgrade request. * * @param {http.IncomingMessage} req The request object * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @public */ handleUpgrade(req, socket, head, cb) { socket.on("error", socketOnError); const key = req.headers["sec-websocket-key"]; const upgrade = req.headers.upgrade; const version = +req.headers["sec-websocket-version"]; if (req.method !== "GET") { const message = "Invalid HTTP method"; abortHandshakeOrEmitwsClientError(this, req, socket, 405, message); return; } if (upgrade === void 0 || upgrade.toLowerCase() !== "websocket") { const message = "Invalid Upgrade header"; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } if (key === void 0 || !keyRegex.test(key)) { const message = "Missing or invalid Sec-WebSocket-Key header"; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } if (version !== 13 && version !== 8) { const message = "Missing or invalid Sec-WebSocket-Version header"; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message, { "Sec-WebSocket-Version": "13, 8" }); return; } if (!this.shouldHandle(req)) { abortHandshake(socket, 400); return; } const secWebSocketProtocol = req.headers["sec-websocket-protocol"]; let protocols = /* @__PURE__ */ new Set(); if (secWebSocketProtocol !== void 0) { try { protocols = subprotocol2.parse(secWebSocketProtocol); } catch (err) { const message = "Invalid Sec-WebSocket-Protocol header"; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } } const secWebSocketExtensions = req.headers["sec-websocket-extensions"]; const extensions = {}; if (this.options.perMessageDeflate && secWebSocketExtensions !== void 0) { const perMessageDeflate = new PerMessageDeflate2({ ...this.options.perMessageDeflate, isServer: true, maxPayload: this.options.maxPayload }); try { const offers = extension2.parse(secWebSocketExtensions); if (offers[PerMessageDeflate2.extensionName]) { perMessageDeflate.accept(offers[PerMessageDeflate2.extensionName]); extensions[PerMessageDeflate2.extensionName] = perMessageDeflate; } } catch (err) { const message = "Invalid or unacceptable Sec-WebSocket-Extensions header"; abortHandshakeOrEmitwsClientError(this, req, socket, 400, message); return; } } if (this.options.verifyClient) { const info = { origin: req.headers[`${version === 8 ? "sec-websocket-origin" : "origin"}`], secure: !!(req.socket.authorized || req.socket.encrypted), req }; if (this.options.verifyClient.length === 2) { this.options.verifyClient(info, (verified, code, message, headers) => { if (!verified) { return abortHandshake(socket, code || 401, message, headers); } this.completeUpgrade( extensions, key, protocols, req, socket, head, cb ); }); return; } if (!this.options.verifyClient(info)) return abortHandshake(socket, 401); } this.completeUpgrade(extensions, key, protocols, req, socket, head, cb); } /** * Upgrade the connection to WebSocket. * * @param {Object} extensions The accepted extensions * @param {String} key The value of the `Sec-WebSocket-Key` header * @param {Set} protocols The subprotocols * @param {http.IncomingMessage} req The request object * @param {Duplex} socket The network socket between the server and client * @param {Buffer} head The first packet of the upgraded stream * @param {Function} cb Callback * @throws {Error} If called more than once with the same socket * @private */ completeUpgrade(extensions, key, protocols, req, socket, head, cb) { if (!socket.readable || !socket.writable) return socket.destroy(); if (socket[kWebSocket]) { throw new Error( "server.handleUpgrade() was called more than once with the same socket, possibly due to a misconfiguration" ); } if (this._state > RUNNING) return abortHandshake(socket, 503); const digest = createHash("sha1").update(key + GUID).digest("base64"); const headers = [ "HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${digest}` ]; const ws = new this.options.WebSocket(null, void 0, this.options); if (protocols.size) { const protocol = this.options.handleProtocols ? this.options.handleProtocols(protocols, req) : protocols.values().next().value; if (protocol) { headers.push(`Sec-WebSocket-Protocol: ${protocol}`); ws._protocol = protocol; } } if (extensions[PerMessageDeflate2.extensionName]) { const params = extensions[PerMessageDeflate2.extensionName].params; const value = extension2.format({ [PerMessageDeflate2.extensionName]: [params] }); headers.push(`Sec-WebSocket-Extensions: ${value}`); ws._extensions = extensions; } this.emit("headers", headers, req); socket.write(headers.concat("\r\n").join("\r\n")); socket.removeListener("error", socketOnError); ws.setSocket(socket, head, { allowSynchronousEvents: this.options.allowSynchronousEvents, maxBufferedChunks: this.options.maxBufferedChunks, maxFragments: this.options.maxFragments, maxPayload: this.options.maxPayload, skipUTF8Validation: this.options.skipUTF8Validation }); if (this.clients) { this.clients.add(ws); ws.on("close", () => { this.clients.delete(ws); if (this._shouldEmitClose && !this.clients.size) { process.nextTick(emitClose, this); } }); } cb(ws, req); } }; module2.exports = WebSocketServer2; function addListeners(server, map) { for (const event of Object.keys(map)) server.on(event, map[event]); return function removeListeners() { for (const event of Object.keys(map)) { server.removeListener(event, map[event]); } }; } function emitClose(server) { server._state = CLOSED; server.emit("close"); } function socketOnError() { this.destroy(); } function abortHandshake(socket, code, message, headers) { message = message || http2.STATUS_CODES[code]; headers = { Connection: "close", "Content-Type": "text/html", "Content-Length": Buffer.byteLength(message), ...headers }; socket.once("finish", socket.destroy); socket.end( `HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message ); } function abortHandshakeOrEmitwsClientError(server, req, socket, code, message, headers) { if (server.listenerCount("wsClientError")) { const err = new Error(message); Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError); server.emit("wsClientError", err, socket, req); } else { abortHandshake(socket, code, message, headers); } } } }); // packages/protocol/src/frame.js var HEADER_SIZE = 7; var MAX_PAYLOAD = 16 * 1024; var T = { CONNECT: 1, // payload: addr + 可选 region,见 addr.js CONNECT_OK: 2, // payload: 空 CONNECT_ERR: 3, // payload: [1B code][utf8 reason] DATA: 4, // payload: 裸数据 FIN: 5, // payload: 空。半关闭:本端不再发数据,但仍可收 RST: 6, // payload: [1B code]。强制关闭 PING: 7, // payload: [8B 发送方单调时钟,原样回显] PONG: 8, // payload: 同上 WINDOW_UPDATE: 9, // payload: [4B 增量 BE] // UDP。一条「关联」对应 SOCKS5 的一次 UDP ASSOCIATE:不是点对点连接, // 同一条关联上可以往任意多个目标发包,所以每个数据报自带目的地址。 ASSOCIATE: 10, // 建立 UDP 关联。payload 同 CONNECT(加密握手块) DATAGRAM: 11 // 一个数据报。payload: 加密后的 [addr][data] }; var TYPE_NAME = Object.fromEntries( Object.entries(T).map(([name, code]) => [code, name]) ); var E = { GENERAL: 1, NOT_ALLOWED: 2, // 被出口策略拦下(内网地址 / 禁用端口) NET_UNREACH: 3, HOST_UNREACH: 4, // 含 DNS 解析失败 REFUSED: 5, TIMEOUT: 6, NO_EXIT: 7, // broker 找不到可用出口节点 EXIT_GONE: 8, // 出口节点中途掉线 PROTOCOL: 9, // 消费端指定的出口节点已经不在了。目标地址是用那个节点的公钥加密的,broker // 改派也没用——只能让消费端重新要一次租约再试。 LEASE_STALE: 10 }; var E_NAME = Object.fromEntries( Object.entries(E).map(([name, code]) => [code, name]) ); var EMPTY = Buffer.alloc(0); function encodeFrame(type, streamId, payload = EMPTY) { if (payload.length > 65535) { throw new RangeError(`payload ${payload.length} \u8D85\u8FC7\u5355\u5E27\u4E0A\u9650 65535`); } const buf = Buffer.allocUnsafe(HEADER_SIZE + payload.length); buf.writeUInt8(type, 0); buf.writeUInt32BE(streamId, 1); buf.writeUInt16BE(payload.length, 5); if (payload.length > 0) payload.copy(buf, HEADER_SIZE); return buf; } function decodeFrame(buf) { if (buf.length < HEADER_SIZE) { throw new Error(`\u5E27\u8FC7\u77ED: ${buf.length} < ${HEADER_SIZE}`); } const len = buf.readUInt16BE(5); if (buf.length !== HEADER_SIZE + len) { throw new Error(`\u5E27\u957F\u4E0D\u7B26: \u58F0\u660E ${len}\uFF0C\u5B9E\u9645 ${buf.length - HEADER_SIZE}`); } return { type: buf.readUInt8(0), streamId: buf.readUInt32BE(1), payload: len > 0 ? buf.subarray(HEADER_SIZE) : EMPTY }; } function encodeError(code, reason = "") { const msg = Buffer.from(String(reason).slice(0, 200), "utf8"); const buf = Buffer.allocUnsafe(1 + msg.length); buf.writeUInt8(code, 0); msg.copy(buf, 1); return buf; } function decodeError(payload) { if (payload.length === 0) return { code: E.GENERAL, reason: "" }; return { code: payload.readUInt8(0), reason: payload.subarray(1).toString("utf8") }; } function encodeUint32(n) { const buf = Buffer.allocUnsafe(4); buf.writeUInt32BE(n >>> 0, 0); return buf; } // packages/protocol/src/addr.js var import_node_net = __toESM(require("node:net"), 1); var ATYP = { IPV4: 1, DOMAIN: 3, IPV6: 4 }; function ipv4ToBytes(str) { const parts = str.split("."); if (parts.length !== 4) throw new Error(`bad IPv4: ${str}`); const buf = Buffer.allocUnsafe(4); for (let i = 0; i < 4; i++) { const n = Number(parts[i]); if (!Number.isInteger(n) || n < 0 || n > 255) throw new Error(`bad IPv4: ${str}`); buf[i] = n; } return buf; } function ipv6ToBytes(input) { let s = input; const pct = s.indexOf("%"); if (pct !== -1) s = s.slice(0, pct); if (s.includes(".")) { const cut = s.lastIndexOf(":"); const v4 = ipv4ToBytes(s.slice(cut + 1)); const hi = (v4[0] << 8 | v4[1]).toString(16); const lo = (v4[2] << 8 | v4[3]).toString(16); s = `${s.slice(0, cut + 1)}${hi}:${lo}`; } const dbl = s.indexOf("::"); let head; let tail; if (dbl === -1) { head = s.split(":"); tail = []; } else { const left = s.slice(0, dbl); const right = s.slice(dbl + 2); head = left ? left.split(":") : []; tail = right ? right.split(":") : []; } const fill = 8 - head.length - tail.length; if (fill < 0 || dbl === -1 && fill !== 0) throw new Error(`bad IPv6: ${input}`); const groups = [...head, ...Array(fill).fill("0"), ...tail]; const buf = Buffer.allocUnsafe(16); for (let i = 0; i < 8; i++) { const n = parseInt(groups[i], 16); if (!Number.isInteger(n) || n < 0 || n > 65535) throw new Error(`bad IPv6: ${input}`); buf.writeUInt16BE(n, i * 2); } return buf; } function bytesToIpv6(buf) { const g = []; for (let i = 0; i < 16; i += 2) g.push(buf.readUInt16BE(i)); let bestStart = -1; let bestLen = 0; for (let i = 0; i < 8; i++) { if (g[i] !== 0) continue; let j = i; while (j < 8 && g[j] === 0) j++; if (j - i > bestLen) { bestLen = j - i; bestStart = i; } i = j; } const hex = g.map((n) => n.toString(16)); if (bestLen < 2) return hex.join(":"); return `${hex.slice(0, bestStart).join(":")}::${hex.slice(bestStart + bestLen).join(":")}`; } function encodeAddr(host, port) { let atyp; let addr; const kind = import_node_net.default.isIP(host); if (kind === 4) { atyp = ATYP.IPV4; addr = ipv4ToBytes(host); } else if (kind === 6) { atyp = ATYP.IPV6; addr = ipv6ToBytes(host); } else { const name = Buffer.from(host, "utf8"); if (name.length === 0 || name.length > 255) throw new Error(`bad domain: ${host}`); atyp = ATYP.DOMAIN; addr = Buffer.concat([Buffer.from([name.length]), name]); } if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`bad port: ${port}`); } const buf = Buffer.allocUnsafe(1 + addr.length + 2); buf.writeUInt8(atyp, 0); addr.copy(buf, 1); buf.writeUInt16BE(port, 1 + addr.length); return buf; } function decodeAddr(buf, offset = 0) { if (buf.length < offset + 1) throw new Error("\u5730\u5740\u622A\u65AD"); const atyp = buf.readUInt8(offset); let host; let cursor = offset + 1; if (atyp === ATYP.IPV4) { if (buf.length < cursor + 4) throw new Error("IPv4 \u5730\u5740\u622A\u65AD"); host = `${buf[cursor]}.${buf[cursor + 1]}.${buf[cursor + 2]}.${buf[cursor + 3]}`; cursor += 4; } else if (atyp === ATYP.IPV6) { if (buf.length < cursor + 16) throw new Error("IPv6 \u5730\u5740\u622A\u65AD"); host = bytesToIpv6(buf.subarray(cursor, cursor + 16)); cursor += 16; } else if (atyp === ATYP.DOMAIN) { const len = buf.readUInt8(cursor); cursor += 1; if (buf.length < cursor + len) throw new Error("\u57DF\u540D\u622A\u65AD"); host = buf.subarray(cursor, cursor + len).toString("utf8"); cursor += len; } else { throw new Error(`\u672A\u77E5 atyp: ${atyp}`); } if (buf.length < cursor + 2) throw new Error("\u7AEF\u53E3\u622A\u65AD"); const port = buf.readUInt16BE(cursor); cursor += 2; return { host, port, atyp, size: cursor - offset }; } var CONNECT_ENCRYPTED = 0; function lenPrefixed(str, max) { const b = Buffer.from(String(str ?? "").slice(0, max), "utf8"); return Buffer.concat([Buffer.from([b.length]), b]); } function encodeConnect({ host, port, region = "", nodeId = "", blob = null }) { if (blob) { return Buffer.concat([ Buffer.from([CONNECT_ENCRYPTED]), lenPrefixed(region, 64), lenPrefixed(nodeId, 200), blob ]); } const addr = encodeAddr(host, port); const r = Buffer.from(String(region ?? "").slice(0, 64), "utf8"); if (r.length === 0) return addr; return Buffer.concat([addr, Buffer.from([r.length]), r]); } function decodeConnect(payload) { if (payload.length === 0) throw new Error("CONNECT \u8F7D\u8377\u4E3A\u7A7A"); if (payload.readUInt8(0) === CONNECT_ENCRYPTED) { let cursor = 1; const readStr = () => { if (payload.length < cursor + 1) throw new Error("\u52A0\u5BC6 CONNECT \u8F7D\u8377\u622A\u65AD"); const len = payload.readUInt8(cursor); cursor += 1; if (payload.length < cursor + len) throw new Error("\u52A0\u5BC6 CONNECT \u8F7D\u8377\u622A\u65AD"); const s = payload.subarray(cursor, cursor + len).toString("utf8"); cursor += len; return s; }; const region2 = readStr(); const nodeId = readStr(); return { host: null, port: null, region: region2, nodeId, blob: payload.subarray(cursor) }; } const { host, port, size } = decodeAddr(payload, 0); let region = ""; if (payload.length > size) { const len = payload.readUInt8(size); if (payload.length >= size + 1 + len) { region = payload.subarray(size + 1, size + 1 + len).toString("utf8"); } } return { host, port, region, nodeId: "", blob: null }; } function encodeDatagram(host, port, data) { return Buffer.concat([encodeAddr(host, port), data]); } function decodeDatagram(buf) { const { host, port, size } = decodeAddr(buf, 0); return { host, port, data: buf.subarray(size) }; } function encodeSocks5Udp(host, port, data) { return Buffer.concat([Buffer.from([0, 0, 0]), encodeAddr(host, port), data]); } function decodeSocks5Udp(buf) { if (buf.length < 4) throw new Error("SOCKS5 UDP \u5305\u8FC7\u77ED"); if (buf.readUInt8(2) !== 0) throw new Error("\u4E0D\u652F\u6301\u5206\u7247\u7684 SOCKS5 UDP \u5305"); const { host, port, size } = decodeAddr(buf, 3); return { host, port, data: buf.subarray(3 + size) }; } function encodeConnectOk(meta = "", blob = null) { const b = blob ?? Buffer.alloc(0); const m = Buffer.from(String(meta).slice(0, 200), "utf8"); const head = Buffer.allocUnsafe(2); head.writeUInt16BE(b.length, 0); return Buffer.concat([head, b, m]); } function decodeConnectOk(payload) { if (!payload || payload.length < 2) return { blob: null, meta: "" }; const len = payload.readUInt16BE(0); if (payload.length < 2 + len) return { blob: null, meta: "" }; return { blob: len > 0 ? payload.subarray(2, 2 + len) : null, meta: payload.subarray(2 + len).toString("utf8") }; } // packages/protocol/src/stream.js var import_node_stream = require("node:stream"); var INITIAL_WINDOW = 128 * 1024; var Stream = class extends import_node_stream.Duplex { constructor(session, id, { window = INITIAL_WINDOW, incoming = false, datagram = false } = {}) { super({ allowHalfOpen: true, highWaterMark: window }); this.session = session; this.id = id; this.incoming = incoming; this.datagram = datagram; this.datagramsIn = 0; this.datagramsOut = 0; this.datagramsDropped = 0; this.connected = false; this.bytesIn = 0; this.bytesOut = 0; this.remoteAddr = null; this.exitInfo = null; this.handshakeBlob = null; this._window = window; this._sendWindow = window; this._unacked = 0; this._pending = null; this._remoteEnded = false; this._finSent = false; this._closeSent = false; this._connectTimer = null; } // ---- 发起方:等 CONNECT_OK ---- _armConnectTimeout(ms) { if (!ms) return; this._connectTimer = setTimeout(() => { this._connectTimer = null; this._fail(E.TIMEOUT, `\u8FDE\u63A5 ${this.remoteAddr ?? "\u76EE\u6807"} \u8D85\u65F6`); }, ms); this._connectTimer.unref?.(); } _onConnectOk(payload) { if (this.connected) return; clearTimeout(this._connectTimer); this._connectTimer = null; this.connected = true; const { blob, meta } = decodeConnectOk(payload); this.handshakeBlob = blob; if (meta) this.exitInfo = meta; this.emit("connect"); this._pump(); } _onConnectErr(code, reason) { clearTimeout(this._connectTimer); this._connectTimer = null; this._closeSent = true; const err = new Error(reason || `\u8FDE\u63A5\u5931\u8D25 (${code})`); err.code = code; this.destroy(err); } // ---- 接收方:accept / reject ---- /** 目标已连上,通知对端可以发数据了。meta/blob 会原样带给对端(见 _onConnectOk)。 */ accept(meta = "", blob = null) { if (this.connected || this.destroyed) return; this.connected = true; this.session._sendFrame(T.CONNECT_OK, this.id, encodeConnectOk(meta, blob)); this._pump(); } /** 拒绝这条流(策略拦截、目标不可达等)。 */ reject(code = E.GENERAL, reason = "") { if (this._closeSent || this.destroyed) return; this._closeSent = true; this.session._sendFrame(T.CONNECT_ERR, this.id, encodeError(code, reason)); const err = new Error(reason || `\u62D2\u7EDD\u8FDE\u63A5 (${code})`); err.code = code; this.destroy(err); } /** 本端主动出错关流,发 RST。 */ _fail(code, reason) { if (this._closeSent || this.destroyed) { if (!this.destroyed) this.destroy(new Error(reason)); return; } this._closeSent = true; this.session._sendFrame(T.RST, this.id, encodeError(code, reason)); const err = new Error(reason); err.code = code; this.destroy(err); } // ---- 收 ---- _onData(chunk) { if (this._remoteEnded || this.destroyed) return; this.bytesIn += chunk.length; this._unacked += chunk.length; if (this.push(chunk)) this._flushWindow(); } _onRemoteFin() { if (this._remoteEnded) return; this._remoteEnded = true; this.push(null); } _onWindowUpdate(n) { this._sendWindow += n; this._pump(); } // ---- 数据报(UDP 关联)---- /** * 发一个数据报。不走窗口流控——UDP 本来就是不可靠的。 * * 隧道拥塞时**直接丢**而不是排队:给 UDP 排队只会把延迟拖到毫无意义, * 丢包正是上层协议本来就要处理的情况。 * * @returns {boolean} false = 被丢弃 */ sendDatagram(payload) { if (this.destroyed || !this.connected) return false; if (payload.length > MAX_PAYLOAD) { this.datagramsDropped += 1; return false; } if (this.session.congested) { this.datagramsDropped += 1; return false; } this.datagramsOut += 1; this.bytesOut += payload.length; return this.session._sendFrame(T.DATAGRAM, this.id, payload); } _onDatagram(payload) { if (this.destroyed) return; this.datagramsIn += 1; this.bytesIn += payload.length; this.emit("datagram", payload); } _flushWindow() { if (this._unacked <= 0 || this.destroyed || this._remoteEnded) return; const n = this._unacked; this._unacked = 0; this.session._sendFrame(T.WINDOW_UPDATE, this.id, encodeUint32(n)); } _read() { this._flushWindow(); } // ---- 发 ---- _write(chunk, _enc, cb) { if (this.destroyed) { cb(new Error("stream \u5DF2\u9500\u6BC1")); return; } this._pending = { buf: chunk, off: 0, cb }; this._pump(); } _pump() { const p = this._pending; if (!p || !this.connected || this.destroyed) return; while (p.off < p.buf.length && this._sendWindow > 0 && !this.session.congested) { const n = Math.min(p.buf.length - p.off, this._sendWindow, MAX_PAYLOAD); this.session._sendFrame(T.DATA, this.id, p.buf.subarray(p.off, p.off + n)); this._sendWindow -= n; this.bytesOut += n; p.off += n; } if (p.off >= p.buf.length) { this._pending = null; p.cb(); } } _final(cb) { if (!this.destroyed && !this._closeSent) { this._finSent = true; this.session._sendFrame(T.FIN, this.id); } cb(); } _destroy(err, cb) { clearTimeout(this._connectTimer); this._connectTimer = null; if (this._pending) { const p = this._pending; this._pending = null; p.cb(err ?? new Error("stream \u5DF2\u5173\u95ED")); } const cleanClose = !err && this._finSent && this._remoteEnded; if (!this._closeSent && !cleanClose) { this.session._sendFrame( T.RST, this.id, encodeError(err?.code ?? E.GENERAL, err?.message ?? "") ); } this._closeSent = true; this.session._removeStream(this.id); cb(err); } }; // packages/protocol/src/session.js var import_node_events = require("node:events"); var MAX_STREAMS = 128; var WS_OPEN = 1; var Session = class extends import_node_events.EventEmitter { constructor(ws, opts = {}) { super(); this.ws = ws; this.label = opts.label ?? ""; this.initiator = opts.initiator !== false; this.window = opts.window ?? INITIAL_WINDOW; this.maxStreams = opts.maxStreams ?? MAX_STREAMS; this.pingInterval = opts.pingInterval ?? 2e4; this.pingTimeout = opts.pingTimeout ?? 6e4; this.highWater = opts.highWater ?? 4 * 1024 * 1024; this.lowWater = opts.lowWater ?? Math.floor(this.highWater / 2); this.congested = false; this.congestedCount = 0; this.streams = /* @__PURE__ */ new Map(); this.alive = true; this.rtt = null; this.openedAt = Date.now(); this.streamsOpened = 0; this.bytesIn = 0; this.bytesOut = 0; this._nextStreamId = this.initiator ? 1 : 2; this._lastPongAt = Date.now(); ws.on("message", (data, isBinary) => this._onMessage(data, isBinary)); ws.on("close", (code, reason) => this._onClose(code, String(reason ?? ""))); ws.on("error", (err) => { this.emit("warn", `websocket \u9519\u8BEF: ${err.message}`); this._onClose(1006, err.message); }); this._pingTimer = setInterval(() => this._keepalive(), this.pingInterval); this._pingTimer.unref?.(); } // ---- 对外 API ---- /** * 开一条流。返回的 Duplex 在收到 CONNECT_OK 后 emit 'connect'。 * * 两种用法: * 明文 open({ host, port, region }) —— 目标地址对 broker 可见 * 加密 open({ region, nodeId, blob }) —— 目标地址在 blob 里,broker 看不到; * nodeId 指定送给哪个出口节点 */ open({ host, port, region = "", nodeId = "", blob = null, connectTimeout = 3e4, /** true 时开的是 UDP 关联(SOCKS5 的 UDP ASSOCIATE),不是 TCP 流 */ datagram = false } = {}) { if (!this.alive) throw Object.assign(new Error("\u4F1A\u8BDD\u5DF2\u5173\u95ED"), { code: E.EXIT_GONE }); if (this.streams.size >= this.maxStreams) { throw Object.assign(new Error("\u5E76\u53D1\u6D41\u8D85\u9650"), { code: E.GENERAL }); } const id = this._allocStreamId(); if (id === null) throw Object.assign(new Error("streamId \u8017\u5C3D"), { code: E.GENERAL }); const stream = new Stream(this, id, { window: this.window, datagram }); stream.remoteAddr = blob ? "(\u52A0\u5BC6)" : `${host}:${port}`; this.streams.set(id, stream); this.streamsOpened += 1; this._sendFrame( datagram ? T.ASSOCIATE : T.CONNECT, id, encodeConnect({ host, port, region, nodeId, blob }) ); stream._armConnectTimeout(connectTimeout); return stream; } close(code = 1e3, reason = "") { if (!this.alive) return; try { this.ws.close(code, reason); } catch { } this._onClose(code, reason); } get bufferedAmount() { return this.ws.bufferedAmount ?? 0; } stats() { return { label: this.label, alive: this.alive, rtt: this.rtt, uptimeMs: Date.now() - this.openedAt, streamsActive: this.streams.size, streamsOpened: this.streamsOpened, bytesIn: this.bytesIn, bytesOut: this.bytesOut, buffered: this.bufferedAmount, congested: this.congested, congestedCount: this.congestedCount }; } // ---- 内部 ---- _allocStreamId() { for (let i = 0; i < 65536; i += 1) { const id = this._nextStreamId; this._nextStreamId = this._nextStreamId + 2 >>> 0; if (this._nextStreamId === 0) this._nextStreamId = 2; if (id !== 0 && !this.streams.has(id)) return id; } return null; } _sendFrame(type, streamId, payload) { if (!this.alive || this.ws.readyState !== WS_OPEN) return false; try { const buf = encodeFrame(type, streamId, payload); this.bytesOut += buf.length; this.ws.send(buf, { binary: true }, () => this._afterSend()); if (!this.congested && this.bufferedAmount > this.highWater) { this.congested = true; this.congestedCount += 1; } return true; } catch (err) { this.emit("warn", `\u53D1\u5E27\u5931\u8D25 ${TYPE_NAME[type] ?? type}#${streamId}: ${err.message}`); return false; } } /** ws 把一帧真正交给 socket 之后回调。掉回低水位就放所有流继续发。 */ _afterSend() { if (!this.congested || !this.alive) return; if (this.bufferedAmount > this.lowWater) return; this.congested = false; for (const stream of this.streams.values()) stream._pump(); } _removeStream(id) { this.streams.delete(id); } _keepalive() { if (!this.alive) return; if (Date.now() - this._lastPongAt > this.pingTimeout) { this.emit("warn", `${this.pingTimeout}ms \u5185\u6CA1\u6536\u5230 PONG\uFF0C\u5224\u5B9A\u94FE\u8DEF\u5DF2\u6B7B`); this.close(1001, "keepalive timeout"); return; } const buf = Buffer.allocUnsafe(8); buf.writeBigUInt64BE(BigInt(Date.now()), 0); this._sendFrame(T.PING, 0, buf); } _onMessage(data, isBinary) { if (!this.alive) return; if (!isBinary) return; const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); this.bytesIn += buf.length; let frame; try { frame = decodeFrame(buf); } catch (err) { this.emit("warn", `\u574F\u5E27: ${err.message}`); this.close(1002, "bad frame"); return; } this._dispatch(frame); } _dispatch({ type, streamId, payload }) { if (streamId === 0) { if (type === T.PING) { this._sendFrame(T.PONG, 0, payload); } else if (type === T.PONG && payload.length >= 8) { this._lastPongAt = Date.now(); this.rtt = Math.max(0, Date.now() - Number(payload.readBigUInt64BE(0))); } return; } if (type === T.CONNECT || type === T.ASSOCIATE) { this._onConnect(streamId, payload, type === T.ASSOCIATE); return; } const stream = this.streams.get(streamId); if (!stream) { if (type === T.DATA) { this._sendFrame(T.RST, streamId, encodeError(E.PROTOCOL, "unknown stream")); } return; } switch (type) { case T.DATA: stream._onData(payload); break; case T.CONNECT_OK: stream._onConnectOk(payload); break; case T.CONNECT_ERR: { const { code, reason } = decodeError(payload); stream._onConnectErr(code, reason); break; } case T.FIN: stream._onRemoteFin(); break; case T.RST: { const { code, reason } = decodeError(payload); stream._closeSent = true; stream.destroy(Object.assign(new Error(reason || "\u5BF9\u7AEF\u91CD\u7F6E\u4E86\u8FDE\u63A5"), { code })); break; } case T.WINDOW_UPDATE: if (payload.length >= 4) stream._onWindowUpdate(payload.readUInt32BE(0)); break; case T.DATAGRAM: stream._onDatagram(payload); break; default: this.emit("warn", `\u672A\u77E5\u5E27\u7C7B\u578B ${type}`); } } _onConnect(streamId, payload, isDatagram = false) { const event = isDatagram ? "association" : "stream"; if (this.streams.has(streamId)) { this.close(1002, "duplicate stream id"); return; } if (this.listenerCount(event) === 0) { this._sendFrame( T.CONNECT_ERR, streamId, encodeError(E.GENERAL, isDatagram ? "\u672C\u7AEF\u4E0D\u63A5\u53D7 UDP \u5173\u8054" : "\u672C\u7AEF\u4E0D\u63A5\u53D7\u5165\u5411\u6D41") ); return; } if (this.streams.size >= this.maxStreams) { this._sendFrame(T.CONNECT_ERR, streamId, encodeError(E.GENERAL, "\u5E76\u53D1\u6D41\u8D85\u9650")); return; } let info; try { info = decodeConnect(payload); } catch (err) { this._sendFrame(T.CONNECT_ERR, streamId, encodeError(E.PROTOCOL, err.message)); return; } const stream = new Stream(this, streamId, { window: this.window, incoming: true, datagram: isDatagram }); stream.remoteAddr = info.blob ? "(\u52A0\u5BC6)" : `${info.host}:${info.port}`; this.streams.set(streamId, stream); this.streamsOpened += 1; this.emit(event, stream, info); } _onClose(code, reason) { if (!this.alive) return; this.alive = false; clearInterval(this._pingTimer); const err = Object.assign(new Error(`\u96A7\u9053\u65AD\u5F00 (${code}) ${reason}`.trim()), { code: E.EXIT_GONE }); for (const stream of [...this.streams.values()]) { stream._closeSent = true; stream.destroy(err); } this.streams.clear(); this.emit("close", code, reason); } }; // packages/protocol/src/crypto.js var import_node_crypto = __toESM(require("node:crypto"), 1); var import_node_stream2 = require("node:stream"); var X25519_SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex"); var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex"); var TAG_LEN = 16; var NONCE_LEN = 12; var KEY_LEN = 32; var RECORD_OVERHEAD = 2 + TAG_LEN; var MAX_RECORD = MAX_PAYLOAD - RECORD_OVERHEAD; function parseGroupKey(str) { if (typeof str !== "string" || str.length === 0) throw new Error("\u7F3A\u5C11\u7FA4\u7EC4\u5BC6\u94A5"); const buf = Buffer.from(str, "base64url"); if (buf.length !== KEY_LEN) { throw new Error(`\u7FA4\u7EC4\u5BC6\u94A5\u5FC5\u987B\u662F 32 \u5B57\u8282\u7684 base64url\uFF0C\u5F53\u524D\u89E3\u51FA ${buf.length} \u5B57\u8282`); } return buf; } function generateIdentitySeed() { const { privateKey } = import_node_crypto.default.generateKeyPairSync("x25519"); const der = privateKey.export({ type: "pkcs8", format: "der" }); return Buffer.from(der.subarray(der.length - 32)).toString("base64url"); } function rawPublicKey(keyObject) { const der = keyObject.export({ type: "spki", format: "der" }); return Buffer.from(der.subarray(der.length - 32)); } function importPublicKey(raw) { if (!Buffer.isBuffer(raw) || raw.length !== 32) throw new Error("X25519 \u516C\u94A5\u5FC5\u987B 32 \u5B57\u8282"); return import_node_crypto.default.createPublicKey({ key: Buffer.concat([X25519_SPKI_PREFIX, raw]), format: "der", type: "spki" }); } function importPrivateKey(raw) { return import_node_crypto.default.createPrivateKey({ key: Buffer.concat([X25519_PKCS8_PREFIX, raw]), format: "der", type: "pkcs8" }); } function loadIdentity(seedB64) { const raw = Buffer.from(String(seedB64 ?? ""), "base64url"); if (raw.length !== 32) { throw new Error(`\u8EAB\u4EFD\u5BC6\u94A5\u5FC5\u987B\u662F 32 \u5B57\u8282\u7684 base64url\uFF0C\u5F53\u524D\u89E3\u51FA ${raw.length} \u5B57\u8282`); } const privateKey = importPrivateKey(raw); const publicKey = rawPublicKey(import_node_crypto.default.createPublicKey(privateKey)); return { privateKey, publicKey, publicKeyB64: publicKey.toString("base64url"), fingerprint: fingerprint(publicKey) }; } function fingerprint(rawPub) { const buf = Buffer.isBuffer(rawPub) ? rawPub : Buffer.from(String(rawPub), "base64url"); const h = import_node_crypto.default.createHash("sha256").update(buf).digest(); return [...h.subarray(0, 8)].map((b) => b.toString(16).padStart(2, "0")).join(":"); } function hkdf(ikm, salt, info, len = KEY_LEN) { return Buffer.from(import_node_crypto.default.hkdfSync("sha256", ikm, salt, info, len)); } function nonce(counter) { const iv = Buffer.alloc(NONCE_LEN); iv.writeBigUInt64BE(BigInt(counter), 4); return iv; } function seal(key, counter, plaintext, aad) { const cipher = import_node_crypto.default.createCipheriv("aes-256-gcm", key, nonce(counter)); if (aad) cipher.setAAD(aad); const body = Buffer.concat([cipher.update(plaintext), cipher.final()]); return Buffer.concat([body, cipher.getAuthTag()]); } function open(key, counter, data, aad) { if (data.length < TAG_LEN) throw new Error("\u5BC6\u6587\u957F\u5EA6\u4E0D\u8DB3"); const decipher = import_node_crypto.default.createDecipheriv("aes-256-gcm", key, nonce(counter)); if (aad) decipher.setAAD(aad); decipher.setAuthTag(data.subarray(data.length - TAG_LEN)); return Buffer.concat([ decipher.update(data.subarray(0, data.length - TAG_LEN)), decipher.final() ]); } function initKey(psk, es, ePub) { return hkdf(Buffer.concat([psk, es]), ePub, Buffer.from("nvpn/v2 init")); } function sessionKeys(psk, es, ee, aPub, bPub) { const transcript = Buffer.concat([aPub, bPub]); const master = hkdf( Buffer.concat([psk, es, ee]), transcript, Buffer.from("nvpn/v2 session"), 64 ); return { a2b: master.subarray(0, 32), b2a: master.subarray(32, 64), confirm: hkdf(master, transcript, Buffer.from("nvpn/v2 confirm")), transcript }; } function initiatorHandshake(groupKey, peerStaticPub, targetPlaintext) { const psk = parseGroupKey(groupKey); const peerPub = Buffer.isBuffer(peerStaticPub) ? peerStaticPub : Buffer.from(String(peerStaticPub), "base64url"); if (peerPub.length !== 32) throw new Error("\u51FA\u53E3\u8282\u70B9\u516C\u94A5\u5FC5\u987B 32 \u5B57\u8282"); const { privateKey, publicKey } = import_node_crypto.default.generateKeyPairSync("x25519"); const ePub = rawPublicKey(publicKey); const es = import_node_crypto.default.diffieHellman({ privateKey, publicKey: importPublicKey(peerPub) }); const k0 = initKey(psk, es, ePub); const aad = Buffer.concat([ePub, peerPub]); const message1 = Buffer.concat([ePub, seal(k0, 0, targetPlaintext, aad)]); return { message1, finish(message2) { if (!message2 || message2.length < 32 + TAG_LEN) { throw new Error("\u63E1\u624B\u54CD\u5E94\u8FC7\u77ED\uFF0C\u5BF9\u7AEF\u53EF\u80FD\u8FD8\u662F\u65E7\u7248\u672C"); } const bPub = Buffer.from(message2.subarray(0, 32)); const ee = import_node_crypto.default.diffieHellman({ privateKey, publicKey: importPublicKey(bPub) }); const s = sessionKeys(psk, es, ee, ePub, bPub); const ok = open(s.confirm, 0, message2.subarray(32), Buffer.concat([s.transcript, peerPub])); if (ok.toString("utf8") !== "ok") throw new Error("\u63E1\u624B\u786E\u8BA4\u6807\u7B7E\u4E0D\u5339\u914D"); return { tx: s.a2b, rx: s.b2a }; } }; } function responderHandshake(groupKeys, identity2, message1) { const keys = (Array.isArray(groupKeys) ? groupKeys : [groupKeys]).filter(Boolean); if (keys.length === 0) throw new Error("\u7F3A\u5C11\u7FA4\u7EC4\u5BC6\u94A5"); if (!message1 || message1.length < 32 + TAG_LEN) throw new Error("\u63E1\u624B\u6D88\u606F\u8FC7\u77ED"); const aPub = Buffer.from(message1.subarray(0, 32)); const es = import_node_crypto.default.diffieHellman({ privateKey: identity2.privateKey, publicKey: importPublicKey(aPub) }); const aad = Buffer.concat([aPub, identity2.publicKey]); let psk = null; let target = null; let lastErr = null; for (const k of keys) { try { const candidate = parseGroupKey(k); target = open(initKey(candidate, es, aPub), 0, message1.subarray(32), aad); psk = candidate; break; } catch (err) { lastErr = err; } } if (!psk) { throw new Error( keys.length > 1 ? `${keys.length} \u628A\u7FA4\u7EC4\u5BC6\u94A5\u90FD\u89E3\u4E0D\u5F00\u8FD9\u6761\u63E1\u624B` : lastErr?.message ?? "\u7FA4\u7EC4\u5BC6\u94A5\u4E0D\u5339\u914D" ); } const { privateKey, publicKey } = import_node_crypto.default.generateKeyPairSync("x25519"); const bPub = rawPublicKey(publicKey); const ee = import_node_crypto.default.diffieHellman({ privateKey, publicKey: importPublicKey(aPub) }); const s = sessionKeys(psk, es, ee, aPub, bPub); const confirm = seal( s.confirm, 0, Buffer.from("ok"), Buffer.concat([s.transcript, identity2.publicKey]) ); return { target, message2: Buffer.concat([bPub, confirm]), keys: { tx: s.b2a, rx: s.a2b } }; } var REPLAY_WINDOW = 64; var ReplayWindow = class { constructor(size = REPLAY_WINDOW) { this.size = size; this.highest = -1; this.bits = 0n; } /** @returns {boolean} false = 重放或太旧,应当丢弃 */ accept(seq) { if (!Number.isInteger(seq) || seq < 0) return false; if (seq > this.highest) { const shift = seq - this.highest; this.bits = shift >= 64 ? 1n : this.bits << BigInt(shift) & 0xffffffffffffffffn | 1n; this.highest = seq; return true; } const offset = this.highest - seq; if (offset >= this.size) return false; const mask = 1n << BigInt(offset); if (this.bits & mask) return false; this.bits |= mask; return true; } }; var DatagramCipher = class { constructor(keys) { this.txKey = keys.tx; this.rxKey = keys.rx; this.txCounter = 0; this.window = new ReplayWindow(); } /** @returns {Buffer} [8B 序号][密文(含标签)] */ seal(plaintext) { const counter = this.txCounter; this.txCounter += 1; const header = Buffer.allocUnsafe(8); header.writeBigUInt64BE(BigInt(counter), 0); return Buffer.concat([header, seal(this.txKey, counter, plaintext, header)]); } /** @returns {Buffer|null} null = 重放/太旧/解不开,直接丢 */ open(packet) { if (!packet || packet.length < 8 + TAG_LEN) return null; const header = packet.subarray(0, 8); const counter = Number(header.readBigUInt64BE(0)); if (!this.window.accept(counter)) return null; try { return open(this.rxKey, counter, packet.subarray(8), header); } catch { return null; } } }; function createEncryptor(key) { let counter = 0; return new import_node_stream2.Transform({ transform(chunk, _enc, cb) { try { for (let off = 0; off < chunk.length; off += MAX_RECORD) { const slice = chunk.subarray(off, Math.min(off + MAX_RECORD, chunk.length)); const ct = seal(key, counter, slice, null); counter += 1; const record = Buffer.allocUnsafe(2 + ct.length); record.writeUInt16BE(ct.length, 0); ct.copy(record, 2); this.push(record); } cb(); } catch (err) { cb(err); } } }); } function createDecryptor(key) { let counter = 0; let buf = Buffer.alloc(0); return new import_node_stream2.Transform({ transform(chunk, _enc, cb) { buf = buf.length === 0 ? chunk : Buffer.concat([buf, chunk]); try { for (; ; ) { if (buf.length < 2) break; const len = buf.readUInt16BE(0); if (len < TAG_LEN || len > MAX_RECORD + TAG_LEN) { throw new Error(`\u52A0\u5BC6\u8BB0\u5F55\u957F\u5EA6\u975E\u6CD5: ${len}`); } if (buf.length < 2 + len) break; const pt = open(key, counter, buf.subarray(2, 2 + len), null); counter += 1; buf = buf.subarray(2 + len); this.push(pt); } cb(); } catch (err) { cb(err); } } }); } // packages/protocol/src/throttle.js var import_node_stream3 = require("node:stream"); var TokenBucket = class { /** * @param {number} bytesPerSec 速率,0 表示不限 * @param {number} burstBytes 桶容量,默认给 1 秒的量(至少 64 KB) */ constructor(bytesPerSec, burstBytes = 0) { this.rate = Math.max(0, bytesPerSec | 0); this.capacity = burstBytes > 0 ? burstBytes : Math.max(64 * 1024, this.rate); this.tokens = this.capacity; this.last = Date.now(); this.waiters = []; this._timer = null; } get unlimited() { return this.rate === 0; } _refill() { const now = Date.now(); const elapsed = (now - this.last) / 1e3; if (elapsed <= 0) return; this.last = now; this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.rate); } /** 取 n 个令牌,不够就等到够为止。 */ take(n) { if (this.unlimited || n <= 0) return Promise.resolve(); const want = Math.min(n, this.capacity); return new Promise((resolve) => { this.waiters.push({ want, resolve }); this._drain(); }); } /** * 非阻塞地取。够就扣掉返回 true,不够直接返回 false。 * * UDP 专用:给数据报排队等额度只会把延迟拖到毫无意义,丢包本来就是 * 上层协议要处理的情况。TCP 那边用 take(),排队等着才是对的。 */ tryTake(n) { if (this.unlimited || n <= 0) return true; this._refill(); if (this.waiters.length > 0 || this.tokens < n) return false; this.tokens -= n; return true; } _clearTimer() { if (this._timer) { clearTimeout(this._timer); this._timer = null; } } _drain() { this._refill(); while (this.waiters.length > 0 && this.waiters[0].want <= this.tokens) { const w = this.waiters.shift(); this.tokens -= w.want; w.resolve(); } if (this.waiters.length === 0) { this._clearTimer(); return; } if (this._timer) return; const need = this.waiters[0].want - this.tokens; const waitMs = Math.max(5, Math.ceil(need / this.rate * 1e3)); this._timer = setTimeout(() => { this._timer = null; this._drain(); }, waitMs); } /** 改速率立即生效,正在排队的请求会按新速率重算。 */ setRate(bytesPerSec) { this._refill(); this.rate = Math.max(0, bytesPerSec | 0); this.capacity = Math.max(64 * 1024, this.rate); this.tokens = Math.min(this.tokens, this.capacity); if (this.unlimited) { for (const w of this.waiters.splice(0)) w.resolve(); this._clearTimer(); } else { this._drain(); } } }; function createThrottle(bucket) { return new import_node_stream3.Transform({ transform(chunk, _enc, cb) { if (!bucket || bucket.unlimited) { cb(null, chunk); return; } bucket.take(chunk.length).then( () => cb(null, chunk), (err) => cb(err) ); } }); } // packages/protocol/src/relay.js function relaySecure(local, tunnel, keys, { onClose, graceMs = 5e3, bucket } = {}) { const encryptor = createEncryptor(keys.tx); const decryptor = createDecryptor(keys.rx); const upThrottle = bucket && !bucket.unlimited ? createThrottle(bucket) : null; const downThrottle = bucket && !bucket.unlimited ? createThrottle(bucket) : null; const parts = [local, tunnel, encryptor, decryptor, upThrottle, downThrottle].filter(Boolean); let finished = false; let timer = null; const finish = (err) => { if (finished) return; finished = true; clearTimeout(timer); timer = null; for (const s of parts) { if (!s.destroyed) s.destroy(err ?? void 0); } onClose?.(err ?? null); }; const armGrace = () => { if (finished || timer) return; timer = setTimeout(() => finish(null), graceMs); timer.unref?.(); }; for (const s of parts) s.on("error", finish); local.on("close", () => tunnel.destroyed ? finish(null) : armGrace()); tunnel.on("close", () => local.destroyed ? finish(null) : armGrace()); if (upThrottle) { local.pipe(upThrottle).pipe(encryptor).pipe(tunnel); tunnel.pipe(decryptor).pipe(downThrottle).pipe(local); } else { local.pipe(encryptor).pipe(tunnel); tunnel.pipe(decryptor).pipe(local); } return finish; } // packages/protocol/src/crash-guard.js function installCrashGuard({ log: log2, name = "process", threshold: threshold2 = 5, windowMs = 6e4, onFatal }) { let hits = []; let dying = false; const record = (kind, err) => { log2.error(`${kind}: ${err?.stack ?? err}`); if (dying) return; const now = Date.now(); hits = hits.filter((t) => now - t < windowMs); hits.push(now); if (hits.length < threshold2) return; dying = true; log2.error( `${Math.round(windowMs / 1e3)} \u79D2\u5185\u51FA\u73B0 ${hits.length} \u6B21\u672A\u6355\u83B7\u5F02\u5E38\uFF0C${name} \u7684\u72B6\u6001\u5DF2\u7ECF\u4E0D\u53EF\u4FE1\uFF0C\u4E3B\u52A8\u9000\u51FA\u4EA4\u7ED9 supervisor \u91CD\u542F` ); try { onFatal?.(); } catch (e) { log2.error(`\u6536\u5C3E\u65F6\u53C8\u51FA\u9519: ${e.message}`); } setTimeout(() => process.exit(1), 1e3).unref(); }; process.on("uncaughtException", (err) => record("\u672A\u6355\u83B7\u5F02\u5E38", err)); process.on("unhandledRejection", (reason) => record("\u672A\u5904\u7406\u7684 rejection", reason)); } // packages/client/src/config.js var import_node_fs = __toESM(require("node:fs"), 1); var import_node_os = __toESM(require("node:os"), 1); var import_node_path = __toESM(require("node:path"), 1); var import_node_crypto2 = __toESM(require("node:crypto"), 1); // packages/client/src/invite.js var PREFIX = "nvpn1:"; function decodeInvite(str) { const s = String(str ?? "").trim(); if (!s.startsWith(PREFIX)) { throw new Error(`\u9080\u8BF7\u4E32\u683C\u5F0F\u4E0D\u5BF9\uFF0C\u5E94\u5F53\u4EE5 ${PREFIX} \u5F00\u5934`); } let payload; try { payload = JSON.parse(Buffer.from(s.slice(PREFIX.length), "base64url").toString("utf8")); } catch (err) { throw new Error(`\u9080\u8BF7\u4E32\u89E3\u4E0D\u5F00\uFF08\u590D\u5236\u7684\u65F6\u5019\u53EF\u80FD\u5C11\u4E86\u51E0\u4E2A\u5B57\u7B26\uFF09: ${err.message}`); } const out = { broker: payload.b, token: payload.t, groupKey: payload.g }; if (payload.n) out.name = payload.n; for (const k of ["broker", "token", "groupKey"]) { if (typeof out[k] !== "string" || out[k] === "") { throw new Error(`\u9080\u8BF7\u4E32\u91CC\u7F3A\u5C11 ${k}`); } } return out; } // packages/client/src/config.js var HOME = process.env.NATIVE_VPN_HOME ?? import_node_path.default.join(import_node_os.default.homedir(), ".native-vpn"); var CONFIG_FILE = import_node_path.default.join(HOME, "config.json"); var DEFAULTS = { /** * 你自己那台 broker 的地址,形如 wss://tunnel.example.com。 * 故意留空:这里要是填了某个具体域名,所有装了这份代码的人开机就会往那台机器上打。 */ broker: "", token: "", /** 机器身份,首次启动生成后固定不变。broker 用它做会话粘性的 key。 */ nodeId: "", name: import_node_os.default.hostname().slice(0, 40), /** 默认出口地区。'any' = 不限;也可以在 SOCKS5 用户名里按次覆盖。 */ region: "any", /** * 群组预共享密钥。组里所有人用同一个,**带外分发**(微信/Signal 直接发)。 * * v2 之后它不再是会话密钥的唯一来源,作用变成「组成员资格」这道闸: * 光偷到 token 而不在组里的人进不了数据面。真正决定谁能解密的是下面的 * 静态身份密钥。用 `node packages/client/src/gen-group-key.js` 生成。 */ groupKey: "", /** * 额外接受的旧群组密钥(逗号分隔),只用于**收**,发出去的握手一律用 groupKey。 * * 换钥不用全组同一秒切换:先所有人把新钥加进这里(新旧都认),再所有人把 * groupKey 切到新钥,最后所有人把旧钥从这里删掉。每一步都不断服务。 */ groupKeysAccepted: "", /** * 本机的静态身份私钥(32 字节 base64url),首次启动自动生成,**永不外发**。 * * 别人要用你的 IP 出网时,会用你的**公钥**加密目标地址,只有握着这把私钥的 * 你才解得开。这是「broker 运维就算是组员也看不到别人流量」的根据—— * 他没有你的私钥。 */ identityKey: "", socksHost: "127.0.0.1", socksPort: 1080, panelHost: "127.0.0.1", panelPort: 1081, /** * 面板访问令牌,首次启动自动生成。启动日志里会打完整带 token 的地址,存成书签即可。 * * 「只监听回环」不等于安全:同一台机器上**别的用户**的进程照样能连上来读状态、 * 甚至替你打开共享出口。有了它,对方得先能读到这个 0600 的配置文件才行。 * 挡不住同一用户下的进程——那种进程本来就能直接改配置文件,无解。 * * 设成 'off' 可以关掉鉴权。 */ panelToken: "", /** * 是否把自己的网络共享出去当出口节点。 * * 默认 false,而且必须由用户手动打开——这条不是保守,是这类软件的底线: * 别人通过你的 IP 做的任何事,在 ISP 和目标网站看来都是你做的。 * Hola VPN 当年被声讨、911.re 被查封,根子都在「用户不知道自己在当出口」。 */ shareExit: false, /** 作为出口时的并发连接上限,别把家里路由器的会话表撑爆 */ maxConcurrent: 64, /** * 作为出口时的 UDP 关联上限。一条关联能往很多目标发包,比 TCP 流「重」, * 而且家用路由器的 UDP 会话表通常比 TCP 的还小,所以给得保守些。 */ maxUdpAssociations: 16, /** * 作为出口时,替别人转发的流量上限(KB/s,0 = 不限)。 * * 只限并发数是不够的——一条连接就能把家里的上行吃满。两个方向共用一个桶, * 限的是这台机器替池子扛的总流量。家宽上行普遍只有几 Mbps, * 想让别人用又不影响自己的话,这个值建议设成上行带宽的一半左右。 */ maxRelayKbps: 0, /** * 出口侧只放行这些端口(逗号分隔,留空 = 用内置黑名单,只挡 SMTP/SMB 那几个)。 * 想把节点收紧成「只给网页用」就填 `80,443`。 */ allowedPorts: "", /** * 记录出口连接审计日志(~/.native-vpn/exit-audit.log),默认关。 * * 两面性:开了,别人拿你的 IP 干了什么你手里有据,ISP 找上门时能自证; * 但同时你也留下了组里其他人的浏览记录,那份文件本身就是隐私负担。 * 不替你做决定。 */ auditLog: false, /** * 出口侧用的 DNS 服务器(逗号分隔,留空 = 用系统配置的)。 * 系统 DNS 慢或者不靠谱时可以指一个,比如 `1.1.1.1,8.8.8.8`。 */ dnsServers: "", /** DNS 自检不过也强行当出口节点。见 selfcheck.js 里为什么默认不让。 */ forceExit: false, logLevel: "info" }; function parseArgs(argv) { const out = {}; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (!arg.startsWith("--")) continue; const eq = arg.indexOf("="); const key = (eq === -1 ? arg.slice(2) : arg.slice(2, eq)).replace( /-([a-z])/g, (_, c) => c.toUpperCase() ); let value = eq === -1 ? argv[i + 1] : arg.slice(eq + 1); if (eq === -1) { if (value === void 0 || value.startsWith("--")) value = "true"; else i += 1; } out[key] = value; } return out; } function coerce(base, patch) { const out = { ...base }; for (const [k, v] of Object.entries(patch)) { if (!(k in DEFAULTS)) continue; const dflt = DEFAULTS[k]; if (typeof dflt === "number") out[k] = Number(v); else if (typeof dflt === "boolean") out[k] = v === true || v === "true" || v === "1"; else out[k] = String(v); } return out; } function loadConfig(argv = process.argv.slice(2)) { import_node_fs.default.mkdirSync(HOME, { recursive: true }); let stored = {}; if (import_node_fs.default.existsSync(CONFIG_FILE)) { try { stored = JSON.parse(import_node_fs.default.readFileSync(CONFIG_FILE, "utf8")); } catch (err) { throw new Error(`\u914D\u7F6E\u6587\u4EF6\u635F\u574F ${CONFIG_FILE}: ${err.message}`); } } const args = parseArgs(argv); let joined = {}; if (args.join) { joined = decodeInvite(args.join); delete args.join; delete joined.name; } let config2 = coerce(DEFAULTS, stored); config2 = coerce(config2, joined); config2 = coerce(config2, args); if (!config2.nodeId) { config2.nodeId = import_node_crypto2.default.randomBytes(8).toString("hex"); } if (!config2.identityKey) { config2.identityKey = generateIdentitySeed(); } if (!config2.panelToken) { config2.panelToken = import_node_crypto2.default.randomBytes(18).toString("base64url"); } saveRaw(config2); return config2; } function saveRaw(config2) { import_node_fs.default.mkdirSync(HOME, { recursive: true }); import_node_fs.default.writeFileSync(CONFIG_FILE, `${JSON.stringify(config2, null, 2)} `, { mode: 384 }); } function updateConfig(current, patch) { const next = coerce(current, patch); saveRaw(next); return next; } // packages/client/src/log.js var LEVELS = { error: 0, warn: 1, info: 2, debug: 3 }; var threshold = LEVELS.info; function setLevel(level) { threshold = LEVELS[level] ?? LEVELS.info; } function emit(level, scope, msg) { if (LEVELS[level] > threshold) return; const ts = (/* @__PURE__ */ new Date()).toTimeString().slice(0, 8); const line = `${ts} ${level.toUpperCase().padEnd(5)} [${scope}] ${msg}`; if (level === "error" || level === "warn") process.stderr.write(`${line} `); else process.stdout.write(`${line} `); } function logger(scope) { return { error: (m) => emit("error", scope, m), warn: (m) => emit("warn", scope, m), info: (m) => emit("info", scope, m), debug: (m) => emit("debug", scope, m) }; } // packages/client/src/tunnel.js var import_node_events2 = require("node:events"); // node_modules/ws/wrapper.mjs var import_stream3 = __toESM(require_stream(), 1); var import_extension = __toESM(require_extension(), 1); var import_permessage_deflate = __toESM(require_permessage_deflate(), 1); var import_receiver = __toESM(require_receiver(), 1); var import_sender = __toESM(require_sender(), 1); var import_subprotocol = __toESM(require_subprotocol(), 1); var import_websocket = __toESM(require_websocket(), 1); var import_websocket_server = __toESM(require_websocket_server(), 1); var wrapper_default = import_websocket.default; // packages/client/src/tunnel.js var Link = class extends import_node_events2.EventEmitter { constructor({ name, brokerUrl, path: wsPath, token, initiator, log: log2, params = {}, maxStreams = 128 }) { super(); this.name = name; this.brokerUrl = brokerUrl; this.path = wsPath; this.token = token; this.initiator = initiator; this.log = log2; this.params = params; this.maxStreams = maxStreams; this.status = "offline"; this.lastError = null; this.connectedAt = null; this.attempt = 0; this._session = null; this._ws = null; this._timer = null; this._stopped = false; } get session() { return this._session?.alive ? this._session : null; } start() { this._stopped = false; this._connect(); } stop() { this._stopped = true; clearTimeout(this._timer); this._timer = null; this._session?.close(1e3, "client shutdown"); try { this._ws?.terminate(); } catch { } this._session = null; this._ws = null; this._setStatus("offline"); } /** 改地区之类的会话级参数,需要重连才生效。 */ reconnect(params) { if (params) Object.assign(this.params, params); clearTimeout(this._timer); this._timer = null; this.attempt = 0; this._session?.close(1e3, "reconnect"); try { this._ws?.terminate(); } catch { } if (!this._stopped) this._connect(); } _setStatus(status) { this.status = status; this.emit("status", status); } _url() { const url = new URL(this.brokerUrl); url.pathname = this.path; for (const [k, v] of Object.entries(this.params)) { if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, String(v)); } return url; } _connect() { if (this._stopped) return; this._setStatus("connecting"); const ws = new wrapper_default(this._url(), { headers: { Authorization: `Bearer ${this.token}` }, handshakeTimeout: 15e3, perMessageDeflate: false // 隧道里跑的是 TLS 密文,压缩纯属白烧 CPU }); this._ws = ws; let opened = false; ws.on("open", () => { opened = true; this.attempt = 0; this.lastError = null; this.connectedAt = Date.now(); const session = new Session(ws, { initiator: this.initiator, label: this.name, maxStreams: this.maxStreams }); this._session = session; session.on("warn", (m) => this.log.warn(`${this.name}: ${m}`)); session.on("close", (code, reason) => { this._session = null; this.connectedAt = null; this.log.warn(`${this.name} \u96A7\u9053\u65AD\u5F00 (${code}) ${reason}`.trim()); this._setStatus("offline"); this._schedule(); }); this.log.info(`${this.name} \u96A7\u9053\u5DF2\u5EFA\u7ACB`); this._setStatus("online"); this.emit("session", session); }); ws.on("unexpected-response", (_req, res) => { this.lastError = `HTTP ${res.statusCode}`; if (res.statusCode === 401) { this.log.error(`${this.name}: token \u88AB\u62D2\u7EDD (401)\uFF0C\u68C0\u67E5\u914D\u7F6E\u91CC\u7684 token \u662F\u5426\u548C\u670D\u52A1\u7AEF\u4E00\u81F4`); } else { this.log.warn(`${this.name}: \u63E1\u624B\u88AB\u62D2 HTTP ${res.statusCode}`); } res.resume(); ws.terminate(); }); ws.on("error", (err) => { this.lastError = err.message; if (!opened) this.log.warn(`${this.name}: \u8FDE\u4E0D\u4E0A ${this.brokerUrl} \u2014 ${err.message}`); }); ws.on("close", () => { if (!opened) { this._setStatus("offline"); this._schedule(); } }); } _schedule() { if (this._stopped || this._timer) return; this.attempt += 1; const base = Math.min(3e4, 1e3 * 2 ** (this.attempt - 1)); const delay = Math.round(base * (0.8 + Math.random() * 0.4)); this.log.info(`${this.name} ${(delay / 1e3).toFixed(1)}s \u540E\u91CD\u8FDE\uFF08\u7B2C ${this.attempt} \u6B21\uFF09`); this._timer = setTimeout(() => { this._timer = null; this._connect(); }, delay); } }; // packages/client/src/socks5.js var import_node_net3 = __toESM(require("node:net"), 1); // packages/client/src/udp.js var import_node_dgram = __toESM(require("node:dgram"), 1); var import_node_net2 = __toESM(require("node:net"), 1); var UdpAssociation = class { constructor({ stream, keys, log: log2, stats: stats2, clientHost }) { this.stream = stream; this.cipher = new DatagramCipher(keys); this.log = log2; this.stats = stats2; this.peer = null; this.expectHost = clientHost && clientHost !== "0.0.0.0" && clientHost !== "::" ? clientHost : null; this.socket = null; this.closed = false; } async listen(host = "127.0.0.1") { this.socket = import_node_dgram.default.createSocket({ type: import_node_net2.default.isIPv6(host) ? "udp6" : "udp4", ipv6Only: false }); await new Promise((resolve, reject) => { this.socket.once("error", reject); this.socket.bind(0, host, () => { this.socket.off("error", reject); resolve(); }); }); this.socket.on("message", (buf, rinfo) => this._fromClient(buf, rinfo)); this.socket.on("error", (err) => { this.log.debug(`\u672C\u5730 UDP \u53E3\u51FA\u9519: ${err.message}`); this.close(); }); this.stream.on("datagram", (payload) => this._fromTunnel(payload)); this.stream.once("close", () => this.close()); return this.socket.address(); } _fromClient(buf, rinfo) { const from = rinfo.address.startsWith("::ffff:") ? rinfo.address.slice(7) : rinfo.address; if (this.peer === null) { if (this.expectHost && from !== this.expectHost) return; this.peer = { host: from, port: rinfo.port }; this.log.debug(`UDP \u5173\u8054\u9501\u5B9A\u5BA2\u6237\u7AEF ${from}:${rinfo.port}`); } else if (from !== this.peer.host || rinfo.port !== this.peer.port) { return; } let host; let port; let data; try { ({ host, port, data } = decodeSocks5Udp(buf)); } catch { this.stats.udpDropped += 1; return; } this.stats.bytesUp += data.length; if (!this.stream.sendDatagram(this.cipher.seal(encodeDatagram(host, port, data)))) { this.stats.udpDropped += 1; } } _fromTunnel(payload) { if (this.closed || this.peer === null) return; const plain = this.cipher.open(payload); if (plain === null) { this.stats.udpDropped += 1; return; } let host; let port; let data; try { ({ host, port, data } = decodeDatagram(plain)); } catch { this.stats.udpDropped += 1; return; } this.stats.bytesDown += data.length; this.socket.send(encodeSocks5Udp(host, port, data), this.peer.port, this.peer.host, (err) => { if (err) this.stats.udpDropped += 1; }); } close() { if (this.closed) return; this.closed = true; try { this.socket?.close(); } catch { } if (!this.stream.destroyed) this.stream.destroy(); } }; // packages/client/src/caps.js var CAP_UDP = "udp"; var NODE_CAPS = [CAP_UDP]; function hasCap(lease, cap) { return Array.isArray(lease?.caps) && lease.caps.includes(cap); } // packages/client/src/socks5.js var UDP_ASSOCIATION_MARKER = 0; var VER = 5; var METHOD_NO_AUTH = 0; var METHOD_USERPASS = 2; var METHOD_NONE = 255; var CMD_CONNECT = 1; var CMD_UDP_ASSOCIATE = 3; var REP = { SUCCESS: 0, GENERAL: 1, NOT_ALLOWED: 2, NET_UNREACH: 3, HOST_UNREACH: 4, REFUSED: 5, TTL_EXPIRED: 6, CMD_UNSUPPORTED: 7, ATYP_UNSUPPORTED: 8 }; var CODE_TO_REP = { [E.NOT_ALLOWED]: REP.NOT_ALLOWED, [E.NET_UNREACH]: REP.NET_UNREACH, [E.HOST_UNREACH]: REP.HOST_UNREACH, [E.REFUSED]: REP.REFUSED, [E.TIMEOUT]: REP.TTL_EXPIRED, [E.NO_EXIT]: REP.NET_UNREACH, [E.EXIT_GONE]: REP.NET_UNREACH }; function readExactly(socket, n) { if (n === 0) return Promise.resolve(Buffer.alloc(0)); return new Promise((resolve, reject) => { const cleanup = () => { socket.off("readable", attempt); socket.off("end", onEnd); socket.off("close", onEnd); socket.off("error", onError); }; const attempt = () => { const chunk = socket.read(n); if (chunk) { cleanup(); resolve(chunk); } }; const onEnd = () => { cleanup(); reject(new Error("\u5BA2\u6237\u7AEF\u5728\u63E1\u624B\u4E2D\u9014\u65AD\u5F00")); }; const onError = (err) => { cleanup(); reject(err); }; socket.on("readable", attempt); socket.on("end", onEnd); socket.on("close", onEnd); socket.on("error", onError); attempt(); }); } function reply(socket, rep, host = "0.0.0.0", port = 0) { if (socket.destroyed || !socket.writable) return; const bnd = port > 0 ? encodeAddr(host, port) : Buffer.from([ATYP.IPV4, 0, 0, 0, 0, 0, 0]); socket.write(Buffer.concat([Buffer.from([VER, rep, 0]), bnd])); } function createSocks5Server({ open: open2, log: log2, stats: stats2, groupKey, leases: leases2, socksHost = "127.0.0.1", getRegion = () => "any" }) { async function handshake(socket) { const hello = await readExactly(socket, 2); if (hello[0] !== VER) throw new Error(`\u4E0D\u662F SOCKS5 (ver=${hello[0]})`); const methods = await readExactly(socket, hello[1]); let region = getRegion(); if (methods.includes(METHOD_USERPASS)) { socket.write(Buffer.from([VER, METHOD_USERPASS])); const head2 = await readExactly(socket, 2); if (head2[0] !== 1) throw new Error("\u7528\u6237\u540D\u8BA4\u8BC1\u5B50\u534F\u8BAE\u7248\u672C\u4E0D\u5BF9"); const uname = await readExactly(socket, head2[1]); const plen = await readExactly(socket, 1); await readExactly(socket, plen[0]); socket.write(Buffer.from([1, 0])); const u = uname.toString("utf8").trim(); if (u) region = u; } else if (methods.includes(METHOD_NO_AUTH)) { socket.write(Buffer.from([VER, METHOD_NO_AUTH])); } else { socket.write(Buffer.from([VER, METHOD_NONE])); throw new Error("\u5BA2\u6237\u7AEF\u4E0D\u652F\u6301\u4EFB\u4F55\u6211\u4EEC\u80FD\u63A5\u53D7\u7684\u8BA4\u8BC1\u65B9\u5F0F"); } const head = await readExactly(socket, 4); if (head[0] !== VER) throw new Error("\u8BF7\u6C42\u7248\u672C\u4E0D\u5BF9"); const cmd = head[1]; const atyp = head[3]; let body; if (atyp === ATYP.IPV4) { body = await readExactly(socket, 4); } else if (atyp === ATYP.IPV6) { body = await readExactly(socket, 16); } else if (atyp === ATYP.DOMAIN) { const len = await readExactly(socket, 1); body = Buffer.concat([len, await readExactly(socket, len[0])]); } else { reply(socket, REP.ATYP_UNSUPPORTED); throw new Error(`\u4E0D\u652F\u6301\u7684\u5730\u5740\u7C7B\u578B ${atyp}`); } const portBuf = await readExactly(socket, 2); if (cmd !== CMD_CONNECT && cmd !== CMD_UDP_ASSOCIATE) { reply(socket, REP.CMD_UNSUPPORTED); throw new Error(`\u4E0D\u652F\u6301\u7684\u547D\u4EE4 ${cmd}`); } const { host, port } = decodeAddr(Buffer.concat([Buffer.from([atyp]), body, portBuf])); return { cmd, host, port, region }; } async function openEncrypted(host, port, region, retry = true, datagram = false) { const lease = await leases2.get(region); const target = datagram ? Buffer.from([UDP_ASSOCIATION_MARKER]) : encodeAddr(host, port); const e2e = initiatorHandshake(groupKey, lease.pubkey, target); const stream = open2({ region, nodeId: lease.nodeId, blob: e2e.message1, datagram }); try { await new Promise((resolve, reject) => { stream.once("connect", resolve); stream.once("error", reject); }); } catch (err) { if (retry && err.code === E.LEASE_STALE) { log2.debug(`\u79DF\u7EA6\u5931\u6548\uFF08${lease.nodeId}\uFF09\uFF0C\u91CD\u65B0\u7533\u8BF7\u540E\u91CD\u8BD5`); leases2.invalidate(region); return openEncrypted(host, port, region, false, datagram); } throw err; } return { stream, keys: e2e.finish(stream.handshakeBlob), lease }; } async function handleAssociate(socket, host, region) { try { const lease = await leases2.get(region); if (!hasCap(lease, CAP_UDP)) { log2.warn( `\u51FA\u53E3\u8282\u70B9 ${lease.name || lease.nodeId} \u7248\u672C\u592A\u65E7\uFF0C\u4E0D\u652F\u6301 UDP\uFF0C\u62D2\u7EDD\u672C\u6B21 ASSOCIATE` ); reply(socket, REP.CMD_UNSUPPORTED); socket.destroy(); return; } } catch (err) { stats2.failed += 1; reply(socket, CODE_TO_REP[err.code] ?? REP.GENERAL); socket.destroy(); return; } let association; try { const { stream, keys } = await openEncrypted(null, null, region, true, true); association = new UdpAssociation({ stream, keys, log: log2, stats: stats2, clientHost: host }); const addr = await association.listen(socksHost); reply(socket, REP.SUCCESS, socksHost, addr.port); stats2.udpActive += 1; stats2.udpTotal += 1; log2.debug(`UDP \u5173\u8054\u5C31\u7EEA\uFF0C\u672C\u5730\u4E2D\u7EE7\u53E3 ${socksHost}:${addr.port}`); } catch (err) { stats2.failed += 1; log2.debug(`UDP \u5173\u8054\u5931\u8D25: ${err.message}`); reply(socket, CODE_TO_REP[err.code] ?? REP.GENERAL); socket.destroy(); association?.close(); return; } socket.on("close", () => { stats2.udpActive -= 1; association.close(); }); socket.resume(); } async function handle(socket) { const { cmd, host, port, region } = await handshake(socket); if (cmd === CMD_UDP_ASSOCIATE) { await handleAssociate(socket, host, region); return; } const target = `${host}:${port}`; let localGone = false; const markGone = () => { localGone = true; }; socket.once("close", markGone); let stream; let keys; try { ({ stream, keys } = await openEncrypted(host, port, region)); } catch (err) { stats2.failed += 1; if (err.noExit) log2.warn(`${target}: ${err.message}`); else log2.debug(`\u5F00\u6D41\u5931\u8D25 ${target}: ${err.message}`); reply(socket, CODE_TO_REP[err.code] ?? REP.GENERAL); socket.destroy(); return; } if (localGone) { stream.destroy(); return; } socket.off("close", markGone); reply(socket, REP.SUCCESS); stats2.active += 1; stats2.total += 1; if (stream.exitInfo) stats2.lastExit = stream.exitInfo; stats2.recent.push({ at: Date.now(), target, region }); if (stats2.recent.length > 50) stats2.recent.shift(); log2.debug(`SOCKS5 ${target} (${region}) \u2192 ${stream.exitInfo ?? "?"}`); relaySecure(socket, stream, keys, { onClose: () => { stats2.active -= 1; stats2.bytesUp += stream.bytesOut; stats2.bytesDown += stream.bytesIn; } }); } const server = import_node_net3.default.createServer({ allowHalfOpen: true }, (socket) => { socket.setNoDelay(true); socket.on("error", () => { }); handle(socket).catch((err) => { log2.debug(`SOCKS5 \u63E1\u624B\u5931\u8D25: ${err.message}`); socket.destroy(); }); }); return server; } // packages/client/src/exit.js var import_node_net5 = __toESM(require("node:net"), 1); // packages/client/src/guard.js var import_node_net4 = __toESM(require("node:net"), 1); var BLOCKED_PORTS = /* @__PURE__ */ new Set([ 0, 25, // SMTP 465, // SMTPS 587, // Submission 2525, // 备用 SMTP 135, // MSRPC 137, 138, 139, // NetBIOS 445 // SMB ]); var V4_BLOCKS = [ ["0.0.0.0", 8], // 本网络 ["10.0.0.0", 8], // 私有 ["100.64.0.0", 10], // 运营商级 NAT ["127.0.0.0", 8], // 回环 ["169.254.0.0", 16], // 链路本地(含云厂商 169.254.169.254 元数据接口) ["172.16.0.0", 12], // 私有 ["192.0.0.0", 24], // IETF 协议专用 ["192.0.2.0", 24], // TEST-NET-1 ["192.168.0.0", 16], // 私有 ["198.18.0.0", 15], // 基准测试段,也是 Clash/Mihomo 的 fake-ip 池 ["198.51.100.0", 24], // TEST-NET-2 ["203.0.113.0", 24], // TEST-NET-3 ["224.0.0.0", 4], // 组播 ["240.0.0.0", 4] // 保留(含 255.255.255.255) ]; var V6_BLOCKS = [ ["::1", 128], // 回环 ["64:ff9b::", 96], // NAT64 ["100::", 64], // 丢弃前缀 ["2001:db8::", 32], // 文档示例 ["fc00::", 7], // 唯一本地地址 ["fe80::", 10], // 链路本地 ["ff00::", 8] // 组播 ]; var V4_TABLE = V4_BLOCKS.map(([addr, bits]) => [ipv4ToBytes(addr), bits]); var V6_TABLE = V6_BLOCKS.map(([addr, bits]) => [ipv6ToBytes(addr), bits]); function inBlock(addr, netAddr, bits) { const fullBytes = bits >> 3; const restBits = bits & 7; for (let i = 0; i < fullBytes; i += 1) { if (addr[i] !== netAddr[i]) return false; } if (restBits === 0) return true; const mask = 255 << 8 - restBits & 255; return (addr[fullBytes] & mask) === (netAddr[fullBytes] & mask); } function v4Embedded(bytes) { for (let i = 0; i < 10; i += 1) { if (bytes[i] !== 0) return null; } const marker = bytes.readUInt16BE(10); if (marker !== 65535 && marker !== 0) return null; return bytes.subarray(12); } function isBlockedIp(ip) { const kind = import_node_net4.default.isIP(ip); if (kind === 4) { const b = ipv4ToBytes(ip); return V4_TABLE.some(([n, bits]) => inBlock(b, n, bits)); } if (kind === 6) { const b = ipv6ToBytes(ip); const embedded = v4Embedded(b); if (embedded) return V4_TABLE.some(([n, bits]) => inBlock(embedded, n, bits)); return V6_TABLE.some(([n, bits]) => inBlock(b, n, bits)); } return true; } function isBlockedPort(port, allowed) { if (!Number.isInteger(port) || port < 1 || port > 65535) return true; if (allowed && allowed.length > 0) return !allowed.includes(port); return BLOCKED_PORTS.has(port); } var GuardError = class extends Error { constructor(code, message) { super(message); this.name = "GuardError"; this.code = code; } }; async function resolveTarget(host, port, { resolver: resolver2, blocklist: blocklist2, allowedPorts: allowedPorts2, maxAddrs = 3 } = {}) { if (isBlockedPort(port, allowedPorts2)) { throw new GuardError(E.NOT_ALLOWED, `\u7AEF\u53E3 ${port} \u4E0D\u5728\u672C\u8282\u70B9\u7684\u51FA\u53E3\u653E\u884C\u8303\u56F4\u5185`); } if (blocklist2) { const hit = blocklist2.check(host); if (hit) throw new GuardError(E.NOT_ALLOWED, `${host} \u547D\u4E2D\u672C\u8282\u70B9\u9ED1\u540D\u5355\u89C4\u5219 ${hit}`); } if (import_node_net4.default.isIP(host)) { if (isBlockedIp(host)) { throw new GuardError(E.NOT_ALLOWED, `\u76EE\u6807 ${host} \u662F\u5185\u7F51/\u4FDD\u7559\u5730\u5740\uFF0C\u62D2\u7EDD\u51FA\u53E3`); } return [host]; } if (!host || host.length > 253) { throw new GuardError(E.HOST_UNREACH, `\u975E\u6CD5\u57DF\u540D: ${String(host).slice(0, 40)}`); } let addresses; try { addresses = await resolver2.resolve(host); } catch (err) { throw new GuardError(E.HOST_UNREACH, `\u89E3\u6790 ${host} \u5931\u8D25: ${err.code ?? err.message}`); } const allowed = addresses.filter((ip) => !isBlockedIp(ip)); if (allowed.length === 0) { throw new GuardError( E.NOT_ALLOWED, `${host} \u89E3\u6790\u5230\u7684\u5168\u662F\u5185\u7F51/\u4FDD\u7559\u5730\u5740\uFF08${addresses.join(", ") || "\u7A7A"}\uFF09\uFF0C\u62D2\u7EDD\u51FA\u53E3` ); } return allowed.slice(0, maxAddrs); } // packages/client/src/exit.js var ERRNO_TO_CODE = { ECONNREFUSED: E.REFUSED, EHOSTUNREACH: E.HOST_UNREACH, ENETUNREACH: E.NET_UNREACH, ENOTFOUND: E.HOST_UNREACH, ETIMEDOUT: E.TIMEOUT, ECONNRESET: E.REFUSED }; function interleaveFamilies(addrs) { const v6 = addrs.filter((a) => import_node_net5.default.isIPv6(a)); const v4 = addrs.filter((a) => !import_node_net5.default.isIPv6(a)); const out = []; while (v6.length > 0 || v4.length > 0) { if (v6.length > 0) out.push(v6.shift()); if (v4.length > 0) out.push(v4.shift()); } return out; } function connectHappy(addrs, port, { timeoutMs, staggerMs = 250 }) { const list = interleaveFamilies(addrs); return new Promise((resolve, reject) => { if (list.length === 0) { reject(Object.assign(new Error("\u6CA1\u6709\u53EF\u7528\u5730\u5740"), { code: E.HOST_UNREACH })); return; } const sockets = []; const timers = []; let settled = false; let started = 0; let failed = 0; let lastErr = null; const cleanup = (winner) => { for (const t of timers) clearTimeout(t); timers.length = 0; for (const s of sockets) { if (s !== winner && !s.destroyed) s.destroy(); } }; const succeed = (socket) => { if (settled) { socket.destroy(); return; } settled = true; cleanup(socket); socket.on("error", () => { }); resolve(socket); }; const fail = (err) => { if (settled) return; settled = true; cleanup(null); reject(err); }; const attempt = (index) => { if (settled || index >= list.length) return; started = Math.max(started, index + 1); const ip = list[index]; const socket = import_node_net5.default.connect({ host: ip, port, allowHalfOpen: true }); sockets.push(socket); const onError = (err) => { lastErr = Object.assign(new Error(`\u8FDE\u63A5 ${ip}:${port} \u5931\u8D25: ${err.code ?? err.message}`), { code: ERRNO_TO_CODE[err.code] ?? E.GENERAL }); socket.destroy(); failed += 1; if (settled) return; if (started < list.length) attempt(started); else if (failed >= list.length) fail(lastErr); }; socket.once("error", onError); socket.once("connect", () => { socket.off("error", onError); succeed(socket); }); }; for (let i = 1; i < list.length; i += 1) { const t = setTimeout(() => attempt(i), i * staggerMs); t.unref?.(); timers.push(t); } const overall = setTimeout( () => fail(Object.assign(new Error(`\u8FDE\u63A5 ${port} \u7AEF\u53E3\u8D85\u65F6`), { code: E.TIMEOUT })), timeoutMs ); overall.unref?.(); timers.push(overall); attempt(0); }); } function createExitHandler({ log: log2, stats: stats2, /** 一组可接受的群组密钥,为了平滑换钥。见 protocol/crypto.js。 */ groupKeys, identity: identity2, bucket, resolver: resolver2, blocklist: blocklist2, audit: audit2, allowedPorts: allowedPorts2, connectTimeoutMs = 1e4, maxConcurrent = 64 }) { return async function handleStream(stream, info) { let gone = false; const markGone = () => { gone = true; }; stream.on("error", markGone); stream.on("close", markGone); let handshake; let host; let port; try { if (!info.blob) throw new Error("\u5BF9\u7AEF\u6CA1\u6709\u542F\u7528\u7AEF\u5230\u7AEF\u52A0\u5BC6"); handshake = responderHandshake(groupKeys, identity2, info.blob); ({ host, port } = decodeAddr(handshake.target, 0)); } catch (err) { stats2.failed += 1; log2.warn(`\u7AEF\u5230\u7AEF\u63E1\u624B\u5931\u8D25: ${err.message}`); stream.reject(E.NOT_ALLOWED, "\u63E1\u624B\u5931\u8D25"); return; } const target = `${host}:${port}`; if (stats2.active >= maxConcurrent) { stats2.failed += 1; stream.reject(E.GENERAL, `\u672C\u8282\u70B9\u5E76\u53D1\u5DF2\u6EE1\uFF08${maxConcurrent}\uFF09`); return; } let addrs; try { addrs = await resolveTarget(host, port, { resolver: resolver2, blocklist: blocklist2, allowedPorts: allowedPorts2 }); } catch (err) { if (gone) return; if (err.code === E.NOT_ALLOWED) { stats2.blocked += 1; log2.warn(`\u51FA\u53E3\u7B56\u7565\u62E6\u622A ${target}: ${err.message}`); } else { stats2.failed += 1; log2.debug(`\u89E3\u6790\u5931\u8D25 ${target}: ${err.message}`); } stream.reject(err.code ?? E.GENERAL, err.message); return; } if (gone) return; let socket; try { socket = await connectHappy(addrs, port, { timeoutMs: connectTimeoutMs }); } catch (err) { if (gone) return; stats2.failed += 1; log2.debug(`\u5EFA\u8FDE\u5931\u8D25 ${target}: ${err.message}`); stream.reject(err.code ?? E.HOST_UNREACH, err.message); return; } if (gone) { socket.destroy(); return; } socket.setNoDelay(true); stats2.active += 1; stats2.total += 1; stream.accept("", handshake.message2); log2.debug(`\u51FA\u53E3 ${target} \u2192 ${socket.remoteAddress}`); const startedAt = Date.now(); const peerIp = socket.remoteAddress; relaySecure(socket, stream, handshake.keys, { bucket, // 全节点共用一个令牌桶,限的是替别人转发的总流量 onClose: (err) => { stats2.active -= 1; stats2.bytesUp += stream.bytesIn; stats2.bytesDown += stream.bytesOut; audit2?.write({ host, port, ip: peerIp, up: stream.bytesIn, down: stream.bytesOut, ms: Date.now() - startedAt, err: err ? String(err.message).slice(0, 120) : void 0 }); } }); }; } // packages/client/src/exit-udp.js var import_node_dgram2 = __toESM(require("node:dgram"), 1); var import_node_net6 = __toESM(require("node:net"), 1); var MAX_TARGETS = 256; var IDLE_TIMEOUT_MS = 12e4; function createAssociationHandler({ log: log2, stats: stats2, groupKeys, identity: identity2, bucket, resolver: resolver2, blocklist: blocklist2, audit: audit2, allowedPorts: allowedPorts2, maxAssociations = 16 }) { let active = 0; return async function handleAssociation(stream, info) { let gone = false; stream.on("error", () => { gone = true; }); stream.on("close", () => { gone = true; }); let handshake; try { if (!info.blob) throw new Error("\u5BF9\u7AEF\u6CA1\u6709\u542F\u7528\u7AEF\u5230\u7AEF\u52A0\u5BC6"); handshake = responderHandshake(groupKeys, identity2, info.blob); } catch (err) { stats2.failed += 1; log2.warn(`UDP \u5173\u8054\u63E1\u624B\u5931\u8D25: ${err.message}`); stream.reject(E.NOT_ALLOWED, "\u63E1\u624B\u5931\u8D25"); return; } if (active >= maxAssociations) { stats2.failed += 1; stream.reject(E.GENERAL, `UDP \u5173\u8054\u6570\u5DF2\u6EE1\uFF08${maxAssociations}\uFF09`); return; } let socket; try { socket = import_node_dgram2.default.createSocket({ type: "udp6", ipv6Only: false, reuseAddr: false }); await new Promise((resolve, reject) => { socket.once("error", reject); socket.bind(0, () => { socket.off("error", reject); resolve(); }); }); } catch (err) { stats2.failed += 1; log2.warn(`UDP \u5173\u8054\u5EFA\u4E0D\u8D77\u6765: ${err.message}`); stream.reject(E.GENERAL, "UDP socket \u5EFA\u7ACB\u5931\u8D25"); return; } if (gone) { socket.close(); return; } active += 1; stats2.udpActive += 1; stats2.udpTotal += 1; const cipher = new DatagramCipher(handshake.keys); const targets = /* @__PURE__ */ new Map(); let lastActivity = Date.now(); let bytesUp = 0; let bytesDown = 0; const idleTimer = setInterval(() => { if (Date.now() - lastActivity > IDLE_TIMEOUT_MS) { log2.debug("UDP \u5173\u8054\u7A7A\u95F2\u8D85\u65F6\uFF0C\u56DE\u6536"); stream.destroy(); } }, 3e4); idleTimer.unref?.(); const cleanup = () => { clearInterval(idleTimer); try { socket.close(); } catch { } active -= 1; stats2.udpActive -= 1; audit2?.write({ host: "udp-association", port: 0, ip: "", up: bytesUp, down: bytesDown, ms: Date.now() - (lastActivity - 0), targets: targets.size }); }; stream.once("close", cleanup); stream.on("datagram", async (payload) => { lastActivity = Date.now(); const plain = cipher.open(payload); if (plain === null) { stats2.udpDropped += 1; return; } let host; let port; let data; try { ({ host, port, data } = decodeDatagram(plain)); } catch { stats2.udpDropped += 1; return; } if (isBlockedPort(port, allowedPorts2)) { stats2.blocked += 1; return; } if (blocklist2 && !import_node_net6.default.isIP(host) && blocklist2.check(host)) { stats2.blocked += 1; return; } let ip = host; if (!import_node_net6.default.isIP(host)) { try { const addrs = await resolver2.resolve(host); ip = addrs.find((a) => !isBlockedIp(a)); } catch { stats2.udpDropped += 1; return; } } if (!ip || isBlockedIp(ip)) { stats2.blocked += 1; return; } if (targets.size >= MAX_TARGETS && !targets.has(`${ip}:${port}`)) { stats2.udpDropped += 1; return; } if (bucket && !bucket.unlimited) { if (!bucket.tryTake(data.length)) { stats2.udpDropped += 1; return; } } targets.set(`${ip}:${port}`, Date.now()); bytesUp += data.length; stats2.bytesUp += data.length; const dest = import_node_net6.default.isIPv4(ip) ? `::ffff:${ip}` : ip; socket.send(data, port, dest, (err) => { if (err) { stats2.udpDropped += 1; log2.debug(`\u53D1\u5F80 ${ip}:${port} \u5931\u8D25: ${err.message}`); } }); }); socket.on("message", (data, rinfo) => { lastActivity = Date.now(); const from = rinfo.address.startsWith("::ffff:") ? rinfo.address.slice(7) : rinfo.address; if (!targets.has(`${from}:${rinfo.port}`)) { stats2.udpDropped += 1; return; } if (bucket && !bucket.unlimited && !bucket.tryTake(data.length)) { stats2.udpDropped += 1; return; } bytesDown += data.length; stats2.bytesDown += data.length; if (!stream.sendDatagram(cipher.seal(encodeDatagram(from, rinfo.port, data)))) { stats2.udpDropped += 1; } }); socket.on("error", (err) => { log2.debug(`UDP socket \u51FA\u9519: ${err.message}`); stream.destroy(); }); stream.accept("", handshake.message2); log2.debug(`UDP \u5173\u8054\u5DF2\u5EFA\u7ACB\uFF08\u672C\u5730\u7AEF\u53E3 ${socket.address().port}\uFF09`); }; } // packages/client/src/panel.js var import_node_http = __toESM(require("node:http"), 1); var import_node_fs2 = __toESM(require("node:fs"), 1); var import_node_path2 = __toESM(require("node:path"), 1); var import_node_crypto3 = __toESM(require("node:crypto"), 1); var import_node_url = require("node:url"); var here = import_node_path2.default.dirname((0, import_node_url.fileURLToPath)(__import_meta_url)); var INDEX_HTML = import_node_path2.default.join(here, "public", "index.html"); var BUNDLED_HTML = true ? ` native-vpn \u63A7\u5236\u53F0

native-vpn \u63A7\u5236\u53F0

\u52A0\u8F7D\u4E2D\u2026

\u96A7\u9053\u72B6\u6001

\u6D88\u8D39\u94FE\u8DEF\uFF08\u7528\u522B\u4EBA\u7684 IP\uFF09
\u2014
\u51FA\u53E3\u94FE\u8DEF\uFF08\u501F\u51FA\u6211\u7684 IP\uFF09
\u2014
\u5F53\u524D\u51FA\u53E3
\u2014
\u96A7\u9053\u5EF6\u8FDF
\u2014

\u8BBE\u7F6E

\u51FA\u53E3\u5730\u533A
\u51FA\u53E3\u9650\u901F\uFF08KB/s\uFF0C0 = \u4E0D\u9650\uFF09
\u5171\u4EAB\u6211\u7684\u7F51\u7EDC\u5F53\u51FA\u53E3\u8282\u70B9
\u5F00\u542F\u5171\u4EAB\u540E\uFF0C\u6C60\u5B50\u91CC\u5176\u4ED6\u4EBA\u7684\u6D41\u91CF\u4F1A\u4ECE\u4F60\u5BB6\u7684 IP \u53D1\u51FA\u53BB\u3002\u5728\u4F60\u7684 ISP \u548C\u88AB\u8BBF\u95EE\u7684\u7F51\u7AD9\u770B\u6765\uFF0C \u90A3\u4E9B\u8BF7\u6C42\u5C31\u662F\u4F60\u53D1\u7684\u3002\u5BA2\u6237\u7AEF\u5DF2\u5F3A\u5236\u5C4F\u853D\u5185\u7F51\u5730\u5740\u548C SMTP \u7AEF\u53E3\uFF0C\u4F46\u4ECD\u8BF7\u53EA\u5728\u4F60\u4FE1\u4EFB\u6C60\u5B50\u91CC\u7684\u4EBA\u65F6\u5F00\u542F\u3002

\u6D41\u91CF

\u6211\u7528\u6389\u7684\uFF08\u4E0A\u884C / \u4E0B\u884C\uFF09
\u2014
\u6211\u8D21\u732E\u7684\uFF08\u4E0A\u884C / \u4E0B\u884C\uFF09
\u2014
\u6D3B\u8DC3\u8FDE\u63A5\uFF08\u7528 / \u501F\uFF09
\u2014
UDP \u5173\u8054\uFF08\u7528 / \u501F\uFF09
\u2014
\u51FA\u53E3\u62E6\u622A\u6B21\u6570
\u2014

\u6C60\u5185\u8282\u70B9

\u8282\u70B9\u5730\u533A\u51FA\u53E3 IP\u8EAB\u4EFD\u6307\u7EB9\u5EF6\u8FDF\u5E76\u53D1\u6210\u529F\u7387

\u6700\u8FD1\u8FDE\u63A5

\u65F6\u95F4\u76EE\u6807\u5730\u533A

\u600E\u4E48\u7528

SOCKS5 \u4EE3\u7406\u5730\u5740 127.0.0.1:1080
\u9A8C\u8BC1\u51FA\u53E3 IP\uFF1Acurl --socks5 127.0.0.1:1080 https://ifconfig.me
\u4E34\u65F6\u6307\u5B9A\u5730\u533A\uFF08\u7528\u6237\u540D\u4F20\u56FD\u5BB6\u7801\uFF09\uFF1Acurl --socks5 JP:x@127.0.0.1:1080 https://ifconfig.me
` : null; function readIndexHtml() { return BUNDLED_HTML !== null ? Buffer.from(BUNDLED_HTML, "utf8") : import_node_fs2.default.readFileSync(INDEX_HTML); } function json(res, code, body) { const buf = Buffer.from(JSON.stringify(body), "utf8"); res.writeHead(code, { "content-type": "application/json; charset=utf-8", "content-length": buf.length, "cache-control": "no-store" }); res.end(buf); } function readBody(req, limit = 64 * 1024) { return new Promise((resolve, reject) => { const chunks = []; let size = 0; req.on("data", (c) => { size += c.length; if (size > limit) { reject(new Error("\u8BF7\u6C42\u4F53\u8FC7\u5927")); req.destroy(); return; } chunks.push(c); }); req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); req.on("error", reject); }); } function createPanel({ state: state2, config: config2, actions: actions2, log: log2 }) { const allowedHosts = /* @__PURE__ */ new Set([ `127.0.0.1:${config2.panelPort}`, `localhost:${config2.panelPort}`, `[::1]:${config2.panelPort}` ]); const authOff = config2.panelToken === "off"; const expected = Buffer.from(String(config2.panelToken)); const tokenOk = (given) => { if (authOff) return true; const got = Buffer.from(String(given ?? "")); return got.length === expected.length && import_node_crypto3.default.timingSafeEqual(got, expected); }; const server = import_node_http.default.createServer(async (req, res) => { const host = String(req.headers.host ?? ""); if (!allowedHosts.has(host)) { json(res, 403, { error: "bad host", hint: `\u8BF7\u7528 http://127.0.0.1:${config2.panelPort}` }); return; } const url = new URL(req.url, `http://${host}`); if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/index.html")) { if (!tokenOk(url.searchParams.get("t"))) { json(res, 401, { error: "unauthorized", hint: "\u5E26\u4E0A ?t=\u3002\u5B8C\u6574\u5730\u5740\u5728\u542F\u52A8\u65E5\u5FD7\u91CC\uFF0C\u6216\u8005\u770B config.json\u3002" }); return; } let html; try { html = readIndexHtml(); } catch { json(res, 500, { error: "\u9762\u677F\u9875\u9762\u7F3A\u5931" }); return; } res.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": html.length }); res.end(html); return; } if (url.pathname.startsWith("/api/") && !tokenOk(req.headers["x-panel-token"])) { json(res, 401, { error: "unauthorized" }); return; } if (req.method === "GET" && url.pathname === "/api/state") { json(res, 200, state2.snapshot()); return; } if (req.method === "GET" && url.pathname === "/api/nodes") { try { json(res, 200, await actions2.fetchNodes()); } catch (err) { json(res, 502, { error: err.message }); } return; } if (req.method === "POST" && url.pathname === "/api/settings") { if (req.headers["x-panel"] !== "1") { json(res, 403, { error: "\u7F3A\u5C11 X-Panel \u5934" }); return; } try { const patch = JSON.parse(await readBody(req) || "{}"); const applied = await actions2.applySettings(patch); json(res, 200, { ok: true, config: applied }); } catch (err) { log2.warn(`\u9762\u677F\u6539\u914D\u7F6E\u5931\u8D25: ${err.message}`); json(res, 400, { error: err.message }); } return; } json(res, 404, { error: "not found" }); }); return server; } // packages/client/src/selfcheck.js var PROBES = ["www.cloudflare.com", "ifconfig.me", "one.one.one.one"]; async function checkExitViability(resolver2) { const results = []; for (const host of PROBES) { try { const addrs = await resolver2.resolve(host); results.push({ host, addrs, ok: addrs.some((a) => !isBlockedIp(a)) }); } catch (err) { results.push({ host, addrs: [], ok: false, error: err.code ?? err.message }); } } const healthy = results.filter((r) => r.ok).length; return { ok: healthy > 0, results }; } function explainFailure(check) { const lines = ["\u672C\u673A DNS \u89E3\u6790\u51FA\u6765\u7684\u5168\u662F\u5185\u7F51/\u4FDD\u7559\u5730\u5740\uFF0C\u4E0D\u80FD\u4F5C\u4E3A\u51FA\u53E3\u8282\u70B9\uFF1A"]; for (const r of check.results) { lines.push(` ${r.host} \u2192 ${r.addrs.join(", ") || r.error || "\u65E0\u7ED3\u679C"}`); } lines.push(" \u6700\u5E38\u89C1\u7684\u539F\u56E0\u662F Clash/Mihomo \u5F00\u4E86 fake-ip \u6A21\u5F0F\uFF08198.18.0.0/15 \u5C31\u662F\u5B83\u7684\u5730\u5740\u6C60\uFF09\u3002"); lines.push(" \u8FD9\u79CD\u673A\u5668\u5F3A\u884C\u52A0\u5165\u6C60\u5B50\u4E5F\u6CA1\u610F\u4E49\uFF1A\u6D41\u91CF\u8FD8\u8981\u518D\u8FC7\u4E00\u6B21\u4F60\u81EA\u5DF1\u7684\u4EE3\u7406\uFF0C"); lines.push(" \u5BF9\u5916\u9732\u51FA\u7684\u662F\u4EE3\u7406\u670D\u52A1\u5546\u7684 IP\uFF0C\u4E0D\u662F\u4F60\u5BB6\u5BBD\u5E26\u7684 IP\u3002"); lines.push(" \u6539\u6210 redir-host \u6A21\u5F0F\u6216\u9000\u51FA\u4EE3\u7406\u540E\u91CD\u8BD5\uFF1B\u786E\u5B9A\u8981\u5F3A\u884C\u52A0\u5165\u5C31\u52A0 --force-exit true\u3002"); return lines; } // packages/client/src/known-nodes.js var import_node_fs3 = __toESM(require("node:fs"), 1); var import_node_path3 = __toESM(require("node:path"), 1); var FILE = import_node_path3.default.join(HOME, "known-nodes.json"); var KnownNodes = class { constructor() { this.map = /* @__PURE__ */ new Map(); this.load(); } load() { try { const raw = JSON.parse(import_node_fs3.default.readFileSync(FILE, "utf8")); for (const [nodeId, rec] of Object.entries(raw)) this.map.set(nodeId, rec); } catch (err) { if (err.code !== "ENOENT") throw new Error(`${FILE} \u635F\u574F: ${err.message}`); } } save() { const obj = Object.fromEntries(this.map); import_node_fs3.default.writeFileSync(FILE, `${JSON.stringify(obj, null, 2)} `, { mode: 384 }); } /** * 校验并(首次见到时)记录。 * @returns {{ status: 'new'|'known', record: object }} * @throws 公钥变了就抛,调用方必须当成致命错误 */ verify(nodeId, pubkey, name = "") { const known = this.map.get(nodeId); if (!known) { const record = { pubkey, name, firstSeen: Date.now() }; this.map.set(nodeId, record); this.save(); return { status: "new", record }; } if (known.pubkey !== pubkey) { throw new Error( `\u51FA\u53E3\u8282\u70B9 ${nodeId} \u7684\u8EAB\u4EFD\u516C\u94A5\u53D8\u4E86\uFF01 \u8BB0\u5F55\u7684: ${fingerprint(known.pubkey)}\uFF08\u9996\u6B21\u89C1\u4E8E ${new Date(known.firstSeen).toLocaleString("zh-CN")}\uFF09 \u73B0\u5728\u7684: ${fingerprint(pubkey)} \u8981\u4E48\u5BF9\u65B9\u91CD\u88C5\u4E86\u5BA2\u6237\u7AEF\uFF08\u6362\u673A\u5668/\u5220\u8FC7\u914D\u7F6E\uFF09\uFF0C\u8981\u4E48 broker \u5728\u5192\u5145\u5B83\u505A\u4E2D\u95F4\u4EBA\u3002 \u786E\u8BA4\u662F\u524D\u8005\u7684\u8BDD\uFF0C\u5220\u6389 ${FILE} \u91CC\u8FD9\u4E00\u6761\u518D\u8FDE\u3002` ); } if (name && known.name !== name) { known.name = name; this.save(); } return { status: "known", record: known }; } list() { return [...this.map.entries()].map(([nodeId, r]) => ({ nodeId, name: r.name, fingerprint: fingerprint(r.pubkey), firstSeen: r.firstSeen })); } forget(nodeId) { const ok = this.map.delete(nodeId); if (ok) this.save(); return ok; } }; var KNOWN_NODES_FILE = FILE; // packages/client/src/lease.js var LeaseManager = class _LeaseManager { constructor({ brokerUrl, token, nodeId, knownNodes: knownNodes2, log: log2, timeoutMs = 1e4 }) { this.base = brokerUrl.replace(/^ws/, "http"); this.token = token; this.nodeId = nodeId; this.knownNodes = knownNodes2; this.log = log2; this.timeoutMs = timeoutMs; this.cache = /* @__PURE__ */ new Map(); this.inflight = /* @__PURE__ */ new Map(); } static normRegion(region) { const r = String(region ?? "").trim().toUpperCase(); return r === "" ? "ANY" : r; } get(region) { const key = _LeaseManager.normRegion(region); const hit = this.cache.get(key); if (hit && hit.expiresAt - 3e4 > Date.now()) return Promise.resolve(hit); const flying = this.inflight.get(key); if (flying) return flying; const p = this._fetch(key).finally(() => this.inflight.delete(key)); this.inflight.set(key, p); return p; } invalidate(region) { this.cache.delete(_LeaseManager.normRegion(region)); } clear() { this.cache.clear(); } async _fetch(region) { const url = new URL("/api/lease", this.base); url.searchParams.set("region", region); url.searchParams.set("nodeId", this.nodeId); const res = await fetch(url, { headers: { Authorization: `Bearer ${this.token}` }, signal: AbortSignal.timeout(this.timeoutMs) }); if (res.status === 503) { throw Object.assign(new Error(`\u6CA1\u6709\u53EF\u7528\u7684\u51FA\u53E3\u8282\u70B9\uFF08\u5730\u533A ${region}\uFF09`), { noExit: true }); } if (!res.ok) throw new Error(`\u7533\u8BF7\u79DF\u7EA6\u5931\u8D25: HTTP ${res.status}`); const lease = await res.json(); if (!lease.nodeId || !lease.pubkey) throw new Error("broker \u8FD4\u56DE\u7684\u79DF\u7EA6\u7F3A\u5C11\u8282\u70B9\u516C\u94A5"); const { status } = this.knownNodes.verify(lease.nodeId, lease.pubkey, lease.name); if (status === "new") { this.log.info( `\u65B0\u51FA\u53E3\u8282\u70B9 ${lease.name || lease.nodeId} (${lease.country})\u3000\u6307\u7EB9 ${fingerprint(lease.pubkey)}` ); } this.cache.set(region, lease); this.log.debug(`\u79DF\u7EA6 ${region} \u2192 ${lease.name || lease.nodeId} (${lease.country})`); return lease; } snapshot() { return [...this.cache.entries()].map(([region, l]) => ({ region, nodeId: l.nodeId, name: l.name, country: l.country, ip: l.ip, fingerprint: fingerprint(l.pubkey), expiresAt: l.expiresAt })); } }; // packages/client/src/dns.js var import_node_dns = __toESM(require("node:dns"), 1); var import_node_dns2 = require("node:dns"); var TTL_MIN_MS = 1e4; var TTL_MAX_MS = 3e5; var RESOLVER_BROKEN = /* @__PURE__ */ new Set([ "ESERVFAIL", "ECONNREFUSED", "ETIMEOUT", "EREFUSED", "EBADRESP", "ENOTINITIALIZED", "ENOSERVER" ]); var DnsResolver = class { constructor({ servers = [], timeoutMs = 5e3, cacheMax = 2e3, log: log2 } = {}) { this.log = log2; this.cacheMax = cacheMax; this.cache = /* @__PURE__ */ new Map(); this.inflight = /* @__PURE__ */ new Map(); this.resolver = new import_node_dns2.promises.Resolver({ timeout: timeoutMs, tries: 2 }); if (servers.length > 0) { this.resolver.setServers(servers); this.log?.info(`DNS \u670D\u52A1\u5668: ${servers.join(", ")}`); } this.servers = this.resolver.getServers(); } _cacheGet(host) { const hit = this.cache.get(host); if (!hit) return null; if (hit.expiresAt <= Date.now()) { this.cache.delete(host); return null; } return hit.addrs; } _cacheSet(host, addrs, ttlSec) { if (this.cache.size >= this.cacheMax) { const oldest = this.cache.keys().next().value; this.cache.delete(oldest); } const ttl = Math.min(TTL_MAX_MS, Math.max(TTL_MIN_MS, (ttlSec || 0) * 1e3)); this.cache.set(host, { addrs, expiresAt: Date.now() + ttl }); } /** @returns {Promise} v6 在前 v4 在后;真正的拨号顺序由 Happy Eyeballs 决定 */ resolve(host) { const cached = this._cacheGet(host); if (cached) return Promise.resolve(cached); const flying = this.inflight.get(host); if (flying) return flying; const p = this._resolve(host).finally(() => this.inflight.delete(host)); this.inflight.set(host, p); return p; } async _resolve(host) { const [v6, v4] = await Promise.allSettled([ this.resolver.resolve6(host, { ttl: true }), this.resolver.resolve4(host, { ttl: true }) ]); const records = []; for (const r of [v6, v4]) { if (r.status === "fulfilled") records.push(...r.value); } if (records.length > 0) { const addrs = records.map((r) => r.address); const minTtl = Math.min(...records.map((r) => r.ttl ?? 0).filter((t) => t > 0), 300); this._cacheSet(host, addrs, minTtl); return addrs; } const codes = [v6, v4].filter((r) => r.status === "rejected").map((r) => r.reason?.code); if (codes.some((c) => RESOLVER_BROKEN.has(c))) { this.log?.warn(`c-ares \u89E3\u6790 ${host} \u5931\u8D25 (${codes.join("/")})\uFF0C\u56DE\u843D\u5230\u7CFB\u7EDF\u89E3\u6790\u5668`); const fallback = await import_node_dns2.promises.lookup(host, { all: true, verbatim: true }); const addrs = fallback.map((r) => r.address); if (addrs.length > 0) { this._cacheSet(host, addrs, 30); return addrs; } } const err = new Error(`\u89E3\u6790 ${host} \u5931\u8D25: ${codes.join("/") || "NODATA"}`); err.code = codes[0] ?? import_node_dns.default.NOTFOUND; throw err; } stats() { return { cached: this.cache.size, servers: this.servers }; } }; // packages/client/src/blocklist.js var import_node_fs4 = __toESM(require("node:fs"), 1); var import_node_path4 = __toESM(require("node:path"), 1); var BLOCKLIST_FILE = import_node_path4.default.join(HOME, "blocklist.txt"); var TEMPLATE = `# \u51FA\u53E3\u57DF\u540D\u9ED1\u540D\u5355\u3002\u8FD9\u53F0\u673A\u5668\u4F5C\u4E3A\u51FA\u53E3\u8282\u70B9\u65F6\uFF0C\u62D2\u7EDD\u66FF\u522B\u4EBA\u8FDE\u63A5\u4E0B\u9762\u8FD9\u4E9B\u57DF\u540D\u3002 # # \u7AEF\u5230\u7AEF\u52A0\u5BC6\u4E4B\u540E broker \u5DF2\u7ECF\u770B\u4E0D\u5230\u76EE\u6807\u57DF\u540D\u4E86\uFF0C\u6240\u4EE5\u8FD9\u7C7B\u7B56\u7565\u53EA\u80FD\u505A\u5728\u51FA\u53E3\u4FA7\u2014\u2014 # \u4E5F\u5C31\u662F\u8FD9\u91CC\uFF0C\u7531\u4F60\u81EA\u5DF1\u51B3\u5B9A\u501F\u51FA\u53BB\u7684\u7F51\u7EDC\u53EF\u4EE5\u8BBF\u95EE\u4EC0\u4E48\u3002 # # \u4E00\u884C\u4E00\u6761\uFF1A # example.com \u8FDE example.com \u548C\u5B83\u6240\u6709\u5B50\u57DF\u4E00\u8D77\u6321 # .example.com \u53EA\u6321\u5B50\u57DF\uFF0C\u653E\u884C example.com \u672C\u8EAB # ads.example.com \u53EA\u6321\u8FD9\u4E00\u4E2A\u786E\u5207\u7684\u540D\u5B57\u53CA\u5176\u5B50\u57DF # !ok.ads.example.com \u611F\u53F9\u53F7\u5F00\u5934\u662F\u4F8B\u5916\uFF0C\u4F18\u5148\u7EA7\u9AD8\u4E8E\u6240\u6709\u62E6\u622A\u89C4\u5219 # # \u6539\u5B8C\u5728\u63A7\u5236\u53F0\u70B9\u300C\u91CD\u65B0\u52A0\u8F7D\u300D\uFF0C\u6216\u8005\u7ED9\u8FDB\u7A0B\u53D1 SIGHUP\u3002 `; function normalize(host) { return String(host ?? "").trim().toLowerCase().replace(/\.$/, ""); } var Blocklist = class _Blocklist { constructor(file = BLOCKLIST_FILE) { this.file = file; this.deny = []; this.allow = []; this.load(); } ensureFile() { if (!import_node_fs4.default.existsSync(this.file)) { import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(this.file), { recursive: true }); import_node_fs4.default.writeFileSync(this.file, TEMPLATE, { mode: 384 }); } } load() { this.ensureFile(); const deny = []; const allow = []; let text = ""; try { text = import_node_fs4.default.readFileSync(this.file, "utf8"); } catch { this.deny = []; this.allow = []; return { deny: 0, allow: 0 }; } for (const raw of text.split(/\r?\n/)) { const line = raw.trim(); if (line === "" || line.startsWith("#")) continue; if (line.startsWith("!")) { const h2 = normalize(line.slice(1)); if (h2) allow.push({ host: h2.replace(/^\./, ""), subOnly: h2.startsWith(".") }); continue; } const h = normalize(line); if (h) deny.push({ host: h.replace(/^\./, ""), subOnly: h.startsWith(".") }); } this.deny = deny; this.allow = allow; return { deny: deny.length, allow: allow.length }; } static matches(rule, host) { if (host === rule.host) return !rule.subOnly; return host.endsWith(`.${rule.host}`); } /** @returns {string|null} 命中的规则,null 表示放行 */ check(host) { const h = normalize(host); if (!h) return null; if (this.allow.some((r) => _Blocklist.matches(r, h))) return null; const hit = this.deny.find((r) => _Blocklist.matches(r, h)); return hit ? hit.host : null; } get size() { return this.deny.length; } }; // packages/client/src/audit.js var import_node_fs5 = __toESM(require("node:fs"), 1); var import_node_path5 = __toESM(require("node:path"), 1); var AUDIT_FILE = import_node_path5.default.join(HOME, "exit-audit.log"); var AuditLog = class { constructor({ enabled = false, file = AUDIT_FILE, maxBytes = 16 * 1024 * 1024, log: log2 } = {}) { this.enabled = enabled; this.file = file; this.maxBytes = maxBytes; this.log = log2; this.bytes = 0; this.stream = null; if (this.enabled) this._open(); } _open() { try { import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(this.file), { recursive: true }); this.bytes = import_node_fs5.default.existsSync(this.file) ? import_node_fs5.default.statSync(this.file).size : 0; this.stream = import_node_fs5.default.createWriteStream(this.file, { flags: "a", mode: 384 }); this.stream.on("error", (err) => { this.log?.warn(`\u5BA1\u8BA1\u65E5\u5FD7\u5199\u5165\u5931\u8D25\uFF0C\u5DF2\u505C\u6B62\u8BB0\u5F55: ${err.message}`); this.stream = null; }); } catch (err) { this.log?.warn(`\u5BA1\u8BA1\u65E5\u5FD7\u6253\u4E0D\u5F00: ${err.message}`); this.stream = null; } } _rotate() { try { this.stream?.end(); import_node_fs5.default.renameSync(this.file, `${this.file}.1`); } catch { } this._open(); } setEnabled(on) { if (on === this.enabled) return; this.enabled = on; if (on) { this._open(); } else { this.stream?.end(); this.stream = null; } } /** @param {{host,port,ip,up,down,ms,err}} rec */ write(rec) { if (!this.enabled || !this.stream) return; const line = `${JSON.stringify({ t: (/* @__PURE__ */ new Date()).toISOString(), ...rec })} `; this.bytes += Buffer.byteLength(line); this.stream.write(line); if (this.bytes >= this.maxBytes) { this.bytes = 0; this._rotate(); } } close() { this.stream?.end(); this.stream = null; } }; // packages/client/src/index.js var config = loadConfig(); setLevel(config.logLevel); var log = logger("main"); if (!config.broker) { console.error("\u8FD8\u6CA1\u914D\u7F6E broker \u5730\u5740\u3002"); console.error(" node packages/client/src/index.js --broker wss://tunnel.example.com ..."); console.error(`\u914D\u7F6E\u6587\u4EF6: ${CONFIG_FILE}`); process.exit(1); } if (!config.token) { console.error("\u8FD8\u6CA1\u914D\u7F6E token\u3002"); console.error(" node packages/client/src/index.js --token <\u4F60\u7684token>"); console.error(`\u914D\u7F6E\u6587\u4EF6: ${CONFIG_FILE}`); process.exit(1); } if (!config.groupKey) { console.error("\u8FD8\u6CA1\u914D\u7F6E\u7FA4\u7EC4\u5BC6\u94A5\uFF08groupKey\uFF09\u3002\u6CA1\u6709\u5B83\u5C31\u6CA1\u6709\u7AEF\u5230\u7AEF\u52A0\u5BC6\uFF0C"); console.error("\u670D\u52A1\u5668\u4F1A\u770B\u5230\u4F60\u8BBF\u95EE\u7684\u6BCF\u4E00\u4E2A\u57DF\u540D\uFF0C\u6240\u4EE5\u8FD9\u91CC\u4E0D\u5141\u8BB8\u7559\u7A7A\u3002"); console.error(""); console.error(" \u751F\u6210: node packages/client/src/gen-group-key.js"); console.error(" \u914D\u7F6E: node packages/client/src/index.js --group-key <\u5BC6\u94A5>"); console.error(""); console.error("\u7EC4\u91CC\u6240\u6709\u4EBA\u5FC5\u987B\u7528\u540C\u4E00\u4E2A\uFF0C\u4E14\u5E26\u5916\u5206\u53D1\u2014\u2014\u7EDD\u5BF9\u4E0D\u8981\u7ECF\u8FC7\u670D\u52A1\u5668\u3002"); process.exit(1); } try { parseGroupKey(config.groupKey); } catch (err) { console.error(`\u7FA4\u7EC4\u5BC6\u94A5\u683C\u5F0F\u4E0D\u5BF9: ${err.message}`); process.exit(1); } var identity; try { identity = loadIdentity(config.identityKey); } catch (err) { console.error(`\u8EAB\u4EFD\u5BC6\u94A5\u635F\u574F: ${err.message}`); console.error(`\u628A ${CONFIG_FILE} \u91CC\u7684 identityKey \u5220\u6389\u53EF\u4EE5\u91CD\u65B0\u751F\u6210\uFF0C`); console.error("\u4F46\u6362\u4E86\u8EAB\u4EFD\u4E4B\u540E\u522B\u4EBA\u90A3\u8FB9\u7684 TOFU \u8BB0\u5F55\u4F1A\u5BF9\u4E0D\u4E0A\uFF0C\u9700\u8981\u4ED6\u4EEC\u624B\u52A8\u6E05\u4E00\u4E0B\u3002"); process.exit(1); } if (process.argv.includes("--show-identity")) { console.log(`nodeId ${config.nodeId}`); console.log(`\u516C\u94A5 ${identity.publicKeyB64}`); console.log(`\u6307\u7EB9 ${identity.fingerprint}`); console.log("\n\u628A\u6307\u7EB9\u5FF5\u7ED9\u7EC4\u91CC\u7684\u4EBA\u6838\u5BF9\uFF0C\u53EF\u4EE5\u786E\u8BA4 broker \u6CA1\u6709\u5192\u5145\u4F60\u3002"); process.exit(0); } { const url = new URL(config.broker); const loopback = ["127.0.0.1", "localhost", "::1", "[::1]"].includes(url.hostname); if (url.protocol !== "wss:" && !loopback) { console.error(`broker \u5730\u5740 ${config.broker} \u7528\u7684\u662F\u660E\u6587 ws://\uFF0Ctoken \u4F1A\u88F8\u5954\u5728\u7F51\u7EDC\u4E0A\u3002`); console.error("\u6539\u6210 wss://\uFF0C\u6216\u8005\u786E\u5B9E\u8981\u8FDE\u672C\u673A\u8C03\u8BD5\u5C31\u7528 127.0.0.1\u3002"); process.exit(1); } } var stats = { consumer: { active: 0, total: 0, failed: 0, bytesUp: 0, bytesDown: 0, lastExit: null, recent: [], udpActive: 0, udpTotal: 0, udpDropped: 0 }, exit: { active: 0, total: 0, failed: 0, blocked: 0, bytesUp: 0, bytesDown: 0, udpActive: 0, udpTotal: 0, udpDropped: 0 } }; var lastRegions = []; var knownNodes = new KnownNodes(); var relayBucket = new TokenBucket(config.maxRelayKbps * 1024); var csv = (s) => String(s ?? "").split(",").map((x) => x.trim()).filter(Boolean); var resolver = new DnsResolver({ servers: csv(config.dnsServers), log: logger("dns") }); var blocklist = new Blocklist(); var audit = new AuditLog({ enabled: config.auditLog, log: logger("audit") }); var allowedPorts = csv(config.allowedPorts).map(Number).filter(Number.isInteger); var acceptedGroupKeys = [config.groupKey, ...csv(config.groupKeysAccepted)]; for (const k of acceptedGroupKeys.slice(1)) { try { parseGroupKey(k); } catch (err) { console.error(`groupKeysAccepted \u91CC\u6709\u4E00\u628A\u683C\u5F0F\u4E0D\u5BF9: ${err.message}`); process.exit(1); } } var consumerLink = new Link({ name: "consumer", brokerUrl: config.broker, path: "/tunnel/consumer", token: config.token, initiator: true, // 消费链路上由本端开流 log, params: { nodeId: config.nodeId, name: config.name, region: config.region } }); var exitLink = new Link({ name: "exit", brokerUrl: config.broker, path: "/tunnel/exit", token: config.token, initiator: false, // 出口链路上由 broker 开流 log, // 上报静态公钥。broker 把它转给消费端,消费端用它加密目标地址—— // 私钥只在本机,所以 broker 转发的密文它自己解不开。 // caps 是能力清单。组里不可能所有人同一秒升级,新功能得能识别出「对面还是旧版」 // 然后快速失败——旧版收到不认识的帧类型是静默丢弃的,不报错就只能干等超时。 params: { nodeId: config.nodeId, name: config.name, pubkey: identity.publicKeyB64, caps: NODE_CAPS.join(",") }, maxStreams: Math.max(16, config.maxConcurrent * 2) }); exitLink.on("session", (session) => { session.on( "association", createAssociationHandler({ log: logger("exit-udp"), stats: stats.exit, groupKeys: acceptedGroupKeys, identity, bucket: relayBucket, resolver, blocklist, audit, allowedPorts, maxAssociations: config.maxUdpAssociations }) ); session.on( "stream", createExitHandler({ log: logger("exit"), stats: stats.exit, groupKeys: acceptedGroupKeys, identity, bucket: relayBucket, resolver, blocklist, audit, allowedPorts, maxConcurrent: config.maxConcurrent }) ); }); var exitBlockedReason = null; async function enableExit() { const check = await checkExitViability(resolver); if (!check.ok && !config.forceExit) { for (const line of explainFailure(check)) log.error(line); exitBlockedReason = "DNS \u88AB\u52AB\u6301\uFF08fake-ip\uFF09\uFF0C\u8BE6\u89C1\u542F\u52A8\u65E5\u5FD7"; return false; } if (!check.ok) log.warn("DNS \u81EA\u68C0\u6CA1\u8FC7\uFF0C\u4F46 forceExit=true\uFF0C\u4ECD\u7136\u52A0\u5165\u51FA\u53E3\u6C60"); exitBlockedReason = null; exitLink.start(); return true; } var leases = new LeaseManager({ brokerUrl: config.broker, token: config.token, nodeId: config.nodeId, knownNodes, log: logger("lease") }); var socks = createSocks5Server({ log: logger("socks"), stats: stats.consumer, groupKey: config.groupKey, leases, socksHost: config.socksHost, getRegion: () => config.region, open(opts) { const session = consumerLink.session; if (!session) { throw Object.assign(new Error("\u5230 broker \u7684\u96A7\u9053\u8FD8\u6CA1\u8FDE\u4E0A"), { code: E.NET_UNREACH }); } return session.open(opts); } }); var state = { snapshot() { const session = consumerLink.session; return { nodeId: config.nodeId, name: config.name, broker: config.broker, region: config.region, shareExit: config.shareExit, socksHost: config.socksHost, socksPort: config.socksPort, regions: lastRegions, fingerprint: identity.fingerprint, maxRelayKbps: config.maxRelayKbps, auditLog: config.auditLog, blocklistSize: blocklist.size, allowedPorts: config.allowedPorts, dns: resolver.stats(), leases: leases.snapshot(), knownNodes: knownNodes.list(), consumer: { status: consumerLink.status, rtt: session?.rtt ?? null, exitNode: stats.consumer.lastExit, active: stats.consumer.active, total: stats.consumer.total, failed: stats.consumer.failed, bytesUp: stats.consumer.bytesUp, bytesDown: stats.consumer.bytesDown, recent: stats.consumer.recent, udpActive: stats.consumer.udpActive, udpTotal: stats.consumer.udpTotal, udpDropped: stats.consumer.udpDropped }, exit: { status: config.shareExit && !exitBlockedReason ? exitLink.status : "disabled", blockedReason: exitBlockedReason, active: stats.exit.active, total: stats.exit.total, failed: stats.exit.failed, blocked: stats.exit.blocked, bytesUp: stats.exit.bytesUp, bytesDown: stats.exit.bytesDown, udpActive: stats.exit.udpActive, udpTotal: stats.exit.udpTotal, udpDropped: stats.exit.udpDropped } }; } }; var actions = { async fetchNodes() { const url = new URL(config.broker.replace(/^ws/, "http")); url.pathname = "/api/nodes"; const res = await fetch(url, { headers: { Authorization: `Bearer ${config.token}` }, signal: AbortSignal.timeout(8e3) }); if (!res.ok) throw new Error(`broker \u8FD4\u56DE ${res.status}`); const data = await res.json(); lastRegions = data.regions ?? []; return data; }, async applySettings(patch) { const allowed = {}; if ("shareExit" in patch) allowed.shareExit = patch.shareExit === true || patch.shareExit === "true"; if ("region" in patch) allowed.region = String(patch.region).slice(0, 32) || "any"; if ("maxRelayKbps" in patch) { allowed.maxRelayKbps = Math.max(0, Number(patch.maxRelayKbps) || 0); } if ("auditLog" in patch) allowed.auditLog = patch.auditLog === true || patch.auditLog === "true"; Object.assign(config, updateConfig(config, allowed)); if ("auditLog" in allowed) { audit.setEnabled(config.auditLog); log.info(config.auditLog ? `\u51FA\u53E3\u5BA1\u8BA1\u65E5\u5FD7\u5DF2\u5F00\u542F \u2192 ${AUDIT_FILE}` : "\u51FA\u53E3\u5BA1\u8BA1\u65E5\u5FD7\u5DF2\u5173\u95ED"); } if (patch.reloadBlocklist) { const n = blocklist.load(); log.info(`\u9ED1\u540D\u5355\u5DF2\u91CD\u65B0\u52A0\u8F7D\uFF1A${n.deny} \u6761\u62E6\u622A / ${n.allow} \u6761\u4F8B\u5916`); } if ("maxRelayKbps" in allowed) { relayBucket.setRate(config.maxRelayKbps * 1024); log.info( config.maxRelayKbps > 0 ? `\u51FA\u53E3\u9650\u901F\u6539\u4E3A ${config.maxRelayKbps} KB/s` : "\u51FA\u53E3\u9650\u901F\u5DF2\u5173\u95ED" ); } if ("shareExit" in allowed) { if (config.shareExit) { log.info("\u5DF2\u5F00\u542F\u5171\u4EAB\u51FA\u53E3\uFF1A\u5176\u4ED6\u4EBA\u7684\u6D41\u91CF\u4F1A\u4ECE\u672C\u673A IP \u53D1\u51FA"); await enableExit(); } else { log.info("\u5DF2\u5173\u95ED\u5171\u4EAB\u51FA\u53E3"); exitBlockedReason = null; exitLink.stop(); } } if ("region" in allowed) { leases.clear(); consumerLink.reconnect({ region: config.region }); } return { shareExit: config.shareExit, region: config.region, maxRelayKbps: config.maxRelayKbps, auditLog: config.auditLog, blocklistSize: blocklist.size }; } }; var panel = createPanel({ state, config, actions, log: logger("panel") }); socks.listen(config.socksPort, config.socksHost, () => { log.info(`SOCKS5 \u76D1\u542C ${config.socksHost}:${config.socksPort}`); }); socks.on("error", (err) => { log.error(`SOCKS5 \u76D1\u542C\u5931\u8D25: ${err.message}`); process.exit(1); }); panel.listen(config.panelPort, config.panelHost, () => { const base = `http://${config.panelHost}:${config.panelPort}`; if (config.panelToken === "off") { log.warn(`\u63A7\u5236\u53F0 ${base}\uFF08\u672A\u9274\u6743\uFF1A\u540C\u673A\u5176\u4ED6\u7528\u6237\u7684\u8FDB\u7A0B\u4E5F\u80FD\u5F00\u5173\u5171\u4EAB\uFF09`); } else { log.info(`\u63A7\u5236\u53F0 ${base}/?t=${config.panelToken}\u3000\u2190 \u5E26 token\uFF0C\u5B58\u6210\u4E66\u7B7E`); } }); log.info(`\u672C\u673A\u8EAB\u4EFD\u6307\u7EB9 ${identity.fingerprint}\uFF08\u7EC4\u91CC\u7684\u4EBA\u53EF\u4EE5\u5E26\u5916\u6838\u5BF9\uFF09`); log.info(`\u5DF2\u56FA\u5B9A ${knownNodes.list().length} \u4E2A\u51FA\u53E3\u8282\u70B9\u516C\u94A5 \xB7 ${KNOWN_NODES_FILE}`); log.info( `\u51FA\u53E3\u7B56\u7565\uFF1A\u9ED1\u540D\u5355 ${blocklist.size} \u6761 \xB7 ${BLOCKLIST_FILE}` + (allowedPorts.length > 0 ? ` \xB7 \u53EA\u653E\u884C\u7AEF\u53E3 ${allowedPorts.join(",")}` : "") + (config.maxRelayKbps > 0 ? ` \xB7 \u9650\u901F ${config.maxRelayKbps} KB/s` : "") + (config.auditLog ? " \xB7 \u5BA1\u8BA1\u65E5\u5FD7\u5DF2\u5F00" : "") ); panel.on("error", (err) => log.error(`\u9762\u677F\u76D1\u542C\u5931\u8D25: ${err.message}`)); consumerLink.start(); if (config.shareExit) { enableExit().catch((err) => log.error(`\u51FA\u53E3\u81EA\u68C0\u5F02\u5E38: ${err.message}`)); } else { log.info("\u5171\u4EAB\u51FA\u53E3\u672A\u5F00\u542F\u3002\u60F3\u8D21\u732E IP \u5C31\u5728\u63A7\u5236\u53F0\u91CC\u6253\u5F00\uFF0C\u6216\u52A0 --share-exit true"); } var shutdown = () => { consumerLink.stop(); exitLink.stop(); socks.close(); panel.close(); audit.close(); }; installCrashGuard({ log, name: "\u5BA2\u6237\u7AEF", onFatal: shutdown }); process.on("SIGHUP", () => { const n = blocklist.load(); log.info(`\u9ED1\u540D\u5355\u5DF2\u91CD\u65B0\u52A0\u8F7D\uFF1A${n.deny} \u6761\u62E6\u622A / ${n.allow} \u6761\u4F8B\u5916`); }); for (const sig of ["SIGINT", "SIGTERM"]) { process.on(sig, () => { log.info(`\u6536\u5230 ${sig}\uFF0C\u9000\u51FA`); shutdown(); setTimeout(() => process.exit(0), 500).unref(); }); }