diff --git a/.gitignore b/.gitignore index 78bc649..0fbb72c 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json data # DB Folder -db \ No newline at end of file +db + +# My Configs +*.toml \ No newline at end of file diff --git a/checkpoint.js b/checkpoint.js index f8b94ae..fb9cde9 100644 --- a/checkpoint.js +++ b/checkpoint.js @@ -124,8 +124,8 @@ async function initConfig() { }); // Extension handling - checkpointConfig.HTMLCheckpointIncludedExtensions = checkpointConfig.Extensions.IncludeOnly; - checkpointConfig.HTMLCheckpointExcludedExtensions = checkpointConfig.Extensions.Exclude; + checkpointConfig.HTMLCheckpointIncludedExtensions = checkpointConfig.Extensions?.IncludeOnly || []; + checkpointConfig.HTMLCheckpointExcludedExtensions = checkpointConfig.Extensions?.Exclude || []; // Remove legacy arrays checkpointConfig.HTMLCheckpointExclusions = []; diff --git a/config/checkpoint.toml b/config/checkpoint.toml index ae3282e..02f5c8e 100644 --- a/config/checkpoint.toml +++ b/config/checkpoint.toml @@ -90,22 +90,9 @@ Path = "/api" Hosts = ["gallery.caileb.com"] # Optional: only for specific hosts [[Exclusion]] -# Skip checkpoint for health checks -Path = "/health" - -[[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"] +# Allows Git pushes w/ ForgeJo +Path = "/info/refs" +Hosts = ["git.caileb.com"] # ----------------------------------------------------------------------------- # BYPASS KEYS diff --git a/config/checkpoint.toml.example b/config/checkpoint.toml.example new file mode 100644 index 0000000..d7b6b02 --- /dev/null +++ b/config/checkpoint.toml.example @@ -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 \ No newline at end of file diff --git a/config/ipfilter.toml.example b/config/ipfilter.toml.example new file mode 100644 index 0000000..0ddc249 --- /dev/null +++ b/config/ipfilter.toml.example @@ -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" \ No newline at end of file diff --git a/config/proxy.toml b/config/proxy.toml index b009c94..e35793b 100644 --- a/config/proxy.toml +++ b/config/proxy.toml @@ -17,7 +17,7 @@ Enabled = true # ----------------------------------------------------------------------------- [Timeouts] # WebSocket connection timeout in milliseconds -WebSocketTimeoutMs = 5000 +WebSocketTimeoutMs = 60000 # Upstream HTTP request timeout in milliseconds UpstreamTimeoutMs = 30000 diff --git a/config/proxy.toml.example b/config/proxy.toml.example new file mode 100644 index 0000000..8ad2372 --- /dev/null +++ b/config/proxy.toml.example @@ -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" \ No newline at end of file diff --git a/config/stats.toml.example b/config/stats.toml.example new file mode 100644 index 0000000..b94fe3f --- /dev/null +++ b/config/stats.toml.example @@ -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" \ No newline at end of file diff --git a/index.js b/index.js index 47891c2..9b90686 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,6 @@ import { secureImportModule } from './utils/plugins.js'; import * as logs from './utils/logs.js'; import express from 'express'; import { createServer } from 'http'; -import { WebSocketServer } from 'ws'; import { spawn } from 'child_process'; // Load environment variables from .env file @@ -135,9 +134,6 @@ async function main() { // Trust proxy headers (important for proper protocol detection) app.set('trust proxy', true); - // Initialize WebSocket server - const wss = new WebSocketServer({ noServer: true }); - try { await secureImportModule('checkpoint.js'); } catch (e) { @@ -203,12 +199,30 @@ async function main() { res.status(500).send(`Server Error: ${err.message}`); }); - // Handle WebSocket upgrades for http-proxy-middleware - server.on('upgrade', (request, socket, head) => { - // http-proxy-middleware handles WebSocket upgrades internally - // This is just a placeholder for any custom WebSocket handling if needed - wss.handleUpgrade(request, socket, head, (ws) => { - wss.emit('connection', ws, request); + // Handle WebSocket upgrades using http-proxy-middleware instances + server.on('upgrade', (req, socket, head) => { + const hostname = req.headers.host?.split(':')[0]; + if (!hostname) { + logs.error('websocket', 'Upgrade request without host header, destroying socket.'); + 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(); }); }); diff --git a/plugins/proxy.js b/plugins/proxy.js index 82d72dd..f842d29 100644 --- a/plugins/proxy.js +++ b/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 {