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); const enabled = proxyConfig.Core.Enabled; const upstreamTimeout = proxyConfig.Timeouts.UpstreamTimeoutMs; const proxyMappings = {}; proxyConfig.Mapping.forEach(mapping => { proxyMappings[mapping.Host] = mapping.Target; }); logs.plugin('proxy', `Proxy mappings loaded: ${JSON.stringify(proxyMappings)}`); // Store for http-proxy-middleware instances const hpmInstances = {}; function createProxyForHost(target) { const proxyOptions = { target, changeOrigin: true, ws: true, logLevel: 'info', timeout: upstreamTimeout, 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(); } }, followRedirects: false, preserveHeaderKeyCase: true, autoRewrite: true, protocolRewrite: 'http', cookieDomainRewrite: { "*": "" } }; return createProxyMiddleware(proxyOptions); } function proxyMiddleware() { const router = express.Router(); router.use('/api/challenge', (req, res, next) => next('route')); router.use('/api/verify', (req, res, next) => next('route')); router.use('/webfont/', (req, res, next) => next('route')); router.use('/js/', (req, res, next) => next('route')); Object.entries(proxyMappings).forEach(([host, target]) => { hpmInstances[host] = createProxyForHost(target); }); router.use((req, res, next) => { const hostname = req.hostname || req.headers.host?.split(':')[0]; const proxyInstance = hpmInstances[hostname]; if (proxyInstance) { proxyInstance(req, res, next); } else { next(); } }); return { middleware: router }; } export function getHpmInstance(hostname) { return hpmInstances[hostname]; } if (enabled) { registerPlugin('proxy', proxyMiddleware()); } else { logs.plugin('proxy', 'Proxy plugin disabled via config'); }