123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- '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
- if (global.FinalizationRegistry) {
- 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 ({ 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
- 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,
- 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
|