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 ObjectAssign, 26 ObjectSetPrototypeOf, 27} = primordials; 28 29require('internal/util').assertCrypto(); 30 31const tls = require('tls'); 32const url = require('url'); 33const { Agent: HttpAgent } = require('_http_agent'); 34const { 35 Server: HttpServer, 36 _connectionListener, 37 kServerResponse 38} = require('_http_server'); 39const { ClientRequest } = require('_http_client'); 40let debug = require('internal/util/debuglog').debuglog('https', (fn) => { 41 debug = fn; 42}); 43const { URL, urlToOptions, searchParamsSymbol } = require('internal/url'); 44const { IncomingMessage, ServerResponse } = require('http'); 45const { kIncomingMessage } = require('_http_common'); 46const { getOptionValue } = require('internal/options'); 47 48const kDefaultHttpServerTimeout = 49 getOptionValue('--http-server-default-timeout'); 50 51function Server(opts, requestListener) { 52 if (!(this instanceof Server)) return new Server(opts, requestListener); 53 54 if (typeof opts === 'function') { 55 requestListener = opts; 56 opts = undefined; 57 } 58 opts = { ...opts }; 59 60 if (!opts.ALPNProtocols) { 61 // http/1.0 is not defined as Protocol IDs in IANA 62 // https://www.iana.org/assignments/tls-extensiontype-values 63 // /tls-extensiontype-values.xhtml#alpn-protocol-ids 64 opts.ALPNProtocols = ['http/1.1']; 65 } 66 67 this[kIncomingMessage] = opts.IncomingMessage || IncomingMessage; 68 this[kServerResponse] = opts.ServerResponse || ServerResponse; 69 70 tls.Server.call(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 = kDefaultHttpServerTimeout; 84 this.keepAliveTimeout = 5000; 85 this.maxHeadersCount = null; 86 this.headersTimeout = 60 * 1000; // 60 seconds 87} 88ObjectSetPrototypeOf(Server.prototype, tls.Server.prototype); 89ObjectSetPrototypeOf(Server, tls.Server); 90 91Server.prototype.setTimeout = HttpServer.prototype.setTimeout; 92 93function createServer(opts, requestListener) { 94 return new Server(opts, requestListener); 95} 96 97 98// HTTPS agents. 99 100function createConnection(port, host, options) { 101 if (port !== null && typeof port === 'object') { 102 options = port; 103 } else if (host !== null && typeof host === 'object') { 104 options = { ...host }; 105 } else if (options === null || typeof options !== 'object') { 106 options = {}; 107 } else { 108 options = { ...options }; 109 } 110 111 if (typeof port === 'number') { 112 options.port = port; 113 } 114 115 if (typeof host === 'string') { 116 options.host = host; 117 } 118 119 debug('createConnection', options); 120 121 if (options._agentKey) { 122 const session = this._getSession(options._agentKey); 123 if (session) { 124 debug('reuse session for %j', options._agentKey); 125 options = { 126 session, 127 ...options 128 }; 129 } 130 } 131 132 const socket = tls.connect(options); 133 134 if (options._agentKey) { 135 // Cache new session for reuse 136 socket.on('session', (session) => { 137 this._cacheSession(options._agentKey, session); 138 }); 139 140 // Evict session on error 141 socket.once('close', (err) => { 142 if (err) 143 this._evictSession(options._agentKey); 144 }); 145 } 146 147 return socket; 148} 149 150 151function Agent(options) { 152 if (!(this instanceof Agent)) 153 return new Agent(options); 154 155 HttpAgent.call(this, options); 156 this.defaultPort = 443; 157 this.protocol = 'https:'; 158 this.maxCachedSessions = this.options.maxCachedSessions; 159 if (this.maxCachedSessions === undefined) 160 this.maxCachedSessions = 100; 161 162 this._sessionCache = { 163 map: {}, 164 list: [] 165 }; 166} 167ObjectSetPrototypeOf(Agent.prototype, HttpAgent.prototype); 168ObjectSetPrototypeOf(Agent, HttpAgent); 169Agent.prototype.createConnection = createConnection; 170 171Agent.prototype.getName = function getName(options) { 172 let name = HttpAgent.prototype.getName.call(this, options); 173 174 name += ':'; 175 if (options.ca) 176 name += options.ca; 177 178 name += ':'; 179 if (options.cert) 180 name += options.cert; 181 182 name += ':'; 183 if (options.clientCertEngine) 184 name += options.clientCertEngine; 185 186 name += ':'; 187 if (options.ciphers) 188 name += options.ciphers; 189 190 name += ':'; 191 if (options.key) 192 name += options.key; 193 194 name += ':'; 195 if (options.pfx) 196 name += options.pfx; 197 198 name += ':'; 199 if (options.rejectUnauthorized !== undefined) 200 name += options.rejectUnauthorized; 201 202 name += ':'; 203 if (options.servername && options.servername !== options.host) 204 name += options.servername; 205 206 name += ':'; 207 if (options.minVersion) 208 name += options.minVersion; 209 210 name += ':'; 211 if (options.maxVersion) 212 name += options.maxVersion; 213 214 name += ':'; 215 if (options.secureProtocol) 216 name += options.secureProtocol; 217 218 name += ':'; 219 if (options.crl) 220 name += options.crl; 221 222 name += ':'; 223 if (options.honorCipherOrder !== undefined) 224 name += options.honorCipherOrder; 225 226 name += ':'; 227 if (options.ecdhCurve) 228 name += options.ecdhCurve; 229 230 name += ':'; 231 if (options.dhparam) 232 name += options.dhparam; 233 234 name += ':'; 235 if (options.secureOptions !== undefined) 236 name += options.secureOptions; 237 238 name += ':'; 239 if (options.sessionIdContext) 240 name += options.sessionIdContext; 241 242 return name; 243}; 244 245Agent.prototype._getSession = function _getSession(key) { 246 return this._sessionCache.map[key]; 247}; 248 249Agent.prototype._cacheSession = function _cacheSession(key, session) { 250 // Cache is disabled 251 if (this.maxCachedSessions === 0) 252 return; 253 254 // Fast case - update existing entry 255 if (this._sessionCache.map[key]) { 256 this._sessionCache.map[key] = session; 257 return; 258 } 259 260 // Put new entry 261 if (this._sessionCache.list.length >= this.maxCachedSessions) { 262 const oldKey = this._sessionCache.list.shift(); 263 debug('evicting %j', oldKey); 264 delete this._sessionCache.map[oldKey]; 265 } 266 267 this._sessionCache.list.push(key); 268 this._sessionCache.map[key] = session; 269}; 270 271Agent.prototype._evictSession = function _evictSession(key) { 272 const index = this._sessionCache.list.indexOf(key); 273 if (index === -1) 274 return; 275 276 this._sessionCache.list.splice(index, 1); 277 delete this._sessionCache.map[key]; 278}; 279 280const globalAgent = new Agent(); 281 282let urlWarningEmitted = false; 283function request(...args) { 284 let options = {}; 285 286 if (typeof args[0] === 'string') { 287 const urlStr = args.shift(); 288 try { 289 options = urlToOptions(new URL(urlStr)); 290 } catch (err) { 291 options = url.parse(urlStr); 292 if (!options.hostname) { 293 throw err; 294 } 295 if (!urlWarningEmitted && !process.noDeprecation) { 296 urlWarningEmitted = true; 297 process.emitWarning( 298 `The provided URL ${urlStr} is not a valid URL, and is supported ` + 299 'in the https module solely for compatibility.', 300 'DeprecationWarning', 'DEP0109'); 301 } 302 } 303 } else if (args[0] && args[0][searchParamsSymbol] && 304 args[0][searchParamsSymbol][searchParamsSymbol]) { 305 // url.URL instance 306 options = urlToOptions(args.shift()); 307 } 308 309 if (args[0] && typeof args[0] !== 'function') { 310 ObjectAssign(options, args.shift()); 311 } 312 313 options._defaultAgent = module.exports.globalAgent; 314 args.unshift(options); 315 316 return new ClientRequest(...args); 317} 318 319function get(input, options, cb) { 320 const req = request(input, options, cb); 321 req.end(); 322 return req; 323} 324 325module.exports = { 326 Agent, 327 globalAgent, 328 Server, 329 createServer, 330 get, 331 request 332}; 333