345 lines
8.7 KiB
TypeScript
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; |