websocket.js (6366B)
1 /** 2 * Module dependencies. 3 */ 4 5 var Transport = require('../transport'); 6 var parser = require('engine.io-parser'); 7 var parseqs = require('parseqs'); 8 var inherit = require('component-inherit'); 9 var yeast = require('yeast'); 10 var debug = require('debug')('engine.io-client:websocket'); 11 12 var BrowserWebSocket, NodeWebSocket; 13 14 if (typeof WebSocket !== 'undefined') { 15 BrowserWebSocket = WebSocket; 16 } else if (typeof self !== 'undefined') { 17 BrowserWebSocket = self.WebSocket || self.MozWebSocket; 18 } 19 20 if (typeof window === 'undefined') { 21 try { 22 NodeWebSocket = require('ws'); 23 } catch (e) { } 24 } 25 26 /** 27 * Get either the `WebSocket` or `MozWebSocket` globals 28 * in the browser or try to resolve WebSocket-compatible 29 * interface exposed by `ws` for Node-like environment. 30 */ 31 32 var WebSocketImpl = BrowserWebSocket || NodeWebSocket; 33 34 /** 35 * Module exports. 36 */ 37 38 module.exports = WS; 39 40 /** 41 * WebSocket transport constructor. 42 * 43 * @api {Object} connection options 44 * @api public 45 */ 46 47 function WS (opts) { 48 var forceBase64 = (opts && opts.forceBase64); 49 if (forceBase64) { 50 this.supportsBinary = false; 51 } 52 this.perMessageDeflate = opts.perMessageDeflate; 53 this.usingBrowserWebSocket = BrowserWebSocket && !opts.forceNode; 54 this.protocols = opts.protocols; 55 if (!this.usingBrowserWebSocket) { 56 WebSocketImpl = NodeWebSocket; 57 } 58 Transport.call(this, opts); 59 } 60 61 /** 62 * Inherits from Transport. 63 */ 64 65 inherit(WS, Transport); 66 67 /** 68 * Transport name. 69 * 70 * @api public 71 */ 72 73 WS.prototype.name = 'websocket'; 74 75 /* 76 * WebSockets support binary 77 */ 78 79 WS.prototype.supportsBinary = true; 80 81 /** 82 * Opens socket. 83 * 84 * @api private 85 */ 86 87 WS.prototype.doOpen = function () { 88 if (!this.check()) { 89 // let probe timeout 90 return; 91 } 92 93 var uri = this.uri(); 94 var protocols = this.protocols; 95 var opts = { 96 agent: this.agent, 97 perMessageDeflate: this.perMessageDeflate 98 }; 99 100 // SSL options for Node.js client 101 opts.pfx = this.pfx; 102 opts.key = this.key; 103 opts.passphrase = this.passphrase; 104 opts.cert = this.cert; 105 opts.ca = this.ca; 106 opts.ciphers = this.ciphers; 107 opts.rejectUnauthorized = this.rejectUnauthorized; 108 if (this.extraHeaders) { 109 opts.headers = this.extraHeaders; 110 } 111 if (this.localAddress) { 112 opts.localAddress = this.localAddress; 113 } 114 115 try { 116 this.ws = 117 this.usingBrowserWebSocket && !this.isReactNative 118 ? protocols 119 ? new WebSocketImpl(uri, protocols) 120 : new WebSocketImpl(uri) 121 : new WebSocketImpl(uri, protocols, opts); 122 } catch (err) { 123 return this.emit('error', err); 124 } 125 126 if (this.ws.binaryType === undefined) { 127 this.supportsBinary = false; 128 } 129 130 if (this.ws.supports && this.ws.supports.binary) { 131 this.supportsBinary = true; 132 this.ws.binaryType = 'nodebuffer'; 133 } else { 134 this.ws.binaryType = 'arraybuffer'; 135 } 136 137 this.addEventListeners(); 138 }; 139 140 /** 141 * Adds event listeners to the socket 142 * 143 * @api private 144 */ 145 146 WS.prototype.addEventListeners = function () { 147 var self = this; 148 149 this.ws.onopen = function () { 150 self.onOpen(); 151 }; 152 this.ws.onclose = function () { 153 self.onClose(); 154 }; 155 this.ws.onmessage = function (ev) { 156 self.onData(ev.data); 157 }; 158 this.ws.onerror = function (e) { 159 self.onError('websocket error', e); 160 }; 161 }; 162 163 /** 164 * Writes data to socket. 165 * 166 * @param {Array} array of packets. 167 * @api private 168 */ 169 170 WS.prototype.write = function (packets) { 171 var self = this; 172 this.writable = false; 173 174 // encodePacket efficient as it uses WS framing 175 // no need for encodePayload 176 var total = packets.length; 177 for (var i = 0, l = total; i < l; i++) { 178 (function (packet) { 179 parser.encodePacket(packet, self.supportsBinary, function (data) { 180 if (!self.usingBrowserWebSocket) { 181 // always create a new object (GH-437) 182 var opts = {}; 183 if (packet.options) { 184 opts.compress = packet.options.compress; 185 } 186 187 if (self.perMessageDeflate) { 188 var len = 'string' === typeof data ? Buffer.byteLength(data) : data.length; 189 if (len < self.perMessageDeflate.threshold) { 190 opts.compress = false; 191 } 192 } 193 } 194 195 // Sometimes the websocket has already been closed but the browser didn't 196 // have a chance of informing us about it yet, in that case send will 197 // throw an error 198 try { 199 if (self.usingBrowserWebSocket) { 200 // TypeError is thrown when passing the second argument on Safari 201 self.ws.send(data); 202 } else { 203 self.ws.send(data, opts); 204 } 205 } catch (e) { 206 debug('websocket closed before onclose event'); 207 } 208 209 --total || done(); 210 }); 211 })(packets[i]); 212 } 213 214 function done () { 215 self.emit('flush'); 216 217 // fake drain 218 // defer to next tick to allow Socket to clear writeBuffer 219 setTimeout(function () { 220 self.writable = true; 221 self.emit('drain'); 222 }, 0); 223 } 224 }; 225 226 /** 227 * Called upon close 228 * 229 * @api private 230 */ 231 232 WS.prototype.onClose = function () { 233 Transport.prototype.onClose.call(this); 234 }; 235 236 /** 237 * Closes socket. 238 * 239 * @api private 240 */ 241 242 WS.prototype.doClose = function () { 243 if (typeof this.ws !== 'undefined') { 244 this.ws.close(); 245 } 246 }; 247 248 /** 249 * Generates uri for connection. 250 * 251 * @api private 252 */ 253 254 WS.prototype.uri = function () { 255 var query = this.query || {}; 256 var schema = this.secure ? 'wss' : 'ws'; 257 var port = ''; 258 259 // avoid port if default for schema 260 if (this.port && (('wss' === schema && Number(this.port) !== 443) || 261 ('ws' === schema && Number(this.port) !== 80))) { 262 port = ':' + this.port; 263 } 264 265 // append timestamp to URI 266 if (this.timestampRequests) { 267 query[this.timestampParam] = yeast(); 268 } 269 270 // communicate binary support capabilities 271 if (!this.supportsBinary) { 272 query.b64 = 1; 273 } 274 275 query = parseqs.encode(query); 276 277 // prepend ? to query 278 if (query.length) { 279 query = '?' + query; 280 } 281 282 var ipv6 = this.hostname.indexOf(':') !== -1; 283 return schema + '://' + (ipv6 ? '[' + this.hostname + ']' : this.hostname) + port + this.path + query; 284 }; 285 286 /** 287 * Feature detection for WebSocket. 288 * 289 * @return {Boolean} whether this transport is available. 290 * @api public 291 */ 292 293 WS.prototype.check = function () { 294 return !!WebSocketImpl && !('__initialize' in WebSocketImpl && this.name === WS.prototype.name); 295 };