fix(networkproxy): Refactor and improve WebSocket handling and request processing

This commit is contained in:
2025-02-04 01:10:58 +01:00
parent 3e66debb01
commit 49b65508a5
4 changed files with 275 additions and 214 deletions

View File

@ -14,19 +14,34 @@ let testCertificates: { privateKey: string; publicKey: string };
async function makeHttpsRequest(
options: https.RequestOptions,
): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders; body: string }> {
console.log('[TEST] Making HTTPS request:', {
hostname: options.hostname,
port: options.port,
path: options.path,
method: options.method,
headers: options.headers,
});
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
console.log('[TEST] Received HTTPS response:', {
statusCode: res.statusCode,
headers: res.headers,
});
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () =>
res.on('end', () => {
console.log('[TEST] Response completed:', { data });
resolve({
statusCode: res.statusCode!,
headers: res.headers,
body: data,
}),
);
});
});
});
req.on('error', (error) => {
console.error('[TEST] Request error:', error);
reject(error);
});
req.on('error', reject);
req.end();
});
}
@ -37,12 +52,13 @@ tap.test('setup test environment', async () => {
console.log('[TEST] Loading and validating certificates');
testCertificates = loadTestCertificates();
console.log('[TEST] Certificates loaded and validated');
// Create a test HTTP server
testServer = http.createServer((req, res) => {
console.log('[TEST SERVER] Received HTTP request:', {
url: req.url,
method: req.method,
headers: req.headers
headers: req.headers,
});
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from test server!');
@ -59,8 +75,8 @@ tap.test('setup test environment', async () => {
connection: request.headers.connection,
'sec-websocket-key': request.headers['sec-websocket-key'],
'sec-websocket-version': request.headers['sec-websocket-version'],
'sec-websocket-protocol': request.headers['sec-websocket-protocol']
}
'sec-websocket-protocol': request.headers['sec-websocket-protocol'],
},
});
if (request.headers.upgrade?.toLowerCase() !== 'websocket') {
@ -76,13 +92,13 @@ tap.test('setup test environment', async () => {
});
});
// Create a WebSocket server
// Create a WebSocket server (for the test HTTP server)
console.log('[TEST SERVER] Creating WebSocket server');
wsServer = new WebSocketServer({
noServer: true,
perMessageDeflate: false,
clientTracking: true,
handleProtocols: () => 'echo-protocol'
handleProtocols: () => 'echo-protocol',
});
wsServer.on('connection', (ws, request) => {
@ -94,8 +110,8 @@ tap.test('setup test environment', async () => {
connection: request.headers.connection,
'sec-websocket-key': request.headers['sec-websocket-key'],
'sec-websocket-version': request.headers['sec-websocket-version'],
'sec-websocket-protocol': request.headers['sec-websocket-protocol']
}
'sec-websocket-protocol': request.headers['sec-websocket-protocol'],
},
});
// Set up connection timeout
@ -132,7 +148,7 @@ tap.test('setup test environment', async () => {
console.log('[TEST SERVER] WebSocket connection closed:', {
code,
reason: reason.toString(),
wasClean: code === 1000 || code === 1001
wasClean: code === 1000 || code === 1001,
});
clearConnectionTimeout();
});
@ -175,30 +191,42 @@ tap.test('should create proxy instance', async () => {
});
tap.test('should start the proxy server', async () => {
// Ensure any previous server is closed
if (testProxy && testProxy.httpsServer) {
await new Promise<void>((resolve) =>
testProxy.httpsServer.close(() => resolve())
);
}
console.log('[TEST] Starting the proxy server');
await testProxy.start();
console.log('[TEST] Proxy server started');
// Configure proxy with test certificates
testProxy.updateProxyConfigs([
// Awaiting the update ensures that the SNI context is added before any requests come in.
await testProxy.updateProxyConfigs([
{
destinationIp: '127.0.0.1',
destinationPort: '3000',
hostName: 'push.rocks',
publicKey: testCertificates.publicKey,
privateKey: testCertificates.privateKey,
}
},
]);
console.log('[TEST] Proxy configuration updated');
});
tap.test('should route HTTPS requests based on host header', async () => {
// IMPORTANT: Connect to localhost (where the proxy is listening) but use the Host header "push.rocks"
const response = await makeHttpsRequest({
hostname: 'push.rocks',
hostname: 'localhost', // changed from 'push.rocks' to 'localhost'
port: 3001,
path: '/',
method: 'GET',
headers: {
host: 'push.rocks', // virtual host for routing
},
rejectUnauthorized: false,
});
@ -207,15 +235,21 @@ tap.test('should route HTTPS requests based on host header', async () => {
});
tap.test('should handle unknown host headers', async () => {
// Connect to localhost but use an unknown host header.
const response = await makeHttpsRequest({
hostname: 'unknown.host',
hostname: 'localhost', // connecting to localhost
port: 3001,
path: '/',
method: 'GET',
headers: {
host: 'unknown.host', // this should not match any proxy config
},
rejectUnauthorized: false,
}).catch((e) => e);
});
expect(response instanceof Error).toEqual(true);
// Expect a 404 response with the appropriate error message.
expect(response.statusCode).toEqual(404);
expect(response.body).toEqual('This route is not available on this server.');
});
tap.test('should support WebSocket connections', async () => {
@ -224,39 +258,38 @@ tap.test('should support WebSocket connections', async () => {
console.log('[TEST] Proxy server port:', 3001);
console.log('\n[TEST] Starting WebSocket test');
// First configure the proxy with test certificates
console.log('[TEST] Configuring proxy with test certificates');
testProxy.updateProxyConfigs([
// Reconfigure proxy with test certificates if necessary
await testProxy.updateProxyConfigs([
{
destinationIp: '127.0.0.1',
destinationPort: '3000',
hostName: 'push.rocks',
publicKey: testCertificates.publicKey,
privateKey: testCertificates.privateKey,
}
},
]);
return new Promise<void>((resolve, reject) => {
console.log('[TEST] Creating WebSocket client');
// Create WebSocket client with SSL/TLS options
const wsUrl = 'wss://push.rocks:3001';
// IMPORTANT: Connect to localhost but specify the SNI servername and Host header as "push.rocks"
const wsUrl = 'wss://localhost:3001'; // changed from 'wss://push.rocks:3001'
console.log('[TEST] Creating WebSocket connection to:', wsUrl);
const ws = new WebSocket(wsUrl, {
rejectUnauthorized: false, // Accept self-signed certificates
handshakeTimeout: 5000,
perMessageDeflate: false,
headers: {
'Host': 'push.rocks',
'Connection': 'Upgrade',
'Upgrade': 'websocket',
'Sec-WebSocket-Version': '13'
Host: 'push.rocks', // required for SNI and routing on the proxy
Connection: 'Upgrade',
Upgrade: 'websocket',
'Sec-WebSocket-Version': '13',
},
protocol: 'echo-protocol',
agent: new https.Agent({
rejectUnauthorized: false // Also needed for the underlying HTTPS connection
})
rejectUnauthorized: false, // Also needed for the underlying HTTPS connection
}),
});
console.log('[TEST] WebSocket client created');
@ -286,7 +319,7 @@ tap.test('should support WebSocket connections', async () => {
ws.on('upgrade', (response) => {
console.log('[TEST] WebSocket upgrade response received:', {
headers: response.headers,
statusCode: response.statusCode
statusCode: response.statusCode,
});
});
@ -304,7 +337,10 @@ tap.test('should support WebSocket connections', async () => {
ws.on('message', (message) => {
console.log('[TEST] Received message:', message.toString());
if (message.toString() === 'Hello WebSocket') {
if (
message.toString() === 'Hello WebSocket' ||
message.toString() === 'Echo: Hello WebSocket'
) {
console.log('[TEST] Message received correctly');
clearTimeout(timeout);
cleanup();
@ -320,7 +356,7 @@ tap.test('should support WebSocket connections', async () => {
ws.on('close', (code, reason) => {
console.log('[TEST] WebSocket connection closed:', {
code,
reason: reason.toString()
reason: reason.toString(),
});
cleanup();
});
@ -328,15 +364,18 @@ tap.test('should support WebSocket connections', async () => {
});
tap.test('should handle custom headers', async () => {
testProxy.addDefaultHeaders({
await testProxy.addDefaultHeaders({
'X-Proxy-Header': 'test-value',
});
const response = await makeHttpsRequest({
hostname: 'push.rocks',
hostname: 'localhost', // changed to 'localhost'
port: 3001,
path: '/',
method: 'GET',
headers: {
host: 'push.rocks', // still routing to push.rocks
},
rejectUnauthorized: false,
});
@ -353,16 +392,20 @@ tap.test('cleanup', async () => {
});
console.log('[TEST] Closing WebSocket server');
await new Promise<void>((resolve) => wsServer.close(() => {
console.log('[TEST] WebSocket server closed');
resolve();
}));
await new Promise<void>((resolve) =>
wsServer.close(() => {
console.log('[TEST] WebSocket server closed');
resolve();
})
);
console.log('[TEST] Closing test server');
await new Promise<void>((resolve) => testServer.close(() => {
console.log('[TEST] Test server closed');
resolve();
}));
await new Promise<void>((resolve) =>
testServer.close(() => {
console.log('[TEST] Test server closed');
resolve();
})
);
console.log('[TEST] Stopping proxy');
await testProxy.stop();
@ -376,4 +419,4 @@ process.on('exit', () => {
testProxy.stop().then(() => console.log('[TEST] Proxy server stopped'));
});
tap.start();
tap.start();