1// Copyright Joyent, Inc. and other Node contributors. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a 4// copy of this software and associated documentation files (the 5// "Software"), to deal in the Software without restriction, including 6// without limitation the rights to use, copy, modify, merge, publish, 7// distribute, sublicense, and/or sell copies of the Software, and to permit 8// persons to whom the Software is furnished to do so, subject to the 9// following conditions: 10// 11// The above copyright notice and this permission notice shall be included 12// in all copies or substantial portions of the Software. 13// 14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20// USE OR OTHER DEALINGS IN THE SOFTWARE. 21 22'use strict'; 23 24const { 25 ArrayPrototypeIndexOf, 26 ArrayPrototypePush, 27 ArrayPrototypeShift, 28 ArrayPrototypeSplice, 29 ArrayPrototypeUnshift, 30 FunctionPrototypeCall, 31 JSONStringify, 32 ObjectAssign, 33 ObjectSetPrototypeOf, 34 ReflectConstruct, 35} = primordials; 36 37const { 38 assertCrypto, 39 kEmptyObject, 40} = require('internal/util'); 41assertCrypto(); 42 43const tls = require('tls'); 44const { Agent: HttpAgent } = require('_http_agent'); 45const { 46 Server: HttpServer, 47 setupConnectionsTracking, 48 storeHTTPOptions, 49 _connectionListener, 50} = require('_http_server'); 51const { ClientRequest } = require('_http_client'); 52let debug = require('internal/util/debuglog').debuglog('https', (fn) => { 53 debug = fn; 54}); 55const { URL, urlToHttpOptions, isURL } = require('internal/url'); 56 57function Server(opts, requestListener) { 58 if (!(this instanceof Server)) return new Server(opts, requestListener); 59 60 if (typeof opts === 'function') { 61 requestListener = opts; 62 opts = undefined; 63 } 64 opts = { ...opts }; 65 66 if (!opts.ALPNProtocols) { 67 // http/1.0 is not defined as Protocol IDs in IANA 68 // https://www.iana.org/assignments/tls-extensiontype-values 69 // /tls-extensiontype-values.xhtml#alpn-protocol-ids 70 opts.ALPNProtocols = ['http/1.1']; 71 } 72 73 FunctionPrototypeCall(storeHTTPOptions, this, opts); 74 FunctionPrototypeCall(tls.Server, this, opts, _connectionListener); 75 76 this.httpAllowHalfOpen = false; 77 78 if (requestListener) { 79 this.addListener('request', requestListener); 80 } 81 82 this.addListener('tlsClientError', function addListener(err, conn) { 83 if (!this.emit('clientError', err, conn)) 84 conn.destroy(err); 85 }); 86 87 this.timeout = 0; 88 this.maxHeadersCount = null; 89 setupConnectionsTracking(this); 90} 91ObjectSetPrototypeOf(Server.prototype, tls.Server.prototype); 92ObjectSetPrototypeOf(Server, tls.Server); 93 94Server.prototype.closeAllConnections = HttpServer.prototype.closeAllConnections; 95 96Server.prototype.closeIdleConnections = HttpServer.prototype.closeIdleConnections; 97 98Server.prototype.setTimeout = HttpServer.prototype.setTimeout; 99 100/** 101 * Creates a new `https.Server` instance. 102 * @param {{ 103 * IncomingMessage?: IncomingMessage; 104 * ServerResponse?: ServerResponse; 105 * insecureHTTPParser?: boolean; 106 * maxHeaderSize?: number; 107 * }} [opts] 108 * @param {Function} [requestListener] 109 * @returns {Server} 110 */ 111function createServer(opts, requestListener) { 112 return new Server(opts, requestListener); 113} 114 115 116// HTTPS agents. 117 118function createConnection(port, host, options) { 119 if (port !== null && typeof port === 'object') { 120 options = port; 121 } else if (host !== null && typeof host === 'object') { 122 options = { ...host }; 123 } else if (options === null || typeof options !== 'object') { 124 options = {}; 125 } else { 126 options = { ...options }; 127 } 128 129 if (typeof port === 'number') { 130 options.port = port; 131 } 132 133 if (typeof host === 'string') { 134 options.host = host; 135 } 136 137 debug('createConnection', options); 138 139 if (options._agentKey) { 140 const session = this._getSession(options._agentKey); 141 if (session) { 142 debug('reuse session for %j', options._agentKey); 143 options = { 144 session, 145 ...options, 146 }; 147 } 148 } 149 150 const socket = tls.connect(options); 151 152 if (options._agentKey) { 153 // Cache new session for reuse 154 socket.on('session', (session) => { 155 this._cacheSession(options._agentKey, session); 156 }); 157 158 // Evict session on error 159 socket.once('close', (err) => { 160 if (err) 161 this._evictSession(options._agentKey); 162 }); 163 } 164 165 return socket; 166} 167 168/** 169 * Creates a new `HttpAgent` instance. 170 * @param {{ 171 * keepAlive?: boolean; 172 * keepAliveMsecs?: number; 173 * maxSockets?: number; 174 * maxTotalSockets?: number; 175 * maxFreeSockets?: number; 176 * scheduling?: string; 177 * timeout?: number; 178 * maxCachedSessions?: number; 179 * servername?: string; 180 * }} [options] 181 * @constructor 182 */ 183function Agent(options) { 184 if (!(this instanceof Agent)) 185 return new Agent(options); 186 187 FunctionPrototypeCall(HttpAgent, this, options); 188 this.defaultPort = 443; 189 this.protocol = 'https:'; 190 this.maxCachedSessions = this.options.maxCachedSessions; 191 if (this.maxCachedSessions === undefined) 192 this.maxCachedSessions = 100; 193 194 this._sessionCache = { 195 map: {}, 196 list: [], 197 }; 198} 199ObjectSetPrototypeOf(Agent.prototype, HttpAgent.prototype); 200ObjectSetPrototypeOf(Agent, HttpAgent); 201Agent.prototype.createConnection = createConnection; 202 203/** 204 * Gets a unique name for a set of options. 205 * @param {{ 206 * host: string; 207 * port: number; 208 * localAddress: string; 209 * family: number; 210 * }} [options] 211 * @returns {string} 212 */ 213Agent.prototype.getName = function getName(options = kEmptyObject) { 214 let name = FunctionPrototypeCall(HttpAgent.prototype.getName, this, options); 215 216 name += ':'; 217 if (options.ca) 218 name += options.ca; 219 220 name += ':'; 221 if (options.cert) 222 name += options.cert; 223 224 name += ':'; 225 if (options.clientCertEngine) 226 name += options.clientCertEngine; 227 228 name += ':'; 229 if (options.ciphers) 230 name += options.ciphers; 231 232 name += ':'; 233 if (options.key) 234 name += options.key; 235 236 name += ':'; 237 if (options.pfx) 238 name += options.pfx; 239 240 name += ':'; 241 if (options.rejectUnauthorized !== undefined) 242 name += options.rejectUnauthorized; 243 244 name += ':'; 245 if (options.servername && options.servername !== options.host) 246 name += options.servername; 247 248 name += ':'; 249 if (options.minVersion) 250 name += options.minVersion; 251 252 name += ':'; 253 if (options.maxVersion) 254 name += options.maxVersion; 255 256 name += ':'; 257 if (options.secureProtocol) 258 name += options.secureProtocol; 259 260 name += ':'; 261 if (options.crl) 262 name += options.crl; 263 264 name += ':'; 265 if (options.honorCipherOrder !== undefined) 266 name += options.honorCipherOrder; 267 268 name += ':'; 269 if (options.ecdhCurve) 270 name += options.ecdhCurve; 271 272 name += ':'; 273 if (options.dhparam) 274 name += options.dhparam; 275 276 name += ':'; 277 if (options.secureOptions !== undefined) 278 name += options.secureOptions; 279 280 name += ':'; 281 if (options.sessionIdContext) 282 name += options.sessionIdContext; 283 284 name += ':'; 285 if (options.sigalgs) 286 name += JSONStringify(options.sigalgs); 287 288 name += ':'; 289 if (options.privateKeyIdentifier) 290 name += options.privateKeyIdentifier; 291 292 name += ':'; 293 if (options.privateKeyEngine) 294 name += options.privateKeyEngine; 295 296 return name; 297}; 298 299Agent.prototype._getSession = function _getSession(key) { 300 return this._sessionCache.map[key]; 301}; 302 303Agent.prototype._cacheSession = function _cacheSession(key, session) { 304 // Cache is disabled 305 if (this.maxCachedSessions === 0) 306 return; 307 308 // Fast case - update existing entry 309 if (this._sessionCache.map[key]) { 310 this._sessionCache.map[key] = session; 311 return; 312 } 313 314 // Put new entry 315 if (this._sessionCache.list.length >= this.maxCachedSessions) { 316 const oldKey = ArrayPrototypeShift(this._sessionCache.list); 317 debug('evicting %j', oldKey); 318 delete this._sessionCache.map[oldKey]; 319 } 320 321 ArrayPrototypePush(this._sessionCache.list, key); 322 this._sessionCache.map[key] = session; 323}; 324 325Agent.prototype._evictSession = function _evictSession(key) { 326 const index = ArrayPrototypeIndexOf(this._sessionCache.list, key); 327 if (index === -1) 328 return; 329 330 ArrayPrototypeSplice(this._sessionCache.list, index, 1); 331 delete this._sessionCache.map[key]; 332}; 333 334const globalAgent = new Agent(); 335 336/** 337 * Makes a request to a secure web server. 338 * @param {...any} args 339 * @returns {ClientRequest} 340 */ 341function request(...args) { 342 let options = {}; 343 344 if (typeof args[0] === 'string') { 345 const urlStr = ArrayPrototypeShift(args); 346 options = urlToHttpOptions(new URL(urlStr)); 347 } else if (isURL(args[0])) { 348 options = urlToHttpOptions(ArrayPrototypeShift(args)); 349 } 350 351 if (args[0] && typeof args[0] !== 'function') { 352 ObjectAssign(options, ArrayPrototypeShift(args)); 353 } 354 355 options._defaultAgent = module.exports.globalAgent; 356 ArrayPrototypeUnshift(args, options); 357 358 return ReflectConstruct(ClientRequest, args); 359} 360 361/** 362 * Makes a GET request to a secure web server. 363 * @param {string | URL} input 364 * @param {{ 365 * agent?: Agent | boolean; 366 * auth?: string; 367 * createConnection?: Function; 368 * defaultPort?: number; 369 * family?: number; 370 * headers?: object; 371 * hints?: number; 372 * host?: string; 373 * hostname?: string; 374 * insecureHTTPParser?: boolean; 375 * localAddress?: string; 376 * localPort?: number; 377 * lookup?: Function; 378 * maxHeaderSize?: number; 379 * method?: string; 380 * path?: string; 381 * port?: number; 382 * protocol?: string; 383 * setHost?: boolean; 384 * socketPath?: string; 385 * timeout?: number; 386 * signal?: AbortSignal; 387 * } | string | URL} [options] 388 * @param {Function} [cb] 389 * @returns {ClientRequest} 390 */ 391function get(input, options, cb) { 392 const req = request(input, options, cb); 393 req.end(); 394 return req; 395} 396 397module.exports = { 398 Agent, 399 globalAgent, 400 Server, 401 createServer, 402 get, 403 request, 404}; 405