Websocket Fixes & New Config Examples
This commit is contained in:
parent
84225a66f9
commit
9bcdc532bb
10 changed files with 389 additions and 96 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -37,4 +37,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
data
|
data
|
||||||
|
|
||||||
# DB Folder
|
# DB Folder
|
||||||
db
|
db
|
||||||
|
|
||||||
|
# My Configs
|
||||||
|
*.toml
|
||||||
|
|
@ -124,8 +124,8 @@ async function initConfig() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extension handling
|
// Extension handling
|
||||||
checkpointConfig.HTMLCheckpointIncludedExtensions = checkpointConfig.Extensions.IncludeOnly;
|
checkpointConfig.HTMLCheckpointIncludedExtensions = checkpointConfig.Extensions?.IncludeOnly || [];
|
||||||
checkpointConfig.HTMLCheckpointExcludedExtensions = checkpointConfig.Extensions.Exclude;
|
checkpointConfig.HTMLCheckpointExcludedExtensions = checkpointConfig.Extensions?.Exclude || [];
|
||||||
|
|
||||||
// Remove legacy arrays
|
// Remove legacy arrays
|
||||||
checkpointConfig.HTMLCheckpointExclusions = [];
|
checkpointConfig.HTMLCheckpointExclusions = [];
|
||||||
|
|
|
||||||
|
|
@ -90,22 +90,9 @@ Path = "/api"
|
||||||
Hosts = ["gallery.caileb.com"] # Optional: only for specific hosts
|
Hosts = ["gallery.caileb.com"] # Optional: only for specific hosts
|
||||||
|
|
||||||
[[Exclusion]]
|
[[Exclusion]]
|
||||||
# Skip checkpoint for health checks
|
# Allows Git pushes w/ ForgeJo
|
||||||
Path = "/health"
|
Path = "/info/refs"
|
||||||
|
Hosts = ["git.caileb.com"]
|
||||||
[[Exclusion]]
|
|
||||||
# Skip checkpoint for metrics endpoint
|
|
||||||
Path = "/metrics"
|
|
||||||
|
|
||||||
# [[Exclusion]]
|
|
||||||
# Example: Mobile app API with specific user agent
|
|
||||||
# Path = "/mobile-api"
|
|
||||||
# UserAgents = ["MyApp/", "Dart/"]
|
|
||||||
|
|
||||||
# [[Exclusion]]
|
|
||||||
# Example: Host-specific exclusion
|
|
||||||
# Path = "/admin"
|
|
||||||
# Hosts = ["admin.internal.com"]
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# BYPASS KEYS
|
# BYPASS KEYS
|
||||||
|
|
|
||||||
123
config/checkpoint.toml.example
Normal file
123
config/checkpoint.toml.example
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
# =============================================================================
|
||||||
|
# CHECKPOINT SECURITY CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# This configuration controls the checkpoint security middleware that protects
|
||||||
|
# your services with proof-of-work challenges and token-based authentication.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CORE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Core]
|
||||||
|
# Enable or disable the checkpoint system entirely
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# Cookie name for storing checkpoint tokens
|
||||||
|
CookieName = "checkpoint_token"
|
||||||
|
|
||||||
|
# Cookie domain (empty = host-only cookie for localhost)
|
||||||
|
# Set to ".yourdomain.com" for all subdomains
|
||||||
|
CookieDomain = ""
|
||||||
|
|
||||||
|
# Enable URL path sanitization to prevent path traversal attacks
|
||||||
|
SanitizeURLs = true
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PROOF OF WORK SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[ProofOfWork]
|
||||||
|
# Number of leading zeros required in the SHA-256 hash
|
||||||
|
Difficulty = 4
|
||||||
|
|
||||||
|
# Random salt length in bytes
|
||||||
|
SaltLength = 16
|
||||||
|
|
||||||
|
# Time allowed to solve a challenge before it expires
|
||||||
|
ChallengeExpiration = "3m"
|
||||||
|
|
||||||
|
# Maximum attempts per IP address per hour
|
||||||
|
MaxAttemptsPerHour = 10
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PROOF OF SPACE-TIME SETTINGS (Optional additional verification)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[ProofOfSpaceTime]
|
||||||
|
# Enable consistency checks for PoS-Time verification
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# Maximum allowed ratio between slowest and fastest PoS runs
|
||||||
|
ConsistencyRatio = 1.35
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# TOKEN SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Token]
|
||||||
|
# How long tokens remain valid
|
||||||
|
Expiration = "24h"
|
||||||
|
|
||||||
|
# Maximum age for used nonces before cleanup
|
||||||
|
MaxNonceAge = "24h"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# STORAGE PATHS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Storage]
|
||||||
|
# HMAC secret storage location
|
||||||
|
SecretPath = "./data/checkpoint_secret.json"
|
||||||
|
|
||||||
|
# Token database directory
|
||||||
|
TokenDBPath = "./db/tokenstore"
|
||||||
|
|
||||||
|
# Interstitial page templates (in order of preference)
|
||||||
|
InterstitialTemplates = [
|
||||||
|
"/pages/interstitial/page.html",
|
||||||
|
"/pages/ipfilter/default.html"
|
||||||
|
]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# EXCLUSION RULES
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Define which requests should bypass the checkpoint system.
|
||||||
|
# Each rule can specify:
|
||||||
|
# - Path (required): URL path or prefix to match
|
||||||
|
# - Hosts (optional): Specific hostnames this rule applies to
|
||||||
|
# - UserAgents (optional): User-Agent patterns to match
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[Exclusion]]
|
||||||
|
# Skip checkpoint for all API endpoints
|
||||||
|
Path = "/api"
|
||||||
|
Hosts = ["api.example.com"] # Optional: only for specific hosts
|
||||||
|
|
||||||
|
[[Exclusion]]
|
||||||
|
# Allows Git operations
|
||||||
|
Path = "/info/refs"
|
||||||
|
Hosts = ["git.example.com"]
|
||||||
|
|
||||||
|
[[Exclusion]]
|
||||||
|
# Skip checkpoint for metrics endpoint
|
||||||
|
Path = "/metrics"
|
||||||
|
|
||||||
|
# [[Exclusion]]
|
||||||
|
# Example: Mobile app API with specific user agent
|
||||||
|
# Path = "/mobile-api"
|
||||||
|
# UserAgents = ["MyApp/", "Dart/"]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# BYPASS KEYS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Special keys that can bypass the checkpoint when provided
|
||||||
|
|
||||||
|
[[BypassKeys]]
|
||||||
|
# Query parameter bypass
|
||||||
|
Type = "query"
|
||||||
|
Key = "bypass_key"
|
||||||
|
Value = "your-secret-key-here"
|
||||||
|
Hosts = ["music.example.com"] # Optional: restrict to specific hosts
|
||||||
|
|
||||||
|
[[BypassKeys]]
|
||||||
|
# Header bypass
|
||||||
|
Type = "header"
|
||||||
|
Key = "X-Bypass-Token"
|
||||||
|
Value = "another-secret-key"
|
||||||
|
# Hosts = [] # If empty or omitted, applies to all hosts
|
||||||
89
config/ipfilter.toml.example
Normal file
89
config/ipfilter.toml.example
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
# =============================================================================
|
||||||
|
# IP FILTER CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# This configuration controls the IP filtering middleware that blocks requests
|
||||||
|
# based on geographic location (country/continent) and network (ASN) information.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CORE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Core]
|
||||||
|
# Enable or disable the IP filter entirely
|
||||||
|
Enabled = false
|
||||||
|
|
||||||
|
# MaxMind account ID for downloading GeoIP databases
|
||||||
|
# Can also be set via MAXMIND_ACCOUNT_ID environment variable or .env file
|
||||||
|
AccountID = ""
|
||||||
|
|
||||||
|
# MaxMind license key for downloading GeoIP databases
|
||||||
|
# Can also be set via MAXMIND_LICENSE_KEY environment variable or .env file
|
||||||
|
LicenseKey = ""
|
||||||
|
|
||||||
|
# How often to check for database updates (in hours)
|
||||||
|
DBUpdateIntervalHours = 12
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CACHING SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Cache]
|
||||||
|
# TTL for cached IP block decisions (in seconds)
|
||||||
|
# 0 = cache indefinitely until server restart
|
||||||
|
IPBlockCacheTTLSec = 300
|
||||||
|
|
||||||
|
# Maximum number of cached IP decisions
|
||||||
|
# 0 = unlimited
|
||||||
|
IPBlockCacheMaxEntries = 10000
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# BLOCKING RULES
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Blocking]
|
||||||
|
# ISO country codes to block (2-letter codes)
|
||||||
|
CountryCodes = [
|
||||||
|
"XX", "YY", "ZZ" # Replace with actual country codes
|
||||||
|
]
|
||||||
|
|
||||||
|
# Continent codes to block
|
||||||
|
ContinentCodes = [] # Example: ["AF", "AS"]
|
||||||
|
|
||||||
|
# Default block page when no specific page is configured
|
||||||
|
DefaultBlockPage = "/pages/ipfilter/default.html"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ASN BLOCKING
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Block by Autonomous System Number (ASN)
|
||||||
|
# Group ASNs by category for different block pages
|
||||||
|
|
||||||
|
# [ASN.Example]
|
||||||
|
# Numbers = [12345, 67890]
|
||||||
|
# BlockPage = "pages/ipfilter/example.html"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ASN NAME BLOCKING
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Block by ASN organization name patterns
|
||||||
|
|
||||||
|
[ASNNames.DataCenter]
|
||||||
|
# Block data center and cloud providers
|
||||||
|
Patterns = [
|
||||||
|
"Cloudflare", "GOOGLE-CLOUD-PLATFORM", "Microsoft", "Amazon", "AWS",
|
||||||
|
"Digitalocean", "OVH", "HUAWEI CLOUDS"
|
||||||
|
]
|
||||||
|
BlockPage = "/pages/ipfilter/datacenter.html"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# COUNTRY-SPECIFIC BLOCK PAGES
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[CountryBlockPages]
|
||||||
|
# Custom block pages for specific countries
|
||||||
|
XX = "/pages/ipfilter/country-xx.html"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CONTINENT-SPECIFIC BLOCK PAGES
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[ContinentBlockPages]
|
||||||
|
# Custom block pages for specific continents
|
||||||
|
# AS = "pages/ipfilter/asia.html"
|
||||||
|
# AF = "pages/ipfilter/africa.html"
|
||||||
|
|
@ -17,7 +17,7 @@ Enabled = true
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
[Timeouts]
|
[Timeouts]
|
||||||
# WebSocket connection timeout in milliseconds
|
# WebSocket connection timeout in milliseconds
|
||||||
WebSocketTimeoutMs = 5000
|
WebSocketTimeoutMs = 60000
|
||||||
|
|
||||||
# Upstream HTTP request timeout in milliseconds
|
# Upstream HTTP request timeout in milliseconds
|
||||||
UpstreamTimeoutMs = 30000
|
UpstreamTimeoutMs = 30000
|
||||||
|
|
|
||||||
55
config/proxy.toml.example
Normal file
55
config/proxy.toml.example
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# =============================================================================
|
||||||
|
# PROXY CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# This configuration controls the reverse proxy middleware that forwards
|
||||||
|
# requests to backend services based on hostname mappings.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CORE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Core]
|
||||||
|
# Enable or disable the proxy middleware
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# TIMEOUT SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Timeouts]
|
||||||
|
# WebSocket connection timeout in milliseconds
|
||||||
|
WebSocketTimeoutMs = 5000
|
||||||
|
|
||||||
|
# Upstream HTTP request timeout in milliseconds
|
||||||
|
UpstreamTimeoutMs = 30000
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PROXY MAPPINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Map hostnames to backend service URLs
|
||||||
|
# Format: "hostname" = "backend_url"
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[Mapping]]
|
||||||
|
# Media server
|
||||||
|
Host = "media.example.com"
|
||||||
|
Target = "http://192.168.1.100:8096"
|
||||||
|
|
||||||
|
[[Mapping]]
|
||||||
|
# Music streaming service
|
||||||
|
Host = "music.example.com"
|
||||||
|
Target = "http://192.168.1.100:4533"
|
||||||
|
|
||||||
|
[[Mapping]]
|
||||||
|
# Git repository
|
||||||
|
Host = "git.example.com"
|
||||||
|
Target = "http://192.168.1.100:3000"
|
||||||
|
|
||||||
|
# [[Mapping]]
|
||||||
|
# API service
|
||||||
|
# Host = "api.example.com"
|
||||||
|
# Target = "http://localhost:3001"
|
||||||
|
|
||||||
|
# [[Mapping]]
|
||||||
|
# Admin panel
|
||||||
|
# Host = "admin.example.com"
|
||||||
|
# Target = "http://localhost:3002"
|
||||||
31
config/stats.toml.example
Normal file
31
config/stats.toml.example
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# =============================================================================
|
||||||
|
# STATS CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# This configuration controls the statistics collection and visualization
|
||||||
|
# middleware that tracks events and provides a web UI for viewing metrics.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CORE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Core]
|
||||||
|
# Enable or disable the stats plugin
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# STORAGE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Storage]
|
||||||
|
# TTL for stats entries
|
||||||
|
# Format: "30d", "24h", "1h", etc.
|
||||||
|
StatsTTL = "30d"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# WEB UI SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[WebUI]
|
||||||
|
# Path for stats UI
|
||||||
|
StatsUIPath = "/stats"
|
||||||
|
|
||||||
|
# Path for stats API
|
||||||
|
StatsAPIPath = "/stats/api"
|
||||||
34
index.js
34
index.js
|
|
@ -6,7 +6,6 @@ import { secureImportModule } from './utils/plugins.js';
|
||||||
import * as logs from './utils/logs.js';
|
import * as logs from './utils/logs.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { createServer } from 'http';
|
import { createServer } from 'http';
|
||||||
import { WebSocketServer } from 'ws';
|
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
// Load environment variables from .env file
|
// Load environment variables from .env file
|
||||||
|
|
@ -135,9 +134,6 @@ async function main() {
|
||||||
// Trust proxy headers (important for proper protocol detection)
|
// Trust proxy headers (important for proper protocol detection)
|
||||||
app.set('trust proxy', true);
|
app.set('trust proxy', true);
|
||||||
|
|
||||||
// Initialize WebSocket server
|
|
||||||
const wss = new WebSocketServer({ noServer: true });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await secureImportModule('checkpoint.js');
|
await secureImportModule('checkpoint.js');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -203,12 +199,30 @@ async function main() {
|
||||||
res.status(500).send(`Server Error: ${err.message}`);
|
res.status(500).send(`Server Error: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle WebSocket upgrades for http-proxy-middleware
|
// Handle WebSocket upgrades using http-proxy-middleware instances
|
||||||
server.on('upgrade', (request, socket, head) => {
|
server.on('upgrade', (req, socket, head) => {
|
||||||
// http-proxy-middleware handles WebSocket upgrades internally
|
const hostname = req.headers.host?.split(':')[0];
|
||||||
// This is just a placeholder for any custom WebSocket handling if needed
|
if (!hostname) {
|
||||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
logs.error('websocket', 'Upgrade request without host header, destroying socket.');
|
||||||
wss.emit('connection', ws, request);
|
socket.destroy();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logs.server(`WebSocket upgrade request for ${hostname}${req.url}`);
|
||||||
|
|
||||||
|
import('./plugins/proxy.js').then(proxyModule => {
|
||||||
|
const hpmInstance = proxyModule.getHpmInstance(hostname);
|
||||||
|
|
||||||
|
if (hpmInstance && typeof hpmInstance.upgrade === 'function') {
|
||||||
|
logs.server(`Attempting to upgrade WebSocket for ${hostname} using HPM instance.`);
|
||||||
|
hpmInstance.upgrade(req, socket, head);
|
||||||
|
} else {
|
||||||
|
logs.error('websocket', `No HPM instance or upgrade method found for ${hostname}${req.url}`);
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
logs.error('websocket', `Error importing proxy module for upgrade: ${err.message}`);
|
||||||
|
socket.destroy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
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 * as logs from '../utils/logs.js';
|
||||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
import express from 'express';
|
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 = {};
|
const proxyConfig = {};
|
||||||
await loadConfig('proxy', proxyConfig);
|
await loadConfig('proxy', proxyConfig);
|
||||||
|
|
||||||
// Map configuration to internal structure
|
|
||||||
const enabled = proxyConfig.Core.Enabled;
|
const enabled = proxyConfig.Core.Enabled;
|
||||||
const wsTimeout = proxyConfig.Timeouts.WebSocketTimeoutMs;
|
|
||||||
const upstreamTimeout = proxyConfig.Timeouts.UpstreamTimeoutMs;
|
const upstreamTimeout = proxyConfig.Timeouts.UpstreamTimeoutMs;
|
||||||
|
|
||||||
// Build proxy mappings from array format
|
|
||||||
const proxyMappings = {};
|
const proxyMappings = {};
|
||||||
proxyConfig.Mapping.forEach(mapping => {
|
proxyConfig.Mapping.forEach(mapping => {
|
||||||
proxyMappings[mapping.Host] = mapping.Target;
|
proxyMappings[mapping.Host] = mapping.Target;
|
||||||
|
|
@ -19,94 +44,56 @@ proxyConfig.Mapping.forEach(mapping => {
|
||||||
|
|
||||||
logs.plugin('proxy', `Proxy mappings loaded: ${JSON.stringify(proxyMappings)}`);
|
logs.plugin('proxy', `Proxy mappings loaded: ${JSON.stringify(proxyMappings)}`);
|
||||||
|
|
||||||
|
// Store for http-proxy-middleware instances
|
||||||
|
const hpmInstances = {};
|
||||||
|
|
||||||
function createProxyForHost(target) {
|
function createProxyForHost(target) {
|
||||||
return createProxyMiddleware({
|
const proxyOptions = {
|
||||||
target,
|
target,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
ws: true, // Enable WebSocket support
|
ws: true,
|
||||||
|
logLevel: 'info',
|
||||||
timeout: upstreamTimeout,
|
timeout: upstreamTimeout,
|
||||||
proxyTimeout: upstreamTimeout,
|
onError: (err, req, res, _target) => {
|
||||||
onProxyReq: (proxyReq, req, res) => {
|
const targetInfo = _target && _target.href ? _target.href : (typeof _target === 'string' ? _target : 'N/A');
|
||||||
// Remove undefined headers
|
logs.error('proxy', `[HPM onError] Proxy error for ${req.method} ${req.url} to ${targetInfo}: ${err.message} (Code: ${err.code || 'N/A'})`);
|
||||||
const headersToRemove = ['x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-for'];
|
if (res && typeof res.writeHead === 'function') {
|
||||||
headersToRemove.forEach(header => {
|
if (!res.headersSent) {
|
||||||
proxyReq.removeHeader(header);
|
res.writeHead(502, { 'Content-Type': 'text/plain' });
|
||||||
});
|
res.end('Bad Gateway');
|
||||||
|
} else if (typeof res.destroy === 'function' && !res.destroyed) {
|
||||||
// Set proper forwarded headers
|
res.destroy();
|
||||||
const forwarded = {
|
}
|
||||||
for: req.ip || req.connection.remoteAddress,
|
} else if (res && typeof res.end === 'function' && res.writable && !res.destroyed) {
|
||||||
host: req.get('host'),
|
logs.plugin('proxy', `[HPM onError] Client WebSocket socket for ${req.url} attempting to end due to proxy error: ${err.message}.`);
|
||||||
proto: req.protocol
|
res.end();
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
followRedirects: false,
|
||||||
preserveHeaderKeyCase: true,
|
preserveHeaderKeyCase: true,
|
||||||
autoRewrite: true,
|
autoRewrite: true,
|
||||||
protocolRewrite: 'http',
|
protocolRewrite: 'http',
|
||||||
cookieDomainRewrite: {
|
cookieDomainRewrite: { "*": "" }
|
||||||
"*": "" // Remove domain restrictions from cookies
|
};
|
||||||
}
|
|
||||||
});
|
return createProxyMiddleware(proxyOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function proxyMiddleware() {
|
function proxyMiddleware() {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Skip checkpoint endpoints
|
|
||||||
router.use('/api/challenge', (req, res, next) => next('route'));
|
router.use('/api/challenge', (req, res, next) => next('route'));
|
||||||
router.use('/api/verify', (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('/webfont/', (req, res, next) => next('route'));
|
||||||
router.use('/js/', (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]) => {
|
Object.entries(proxyMappings).forEach(([host, target]) => {
|
||||||
proxyInstances[host] = createProxyForHost(target);
|
hpmInstances[host] = createProxyForHost(target);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Main proxy handler
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
const hostname = req.hostname || req.headers.host?.split(':')[0];
|
const hostname = req.hostname || req.headers.host?.split(':')[0];
|
||||||
const proxyInstance = proxyInstances[hostname];
|
const proxyInstance = hpmInstances[hostname];
|
||||||
|
|
||||||
if (proxyInstance) {
|
if (proxyInstance) {
|
||||||
proxyInstance(req, res, next);
|
proxyInstance(req, res, next);
|
||||||
|
|
@ -118,6 +105,10 @@ function proxyMiddleware() {
|
||||||
return { middleware: router };
|
return { middleware: router };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHpmInstance(hostname) {
|
||||||
|
return hpmInstances[hostname];
|
||||||
|
}
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
registerPlugin('proxy', proxyMiddleware());
|
registerPlugin('proxy', proxyMiddleware());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue