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