Implement private IP range request host validator (#5497)
* feat: implement private IP range request host validator for server-side HTTP requests * feat: add link-local address support * fix: use correct config keys * fix: if config missing use default loopback addresses * fix: re-use resolved address for connection * test: add unit coverage for private request filter and proxy interaction Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/1813593e-2263-45e2-aa53-74d39515f1df Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * test: remove request-proxy.test.js * perf: cache resolved matches * fix: remove unused import * fix: use proper ipv4 loopback cidr * fix: correct raiseError comment * test: uses tls.connect for secure endpoints * Implement private IP range request host validator Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/e76ba122-136e-43ad-b4bc-ea48a01fcdda Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Revert "Implement private IP range request host validator" This reverts commit 14e271470227b485b7d23caac31a237abf9f7835. * fix: close request without sending status in CORS forwarding when headers were sent * fix: not enabled -> disabled * feat: add enableKeepAlive option to PrivateRequestAgent Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -151,6 +151,26 @@ hostWhitelist:
|
||||
# - .trycloudflare.com
|
||||
hosts: []
|
||||
|
||||
# Perform whitelist checks against server-side HTTP requests that resolve to private IP addresses.
|
||||
# This is an additional layer of security to prevent Server-Side Request Forgery (SSRF) attacks.
|
||||
# Recommended when listen mode is enabled, or if your server is accessible by untrusted users.
|
||||
privateAddressWhitelist:
|
||||
# Enable private address whitelist to block requests to private IP ranges.
|
||||
enabled: false
|
||||
# If true, requests to hosts that cannot be resolved will be allowed instead of blocked.
|
||||
allowUnresolvedHosts: false
|
||||
# Log blocked and allowed requests to the console.
|
||||
log:
|
||||
# Log blocked requests to the console with a warning message
|
||||
blockedRequests: true
|
||||
# Log allowed requests to the console with an info message
|
||||
allowedRequests: false
|
||||
# List of allowed private IP ranges (in CIDR notation or wildcard format).
|
||||
# Allows loopback IP ranges by default, but you can customize this list to fit your needs.
|
||||
allowedRanges:
|
||||
- '127.0.0.0/8' # Loopback (IPv4)
|
||||
- '::1/128' # Loopback (IPv6)
|
||||
|
||||
# User session timeout *in seconds* (defaults to 24 hours).
|
||||
## Set to a positive number to expire session after a certain time of inactivity
|
||||
## Set to 0 to expire session when the browser is closed
|
||||
|
||||
Generated
+1
@@ -38,6 +38,7 @@
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zeldafan0225/ai_horde": "^5.2.0",
|
||||
"agent-base": "^7.1.3",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^4.1.0",
|
||||
"body-parser": "^1.20.2",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zeldafan0225/ai_horde": "^5.2.0",
|
||||
"agent-base": "^7.1.3",
|
||||
"archiver": "^7.0.1",
|
||||
"bing-translate-api": "^4.1.0",
|
||||
"body-parser": "^1.20.2",
|
||||
|
||||
@@ -37,6 +37,10 @@ export default async function corsProxyMiddleware(req, res) {
|
||||
// Copy over relevant response params to the proxy response
|
||||
await forwardFetchResponse(response, res);
|
||||
} catch (error) {
|
||||
res.status(500).send('Error occurred while trying to proxy to: ' + url + ' ' + error);
|
||||
console.error('Error in CORS proxy middleware:', error);
|
||||
if (!res.headersSent) {
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
return res.end();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import net from 'node:net';
|
||||
import tls from 'node:tls';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import dns from 'node:dns';
|
||||
import ipMatch from 'ip-matching';
|
||||
import ipRegex from 'ip-regex';
|
||||
import { Agent } from 'agent-base';
|
||||
import { color } from './util.js';
|
||||
import { filterValidIpPatterns } from './express-common.js';
|
||||
|
||||
const LOG_HEADER = '[Private Request Filter]';
|
||||
|
||||
/** @type {import('ip-matching').IPMatch[]} */
|
||||
const privateIpRanges = [
|
||||
// Loopback (IPv4)
|
||||
ipMatch.getMatch('127.0.0.0/8'),
|
||||
// Class A private network
|
||||
ipMatch.getMatch('10.0.0.0/8'),
|
||||
// Class B private network
|
||||
ipMatch.getMatch('172.16.0.0/12'),
|
||||
// Class C private network
|
||||
ipMatch.getMatch('192.168.0.0/16'),
|
||||
// Link-local address (IPv4)
|
||||
ipMatch.getMatch('169.254.0.0/16'),
|
||||
// Loopback (IPv6)
|
||||
ipMatch.getMatch('::1/128'),
|
||||
// Unique local address (IPv6)
|
||||
ipMatch.getMatch('fc00::/7'),
|
||||
// Link-local address (IPv6)
|
||||
ipMatch.getMatch('fe80::/10'),
|
||||
];
|
||||
|
||||
/**
|
||||
* Custom HTTP/HTTPS agent that blocks requests to private IP addresses unless they are explicitly allowed in the private address whitelist.
|
||||
* This is used to prevent Server-Side Request Forgery (SSRF) attacks by ensuring that the server cannot make requests to internal services or resources that are not intended to be exposed.
|
||||
* The agent checks if the target host resolves to a private IP address and blocks the request if it does, unless the IP address is included in the private address whitelist.
|
||||
* The private address whitelist can contain specific IP addresses or CIDR ranges that are allowed to be accessed even if they fall within private IP ranges.
|
||||
*/
|
||||
class PrivateRequestAgent extends Agent {
|
||||
/**
|
||||
* List of private IP addresses or CIDR ranges to allow
|
||||
* @type {Readonly<import('ip-matching').IPMatch[]>}
|
||||
*/
|
||||
privateAddressWhitelist = [];
|
||||
|
||||
/**
|
||||
* Whether to log blocked requests to the console
|
||||
* @type {boolean}
|
||||
*/
|
||||
logBlocked = true;
|
||||
|
||||
/**
|
||||
* Whether to log allowed requests to the console
|
||||
* @type {boolean}
|
||||
*/
|
||||
logAllowed = false;
|
||||
|
||||
/**
|
||||
* Whether to allow requests to hosts that cannot be resolved
|
||||
* @type {boolean}
|
||||
*/
|
||||
allowUnresolvedHosts = false;
|
||||
|
||||
/**
|
||||
* Create a new PrivateRequestAgent instance.
|
||||
* @param {object} options
|
||||
* @param {string[]} options.privateAddressWhitelist List of private IP addresses or CIDR ranges to allow.
|
||||
* @param {boolean} options.logBlocked Whether to log blocked requests to the console.
|
||||
* @param {boolean} options.logAllowed Whether to log allowed requests to the console.
|
||||
* @param {boolean} options.allowUnresolvedHosts Whether to allow requests to hosts that cannot be resolved.
|
||||
* @param {boolean} options.enableKeepAlive Whether to enable HTTP/HTTPS keep-alive.
|
||||
*/
|
||||
constructor(options = { privateAddressWhitelist: [], logBlocked: true, logAllowed: false, allowUnresolvedHosts: false, enableKeepAlive: false }) {
|
||||
super({ keepAlive: options.enableKeepAlive });
|
||||
|
||||
const logEntryWarning = (entry, message) => `${color.red('Warning')}: Ignoring invalid private whitelist entry ${color.yellow(entry)} - ${message}`;
|
||||
const whitelistArray = Array.isArray(options.privateAddressWhitelist) ? options.privateAddressWhitelist : [];
|
||||
this.privateAddressWhitelist = Object.freeze(filterValidIpPatterns(whitelistArray, logEntryWarning).map(pattern => ipMatch.getMatch(pattern)));
|
||||
this.allowUnresolvedHosts = options.allowUnresolvedHosts;
|
||||
this.logBlocked = options.logBlocked;
|
||||
this.logAllowed = options.logAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given address is a private IP address.
|
||||
* @param {string} address The IP address to check.
|
||||
* @returns {boolean} Whether the given address is a private IP address.
|
||||
*/
|
||||
#isPrivateIp(address) {
|
||||
return privateIpRanges.some(range => range.matches(address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given address is allowed based on the private address whitelist.
|
||||
* @param {string} address The IP address to check.
|
||||
* @returns {boolean} Whether the given address is allowed based on the private address whitelist.
|
||||
*/
|
||||
#isAllowedPrivateAddress(address) {
|
||||
// Permit the request if the private IP address is in the whitelist
|
||||
return this.privateAddressWhitelist.some(match => match.matches(address));
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect method that checks if the target host resolves to a private IP address and blocks the request if it does.
|
||||
* @param {http.ClientRequest} _req HTTP request object.
|
||||
* @param {import('agent-base').AgentConnectOpts} options Agent connection options.
|
||||
*/
|
||||
async connect(_req, options) {
|
||||
/**
|
||||
* Raise an error and log it if necessary.
|
||||
* @param {string} message The error message.
|
||||
* @param {boolean} [log=true] Whether to log the error to the console.
|
||||
*/
|
||||
const raiseError = (message, log = true) => {
|
||||
if (log) {
|
||||
console.error(color.red(LOG_HEADER), message);
|
||||
}
|
||||
throw new Error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Establish a connection to the target host using either TLS or a regular socket based on the options provided.
|
||||
* @param {string|null} [hostOverride] Pass a host to override the one in options when connecting.
|
||||
* @returns {net.Socket|tls.TLSSocket} A socket connected to the target host.
|
||||
*/
|
||||
const connect = (hostOverride = null) => {
|
||||
if (hostOverride) {
|
||||
options.host = hostOverride;
|
||||
}
|
||||
if (options.secureEndpoint) {
|
||||
return tls.connect(options);
|
||||
} else {
|
||||
return net.connect(options);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the given IP address against the private address whitelist and connect if it's allowed.
|
||||
* @param {string} ip The IP address to validate.
|
||||
* @returns {net.Socket|tls.TLSSocket} A socket connected to the target IP address if it's allowed, otherwise an error is raised.
|
||||
*/
|
||||
const validateIpAddress = (ip) => {
|
||||
// Not a private IP address, allow the request
|
||||
if (!this.#isPrivateIp(ip)) {
|
||||
return connect(ip);
|
||||
}
|
||||
|
||||
// Private IP address, check if it's allowed in the whitelist
|
||||
if (this.#isAllowedPrivateAddress(ip)) {
|
||||
if (this.logAllowed) {
|
||||
console.info(color.green(LOG_HEADER), 'Allowed request to private IP address:', color.blue(ip));
|
||||
}
|
||||
|
||||
return connect(ip);
|
||||
}
|
||||
|
||||
return raiseError(`Blocked request to private IP address: ${ip}`, this.logBlocked);
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the given host to an IP address using DNS lookup.
|
||||
* @param {string} host The host to resolve to an IP address.
|
||||
* @returns {Promise<string>} The resolved IP address for the given host, or an empty string if the host cannot be resolved.
|
||||
*/
|
||||
const lookupHost = async (host) => {
|
||||
try {
|
||||
return (await dns.promises.lookup(host)).address;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const host = options.host;
|
||||
|
||||
if (!host) {
|
||||
return raiseError('No host specified in request options', true);
|
||||
}
|
||||
|
||||
const isIp = ipRegex.v4({ exact: true }).test(host) || ipRegex.v6({ exact: true }).test(host);
|
||||
|
||||
if (isIp) {
|
||||
return validateIpAddress(host);
|
||||
} else {
|
||||
const address = await lookupHost(host);
|
||||
if (!address) {
|
||||
if (this.allowUnresolvedHosts) {
|
||||
return connect();
|
||||
} else {
|
||||
return raiseError(`Unable to resolve host: ${host}. Set privateAddressWhitelist.allowUnresolvedHosts to true to bypass this check.`, true);
|
||||
}
|
||||
}
|
||||
|
||||
return validateIpAddress(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the private request filter by replacing the global HTTP and HTTPS agents with an instance of PrivateRequestAgent.
|
||||
* @param {object} options Options for initializing the private request filter.
|
||||
* @param {boolean} options.listen Whether the server is listening for incoming requests. This is used to determine whether to log a warning if the private request filter is not enabled.
|
||||
* @param {boolean} options.enabled Whether the private request filter is enabled.
|
||||
* @param {string[]} options.privateAddressWhitelist List of private IP addresses or CIDR ranges to allow.
|
||||
* @param {boolean} options.logBlocked Whether to log blocked requests to the console.
|
||||
* @param {boolean} options.logAllowed Whether to log allowed requests to the console.
|
||||
* @param {boolean} options.allowUnresolvedHosts Whether to allow requests to hosts that cannot be resolved.
|
||||
* @param {boolean} options.enableKeepAlive Whether to enable HTTP/HTTPS keep-alive.
|
||||
*/
|
||||
export default function initPrivateRequestFilter({ listen, enabled, privateAddressWhitelist, logBlocked, logAllowed, allowUnresolvedHosts, enableKeepAlive }) {
|
||||
if (!enabled) {
|
||||
if (listen) {
|
||||
console.warn();
|
||||
console.warn(color.yellow('Warning: listen is enabled but private request filter is disabled. This may expose your server to SSRF attacks.'));
|
||||
console.warn(color.blue('To enable, provide trusted addresses in privateAddressWhitelist.allowedRanges and set privateAddressWhitelist.enabled to true in config.yaml and restart the server.'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const agent = new PrivateRequestAgent({ privateAddressWhitelist, logBlocked, logAllowed, allowUnresolvedHosts, enableKeepAlive });
|
||||
|
||||
http.globalAgent = agent;
|
||||
https.globalAgent = agent;
|
||||
|
||||
console.info();
|
||||
console.info(color.green(LOG_HEADER), 'Enabled');
|
||||
if (agent.privateAddressWhitelist.length > 0) {
|
||||
console.info(color.green(LOG_HEADER), 'Allowed private addresses:', color.blue(agent.privateAddressWhitelist.join(', ')));
|
||||
}
|
||||
console.info();
|
||||
}
|
||||
+12
-3
@@ -14,14 +14,20 @@ const LOG_HEADER = '[Request Proxy]';
|
||||
* @property {string} url Proxy URL.
|
||||
* @property {string[]} bypass List of URLs to bypass proxy.
|
||||
* @property {boolean} enableKeepAlive Enable HTTP/HTTPS keep-alive.
|
||||
* @property {boolean} privateRequestFilterEnabled Whether the private request filter is enabled.
|
||||
*/
|
||||
export default function initRequestProxy({ enabled, url, bypass, enableKeepAlive }) {
|
||||
export default function initRequestProxy({ enabled, url, bypass, enableKeepAlive, privateRequestFilterEnabled }) {
|
||||
try {
|
||||
// No proxy is enabled, so return
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (privateRequestFilterEnabled) {
|
||||
console.warn(color.yellow(LOG_HEADER), 'Warning: Request proxy is enabled while private request filter is also enabled. Only URLs that BYPASS the request proxy will be checked.');
|
||||
console.warn(color.yellow(LOG_HEADER), 'To ensure all requests are properly filtered, disable the request proxy.');
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
console.error(color.red(LOG_HEADER), 'No proxy URL provided');
|
||||
return;
|
||||
@@ -40,8 +46,11 @@ export default function initRequestProxy({ enabled, url, bypass, enableKeepAlive
|
||||
process.env.no_proxy = bypass.join(',');
|
||||
}
|
||||
|
||||
const proxyAgentOptions = enableKeepAlive ? { keepAlive: true } : { keepAlive: false };
|
||||
const proxyAgent = new ProxyAgent(proxyAgentOptions);
|
||||
const httpAgent = http.globalAgent;
|
||||
const httpsAgent = https.globalAgent;
|
||||
|
||||
const proxyAgent = new ProxyAgent({ httpAgent, httpsAgent, keepAlive: enableKeepAlive });
|
||||
|
||||
http.globalAgent = proxyAgent;
|
||||
https.globalAgent = proxyAgent;
|
||||
|
||||
|
||||
+14
-1
@@ -48,6 +48,7 @@ import getWhitelistMiddleware from './middleware/whitelist.js';
|
||||
import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './middleware/accessLogWriter.js';
|
||||
import multerMonkeyPatch from './middleware/multerMonkeyPatch.js';
|
||||
import initRequestProxy from './request-proxy.js';
|
||||
import initPrivateRequestFilter from './private-request-filter.js';
|
||||
import cacheBuster from './middleware/cacheBuster.js';
|
||||
import corsProxyMiddleware from './middleware/corsProxy.js';
|
||||
import hostWhitelistMiddleware from './middleware/hostWhitelist.js';
|
||||
@@ -333,8 +334,20 @@ async function preSetupTasks() {
|
||||
exitProcess();
|
||||
});
|
||||
|
||||
// Add private request filter.
|
||||
const requestFilterOptions = {
|
||||
listen: cliArgs.listen,
|
||||
enabled: !!getConfigValue('privateAddressWhitelist.enabled', false, 'boolean'),
|
||||
privateAddressWhitelist: getConfigValue('privateAddressWhitelist.allowedRanges', ['127.0.0.0/8', '::1/128']),
|
||||
logBlocked: !!getConfigValue('privateAddressWhitelist.log.blockedRequests', true, 'boolean'),
|
||||
logAllowed: !!getConfigValue('privateAddressWhitelist.log.allowedRequests', false, 'boolean'),
|
||||
allowUnresolvedHosts: !!getConfigValue('privateAddressWhitelist.allowUnresolvedHosts', false, 'boolean'),
|
||||
enableKeepAlive: cliArgs.enableKeepAlive,
|
||||
};
|
||||
initPrivateRequestFilter(requestFilterOptions);
|
||||
|
||||
// Add request proxy.
|
||||
initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass, enableKeepAlive: cliArgs.enableKeepAlive });
|
||||
initRequestProxy({ enabled: cliArgs.requestProxyEnabled, url: cliArgs.requestProxyUrl, bypass: cliArgs.requestProxyBypass, enableKeepAlive: cliArgs.enableKeepAlive, privateRequestFilterEnabled: requestFilterOptions.enabled });
|
||||
|
||||
// Wait for frontend libs to compile
|
||||
await webpackMiddleware.runWebpackCompiler({ pruneCache: true });
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { describe, test, expect, jest, beforeAll, beforeEach, afterAll } from '@jest/globals';
|
||||
|
||||
const mockNetConnect = jest.fn(() => ({ type: 'net-socket' }));
|
||||
const mockTlsConnect = jest.fn(() => ({ type: 'tls-socket' }));
|
||||
const mockLookup = jest.fn();
|
||||
|
||||
jest.unstable_mockModule('node:net', () => ({
|
||||
default: { connect: mockNetConnect },
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('node:tls', () => ({
|
||||
default: { connect: mockTlsConnect },
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('node:dns', () => ({
|
||||
default: { promises: { lookup: mockLookup } },
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../src/util.js', () => ({
|
||||
color: {
|
||||
red: text => text,
|
||||
green: text => text,
|
||||
blue: text => text,
|
||||
yellow: text => text,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.unstable_mockModule('../src/express-common.js', () => ({
|
||||
filterValidIpPatterns: patterns => patterns,
|
||||
}));
|
||||
|
||||
/** @type {import('../src/private-request-filter.js').default} */
|
||||
let initPrivateRequestFilter;
|
||||
/** @type {import('node:http').default} */
|
||||
let http;
|
||||
/** @type {import('node:https').default} */
|
||||
let https;
|
||||
let originalHttpGlobalAgent;
|
||||
let originalHttpsGlobalAgent;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ default: initPrivateRequestFilter } = await import('../src/private-request-filter.js'));
|
||||
({ default: http } = await import('node:http'));
|
||||
({ default: https } = await import('node:https'));
|
||||
originalHttpGlobalAgent = http.globalAgent;
|
||||
originalHttpsGlobalAgent = https.globalAgent;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockNetConnect.mockClear();
|
||||
mockTlsConnect.mockClear();
|
||||
mockLookup.mockReset();
|
||||
http.globalAgent = originalHttpGlobalAgent;
|
||||
https.globalAgent = originalHttpsGlobalAgent;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
http.globalAgent = originalHttpGlobalAgent;
|
||||
https.globalAgent = originalHttpsGlobalAgent;
|
||||
});
|
||||
|
||||
function initAgent({ privateAddressWhitelist = [], allowUnresolvedHosts = false } = {}) {
|
||||
initPrivateRequestFilter({
|
||||
listen: false,
|
||||
enabled: true,
|
||||
privateAddressWhitelist,
|
||||
logBlocked: false,
|
||||
logAllowed: false,
|
||||
allowUnresolvedHosts,
|
||||
});
|
||||
|
||||
return http.globalAgent;
|
||||
}
|
||||
|
||||
describe('private request filter', () => {
|
||||
test('allows direct private IP requests only when whitelisted', async () => {
|
||||
const agent = initAgent({ privateAddressWhitelist: ['127.0.0.0/8'] });
|
||||
await agent.connect({}, { host: '127.0.0.1', secureEndpoint: false });
|
||||
|
||||
expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '127.0.0.1' }));
|
||||
|
||||
const blockedAgent = initAgent({ privateAddressWhitelist: [] });
|
||||
await expect(blockedAgent.connect({}, { host: '127.0.0.1', secureEndpoint: false }))
|
||||
.rejects
|
||||
.toThrow('Blocked request to private IP address: 127.0.0.1');
|
||||
});
|
||||
|
||||
test('resolves hostnames and blocks when DNS returns private IP', async () => {
|
||||
mockLookup.mockResolvedValue({ address: '192.168.1.8' });
|
||||
const agent = initAgent();
|
||||
|
||||
await expect(agent.connect({}, { host: 'example.com', secureEndpoint: false }))
|
||||
.rejects
|
||||
.toThrow('Blocked request to private IP address: 192.168.1.8');
|
||||
expect(mockNetConnect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('connects to resolved public IP to avoid hostname re-resolution', async () => {
|
||||
mockLookup.mockResolvedValue({ address: '93.184.216.34' });
|
||||
const agent = initAgent();
|
||||
|
||||
await agent.connect({}, { host: 'example.com', secureEndpoint: false });
|
||||
|
||||
expect(mockLookup).toHaveBeenCalledWith('example.com');
|
||||
expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '93.184.216.34' }));
|
||||
});
|
||||
|
||||
test('handles unresolved hosts according to allowUnresolvedHosts setting', async () => {
|
||||
mockLookup.mockRejectedValue(new Error('lookup failed'));
|
||||
const blockedAgent = initAgent({ allowUnresolvedHosts: false });
|
||||
|
||||
await expect(blockedAgent.connect({}, { host: 'missing-host.local', secureEndpoint: false }))
|
||||
.rejects
|
||||
.toThrow('Unable to resolve host: missing-host.local. Set privateAddressWhitelist.allowUnresolvedHosts to true to bypass this check.');
|
||||
expect(mockNetConnect).not.toHaveBeenCalled();
|
||||
|
||||
const allowedAgent = initAgent({ allowUnresolvedHosts: true });
|
||||
await allowedAgent.connect({}, { host: 'missing-host.local', secureEndpoint: false });
|
||||
expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: 'missing-host.local' }));
|
||||
});
|
||||
|
||||
test('uses tls.connect for secure endpoints', async () => {
|
||||
mockLookup.mockResolvedValue({ address: '93.184.216.34' });
|
||||
const agent = initAgent();
|
||||
await agent.connect({}, { host: 'example.com', secureEndpoint: true });
|
||||
expect(mockLookup).toHaveBeenCalledWith('example.com');
|
||||
expect(mockTlsConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '93.184.216.34' }));
|
||||
expect(mockNetConnect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user