smartproxy/test/test.port80-management.node.ts

345 lines
8.7 KiB
TypeScript

import { expect, tap } from '@push.rocks/tapbundle';
import { SmartProxy } from '../ts/index.js';
tap.test('should not double-register port 80 when user route and ACME use same port', async (tools) => {
tools.timeout(5000);
let port80AddCount = 0;
const activePorts = new Set<number>();
const settings = {
port: 9901,
routes: [
{
name: 'user-route',
match: {
ports: [80]
},
action: {
type: 'forward' as const,
targetUrl: 'http://localhost:3000'
}
},
{
name: 'acme-route',
match: {
ports: [443]
},
action: {
type: 'forward' as const,
targetUrl: 'https://localhost:3001',
tls: {
mode: 'terminate' as const,
certificate: 'auto'
}
}
}
],
acme: {
email: 'test@test.com',
port: 80 // ACME on same port as user route
}
};
const proxy = new SmartProxy(settings);
// Mock the port manager to track port additions
(proxy as any).portManager = {
addPort: async (port: number) => {
if (activePorts.has(port)) {
// This is the deduplication behavior we're testing
return;
}
activePorts.add(port);
if (port === 80) {
port80AddCount++;
}
},
addPorts: async (ports: number[]) => {
for (const port of ports) {
await (proxy as any).portManager.addPort(port);
}
},
removePort: async (port: number) => {
activePorts.delete(port);
},
updatePorts: async (requiredPorts: Set<number>) => {
const portsToRemove = [];
for (const port of activePorts) {
if (!requiredPorts.has(port)) {
portsToRemove.push(port);
}
}
const portsToAdd = [];
for (const port of requiredPorts) {
if (!activePorts.has(port)) {
portsToAdd.push(port);
}
}
for (const port of portsToRemove) {
await (proxy as any).portManager.removePort(port);
}
for (const port of portsToAdd) {
await (proxy as any).portManager.addPort(port);
}
},
setShuttingDown: () => {},
getPortForRoutes: () => new Map(),
closeAll: async () => { activePorts.clear(); },
stop: async () => { await (proxy as any).portManager.closeAll(); }
};
// Mock NFTables
(proxy as any).nftablesManager = {
ensureNFTablesSetup: async () => {},
stop: async () => {}
};
// Mock certificate manager to prevent ACME
(proxy as any).createCertificateManager = async function(routes: any[], certDir: string, acmeOptions: any) {
const certManager = {
routes: routes,
globalAcmeDefaults: acmeOptions,
updateRoutesCallback: null as any,
challengeRouteActive: false,
setUpdateRoutesCallback: function(callback: any) {
this.updateRoutesCallback = callback;
},
setNetworkProxy: function() {},
setGlobalAcmeDefaults: function(defaults: any) {
this.globalAcmeDefaults = defaults;
},
initialize: async function() {
const hasAcmeRoutes = routes.some((r: any) =>
r.action.tls?.certificate === 'auto'
);
if (hasAcmeRoutes && acmeOptions?.email) {
const challengeRoute = {
name: 'acme-challenge',
priority: 1000,
match: {
ports: acmeOptions.port || 80,
path: '/.well-known/acme-challenge/*'
},
action: {
type: 'static',
handler: async () => ({ status: 200, body: 'challenge' })
}
};
const updatedRoutes = [...routes, challengeRoute];
if (this.updateRoutesCallback) {
await this.updateRoutesCallback(updatedRoutes);
}
this.challengeRouteActive = true;
}
},
getAcmeOptions: function() {
return acmeOptions;
},
stop: async function() {}
};
certManager.setUpdateRoutesCallback(async (routes: any[]) => {
await this.updateRoutes(routes);
});
await certManager.initialize();
return certManager;
};
// Mock admin server to prevent binding
(proxy as any).startAdminServer = async function() {
this.servers.set(this.settings.port, {
port: this.settings.port,
close: async () => {}
});
};
try {
await proxy.start();
// Verify that port 80 was added only once
tools.expect(port80AddCount).toEqual(1);
} finally {
await proxy.stop();
}
});
tap.test('should handle ACME on different port than user routes', async (tools) => {
tools.timeout(5000);
const portAddHistory: number[] = [];
const activePorts = new Set<number>();
const settings = {
port: 9902,
routes: [
{
name: 'user-route',
match: {
ports: [80]
},
action: {
type: 'forward' as const,
targetUrl: 'http://localhost:3000'
}
},
{
name: 'acme-route',
match: {
ports: [443]
},
action: {
type: 'forward' as const,
targetUrl: 'https://localhost:3001',
tls: {
mode: 'terminate' as const,
certificate: 'auto'
}
}
}
],
acme: {
email: 'test@test.com',
port: 8080 // ACME on different port than user routes
}
};
const proxy = new SmartProxy(settings);
// Mock the port manager
(proxy as any).portManager = {
addPort: async (port: number) => {
if (!activePorts.has(port)) {
activePorts.add(port);
portAddHistory.push(port);
}
},
addPorts: async (ports: number[]) => {
for (const port of ports) {
await (proxy as any).portManager.addPort(port);
}
},
removePort: async (port: number) => {
activePorts.delete(port);
},
updatePorts: async (requiredPorts: Set<number>) => {
for (const port of requiredPorts) {
await (proxy as any).portManager.addPort(port);
}
},
setShuttingDown: () => {},
getPortForRoutes: () => new Map(),
closeAll: async () => { activePorts.clear(); },
stop: async () => { await (proxy as any).portManager.closeAll(); }
};
// Mock NFTables
(proxy as any).nftablesManager = {
ensureNFTablesSetup: async () => {},
stop: async () => {}
};
// Mock certificate manager
(proxy as any).createCertificateManager = async function(routes: any[], certDir: string, acmeOptions: any) {
const certManager = {
routes: routes,
globalAcmeDefaults: acmeOptions,
updateRoutesCallback: null as any,
challengeRouteActive: false,
setUpdateRoutesCallback: function(callback: any) {
this.updateRoutesCallback = callback;
},
setNetworkProxy: function() {},
setGlobalAcmeDefaults: function(defaults: any) {
this.globalAcmeDefaults = defaults;
},
initialize: async function() {
const hasAcmeRoutes = routes.some((r: any) =>
r.action.tls?.certificate === 'auto'
);
if (hasAcmeRoutes && acmeOptions?.email) {
const challengeRoute = {
name: 'acme-challenge',
priority: 1000,
match: {
ports: acmeOptions.port || 80,
path: '/.well-known/acme-challenge/*'
},
action: {
type: 'static',
handler: async () => ({ status: 200, body: 'challenge' })
}
};
const updatedRoutes = [...routes, challengeRoute];
if (this.updateRoutesCallback) {
await this.updateRoutesCallback(updatedRoutes);
}
this.challengeRouteActive = true;
}
},
getAcmeOptions: function() {
return acmeOptions;
},
stop: async function() {}
};
certManager.setUpdateRoutesCallback(async (routes: any[]) => {
await this.updateRoutes(routes);
});
await certManager.initialize();
return certManager;
};
// Mock admin server
(proxy as any).startAdminServer = async function() {
this.servers.set(this.settings.port, {
port: this.settings.port,
close: async () => {}
});
};
try {
await proxy.start();
// Verify that all expected ports were added
tools.expect(portAddHistory).toInclude(80); // User route
tools.expect(portAddHistory).toInclude(443); // TLS route
tools.expect(portAddHistory).toInclude(8080); // ACME challenge
} finally {
await proxy.stop();
}
});
export default tap;