From 84225a66f93ee79bac993e8eec2af043fbf79ce5 Mon Sep 17 00:00:00 2001 From: Caileb Date: Tue, 27 May 2025 18:16:16 -0500 Subject: [PATCH] Migration Cleanup --- README.md | 82 +++++++++++ checkpoint.js | 306 ++++++++++++++++------------------------- config/checkpoint.toml | 18 +-- index.js | 65 +++------ package-lock.json | 91 ------------ package.json | 2 - plugins/proxy.js | 10 -- plugins/stats.js | 4 +- 8 files changed, 224 insertions(+), 354 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..93cac43 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Checkpoint + +> Secure, extensible, high-performance Node.js middleware server for proof-of-work security, IP filtering, reverse proxying, and real-time analytics. + +**Features:** + +- 🔐 **Checkpoint Security:** Enforce proof-of-work (PoW) and proof-of-space-time (PoST) challenges before granting access. +- 🌎 **IP & Geo-Blocking:** Block or allow traffic based on country, continent, or ASN using MaxMind GeoIP2. +- 🔀 **Reverse Proxy:** Route incoming requests to backend services based on hostname mappings. +- 📊 **Real-time Stats:** Collect detailed metrics and browse via built-in web UI or API. +- 🧩 **Plugin Architecture:** Easily extend and customize via modular plugins. +- 🛠️ **Flexible Configuration:** Manage settings in TOML files and via environment variables. +- ⚙️ **Daemon & PM2 Support:** Run as a background service with built-in daemon mode or PM2. +- 📂 **Data Persistence:** Secure token storage with LevelDB + TTL and HMAC protection. + +## 🚀 Quick Start + +1. **Clone the repository** + ```bash + git clone https://git.caileb.com/Caileb/Checkpoint.git + cd Checkpoint + ``` +2. **Install dependencies** + ```bash + npm install + ``` +3. **Set up environment variables** (optional) + Create a `.env` file in the project root: + ```ini + MAXMIND_ACCOUNT_ID=your_account_id + MAXMIND_LICENSE_KEY=your_license_key + PORT=8080 # Default: 3000 + ``` +4. **Development mode** + ```bash + npm run dev + ``` +5. **Start the server** + ```bash + npm start + ``` +6. **Daemonize** + ```bash + npm run daemon # Start in background + npm run stop # Stop daemon + npm run restart # Restart daemon + npm run logs # Show logs + ``` + Or use PM2 directly: + ```bash + pm2 start index.js --name checkpoint + ``` + +## ⚙️ Configuration + +All core settings are stored in the `config/` directory as TOML files: + +- `checkpoint.toml` — PoW/PoST parameters, tokens, exclusions, interstitial templates. +- `ipfilter.toml` — Country, continent, ASN filtering rules and custom block pages. +- `proxy.toml` — Hostname-to-backend mappings and timeouts. +- `stats.toml` — Metrics TTL and paths for UI/API. + +Override any setting via environment variables or by editing these files directly. + +## 📂 Directory Structure + +```plaintext +. +├── config/ # TOML configuration files +├── data/ # Runtime data (secrets, snapshots) +├── db/ # LevelDB token stores +├── plugins/ # Plugin modules (checkpoint, ipfilter, proxy, stats) +├── pages/ # Static assets and UI templates +│ ├── interstitial/ # Proof-of-work challenge pages +│ ├── ipfilter/ # Custom block pages +│ └── stats/ # Statistics web UI +├── utils/ # Internal utilities (logging, network, proof, time) +├── index.js # Core server & plugin loader +├── checkpoint.js # Checkpoint security middleware +├── package.json # Project metadata & scripts +└── README.md # This file +``` \ No newline at end of file diff --git a/checkpoint.js b/checkpoint.js index edfd9b5..f8b94ae 100644 --- a/checkpoint.js +++ b/checkpoint.js @@ -19,6 +19,7 @@ import { verifyPoW, verifyPoS, } from './utils/proof.js'; +import express from 'express'; // Import recordEvent dynamically to avoid circular dependency issues let recordEvent; let statsLoadPromise = import('./plugins/stats.js') @@ -560,52 +561,6 @@ async function validateToken(tokenStr, request) { } } -async function createProxyResponse(targetURL, request) { - const url = new URL(request.url); - const targetUrl = new URL(url.pathname + url.search, targetURL); - - const headers = Object.fromEntries(request.headers.entries()); - delete headers.host; - - try { - const method = request.method; - const options = { - method, - headers, - redirect: 'manual', - }; - - if (method !== 'GET' && method !== 'HEAD') { - options.body = await request.blob(); - } - - const response = await fetch(targetUrl.toString(), options); - - const responseHeaders = new Headers(response.headers); - const hopByHopHeaders = [ - 'connection', - 'keep-alive', - 'proxy-authenticate', - 'proxy-authorization', - 'te', - 'trailer', - 'transfer-encoding', - 'upgrade', - ]; - - hopByHopHeaders.forEach((h) => responseHeaders.delete(h)); - - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers: responseHeaders, - }); - } catch (err) { - console.error('Proxy error:', err); - return new Response('Bad Gateway', { status: 502 }); - } -} - async function handleTokenRedirect(request) { const url = new URL(request.url); const tokenStr = url.searchParams.get('token'); @@ -660,162 +615,145 @@ async function handleTokenRedirect(request) { function CheckpointMiddleware() { // Return Express-compatible middleware return { - middleware: async (req, res, next) => { - // Check if checkpoint is enabled - if (checkpointConfig.Enabled === false) { - return next(); - } + middleware: [ + // Add body parser middleware for JSON + express.json({ limit: '10mb' }), + // Main checkpoint middleware + async (req, res, next) => { + // Check if checkpoint is enabled + if (checkpointConfig.Enabled === false) { + return next(); + } - // Convert Express request to the format expected by checkpoint logic - const request = { - url: `${req.protocol}://${req.get('host')}${req.originalUrl}`, - method: req.method, - headers: { - get: (name) => req.get(name), - entries: () => Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v]) - }, - json: () => new Promise((resolve, reject) => { - let body = ''; - req.on('data', chunk => body += chunk); - req.on('end', () => { - try { - resolve(JSON.parse(body)); - } catch (e) { - reject(e); + // Convert Express request to the format expected by checkpoint logic + const request = { + url: `${req.protocol}://${req.get('host')}${req.originalUrl}`, + method: req.method, + headers: { + get: (name) => req.get(name), + entries: () => Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v]) + }, + json: () => Promise.resolve(req.body) + }; + + const urlObj = new URL(request.url); + const host = request.headers.get('host')?.split(':')[0]; + const userAgent = request.headers.get('user-agent') || ''; + + // 1) Bypass via query keys + for (const { Key, Value, Domains } of checkpointConfig.BypassQueryKeys) { + if (urlObj.searchParams.get(Key) === Value) { + if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) { + return next(); } + } + } + + // 2) Bypass via header keys + for (const { Name, Value, Domains } of checkpointConfig.BypassHeaderKeys) { + const headerVal = request.headers.get(Name); + if (headerVal === Value) { + if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) { + return next(); + } + } + } + + // Handle token redirect for URL-token login + const tokenResponse = await handleTokenRedirect(request); + if (tokenResponse) { + // Convert Response to Express response + res.status(tokenResponse.status); + tokenResponse.headers.forEach((value, key) => { + res.setHeader(key, value); }); - req.on('error', reject); - }) - }; - - const urlObj = new URL(request.url); - const host = request.headers.get('host')?.split(':')[0]; - const userAgent = request.headers.get('user-agent') || ''; - - // 1) Bypass via query keys - for (const { Key, Value, Domains } of checkpointConfig.BypassQueryKeys) { - if (urlObj.searchParams.get(Key) === Value) { - if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) { - return next(); - } + const body = await tokenResponse.text(); + return res.send(body); } - } - // 2) Bypass via header keys - for (const { Name, Value, Domains } of checkpointConfig.BypassHeaderKeys) { - const headerVal = request.headers.get(Name); - if (headerVal === Value) { - if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) { - return next(); - } + // Setup request context + const url = new URL(request.url); + let path = url.pathname; + if (checkpointConfig.SanitizeURLs) { + path = sanitizePath(path); } - } + const method = request.method; - // Handle token redirect for URL-token login - const tokenResponse = await handleTokenRedirect(request); - if (tokenResponse) { - // Convert Response to Express response - res.status(tokenResponse.status); - tokenResponse.headers.forEach((value, key) => { - res.setHeader(key, value); - }); - const body = await tokenResponse.text(); - return res.send(body); - } + // Always allow challenge & verify endpoints + if (method === 'GET' && path === '/api/challenge') { + const response = await handleGetCheckpointChallenge(request); + res.status(response.status); + response.headers.forEach((value, key) => { + res.setHeader(key, value); + }); + const body = await response.text(); + return res.send(body); + } + if (method === 'POST' && path === '/api/verify') { + const response = await handleVerifyCheckpoint(request); + res.status(response.status); + response.headers.forEach((value, key) => { + res.setHeader(key, value); + }); + const body = await response.text(); + return res.send(body); + } - // Setup request context - const url = new URL(request.url); - let path = url.pathname; - if (checkpointConfig.SanitizeURLs) { - path = sanitizePath(path); - } - const method = request.method; - - // Always allow challenge & verify endpoints - if (method === 'GET' && path === '/api/challenge') { - const response = await handleGetCheckpointChallenge(request); - res.status(response.status); - response.headers.forEach((value, key) => { - res.setHeader(key, value); - }); - const body = await response.text(); - return res.send(body); - } - if (method === 'POST' && path === '/api/verify') { - const response = await handleVerifyCheckpoint(request); - res.status(response.status); - response.headers.forEach((value, key) => { - res.setHeader(key, value); - }); - const body = await response.text(); - return res.send(body); - } - - // Check new exclusion rules - if (checkpointConfig.ExclusionRules && checkpointConfig.ExclusionRules.length > 0) { - for (const rule of checkpointConfig.ExclusionRules) { - // Check if path matches - if (!rule.Path || !path.startsWith(rule.Path)) { - continue; - } - - // Check if host matches (if specified) - if (rule.Hosts && rule.Hosts.length > 0 && !rule.Hosts.includes(host)) { - continue; - } - - // Check if user agent matches (if specified) - if (rule.UserAgents && rule.UserAgents.length > 0) { - const matchesUA = rule.UserAgents.some((ua) => userAgent.includes(ua)); - if (!matchesUA) { + // Check new exclusion rules + if (checkpointConfig.ExclusionRules && checkpointConfig.ExclusionRules.length > 0) { + for (const rule of checkpointConfig.ExclusionRules) { + // Check if path matches + if (!rule.Path || !path.startsWith(rule.Path)) { continue; } + + // Check if host matches (if specified) + if (rule.Hosts && rule.Hosts.length > 0 && !rule.Hosts.includes(host)) { + continue; + } + + // Check if user agent matches (if specified) + if (rule.UserAgents && rule.UserAgents.length > 0) { + const matchesUA = rule.UserAgents.some((ua) => userAgent.includes(ua)); + if (!matchesUA) { + continue; + } + } + + // All conditions match - exclude this request + return next(); } + } - // All conditions match - exclude this request + // Skip checkpoint for requests that don't accept HTML + if (!req.accepts('html')) { return next(); } - } - // Check file extensions - const ext = path.includes('.') ? path.slice(path.lastIndexOf('.')) : ''; - - // First check excluded extensions - if (ext && checkpointConfig.HTMLCheckpointExcludedExtensions.includes(ext)) { - return next(); - } - - // Then check if we should only include specific extensions - if (checkpointConfig.HTMLCheckpointIncludedExtensions.length > 0) { - // If extension list is specified and current extension is not in it, skip - if (!checkpointConfig.HTMLCheckpointIncludedExtensions.includes(ext)) { + // Validate session token + const cookies = cookie.parse(request.headers.get('cookie') || ''); + const tokenCookie = cookies[checkpointConfig.CookieName]; + const validation = await validateToken(tokenCookie, request); + if (validation) { + // Active session: bypass checkpoint return next(); } + + // Log new checkpoint flow + console.log(`checkpoint: incoming ${method} ${request.url}`); + console.log(`checkpoint: tokenCookie=${tokenCookie}`); + console.log(`checkpoint: validateToken => ${validation}`); + + // Serve interstitial challenge + const response = await serveInterstitial(request); + res.status(response.status); + response.headers.forEach((value, key) => { + res.setHeader(key, value); + }); + const body = await response.text(); + return res.send(body); } - - // Validate session token - const cookies = cookie.parse(request.headers.get('cookie') || ''); - const tokenCookie = cookies[checkpointConfig.CookieName]; - const validation = await validateToken(tokenCookie, request); - if (validation) { - // Active session: bypass checkpoint - return next(); - } - - // Log new checkpoint flow - console.log(`checkpoint: incoming ${method} ${request.url}`); - console.log(`checkpoint: tokenCookie=${tokenCookie}`); - console.log(`checkpoint: validateToken => ${validation}`); - - // Serve interstitial challenge - const response = await serveInterstitial(request); - res.status(response.status); - response.headers.forEach((value, key) => { - res.setHeader(key, value); - }); - const body = await response.text(); - return res.send(body); - } + ] }; } diff --git a/config/checkpoint.toml b/config/checkpoint.toml index a01cec6..ae3282e 100644 --- a/config/checkpoint.toml +++ b/config/checkpoint.toml @@ -124,20 +124,4 @@ Hosts = ["music.caileb.com"] # Optional: restrict to specific hosts Type = "header" Key = "X-Bypass-Token" Value = "another-secret-key" -# Hosts = [] # If empty or omitted, applies to all hosts - -# ----------------------------------------------------------------------------- -# FILE EXTENSION HANDLING -# ----------------------------------------------------------------------------- -[Extensions] -# Only apply checkpoint to these file extensions (for HTML content) -# Empty = check all paths -IncludeOnly = [".html", ".htm", ".shtml", ""] - -# Never apply checkpoint to these file extensions -# This takes precedence over IncludeOnly -Exclude = [ - ".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg", - ".ico", ".woff", ".woff2", ".ttf", ".eot", ".map", - ".json", ".xml", ".txt", ".webp", ".avif" -] \ No newline at end of file +# Hosts = [] # If empty or omitted, applies to all hosts \ No newline at end of file diff --git a/index.js b/index.js index 90ea105..47891c2 100644 --- a/index.js +++ b/index.js @@ -138,9 +138,6 @@ async function main() { // Initialize WebSocket server const wss = new WebSocketServer({ noServer: true }); - // Store WebSocket handlers - let wsHandlers = {}; - try { await secureImportModule('checkpoint.js'); } catch (e) { @@ -153,10 +150,6 @@ async function main() { } try { await secureImportModule('plugins/proxy.js'); - const mod = await import('./plugins/proxy.js'); - if (mod.proxyWebSocketHandler) { - wsHandlers = mod.proxyWebSocketHandler; - } } catch (e) { logs.error('proxy', `Failed to load proxy plugin: ${e}`); } @@ -183,30 +176,19 @@ async function main() { // Apply all plugin middlewares to Express const middlewareHandlers = loadPlugins(); middlewareHandlers.forEach(handler => { - if (typeof handler === 'function') { - // Wrap plugin handlers to work with Express - app.use(async (req, res, next) => { - try { - const result = await handler(req, { upgrade: () => false }); - if (result instanceof Response) { - // Convert Response to Express response - res.status(result.status); - result.headers.forEach((value, key) => { - res.setHeader(key, value); - }); - const body = await result.text(); - res.send(body); - } else { - next(); - } - } catch (err) { - logs.error('server', `Handler error: ${err}`); - next(err); - } - }); - } else if (handler && handler.middleware) { - // If plugin exports Express middleware directly - app.use(handler.middleware); + if (handler && handler.middleware) { + // If plugin exports an object with middleware property + if (Array.isArray(handler.middleware)) { + // If middleware is an array, apply each one + handler.middleware.forEach(mw => app.use(mw)); + } else { + // Single middleware + app.use(handler.middleware); + } + } else if (typeof handler === 'function') { + // Legacy function-style handlers (shouldn't exist anymore) + logs.warn('server', 'Found legacy function-style plugin handler'); + app.use(handler); } }); @@ -221,30 +203,15 @@ async function main() { res.status(500).send(`Server Error: ${err.message}`); }); - // Handle WebSocket upgrades + // 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); }); }); - // WebSocket connection handler - if (wsHandlers.open) { - wss.on('connection', (ws, request) => { - ws.data = {}; - if (wsHandlers.open) wsHandlers.open(ws); - if (wsHandlers.message) { - ws.on('message', (message) => wsHandlers.message(ws, message)); - } - if (wsHandlers.close) { - ws.on('close', (code, reason) => wsHandlers.close(ws, code, reason)); - } - if (wsHandlers.error) { - ws.on('error', (err) => wsHandlers.error(ws, err)); - } - }); - } - logs.section('SERVER'); const portNumber = Number(process.env.PORT || 3000); server.listen(portNumber, () => { diff --git a/package-lock.json b/package-lock.json index 9543dec..ccf80e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,7 @@ "maxmind": "^4.3.25", "pm2": "^5.3.0", "string-dsa": "^2.1.0", - "tar": "^7.4.3", "tar-stream": "^3.1.7", - "toml": "^3.0.0", "ws": "^8.16.0" }, "devDependencies": { @@ -35,18 +33,6 @@ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", "license": "ISC" }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@pm2/agent": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz", @@ -772,15 +758,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, "node_modules/classic-level": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-3.0.0.tgz", @@ -1991,27 +1968,6 @@ "node": "*" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -3191,23 +3147,6 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, - "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", @@ -3219,21 +3158,6 @@ "streamx": "^2.15.0" } }, - "node_modules/tar/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/text-decoder": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", @@ -3273,12 +3197,6 @@ "node": ">=0.6" } }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "license": "MIT" - }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -3435,15 +3353,6 @@ "engines": { "node": ">=0.4" } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } } } } diff --git a/package.json b/package.json index 5b20216..a80f681 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,7 @@ "maxmind": "^4.3.25", "pm2": "^5.3.0", "string-dsa": "^2.1.0", - "tar": "^7.4.3", "tar-stream": "^3.1.7", - "toml": "^3.0.0", "ws": "^8.16.0" }, "engines": { diff --git a/plugins/proxy.js b/plugins/proxy.js index de3d357..82d72dd 100644 --- a/plugins/proxy.js +++ b/plugins/proxy.js @@ -118,16 +118,6 @@ function proxyMiddleware() { return { middleware: router }; } -// Export WebSocket handler for compatibility -export const proxyWebSocketHandler = { - // http-proxy-middleware handles WebSocket internally - // These are kept for compatibility but won't be used - open: () => {}, - message: () => {}, - close: () => {}, - error: () => {} -}; - if (enabled) { registerPlugin('proxy', proxyMiddleware()); } else { diff --git a/plugins/stats.js b/plugins/stats.js index 34bce90..62eec29 100644 --- a/plugins/stats.js +++ b/plugins/stats.js @@ -76,7 +76,9 @@ async function handleStatsPage(req, res) { const url = new URL(`${req.protocol}://${req.get('host')}${req.originalUrl}`); if (url.pathname !== statsUIPath) return false; try { - const html = await fs.readFile(path.join(__dirname, 'stats.html'), 'utf8'); + // Load the stats UI from pages/stats/stats.html in the project root + const statsHtmlPath = path.join(rootDir, 'pages', 'stats', 'stats.html'); + const html = await fs.readFile(statsHtmlPath, 'utf8'); res.status(200).type('html').send(html); return true; } catch (e) {