• 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
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