Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

264 lines
5.7 KiB

4 years ago
const Transport = require("../transport");
const parser = require("engine.io-parser");
const parseqs = require("parseqs");
const yeast = require("yeast");
const { pick } = require("../util");
const {
WebSocket,
usingBrowserWebSocket,
defaultBinaryType
} = require("./websocket-constructor");
const debug = require("debug")("engine.io-client:websocket");
// detect ReactNative environment
const isReactNative =
typeof navigator !== "undefined" &&
typeof navigator.product === "string" &&
navigator.product.toLowerCase() === "reactnative";
class WS extends Transport {
/**
* WebSocket transport constructor.
*
* @api {Object} connection options
* @api public
*/
constructor(opts) {
super(opts);
this.supportsBinary = !opts.forceBase64;
}
/**
* Transport name.
*
* @api public
*/
get name() {
return "websocket";
}
/**
* Opens socket.
*
* @api private
*/
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"
);
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) {
return this.emit("error", err);
}
this.ws.binaryType = this.socket.binaryType || defaultBinaryType;
this.addEventListeners();
}
/**
* Adds event listeners to the socket
*
* @api private
*/
addEventListeners() {
const self = this;
this.ws.onopen = function() {
self.onOpen();
};
this.ws.onclose = function() {
self.onClose();
};
this.ws.onmessage = function(ev) {
self.onData(ev.data);
};
this.ws.onerror = function(e) {
self.onError("websocket error", e);
};
}
/**
* Writes data to socket.
*
* @param {Array} array of packets.
* @api private
*/
write(packets) {
const self = this;
this.writable = false;
// encodePacket efficient as it uses WS framing
// no need for encodePayload
let total = packets.length;
let i = 0;
const l = total;
for (; i < l; i++) {
(function(packet) {
parser.encodePacket(packet, self.supportsBinary, function(data) {
// always create a new object (GH-437)
const opts = {};
if (!usingBrowserWebSocket) {
if (packet.options) {
opts.compress = packet.options.compress;
}
if (self.opts.perMessageDeflate) {
const len =
"string" === typeof data
? Buffer.byteLength(data)
: data.length;
if (len < self.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
self.ws.send(data);
} else {
self.ws.send(data, opts);
}
} catch (e) {
debug("websocket closed before onclose event");
}
--total || done();
});
})(packets[i]);
}
function done() {
self.emit("flush");
// fake drain
// defer to next tick to allow Socket to clear writeBuffer
setTimeout(function() {
self.writable = true;
self.emit("drain");
}, 0);
}
}
/**
* Called upon close
*
* @api private
*/
onClose() {
Transport.prototype.onClose.call(this);
}
/**
* Closes socket.
*
* @api private
*/
doClose() {
if (typeof this.ws !== "undefined") {
this.ws.close();
}
}
/**
* Generates uri for connection.
*
* @api private
*/
uri() {
let query = 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;
}
query = parseqs.encode(query);
// prepend ? to query
if (query.length) {
query = "?" + query;
}
const ipv6 = this.opts.hostname.indexOf(":") !== -1;
return (
schema +
"://" +
(ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) +
port +
this.opts.path +
query
);
}
/**
* Feature detection for WebSocket.
*
* @return {Boolean} whether this transport is available.
* @api public
*/
check() {
return (
!!WebSocket &&
!("__initialize" in WebSocket && this.name === WS.prototype.name)
);
}
}
module.exports = WS;