Websocket Fixes & New Config Examples
This commit is contained in:
parent
84225a66f9
commit
9bcdc532bb
10 changed files with 389 additions and 96 deletions
123
plugins/proxy.js
123
plugins/proxy.js
|
|
@ -2,16 +2,41 @@ import { registerPlugin, loadConfig } from '../index.js';
|
|||
import * as logs from '../utils/logs.js';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
import express from 'express';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { createRequire } from 'module';
|
||||
|
||||
// Setup require for ESM modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
// Monkey patch the ws module to prevent "write after end" errors
|
||||
// Based on https://stackoverflow.com/questions/27769842/write-after-end-error-in-node-js-webserver/33591429
|
||||
try {
|
||||
const ws = require('ws');
|
||||
const originalClose = ws.Sender.prototype.close;
|
||||
|
||||
// Override the close method to check if the socket is already closed
|
||||
ws.Sender.prototype.close = function(code, data, mask, cb) {
|
||||
if (this._socket && (this._socket.destroyed || !this._socket.writable)) {
|
||||
logs.plugin('proxy', 'WebSocket close called on already closed socket - ignoring');
|
||||
if (typeof cb === 'function') cb();
|
||||
return;
|
||||
}
|
||||
return originalClose.call(this, code, data, mask, cb);
|
||||
};
|
||||
logs.plugin('proxy', 'Monkey patched ws module to prevent write after end errors');
|
||||
} catch (err) {
|
||||
logs.error('proxy', `Failed to monkey patch ws module: ${err.message}`);
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -19,94 +44,56 @@ proxyConfig.Mapping.forEach(mapping => {
|
|||
|
||||
logs.plugin('proxy', `Proxy mappings loaded: ${JSON.stringify(proxyMappings)}`);
|
||||
|
||||
// Store for http-proxy-middleware instances
|
||||
const hpmInstances = {};
|
||||
|
||||
function createProxyForHost(target) {
|
||||
return createProxyMiddleware({
|
||||
const proxyOptions = {
|
||||
target,
|
||||
changeOrigin: true,
|
||||
ws: true, // Enable WebSocket support
|
||||
ws: true,
|
||||
logLevel: 'info',
|
||||
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');
|
||||
onError: (err, req, res, _target) => {
|
||||
const targetInfo = _target && _target.href ? _target.href : (typeof _target === 'string' ? _target : 'N/A');
|
||||
logs.error('proxy', `[HPM onError] Proxy error for ${req.method} ${req.url} to ${targetInfo}: ${err.message} (Code: ${err.code || 'N/A'})`);
|
||||
if (res && typeof res.writeHead === 'function') {
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
||||
res.end('Bad Gateway');
|
||||
} else if (typeof res.destroy === 'function' && !res.destroyed) {
|
||||
res.destroy();
|
||||
}
|
||||
} else if (res && typeof res.end === 'function' && res.writable && !res.destroyed) {
|
||||
logs.plugin('proxy', `[HPM onError] Client WebSocket socket for ${req.url} attempting to end due to proxy error: ${err.message}.`);
|
||||
res.end();
|
||||
}
|
||||
},
|
||||
// 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
|
||||
}
|
||||
});
|
||||
cookieDomainRewrite: { "*": "" }
|
||||
};
|
||||
|
||||
return createProxyMiddleware(proxyOptions);
|
||||
}
|
||||
|
||||
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);
|
||||
hpmInstances[host] = createProxyForHost(target);
|
||||
});
|
||||
|
||||
// Main proxy handler
|
||||
router.use((req, res, next) => {
|
||||
const hostname = req.hostname || req.headers.host?.split(':')[0];
|
||||
const proxyInstance = proxyInstances[hostname];
|
||||
const proxyInstance = hpmInstances[hostname];
|
||||
|
||||
if (proxyInstance) {
|
||||
proxyInstance(req, res, next);
|
||||
|
|
@ -118,6 +105,10 @@ function proxyMiddleware() {
|
|||
return { middleware: router };
|
||||
}
|
||||
|
||||
export function getHpmInstance(hostname) {
|
||||
return hpmInstances[hostname];
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
registerPlugin('proxy', proxyMiddleware());
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue