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