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.

818 lines
23 KiB

  1. /*!
  2. * ws: a node.js websocket client
  3. * Copyright(c) 2011 Einar Otto Stangvik <einaros@gmail.com>
  4. * MIT Licensed
  5. */
  6. var util = require('util')
  7. , events = require('events')
  8. , http = require('http')
  9. , https = require('https')
  10. , crypto = require('crypto')
  11. , url = require('url')
  12. , fs = require('fs')
  13. , Options = require('options')
  14. , Sender = require('./Sender')
  15. , Receiver = require('./Receiver')
  16. , SenderHixie = require('./Sender.hixie')
  17. , ReceiverHixie = require('./Receiver.hixie');
  18. /**
  19. * Constants
  20. */
  21. // Default protocol version
  22. var protocolVersion = 13;
  23. // Close timeout
  24. var closeTimeout = 30000; // Allow 5 seconds to terminate the connection cleanly
  25. /**
  26. * Node version 0.4 and 0.6 compatibility
  27. */
  28. var isNodeV4 = /^v0\.4/.test(process.version);
  29. /**
  30. * WebSocket implementation
  31. */
  32. function WebSocket(address, protocols, options) {
  33. if (protocols && !Array.isArray(protocols) && 'object' == typeof protocols) {
  34. // accept the "options" Object as the 2nd argument
  35. options = protocols;
  36. protocols = null;
  37. }
  38. if ('string' == typeof protocols) {
  39. protocols = [ protocols ];
  40. }
  41. if (!Array.isArray(protocols)) {
  42. protocols = [];
  43. }
  44. // TODO: actually handle the `Sub-Protocols` part of the WebSocket client
  45. this._socket = null;
  46. this.bytesReceived = 0;
  47. this.readyState = null;
  48. this.supports = {};
  49. if (Array.isArray(address)) {
  50. initAsServerClient.apply(this, address.concat(options));
  51. } else {
  52. initAsClient.apply(this, [address, protocols, options]);
  53. }
  54. }
  55. /**
  56. * Inherits from EventEmitter.
  57. */
  58. util.inherits(WebSocket, events.EventEmitter);
  59. /**
  60. * Ready States
  61. */
  62. ["CONNECTING", "OPEN", "CLOSING", "CLOSED"].forEach(function (state, index) {
  63. WebSocket.prototype[state] = WebSocket[state] = index;
  64. });
  65. /**
  66. * Gracefully closes the connection, after sending a description message to the server
  67. *
  68. * @param {Object} data to be sent to the server
  69. * @api public
  70. */
  71. WebSocket.prototype.close = function(code, data) {
  72. if (this.readyState == WebSocket.CLOSING || this.readyState == WebSocket.CLOSED) return;
  73. if (this.readyState == WebSocket.CONNECTING) {
  74. this.readyState = WebSocket.CLOSED;
  75. return;
  76. }
  77. try {
  78. this.readyState = WebSocket.CLOSING;
  79. this._closeCode = code;
  80. this._closeMessage = data;
  81. var mask = !this._isServer;
  82. this._sender.close(code, data, mask);
  83. }
  84. catch (e) {
  85. this.emit('error', e);
  86. }
  87. finally {
  88. this.terminate();
  89. }
  90. }
  91. /**
  92. * Pause the client stream
  93. *
  94. * @api public
  95. */
  96. WebSocket.prototype.pause = function() {
  97. if (this.readyState != WebSocket.OPEN) throw new Error('not opened');
  98. return this._socket.pause();
  99. }
  100. /**
  101. * Sends a ping
  102. *
  103. * @param {Object} data to be sent to the server
  104. * @param {Object} Members - mask: boolean, binary: boolean
  105. * @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
  106. * @api public
  107. */
  108. WebSocket.prototype.ping = function(data, options, dontFailWhenClosed) {
  109. if (this.readyState != WebSocket.OPEN) {
  110. if (dontFailWhenClosed === true) return;
  111. throw new Error('not opened');
  112. }
  113. options = options || {};
  114. if (typeof options.mask == 'undefined') options.mask = !this._isServer;
  115. this._sender.ping(data, options);
  116. }
  117. /**
  118. * Sends a pong
  119. *
  120. * @param {Object} data to be sent to the server
  121. * @param {Object} Members - mask: boolean, binary: boolean
  122. * @param {boolean} dontFailWhenClosed indicates whether or not to throw if the connection isnt open
  123. * @api public
  124. */
  125. WebSocket.prototype.pong = function(data, options, dontFailWhenClosed) {
  126. if (this.readyState != WebSocket.OPEN) {
  127. if (dontFailWhenClosed === true) return;
  128. throw new Error('not opened');
  129. }
  130. options = options || {};
  131. if (typeof options.mask == 'undefined') options.mask = !this._isServer;
  132. this._sender.pong(data, options);
  133. }
  134. /**
  135. * Resume the client stream
  136. *
  137. * @api public
  138. */
  139. WebSocket.prototype.resume = function() {
  140. if (this.readyState != WebSocket.OPEN) throw new Error('not opened');
  141. return this._socket.resume();
  142. }
  143. /**
  144. * Sends a piece of data
  145. *
  146. * @param {Object} data to be sent to the server
  147. * @param {Object} Members - mask: boolean, binary: boolean
  148. * @param {function} Optional callback which is executed after the send completes
  149. * @api public
  150. */
  151. WebSocket.prototype.send = function(data, options, cb) {
  152. if (typeof options == 'function') {
  153. cb = options;
  154. options = {};
  155. }
  156. if (this.readyState != WebSocket.OPEN) {
  157. if (typeof cb == 'function') cb(new Error('not opened'));
  158. else throw new Error('not opened');
  159. return;
  160. }
  161. if (!data) data = '';
  162. if (this._queue) {
  163. var self = this;
  164. this._queue.push(function() { self.send(data, options, cb); });
  165. return;
  166. }
  167. options = options || {};
  168. options.fin = true;
  169. if (typeof options.binary == 'undefined') {
  170. options.binary = (data instanceof ArrayBuffer || data instanceof Buffer ||
  171. data instanceof Uint8Array ||
  172. data instanceof Uint16Array ||
  173. data instanceof Uint32Array ||
  174. data instanceof Int8Array ||
  175. data instanceof Int16Array ||
  176. data instanceof Int32Array ||
  177. data instanceof Float32Array ||
  178. data instanceof Float64Array);
  179. }
  180. if (typeof options.mask == 'undefined') options.mask = !this._isServer;
  181. if (data instanceof fs.ReadStream) {
  182. startQueue(this);
  183. var self = this;
  184. sendStream(this, data, options, function(error) {
  185. process.nextTick(function() { executeQueueSends(self); });
  186. if (typeof cb == 'function') cb(error);
  187. });
  188. }
  189. else this._sender.send(data, options, cb);
  190. }
  191. /**
  192. * Streams data through calls to a user supplied function
  193. *
  194. * @param {Object} Members - mask: boolean, binary: boolean
  195. * @param {function} 'function (error, send)' which is executed on successive ticks of which send is 'function (data, final)'.
  196. * @api public
  197. */
  198. WebSocket.prototype.stream = function(options, cb) {
  199. if (typeof options == 'function') {
  200. cb = options;
  201. options = {};
  202. }
  203. var self = this;
  204. if (typeof cb != 'function') throw new Error('callback must be provided');
  205. if (this.readyState != WebSocket.OPEN) {
  206. if (typeof cb == 'function') cb(new Error('not opened'));
  207. else throw new Error('not opened');
  208. return;
  209. }
  210. if (this._queue) {
  211. this._queue.push(function() { self.stream(options, cb); });
  212. return;
  213. }
  214. options = options || {};
  215. if (typeof options.mask == 'undefined') options.mask = !this._isServer;
  216. startQueue(this);
  217. var send = function(data, final) {
  218. try {
  219. if (self.readyState != WebSocket.OPEN) throw new Error('not opened');
  220. options.fin = final === true;
  221. self._sender.send(data, options);
  222. if (!final) process.nextTick(cb.bind(null, null, send));
  223. else executeQueueSends(self);
  224. }
  225. catch (e) {
  226. if (typeof cb == 'function') cb(e);
  227. else {
  228. delete self._queue;
  229. self.emit('error', e);
  230. }
  231. }
  232. }
  233. process.nextTick(cb.bind(null, null, send));
  234. }
  235. /**
  236. * Immediately shuts down the connection
  237. *
  238. * @api public
  239. */
  240. WebSocket.prototype.terminate = function() {
  241. if (this.readyState == WebSocket.CLOSED) return;
  242. if (this._socket) {
  243. try {
  244. // End the connection
  245. this._socket.end();
  246. }
  247. catch (e) {
  248. // Socket error during end() call, so just destroy it right now
  249. cleanupWebsocketResources.call(this, true);
  250. return;
  251. }
  252. // Add a timeout to ensure that the connection is completely
  253. // cleaned up within 30 seconds, even if the clean close procedure
  254. // fails for whatever reason
  255. this._closeTimer = setTimeout(cleanupWebsocketResources.bind(this, true), closeTimeout);
  256. }
  257. else if (this.readyState == WebSocket.CONNECTING) {
  258. cleanupWebsocketResources.call(this, true);
  259. }
  260. };
  261. /**
  262. * Expose bufferedAmount
  263. *
  264. * @api public
  265. */
  266. Object.defineProperty(WebSocket.prototype, 'bufferedAmount', {
  267. get: function get() {
  268. var amount = 0;
  269. if (this._socket) {
  270. amount = this._socket.bufferSize || 0;
  271. }
  272. return amount;
  273. }
  274. });
  275. /**
  276. * Emulates the W3C Browser based WebSocket interface using function members.
  277. *
  278. * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
  279. * @api public
  280. */
  281. ['open', 'error', 'close', 'message'].forEach(function(method) {
  282. Object.defineProperty(WebSocket.prototype, 'on' + method, {
  283. /**
  284. * Returns the current listener
  285. *
  286. * @returns {Mixed} the set function or undefined
  287. * @api public
  288. */
  289. get: function get() {
  290. var listener = this.listeners(method)[0];
  291. return listener ? (listener._listener ? listener._listener : listener) : undefined;
  292. },
  293. /**
  294. * Start listening for events
  295. *
  296. * @param {Function} listener the listener
  297. * @returns {Mixed} the set function or undefined
  298. * @api public
  299. */
  300. set: function set(listener) {
  301. this.removeAllListeners(method);
  302. this.addEventListener(method, listener);
  303. }
  304. });
  305. });
  306. /**
  307. * Emulates the W3C Browser based WebSocket interface using addEventListener.
  308. *
  309. * @see https://developer.mozilla.org/en/DOM/element.addEventListener
  310. * @see http://dev.w3.org/html5/websockets/#the-websocket-interface
  311. * @api public
  312. */
  313. WebSocket.prototype.addEventListener = function(method, listener) {
  314. var target = this;
  315. if (typeof listener === 'function') {
  316. if (method === 'message') {
  317. function onMessage (data, flags) {
  318. listener.call(this, new MessageEvent(data, flags.binary ? 'Binary' : 'Text', target));
  319. }
  320. // store a reference so we can return the original function from the addEventListener hook
  321. onMessage._listener = listener;
  322. this.on(method, onMessage);
  323. } else if (method === 'close') {
  324. function onClose (code, message) {
  325. listener.call(this, new CloseEvent(code, message, target));
  326. }
  327. // store a reference so we can return the original function from the addEventListener hook
  328. onClose._listener = listener;
  329. this.on(method, onClose);
  330. } else if (method === 'error') {
  331. function onError (event) {
  332. event.target = target;
  333. listener.call(this, event);
  334. }
  335. // store a reference so we can return the original function from the addEventListener hook
  336. onError._listener = listener;
  337. this.on(method, onError);
  338. } else if (method === 'open') {
  339. function onOpen () {
  340. listener.call(this, new OpenEvent(target));
  341. }
  342. // store a reference so we can return the original function from the addEventListener hook
  343. onOpen._listener = listener;
  344. this.on(method, onOpen);
  345. } else {
  346. this.on(method, listener);
  347. }
  348. }
  349. }
  350. module.exports = WebSocket;
  351. /**
  352. * W3C MessageEvent
  353. *
  354. * @see http://www.w3.org/TR/html5/comms.html
  355. * @api private
  356. */
  357. function MessageEvent(dataArg, typeArg, target) {
  358. this.data = dataArg;
  359. this.type = typeArg;
  360. this.target = target;
  361. }
  362. /**
  363. * W3C CloseEvent
  364. *
  365. * @see http://www.w3.org/TR/html5/comms.html
  366. * @api private
  367. */
  368. function CloseEvent(code, reason, target) {
  369. this.wasClean = (typeof code == 'undefined' || code == 1000);
  370. this.code = code;
  371. this.reason = reason;
  372. this.target = target;
  373. }
  374. /**
  375. * W3C OpenEvent
  376. *
  377. * @see http://www.w3.org/TR/html5/comms.html
  378. * @api private
  379. */
  380. function OpenEvent(target) {
  381. this.target = target;
  382. }
  383. /**
  384. * Entirely private apis,
  385. * which may or may not be bound to a sepcific WebSocket instance.
  386. */
  387. function initAsServerClient(req, socket, upgradeHead, options) {
  388. options = new Options({
  389. protocolVersion: protocolVersion,
  390. protocol: null
  391. }).merge(options);
  392. // expose state properties
  393. this.protocol = options.value.protocol;
  394. this.protocolVersion = options.value.protocolVersion;
  395. this.supports.binary = (this.protocolVersion != 'hixie-76');
  396. this.upgradeReq = req;
  397. this.readyState = WebSocket.CONNECTING;
  398. this._isServer = true;
  399. // establish connection
  400. if (options.value.protocolVersion == 'hixie-76') establishConnection.call(this, ReceiverHixie, SenderHixie, socket, upgradeHead);
  401. else establishConnection.call(this, Receiver, Sender, socket, upgradeHead);
  402. }
  403. function initAsClient(address, protocols, options) {
  404. options = new Options({
  405. origin: null,
  406. protocolVersion: protocolVersion,
  407. host: null,
  408. headers: null,
  409. protocol: null,
  410. agent: null,
  411. // ssl-related options
  412. pfx: null,
  413. key: null,
  414. passphrase: null,
  415. cert: null,
  416. ca: null,
  417. ciphers: null,
  418. rejectUnauthorized: null
  419. }).merge(options);
  420. if (options.value.protocolVersion != 8 && options.value.protocolVersion != 13) {
  421. throw new Error('unsupported protocol version');
  422. }
  423. // verify url and establish http class
  424. var serverUrl = url.parse(address);
  425. var isUnixSocket = serverUrl.protocol === 'ws+unix:';
  426. if (!serverUrl.host && !isUnixSocket) throw new Error('invalid url');
  427. var isSecure = serverUrl.protocol === 'wss:' || serverUrl.protocol === 'https:';
  428. var httpObj = isSecure ? https : http;
  429. var port = serverUrl.port || (isSecure ? 443 : 80);
  430. var auth = serverUrl.auth;
  431. // expose state properties
  432. this._isServer = false;
  433. this.url = address;
  434. this.protocolVersion = options.value.protocolVersion;
  435. this.supports.binary = (this.protocolVersion != 'hixie-76');
  436. // begin handshake
  437. var key = new Buffer(options.value.protocolVersion + '-' + Date.now()).toString('base64');
  438. var shasum = crypto.createHash('sha1');
  439. shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
  440. var expectedServerKey = shasum.digest('base64');
  441. var agent = options.value.agent;
  442. // node<=v0.4.x compatibility
  443. if (!agent && isNodeV4) {
  444. isNodeV4 = true;
  445. agent = new httpObj.Agent({
  446. host: serverUrl.hostname,
  447. port: port
  448. });
  449. }
  450. var headerHost = serverUrl.hostname;
  451. // Append port number to Host and Origin header, only if specified in the url and non-default
  452. if(serverUrl.port) {
  453. if((isSecure && (port != 443)) || (!isSecure && (port != 80))){
  454. headerHost = headerHost + ':' + port;
  455. }
  456. }
  457. var requestOptions = {
  458. port: port,
  459. host: serverUrl.hostname,
  460. headers: {
  461. 'Connection': 'Upgrade',
  462. 'Upgrade': 'websocket',
  463. 'Host': headerHost,
  464. 'Origin': headerHost,
  465. 'Sec-WebSocket-Version': options.value.protocolVersion,
  466. 'Sec-WebSocket-Key': key
  467. }
  468. };
  469. // If we have basic auth.
  470. if (auth) {
  471. requestOptions.headers['Authorization'] = 'Basic ' + new Buffer(auth).toString('base64');
  472. }
  473. if (options.value.protocol) {
  474. requestOptions.headers['Sec-WebSocket-Protocol'] = options.value.protocol;
  475. }
  476. if (options.value.host) {
  477. requestOptions.headers['Host'] = options.value.host;
  478. }
  479. if (options.value.headers) {
  480. for (var header in options.value.headers) {
  481. if (options.value.headers.hasOwnProperty(header)) {
  482. requestOptions.headers[header] = options.value.headers[header];
  483. }
  484. }
  485. }
  486. if (options.isDefinedAndNonNull('pfx')
  487. || options.isDefinedAndNonNull('key')
  488. || options.isDefinedAndNonNull('passphrase')
  489. || options.isDefinedAndNonNull('cert')
  490. || options.isDefinedAndNonNull('ca')
  491. || options.isDefinedAndNonNull('ciphers')
  492. || options.isDefinedAndNonNull('rejectUnauthorized')) {
  493. if (isNodeV4) {
  494. throw new Error('Client side certificates are not supported on Node 0.4.x');
  495. }
  496. if (options.isDefinedAndNonNull('pfx')) requestOptions.pfx = options.value.pfx;
  497. if (options.isDefinedAndNonNull('key')) requestOptions.key = options.value.key;
  498. if (options.isDefinedAndNonNull('passphrase')) requestOptions.passphrase = options.value.passphrase;
  499. if (options.isDefinedAndNonNull('cert')) requestOptions.cert = options.value.cert;
  500. if (options.isDefinedAndNonNull('ca')) requestOptions.ca = options.value.ca;
  501. if (options.isDefinedAndNonNull('ciphers')) requestOptions.ciphers = options.value.ciphers;
  502. if (options.isDefinedAndNonNull('rejectUnauthorized')) requestOptions.rejectUnauthorized = options.value.rejectUnauthorized;
  503. if (!agent) {
  504. // global agent ignores client side certificates
  505. agent = new httpObj.Agent(requestOptions);
  506. }
  507. }
  508. if (isNodeV4) {
  509. requestOptions.path = (serverUrl.pathname || '/') + (serverUrl.search || '');
  510. }
  511. else requestOptions.path = serverUrl.path || '/';
  512. if (agent) {
  513. requestOptions.agent = agent;
  514. }
  515. if (isUnixSocket) {
  516. requestOptions.socketPath = serverUrl.pathname;
  517. }
  518. if (options.value.origin) {
  519. if (options.value.protocolVersion < 13) requestOptions.headers['Sec-WebSocket-Origin'] = options.value.origin;
  520. else requestOptions.headers['Origin'] = options.value.origin;
  521. }
  522. var self = this;
  523. var req = httpObj.request(requestOptions);
  524. (isNodeV4 ? agent : req).on('error', function(error) {
  525. self.emit('error', error);
  526. cleanupWebsocketResources.call(this, error);
  527. });
  528. (isNodeV4 ? agent : req).once('response', function(res) {
  529. var error = new Error('unexpected server response (' + res.statusCode + ')');
  530. self.emit('error', error);
  531. cleanupWebsocketResources.call(this, error);
  532. });
  533. (isNodeV4 ? agent : req).once('upgrade', function(res, socket, upgradeHead) {
  534. if (self.readyState == WebSocket.CLOSED) {
  535. // client closed before server accepted connection
  536. self.emit('close');
  537. removeAllListeners(self);
  538. socket.end();
  539. return;
  540. }
  541. var serverKey = res.headers['sec-websocket-accept'];
  542. if (typeof serverKey == 'undefined' || serverKey !== expectedServerKey) {
  543. self.emit('error', 'invalid server key');
  544. removeAllListeners(self);
  545. socket.end();
  546. return;
  547. }
  548. var serverProt = res.headers['sec-websocket-protocol'];
  549. var protList = (options.value.protocol || "").split(/, */);
  550. var protError = null;
  551. if (!options.value.protocol && serverProt) {
  552. protError = 'server sent a subprotocol even though none requested';
  553. } else if (options.value.protocol && !serverProt) {
  554. protError = 'server sent no subprotocol even though requested';
  555. } else if (serverProt && protList.indexOf(serverProt) === -1) {
  556. protError = 'server responded with an invalid protocol';
  557. }
  558. if (protError) {
  559. self.emit('error', protError);
  560. removeAllListeners(self);
  561. socket.end();
  562. return;
  563. } else if (serverProt) {
  564. self.protocol = serverProt;
  565. }
  566. establishConnection.call(self, Receiver, Sender, socket, upgradeHead);
  567. // perform cleanup on http resources
  568. removeAllListeners(isNodeV4 ? agent : req);
  569. req = null;
  570. agent = null;
  571. });
  572. req.end();
  573. this.readyState = WebSocket.CONNECTING;
  574. }
  575. function establishConnection(ReceiverClass, SenderClass, socket, upgradeHead) {
  576. this._socket = socket;
  577. socket.setTimeout(0);
  578. socket.setNoDelay(true);
  579. var self = this;
  580. this._receiver = new ReceiverClass();
  581. // socket cleanup handlers
  582. socket.on('end', cleanupWebsocketResources.bind(this));
  583. socket.on('close', cleanupWebsocketResources.bind(this));
  584. socket.on('error', cleanupWebsocketResources.bind(this));
  585. // ensure that the upgradeHead is added to the receiver
  586. function firstHandler(data) {
  587. if (self.readyState != WebSocket.OPEN) return;
  588. if (upgradeHead && upgradeHead.length > 0) {
  589. self.bytesReceived += upgradeHead.length;
  590. var head = upgradeHead;
  591. upgradeHead = null;
  592. self._receiver.add(head);
  593. }
  594. dataHandler = realHandler;
  595. if (data) {
  596. self.bytesReceived += data.length;
  597. self._receiver.add(data);
  598. }
  599. }
  600. // subsequent packets are pushed straight to the receiver
  601. function realHandler(data) {
  602. if (data) self.bytesReceived += data.length;
  603. self._receiver.add(data);
  604. }
  605. var dataHandler = firstHandler;
  606. // if data was passed along with the http upgrade,
  607. // this will schedule a push of that on to the receiver.
  608. // this has to be done on next tick, since the caller
  609. // hasn't had a chance to set event handlers on this client
  610. // object yet.
  611. process.nextTick(firstHandler);
  612. // receiver event handlers
  613. self._receiver.ontext = function (data, flags) {
  614. flags = flags || {};
  615. self.emit('message', data, flags);
  616. };
  617. self._receiver.onbinary = function (data, flags) {
  618. flags = flags || {};
  619. flags.binary = true;
  620. self.emit('message', data, flags);
  621. };
  622. self._receiver.onping = function(data, flags) {
  623. flags = flags || {};
  624. self.pong(data, {mask: !self._isServer, binary: flags.binary === true}, true);
  625. self.emit('ping', data, flags);
  626. };
  627. self._receiver.onpong = function(data, flags) {
  628. self.emit('pong', data, flags);
  629. };
  630. self._receiver.onclose = function(code, data, flags) {
  631. flags = flags || {};
  632. self.close(code, data);
  633. };
  634. self._receiver.onerror = function(reason, errorCode) {
  635. // close the connection when the receiver reports a HyBi error code
  636. self.close(typeof errorCode != 'undefined' ? errorCode : 1002, '');
  637. self.emit('error', reason, errorCode);
  638. };
  639. // finalize the client
  640. this._sender = new SenderClass(socket);
  641. this._sender.on('error', function(error) {
  642. self.close(1002, '');
  643. self.emit('error', error);
  644. });
  645. this.readyState = WebSocket.OPEN;
  646. this.emit('open');
  647. socket.on('data', dataHandler);
  648. }
  649. function startQueue(instance) {
  650. instance._queue = instance._queue || [];
  651. }
  652. function executeQueueSends(instance) {
  653. var queue = instance._queue;
  654. if (typeof queue == 'undefined') return;
  655. delete instance._queue;
  656. for (var i = 0, l = queue.length; i < l; ++i) {
  657. queue[i]();
  658. }
  659. }
  660. function sendStream(instance, stream, options, cb) {
  661. stream.on('data', function(data) {
  662. if (instance.readyState != WebSocket.OPEN) {
  663. if (typeof cb == 'function') cb(new Error('not opened'));
  664. else {
  665. delete instance._queue;
  666. instance.emit('error', new Error('not opened'));
  667. }
  668. return;
  669. }
  670. options.fin = false;
  671. instance._sender.send(data, options);
  672. });
  673. stream.on('end', function() {
  674. if (instance.readyState != WebSocket.OPEN) {
  675. if (typeof cb == 'function') cb(new Error('not opened'));
  676. else {
  677. delete instance._queue;
  678. instance.emit('error', new Error('not opened'));
  679. }
  680. return;
  681. }
  682. options.fin = true;
  683. instance._sender.send(null, options);
  684. if (typeof cb == 'function') cb(null);
  685. });
  686. }
  687. function cleanupWebsocketResources(error) {
  688. if (this.readyState == WebSocket.CLOSED) return;
  689. var emitClose = this.readyState != WebSocket.CONNECTING;
  690. this.readyState = WebSocket.CLOSED;
  691. clearTimeout(this._closeTimer);
  692. this._closeTimer = null;
  693. if (emitClose) this.emit('close', this._closeCode || 1000, this._closeMessage || '');
  694. if (this._socket) {
  695. removeAllListeners(this._socket);
  696. // catch all socket error after removing all standard handlers
  697. var socket = this._socket;
  698. this._socket.on('error', function() {
  699. try { socket.destroy(); } catch (e) {}
  700. });
  701. try {
  702. if (!error) this._socket.end();
  703. else this._socket.destroy();
  704. }
  705. catch (e) { /* Ignore termination errors */ }
  706. this._socket = null;
  707. }
  708. if (this._sender) {
  709. removeAllListeners(this._sender);
  710. this._sender = null;
  711. }
  712. if (this._receiver) {
  713. this._receiver.cleanup();
  714. this._receiver = null;
  715. }
  716. removeAllListeners(this);
  717. this.on('error', function() {}); // catch all errors after this
  718. delete this._queue;
  719. }
  720. function removeAllListeners(instance) {
  721. if (isNodeV4) {
  722. // node v4 doesn't *actually* remove all listeners globally,
  723. // so we do that instead
  724. instance._events = {};
  725. }
  726. else instance.removeAllListeners();
  727. }