connect.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. 'use strict'
  2. const net = require('net')
  3. const assert = require('assert')
  4. const util = require('./util')
  5. const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
  6. let tls // include tls conditionally since it is not always available
  7. // TODO: session re-use does not wait for the first
  8. // connection to resolve the session and might therefore
  9. // resolve the same servername multiple times even when
  10. // re-use is enabled.
  11. let SessionCache
  12. if (global.FinalizationRegistry) {
  13. SessionCache = class WeakSessionCache {
  14. constructor (maxCachedSessions) {
  15. this._maxCachedSessions = maxCachedSessions
  16. this._sessionCache = new Map()
  17. this._sessionRegistry = new global.FinalizationRegistry((key) => {
  18. if (this._sessionCache.size < this._maxCachedSessions) {
  19. return
  20. }
  21. const ref = this._sessionCache.get(key)
  22. if (ref !== undefined && ref.deref() === undefined) {
  23. this._sessionCache.delete(key)
  24. }
  25. })
  26. }
  27. get (sessionKey) {
  28. const ref = this._sessionCache.get(sessionKey)
  29. return ref ? ref.deref() : null
  30. }
  31. set (sessionKey, session) {
  32. if (this._maxCachedSessions === 0) {
  33. return
  34. }
  35. this._sessionCache.set(sessionKey, new WeakRef(session))
  36. this._sessionRegistry.register(session, sessionKey)
  37. }
  38. }
  39. } else {
  40. SessionCache = class SimpleSessionCache {
  41. constructor (maxCachedSessions) {
  42. this._maxCachedSessions = maxCachedSessions
  43. this._sessionCache = new Map()
  44. }
  45. get (sessionKey) {
  46. return this._sessionCache.get(sessionKey)
  47. }
  48. set (sessionKey, session) {
  49. if (this._maxCachedSessions === 0) {
  50. return
  51. }
  52. if (this._sessionCache.size >= this._maxCachedSessions) {
  53. // remove the oldest session
  54. const { value: oldestKey } = this._sessionCache.keys().next()
  55. this._sessionCache.delete(oldestKey)
  56. }
  57. this._sessionCache.set(sessionKey, session)
  58. }
  59. }
  60. }
  61. function buildConnector ({ maxCachedSessions, socketPath, timeout, ...opts }) {
  62. if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) {
  63. throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero')
  64. }
  65. const options = { path: socketPath, ...opts }
  66. const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions)
  67. timeout = timeout == null ? 10e3 : timeout
  68. return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) {
  69. let socket
  70. if (protocol === 'https:') {
  71. if (!tls) {
  72. tls = require('tls')
  73. }
  74. servername = servername || options.servername || util.getServerName(host) || null
  75. const sessionKey = servername || hostname
  76. const session = sessionCache.get(sessionKey) || null
  77. assert(sessionKey)
  78. socket = tls.connect({
  79. highWaterMark: 16384, // TLS in node can't have bigger HWM anyway...
  80. ...options,
  81. servername,
  82. session,
  83. localAddress,
  84. socket: httpSocket, // upgrade socket connection
  85. port: port || 443,
  86. host: hostname
  87. })
  88. socket
  89. .on('session', function (session) {
  90. // TODO (fix): Can a session become invalid once established? Don't think so?
  91. sessionCache.set(sessionKey, session)
  92. })
  93. } else {
  94. assert(!httpSocket, 'httpSocket can only be sent on TLS update')
  95. socket = net.connect({
  96. highWaterMark: 64 * 1024, // Same as nodejs fs streams.
  97. ...options,
  98. localAddress,
  99. port: port || 80,
  100. host: hostname
  101. })
  102. }
  103. // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket
  104. if (options.keepAlive == null || options.keepAlive) {
  105. const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay
  106. socket.setKeepAlive(true, keepAliveInitialDelay)
  107. }
  108. const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout)
  109. socket
  110. .setNoDelay(true)
  111. .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () {
  112. cancelTimeout()
  113. if (callback) {
  114. const cb = callback
  115. callback = null
  116. cb(null, this)
  117. }
  118. })
  119. .on('error', function (err) {
  120. cancelTimeout()
  121. if (callback) {
  122. const cb = callback
  123. callback = null
  124. cb(err)
  125. }
  126. })
  127. return socket
  128. }
  129. }
  130. function setupTimeout (onConnectTimeout, timeout) {
  131. if (!timeout) {
  132. return () => {}
  133. }
  134. let s1 = null
  135. let s2 = null
  136. const timeoutId = setTimeout(() => {
  137. // setImmediate is added to make sure that we priotorise socket error events over timeouts
  138. s1 = setImmediate(() => {
  139. if (process.platform === 'win32') {
  140. // Windows needs an extra setImmediate probably due to implementation differences in the socket logic
  141. s2 = setImmediate(() => onConnectTimeout())
  142. } else {
  143. onConnectTimeout()
  144. }
  145. })
  146. }, timeout)
  147. return () => {
  148. clearTimeout(timeoutId)
  149. clearImmediate(s1)
  150. clearImmediate(s2)
  151. }
  152. }
  153. function onConnectTimeout (socket) {
  154. util.destroy(socket, new ConnectTimeoutError())
  155. }
  156. module.exports = buildConnector