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