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