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