fix(acme): Fix port 80 ACME management and challenge route concurrency issues by deduplicating port listeners, preserving challenge route state across certificate manager recreations, and adding mutex locks to route updates.

This commit is contained in:
2025-05-19 03:40:58 +00:00
parent 0bd35c4fb3
commit 3fcdce611c
13 changed files with 1494 additions and 351 deletions

View File

@ -0,0 +1,328 @@
import { expect, tap } from '@push.rocks/tapbundle';
import { SmartProxy } from '../ts/index.js';
tap.test('should handle concurrent route updates without race conditions', async (tools) => {
tools.timeout(10000);
const settings = {
port: 6001,
routes: [
{
name: 'initial-route',
match: {
ports: 80
},
action: {
type: 'forward' as const,
targetUrl: 'http://localhost:3000'
}
}
],
acme: {
email: 'test@test.com',
port: 80
}
};
const proxy = new SmartProxy(settings);
await proxy.start();
// Simulate concurrent route updates
const updates = [];
for (let i = 0; i < 5; i++) {
updates.push(proxy.updateRoutes([
...settings.routes,
{
name: `route-${i}`,
match: {
ports: [443]
},
action: {
type: 'forward' as const,
targetUrl: `https://localhost:${3001 + i}`,
tls: {
mode: 'terminate' as const,
certificate: 'auto'
}
}
}
]));
}
// All updates should complete without errors
await Promise.all(updates);
// Verify final state
const currentRoutes = proxy['settings'].routes;
tools.expect(currentRoutes.length).toBeGreaterThan(1);
await proxy.stop();
});
tap.test('should preserve certificate manager state during rapid updates', async (tools) => {
tools.timeout(10000);
const settings = {
port: 6002,
routes: [
{
name: 'test-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
}
};
const proxy = new SmartProxy(settings);
await proxy.start();
// Get initial certificate manager reference
const initialCertManager = proxy['certManager'];
tools.expect(initialCertManager).not.toBeNull();
// Perform rapid route updates
for (let i = 0; i < 3; i++) {
await proxy.updateRoutes([
...settings.routes,
{
name: `extra-route-${i}`,
match: {
ports: [8000 + i]
},
action: {
type: 'forward' as const,
targetUrl: `http://localhost:${4000 + i}`
}
}
]);
}
// Certificate manager should be recreated but state preserved
const finalCertManager = proxy['certManager'];
tools.expect(finalCertManager).not.toBeNull();
tools.expect(finalCertManager).not.toEqual(initialCertManager);
await proxy.stop();
});
tap.test('should handle challenge route state correctly across recreations', async (tools) => {
tools.timeout(10000);
let challengeRouteAddCount = 0;
const settings = {
port: 6003,
routes: [
{
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
}
};
const proxy = new SmartProxy(settings);
// Mock the route update to count challenge route additions
const originalUpdateRoutes = proxy['updateRoutes'].bind(proxy);
proxy['updateRoutes'] = async (routes: any[]) => {
if (routes.some(r => r.name === 'acme-challenge')) {
challengeRouteAddCount++;
}
return originalUpdateRoutes(routes);
};
await proxy.start();
// Multiple route updates
for (let i = 0; i < 3; i++) {
await proxy.updateRoutes([
...settings.routes,
{
name: `dynamic-route-${i}`,
match: {
ports: [9000 + i]
},
action: {
type: 'forward' as const,
targetUrl: `http://localhost:${5000 + i}`
}
}
]);
}
// Challenge route should only be added once during initial start
tools.expect(challengeRouteAddCount).toEqual(1);
await proxy.stop();
});
tap.test('should prevent port conflicts during certificate manager recreation', async (tools) => {
tools.timeout(10000);
const settings = {
port: 6004,
routes: [
{
name: 'http-route',
match: {
ports: [80]
},
action: {
type: 'forward' as const,
targetUrl: 'http://localhost:3000'
}
},
{
name: 'https-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 // Same as user route
}
};
const proxy = new SmartProxy(settings);
// Track port operations
let port80AddCount = 0;
const originalPortManager = proxy['portManager'];
const originalAddPort = originalPortManager.addPort.bind(originalPortManager);
originalPortManager.addPort = async (port: number) => {
if (port === 80) {
port80AddCount++;
}
return originalAddPort(port);
};
await proxy.start();
// Update routes multiple times
for (let i = 0; i < 3; i++) {
await proxy.updateRoutes([
...settings.routes,
{
name: `temp-route-${i}`,
match: {
ports: [7000 + i]
},
action: {
type: 'forward' as const,
targetUrl: `http://localhost:${6000 + i}`
}
}
]);
}
// Port 80 should be maintained properly without conflicts
tools.expect(port80AddCount).toBeGreaterThan(0);
await proxy.stop();
});
tap.test('should handle mutex locking correctly', async (tools) => {
tools.timeout(10000);
const settings = {
port: 6005,
routes: [
{
name: 'test-route',
match: {
ports: [80]
},
action: {
type: 'forward' as const,
targetUrl: 'http://localhost:3000'
}
}
]
};
const proxy = new SmartProxy(settings);
await proxy.start();
let updateStartCount = 0;
let updateEndCount = 0;
// Wrap updateRoutes to track concurrent execution
const originalUpdateRoutes = proxy['updateRoutes'].bind(proxy);
proxy['updateRoutes'] = async (routes: any[]) => {
updateStartCount++;
const startCount = updateStartCount;
const endCount = updateEndCount;
// If mutex is working, start count should never be more than end count + 1
tools.expect(startCount).toBeLessThanOrEqual(endCount + 1);
const result = await originalUpdateRoutes(routes);
updateEndCount++;
return result;
};
// Trigger multiple concurrent updates
const updates = [];
for (let i = 0; i < 5; i++) {
updates.push(proxy.updateRoutes([
...settings.routes,
{
name: `concurrent-route-${i}`,
match: {
ports: [2000 + i]
},
action: {
type: 'forward' as const,
targetUrl: `http://localhost:${3000 + i}`
}
}
]));
}
await Promise.all(updates);
// All updates should have completed
tools.expect(updateStartCount).toEqual(5);
tools.expect(updateEndCount).toEqual(5);
await proxy.stop();
});
export default tap;