Checkpoint/plugins/proxy.js

116 lines
3.9 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';
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');
}