Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 144 additions & 129 deletions ext/node/polyfills/https.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,176 +107,191 @@ export function get(...args: any[]) {
return req;
}

export class Agent extends HttpAgent {
declare maxCachedSessions: number;
declare _sessionCache: { map: Record<string, any>; list: string[] };

constructor(options: any) {
options = { __proto__: null, ...options };
options.defaultPort ??= 443;
options.protocol ??= "https:";
super(options);

this.maxCachedSessions = this.options.maxCachedSessions;
if (this.maxCachedSessions === undefined) {
this.maxCachedSessions = 100;
}

this._sessionCache = {
map: {},
list: [],
};
// Defined as a regular function (not a `class`) so that `https.Agent()` may be
// invoked without `new`, matching Node:
// https://github.com/nodejs/node/blob/main/lib/https.js
export function Agent(this: any, options: any) {
if (!(this instanceof Agent)) {
return new (Agent as any)(options);
}

getName(options: any = {}) {
let name = super.getName(options);
options = { __proto__: null, ...options };
options.defaultPort ??= 443;
options.protocol ??= "https:";
HttpAgent.call(this, options);

name += ":";
if (options.ca) name += options.ca;
this.maxCachedSessions = this.options.maxCachedSessions;
if (this.maxCachedSessions === undefined) {
this.maxCachedSessions = 100;
}

name += ":";
if (options.cert) name += options.cert;
this._sessionCache = {
map: {},
list: [],
};
}
Object.setPrototypeOf(Agent.prototype, HttpAgent.prototype);
Object.setPrototypeOf(Agent, HttpAgent);

name += ":";
if (options.clientCertEngine) name += options.clientCertEngine;
Agent.prototype.getName = function getName(this: any, options: any = {}) {
let name = HttpAgent.prototype.getName.call(this, options);

name += ":";
if (options.ciphers) name += options.ciphers;
name += ":";
if (options.ca) name += options.ca;

name += ":";
if (options.key) name += options.key;
name += ":";
if (options.cert) name += options.cert;

name += ":";
if (options.pfx) name += options.pfx;
name += ":";
if (options.clientCertEngine) name += options.clientCertEngine;

name += ":";
if (options.rejectUnauthorized !== undefined) {
name += options.rejectUnauthorized;
}
name += ":";
if (options.ciphers) name += options.ciphers;

name += ":";
if (options.servername && options.servername !== options.host) {
name += options.servername;
}
name += ":";
if (options.key) name += options.key;

name += ":";
if (options.minVersion) name += options.minVersion;
name += ":";
if (options.pfx) name += options.pfx;

name += ":";
if (options.maxVersion) name += options.maxVersion;
name += ":";
if (options.rejectUnauthorized !== undefined) {
name += options.rejectUnauthorized;
}

name += ":";
if (options.secureProtocol) name += options.secureProtocol;
name += ":";
if (options.servername && options.servername !== options.host) {
name += options.servername;
}

name += ":";
if (options.crl) name += options.crl;
name += ":";
if (options.minVersion) name += options.minVersion;

name += ":";
if (options.honorCipherOrder !== undefined) {
name += options.honorCipherOrder;
}
name += ":";
if (options.maxVersion) name += options.maxVersion;

name += ":";
if (options.secureProtocol) name += options.secureProtocol;

name += ":";
if (options.ecdhCurve) name += options.ecdhCurve;
name += ":";
if (options.crl) name += options.crl;

name += ":";
if (options.dhparam) name += options.dhparam;
name += ":";
if (options.honorCipherOrder !== undefined) {
name += options.honorCipherOrder;
}

name += ":";
if (options.secureOptions !== undefined) name += options.secureOptions;
name += ":";
if (options.ecdhCurve) name += options.ecdhCurve;

name += ":";
if (options.sessionIdContext) name += options.sessionIdContext;
name += ":";
if (options.dhparam) name += options.dhparam;

name += ":";
if (options.sigalgs) name += JSON.stringify(options.sigalgs);
name += ":";
if (options.secureOptions !== undefined) name += options.secureOptions;

name += ":";
if (options.privateKeyIdentifier) name += options.privateKeyIdentifier;
name += ":";
if (options.sessionIdContext) name += options.sessionIdContext;

name += ":";
if (options.privateKeyEngine) name += options.privateKeyEngine;
name += ":";
if (options.sigalgs) name += JSON.stringify(options.sigalgs);

return name;
}
name += ":";
if (options.privateKeyIdentifier) name += options.privateKeyIdentifier;

_getSession(key: string) {
return this._sessionCache.map[key];
}
name += ":";
if (options.privateKeyEngine) name += options.privateKeyEngine;

_cacheSession(key: string, session: any) {
if (this.maxCachedSessions === 0) return;
return name;
};

if (this._sessionCache.map[key]) {
this._sessionCache.map[key] = session;
return;
}
Agent.prototype._getSession = function _getSession(this: any, key: string) {
return this._sessionCache.map[key];
};

if (this._sessionCache.list.length >= this.maxCachedSessions) {
const oldKey = this._sessionCache.list.shift()!;
delete this._sessionCache.map[oldKey];
}
Agent.prototype._cacheSession = function _cacheSession(
this: any,
key: string,
session: any,
) {
if (this.maxCachedSessions === 0) return;

this._sessionCache.list.push(key);
if (this._sessionCache.map[key]) {
this._sessionCache.map[key] = session;
return;
}

_evictSession(key: string) {
const index = this._sessionCache.list.indexOf(key);
if (index === -1) return;

this._sessionCache.list.splice(index, 1);
delete this._sessionCache.map[key];
if (this._sessionCache.list.length >= this.maxCachedSessions) {
const oldKey = this._sessionCache.list.shift()!;
delete this._sessionCache.map[oldKey];
}

createConnection(options: any, cb?: any) {
if (typeof options === "number") {
// createConnection(port, host, options) signature
const args = arguments;
const opts: any = {};
if (args[0] !== null && typeof args[0] === "object") {
Object.assign(opts, args[0]);
} else if (args[1] !== null && typeof args[1] === "object") {
Object.assign(opts, args[1]);
} else if (args[2] !== null && typeof args[2] === "object") {
Object.assign(opts, args[2]);
}
if (typeof args[0] === "number") opts.port = args[0];
if (typeof args[1] === "string") opts.host = args[1];
if (typeof args[args.length - 1] === "function") {
cb = args[args.length - 1];
}
options = opts;
}

// Look up cached TLS session for reuse
if (options._agentKey) {
const session = this._getSession(options._agentKey);
if (session) {
options = { session, ...options };
}
}
this._sessionCache.list.push(key);
this._sessionCache.map[key] = session;
};

const socket = tls.connect(options as any);
Agent.prototype._evictSession = function _evictSession(
this: any,
key: string,
) {
const index = this._sessionCache.list.indexOf(key);
if (index === -1) return;

// Cache session on new session event
if (options._agentKey) {
socket.on("session", (session: any) => {
this._cacheSession(options._agentKey, session);
});
this._sessionCache.list.splice(index, 1);
delete this._sessionCache.map[key];
};

socket.once("close", (err: any) => {
if (err) this._evictSession(options._agentKey);
});
Agent.prototype.createConnection = function createConnection(
this: any,
options: any,
cb?: any,
) {
if (typeof options === "number") {
// createConnection(port, host, options) signature
const args = arguments;
const opts: any = {};
if (args[0] !== null && typeof args[0] === "object") {
Object.assign(opts, args[0]);
} else if (args[1] !== null && typeof args[1] === "object") {
Object.assign(opts, args[1]);
} else if (args[2] !== null && typeof args[2] === "object") {
Object.assign(opts, args[2]);
}
if (typeof args[0] === "number") opts.port = args[0];
if (typeof args[1] === "string") opts.host = args[1];
if (typeof args[args.length - 1] === "function") {
cb = args[args.length - 1];
}
options = opts;
}

if (cb) {
socket.once("secureConnect", cb);
// Look up cached TLS session for reuse
if (options._agentKey) {
const session = this._getSession(options._agentKey);
if (session) {
options = { session, ...options };
}
}

const socket = tls.connect(options as any);

return socket;
// Cache session on new session event
if (options._agentKey) {
socket.on("session", (session: any) => {
this._cacheSession(options._agentKey, session);
});

socket.once("close", (err: any) => {
if (err) this._evictSession(options._agentKey);
});
}
}

if (cb) {
socket.once("secureConnect", cb);
}

return socket;
};

export const globalAgent = new Agent({
keepAlive: true,
Expand Down
1 change: 1 addition & 0 deletions tests/node_compat/config.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,7 @@
"parallel/test-http2-zero-length-header.js": {},
"parallel/test-http2-zero-length-write.js": {},
"parallel/test-https-agent-abort-controller.js": {},
"parallel/test-https-agent-constructor.js": {},
"parallel/test-https-agent-getname.js": {},
"parallel/test-https-agent-servername.js": {},
"parallel/test-https-agent-sockets-leak.js": {},
Expand Down
Loading