218 lines
6.1 KiB
TypeScript
218 lines
6.1 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { SmartProxy } from '../ts/index.js';
|
|
import * as plugins from '../ts/plugins.js';
|
|
|
|
/**
|
|
* Test that verifies ACME challenge routes are properly created
|
|
*/
|
|
tap.test('should create ACME challenge route', async (tools) => {
|
|
tools.timeout(5000);
|
|
|
|
// Create a challenge route manually to test its structure
|
|
const challengeRoute = {
|
|
name: 'acme-challenge',
|
|
priority: 1000,
|
|
match: {
|
|
ports: 18080,
|
|
path: '/.well-known/acme-challenge/*'
|
|
},
|
|
action: {
|
|
type: 'socket-handler' as const,
|
|
socketHandler: (socket: any, context: any) => {
|
|
socket.once('data', (data: Buffer) => {
|
|
const request = data.toString();
|
|
const lines = request.split('\r\n');
|
|
const [method, path] = lines[0].split(' ');
|
|
const token = path?.split('/').pop() || '';
|
|
|
|
const response = [
|
|
'HTTP/1.1 200 OK',
|
|
'Content-Type: text/plain',
|
|
`Content-Length: ${token.length}`,
|
|
'Connection: close',
|
|
'',
|
|
token
|
|
].join('\r\n');
|
|
|
|
socket.write(response);
|
|
socket.end();
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// Test that the challenge route has the correct structure
|
|
expect(challengeRoute).toBeDefined();
|
|
expect(challengeRoute.match.path).toEqual('/.well-known/acme-challenge/*');
|
|
expect(challengeRoute.match.ports).toEqual(18080);
|
|
expect(challengeRoute.action.type).toEqual('socket-handler');
|
|
expect(challengeRoute.priority).toEqual(1000);
|
|
|
|
// Create a proxy with the challenge route
|
|
const settings = {
|
|
routes: [
|
|
{
|
|
name: 'secure-route',
|
|
match: {
|
|
ports: [18443],
|
|
domains: 'test.local'
|
|
},
|
|
action: {
|
|
type: 'forward' as const,
|
|
target: { host: 'localhost', port: 8080 }
|
|
}
|
|
},
|
|
challengeRoute
|
|
]
|
|
};
|
|
|
|
const proxy = new SmartProxy(settings);
|
|
|
|
// Mock NFTables manager
|
|
(proxy as any).nftablesManager = {
|
|
ensureNFTablesSetup: async () => {},
|
|
stop: async () => {}
|
|
};
|
|
|
|
// Mock certificate manager to prevent real ACME initialization
|
|
(proxy as any).createCertificateManager = async function() {
|
|
return {
|
|
setUpdateRoutesCallback: () => {},
|
|
setHttpProxy: () => {},
|
|
setGlobalAcmeDefaults: () => {},
|
|
setAcmeStateManager: () => {},
|
|
initialize: async () => {},
|
|
provisionAllCertificates: async () => {},
|
|
stop: async () => {},
|
|
getAcmeOptions: () => ({}),
|
|
getState: () => ({ challengeRouteActive: false })
|
|
};
|
|
};
|
|
|
|
await proxy.start();
|
|
|
|
// Verify the challenge route is in the proxy's routes
|
|
const proxyRoutes = proxy.routeManager.getAllRoutes();
|
|
const foundChallengeRoute = proxyRoutes.find((r: any) => r.name === 'acme-challenge');
|
|
|
|
expect(foundChallengeRoute).toBeDefined();
|
|
expect(foundChallengeRoute?.match.path).toEqual('/.well-known/acme-challenge/*');
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.test('should handle HTTP request parsing correctly', async (tools) => {
|
|
tools.timeout(5000);
|
|
|
|
let handlerCalled = false;
|
|
let receivedContext: any;
|
|
let parsedRequest: any = {};
|
|
|
|
const settings = {
|
|
routes: [
|
|
{
|
|
name: 'test-static',
|
|
match: {
|
|
ports: [18090],
|
|
path: '/test/*'
|
|
},
|
|
action: {
|
|
type: 'socket-handler' as const,
|
|
socketHandler: (socket, context) => {
|
|
handlerCalled = true;
|
|
receivedContext = context;
|
|
|
|
// Parse HTTP request from socket
|
|
socket.once('data', (data) => {
|
|
const request = data.toString();
|
|
const lines = request.split('\r\n');
|
|
const [method, path, protocol] = lines[0].split(' ');
|
|
|
|
// Parse headers
|
|
const headers: any = {};
|
|
for (let i = 1; i < lines.length; i++) {
|
|
if (lines[i] === '') break;
|
|
const [key, value] = lines[i].split(': ');
|
|
if (key && value) {
|
|
headers[key.toLowerCase()] = value;
|
|
}
|
|
}
|
|
|
|
// Store parsed request data
|
|
parsedRequest = { method, path, headers };
|
|
|
|
// Send HTTP response
|
|
const response = [
|
|
'HTTP/1.1 200 OK',
|
|
'Content-Type: text/plain',
|
|
'Content-Length: 2',
|
|
'Connection: close',
|
|
'',
|
|
'OK'
|
|
].join('\r\n');
|
|
|
|
socket.write(response);
|
|
socket.end();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
]
|
|
};
|
|
|
|
const proxy = new SmartProxy(settings);
|
|
|
|
// Mock NFTables manager
|
|
(proxy as any).nftablesManager = {
|
|
ensureNFTablesSetup: async () => {},
|
|
stop: async () => {}
|
|
};
|
|
|
|
await proxy.start();
|
|
|
|
// Create a simple HTTP request
|
|
const client = new plugins.net.Socket();
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
client.connect(18090, 'localhost', () => {
|
|
// Send HTTP request
|
|
const request = [
|
|
'GET /test/example HTTP/1.1',
|
|
'Host: localhost:18090',
|
|
'User-Agent: test-client',
|
|
'',
|
|
''
|
|
].join('\r\n');
|
|
|
|
client.write(request);
|
|
|
|
// Wait for response
|
|
client.on('data', (data) => {
|
|
const response = data.toString();
|
|
expect(response).toContain('HTTP/1.1 200');
|
|
expect(response).toContain('OK');
|
|
client.end();
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
client.on('error', reject);
|
|
});
|
|
|
|
// Verify handler was called
|
|
expect(handlerCalled).toBeTrue();
|
|
expect(receivedContext).toBeDefined();
|
|
|
|
// The context passed to socket handlers is IRouteContext, not HTTP request data
|
|
expect(receivedContext.port).toEqual(18090);
|
|
expect(receivedContext.routeName).toEqual('test-static');
|
|
|
|
// Verify the parsed HTTP request data
|
|
expect(parsedRequest.path).toEqual('/test/example');
|
|
expect(parsedRequest.method).toEqual('GET');
|
|
expect(parsedRequest.headers.host).toEqual('localhost:18090');
|
|
|
|
await proxy.stop();
|
|
});
|
|
|
|
tap.start(); |