245 lines
7.4 KiB
TypeScript
245 lines
7.4 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { SmartProxy } from '../ts/index.js';
|
|
import * as plugins from '../ts/plugins.js';
|
|
import * as net from 'net';
|
|
import * as http from 'http';
|
|
|
|
/**
|
|
* This test verifies our improved port binding intelligence for ACME challenges.
|
|
* It specifically tests:
|
|
* 1. Using port 8080 instead of 80 for ACME HTTP challenges
|
|
* 2. Correctly handling shared port bindings between regular routes and challenge routes
|
|
* 3. Avoiding port conflicts when updating routes
|
|
*/
|
|
|
|
tap.test('should handle ACME challenges on port 8080 with improved port binding intelligence', async (tapTest) => {
|
|
// Create a simple echo server to act as our target
|
|
const targetPort = 9001;
|
|
let receivedData = '';
|
|
|
|
const targetServer = net.createServer((socket) => {
|
|
console.log('Target server received connection');
|
|
|
|
socket.on('data', (data) => {
|
|
receivedData += data.toString();
|
|
console.log('Target server received data:', data.toString().split('\n')[0]);
|
|
|
|
// Send a simple HTTP response
|
|
const response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!';
|
|
socket.write(response);
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => {
|
|
targetServer.listen(targetPort, () => {
|
|
console.log(`Target server listening on port ${targetPort}`);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
// In this test we will NOT create a mock ACME server on the same port
|
|
// as SmartProxy will use, instead we'll let SmartProxy handle it
|
|
const acmeServerPort = 9009;
|
|
const acmeRequests: string[] = [];
|
|
let acmeServer: http.Server | null = null;
|
|
|
|
// We'll assume the ACME port is available for SmartProxy
|
|
let acmePortAvailable = true;
|
|
|
|
// Create SmartProxy with ACME configured to use port 8080
|
|
console.log('Creating SmartProxy with ACME port 8080...');
|
|
const tempCertDir = './temp-certs';
|
|
|
|
try {
|
|
await plugins.smartfile.fs.ensureDir(tempCertDir);
|
|
} catch (error) {
|
|
// Directory may already exist, that's ok
|
|
}
|
|
|
|
const proxy = new SmartProxy({
|
|
enableDetailedLogging: true,
|
|
routes: [
|
|
{
|
|
name: 'test-route',
|
|
match: {
|
|
ports: [9003],
|
|
domains: ['test.example.com']
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: targetPort },
|
|
tls: {
|
|
mode: 'terminate',
|
|
certificate: 'auto' // Use ACME for certificate
|
|
}
|
|
}
|
|
},
|
|
// Also add a route for port 8080 to test port sharing
|
|
{
|
|
name: 'http-route',
|
|
match: {
|
|
ports: [9009],
|
|
domains: ['test.example.com']
|
|
},
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: targetPort }
|
|
}
|
|
}
|
|
],
|
|
acme: {
|
|
email: 'test@example.com',
|
|
useProduction: false,
|
|
port: 9009, // Use 9009 instead of default 80
|
|
certificateStore: tempCertDir
|
|
}
|
|
});
|
|
|
|
// Mock the certificate manager to avoid actual ACME operations
|
|
console.log('Mocking certificate manager...');
|
|
const createCertManager = (proxy as any).createCertificateManager;
|
|
(proxy as any).createCertificateManager = async function(...args: any[]) {
|
|
// Create a completely mocked certificate manager that doesn't use ACME at all
|
|
return {
|
|
initialize: async () => {},
|
|
getCertPair: async () => {
|
|
return {
|
|
publicKey: 'MOCK CERTIFICATE',
|
|
privateKey: 'MOCK PRIVATE KEY'
|
|
};
|
|
},
|
|
getAcmeOptions: () => {
|
|
return {
|
|
port: 9009
|
|
};
|
|
},
|
|
getState: () => {
|
|
return {
|
|
initializing: false,
|
|
ready: true,
|
|
port: 9009
|
|
};
|
|
},
|
|
provisionAllCertificates: async () => {
|
|
console.log('Mock: Provisioning certificates');
|
|
return [];
|
|
},
|
|
stop: async () => {},
|
|
smartAcme: {
|
|
getCertificateForDomain: async () => {
|
|
// Return a mock certificate
|
|
return {
|
|
publicKey: 'MOCK CERTIFICATE',
|
|
privateKey: 'MOCK PRIVATE KEY',
|
|
validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000,
|
|
created: Date.now()
|
|
};
|
|
},
|
|
start: async () => {},
|
|
stop: async () => {}
|
|
}
|
|
};
|
|
};
|
|
|
|
// Track port binding attempts to verify intelligence
|
|
const portBindAttempts: number[] = [];
|
|
const originalAddPort = (proxy as any).portManager.addPort;
|
|
(proxy as any).portManager.addPort = async function(port: number) {
|
|
portBindAttempts.push(port);
|
|
return originalAddPort.call(this, port);
|
|
};
|
|
|
|
try {
|
|
console.log('Starting SmartProxy...');
|
|
await proxy.start();
|
|
|
|
console.log('Port binding attempts:', portBindAttempts);
|
|
|
|
// Check that we tried to bind to port 9009
|
|
// Should attempt to bind to port 9009
|
|
expect(portBindAttempts.includes(9009)).toEqual(true);
|
|
// Should attempt to bind to port 9003
|
|
expect(portBindAttempts.includes(9003)).toEqual(true);
|
|
|
|
// Get actual bound ports
|
|
const boundPorts = proxy.getListeningPorts();
|
|
console.log('Actually bound ports:', boundPorts);
|
|
|
|
// If port 9009 was available, we should be bound to it
|
|
if (acmePortAvailable) {
|
|
// Should be bound to port 9009 if available
|
|
expect(boundPorts.includes(9009)).toEqual(true);
|
|
}
|
|
|
|
// Should be bound to port 9003
|
|
expect(boundPorts.includes(9003)).toEqual(true);
|
|
|
|
// Test adding a new route on port 8080
|
|
console.log('Testing route update with port reuse...');
|
|
|
|
// Reset tracking
|
|
portBindAttempts.length = 0;
|
|
|
|
// Add a new route on port 8080
|
|
const newRoutes = [
|
|
...proxy.settings.routes,
|
|
{
|
|
name: 'additional-route',
|
|
match: {
|
|
ports: [9009],
|
|
path: '/additional'
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
target: { host: 'localhost', port: targetPort }
|
|
}
|
|
}
|
|
];
|
|
|
|
// Update routes - this should NOT try to rebind port 8080
|
|
await proxy.updateRoutes(newRoutes);
|
|
|
|
console.log('Port binding attempts after update:', portBindAttempts);
|
|
|
|
// We should not try to rebind port 9009 since it's already bound
|
|
// Should not attempt to rebind port 9009
|
|
expect(portBindAttempts.includes(9009)).toEqual(false);
|
|
|
|
// We should still be listening on both ports
|
|
const portsAfterUpdate = proxy.getListeningPorts();
|
|
console.log('Bound ports after update:', portsAfterUpdate);
|
|
|
|
if (acmePortAvailable) {
|
|
// Should still be bound to port 9009
|
|
expect(portsAfterUpdate.includes(9009)).toEqual(true);
|
|
}
|
|
// Should still be bound to port 9003
|
|
expect(portsAfterUpdate.includes(9003)).toEqual(true);
|
|
|
|
// The test is successful at this point - we've verified the port binding intelligence
|
|
console.log('Port binding intelligence verified successfully!');
|
|
// We'll skip the actual connection test to avoid timeouts
|
|
} finally {
|
|
// Clean up
|
|
console.log('Cleaning up...');
|
|
await proxy.stop();
|
|
|
|
if (targetServer) {
|
|
await new Promise<void>((resolve) => {
|
|
targetServer.close(() => resolve());
|
|
});
|
|
}
|
|
|
|
// No acmeServer to close in this test
|
|
|
|
// Clean up temp directory
|
|
try {
|
|
// Remove temp directory
|
|
await plugins.smartfile.fs.remove(tempCertDir);
|
|
} catch (error) {
|
|
console.error('Failed to remove temp directory:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
tap.start(); |