navflow-proxy-server
Version:
Dynamic WebSocket proxy server for NavFlow
145 lines (127 loc) • 4.41 kB
JavaScript
/**
* HTTP request proxy handler - forwards requests through WebSocket tunnels
*/
/**
* Handle HTTP request proxying through WebSocket tunnel
* @param {TunnelManager} tunnelManager
* @returns {Function} Express route handler
*/
function createProxyHandler(tunnelManager) {
return async (req, res) => {
const tunnelId = req.tunnel.id;
try {
// Prepare request data to send through tunnel
const requestData = {
type: 'http_request',
method: req.method,
url: req.url,
path: req.path,
query: req.query,
headers: filterHeaders(req.headers),
body: req.body
};
console.log(`[ProxyHandler] Forwarding ${req.method} ${req.url} through tunnel ${tunnelId}`);
// Send request through WebSocket tunnel
const response = await tunnelManager.sendToTunnel(tunnelId, requestData);
if (response.error) {
console.error(`[ProxyHandler] Tunnel error for ${tunnelId}:`, response.error);
return res.status(500).json({
error: 'Tunnel request failed',
details: response.error
});
}
// Set response headers
if (response.headers) {
Object.entries(response.headers).forEach(([key, value]) => {
// Skip headers that shouldn't be forwarded
if (!shouldSkipHeader(key)) {
res.set(key, value);
}
});
}
// Set CORS headers for browser requests
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Tunnel-Auth, X-Tunnel-ID');
// Send response
const statusCode = response.statusCode || 200;
if (response.body !== undefined) {
// Handle different body types
if (typeof response.body === 'string') {
res.status(statusCode).send(response.body);
} else if (Buffer.isBuffer(response.body)) {
res.status(statusCode).send(response.body);
} else {
res.status(statusCode).json(response.body);
}
} else {
res.status(statusCode).end();
}
} catch (error) {
console.error(`[ProxyHandler] Error proxying request for tunnel ${tunnelId}:`, error);
if (error.message === 'Tunnel not available') {
res.status(503).json({
error: 'Local server not connected',
message: 'The local browser server is not connected to this tunnel'
});
} else if (error.message === 'Tunnel request timeout') {
res.status(504).json({
error: 'Request timeout',
message: 'The local server did not respond within the timeout period'
});
} else {
res.status(500).json({
error: 'Proxy error',
message: error.message
});
}
}
};
}
/**
* Filter headers to remove problematic ones
* @param {Object} headers
* @returns {Object} Filtered headers
*/
function filterHeaders(headers) {
const filtered = { ...headers };
// Remove headers that shouldn't be forwarded
delete filtered.host;
delete filtered.connection;
delete filtered['x-forwarded-for'];
delete filtered['x-forwarded-proto'];
delete filtered['x-forwarded-host'];
delete filtered['x-tunnel-auth']; // Remove our auth header
delete filtered['x-tunnel-id']; // Remove our tunnel ID header
return filtered;
}
/**
* Check if header should be skipped in response
* @param {string} headerName
* @returns {boolean}
*/
function shouldSkipHeader(headerName) {
const skipHeaders = [
'connection',
'transfer-encoding',
'content-encoding', // Let browser handle encoding
'content-length' // Will be set automatically
];
return skipHeaders.includes(headerName.toLowerCase());
}
/**
* Handle CORS preflight requests
* @param {Object} req
* @param {Object} res
*/
function handleCorsOptions(req, res) {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH');
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Tunnel-Auth, X-Tunnel-ID');
res.set('Access-Control-Max-Age', '86400'); // 24 hours
res.status(200).end();
}
module.exports = {
createProxyHandler,
handleCorsOptions
};