fix: enable SSL session caching for faster consecutive requests (#6929)

* fix: enable SSL session caching for faster consecutive requests

Previously, Bruno created a new HTTPS agent for every request, which meant
SSL/TLS sessions couldn't be reused. This caused the full TLS handshake
(~450ms) to run on every request, even to the same endpoint.

Changes:
- Add agent caching based on TLS configuration (certs, proxy, SSL options)
- Reuse cached agents for requests with matching configuration
- SSL sessions are now cached and reused, significantly reducing
  response time for consecutive requests to the same host

The fix maintains backward compatibility:
- Timeline logging moved to setup phase (before agent creation)
- Proxy and SSL validation behavior unchanged
- Added clearAgentCache() for testing and configuration changes

Fixes #5574

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address review feedback for SSL session caching

- Add passphrase to cache key to prevent incorrect agent reuse
- Add MAX_AGENT_CACHE_SIZE (100) with LRU-style eviction
- Use consistent node: prefix for crypto import

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: lohit <lohit@usebruno.com>
This commit is contained in:
karthik
2026-01-29 06:18:44 -05:00
committed by lohit-bruno
parent 0b7cd0e540
commit 9cbc58df70

View File

@@ -1,11 +1,74 @@
const parseUrl = require('url').parse;
const https = require('node:https');
const crypto = require('node:crypto');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { interpolateString } = require('../ipc/network/interpolate-string');
const { SocksProxyAgent } = require('socks-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent');
const { isEmpty, get, isUndefined, isNull } = require('lodash');
/**
* Agent cache for SSL session reuse.
* Agents are cached by their configuration to enable TLS session resumption,
* which significantly reduces SSL handshake time for repeated requests.
*/
const agentCache = new Map();
const MAX_AGENT_CACHE_SIZE = 100;
/**
* Generate a cache key from agent options.
* Uses a hash of the serialized options to create a compact key.
*/
function getAgentCacheKey(options, proxyUri = null) {
// Extract the TLS-relevant options for the cache key
const keyData = {
proxyUri,
rejectUnauthorized: options.rejectUnauthorized,
// Hash certificates and passphrase instead of including full content
ca: options.ca ? crypto.createHash('md5').update(String(options.ca)).digest('hex') : null,
cert: options.cert ? crypto.createHash('md5').update(String(options.cert)).digest('hex') : null,
key: options.key ? crypto.createHash('md5').update(String(options.key)).digest('hex') : null,
pfx: options.pfx ? crypto.createHash('md5').update(String(options.pfx)).digest('hex') : null,
passphrase: options.passphrase ? crypto.createHash('md5').update(String(options.passphrase)).digest('hex') : null,
minVersion: options.minVersion,
secureProtocol: options.secureProtocol
};
return JSON.stringify(keyData);
}
/**
* Get or create a cached agent.
* Reuses existing agents to enable SSL session caching.
* Uses LRU-style eviction when cache exceeds MAX_AGENT_CACHE_SIZE.
*/
function getOrCreateAgent(AgentClass, options, proxyUri = null) {
const cacheKey = getAgentCacheKey(options, proxyUri);
if (agentCache.has(cacheKey)) {
// Move to end for LRU (delete and re-add)
const agent = agentCache.get(cacheKey);
agentCache.delete(cacheKey);
agentCache.set(cacheKey, agent);
return agent;
}
let agent;
if (proxyUri) {
agent = new AgentClass(proxyUri, options);
} else {
agent = new AgentClass(options);
}
// Evict oldest entry if cache is full
if (agentCache.size >= MAX_AGENT_CACHE_SIZE) {
const oldestKey = agentCache.keys().next().value;
agentCache.delete(oldestKey);
}
agentCache.set(cacheKey, agent);
return agent;
}
const DEFAULT_PORTS = {
ftp: 21,
gopher: 70,
@@ -331,13 +394,20 @@ function setupProxyAgents({
secureProtocol: undefined,
// Allow Node.js to choose the protocol
minVersion: 'TLSv1',
rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true
};
const httpProxyAgentOptions = {
rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true,
// Enable keepAlive for connection reuse
keepAlive: true
};
// Log SSL validation status to timeline
if (timeline) {
timeline.push({
timestamp: new Date(),
type: 'info',
message: `SSL validation: ${tlsOptions.rejectUnauthorized ? 'enabled' : 'disabled'}`
});
}
if (proxyMode === 'on') {
const shouldProxy = shouldUseProxy(requestConfig.url, get(proxyConfig, 'bypassProxy', ''));
if (shouldProxy) {
@@ -358,23 +428,26 @@ function setupProxyAgents({
proxyUri = `${proxyProtocol}://${proxyHostname}${uriPort}`;
}
if (timeline) {
timeline.push({
timestamp: new Date(),
type: 'info',
message: `Using proxy: ${proxyProtocol}://${proxyHostname}${uriPort}`
});
}
if (socksEnabled) {
const TimelineSocksProxyAgent = createTimelineAgentClass(SocksProxyAgent);
requestConfig.httpAgent = new TimelineSocksProxyAgent({ proxy: proxyUri }, timeline);
requestConfig.httpsAgent = new TimelineSocksProxyAgent({ proxy: proxyUri, ...tlsOptions }, timeline);
// Use cached agents for SSL session reuse
requestConfig.httpAgent = getOrCreateAgent(SocksProxyAgent, { keepAlive: true }, proxyUri);
requestConfig.httpsAgent = getOrCreateAgent(SocksProxyAgent, tlsOptions, proxyUri);
} else {
const TimelineHttpProxyAgent = createTimelineHttpAgentClass(HttpProxyAgent);
const TimelineHttpsProxyAgent = createTimelineAgentClass(PatchedHttpsProxyAgent);
requestConfig.httpAgent = new TimelineHttpProxyAgent({ proxy: proxyUri, httpProxyAgentOptions }, timeline);
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
{ proxy: proxyUri, ...tlsOptions },
timeline
);
// Use cached agents for SSL session reuse
requestConfig.httpAgent = getOrCreateAgent(HttpProxyAgent, { keepAlive: true }, proxyUri);
requestConfig.httpsAgent = getOrCreateAgent(PatchedHttpsProxyAgent, tlsOptions, proxyUri);
}
} else {
// If proxy should not be used, set default HTTPS agent
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
// If proxy should not be used, use cached default HTTPS agent
requestConfig.httpsAgent = getOrCreateAgent(https.Agent, tlsOptions);
}
} else if (proxyMode === 'system') {
const { http_proxy, https_proxy, no_proxy } = proxyConfig || {};
@@ -385,8 +458,7 @@ function setupProxyAgents({
try {
if (http_proxy?.length && !isHttpsRequest) {
new URL(http_proxy);
const TimelineHttpProxyAgent = createTimelineHttpAgentClass(HttpProxyAgent);
requestConfig.httpAgent = new TimelineHttpProxyAgent({ proxy: http_proxy, httpProxyAgentOptions }, timeline);
requestConfig.httpAgent = getOrCreateAgent(HttpProxyAgent, { keepAlive: true }, http_proxy);
}
} catch (error) {
throw new Error(`Invalid system http_proxy "${http_proxy}": ${error.message}`);
@@ -394,30 +466,47 @@ function setupProxyAgents({
try {
if (https_proxy?.length && isHttpsRequest) {
new URL(https_proxy);
const TimelineHttpsProxyAgent = createTimelineAgentClass(PatchedHttpsProxyAgent);
requestConfig.httpsAgent = new TimelineHttpsProxyAgent(
{ proxy: https_proxy, ...tlsOptions },
timeline
);
if (timeline) {
timeline.push({
timestamp: new Date(),
type: 'info',
message: `Using system proxy: ${https_proxy}`
});
}
requestConfig.httpsAgent = getOrCreateAgent(PatchedHttpsProxyAgent, tlsOptions, https_proxy);
} else {
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
requestConfig.httpsAgent = getOrCreateAgent(https.Agent, tlsOptions);
}
} catch (error) {
throw new Error(`Invalid system https_proxy "${https_proxy}": ${error.message}`);
}
} else {
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
requestConfig.httpsAgent = getOrCreateAgent(https.Agent, tlsOptions);
}
} else {
const TimelineHttpsAgent = createTimelineAgentClass(https.Agent);
requestConfig.httpsAgent = new TimelineHttpsAgent(tlsOptions, timeline);
// No proxy - use cached HTTPS agent for SSL session reuse
requestConfig.httpsAgent = getOrCreateAgent(https.Agent, tlsOptions);
}
}
/**
* Clear the agent cache. Useful for testing or when SSL configuration changes.
*/
function clearAgentCache() {
agentCache.clear();
}
/**
* Get the current size of the agent cache.
*/
function getAgentCacheSize() {
return agentCache.size;
}
module.exports = {
shouldUseProxy,
PatchedHttpsProxyAgent,
setupProxyAgents
setupProxyAgents,
clearAgentCache,
getAgentCacheSize
};