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:
328
test/test.race-conditions.node.ts
Normal file
328
test/test.race-conditions.node.ts
Normal 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;
|
Reference in New Issue
Block a user