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.
286 lines
6.1 KiB
286 lines
6.1 KiB
/** |
|
* 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 = global.WebSocket || global.MozWebSocket; |
|
var NodeWebSocket; |
|
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 WebSocket = BrowserWebSocket; |
|
if (!WebSocket && typeof window === 'undefined') { |
|
WebSocket = 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) { |
|
WebSocket = 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 ? (protocols ? new WebSocket(uri, protocols) : new WebSocket(uri)) : new WebSocket(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 ? global.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 !!WebSocket && !('__initialize' in WebSocket && this.name === WS.prototype.name); |
|
};
|
|
|