• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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