happy-eyeballs.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.createSocket = createSocket;
  6. exports.httpsHappyEyeballsAgent = exports.httpHappyEyeballsAgent = void 0;
  7. var dns = _interopRequireWildcard(require("dns"));
  8. var http = _interopRequireWildcard(require("http"));
  9. var https = _interopRequireWildcard(require("https"));
  10. var net = _interopRequireWildcard(require("net"));
  11. var tls = _interopRequireWildcard(require("tls"));
  12. var _manualPromise = require("./manualPromise");
  13. function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
  14. function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
  15. /**
  16. * Copyright (c) Microsoft Corporation.
  17. *
  18. * Licensed under the Apache License, Version 2.0 (the "License");
  19. * you may not use this file except in compliance with the License.
  20. * You may obtain a copy of the License at
  21. *
  22. * http://www.apache.org/licenses/LICENSE-2.0
  23. *
  24. * Unless required by applicable law or agreed to in writing, software
  25. * distributed under the License is distributed on an "AS IS" BASIS,
  26. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  27. * See the License for the specific language governing permissions and
  28. * limitations under the License.
  29. */
  30. // Implementation(partial) of Happy Eyeballs 2 algorithm described in
  31. // https://www.rfc-editor.org/rfc/rfc8305
  32. // Same as in Chromium (https://source.chromium.org/chromium/chromium/src/+/5666ff4f5077a7e2f72902f3a95f5d553ea0d88d:net/socket/transport_connect_job.cc;l=102)
  33. const connectionAttemptDelayMs = 300;
  34. class HttpHappyEyeballsAgent extends http.Agent {
  35. createConnection(options, oncreate) {
  36. // There is no ambiguity in case of IP address.
  37. if (net.isIP(clientRequestArgsToHostName(options))) return net.createConnection(options);
  38. createConnectionAsync(options, oncreate, /* useTLS */false).catch(err => oncreate === null || oncreate === void 0 ? void 0 : oncreate(err));
  39. }
  40. }
  41. class HttpsHappyEyeballsAgent extends https.Agent {
  42. createConnection(options, oncreate) {
  43. // There is no ambiguity in case of IP address.
  44. if (net.isIP(clientRequestArgsToHostName(options))) return tls.connect(options);
  45. createConnectionAsync(options, oncreate, /* useTLS */true).catch(err => oncreate === null || oncreate === void 0 ? void 0 : oncreate(err));
  46. }
  47. }
  48. const httpsHappyEyeballsAgent = exports.httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent();
  49. const httpHappyEyeballsAgent = exports.httpHappyEyeballsAgent = new HttpHappyEyeballsAgent();
  50. async function createSocket(host, port) {
  51. return new Promise((resolve, reject) => {
  52. if (net.isIP(host)) {
  53. const socket = net.createConnection({
  54. host,
  55. port
  56. });
  57. socket.on('connect', () => resolve(socket));
  58. socket.on('error', error => reject(error));
  59. } else {
  60. createConnectionAsync({
  61. host,
  62. port
  63. }, (err, socket) => {
  64. if (err) reject(err);
  65. if (socket) resolve(socket);
  66. }, /* useTLS */false).catch(err => reject(err));
  67. }
  68. });
  69. }
  70. async function createConnectionAsync(options, oncreate, useTLS) {
  71. const lookup = options.__testHookLookup || lookupAddresses;
  72. const hostname = clientRequestArgsToHostName(options);
  73. const addresses = await lookup(hostname);
  74. const sockets = new Set();
  75. let firstError;
  76. let errorCount = 0;
  77. const handleError = (socket, err) => {
  78. var _firstError;
  79. if (!sockets.delete(socket)) return;
  80. ++errorCount;
  81. (_firstError = firstError) !== null && _firstError !== void 0 ? _firstError : firstError = err;
  82. if (errorCount === addresses.length) oncreate === null || oncreate === void 0 ? void 0 : oncreate(firstError);
  83. };
  84. const connected = new _manualPromise.ManualPromise();
  85. for (const {
  86. address
  87. } of addresses) {
  88. const socket = useTLS ? tls.connect({
  89. ...options,
  90. port: options.port,
  91. host: address,
  92. servername: hostname
  93. }) : net.createConnection({
  94. ...options,
  95. port: options.port,
  96. host: address
  97. });
  98. // Each socket may fire only one of 'connect', 'timeout' or 'error' events.
  99. // None of these events are fired after socket.destroy() is called.
  100. socket.on('connect', () => {
  101. connected.resolve();
  102. oncreate === null || oncreate === void 0 ? void 0 : oncreate(null, socket);
  103. // TODO: Cache the result?
  104. // Close other outstanding sockets.
  105. sockets.delete(socket);
  106. for (const s of sockets) s.destroy();
  107. sockets.clear();
  108. });
  109. socket.on('timeout', () => {
  110. // Timeout is not an error, so we have to manually close the socket.
  111. socket.destroy();
  112. handleError(socket, new Error('Connection timeout'));
  113. });
  114. socket.on('error', e => handleError(socket, e));
  115. sockets.add(socket);
  116. await Promise.race([connected, new Promise(f => setTimeout(f, connectionAttemptDelayMs))]);
  117. if (connected.isDone()) break;
  118. }
  119. }
  120. async function lookupAddresses(hostname) {
  121. const addresses = await dns.promises.lookup(hostname, {
  122. all: true,
  123. family: 0,
  124. verbatim: true
  125. });
  126. let firstFamily = addresses.filter(({
  127. family
  128. }) => family === 6);
  129. let secondFamily = addresses.filter(({
  130. family
  131. }) => family === 4);
  132. // Make sure first address in the list is the same as in the original order.
  133. if (firstFamily.length && firstFamily[0] !== addresses[0]) {
  134. const tmp = firstFamily;
  135. firstFamily = secondFamily;
  136. secondFamily = tmp;
  137. }
  138. const result = [];
  139. // Alternate ipv6 and ipv4 addresses.
  140. for (let i = 0; i < Math.max(firstFamily.length, secondFamily.length); i++) {
  141. if (firstFamily[i]) result.push(firstFamily[i]);
  142. if (secondFamily[i]) result.push(secondFamily[i]);
  143. }
  144. return result;
  145. }
  146. function clientRequestArgsToHostName(options) {
  147. if (options.hostname) return options.hostname;
  148. if (options.host) return options.host;
  149. throw new Error('Either options.hostname or options.host must be provided');
  150. }