connect.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. 'use strict'
  2. const net = require('node:net')
  3. const assert = require('node:assert')
  4. const util = require('./util')
  5. const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
  6. const timers = require('../util/timers')
  7. function noop () {}
  8. let tls // include tls conditionally since it is not always available
  9. // TODO: session re-use does not wait for the first
  10. // connection to resolve the session and might therefore
  11. // resolve the same servername multiple times even when
  12. // re-use is enabled.
  13. let SessionCache
  14. // FIXME: remove workaround when the Node bug is fixed
  15. // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
  16. if (global.FinalizationRegistry && !(process.env.NODE_V8_COVERAGE || process.env.UNDICI_NO_FG)) {
  17. SessionCache = class WeakSessionCache {
  18. constructor (maxCachedSessions) {
  19. this._maxCachedSessions = maxCachedSessions
  20. this._sessionCache = new Map()
  21. this._sessionRegistry = new global.FinalizationRegistry((key) => {
  22. if (this._sessionCache.size < this._maxCachedSessions) {
  23. return
  24. }
  25. const ref = this._sessionCache.get(key)
  26. if (ref !== undefined && ref.deref() === undefined) {
  27. this._sessionCache.delete(key)
  28. }
  29. })
  30. }
  31. get (sessionKey) {
  32. const ref = this._sessionCache.get(sessionKey)
  33. return ref ? ref.deref() : null
  34. }
  35. set (sessionKey, session) {
  36. if (this._maxCachedSessions === 0) {
  37. return
  38. }
  39. this._sessionCache.set(sessionKey, new WeakRef(session))
  40. this._sessionRegistry.register(session, sessionKey)
  41. }
  42. }
  43. } else {
  44. SessionCache = class SimpleSessionCache {
  45. constructor (maxCachedSessions) {
  46. this._maxCachedSessions = maxCachedSessions
  47. this._sessionCache = new Map()
  48. }
  49. get (sessionKey) {
  50. return this._sessionCache.get(sessionKey)
  51. }
  52. set (sessionKey, session) {
  53. if (this._maxCachedSessions === 0) {
  54. return
  55. }
  56. if (this._sessionCache.size >= this._maxCachedSessions) {
  57. // remove the oldest session
  58. const { value: oldestKey } = this._sessionCache.keys().next()
  59. this._sessionCache.delete(oldestKey)
  60. }
  61. this._sessionCache.set(sessionKey, session)
  62. }
  63. }
  64. }
  65. function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, session: customSession, ...opts }) {
  66. if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
  67. throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
  68. }
  69. const options = { path: socketPath, ...opts }
  70. const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
  71. timeout = timeout == null ? 10e3 : timeout
  72. allowH2 = allowH2 != null ? allowH2 : false
  73. return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
  74. let socket
  75. if (protocol === 'https:') {
  76. if (!tls) {
  77. tls = require('node:tls')
  78. }
  79. servername = servername || options.servername || util.getServerName(host) || null
  80. const sessionKey = servername || hostname
  81. assert(sessionKey)
  82. const session = customSession || sessionCache.get(sessionKey) || null
  83. port = port || 443
  84. socket = tls.connect({
  85. highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
  86. ...options,
  87. servername,
  88. session,
  89. localAddress,
  90. // TODO(HTTP/2): Add support for h2c
  91. ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'],
  92. socket: httpSocket, // upgrade socket connection
  93. port,
  94. host: hostname
  95. })
  96. socket
  97. .on('session', function (session) {
  98. // TODO (fix): Can a session become invalid once established? Don't think so?
  99. sessionCache.set(sessionKey, session)
  100. })
  101. } else {
  102. assert(!httpSocket, 'httpSocket can only be sent on TLS update')
  103. port = port || 80
  104. socket = net.connect({
  105. highWaterMark: 64 * 1024, // Same as nodejs fs streams.
  106. ...options,
  107. localAddress,
  108. port,
  109. host: hostname
  110. })
  111. }
  112. // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
  113. if (options.keepAlive == null || options.keepAlive) {
  114. const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay
  115. socket.setKeepAlive(true, keepAliveInitialDelay)
  116. }
  117. const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port })
  118. socket
  119. .setNoDelay(true)
  120. .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
  121. queueMicrotask(clearConnectTimeout)
  122. if (callback) {
  123. const cb = callback
  124. callback = null
  125. cb(null, this)
  126. }
  127. })
  128. .on('error', function (err) {
  129. queueMicrotask(clearConnectTimeout)
  130. if (callback) {
  131. const cb = callback
  132. callback = null
  133. cb(err)
  134. }
  135. })
  136. return socket
  137. }
  138. }
  139. /**
  140. * @param {WeakRef<net.Socket>} socketWeakRef
  141. * @param {object} opts
  142. * @param {number} opts.timeout
  143. * @param {string} opts.hostname
  144. * @param {number} opts.port
  145. * @returns {() => void}
  146. */
  147. const setupConnectTimeout = process.platform === 'win32'
  148. ? (socketWeakRef, opts) => {
  149. if (!opts.timeout) {
  150. return noop
  151. }
  152. let s1 = null
  153. let s2 = null
  154. const fastTimer = timers.setFastTimeout(() => {
  155. // setImmediate is added to make sure that we prioritize socket error events over timeouts
  156. s1 = setImmediate(() => {
  157. // Windows needs an extra setImmediate probably due to implementation differences in the socket logic
  158. s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts))
  159. })
  160. }, opts.timeout)
  161. return () => {
  162. timers.clearFastTimeout(fastTimer)
  163. clearImmediate(s1)
  164. clearImmediate(s2)
  165. }
  166. }
  167. : (socketWeakRef, opts) => {
  168. if (!opts.timeout) {
  169. return noop
  170. }
  171. let s1 = null
  172. const fastTimer = timers.setFastTimeout(() => {
  173. // setImmediate is added to make sure that we prioritize socket error events over timeouts
  174. s1 = setImmediate(() => {
  175. onConnectTimeout(socketWeakRef.deref(), opts)
  176. })
  177. }, opts.timeout)
  178. return () => {
  179. timers.clearFastTimeout(fastTimer)
  180. clearImmediate(s1)
  181. }
  182. }
  183. /**
  184. * @param {net.Socket} socket
  185. * @param {object} opts
  186. * @param {number} opts.timeout
  187. * @param {string} opts.hostname
  188. * @param {number} opts.port
  189. */
  190. function onConnectTimeout (socket, opts) {
  191. // The socket could be already garbage collected
  192. if (socket == null) {
  193. return
  194. }
  195. let message = 'Connect Timeout Error'
  196. if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
  197. message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},`
  198. } else {
  199. message += ` (attempted address: ${opts.hostname}:${opts.port},`
  200. }
  201. message += ` timeout: ${opts.timeout}ms)`
  202. util.destroy(socket, new ConnectTimeoutError(message))
  203. }
  204. module.exports = buildConnector