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
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;
|