296 lines
6.2 KiB
JavaScript
296 lines
6.2 KiB
JavaScript
|
/**
|
||
|
* Module dependencies.
|
||
|
*/
|
||
|
|
||
|
var Transport = require('../transport');
|
||
|
var parser = require('engine.io-parser');
|
||
|
var parseqs = require('parseqs');
|
||
|
var inherit = require('component-inherit');
|
||
|
var yeast = require('yeast');
|
||
|
var debug = require('debug')('engine.io-client:websocket');
|
||
|
|
||
|
var BrowserWebSocket, NodeWebSocket;
|
||
|
|
||
|
if (typeof WebSocket !== 'undefined') {
|
||
|
BrowserWebSocket = WebSocket;
|
||
|
} else if (typeof self !== 'undefined') {
|
||
|
BrowserWebSocket = self.WebSocket || self.MozWebSocket;
|
||
|
}
|
||
|
|
||
|
if (typeof window === 'undefined') {
|
||
|
try {
|
||
|
NodeWebSocket = require('ws');
|
||
|
} catch (e) { }
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get either the `WebSocket` or `MozWebSocket` globals
|
||
|
* in the browser or try to resolve WebSocket-compatible
|
||
|
* interface exposed by `ws` for Node-like environment.
|
||
|
*/
|
||
|
|
||
|
var WebSocketImpl = BrowserWebSocket || NodeWebSocket;
|
||
|
|
||
|
/**
|
||
|
* Module exports.
|
||
|
*/
|
||
|
|
||
|
module.exports = WS;
|
||
|
|
||
|
/**
|
||
|
* WebSocket transport constructor.
|
||
|
*
|
||
|
* @api {Object} connection options
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
function WS (opts) {
|
||
|
var forceBase64 = (opts && opts.forceBase64);
|
||
|
if (forceBase64) {
|
||
|
this.supportsBinary = false;
|
||
|
}
|
||
|
this.perMessageDeflate = opts.perMessageDeflate;
|
||
|
this.usingBrowserWebSocket = BrowserWebSocket && !opts.forceNode;
|
||
|
this.protocols = opts.protocols;
|
||
|
if (!this.usingBrowserWebSocket) {
|
||
|
WebSocketImpl = NodeWebSocket;
|
||
|
}
|
||
|
Transport.call(this, opts);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Inherits from Transport.
|
||
|
*/
|
||
|
|
||
|
inherit(WS, Transport);
|
||
|
|
||
|
/**
|
||
|
* Transport name.
|
||
|
*
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
WS.prototype.name = 'websocket';
|
||
|
|
||
|
/*
|
||
|
* WebSockets support binary
|
||
|
*/
|
||
|
|
||
|
WS.prototype.supportsBinary = true;
|
||
|
|
||
|
/**
|
||
|
* Opens socket.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
WS.prototype.doOpen = function () {
|
||
|
if (!this.check()) {
|
||
|
// let probe timeout
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var uri = this.uri();
|
||
|
var protocols = this.protocols;
|
||
|
var opts = {
|
||
|
agent: this.agent,
|
||
|
perMessageDeflate: this.perMessageDeflate
|
||
|
};
|
||
|
|
||
|
// SSL options for Node.js client
|
||
|
opts.pfx = this.pfx;
|
||
|
opts.key = this.key;
|
||
|
opts.passphrase = this.passphrase;
|
||
|
opts.cert = this.cert;
|
||
|
opts.ca = this.ca;
|
||
|
opts.ciphers = this.ciphers;
|
||
|
opts.rejectUnauthorized = this.rejectUnauthorized;
|
||
|
if (this.extraHeaders) {
|
||
|
opts.headers = this.extraHeaders;
|
||
|
}
|
||
|
if (this.localAddress) {
|
||
|
opts.localAddress = this.localAddress;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
this.ws =
|
||
|
this.usingBrowserWebSocket && !this.isReactNative
|
||
|
? protocols
|
||
|
? new WebSocketImpl(uri, protocols)
|
||
|
: new WebSocketImpl(uri)
|
||
|
: new WebSocketImpl(uri, protocols, opts);
|
||
|
} catch (err) {
|
||
|
return this.emit('error', err);
|
||
|
}
|
||
|
|
||
|
if (this.ws.binaryType === undefined) {
|
||
|
this.supportsBinary = false;
|
||
|
}
|
||
|
|
||
|
if (this.ws.supports && this.ws.supports.binary) {
|
||
|
this.supportsBinary = true;
|
||
|
this.ws.binaryType = 'nodebuffer';
|
||
|
} else {
|
||
|
this.ws.binaryType = 'arraybuffer';
|
||
|
}
|
||
|
|
||
|
this.addEventListeners();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Adds event listeners to the socket
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
WS.prototype.addEventListeners = function () {
|
||
|
var 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
|
||
|
*/
|
||
|
|
||
|
WS.prototype.write = function (packets) {
|
||
|
var self = this;
|
||
|
this.writable = false;
|
||
|
|
||
|
// encodePacket efficient as it uses WS framing
|
||
|
// no need for encodePayload
|
||
|
var total = packets.length;
|
||
|
for (var i = 0, l = total; i < l; i++) {
|
||
|
(function (packet) {
|
||
|
parser.encodePacket(packet, self.supportsBinary, function (data) {
|
||
|
if (!self.usingBrowserWebSocket) {
|
||
|
// always create a new object (GH-437)
|
||
|
var opts = {};
|
||
|
if (packet.options) {
|
||
|
opts.compress = packet.options.compress;
|
||
|
}
|
||
|
|
||
|
if (self.perMessageDeflate) {
|
||
|
var len = 'string' === typeof data ? Buffer.byteLength(data) : data.length;
|
||
|
if (len < self.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 (self.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
|
||
|
*/
|
||
|
|
||
|
WS.prototype.onClose = function () {
|
||
|
Transport.prototype.onClose.call(this);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Closes socket.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
WS.prototype.doClose = function () {
|
||
|
if (typeof this.ws !== 'undefined') {
|
||
|
this.ws.close();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Generates uri for connection.
|
||
|
*
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
WS.prototype.uri = function () {
|
||
|
var query = this.query || {};
|
||
|
var schema = this.secure ? 'wss' : 'ws';
|
||
|
var port = '';
|
||
|
|
||
|
// avoid port if default for schema
|
||
|
if (this.port && (('wss' === schema && Number(this.port) !== 443) ||
|
||
|
('ws' === schema && Number(this.port) !== 80))) {
|
||
|
port = ':' + this.port;
|
||
|
}
|
||
|
|
||
|
// append timestamp to URI
|
||
|
if (this.timestampRequests) {
|
||
|
query[this.timestampParam] = yeast();
|
||
|
}
|
||
|
|
||
|
// communicate binary support capabilities
|
||
|
if (!this.supportsBinary) {
|
||
|
query.b64 = 1;
|
||
|
}
|
||
|
|
||
|
query = parseqs.encode(query);
|
||
|
|
||
|
// prepend ? to query
|
||
|
if (query.length) {
|
||
|
query = '?' + query;
|
||
|
}
|
||
|
|
||
|
var ipv6 = this.hostname.indexOf(':') !== -1;
|
||
|
return schema + '://' + (ipv6 ? '[' + this.hostname + ']' : this.hostname) + port + this.path + query;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Feature detection for WebSocket.
|
||
|
*
|
||
|
* @return {Boolean} whether this transport is available.
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
WS.prototype.check = function () {
|
||
|
return !!WebSocketImpl && !('__initialize' in WebSocketImpl && this.name === WS.prototype.name);
|
||
|
};
|