websocket.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { Transport } from "../transport.js";
  2. import parseqs from "parseqs";
  3. import yeast from "yeast";
  4. import { pick } from "../util.js";
  5. import { defaultBinaryType, nextTick, usingBrowserWebSocket, WebSocket } from "./websocket-constructor.js";
  6. import debugModule from "debug"; // debug()
  7. import { encodePacket } from "engine.io-parser";
  8. const debug = debugModule("engine.io-client:websocket"); // debug()
  9. // detect ReactNative environment
  10. const isReactNative = typeof navigator !== "undefined" &&
  11. typeof navigator.product === "string" &&
  12. navigator.product.toLowerCase() === "reactnative";
  13. export class WS extends Transport {
  14. /**
  15. * WebSocket transport constructor.
  16. *
  17. * @api {Object} connection options
  18. * @api public
  19. */
  20. constructor(opts) {
  21. super(opts);
  22. this.supportsBinary = !opts.forceBase64;
  23. }
  24. /**
  25. * Transport name.
  26. *
  27. * @api public
  28. */
  29. get name() {
  30. return "websocket";
  31. }
  32. /**
  33. * Opens socket.
  34. *
  35. * @api private
  36. */
  37. doOpen() {
  38. if (!this.check()) {
  39. // let probe timeout
  40. return;
  41. }
  42. const uri = this.uri();
  43. const protocols = this.opts.protocols;
  44. // React Native only supports the 'headers' option, and will print a warning if anything else is passed
  45. const opts = isReactNative
  46. ? {}
  47. : pick(this.opts, "agent", "perMessageDeflate", "pfx", "key", "passphrase", "cert", "ca", "ciphers", "rejectUnauthorized", "localAddress", "protocolVersion", "origin", "maxPayload", "family", "checkServerIdentity");
  48. if (this.opts.extraHeaders) {
  49. opts.headers = this.opts.extraHeaders;
  50. }
  51. try {
  52. this.ws =
  53. usingBrowserWebSocket && !isReactNative
  54. ? protocols
  55. ? new WebSocket(uri, protocols)
  56. : new WebSocket(uri)
  57. : new WebSocket(uri, protocols, opts);
  58. }
  59. catch (err) {
  60. return this.emit("error", err);
  61. }
  62. this.ws.binaryType = this.socket.binaryType || defaultBinaryType;
  63. this.addEventListeners();
  64. }
  65. /**
  66. * Adds event listeners to the socket
  67. *
  68. * @api private
  69. */
  70. addEventListeners() {
  71. this.ws.onopen = () => {
  72. if (this.opts.autoUnref) {
  73. this.ws._socket.unref();
  74. }
  75. this.onOpen();
  76. };
  77. this.ws.onclose = this.onClose.bind(this);
  78. this.ws.onmessage = ev => this.onData(ev.data);
  79. this.ws.onerror = e => this.onError("websocket error", e);
  80. }
  81. /**
  82. * Writes data to socket.
  83. *
  84. * @param {Array} array of packets.
  85. * @api private
  86. */
  87. write(packets) {
  88. this.writable = false;
  89. // encodePacket efficient as it uses WS framing
  90. // no need for encodePayload
  91. for (let i = 0; i < packets.length; i++) {
  92. const packet = packets[i];
  93. const lastPacket = i === packets.length - 1;
  94. encodePacket(packet, this.supportsBinary, data => {
  95. // always create a new object (GH-437)
  96. const opts = {};
  97. if (!usingBrowserWebSocket) {
  98. if (packet.options) {
  99. opts.compress = packet.options.compress;
  100. }
  101. if (this.opts.perMessageDeflate) {
  102. const len = "string" === typeof data ? Buffer.byteLength(data) : data.length;
  103. if (len < this.opts.perMessageDeflate.threshold) {
  104. opts.compress = false;
  105. }
  106. }
  107. }
  108. // Sometimes the websocket has already been closed but the browser didn't
  109. // have a chance of informing us about it yet, in that case send will
  110. // throw an error
  111. try {
  112. if (usingBrowserWebSocket) {
  113. // TypeError is thrown when passing the second argument on Safari
  114. this.ws.send(data);
  115. }
  116. else {
  117. this.ws.send(data, opts);
  118. }
  119. }
  120. catch (e) {
  121. debug("websocket closed before onclose event");
  122. }
  123. if (lastPacket) {
  124. // fake drain
  125. // defer to next tick to allow Socket to clear writeBuffer
  126. nextTick(() => {
  127. this.writable = true;
  128. this.emit("drain");
  129. }, this.setTimeoutFn);
  130. }
  131. });
  132. }
  133. }
  134. /**
  135. * Closes socket.
  136. *
  137. * @api private
  138. */
  139. doClose() {
  140. if (typeof this.ws !== "undefined") {
  141. this.ws.close();
  142. this.ws = null;
  143. }
  144. }
  145. /**
  146. * Generates uri for connection.
  147. *
  148. * @api private
  149. */
  150. uri() {
  151. let query = this.query || {};
  152. const schema = this.opts.secure ? "wss" : "ws";
  153. let port = "";
  154. // avoid port if default for schema
  155. if (this.opts.port &&
  156. (("wss" === schema && Number(this.opts.port) !== 443) ||
  157. ("ws" === schema && Number(this.opts.port) !== 80))) {
  158. port = ":" + this.opts.port;
  159. }
  160. // append timestamp to URI
  161. if (this.opts.timestampRequests) {
  162. query[this.opts.timestampParam] = yeast();
  163. }
  164. // communicate binary support capabilities
  165. if (!this.supportsBinary) {
  166. query.b64 = 1;
  167. }
  168. const encodedQuery = parseqs.encode(query);
  169. const ipv6 = this.opts.hostname.indexOf(":") !== -1;
  170. return (schema +
  171. "://" +
  172. (ipv6 ? "[" + this.opts.hostname + "]" : this.opts.hostname) +
  173. port +
  174. this.opts.path +
  175. (encodedQuery.length ? "?" + encodedQuery : ""));
  176. }
  177. /**
  178. * Feature detection for WebSocket.
  179. *
  180. * @return {Boolean} whether this transport is available.
  181. * @api public
  182. */
  183. check() {
  184. return (!!WebSocket &&
  185. !("__initialize" in WebSocket && this.name === WS.prototype.name));
  186. }
  187. }