#!/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
\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 |
\u6682\u65E0\u5728\u7EBF\u8282\u70B9
\u6700\u8FD1\u8FDE\u63A5
| \u65F6\u95F4 | \u76EE\u6807 | \u5730\u533A |
\u8FD8\u6CA1\u6709\u8BF7\u6C42\u7ECF\u8FC7
\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();
});
}