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

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

View File

@ -1,5 +1,11 @@
# Changelog # Changelog
## 2025-02-04 - 3.1.3 - fix(networkproxy)
Refactor and improve WebSocket handling and request processing
- Improved error handling in WebSocket connection and request processing.
- Refactored the WebSocket handling in NetworkProxy to use a unified error logging mechanism.
## 2025-02-04 - 3.1.2 - fix(core) ## 2025-02-04 - 3.1.2 - fix(core)
Refactor certificate handling across the project Refactor certificate handling across the project

View File

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

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '3.1.2', version: '3.1.3',
description: 'a proxy for handling high workloads of proxying' description: 'a proxy for handling high workloads of proxying'
} }

View File

@ -8,12 +8,11 @@ export interface INetworkProxyOptions {
port: number; port: number;
} }
interface WebSocketWithHeartbeat extends plugins.wsDefault { interface IWebSocketWithHeartbeat extends plugins.wsDefault {
lastPong: number; lastPong: number;
} }
export class NetworkProxy { export class NetworkProxy {
// INSTANCE
public options: INetworkProxyOptions; public options: INetworkProxyOptions;
public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = []; public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
public httpsServer: plugins.https.Server; public httpsServer: plugins.https.Server;
@ -43,148 +42,23 @@ export class NetworkProxy {
} }
} }
/**
* starts the proxyInstance
*/
public async start() { public async start() {
// Instead of marking the callback async (which Node won't await),
// we call our async handler and catch errors.
this.httpsServer = plugins.https.createServer( this.httpsServer = plugins.https.createServer(
{ {
key: this.defaultCertificates.key, key: this.defaultCertificates.key,
cert: this.defaultCertificates.cert cert: this.defaultCertificates.cert
}, },
async (originRequest, originResponse) => { (originRequest, originResponse) => {
/** this.handleRequest(originRequest, originResponse).catch((error) => {
* endRequest function console.error('Unhandled error in request handler:', error);
* can be used to prematurely end a request try {
*/
const endOriginReqRes = (
statusArg: number = 404,
messageArg: string = 'This route is not available on this server.',
headers: plugins.http.OutgoingHttpHeaders = {},
) => {
originResponse.writeHead(statusArg, messageArg);
originResponse.end(messageArg);
if (originRequest.socket !== originResponse.socket) {
console.log('hey, something is strange.');
}
originResponse.destroy();
};
console.log(
`got request: ${originRequest.headers.host}${plugins.url.parse(originRequest.url).path}`,
);
const destinationConfig = this.router.routeReq(originRequest);
if (!destinationConfig) {
console.log(
`${originRequest.headers.host} can't be routed properly. Terminating request.`,
);
endOriginReqRes();
return;
}
// authentication
if (destinationConfig.authentication) {
const authInfo = destinationConfig.authentication;
switch (authInfo.type) {
case 'Basic':
const authHeader = originRequest.headers.authorization;
if (authHeader) {
if (!authHeader.includes('Basic ')) {
return endOriginReqRes(401, 'Authentication required', {
'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"',
});
}
const authStringBase64 = originRequest.headers.authorization.replace('Basic ', '');
const authString: string = plugins.smartstring.base64.decode(authStringBase64);
const userPassArray = authString.split(':');
const user = userPassArray[0];
const pass = userPassArray[1];
if (user === authInfo.user && pass === authInfo.pass) {
console.log('request successfully authenticated');
} else {
return endOriginReqRes(403, 'Forbidden: Wrong credentials');
}
}
break;
default:
return endOriginReqRes(
403,
'Forbidden: unsupported authentication method configured. Please report to the admin.',
);
}
}
let destinationUrl: string;
if (destinationConfig) {
destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
} else {
return endOriginReqRes();
}
console.log(destinationUrl);
try {
const proxyResponse = await plugins.smartrequest.request(
destinationUrl,
{
method: originRequest.method,
headers: {
...originRequest.headers,
'X-Forwarded-Host': originRequest.headers.host,
'X-Forwarded-Proto': 'https',
},
keepAlive: true,
},
true, // lets make this streaming (keepAlive)
(proxyRequest) => {
originRequest.on('data', (data) => {
proxyRequest.write(data);
});
originRequest.on('end', () => {
proxyRequest.end();
});
originRequest.on('error', () => {
proxyRequest.end();
});
originRequest.on('close', () => {
proxyRequest.end();
});
originRequest.on('timeout', () => {
proxyRequest.end();
originRequest.destroy();
});
proxyRequest.on('error', () => {
endOriginReqRes();
});
},
);
originResponse.statusCode = proxyResponse.statusCode;
console.log(proxyResponse.statusCode);
for (const defaultHeader of Object.keys(this.defaultHeaders)) {
originResponse.setHeader(defaultHeader, this.defaultHeaders[defaultHeader]);
}
for (const header of Object.keys(proxyResponse.headers)) {
originResponse.setHeader(header, proxyResponse.headers[header]);
}
proxyResponse.on('data', (data) => {
originResponse.write(data);
});
proxyResponse.on('end', () => {
originResponse.end(); originResponse.end();
}); } catch (err) {
proxyResponse.on('error', () => { // ignore errors during cleanup
originResponse.destroy(); }
}); });
proxyResponse.on('close', () => {
originResponse.end();
});
proxyResponse.on('timeout', () => {
originResponse.end();
originResponse.destroy();
});
} catch (error) {
console.error('Error while processing request:', error);
endOriginReqRes(502, 'Bad Gateway: Error processing the request');
}
}, },
); );
@ -194,7 +68,7 @@ export class NetworkProxy {
// Set up the heartbeat interval // Set up the heartbeat interval
this.heartbeatInterval = setInterval(() => { this.heartbeatInterval = setInterval(() => {
wsServer.clients.forEach((ws: plugins.wsDefault) => { wsServer.clients.forEach((ws: plugins.wsDefault) => {
const wsIncoming = ws as WebSocketWithHeartbeat; const wsIncoming = ws as IWebSocketWithHeartbeat;
if (!wsIncoming.lastPong) { if (!wsIncoming.lastPong) {
wsIncoming.lastPong = Date.now(); wsIncoming.lastPong = Date.now();
} }
@ -209,7 +83,7 @@ export class NetworkProxy {
wsServer.on( wsServer.on(
'connection', 'connection',
async (wsIncoming: WebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage) => { (wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage) => {
console.log( console.log(
`wss proxy: got connection for wsc for https://${reqArg.headers.host}${reqArg.url}`, `wss proxy: got connection for wsc for https://${reqArg.headers.host}${reqArg.url}`,
); );
@ -220,21 +94,24 @@ export class NetworkProxy {
}); });
let wsOutgoing: plugins.wsDefault; let wsOutgoing: plugins.wsDefault;
const outGoingDeferred = plugins.smartpromise.defer(); const outGoingDeferred = plugins.smartpromise.defer();
// --- Improvement 2: Only call routeReq once ---
const wsDestinationConfig = this.router.routeReq(reqArg);
if (!wsDestinationConfig) {
wsIncoming.terminate();
return;
}
try { try {
wsOutgoing = new plugins.wsDefault( wsOutgoing = new plugins.wsDefault(
`ws://${this.router.routeReq(reqArg).destinationIp}:${ `ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`,
this.router.routeReq(reqArg).destinationPort
}${reqArg.url}`,
); );
console.log('wss proxy: initiated outgoing proxy'); console.log('wss proxy: initiated outgoing proxy');
wsOutgoing.on('open', async () => { wsOutgoing.on('open', async () => {
outGoingDeferred.resolve(); outGoingDeferred.resolve();
}); });
} catch (err) { } catch (err) {
console.log(err); console.error('Error initiating outgoing WebSocket:', err);
wsIncoming.terminate(); wsIncoming.terminate();
return; return;
} }
@ -259,20 +136,20 @@ export class NetworkProxy {
const terminateWsOutgoing = () => { const terminateWsOutgoing = () => {
if (wsOutgoing) { if (wsOutgoing) {
wsOutgoing.terminate(); wsOutgoing.terminate();
console.log('terminated outgoing ws.'); console.log('Terminated outgoing ws.');
} }
}; };
wsIncoming.on('error', () => terminateWsOutgoing()); wsIncoming.on('error', terminateWsOutgoing);
wsIncoming.on('close', () => terminateWsOutgoing()); wsIncoming.on('close', terminateWsOutgoing);
const terminateWsIncoming = () => { const terminateWsIncoming = () => {
if (wsIncoming) { if (wsIncoming) {
wsIncoming.terminate(); wsIncoming.terminate();
console.log('terminated incoming ws.'); console.log('Terminated incoming ws.');
} }
}; };
wsOutgoing.on('error', () => terminateWsIncoming()); wsOutgoing.on('error', terminateWsIncoming);
wsOutgoing.on('close', () => terminateWsIncoming()); wsOutgoing.on('close', terminateWsIncoming);
}, },
); );
@ -281,26 +158,18 @@ export class NetworkProxy {
this.httpsServer.on('connection', (connection: plugins.net.Socket) => { this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
this.socketMap.add(connection); this.socketMap.add(connection);
console.log(`added connection. now ${this.socketMap.getArray().length} sockets connected.`); console.log(`Added connection. Now ${this.socketMap.getArray().length} sockets connected.`);
const cleanupConnection = () => { const cleanupConnection = () => {
if (this.socketMap.checkForObject(connection)) { if (this.socketMap.checkForObject(connection)) {
this.socketMap.remove(connection); this.socketMap.remove(connection);
console.log(`removed connection. ${this.socketMap.getArray().length} sockets remaining.`); console.log(`Removed connection. ${this.socketMap.getArray().length} sockets remaining.`);
connection.destroy(); connection.destroy();
} }
}; };
connection.on('close', () => { connection.on('close', cleanupConnection);
cleanupConnection(); connection.on('error', cleanupConnection);
}); connection.on('end', cleanupConnection);
connection.on('error', () => { connection.on('timeout', cleanupConnection);
cleanupConnection();
});
connection.on('end', () => {
cleanupConnection();
});
connection.on('timeout', () => {
cleanupConnection();
});
}); });
this.httpsServer.listen(this.options.port); this.httpsServer.listen(this.options.port);
@ -309,7 +178,150 @@ export class NetworkProxy {
); );
} }
public async updateProxyConfigs(proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[]) { /**
* Internal async handler for processing HTTP/HTTPS requests.
*/
private async handleRequest(
originRequest: plugins.http.IncomingMessage,
originResponse: plugins.http.ServerResponse,
): Promise<void> {
const endOriginReqRes = (
statusArg: number = 404,
messageArg: string = 'This route is not available on this server.',
headers: plugins.http.OutgoingHttpHeaders = {},
) => {
originResponse.writeHead(statusArg, messageArg);
originResponse.end(messageArg);
if (originRequest.socket !== originResponse.socket) {
console.log('hey, something is strange.');
}
originResponse.destroy();
};
console.log(
`got request: ${originRequest.headers.host}${plugins.url.parse(originRequest.url).path}`,
);
const destinationConfig = this.router.routeReq(originRequest);
if (!destinationConfig) {
console.log(
`${originRequest.headers.host} can't be routed properly. Terminating request.`,
);
endOriginReqRes();
return;
}
// authentication
if (destinationConfig.authentication) {
const authInfo = destinationConfig.authentication;
switch (authInfo.type) {
case 'Basic': {
const authHeader = originRequest.headers.authorization;
if (!authHeader) {
return endOriginReqRes(401, 'Authentication required', {
'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"',
});
}
if (!authHeader.includes('Basic ')) {
return endOriginReqRes(401, 'Authentication required', {
'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"',
});
}
const authStringBase64 = authHeader.replace('Basic ', '');
const authString: string = plugins.smartstring.base64.decode(authStringBase64);
const userPassArray = authString.split(':');
const user = userPassArray[0];
const pass = userPassArray[1];
if (user === authInfo.user && pass === authInfo.pass) {
console.log('Request successfully authenticated');
} else {
return endOriginReqRes(403, 'Forbidden: Wrong credentials');
}
break;
}
default:
return endOriginReqRes(
403,
'Forbidden: unsupported authentication method configured. Please report to the admin.',
);
}
}
let destinationUrl: string;
if (destinationConfig) {
destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
} else {
return endOriginReqRes();
}
console.log(destinationUrl);
try {
const proxyResponse = await plugins.smartrequest.request(
destinationUrl,
{
method: originRequest.method,
headers: {
...originRequest.headers,
'X-Forwarded-Host': originRequest.headers.host,
'X-Forwarded-Proto': 'https',
},
keepAlive: true,
},
true, // streaming (keepAlive)
(proxyRequest) => {
originRequest.on('data', (data) => {
proxyRequest.write(data);
});
originRequest.on('end', () => {
proxyRequest.end();
});
originRequest.on('error', () => {
proxyRequest.end();
});
originRequest.on('close', () => {
proxyRequest.end();
});
originRequest.on('timeout', () => {
proxyRequest.end();
originRequest.destroy();
});
proxyRequest.on('error', () => {
endOriginReqRes();
});
},
);
originResponse.statusCode = proxyResponse.statusCode;
console.log(proxyResponse.statusCode);
for (const defaultHeader of Object.keys(this.defaultHeaders)) {
originResponse.setHeader(defaultHeader, this.defaultHeaders[defaultHeader]);
}
for (const header of Object.keys(proxyResponse.headers)) {
originResponse.setHeader(header, proxyResponse.headers[header]);
}
proxyResponse.on('data', (data) => {
originResponse.write(data);
});
proxyResponse.on('end', () => {
originResponse.end();
});
proxyResponse.on('error', () => {
originResponse.destroy();
});
proxyResponse.on('close', () => {
originResponse.end();
});
proxyResponse.on('timeout', () => {
originResponse.end();
originResponse.destroy();
});
} catch (error) {
console.error('Error while processing request:', error);
endOriginReqRes(502, 'Bad Gateway: Error processing the request');
}
}
public async updateProxyConfigs(
proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[],
) {
console.log(`got new proxy configs`); console.log(`got new proxy configs`);
this.proxyConfigs = proxyConfigsArg; this.proxyConfigs = proxyConfigsArg;
this.router.setNewProxyConfigs(proxyConfigsArg); this.router.setNewProxyConfigs(proxyConfigsArg);
@ -347,11 +359,11 @@ export class NetworkProxy {
this.httpsServer.close(() => { this.httpsServer.close(() => {
done.resolve(); done.resolve();
}); });
await this.socketMap.forEach(async (socket) => { for (const socket of this.socketMap.getArray()) {
socket.destroy(); socket.destroy();
}); }
await done.promise; await done.promise;
clearInterval(this.heartbeatInterval); clearInterval(this.heartbeatInterval);
console.log('NetworkProxy -> OK: Server has been stopped and all connections closed.'); console.log('NetworkProxy -> OK: Server has been stopped and all connections closed.');
} }
} }