import { Transport } from "../transport" import { encode } from "../contrib/parseqs" import { yeast } from "../contrib/yeast" import { pick } from "../util" import { defaultBinaryType, nextTick, usingBrowserWebSocket, WebSocket } from "./websocket-constructor" // import debugModule from "debug" // debug() import { encodePacket } from "../../engine.io-parser" // const debug = debugModule("engine.io-client:websocket") // debug() const debug = (...args: any) => console.debug('engine.io-client:websocket', ...args) // detect ReactNative environment const isReactNative = typeof navigator !== "undefined" && typeof navigator.product === "string" && navigator.product.toLowerCase() === "reactnative" export class WS extends Transport { private ws: any /** * WebSocket transport constructor. * * @param {Object} opts - connection options * @protected */ constructor(opts) { super(opts) this.supportsBinary = !opts.forceBase64 } override get name() { return "websocket" } override doOpen() { if (!this.check()) { // let probe timeout return } const uri = this.uri() const protocols = this.opts.protocols // React Native only supports the 'headers' option, and will print a warning if anything else is passed const opts = isReactNative ? {} : pick( this.opts, "agent", "perMessageDeflate", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "localAddress", "protocolVersion", "origin", "maxPayload", "family", "checkServerIdentity" ) if (this.opts.extraHeaders) { opts.headers = this.opts.extraHeaders } try { this.ws = usingBrowserWebSocket && !isReactNative ? protocols ? new WebSocket(uri, protocols) : new WebSocket(uri) : new WebSocket(uri, protocols, opts) } catch (err: any) { return this.emitReserved("error", err) } this.ws.binaryType = this.socket.binaryType || defaultBinaryType this.addEventListeners() } /** * Adds event listeners to the socket * * @private */ private addEventListeners() { this.ws.onopen = () => { if (this.opts.autoUnref) { this.ws._socket.unref() } this.onOpen() } this.ws.onclose = (closeEvent) => this.onClose({ description: "websocket connection closed", context: closeEvent, }) this.ws.onmessage = (ev) => this.onData(ev.data) this.ws.onerror = (e) => this.onError("websocket error", e) } override write(packets) { this.writable = false // encodePacket efficient as it uses WS framing // no need for encodePayload for (let i = 0; i < packets.length; i++) { const packet = packets[i] const lastPacket = i === packets.length - 1 encodePacket(packet, this.supportsBinary, (data) => { // always create a new object (GH-437) const opts: { compress?: boolean } = {} if (!usingBrowserWebSocket) { if (packet.options) { opts.compress = packet.options.compress } if (this.opts.perMessageDeflate) { const len = // @ts-ignore "string" === typeof data ? Buffer.byteLength(data) : data.length if (len < this.opts.perMessageDeflate.threshold) { opts.compress = false } } } // Sometimes the websocket has already been closed but the browser didn't // have a chance of informing us about it yet, in that case send will // throw an error try { if (usingBrowserWebSocket) { // TypeError is thrown when passing the second argument on Safari this.ws.send(data) } else { this.ws.send(data, opts) } } catch (e) { debug("websocket closed before onclose event") } if (lastPacket) { // fake drain // defer to next tick to allow Socket to clear writeBuffer nextTick(() => { this.writable = true this.emitReserved("drain") }, this.setTimeoutFn) } }) } } override doClose() { if (typeof this.ws !== "undefined") { this.ws.close() this.ws = null } } /** * Generates uri for connection. * * @private */ uri() { let query: { b64?: number } = this.query || {} const schema = this.opts.secure ? "wss" : "ws" let port = "" // avoid port if default for schema if ( this.opts.port && (("wss" === schema && Number(this.opts.port) !== 443) || ("ws" === schema && Number(this.opts.port) !== 80)) ) { port = ":" + this.opts.port } // append timestamp to URI if (this.opts.timestampRequests) { query[this.opts.timestampParam] = yeast() } // communicate binary support capabilities if (!this.supportsBinary) { query.b64 = 1 } const encodedQuery = encode(query) const ipv6 = this.opts.hostname.indexOf(":") !== -1 return ( schema + "://" + (ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) + port + this.opts.path + (encodedQuery.length ? "?" + encodedQuery : "") ) } /** * Feature detection for WebSocket. * * @return {Boolean} whether this transport is available. * @private */ private check() { return !!WebSocket } }