diff options
Diffstat (limited to 'alarm/node_modules/jsdom/lib/jsdom/living/websockets/WebSocket-impl.js')
-rw-r--r-- | alarm/node_modules/jsdom/lib/jsdom/living/websockets/WebSocket-impl.js | 328 |
1 files changed, 328 insertions, 0 deletions
diff --git a/alarm/node_modules/jsdom/lib/jsdom/living/websockets/WebSocket-impl.js b/alarm/node_modules/jsdom/lib/jsdom/living/websockets/WebSocket-impl.js new file mode 100644 index 0000000..ff64ff0 --- /dev/null +++ b/alarm/node_modules/jsdom/lib/jsdom/living/websockets/WebSocket-impl.js @@ -0,0 +1,328 @@ +"use strict"; + +const nodeURL = require("url"); + +const DOMException = require("domexception/webidl2js-wrapper"); +const { parseURL, serializeURL, serializeURLOrigin } = require("whatwg-url"); +const WebSocket = require("ws"); + +const { setupForSimpleEventAccessors } = require("../helpers/create-event-accessor"); +const { fireAnEvent } = require("../helpers/events"); +const { isArrayBuffer } = require("../generated/utils"); +const { copyToArrayBufferInNewRealm } = require("../helpers/binary-data"); + +const EventTargetImpl = require("../events/EventTarget-impl").implementation; + +const idlUtils = require("../generated/utils"); +const Blob = require("../generated/Blob"); +const CloseEvent = require("../generated/CloseEvent"); +const MessageEvent = require("../generated/MessageEvent"); + +const CONNECTING = 0; +const OPEN = 1; +const CLOSING = 2; +const CLOSED = 3; + +const productions = { + // https://tools.ietf.org/html/rfc7230#section-3.2.6 + token: /^[!#$%&'*+\-.^_`|~\dA-Za-z]+$/ +}; + +const readyStateWSToDOM = []; +readyStateWSToDOM[WebSocket.CONNECTING] = CONNECTING; +readyStateWSToDOM[WebSocket.OPEN] = OPEN; +readyStateWSToDOM[WebSocket.CLOSING] = CLOSING; +readyStateWSToDOM[WebSocket.CLOSED] = CLOSED; + +// https://tools.ietf.org/html/rfc6455#section-4.3 +// See Sec-WebSocket-Protocol-Client, which is for the syntax of an entire header value. This function checks if a +// single header conforms to the rules. +function verifySecWebSocketProtocol(str) { + return productions.token.test(str); +} + +class PromiseQueues extends WeakMap { + get(window) { + const cur = super.get(window); + return cur !== undefined ? cur : Promise.resolve(); + } +} + +const openSockets = new WeakMap(); +const openingQueues = new PromiseQueues(); + +class WebSocketImpl extends EventTargetImpl { + constructor(globalObject, args, privateData) { + super(globalObject, args, privateData); + + this._ownerDocument = idlUtils.implForWrapper(globalObject._document); + + const url = args[0]; + let protocols = args[1] !== undefined ? args[1] : []; + + const urlRecord = parseURL(url); + if (urlRecord === null) { + throw DOMException.create(this._globalObject, [`The URL '${url}' is invalid.`, "SyntaxError"]); + } + if (urlRecord.scheme !== "ws" && urlRecord.scheme !== "wss") { + throw DOMException.create(this._globalObject, [ + `The URL's scheme must be either 'ws' or 'wss'. '${urlRecord.scheme}' is not allowed.`, + "SyntaxError" + ]); + } + if (urlRecord.fragment !== null) { + throw DOMException.create(this._globalObject, [ + `The URL contains a fragment identifier ('${urlRecord.fragment}'). Fragment identifiers ` + + "are not allowed in WebSocket URLs.", + "SyntaxError" + ]); + } + + if (typeof protocols === "string") { + protocols = [protocols]; + } + const protocolSet = new Set(); + for (const protocol of protocols) { + if (!verifySecWebSocketProtocol(protocol)) { + throw DOMException.create(this._globalObject, [`The subprotocol '${protocol}' is invalid.`, "SyntaxError"]); + } + const lowered = protocol.toLowerCase(); + if (protocolSet.has(lowered)) { + throw DOMException.create(this._globalObject, [ + `The subprotocol '${protocol}' is duplicated.`, + "SyntaxError" + ]); + } + protocolSet.add(lowered); + } + + this._urlRecord = urlRecord; + this.url = serializeURL(urlRecord); + const nodeParsedURL = nodeURL.parse(this.url); + this.extensions = ""; + + this.binaryType = "blob"; + + this._ws = null; + // Used when this._ws has not been initialized yet. + this._readyState = CONNECTING; + this._requiredToFail = false; + this.bufferedAmount = 0; + this._sendQueue = []; + + let openSocketsForWindow = openSockets.get(globalObject._globalProxy); + if (openSocketsForWindow === undefined) { + openSocketsForWindow = new Set(); + openSockets.set(globalObject._globalProxy, openSocketsForWindow); + } + openSocketsForWindow.add(this); + + openingQueues.set(this._ownerDocument, openingQueues.get(this._ownerDocument).then(() => new Promise(resolve => { + // close() called before _ws has been initialized. + if (this._requiredToFail) { + resolve(); + this._readyState = CLOSED; + this._onConnectionClosed(1006, ""); + return; + } + + this._ws = new WebSocket(this.url, protocols, { + headers: { + "user-agent": globalObject.navigator.userAgent, + "cookie": this._ownerDocument._cookieJar.getCookieStringSync(nodeParsedURL, { http: true }), + "origin": globalObject._origin + }, + rejectUnauthorized: this._ownerDocument._strictSSL + }); + this._ws.once("open", () => { + resolve(); + this._onConnectionEstablished(); + }); + this._ws.on("message", this._onMessageReceived.bind(this)); + this._ws.once("close", (...closeArgs) => { + resolve(); + this._onConnectionClosed(...closeArgs); + }); + this._ws.once("upgrade", ({ headers }) => { + if (Array.isArray(headers["set-cookie"])) { + for (const cookie of headers["set-cookie"]) { + this._ownerDocument._cookieJar.setCookieSync( + cookie, + nodeParsedURL, + { http: true, ignoreError: true } + ); + } + } else if (headers["set-cookie"] !== undefined) { + this._ownerDocument._cookieJar.setCookieSync( + headers["set-cookie"], + nodeParsedURL, + { http: true, ignoreError: true } + ); + } + }); + this._ws.once("error", () => { + // The exact error is passed into this callback, but it is ignored as we don't really care about it. + resolve(); + this._requiredToFail = true; + // Do not emit an error here, as that will be handled in _onConnectionClosed. ws always emits a close event + // after errors. + }); + }))); + } + + // https://html.spec.whatwg.org/multipage/web-sockets.html#make-disappear + _makeDisappear() { + this._eventListeners = Object.create(null); + this._close(1001); + } + + static cleanUpWindow(window) { + const openSocketsForWindow = openSockets.get(window._globalProxy); + if (openSocketsForWindow !== undefined) { + for (const ws of openSocketsForWindow) { + ws._makeDisappear(); + } + } + } + + // https://html.spec.whatwg.org/multipage/web-sockets.html#feedback-from-the-protocol + _onConnectionEstablished() { + // readyState is a getter. + if (this._ws.extensions !== null) { + // Right now, ws only supports one extension, permessage-deflate, without any parameters. This algorithm may need + // to be more sophiscated as more extenions are supported. + this.extensions = Object.keys(this._ws.extensions).join(", "); + } + // protocol is a getter. + fireAnEvent("open", this); + } + + _onMessageReceived(data) { + if (this.readyState !== OPEN) { + return; + } + let dataForEvent; + if (typeof data === "string") { + dataForEvent = data; + } else if (this.binaryType === "arraybuffer") { + if (isArrayBuffer(data)) { + dataForEvent = data; + } else if (Array.isArray(data)) { + dataForEvent = copyToArrayBufferInNewRealm(Buffer.concat(data), this._globalObject); + } else { + dataForEvent = copyToArrayBufferInNewRealm(data, this._globalObject); + } + } else { // this.binaryType === "blob" + if (!Array.isArray(data)) { + data = [data]; + } + dataForEvent = Blob.create(this._globalObject, [data, { type: "" }]); + } + fireAnEvent("message", this, MessageEvent, { + data: dataForEvent, + origin: serializeURLOrigin(this._urlRecord) + }); + } + + _onConnectionClosed(code, reason) { + const openSocketsForWindow = openSockets.get(this._ownerDocument._defaultView); + openSocketsForWindow.delete(this); + + const wasClean = !this._requiredToFail; + if (this._requiredToFail) { + fireAnEvent("error", this); + } + fireAnEvent("close", this, CloseEvent, { + wasClean, + code, + reason + }); + } + + get readyState() { + if (this._ws !== null) { + return readyStateWSToDOM[this._ws.readyState]; + } + return this._readyState; + } + + get protocol() { + if (this._ws === null) { + return ""; + } + return this._ws.protocol; + } + + close(code = undefined, reason = undefined) { + if (code !== undefined && code !== 1000 && !(code >= 3000 && code <= 4999)) { + throw DOMException.create(this._globalObject, [ + `The code must be either 1000, or between 3000 and 4999. ${code} is neither.`, + "InvalidAccessError" + ]); + } + if (reason !== undefined && Buffer.byteLength(reason, "utf8") > 123) { + throw DOMException.create(this._globalObject, [ + "The message must not be greater than 123 bytes.", + "SyntaxError" + ]); + } + this._close(code, reason); + } + + _close(code = undefined, reason = undefined) { + if (this.readyState === CONNECTING) { + this._requiredToFail = true; + if (this._ws !== null) { + this._ws.terminate(); + } else { + this._readyState = CLOSING; + } + } else if (this.readyState === OPEN) { + this._ws.close(code, reason); + } + } + + send(data) { + if (this.readyState === CONNECTING) { + throw DOMException.create(this._globalObject, ["Still in CONNECTING state.", "InvalidStateError"]); + } + if (this.readyState !== OPEN) { + return; + } + if (Blob.isImpl(data)) { + data = data._buffer; + } + let length; + if (typeof data === "string") { + length = Buffer.byteLength(data, "utf8"); + } else { + length = data.byteLength; + } + this.bufferedAmount += length; + this._sendQueue.push([data, length]); + this._scheduleSend(); + } + + _actuallySend() { + for (const [data, length] of this._sendQueue.splice(0)) { + this._ws.send(data, { binary: typeof data !== "string" }, () => { + this.bufferedAmount -= length; + }); + } + } + + _scheduleSend() { + if (this._dequeueScheduled) { + return; + } + this._dequeueScheduled = true; + process.nextTick(() => { + this._dequeueScheduled = false; + this._actuallySend(); + }); + } +} + +setupForSimpleEventAccessors(WebSocketImpl.prototype, ["open", "message", "error", "close"]); + +exports.implementation = WebSocketImpl; |