Checkpoint/plugins/proxy.js

135 lines
4.3 KiB
JavaScript

import { registerPlugin, loadConfig } from '../index.js';
import * as logs from '../utils/logs.js';
import { createProxyMiddleware } from 'http-proxy-middleware';
import express from 'express';
const proxyConfig = {};
await loadConfig('proxy', proxyConfig);
// Map configuration to internal structure
const enabled = proxyConfig.Core.Enabled;
const wsTimeout = proxyConfig.Timeouts.WebSocketTimeoutMs;
const upstreamTimeout = proxyConfig.Timeouts.UpstreamTimeoutMs;
// Build proxy mappings from array format
const proxyMappings = {};
proxyConfig.Mapping.forEach(mapping => {
proxyMappings[mapping.Host] = mapping.Target;
});
logs.plugin('proxy', `Proxy mappings loaded: ${JSON.stringify(proxyMappings)}`);
function createProxyForHost(target) {
return createProxyMiddleware({
target,
changeOrigin: true,
ws: true, // Enable WebSocket support
timeout: upstreamTimeout,
proxyTimeout: upstreamTimeout,
onProxyReq: (proxyReq, req, res) => {
// Remove undefined headers
const headersToRemove = ['x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-for'];
headersToRemove.forEach(header => {
proxyReq.removeHeader(header);
});
// Set proper forwarded headers
const forwarded = {
for: req.ip || req.connection.remoteAddress,
host: req.get('host'),
proto: req.protocol
};
proxyReq.setHeader('X-Forwarded-For', forwarded.for);
proxyReq.setHeader('X-Forwarded-Host', forwarded.host);
proxyReq.setHeader('X-Forwarded-Proto', forwarded.proto);
// Log the proxied request
const startTime = Date.now();
res.on('finish', () => {
const latency = Date.now() - startTime;
logs.plugin('proxy', `Proxied request to: ${target}${req.url} (${res.statusCode}) (${latency}ms)`);
});
},
onProxyReqWs: (proxyReq, req, socket, options, head) => {
// Set WebSocket timeout
socket.setTimeout(wsTimeout);
logs.plugin('proxy', `WebSocket proxied to: ${target}${req.url}`);
},
onError: (err, req, res) => {
logs.error('proxy', `Proxy error: ${err.message}`);
if (!res.headersSent) {
res.status(502).send('Bad Gateway');
}
},
// Handle SSE and streaming responses properly
onProxyRes: (proxyRes, req, res) => {
// For SSE responses, ensure proper headers
const contentType = proxyRes.headers['content-type'];
if (contentType && contentType.includes('text/event-stream')) {
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('X-Accel-Buffering', 'no');
// Remove compression for SSE
delete proxyRes.headers['content-encoding'];
// Force connection keep-alive
res.setHeader('Connection', 'keep-alive');
}
},
// Advanced options for better compatibility
followRedirects: false,
preserveHeaderKeyCase: true,
autoRewrite: true,
protocolRewrite: 'http',
cookieDomainRewrite: {
"*": "" // Remove domain restrictions from cookies
}
});
}
function proxyMiddleware() {
const router = express.Router();
// Skip checkpoint endpoints
router.use('/api/challenge', (req, res, next) => next('route'));
router.use('/api/verify', (req, res, next) => next('route'));
// Skip static assets (already handled by static middleware)
router.use('/webfont/', (req, res, next) => next('route'));
router.use('/js/', (req, res, next) => next('route'));
// Create a proxy instance for each host
const proxyInstances = {};
Object.entries(proxyMappings).forEach(([host, target]) => {
proxyInstances[host] = createProxyForHost(target);
});
// Main proxy handler
router.use((req, res, next) => {
const hostname = req.hostname || req.headers.host?.split(':')[0];
const proxyInstance = proxyInstances[hostname];
if (proxyInstance) {
proxyInstance(req, res, next);
} else {
next();
}
});
return { middleware: router };
}
// Export WebSocket handler for compatibility
export const proxyWebSocketHandler = {
// http-proxy-middleware handles WebSocket internally
// These are kept for compatibility but won't be used
open: () => {},
message: () => {},
close: () => {},
error: () => {}
};
if (enabled) {
registerPlugin('proxy', proxyMiddleware());
} else {
logs.plugin('proxy', 'Proxy plugin disabled via config');
}