4630 lines
160 KiB
JavaScript
4630 lines
160 KiB
JavaScript
"use strict";
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __commonJS = (cb, mod) => function __require() {
|
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
};
|
|
|
|
// 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: Symbol("kIsForOnEventAttribute"),
|
|
kListener: Symbol("kListener"),
|
|
kStatusCode: Symbol("status-code"),
|
|
kWebSocket: 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 = Symbol("kDone");
|
|
var kRun = 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 = Symbol("permessage-deflate");
|
|
var kTotalLength = Symbol("total-length");
|
|
var kCallback = Symbol("callback");
|
|
var kBuffers = Symbol("buffers");
|
|
var kError = Symbol("error");
|
|
var zlibLimiter;
|
|
var PerMessageDeflate = 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 = PerMessageDeflate;
|
|
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 PerMessageDeflate = 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 Receiver = 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[PerMessageDeflate.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[PerMessageDeflate.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 = Receiver;
|
|
}
|
|
});
|
|
|
|
// node_modules/ws/lib/sender.js
|
|
var require_sender = __commonJS({
|
|
"node_modules/ws/lib/sender.js"(exports2, module2) {
|
|
"use strict";
|
|
var { Duplex } = require("stream");
|
|
var { randomFillSync } = require("crypto");
|
|
var {
|
|
types: { isUint8Array }
|
|
} = require("util");
|
|
var PerMessageDeflate = require_permessage_deflate();
|
|
var { EMPTY_BUFFER, kWebSocket, NOOP } = require_constants();
|
|
var { isBlob, isValidStatusCode } = require_validation();
|
|
var { mask: applyMask, toBuffer } = require_buffer_util();
|
|
var kByteLength = 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 Sender = 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[PerMessageDeflate.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[PerMessageDeflate.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 = Sender;
|
|
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 = Symbol("kCode");
|
|
var kData = Symbol("kData");
|
|
var kError = Symbol("kError");
|
|
var kMessage = Symbol("kMessage");
|
|
var kReason = Symbol("kReason");
|
|
var kTarget = Symbol("kTarget");
|
|
var kType = Symbol("kType");
|
|
var kWasClean = 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((extension) => {
|
|
let configurations = extensions[extension];
|
|
if (!Array.isArray(configurations)) configurations = [configurations];
|
|
return configurations.map((params) => {
|
|
return [extension].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 EventEmitter = require("events");
|
|
var https = require("https");
|
|
var http = require("http");
|
|
var net = require("net");
|
|
var tls = require("tls");
|
|
var { randomBytes, createHash } = require("crypto");
|
|
var { Duplex, Readable } = require("stream");
|
|
var { URL } = require("url");
|
|
var PerMessageDeflate = require_permessage_deflate();
|
|
var Receiver = require_receiver();
|
|
var Sender = 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 = Symbol("kAborted");
|
|
var protocolVersions = [8, 13];
|
|
var readyStates = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
|
|
var subprotocolRegex = /^[!#$%&'*+\-.0-9A-Z^_`|a-z~]+$/;
|
|
var WebSocket = class _WebSocket extends EventEmitter {
|
|
/**
|
|
* 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 Receiver({
|
|
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 Sender(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[PerMessageDeflate.extensionName]) {
|
|
this._extensions[PerMessageDeflate.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[PerMessageDeflate.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(WebSocket, "CONNECTING", {
|
|
enumerable: true,
|
|
value: readyStates.indexOf("CONNECTING")
|
|
});
|
|
Object.defineProperty(WebSocket.prototype, "CONNECTING", {
|
|
enumerable: true,
|
|
value: readyStates.indexOf("CONNECTING")
|
|
});
|
|
Object.defineProperty(WebSocket, "OPEN", {
|
|
enumerable: true,
|
|
value: readyStates.indexOf("OPEN")
|
|
});
|
|
Object.defineProperty(WebSocket.prototype, "OPEN", {
|
|
enumerable: true,
|
|
value: readyStates.indexOf("OPEN")
|
|
});
|
|
Object.defineProperty(WebSocket, "CLOSING", {
|
|
enumerable: true,
|
|
value: readyStates.indexOf("CLOSING")
|
|
});
|
|
Object.defineProperty(WebSocket.prototype, "CLOSING", {
|
|
enumerable: true,
|
|
value: readyStates.indexOf("CLOSING")
|
|
});
|
|
Object.defineProperty(WebSocket, "CLOSED", {
|
|
enumerable: true,
|
|
value: readyStates.indexOf("CLOSED")
|
|
});
|
|
Object.defineProperty(WebSocket.prototype, "CLOSED", {
|
|
enumerable: true,
|
|
value: readyStates.indexOf("CLOSED")
|
|
});
|
|
[
|
|
"binaryType",
|
|
"bufferedAmount",
|
|
"extensions",
|
|
"isPaused",
|
|
"protocol",
|
|
"readyState",
|
|
"url"
|
|
].forEach((property) => {
|
|
Object.defineProperty(WebSocket.prototype, property, { enumerable: true });
|
|
});
|
|
["open", "error", "close", "message"].forEach((method) => {
|
|
Object.defineProperty(WebSocket.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
|
|
});
|
|
}
|
|
});
|
|
});
|
|
WebSocket.prototype.addEventListener = addEventListener;
|
|
WebSocket.prototype.removeEventListener = removeEventListener;
|
|
module2.exports = WebSocket;
|
|
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 URL) {
|
|
parsedUrl = address;
|
|
} else {
|
|
try {
|
|
parsedUrl = new URL(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 : http.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 PerMessageDeflate({
|
|
...opts.perMessageDeflate,
|
|
isServer: false,
|
|
maxPayload: opts.maxPayload
|
|
});
|
|
opts.headers["Sec-WebSocket-Extensions"] = format({
|
|
[PerMessageDeflate.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 URL(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 !== WebSocket.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] !== PerMessageDeflate.extensionName) {
|
|
const message = "Server indicated an extension that was not requested";
|
|
abortHandshake(websocket, socket, message);
|
|
return;
|
|
}
|
|
try {
|
|
perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
|
|
} catch (err) {
|
|
const message = "Invalid Sec-WebSocket-Extensions header";
|
|
abortHandshake(websocket, socket, message);
|
|
return;
|
|
}
|
|
websocket._extensions[PerMessageDeflate.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 = WebSocket.CLOSING;
|
|
websocket._errorEmitted = true;
|
|
websocket.emit("error", err);
|
|
websocket.emitClose();
|
|
}
|
|
function netConnect(options) {
|
|
options.path = options.socketPath;
|
|
return net.connect(options);
|
|
}
|
|
function tlsConnect(options) {
|
|
options.path = void 0;
|
|
if (!options.servername && options.servername !== "") {
|
|
options.servername = net.isIP(options.host) ? "" : options.host;
|
|
}
|
|
return tls.connect(options);
|
|
}
|
|
function abortHandshake(websocket, stream, message) {
|
|
websocket._readyState = WebSocket.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 === WebSocket.CLOSED) return;
|
|
if (websocket.readyState === WebSocket.OPEN) {
|
|
websocket._readyState = WebSocket.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 = WebSocket.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 = WebSocket.CLOSING;
|
|
websocket._receiver.end();
|
|
this.end();
|
|
}
|
|
function socketOnError() {
|
|
const websocket = this[kWebSocket];
|
|
this.removeListener("error", socketOnError);
|
|
this.on("error", NOOP);
|
|
if (websocket) {
|
|
websocket._readyState = WebSocket.CLOSING;
|
|
this.destroy();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// node_modules/ws/lib/stream.js
|
|
var require_stream = __commonJS({
|
|
"node_modules/ws/lib/stream.js"(exports2, module2) {
|
|
"use strict";
|
|
var WebSocket = require_websocket();
|
|
var { Duplex } = 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 createWebSocketStream(ws, options) {
|
|
let terminateOnDestroy = true;
|
|
const duplex = new Duplex({
|
|
...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 open() {
|
|
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 open() {
|
|
duplex._write(chunk, encoding, callback);
|
|
});
|
|
return;
|
|
}
|
|
ws.send(chunk, callback);
|
|
};
|
|
duplex.on("end", duplexOnEnd);
|
|
duplex.on("error", duplexOnError);
|
|
return duplex;
|
|
}
|
|
module2.exports = createWebSocketStream;
|
|
}
|
|
});
|
|
|
|
// 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 EventEmitter = require("events");
|
|
var http = require("http");
|
|
var { Duplex } = require("stream");
|
|
var { createHash } = require("crypto");
|
|
var extension = require_extension();
|
|
var PerMessageDeflate = require_permessage_deflate();
|
|
var subprotocol = require_subprotocol();
|
|
var WebSocket = 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 WebSocketServer = class extends EventEmitter {
|
|
/**
|
|
* 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,
|
|
...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 = http.createServer((req, res) => {
|
|
const body = http.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 = subprotocol.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 PerMessageDeflate({
|
|
...this.options.perMessageDeflate,
|
|
isServer: true,
|
|
maxPayload: this.options.maxPayload
|
|
});
|
|
try {
|
|
const offers = extension.parse(secWebSocketExtensions);
|
|
if (offers[PerMessageDeflate.extensionName]) {
|
|
perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
|
|
extensions[PerMessageDeflate.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[PerMessageDeflate.extensionName]) {
|
|
const params = extensions[PerMessageDeflate.extensionName].params;
|
|
const value = extension.format({
|
|
[PerMessageDeflate.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 = WebSocketServer;
|
|
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 || http.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} ${http.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);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// node_modules/ws/index.js
|
|
var require_ws = __commonJS({
|
|
"node_modules/ws/index.js"(exports2, module2) {
|
|
"use strict";
|
|
var createWebSocketStream = require_stream();
|
|
var extension = require_extension();
|
|
var PerMessageDeflate = require_permessage_deflate();
|
|
var Receiver = require_receiver();
|
|
var Sender = require_sender();
|
|
var subprotocol = require_subprotocol();
|
|
var WebSocket = require_websocket();
|
|
var WebSocketServer = require_websocket_server();
|
|
WebSocket.createWebSocketStream = createWebSocketStream;
|
|
WebSocket.extension = extension;
|
|
WebSocket.PerMessageDeflate = PerMessageDeflate;
|
|
WebSocket.Receiver = Receiver;
|
|
WebSocket.Sender = Sender;
|
|
WebSocket.Server = WebSocketServer;
|
|
WebSocket.subprotocol = subprotocol;
|
|
WebSocket.WebSocket = WebSocket;
|
|
WebSocket.WebSocketServer = WebSocketServer;
|
|
module2.exports = WebSocket;
|
|
}
|
|
});
|
|
|
|
// src/host.js
|
|
var require_host = __commonJS({
|
|
"src/host.js"(exports2, module2) {
|
|
"use strict";
|
|
var { EventEmitter } = require("events");
|
|
function parseArgs2(argv) {
|
|
const found = {};
|
|
for (let i = 0; i < argv.length - 1; i += 1) {
|
|
switch (argv[i]) {
|
|
case "-port":
|
|
found.port = argv[i + 1];
|
|
break;
|
|
case "-pluginUUID":
|
|
found.uuid = argv[i + 1];
|
|
break;
|
|
case "-registerEvent":
|
|
found.registerEvent = argv[i + 1];
|
|
break;
|
|
case "-info":
|
|
found.info = argv[i + 1];
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
let info = {};
|
|
try {
|
|
info = JSON.parse(found.info || "{}");
|
|
} catch {
|
|
info = {};
|
|
}
|
|
return { port: found.port, uuid: found.uuid, registerEvent: found.registerEvent, info };
|
|
}
|
|
var WS_OPEN = 1;
|
|
var StreamDockHost2 = class extends EventEmitter {
|
|
/**
|
|
* @param {{port: string|number, uuid: string, registerEvent: string, info?: object}} args
|
|
* @param {{WebSocket?: Function}} [deps] - Ersatz fuer das ws-Paket (Tests)
|
|
*/
|
|
constructor(args, deps = {}) {
|
|
super();
|
|
this.port = args.port;
|
|
this.uuid = args.uuid;
|
|
this.registerEvent = args.registerEvent;
|
|
this.info = args.info || {};
|
|
this.WebSocket = deps.WebSocket || require_ws();
|
|
this.ws = null;
|
|
}
|
|
get language() {
|
|
return this.info.application && this.info.application.language || "en";
|
|
}
|
|
connect() {
|
|
this.ws = new this.WebSocket(`ws://127.0.0.1:${this.port}`);
|
|
this.ws.on("open", () => {
|
|
this._send({ uuid: this.uuid, event: this.registerEvent });
|
|
this.emit("open");
|
|
});
|
|
this.ws.on("message", (raw) => {
|
|
let message;
|
|
try {
|
|
message = JSON.parse(raw.toString());
|
|
} catch {
|
|
return;
|
|
}
|
|
this.emit("event", message);
|
|
});
|
|
this.ws.on("close", () => this.emit("close"));
|
|
this.ws.on("error", (error) => this.emit("error", error));
|
|
}
|
|
close() {
|
|
if (this.ws) this.ws.close();
|
|
}
|
|
// ------------------------------------------------------------- Befehle
|
|
setTitle(context, title) {
|
|
this._send({ event: "setTitle", context, payload: { target: 0, title: String(title ?? "") } });
|
|
}
|
|
/** image = Data-URI (PNG/JPEG/SVG); ohne Wert wird das Standardbild des Zustands wiederhergestellt. */
|
|
setImage(context, image) {
|
|
const payload = { target: 0 };
|
|
if (image) payload.image = image;
|
|
this._send({ event: "setImage", context, payload });
|
|
}
|
|
setState(context, state) {
|
|
this._send({ event: "setState", context, payload: { state } });
|
|
}
|
|
setSettings(context, settings) {
|
|
this._send({ event: "setSettings", context, payload: settings });
|
|
}
|
|
getSettings(context) {
|
|
this._send({ event: "getSettings", context });
|
|
}
|
|
showAlert(context) {
|
|
this._send({ event: "showAlert", context });
|
|
}
|
|
showOk(context) {
|
|
this._send({ event: "showOk", context });
|
|
}
|
|
sendToPropertyInspector(action, context, payload) {
|
|
this._send({ event: "sendToPropertyInspector", action, context, payload });
|
|
}
|
|
_send(message) {
|
|
if (this.ws && this.ws.readyState === WS_OPEN) {
|
|
this.ws.send(JSON.stringify(message));
|
|
}
|
|
}
|
|
};
|
|
module2.exports = { StreamDockHost: StreamDockHost2, parseArgs: parseArgs2 };
|
|
}
|
|
});
|
|
|
|
// src/playtubeClient.js
|
|
var require_playtubeClient = __commonJS({
|
|
"src/playtubeClient.js"(exports2, module2) {
|
|
"use strict";
|
|
var net = require("net");
|
|
var os = require("os");
|
|
var path2 = require("path");
|
|
var { EventEmitter } = require("events");
|
|
var PIPE_PREFIX = "Playtube.Remote.";
|
|
var PIPE_NAMES = ["Playtube", "PlaytubeDev", "PlaytubeEdge", "PlaytubeEdgeDev"];
|
|
var MAX_BUFFER_CHARS = 1 << 20;
|
|
var PlaytubeError = class extends Error {
|
|
constructor(code, message) {
|
|
super(message);
|
|
this.name = "PlaytubeError";
|
|
this.code = code;
|
|
}
|
|
};
|
|
function pipePath(name, platform = process.platform) {
|
|
const full = PIPE_PREFIX + name;
|
|
return platform === "win32" ? `\\\\.\\pipe\\${full}` : path2.join(os.tmpdir(), full);
|
|
}
|
|
var PlaytubeClient2 = class extends EventEmitter {
|
|
/**
|
|
* @param {object} [options]
|
|
* @param {string[]} [options.pipeNames]
|
|
* @param {string} [options.platform]
|
|
* @param {Function} [options.connect] - Ersatz fuer net.connect (Tests)
|
|
* @param {number} [options.requestTimeoutMs]
|
|
* @param {number[]} [options.backoffMs]
|
|
*/
|
|
constructor(options = {}) {
|
|
super();
|
|
this.pipeNames = options.pipeNames || PIPE_NAMES;
|
|
this.platform = options.platform || process.platform;
|
|
this.connectFn = options.connect || net.connect;
|
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 3e3;
|
|
this.backoffMs = options.backoffMs || [300, 600, 1200, 2500, 5e3];
|
|
this.socket = null;
|
|
this.connected = false;
|
|
this.pipeName = null;
|
|
this.running = false;
|
|
this._nextId = 1;
|
|
this._pending = /* @__PURE__ */ new Map();
|
|
this._buffer = "";
|
|
this._failedRounds = 0;
|
|
this._retryTimer = null;
|
|
}
|
|
start() {
|
|
if (this.running) return;
|
|
this.running = true;
|
|
this._attempt(0);
|
|
}
|
|
stop() {
|
|
this.running = false;
|
|
clearTimeout(this._retryTimer);
|
|
this._retryTimer = null;
|
|
const socket = this.socket;
|
|
this.socket = null;
|
|
if (this.connected) {
|
|
this.connected = false;
|
|
this._rejectAll(new PlaytubeError("stopped", "Client gestoppt"));
|
|
}
|
|
if (socket) socket.destroy();
|
|
}
|
|
/**
|
|
* Schickt einen Befehl und wartet auf die Antwort.
|
|
* @param {string} cmd
|
|
* @param {object} [fields] - target / value / playlist (undefined-Werte werden weggelassen)
|
|
* @returns {Promise<object>} die komplette Antwort ({ok: true, ...})
|
|
*/
|
|
request(cmd, fields = {}) {
|
|
if (!this.connected || !this.socket) {
|
|
return Promise.reject(new PlaytubeError("not_connected", "Playtube ist nicht verbunden"));
|
|
}
|
|
const id = this._nextId++;
|
|
const payload = { id, cmd };
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
if (value !== void 0 && value !== null) payload[key] = value;
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
this._pending.delete(id);
|
|
reject(new PlaytubeError("timeout", `Keine Antwort von Playtube auf "${cmd}"`));
|
|
}, this.requestTimeoutMs);
|
|
this._pending.set(id, { resolve, reject, timer });
|
|
this.socket.write(`${JSON.stringify(payload)}
|
|
`, (error) => {
|
|
if (error && this._pending.delete(id)) {
|
|
clearTimeout(timer);
|
|
reject(new PlaytubeError("write_failed", error.message));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
async status() {
|
|
const response = await this.request("status");
|
|
if (!response.state) throw new PlaytubeError("bad_response", 'Status ohne "state"');
|
|
return response.state;
|
|
}
|
|
async listPlaylists() {
|
|
const response = await this.request("list_playlists");
|
|
return Array.isArray(response.playlists) ? response.playlists : [];
|
|
}
|
|
playPlaylist(playlistId) {
|
|
return this.request("play_playlist", { playlist: playlistId });
|
|
}
|
|
// ------------------------------------------------------------------ intern
|
|
_attempt(index) {
|
|
if (!this.running) return;
|
|
if (index >= this.pipeNames.length) {
|
|
this._scheduleRetry();
|
|
return;
|
|
}
|
|
const name = this.pipeNames[index];
|
|
const socket = this.connectFn(pipePath(name, this.platform));
|
|
let opened = false;
|
|
socket.setEncoding("utf8");
|
|
socket.once("connect", () => {
|
|
if (!this.running) {
|
|
socket.destroy();
|
|
return;
|
|
}
|
|
opened = true;
|
|
this.socket = socket;
|
|
this.connected = true;
|
|
this.pipeName = name;
|
|
this._failedRounds = 0;
|
|
this._buffer = "";
|
|
this.emit("connected", name);
|
|
});
|
|
socket.on("data", (chunk) => {
|
|
if (socket === this.socket) this._onData(chunk);
|
|
});
|
|
socket.on("error", () => {
|
|
});
|
|
socket.on("close", () => {
|
|
if (!opened) {
|
|
this._attempt(index + 1);
|
|
} else if (socket === this.socket) {
|
|
this._onLost();
|
|
}
|
|
});
|
|
}
|
|
_onLost() {
|
|
this.socket = null;
|
|
this.connected = false;
|
|
this.pipeName = null;
|
|
this._rejectAll(new PlaytubeError("disconnected", "Verbindung zu Playtube getrennt"));
|
|
this.emit("disconnected");
|
|
this._failedRounds = 0;
|
|
this._scheduleRetry();
|
|
}
|
|
_scheduleRetry() {
|
|
if (!this.running) return;
|
|
const delay = this.backoffMs[Math.min(this._failedRounds, this.backoffMs.length - 1)];
|
|
this._failedRounds += 1;
|
|
this._retryTimer = setTimeout(() => this._attempt(0), delay);
|
|
}
|
|
_onData(chunk) {
|
|
this._buffer += chunk;
|
|
let newline;
|
|
while ((newline = this._buffer.indexOf("\n")) >= 0) {
|
|
const line = this._buffer.slice(0, newline).trim();
|
|
this._buffer = this._buffer.slice(newline + 1);
|
|
if (line) this._onLine(line);
|
|
}
|
|
if (this._buffer.length > MAX_BUFFER_CHARS) {
|
|
this._buffer = "";
|
|
if (this.socket) this.socket.destroy();
|
|
}
|
|
}
|
|
_onLine(line) {
|
|
let message;
|
|
try {
|
|
message = JSON.parse(line);
|
|
} catch {
|
|
return;
|
|
}
|
|
const entry = this._pending.get(message.id);
|
|
if (!entry) return;
|
|
this._pending.delete(message.id);
|
|
clearTimeout(entry.timer);
|
|
if (message.ok) {
|
|
entry.resolve(message);
|
|
} else {
|
|
entry.reject(new PlaytubeError("remote", message.error || "Fehler in Playtube"));
|
|
}
|
|
}
|
|
_rejectAll(error) {
|
|
for (const entry of this._pending.values()) {
|
|
clearTimeout(entry.timer);
|
|
entry.reject(error);
|
|
}
|
|
this._pending.clear();
|
|
}
|
|
};
|
|
module2.exports = { PlaytubeClient: PlaytubeClient2, PlaytubeError, pipePath, PIPE_NAMES, PIPE_PREFIX };
|
|
}
|
|
});
|
|
|
|
// src/actions.js
|
|
var require_actions = __commonJS({
|
|
"src/actions.js"(exports2, module2) {
|
|
"use strict";
|
|
var PREFIX = "com.fojadrachi.playtube.";
|
|
var DEFAULTS = Object.freeze({
|
|
target: "auto",
|
|
// "auto" | "youtube" | "music"
|
|
step: 5,
|
|
// Lautstaerke-Schritt in Prozent
|
|
volume: 30,
|
|
// Ziel-Lautstaerke der Aktion "Lautstaerke setzen"
|
|
seconds: 10,
|
|
// Sprungweite beim Vor-/Zurueckspulen
|
|
tab: "toggle",
|
|
// Aktion "Tab wechseln": "toggle" | "youtube" | "music"
|
|
playlistId: "",
|
|
playlistTitle: ""
|
|
});
|
|
var TARGETS = ["auto", "youtube", "music"];
|
|
var TABS = ["toggle", "youtube", "music"];
|
|
function clampInt(value, min, max, fallback) {
|
|
const number = Number.parseInt(value, 10);
|
|
if (!Number.isFinite(number)) return fallback;
|
|
return Math.max(min, Math.min(max, number));
|
|
}
|
|
function normalizeSettings(raw, titleDefault = false) {
|
|
const settings = { ...DEFAULTS, ...raw || {} };
|
|
if (!TARGETS.includes(settings.target)) settings.target = DEFAULTS.target;
|
|
if (!TABS.includes(settings.tab)) settings.tab = DEFAULTS.tab;
|
|
settings.step = clampInt(settings.step, 1, 25, DEFAULTS.step);
|
|
settings.volume = clampInt(settings.volume, 0, 100, DEFAULTS.volume);
|
|
settings.seconds = clampInt(settings.seconds, 1, 300, DEFAULTS.seconds);
|
|
const show = raw && raw.showTitle !== void 0 ? raw.showTitle : titleDefault;
|
|
settings.showTitle = show === true || show === "true";
|
|
settings.playlistId = typeof settings.playlistId === "string" ? settings.playlistId.trim() : "";
|
|
settings.playlistTitle = typeof settings.playlistTitle === "string" ? settings.playlistTitle : "";
|
|
return settings;
|
|
}
|
|
function pickTab(state, target) {
|
|
if (!state || !state.tabs) return null;
|
|
const name = target === "auto" ? state.activeTab : target;
|
|
const info = state.tabs[name];
|
|
return info ? { name, info } : null;
|
|
}
|
|
function wrapTitle(text, perLine = 8, maxLines = 3) {
|
|
const words = String(text || "").trim().split(/\s+/).filter(Boolean);
|
|
const lines = [];
|
|
let current = "";
|
|
let truncated = false;
|
|
const push = (line) => {
|
|
if (lines.length < maxLines) lines.push(line);
|
|
else truncated = true;
|
|
};
|
|
for (let word of words) {
|
|
while (word.length > perLine) {
|
|
if (current) {
|
|
push(current);
|
|
current = "";
|
|
}
|
|
push(word.slice(0, perLine));
|
|
word = word.slice(perLine);
|
|
}
|
|
if (!word) continue;
|
|
if (!current) current = word;
|
|
else if (current.length + 1 + word.length <= perLine) current += ` ${word}`;
|
|
else {
|
|
push(current);
|
|
current = word;
|
|
}
|
|
}
|
|
if (current) push(current);
|
|
if (truncated && lines.length) lines[lines.length - 1] = `${lines[lines.length - 1].slice(0, perLine - 2)}..`;
|
|
return lines.join("\n");
|
|
}
|
|
function volumeTitle(tab) {
|
|
const volume = tab && tab.info.volume;
|
|
return typeof volume === "number" ? `${Math.round(volume)}%` : "";
|
|
}
|
|
var OFFLINE_TITLE = "Playtube\noffline";
|
|
var DEFINITIONS = {
|
|
playpause: {
|
|
titleDefault: false,
|
|
request: (s) => ({ cmd: "play_pause", fields: { target: s.target } }),
|
|
render: (s, state, connected) => {
|
|
if (!connected) return { state: 0, title: s.showTitle ? OFFLINE_TITLE : "" };
|
|
const tab = pickTab(state, s.target);
|
|
return {
|
|
state: tab && tab.info.playing ? 1 : 0,
|
|
title: s.showTitle && tab && tab.info.title ? wrapTitle(tab.info.title) : ""
|
|
};
|
|
}
|
|
},
|
|
next: { request: (s) => ({ cmd: "next", fields: { target: s.target } }) },
|
|
previous: { request: (s) => ({ cmd: "previous", fields: { target: s.target } }) },
|
|
seekback: {
|
|
request: (s) => ({ cmd: "seek", fields: { target: s.target, value: -s.seconds } }),
|
|
render: (s) => ({ title: `-${s.seconds}s` })
|
|
},
|
|
seekforward: {
|
|
request: (s) => ({ cmd: "seek", fields: { target: s.target, value: s.seconds } }),
|
|
render: (s) => ({ title: `+${s.seconds}s` })
|
|
},
|
|
volumeup: {
|
|
press: "down",
|
|
repeat: { delayMs: 450, intervalMs: 140 },
|
|
request: (s) => ({ cmd: "volume_change", fields: { target: s.target, value: s.step } }),
|
|
render: (s, state, connected) => ({ title: connected ? volumeTitle(pickTab(state, s.target)) : "" })
|
|
},
|
|
volumedown: {
|
|
press: "down",
|
|
repeat: { delayMs: 450, intervalMs: 140 },
|
|
request: (s) => ({ cmd: "volume_change", fields: { target: s.target, value: -s.step } }),
|
|
render: (s, state, connected) => ({ title: connected ? volumeTitle(pickTab(state, s.target)) : "" })
|
|
},
|
|
volumeset: {
|
|
request: (s) => ({ cmd: "set_volume", fields: { target: s.target, value: s.volume } }),
|
|
render: (s) => ({ title: `${s.volume}%` })
|
|
},
|
|
mute: {
|
|
request: (s) => ({ cmd: "mute_toggle", fields: { target: s.target } }),
|
|
render: (s, state, connected) => {
|
|
const tab = connected ? pickTab(state, s.target) : null;
|
|
return { state: tab && tab.info.muted ? 1 : 0, title: tab && !tab.info.muted ? volumeTitle(tab) : "" };
|
|
}
|
|
},
|
|
nowplaying: {
|
|
titleDefault: true,
|
|
request: (s) => ({ cmd: "play_pause", fields: { target: s.target } }),
|
|
render: (s, state, connected) => {
|
|
if (!connected) return { coverUrl: null, title: s.showTitle ? OFFLINE_TITLE : "" };
|
|
const tab = pickTab(state, s.target);
|
|
const info = tab && tab.info;
|
|
return {
|
|
coverUrl: info && info.hasVideo && info.thumbnail ? info.thumbnail : null,
|
|
title: s.showTitle && info && info.title ? wrapTitle(info.title) : ""
|
|
};
|
|
}
|
|
},
|
|
like: {
|
|
request: (s) => ({ cmd: "like", fields: { target: s.target } }),
|
|
render: (s, state, connected) => {
|
|
const tab = connected ? pickTab(state, s.target) : null;
|
|
return { state: tab && tab.info.likeStatus === "like" ? 1 : 0 };
|
|
}
|
|
},
|
|
shuffle: { request: (s) => ({ cmd: "shuffle", fields: { target: s.target } }) },
|
|
repeat: {
|
|
request: (s) => ({ cmd: "repeat", fields: { target: s.target } }),
|
|
render: (s, state, connected) => {
|
|
const tab = connected ? pickTab(state, s.target) : null;
|
|
const mode = String(tab && tab.info.repeatMode || "").toUpperCase();
|
|
return { state: mode === "ALL" ? 1 : mode === "ONE" ? 2 : 0 };
|
|
}
|
|
},
|
|
playlist: {
|
|
request: (s) => s.playlistId ? { cmd: "play_playlist", fields: { playlist: s.playlistId } } : null,
|
|
render: (s) => ({ title: s.playlistTitle ? wrapTitle(s.playlistTitle) : "" })
|
|
},
|
|
switchtab: {
|
|
request: (s) => ({ cmd: "switch_tab", fields: { target: s.tab === "toggle" ? "auto" : s.tab } }),
|
|
render: (s, state, connected) => {
|
|
if (!connected || !state) return { title: "" };
|
|
if (state.visibleTab === "music") return { title: "Music" };
|
|
if (state.visibleTab === "youtube") return { title: "YouTube" };
|
|
return { title: "" };
|
|
}
|
|
},
|
|
show: { request: () => ({ cmd: "show", fields: {} }) }
|
|
};
|
|
function actionIdFromUuid(uuid) {
|
|
if (typeof uuid !== "string" || !uuid.startsWith(PREFIX)) return null;
|
|
const id = uuid.slice(PREFIX.length);
|
|
return Object.prototype.hasOwnProperty.call(DEFINITIONS, id) ? id : null;
|
|
}
|
|
function renderAction(id, rawSettings, state, connected) {
|
|
const definition = DEFINITIONS[id];
|
|
const settings = normalizeSettings(rawSettings, definition.titleDefault);
|
|
return definition.render ? definition.render(settings, state, connected) : {};
|
|
}
|
|
function buildRequest(id, rawSettings) {
|
|
const definition = DEFINITIONS[id];
|
|
const settings = normalizeSettings(rawSettings, definition.titleDefault);
|
|
return definition.request(settings);
|
|
}
|
|
module2.exports = {
|
|
PREFIX,
|
|
DEFAULTS,
|
|
DEFINITIONS,
|
|
normalizeSettings,
|
|
pickTab,
|
|
wrapTitle,
|
|
actionIdFromUuid,
|
|
renderAction,
|
|
buildRequest
|
|
};
|
|
}
|
|
});
|
|
|
|
// src/infoPanel.js
|
|
var require_infoPanel = __commonJS({
|
|
"src/infoPanel.js"(exports2, module2) {
|
|
"use strict";
|
|
var { wrapTitle } = require_actions();
|
|
var SIZE = 144;
|
|
var BACKGROUND = "#171b24";
|
|
var ACCENT = "#ff4d5e";
|
|
var TRACK = "#3a4152";
|
|
var MUTED_TEXT = "#c3c9d6";
|
|
var BAR_X = 10;
|
|
var BAR_WIDTH = SIZE - 2 * BAR_X;
|
|
var BAR_Y = 132;
|
|
var BAR_HEIGHT = 6;
|
|
var TITLE_LINE_HEIGHT = 25;
|
|
var TITLE_BASELINE_OFFSET = 7;
|
|
var FONT = 'font-family="Segoe UI, Arial, sans-serif"';
|
|
var XML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
function escapeXml(text) {
|
|
return String(text).replace(/[&<>"']/g, (char) => XML_ESCAPES[char]);
|
|
}
|
|
function formatTime(seconds) {
|
|
if (!Number.isFinite(seconds) || seconds < 0) return "0:00";
|
|
const whole = Math.floor(seconds);
|
|
return `${Math.floor(whole / 60)}:${String(whole % 60).padStart(2, "0")}`;
|
|
}
|
|
function textLines(text, x, y, lineHeight, size, color, weight, anchor = "middle") {
|
|
return String(text).split("\n").filter(Boolean).map((line, index) => `<text x="${x}" y="${y + index * lineHeight}" ${FONT} font-size="${size}" font-weight="${weight}" fill="${color}" text-anchor="${anchor}">${escapeXml(line)}</text>`).join("");
|
|
}
|
|
function noteMarkup(cx, cy, scale) {
|
|
const s = scale;
|
|
return `<circle cx="${cx - 9 * s}" cy="${cy + 14 * s}" r="${10 * s}" fill="${ACCENT}"/><rect x="${cx - 1 * s}" y="${cy - 22 * s}" width="${6 * s}" height="${38 * s}" fill="${ACCENT}"/><polygon points="${cx + 5 * s},${cy - 22 * s} ${cx + 24 * s},${cy - 12 * s} ${cx + 5 * s},${cy - 4 * s}" fill="${ACCENT}"/>`;
|
|
}
|
|
function backgroundMarkup(coverDataUri) {
|
|
if (!coverDataUri) return `<rect width="${SIZE}" height="${SIZE}" fill="${BACKGROUND}"/>${noteMarkup(72, 44, 1.1)}`;
|
|
return `<rect width="${SIZE}" height="${SIZE}" fill="${BACKGROUND}"/><image x="0" y="0" width="${SIZE}" height="${SIZE}" preserveAspectRatio="xMidYMid slice" href="${coverDataUri}"/>`;
|
|
}
|
|
function shadeMarkup() {
|
|
return `<defs><linearGradient id="shade" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#000" stop-opacity="0"/><stop offset="0.35" stop-color="#000" stop-opacity="0.72"/><stop offset="1" stop-color="#000" stop-opacity="0.9"/></linearGradient></defs><rect x="0" y="30" width="${SIZE}" height="${SIZE - 30}" fill="url(#shade)"/>`;
|
|
}
|
|
function playStateMarkup(playing) {
|
|
const cx = 118;
|
|
const cy = 26;
|
|
const disc = `<circle cx="${cx}" cy="${cy}" r="19" fill="#000" fill-opacity="0.6"/>`;
|
|
if (playing) {
|
|
return `${disc}<rect x="${cx - 9}" y="${cy - 10}" width="6" height="20" fill="#fff"/><rect x="${cx + 3}" y="${cy - 10}" width="6" height="20" fill="#fff"/>`;
|
|
}
|
|
return `${disc}<polygon points="${cx - 7},${cy - 11} ${cx + 11},${cy} ${cx - 7},${cy + 11}" fill="#fff"/>`;
|
|
}
|
|
function buildInfoSvg(view) {
|
|
const head = `<svg xmlns="http://www.w3.org/2000/svg" width="${SIZE}" height="${SIZE}" viewBox="0 0 ${SIZE} ${SIZE}">`;
|
|
if (!view) {
|
|
return `${head}<rect width="${SIZE}" height="${SIZE}" fill="${BACKGROUND}"/>${noteMarkup(72, 44, 1.1)}${textLines("Playtube offline", 72, 120, 0, 14, "#ffffff", "bold")}</svg>`;
|
|
}
|
|
const hasTrack = Boolean(view.title);
|
|
const ratio = view.duration > 0 ? Math.max(0, Math.min(1, view.currentTime / view.duration)) : 0;
|
|
const parts = [backgroundMarkup(view.coverDataUri), shadeMarkup()];
|
|
if (hasTrack) {
|
|
const title = wrapTitle(view.title, 10, 2);
|
|
const lineCount = title.split("\n").length;
|
|
const blockCenter = view.subtitle ? SIZE / 2 - 12 : SIZE / 2;
|
|
const firstBaseline = blockCenter - (lineCount - 1) * TITLE_LINE_HEIGHT / 2 + TITLE_BASELINE_OFFSET;
|
|
parts.push(textLines(title, SIZE / 2, firstBaseline, TITLE_LINE_HEIGHT, 22, "#ffffff", "bold"));
|
|
if (view.subtitle) {
|
|
const subtitleY = firstBaseline + (lineCount - 1) * TITLE_LINE_HEIGHT + 27;
|
|
parts.push(textLines(wrapTitle(view.subtitle, 13, 1), SIZE / 2, subtitleY, 0, 16, MUTED_TEXT, "normal"));
|
|
}
|
|
parts.push(playStateMarkup(view.playing));
|
|
} else {
|
|
parts.push(textLines("Nichts spielt", 72, 120, 0, 14, "#ffffff", "bold"));
|
|
}
|
|
parts.push(`<rect x="${BAR_X}" y="${BAR_Y}" width="${BAR_WIDTH}" height="${BAR_HEIGHT}" rx="3" fill="${TRACK}"/>`);
|
|
parts.push(`<rect x="${BAR_X}" y="${BAR_Y}" width="${(BAR_WIDTH * ratio).toFixed(1)}" height="${BAR_HEIGHT}" rx="3" fill="${ACCENT}"/>`);
|
|
return `${head}${parts.join("")}</svg>`;
|
|
}
|
|
function svgToDataUri(svg) {
|
|
return `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`;
|
|
}
|
|
module2.exports = { buildInfoSvg, svgToDataUri, formatTime, SIZE };
|
|
}
|
|
});
|
|
|
|
// src/controller.js
|
|
var require_controller = __commonJS({
|
|
"src/controller.js"(exports2, module2) {
|
|
"use strict";
|
|
var {
|
|
DEFINITIONS,
|
|
actionIdFromUuid,
|
|
buildRequest,
|
|
normalizeSettings,
|
|
pickTab,
|
|
renderAction
|
|
} = require_actions();
|
|
var { buildInfoSvg, svgToDataUri } = require_infoPanel();
|
|
var POLL_INTERVAL_MS = 1e3;
|
|
var REFRESH_AFTER_COMMAND_MS = 350;
|
|
var COVER_CACHE_LIMIT = 24;
|
|
var COVER_TIMEOUT_MS = 4e3;
|
|
var KEY_IMAGE_PX = 144;
|
|
function sharpCoverUrl(url) {
|
|
return String(url).replace(/=w\d+-h\d+/, `=w${KEY_IMAGE_PX}-h${KEY_IMAGE_PX}`);
|
|
}
|
|
async function defaultFetchImage(url) {
|
|
const response = await fetch(url, { signal: AbortSignal.timeout(COVER_TIMEOUT_MS) });
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const type = (response.headers.get("content-type") || "image/jpeg").split(";")[0];
|
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
return `data:${type};base64,${bytes.toString("base64")}`;
|
|
}
|
|
var Controller2 = class {
|
|
/**
|
|
* @param {object} deps
|
|
* @param {import('./host').StreamDockHost} deps.host
|
|
* @param {import('./playtubeClient').PlaytubeClient} deps.client
|
|
* @param {{info: Function, warn: Function, error: Function}} deps.logger
|
|
* @param {Function} [deps.fetchImage] - (url) => Promise<dataUri>
|
|
* @param {number} [deps.pollIntervalMs]
|
|
*/
|
|
constructor({ host, client, logger, fetchImage, pollIntervalMs }) {
|
|
this.host = host;
|
|
this.client = client;
|
|
this.logger = logger;
|
|
this.fetchImage = fetchImage || defaultFetchImage;
|
|
this.pollIntervalMs = pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
this.keys = /* @__PURE__ */ new Map();
|
|
this.state = null;
|
|
this.covers = /* @__PURE__ */ new Map();
|
|
this.coverLoading = /* @__PURE__ */ new Set();
|
|
this.repeaters = /* @__PURE__ */ new Map();
|
|
this.pollTimer = null;
|
|
this.refreshTimer = null;
|
|
this.polling = false;
|
|
}
|
|
start() {
|
|
this.host.on("event", (message) => this.handleEvent(message));
|
|
this.client.on("connected", (name) => {
|
|
this.logger.info(`Mit Playtube verbunden (${name})`);
|
|
this.poll();
|
|
});
|
|
this.client.on("disconnected", () => {
|
|
this.logger.info("Verbindung zu Playtube getrennt");
|
|
this.state = null;
|
|
this.renderAll();
|
|
});
|
|
this.client.start();
|
|
this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
|
|
}
|
|
stop() {
|
|
clearInterval(this.pollTimer);
|
|
clearTimeout(this.refreshTimer);
|
|
for (const context of [...this.repeaters.keys()]) this.stopRepeat(context);
|
|
this.client.stop();
|
|
}
|
|
// ------------------------------------------------------------ Ereignisse
|
|
handleEvent(message) {
|
|
const { event, context } = message;
|
|
switch (event) {
|
|
case "willAppear":
|
|
return this.onAppear(message);
|
|
case "willDisappear":
|
|
this.stopRepeat(context);
|
|
this.keys.delete(context);
|
|
return void 0;
|
|
case "didReceiveSettings":
|
|
return this.onSettings(message);
|
|
case "keyDown":
|
|
return this.onKeyDown(context);
|
|
case "keyUp":
|
|
return this.onKeyUp(context);
|
|
case "sendToPlugin":
|
|
return this.onPropertyInspectorMessage(message);
|
|
default:
|
|
return void 0;
|
|
}
|
|
}
|
|
onAppear(message) {
|
|
const actionId = actionIdFromUuid(message.action);
|
|
if (!actionId) return;
|
|
const payload = message.payload || {};
|
|
const settings = payload.settings || {};
|
|
const info = payload.controller === "Information";
|
|
this.logger.info(`willAppear ${actionId} controller=${payload.controller} info=${info}`);
|
|
this.keys.set(message.context, { actionId, settings, info, last: null });
|
|
this.renderKey(message.context);
|
|
if (this.client.connected && !this.state) this.poll();
|
|
}
|
|
onSettings(message) {
|
|
const key = this.keys.get(message.context);
|
|
if (!key) return;
|
|
key.settings = message.payload && message.payload.settings || {};
|
|
key.last = null;
|
|
this.renderKey(message.context);
|
|
}
|
|
onKeyDown(context) {
|
|
const key = this.keys.get(context);
|
|
if (!key) return;
|
|
const definition = DEFINITIONS[key.actionId];
|
|
if (definition.press !== "down") return;
|
|
this.execute(context);
|
|
const { delayMs, intervalMs } = definition.repeat || {};
|
|
if (!delayMs) return;
|
|
this.stopRepeat(context);
|
|
const timers = {};
|
|
timers.delayTimer = setTimeout(() => {
|
|
timers.intervalTimer = setInterval(() => this.execute(context, { quiet: true }), intervalMs);
|
|
}, delayMs);
|
|
this.repeaters.set(context, timers);
|
|
}
|
|
onKeyUp(context) {
|
|
const key = this.keys.get(context);
|
|
if (!key) return;
|
|
if (DEFINITIONS[key.actionId].press === "down") {
|
|
this.stopRepeat(context);
|
|
return;
|
|
}
|
|
this.execute(context);
|
|
}
|
|
stopRepeat(context) {
|
|
const timers = this.repeaters.get(context);
|
|
if (!timers) return;
|
|
clearTimeout(timers.delayTimer);
|
|
clearInterval(timers.intervalTimer);
|
|
this.repeaters.delete(context);
|
|
}
|
|
// ------------------------------------------------------------ Befehle
|
|
/** Fuehrt die Aktion einer Taste aus; `quiet` = kein Alarm bei Fehler (Dauerdruck). */
|
|
async execute(context, { quiet = false } = {}) {
|
|
const key = this.keys.get(context);
|
|
if (!key) return;
|
|
const request = buildRequest(key.actionId, key.settings);
|
|
if (!request) {
|
|
this.host.showAlert(context);
|
|
return;
|
|
}
|
|
try {
|
|
await this.client.request(request.cmd, request.fields);
|
|
this.scheduleRefresh();
|
|
} catch (error) {
|
|
this.logger.warn(`${request.cmd} fehlgeschlagen: ${error.message}`);
|
|
if (quiet) this.stopRepeat(context);
|
|
else this.host.showAlert(context);
|
|
}
|
|
}
|
|
scheduleRefresh() {
|
|
clearTimeout(this.refreshTimer);
|
|
this.refreshTimer = setTimeout(() => this.poll(), REFRESH_AFTER_COMMAND_MS);
|
|
}
|
|
// ------------------------------------------------------------ Status
|
|
async poll() {
|
|
if (this.polling || !this.client.connected || this.keys.size === 0) return;
|
|
this.polling = true;
|
|
try {
|
|
this.state = await this.client.status();
|
|
this.renderAll();
|
|
} catch (error) {
|
|
this.logger.warn(`Status nicht lesbar: ${error.message}`);
|
|
} finally {
|
|
this.polling = false;
|
|
}
|
|
}
|
|
renderAll() {
|
|
for (const context of this.keys.keys()) this.renderKey(context);
|
|
}
|
|
renderKey(context) {
|
|
const key = this.keys.get(context);
|
|
if (!key) return;
|
|
if (key.info && key.actionId === "nowplaying") {
|
|
this.renderInfo(context, key);
|
|
return;
|
|
}
|
|
const view = renderAction(key.actionId, key.settings, this.state, this.client.connected);
|
|
const coverUrl = view.coverUrl ? sharpCoverUrl(view.coverUrl) : null;
|
|
const cover = coverUrl ? this.covers.get(coverUrl) || null : null;
|
|
if (coverUrl && !cover) this.loadCover(coverUrl);
|
|
const next = { state: view.state, title: view.title, cover: cover ? coverUrl : null };
|
|
const previous = key.last || {};
|
|
const changed = (name) => previous[name] !== next[name];
|
|
key.last = next;
|
|
if (view.state !== void 0 && changed("state")) this.host.setState(context, view.state);
|
|
if (view.title !== void 0 && changed("title")) this.host.setTitle(context, view.title);
|
|
if (view.coverUrl !== void 0 && changed("cover")) this.host.setImage(context, cover);
|
|
}
|
|
/** Hohes Info-Display: komplette "Aktueller Titel"-Karte als SVG-Bild. */
|
|
renderInfo(context, key) {
|
|
let view = null;
|
|
if (this.client.connected) {
|
|
const tab = pickTab(this.state, normalizeSettings(key.settings).target);
|
|
const info = tab && tab.info || {};
|
|
const coverUrl = info.thumbnail ? sharpCoverUrl(info.thumbnail) : null;
|
|
const coverDataUri = coverUrl ? this.covers.get(coverUrl) || null : null;
|
|
if (coverUrl && !coverDataUri) this.loadCover(coverUrl);
|
|
view = {
|
|
title: info.title || "",
|
|
subtitle: info.subtitle || "",
|
|
playing: Boolean(info.playing),
|
|
currentTime: info.currentTime,
|
|
duration: info.duration,
|
|
volume: info.volume,
|
|
coverDataUri
|
|
};
|
|
}
|
|
const svg = buildInfoSvg(view);
|
|
if (svg === key.lastSvg) return;
|
|
key.lastSvg = svg;
|
|
this.host.setTitle(context, "");
|
|
this.host.setImage(context, svgToDataUri(svg));
|
|
}
|
|
async loadCover(url) {
|
|
if (this.coverLoading.has(url)) return;
|
|
this.coverLoading.add(url);
|
|
try {
|
|
const dataUri = await this.fetchImage(url);
|
|
this.covers.set(url, dataUri);
|
|
while (this.covers.size > COVER_CACHE_LIMIT) this.covers.delete(this.covers.keys().next().value);
|
|
this.renderAll();
|
|
} catch (error) {
|
|
this.logger.warn(`Cover nicht ladbar: ${error.message}`);
|
|
} finally {
|
|
this.coverLoading.delete(url);
|
|
}
|
|
}
|
|
// ------------------------------------------------------------ Property Inspector
|
|
async onPropertyInspectorMessage(message) {
|
|
const payload = message.payload || {};
|
|
if (payload.command !== "requestPlaylists") return;
|
|
const reply = (body) => this.host.sendToPropertyInspector(message.action, message.context, body);
|
|
if (!this.client.connected) {
|
|
reply({ command: "playlists", ok: false, error: "offline", playlists: [] });
|
|
return;
|
|
}
|
|
try {
|
|
const playlists = await this.client.listPlaylists();
|
|
reply({ command: "playlists", ok: true, playlists });
|
|
} catch (error) {
|
|
this.logger.warn(`Playlists nicht lesbar: ${error.message}`);
|
|
reply({ command: "playlists", ok: false, error: error.code || "error", message: error.message, playlists: [] });
|
|
}
|
|
}
|
|
};
|
|
module2.exports = { Controller: Controller2, sharpCoverUrl };
|
|
}
|
|
});
|
|
|
|
// src/logger.js
|
|
var require_logger = __commonJS({
|
|
"src/logger.js"(exports2, module2) {
|
|
"use strict";
|
|
var fs = require("fs");
|
|
var path2 = require("path");
|
|
var MAX_BYTES = 512 * 1024;
|
|
function createLogger2(directory) {
|
|
const file = path2.join(directory, "plugin.log");
|
|
let ready = false;
|
|
function ensure() {
|
|
if (ready) return true;
|
|
try {
|
|
fs.mkdirSync(directory, { recursive: true });
|
|
ready = true;
|
|
} catch {
|
|
ready = false;
|
|
}
|
|
return ready;
|
|
}
|
|
function write(level, message) {
|
|
if (!ensure()) return;
|
|
try {
|
|
try {
|
|
if (fs.statSync(file).size > MAX_BYTES) fs.renameSync(file, path2.join(directory, "plugin.old.log"));
|
|
} catch {
|
|
}
|
|
fs.appendFileSync(file, `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
|
|
`);
|
|
} catch {
|
|
}
|
|
}
|
|
const describe = (value) => value instanceof Error ? value.stack || value.message : String(value);
|
|
return {
|
|
info: (message) => write("INFO ", describe(message)),
|
|
warn: (message) => write("WARN ", describe(message)),
|
|
error: (message) => write("ERROR", describe(message))
|
|
};
|
|
}
|
|
module2.exports = { createLogger: createLogger2 };
|
|
}
|
|
});
|
|
|
|
// src/main.js
|
|
var path = require("path");
|
|
var { StreamDockHost, parseArgs } = require_host();
|
|
var { PlaytubeClient } = require_playtubeClient();
|
|
var { Controller } = require_controller();
|
|
var { createLogger } = require_logger();
|
|
var EXIT_GRACE_MS = 2e3;
|
|
function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
const logger = createLogger(path.join(__dirname, "..", "log"));
|
|
if (!args.port || !args.uuid || !args.registerEvent) {
|
|
logger.error(`Ungueltige Startparameter: ${process.argv.slice(2).join(" ")}`);
|
|
process.exit(1);
|
|
}
|
|
process.on("uncaughtException", (error) => logger.error(error));
|
|
process.on("unhandledRejection", (error) => logger.error(error));
|
|
const host = new StreamDockHost(args);
|
|
const pipeNames = process.env.PLAYTUBE_PIPE_NAMES ? process.env.PLAYTUBE_PIPE_NAMES.split(",") : void 0;
|
|
const client = new PlaytubeClient({ pipeNames });
|
|
const controller = new Controller({ host, client, logger });
|
|
controller.start();
|
|
host.on("open", () => logger.info(`Bei Stream Dock registriert (Port ${args.port})`));
|
|
host.on("error", (error) => logger.warn(`WebSocket-Fehler: ${error.message}`));
|
|
host.on("close", () => {
|
|
logger.info("Verbindung zu Stream Dock beendet");
|
|
controller.stop();
|
|
setTimeout(() => process.exit(0), EXIT_GRACE_MS).unref();
|
|
});
|
|
host.connect();
|
|
}
|
|
main();
|