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 37require('internal/util').assertCrypto(); 38 39const tls = require('tls'); 40const url = require('url'); 41const { Agent: HttpAgent } = require('_http_agent'); 42const { 43 Server: HttpServer, 44 storeHTTPOptions, 45 _connectionListener, 46} = require('_http_server'); 47const { ClientRequest } = require('_http_client'); 48let debug = require('internal/util/debuglog').debuglog('https', (fn) => { 49 debug = fn; 50}); 51const { URL, urlToHttpOptions, searchParamsSymbol } = require('internal/url'); 52 53function Server(opts, requestListener) { 54 if (!(this instanceof Server)) return new Server(opts, requestListener); 55 56 if (typeof opts === 'function') { 57 requestListener = opts; 58 opts = undefined; 59 } 60 opts = { ...opts }; 61 62 if (!opts.ALPNProtocols) { 63 // http/1.0 is not defined as Protocol IDs in IANA 64 // https://www.iana.org/assignments/tls-extensiontype-values 65 // /tls-extensiontype-values.xhtml#alpn-protocol-ids 66 opts.ALPNProtocols = ['http/1.1']; 67 } 68 69 FunctionPrototypeCall(storeHTTPOptions, this, opts); 70 FunctionPrototypeCall(tls.Server, this, opts, _connectionListener); 71 72 this.httpAllowHalfOpen = false; 73 74 if (requestListener) { 75 this.addListener('request', requestListener); 76 } 77 78 this.addListener('tlsClientError', function addListener(err, conn) { 79 if (!this.emit('clientError', err, conn)) 80 conn.destroy(err); 81 }); 82 83 this.timeout = 0; 84 this.keepAliveTimeout = 5000; 85 this.maxHeadersCount = null; 86 this.headersTimeout = 60 * 1000; // 60 seconds 87 this.requestTimeout = 0; 88} 89ObjectSetPrototypeOf(Server.prototype, tls.Server.prototype); 90ObjectSetPrototypeOf(Server, tls.Server); 91 92Server.prototype.setTimeout = HttpServer.prototype.setTimeout; 93 94/** 95 * Creates a new `https.Server` instance. 96 * @param {{ 97 * IncomingMessage?: IncomingMessage; 98 * ServerResponse?: ServerResponse; 99 * insecureHTTPParser?: boolean; 100 * maxHeaderSize?: number; 101 * }} [opts] 102 * @param {Function} [requestListener] 103 * @returns {Server} 104 */ 105function createServer(opts, requestListener) { 106 return new Server(opts, requestListener); 107} 108 109 110// HTTPS agents. 111 112function createConnection(port, host, options) { 113 if (port !== null && typeof port === 'object') { 114 options = port; 115 } else if (host !== null && typeof host === 'object') { 116 options = { ...host }; 117 } else if (options === null || typeof options !== 'object') { 118 options = {}; 119 } else { 120 options = { ...options }; 121 } 122 123 if (typeof port === 'number') { 124 options.port = port; 125 } 126 127 if (typeof host === 'string') { 128 options.host = host; 129 } 130 131 debug('createConnection', options); 132 133 if (options._agentKey) { 134 const session = this._getSession(options._agentKey); 135 if (session) { 136 debug('reuse session for %j', options._agentKey); 137 options = { 138 session, 139 ...options 140 }; 141 } 142 } 143 144 const socket = tls.connect(options); 145 146 if (options._agentKey) { 147 // Cache new session for reuse 148 socket.on('session', (session) => { 149 this._cacheSession(options._agentKey, session); 150 }); 151 152 // Evict session on error 153 socket.once('close', (err) => { 154 if (err) 155 this._evictSession(options._agentKey); 156 }); 157 } 158 159 return socket; 160} 161 162/** 163 * Creates a new `HttpAgent` instance. 164 * @param {{ 165 * keepAlive?: boolean; 166 * keepAliveMsecs?: number; 167 * maxSockets?: number; 168 * maxTotalSockets?: number; 169 * maxFreeSockets?: number; 170 * scheduling?: string; 171 * timeout?: number; 172 * maxCachedSessions?: number; 173 * servername?: string; 174 * }} [options] 175 * @returns {Agent} 176 */ 177function Agent(options) { 178 if (!(this instanceof Agent)) 179 return new Agent(options); 180 181 FunctionPrototypeCall(HttpAgent, this, options); 182 this.defaultPort = 443; 183 this.protocol = 'https:'; 184 this.maxCachedSessions = this.options.maxCachedSessions; 185 if (this.maxCachedSessions === undefined) 186 this.maxCachedSessions = 100; 187 188 this._sessionCache = { 189 map: {}, 190 list: [] 191 }; 192} 193ObjectSetPrototypeOf(Agent.prototype, HttpAgent.prototype); 194ObjectSetPrototypeOf(Agent, HttpAgent); 195Agent.prototype.createConnection = createConnection; 196 197/** 198 * Gets a unique name for a set of options. 199 * @param {{ 200 * host: string; 201 * port: number; 202 * localAddress: string; 203 * family: number; 204 * }} [options] 205 * @returns {string} 206 */ 207Agent.prototype.getName = function getName(options) { 208 let name = FunctionPrototypeCall(HttpAgent.prototype.getName, this, options); 209 210 name += ':'; 211 if (options.ca) 212 name += options.ca; 213 214 name += ':'; 215 if (options.cert) 216 name += options.cert; 217 218 name += ':'; 219 if (options.clientCertEngine) 220 name += options.clientCertEngine; 221 222 name += ':'; 223 if (options.ciphers) 224 name += options.ciphers; 225 226 name += ':'; 227 if (options.key) 228 name += options.key; 229 230 name += ':'; 231 if (options.pfx) 232 name += options.pfx; 233 234 name += ':'; 235 if (options.rejectUnauthorized !== undefined) 236 name += options.rejectUnauthorized; 237 238 name += ':'; 239 if (options.servername && options.servername !== options.host) 240 name += options.servername; 241 242 name += ':'; 243 if (options.minVersion) 244 name += options.minVersion; 245 246 name += ':'; 247 if (options.maxVersion) 248 name += options.maxVersion; 249 250 name += ':'; 251 if (options.secureProtocol) 252 name += options.secureProtocol; 253 254 name += ':'; 255 if (options.crl) 256 name += options.crl; 257 258 name += ':'; 259 if (options.honorCipherOrder !== undefined) 260 name += options.honorCipherOrder; 261 262 name += ':'; 263 if (options.ecdhCurve) 264 name += options.ecdhCurve; 265 266 name += ':'; 267 if (options.dhparam) 268 name += options.dhparam; 269 270 name += ':'; 271 if (options.secureOptions !== undefined) 272 name += options.secureOptions; 273 274 name += ':'; 275 if (options.sessionIdContext) 276 name += options.sessionIdContext; 277 278 name += ':'; 279 if (options.sigalgs) 280 name += JSONStringify(options.sigalgs); 281 282 name += ':'; 283 if (options.privateKeyIdentifier) 284 name += options.privateKeyIdentifier; 285 286 name += ':'; 287 if (options.privateKeyEngine) 288 name += options.privateKeyEngine; 289 290 return name; 291}; 292 293Agent.prototype._getSession = function _getSession(key) { 294 return this._sessionCache.map[key]; 295}; 296 297Agent.prototype._cacheSession = function _cacheSession(key, session) { 298 // Cache is disabled 299 if (this.maxCachedSessions === 0) 300 return; 301 302 // Fast case - update existing entry 303 if (this._sessionCache.map[key]) { 304 this._sessionCache.map[key] = session; 305 return; 306 } 307 308 // Put new entry 309 if (this._sessionCache.list.length >= this.maxCachedSessions) { 310 const oldKey = ArrayPrototypeShift(this._sessionCache.list); 311 debug('evicting %j', oldKey); 312 delete this._sessionCache.map[oldKey]; 313 } 314 315 ArrayPrototypePush(this._sessionCache.list, key); 316 this._sessionCache.map[key] = session; 317}; 318 319Agent.prototype._evictSession = function _evictSession(key) { 320 const index = ArrayPrototypeIndexOf(this._sessionCache.list, key); 321 if (index === -1) 322 return; 323 324 ArrayPrototypeSplice(this._sessionCache.list, index, 1); 325 delete this._sessionCache.map[key]; 326}; 327 328const globalAgent = new Agent(); 329 330let urlWarningEmitted = false; 331 332/** 333 * Makes a request to a secure web server. 334 * @param {...any} args 335 * @returns {ClientRequest} 336 */ 337function request(...args) { 338 let options = {}; 339 340 if (typeof args[0] === 'string') { 341 const urlStr = ArrayPrototypeShift(args); 342 try { 343 options = urlToHttpOptions(new URL(urlStr)); 344 } catch (err) { 345 options = url.parse(urlStr); 346 if (!options.hostname) { 347 throw err; 348 } 349 if (!urlWarningEmitted && !process.noDeprecation) { 350 urlWarningEmitted = true; 351 process.emitWarning( 352 `The provided URL ${urlStr} is not a valid URL, and is supported ` + 353 'in the https module solely for compatibility.', 354 'DeprecationWarning', 'DEP0109'); 355 } 356 } 357 } else if (args[0] && args[0][searchParamsSymbol] && 358 args[0][searchParamsSymbol][searchParamsSymbol]) { 359 // url.URL instance 360 options = urlToHttpOptions(ArrayPrototypeShift(args)); 361 } 362 363 if (args[0] && typeof args[0] !== 'function') { 364 ObjectAssign(options, ArrayPrototypeShift(args)); 365 } 366 367 options._defaultAgent = module.exports.globalAgent; 368 ArrayPrototypeUnshift(args, options); 369 370 return ReflectConstruct(ClientRequest, args); 371} 372 373/** 374 * Makes a GET request to a secure web server. 375 * @param {string | URL} input 376 * @param {{ 377 * agent?: Agent | boolean; 378 * auth?: string; 379 * createConnection?: Function; 380 * defaultPort?: number; 381 * family?: number; 382 * headers?: Object; 383 * hints?: number; 384 * host?: string; 385 * hostname?: string; 386 * insecureHTTPParser?: boolean; 387 * localAddress?: string; 388 * localPort?: number; 389 * lookup?: Function; 390 * maxHeaderSize?: number; 391 * method?: string; 392 * path?: string; 393 * port?: number; 394 * protocol?: string; 395 * setHost?: boolean; 396 * socketPath?: string; 397 * timeout?: number; 398 * signal?: AbortSignal; 399 * } | string | URL} [options] 400 * @param {Function} [cb] 401 * @returns {ClientRequest} 402 */ 403function get(input, options, cb) { 404 const req = request(input, options, cb); 405 req.end(); 406 return req; 407} 408 409module.exports = { 410 Agent, 411 globalAgent, 412 Server, 413 createServer, 414 get, 415 request 416}; 417