websocket.js 6.0 KB

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