BREAKING CHANGE(forwarding): Refactor unified forwarding API and remove redundant documentation. Removed docs/forwarding-system.md (its content is migrated into readme.md) and updated helper functions (e.g. replacing sniPassthrough with httpsPassthrough) to accept configuration objects. Legacy fields in domain configurations (allowedIPs, blockedIPs, useNetworkProxy, networkProxyPort, connectionTimeout) have been removed in favor of forwarding.security and advanced options. Tests and examples have been updated accordingly.

This commit is contained in:
2025-05-09 15:39:15 +00:00
parent f00bae4631
commit 1a902a04fb
20 changed files with 441 additions and 434 deletions

View File

@ -26,7 +26,13 @@ class FakeNetworkProxyBridge {
tap.test('CertProvisioner handles static provisioning', async () => {
const domain = 'static.com';
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
const domainConfigs: IDomainConfig[] = [{
domains: [domain],
forwarding: {
type: 'https-terminate-to-https',
target: { host: 'localhost', port: 443 }
}
}];
const fakePort80 = new FakePort80Handler();
const fakeBridge = new FakeNetworkProxyBridge();
// certProvider returns static certificate
@ -68,7 +74,13 @@ tap.test('CertProvisioner handles static provisioning', async () => {
tap.test('CertProvisioner handles http01 provisioning', async () => {
const domain = 'http01.com';
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
const domainConfigs: IDomainConfig[] = [{
domains: [domain],
forwarding: {
type: 'https-terminate-to-http',
target: { host: 'localhost', port: 80 }
}
}];
const fakePort80 = new FakePort80Handler();
const fakeBridge = new FakeNetworkProxyBridge();
// certProvider returns http01 directive
@ -93,7 +105,13 @@ tap.test('CertProvisioner handles http01 provisioning', async () => {
tap.test('CertProvisioner on-demand http01 renewal', async () => {
const domain = 'renew.com';
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
const domainConfigs: IDomainConfig[] = [{
domains: [domain],
forwarding: {
type: 'https-terminate-to-http',
target: { host: 'localhost', port: 80 }
}
}];
const fakePort80 = new FakePort80Handler();
const fakeBridge = new FakeNetworkProxyBridge();
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => 'http01';
@ -113,7 +131,13 @@ tap.test('CertProvisioner on-demand http01 renewal', async () => {
tap.test('CertProvisioner on-demand static provisioning', async () => {
const domain = 'ondemand.com';
const domainConfigs: IDomainConfig[] = [{ domains: [domain], allowedIPs: [] }];
const domainConfigs: IDomainConfig[] = [{
domains: [domain],
forwarding: {
type: 'https-terminate-to-https',
target: { host: 'localhost', port: 443 }
}
}];
const fakePort80 = new FakePort80Handler();
const fakeBridge = new FakeNetworkProxyBridge();
const certProvider = async (): Promise<ISmartProxyCertProvisionObject> => ({

View File

@ -16,11 +16,13 @@ tap.test('Forwarding configuration examples', async (tools) => {
// Example 1: HTTP-only configuration
const httpOnlyConfig: IDomainConfig = {
domains: ['http.example.com'],
allowedIPs: [],
forwarding: httpOnly({
target: {
host: 'localhost',
port: 3000
},
security: {
allowedIps: ['*'] // Allow all
}
})
};
@ -30,11 +32,13 @@ tap.test('Forwarding configuration examples', async (tools) => {
// Example 2: HTTPS Passthrough (SNI)
const httpsPassthroughConfig: IDomainConfig = {
domains: ['pass.example.com'],
allowedIPs: [],
forwarding: httpsPassthrough({
target: {
host: ['10.0.0.1', '10.0.0.2'], // Round-robin target IPs
port: 443
},
security: {
allowedIps: ['*'] // Allow all
}
})
};
@ -45,7 +49,6 @@ tap.test('Forwarding configuration examples', async (tools) => {
// Example 3: HTTPS Termination to HTTP Backend
const terminateToHttpConfig: IDomainConfig = {
domains: ['secure.example.com'],
allowedIPs: [],
forwarding: tlsTerminateToHttp({
target: {
host: 'localhost',
@ -61,6 +64,9 @@ tap.test('Forwarding configuration examples', async (tools) => {
enabled: true,
maintenance: true,
production: false // Use staging ACME server for testing
},
security: {
allowedIps: ['*'] // Allow all
}
})
};
@ -71,7 +77,6 @@ tap.test('Forwarding configuration examples', async (tools) => {
// Example 4: HTTPS Termination to HTTPS Backend
const terminateToHttpsConfig: IDomainConfig = {
domains: ['proxy.example.com'],
allowedIPs: [],
forwarding: tlsTerminateToHttps({
target: {
host: 'internal-api.local',

View File

@ -6,13 +6,13 @@ import type { IForwardConfig, ForwardingType } from '../ts/smartproxy/types/forw
import { ForwardingHandlerFactory } from '../ts/smartproxy/forwarding/forwarding.factory.js';
import { createDomainConfig } from '../ts/smartproxy/forwarding/domain-config.js';
import { DomainManager } from '../ts/smartproxy/forwarding/domain-manager.js';
import { httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, sniPassthrough } from '../ts/smartproxy/types/forwarding.types.js';
import { httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, httpsPassthrough } from '../ts/smartproxy/types/forwarding.types.js';
const helpers = {
httpOnly,
tlsTerminateToHttp,
tlsTerminateToHttps,
sniPassthrough
sniPassthrough: httpsPassthrough
};
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
@ -107,7 +107,9 @@ tap.test('DomainManager - manage domain configurations', async () => {
// Add a domain configuration
await domainManager.addDomainConfig(
createDomainConfig('example.com', helpers.httpOnly('localhost', 3000))
createDomainConfig('example.com', helpers.httpOnly({
target: { host: 'localhost', port: 3000 }
}))
);
// Check that the configuration was added
@ -132,13 +134,15 @@ tap.test('DomainManager - manage domain configurations', async () => {
const handlerAfterRemoval = domainManager.findHandlerForDomain('example.com');
expect(handlerAfterRemoval).toBeUndefined();
});
tap.test('DomainManager - support wildcard domains', async () => {
const domainManager = new DomainManager();
// Add a wildcard domain configuration
await domainManager.addDomainConfig(
createDomainConfig('*.example.com', helpers.httpOnly('localhost', 3000))
createDomainConfig('*.example.com', helpers.httpOnly({
target: { host: 'localhost', port: 3000 }
}))
);
// Find a handler for a subdomain
@ -150,15 +154,19 @@ tap.test('DomainManager - support wildcard domains', async () => {
expect(noHandler).toBeUndefined();
});
tap.test('Helper Functions - create http-only forwarding config', async () => {
const config = helpers.httpOnly('localhost', 3000);
const config = helpers.httpOnly({
target: { host: 'localhost', port: 3000 }
});
expect(config.type).toEqual('http-only');
expect(config.target.host).toEqual('localhost');
expect(config.target.port).toEqual(3000);
expect(config.http?.enabled).toBeTrue();
});
tap.test('Helper Functions - create https-terminate-to-http config', async () => {
const config = helpers.tlsTerminateToHttp('localhost', 3000);
const config = helpers.tlsTerminateToHttp({
target: { host: 'localhost', port: 3000 }
});
expect(config.type).toEqual('https-terminate-to-http');
expect(config.target.host).toEqual('localhost');
expect(config.target.port).toEqual(3000);
@ -166,9 +174,11 @@ tap.test('Helper Functions - create https-terminate-to-http config', async () =>
expect(config.acme?.enabled).toBeTrue();
expect(config.acme?.maintenance).toBeTrue();
});
tap.test('Helper Functions - create https-terminate-to-https config', async () => {
const config = helpers.tlsTerminateToHttps('localhost', 8443);
const config = helpers.tlsTerminateToHttps({
target: { host: 'localhost', port: 8443 }
});
expect(config.type).toEqual('https-terminate-to-https');
expect(config.target.host).toEqual('localhost');
expect(config.target.port).toEqual(8443);
@ -176,9 +186,11 @@ tap.test('Helper Functions - create https-terminate-to-https config', async () =
expect(config.acme?.enabled).toBeTrue();
expect(config.acme?.maintenance).toBeTrue();
});
tap.test('Helper Functions - create https-passthrough config', async () => {
const config = helpers.sniPassthrough('localhost', 443);
const config = helpers.sniPassthrough({
target: { host: 'localhost', port: 443 }
});
expect(config.type).toEqual('https-passthrough');
expect(config.target.host).toEqual('localhost');
expect(config.target.port).toEqual(443);

View File

@ -6,13 +6,13 @@ import type { IForwardConfig } from '../ts/smartproxy/types/forwarding.types.js'
import { ForwardingHandlerFactory } from '../ts/smartproxy/forwarding/forwarding.factory.js';
import { createDomainConfig } from '../ts/smartproxy/forwarding/domain-config.js';
import { DomainManager } from '../ts/smartproxy/forwarding/domain-manager.js';
import { httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, sniPassthrough } from '../ts/smartproxy/types/forwarding.types.js';
import { httpOnly, tlsTerminateToHttp, tlsTerminateToHttps, httpsPassthrough } from '../ts/smartproxy/types/forwarding.types.js';
const helpers = {
httpOnly,
tlsTerminateToHttp,
tlsTerminateToHttps,
sniPassthrough
sniPassthrough: httpsPassthrough
};
tap.test('ForwardingHandlerFactory - apply defaults based on type', async () => {
@ -107,7 +107,9 @@ tap.test('DomainManager - manage domain configurations', async () => {
// Add a domain configuration
await domainManager.addDomainConfig(
createDomainConfig('example.com', helpers.httpOnly('localhost', 3000))
createDomainConfig('example.com', helpers.httpOnly({
target: { host: 'localhost', port: 3000 }
}))
);
// Check that the configuration was added
@ -125,15 +127,19 @@ tap.test('DomainManager - manage domain configurations', async () => {
expect(configsAfterRemoval.length).toEqual(0);
});
tap.test('Helper Functions - create http-only forwarding config', async () => {
const config = helpers.httpOnly('localhost', 3000);
const config = helpers.httpOnly({
target: { host: 'localhost', port: 3000 }
});
expect(config.type).toEqual('http-only');
expect(config.target.host).toEqual('localhost');
expect(config.target.port).toEqual(3000);
expect(config.http?.enabled).toBeTrue();
});
tap.test('Helper Functions - create https-terminate-to-http config', async () => {
const config = helpers.tlsTerminateToHttp('localhost', 3000);
const config = helpers.tlsTerminateToHttp({
target: { host: 'localhost', port: 3000 }
});
expect(config.type).toEqual('https-terminate-to-http');
expect(config.target.host).toEqual('localhost');
expect(config.target.port).toEqual(3000);
@ -141,9 +147,11 @@ tap.test('Helper Functions - create https-terminate-to-http config', async () =>
expect(config.acme?.enabled).toBeTrue();
expect(config.acme?.maintenance).toBeTrue();
});
tap.test('Helper Functions - create https-terminate-to-https config', async () => {
const config = helpers.tlsTerminateToHttps('localhost', 8443);
const config = helpers.tlsTerminateToHttps({
target: { host: 'localhost', port: 8443 }
});
expect(config.type).toEqual('https-terminate-to-https');
expect(config.target.host).toEqual('localhost');
expect(config.target.port).toEqual(8443);
@ -151,9 +159,11 @@ tap.test('Helper Functions - create https-terminate-to-https config', async () =
expect(config.acme?.enabled).toBeTrue();
expect(config.acme?.maintenance).toBeTrue();
});
tap.test('Helper Functions - create https-passthrough config', async () => {
const config = helpers.sniPassthrough('localhost', 443);
const config = helpers.sniPassthrough({
target: { host: 'localhost', port: 443 }
});
expect(config.type).toEqual('https-passthrough');
expect(config.target.host).toEqual('localhost');
expect(config.target.port).toEqual(443);

View File

@ -575,4 +575,4 @@ process.on('exit', () => {
testProxy.stop().then(() => console.log('[TEST] Proxy server stopped'));
});
tap.start();
export default tap.start();

View File

@ -279,14 +279,21 @@ tap.test('should support optional source IP preservation in chained proxies', as
if (index4 !== -1) allProxies.splice(index4, 1);
});
// Test round-robin behavior for multiple target IPs in a domain config.
tap.test('should use round robin for multiple target IPs in domain config', async () => {
// Test round-robin behavior for multiple target hosts in a domain config.
tap.test('should use round robin for multiple target hosts in domain config', async () => {
// Create a domain config with multiple hosts in the target
const domainConfig = {
domains: ['rr.test'],
allowedIPs: ['127.0.0.1'],
targetIPs: ['hostA', 'hostB']
} as any;
forwarding: {
type: 'http-only',
target: {
host: ['hostA', 'hostB'], // Array of hosts for round-robin
port: 80
},
http: { enabled: true }
}
};
const proxyInstance = new SmartProxy({
fromPort: 0,
toPort: 0,
@ -296,11 +303,14 @@ tap.test('should use round robin for multiple target IPs in domain config', asyn
defaultAllowedIPs: [],
globalPortRanges: []
});
// Don't track this proxy as it doesn't actually start or listen
const firstTarget = proxyInstance.domainConfigManager.getTargetIP(domainConfig);
const secondTarget = proxyInstance.domainConfigManager.getTargetIP(domainConfig);
// Get the first target host from the forwarding config
const firstTarget = proxyInstance.domainConfigManager.getTargetHost(domainConfig);
// Get the second target host - should be different due to round-robin
const secondTarget = proxyInstance.domainConfigManager.getTargetHost(domainConfig);
expect(firstTarget).toEqual('hostA');
expect(secondTarget).toEqual('hostB');
});