| 'use strict' |
| |
| const net = require('net') |
| const assert = require('assert') |
| const util = require('./util') |
| const { InvalidArgumentError, ConnectTimeoutError } = require('./errors') |
| |
| let tls // include tls conditionally since it is not always available |
| |
| // TODO: session re-use does not wait for the first |
| // connection to resolve the session and might therefore |
| // resolve the same servername multiple times even when |
| // re-use is enabled. |
| |
| let SessionCache |
| // FIXME: remove workaround when the Node bug is fixed |
| // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308 |
| if (global.FinalizationRegistry && !process.env.NODE_V8_COVERAGE) { |
| SessionCache = class WeakSessionCache { |
| constructor (maxCachedSessions) { |
| this._maxCachedSessions = maxCachedSessions |
| this._sessionCache = new Map() |
| this._sessionRegistry = new global.FinalizationRegistry((key) => { |
| if (this._sessionCache.size < this._maxCachedSessions) { |
| return |
| } |
| |
| const ref = this._sessionCache.get(key) |
| if (ref !== undefined && ref.deref() === undefined) { |
| this._sessionCache.delete(key) |
| } |
| }) |
| } |
| |
| get (sessionKey) { |
| const ref = this._sessionCache.get(sessionKey) |
| return ref ? ref.deref() : null |
| } |
| |
| set (sessionKey, session) { |
| if (this._maxCachedSessions === 0) { |
| return |
| } |
| |
| this._sessionCache.set(sessionKey, new WeakRef(session)) |
| this._sessionRegistry.register(session, sessionKey) |
| } |
| } |
| } else { |
| SessionCache = class SimpleSessionCache { |
| constructor (maxCachedSessions) { |
| this._maxCachedSessions = maxCachedSessions |
| this._sessionCache = new Map() |
| } |
| |
| get (sessionKey) { |
| return this._sessionCache.get(sessionKey) |
| } |
| |
| set (sessionKey, session) { |
| if (this._maxCachedSessions === 0) { |
| return |
| } |
| |
| if (this._sessionCache.size >= this._maxCachedSessions) { |
| // remove the oldest session |
| const { value: oldestKey } = this._sessionCache.keys().next() |
| this._sessionCache.delete(oldestKey) |
| } |
| |
| this._sessionCache.set(sessionKey, session) |
| } |
| } |
| } |
| |
| function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, ...opts }) { |
| if (maxCachedSessions != null && (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)) { |
| throw new InvalidArgumentError('maxCachedSessions must be a positive integer or zero') |
| } |
| |
| const options = { path: socketPath, ...opts } |
| const sessionCache = new SessionCache(maxCachedSessions == null ? 100 : maxCachedSessions) |
| timeout = timeout == null ? 10e3 : timeout |
| allowH2 = allowH2 != null ? allowH2 : false |
| return function connect ({ hostname, host, protocol, port, servername, localAddress, httpSocket }, callback) { |
| let socket |
| if (protocol === 'https:') { |
| if (!tls) { |
| tls = require('tls') |
| } |
| servername = servername || options.servername || util.getServerName(host) || null |
| |
| const sessionKey = servername || hostname |
| const session = sessionCache.get(sessionKey) || null |
| |
| assert(sessionKey) |
| |
| socket = tls.connect({ |
| highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... |
| ...options, |
| servername, |
| session, |
| localAddress, |
| // TODO(HTTP/2): Add support for h2c |
| ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'], |
| socket: httpSocket, // upgrade socket connection |
| port: port || 443, |
| host: hostname |
| }) |
| |
| socket |
| .on('session', function (session) { |
| // TODO (fix): Can a session become invalid once established? Don't think so? |
| sessionCache.set(sessionKey, session) |
| }) |
| } else { |
| assert(!httpSocket, 'httpSocket can only be sent on TLS update') |
| socket = net.connect({ |
| highWaterMark: 64 * 1024, // Same as nodejs fs streams. |
| ...options, |
| localAddress, |
| port: port || 80, |
| host: hostname |
| }) |
| } |
| |
| // Set TCP keep alive options on the socket here instead of in connect() for the case of assigning the socket |
| if (options.keepAlive == null || options.keepAlive) { |
| const keepAliveInitialDelay = options.keepAliveInitialDelay === undefined ? 60e3 : options.keepAliveInitialDelay |
| socket.setKeepAlive(true, keepAliveInitialDelay) |
| } |
| |
| const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) |
| |
| socket |
| .setNoDelay(true) |
| .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { |
| cancelTimeout() |
| |
| if (callback) { |
| const cb = callback |
| callback = null |
| cb(null, this) |
| } |
| }) |
| .on('error', function (err) { |
| cancelTimeout() |
| |
| if (callback) { |
| const cb = callback |
| callback = null |
| cb(err) |
| } |
| }) |
| |
| return socket |
| } |
| } |
| |
| function setupTimeout (onConnectTimeout, timeout) { |
| if (!timeout) { |
| return () => {} |
| } |
| |
| let s1 = null |
| let s2 = null |
| const timeoutId = setTimeout(() => { |
| // setImmediate is added to make sure that we priotorise socket error events over timeouts |
| s1 = setImmediate(() => { |
| if (process.platform === 'win32') { |
| // Windows needs an extra setImmediate probably due to implementation differences in the socket logic |
| s2 = setImmediate(() => onConnectTimeout()) |
| } else { |
| onConnectTimeout() |
| } |
| }) |
| }, timeout) |
| return () => { |
| clearTimeout(timeoutId) |
| clearImmediate(s1) |
| clearImmediate(s2) |
| } |
| } |
| |
| function onConnectTimeout (socket) { |
| util.destroy(socket, new ConnectTimeoutError()) |
| } |
| |
| module.exports = buildConnector |