Migration Cleanup
This commit is contained in:
parent
d2c014e744
commit
84225a66f9
8 changed files with 224 additions and 354 deletions
82
README.md
Normal file
82
README.md
Normal file
|
|
@ -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
|
||||||
|
```
|
||||||
306
checkpoint.js
306
checkpoint.js
|
|
@ -19,6 +19,7 @@ import {
|
||||||
verifyPoW,
|
verifyPoW,
|
||||||
verifyPoS,
|
verifyPoS,
|
||||||
} from './utils/proof.js';
|
} from './utils/proof.js';
|
||||||
|
import express from 'express';
|
||||||
// Import recordEvent dynamically to avoid circular dependency issues
|
// Import recordEvent dynamically to avoid circular dependency issues
|
||||||
let recordEvent;
|
let recordEvent;
|
||||||
let statsLoadPromise = import('./plugins/stats.js')
|
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) {
|
async function handleTokenRedirect(request) {
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const tokenStr = url.searchParams.get('token');
|
const tokenStr = url.searchParams.get('token');
|
||||||
|
|
@ -660,162 +615,145 @@ async function handleTokenRedirect(request) {
|
||||||
function CheckpointMiddleware() {
|
function CheckpointMiddleware() {
|
||||||
// Return Express-compatible middleware
|
// Return Express-compatible middleware
|
||||||
return {
|
return {
|
||||||
middleware: async (req, res, next) => {
|
middleware: [
|
||||||
// Check if checkpoint is enabled
|
// Add body parser middleware for JSON
|
||||||
if (checkpointConfig.Enabled === false) {
|
express.json({ limit: '10mb' }),
|
||||||
return next();
|
// 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
|
// Convert Express request to the format expected by checkpoint logic
|
||||||
const request = {
|
const request = {
|
||||||
url: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
|
url: `${req.protocol}://${req.get('host')}${req.originalUrl}`,
|
||||||
method: req.method,
|
method: req.method,
|
||||||
headers: {
|
headers: {
|
||||||
get: (name) => req.get(name),
|
get: (name) => req.get(name),
|
||||||
entries: () => Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v])
|
entries: () => Object.entries(req.headers).map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v])
|
||||||
},
|
},
|
||||||
json: () => new Promise((resolve, reject) => {
|
json: () => Promise.resolve(req.body)
|
||||||
let body = '';
|
};
|
||||||
req.on('data', chunk => body += chunk);
|
|
||||||
req.on('end', () => {
|
const urlObj = new URL(request.url);
|
||||||
try {
|
const host = request.headers.get('host')?.split(':')[0];
|
||||||
resolve(JSON.parse(body));
|
const userAgent = request.headers.get('user-agent') || '';
|
||||||
} catch (e) {
|
|
||||||
reject(e);
|
// 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 body = await tokenResponse.text();
|
||||||
})
|
return res.send(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
|
// Setup request context
|
||||||
for (const { Name, Value, Domains } of checkpointConfig.BypassHeaderKeys) {
|
const url = new URL(request.url);
|
||||||
const headerVal = request.headers.get(Name);
|
let path = url.pathname;
|
||||||
if (headerVal === Value) {
|
if (checkpointConfig.SanitizeURLs) {
|
||||||
if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) {
|
path = sanitizePath(path);
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
const method = request.method;
|
||||||
|
|
||||||
// Handle token redirect for URL-token login
|
// Always allow challenge & verify endpoints
|
||||||
const tokenResponse = await handleTokenRedirect(request);
|
if (method === 'GET' && path === '/api/challenge') {
|
||||||
if (tokenResponse) {
|
const response = await handleGetCheckpointChallenge(request);
|
||||||
// Convert Response to Express response
|
res.status(response.status);
|
||||||
res.status(tokenResponse.status);
|
response.headers.forEach((value, key) => {
|
||||||
tokenResponse.headers.forEach((value, key) => {
|
res.setHeader(key, value);
|
||||||
res.setHeader(key, value);
|
});
|
||||||
});
|
const body = await response.text();
|
||||||
const body = await tokenResponse.text();
|
return res.send(body);
|
||||||
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
|
// Check new exclusion rules
|
||||||
const url = new URL(request.url);
|
if (checkpointConfig.ExclusionRules && checkpointConfig.ExclusionRules.length > 0) {
|
||||||
let path = url.pathname;
|
for (const rule of checkpointConfig.ExclusionRules) {
|
||||||
if (checkpointConfig.SanitizeURLs) {
|
// Check if path matches
|
||||||
path = sanitizePath(path);
|
if (!rule.Path || !path.startsWith(rule.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) {
|
|
||||||
continue;
|
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();
|
return next();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check file extensions
|
// Validate session token
|
||||||
const ext = path.includes('.') ? path.slice(path.lastIndexOf('.')) : '';
|
const cookies = cookie.parse(request.headers.get('cookie') || '');
|
||||||
|
const tokenCookie = cookies[checkpointConfig.CookieName];
|
||||||
// First check excluded extensions
|
const validation = await validateToken(tokenCookie, request);
|
||||||
if (ext && checkpointConfig.HTMLCheckpointExcludedExtensions.includes(ext)) {
|
if (validation) {
|
||||||
return next();
|
// Active session: bypass checkpoint
|
||||||
}
|
|
||||||
|
|
||||||
// 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)) {
|
|
||||||
return next();
|
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,20 +124,4 @@ Hosts = ["music.caileb.com"] # Optional: restrict to specific hosts
|
||||||
Type = "header"
|
Type = "header"
|
||||||
Key = "X-Bypass-Token"
|
Key = "X-Bypass-Token"
|
||||||
Value = "another-secret-key"
|
Value = "another-secret-key"
|
||||||
# Hosts = [] # If empty or omitted, applies to all hosts
|
# 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"
|
|
||||||
]
|
|
||||||
65
index.js
65
index.js
|
|
@ -138,9 +138,6 @@ async function main() {
|
||||||
// Initialize WebSocket server
|
// Initialize WebSocket server
|
||||||
const wss = new WebSocketServer({ noServer: true });
|
const wss = new WebSocketServer({ noServer: true });
|
||||||
|
|
||||||
// Store WebSocket handlers
|
|
||||||
let wsHandlers = {};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await secureImportModule('checkpoint.js');
|
await secureImportModule('checkpoint.js');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -153,10 +150,6 @@ async function main() {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await secureImportModule('plugins/proxy.js');
|
await secureImportModule('plugins/proxy.js');
|
||||||
const mod = await import('./plugins/proxy.js');
|
|
||||||
if (mod.proxyWebSocketHandler) {
|
|
||||||
wsHandlers = mod.proxyWebSocketHandler;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logs.error('proxy', `Failed to load proxy plugin: ${e}`);
|
logs.error('proxy', `Failed to load proxy plugin: ${e}`);
|
||||||
}
|
}
|
||||||
|
|
@ -183,30 +176,19 @@ async function main() {
|
||||||
// Apply all plugin middlewares to Express
|
// Apply all plugin middlewares to Express
|
||||||
const middlewareHandlers = loadPlugins();
|
const middlewareHandlers = loadPlugins();
|
||||||
middlewareHandlers.forEach(handler => {
|
middlewareHandlers.forEach(handler => {
|
||||||
if (typeof handler === 'function') {
|
if (handler && handler.middleware) {
|
||||||
// Wrap plugin handlers to work with Express
|
// If plugin exports an object with middleware property
|
||||||
app.use(async (req, res, next) => {
|
if (Array.isArray(handler.middleware)) {
|
||||||
try {
|
// If middleware is an array, apply each one
|
||||||
const result = await handler(req, { upgrade: () => false });
|
handler.middleware.forEach(mw => app.use(mw));
|
||||||
if (result instanceof Response) {
|
} else {
|
||||||
// Convert Response to Express response
|
// Single middleware
|
||||||
res.status(result.status);
|
app.use(handler.middleware);
|
||||||
result.headers.forEach((value, key) => {
|
}
|
||||||
res.setHeader(key, value);
|
} else if (typeof handler === 'function') {
|
||||||
});
|
// Legacy function-style handlers (shouldn't exist anymore)
|
||||||
const body = await result.text();
|
logs.warn('server', 'Found legacy function-style plugin handler');
|
||||||
res.send(body);
|
app.use(handler);
|
||||||
} 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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -221,30 +203,15 @@ async function main() {
|
||||||
res.status(500).send(`Server Error: ${err.message}`);
|
res.status(500).send(`Server Error: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle WebSocket upgrades
|
// Handle WebSocket upgrades for http-proxy-middleware
|
||||||
server.on('upgrade', (request, socket, head) => {
|
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.handleUpgrade(request, socket, head, (ws) => {
|
||||||
wss.emit('connection', ws, request);
|
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');
|
logs.section('SERVER');
|
||||||
const portNumber = Number(process.env.PORT || 3000);
|
const portNumber = Number(process.env.PORT || 3000);
|
||||||
server.listen(portNumber, () => {
|
server.listen(portNumber, () => {
|
||||||
|
|
|
||||||
91
package-lock.json
generated
91
package-lock.json
generated
|
|
@ -16,9 +16,7 @@
|
||||||
"maxmind": "^4.3.25",
|
"maxmind": "^4.3.25",
|
||||||
"pm2": "^5.3.0",
|
"pm2": "^5.3.0",
|
||||||
"string-dsa": "^2.1.0",
|
"string-dsa": "^2.1.0",
|
||||||
"tar": "^7.4.3",
|
|
||||||
"tar-stream": "^3.1.7",
|
"tar-stream": "^3.1.7",
|
||||||
"toml": "^3.0.0",
|
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -35,18 +33,6 @@
|
||||||
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
|
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/@pm2/agent": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz",
|
||||||
|
|
@ -772,15 +758,6 @@
|
||||||
"fsevents": "~2.3.2"
|
"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": {
|
"node_modules/classic-level": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-3.0.0.tgz",
|
||||||
|
|
@ -1991,27 +1968,6 @@
|
||||||
"node": "*"
|
"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": {
|
"node_modules/mkdirp": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||||
|
|
@ -3191,23 +3147,6 @@
|
||||||
"url": "https://www.buymeacoffee.com/systeminfo"
|
"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": {
|
"node_modules/tar-stream": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||||
|
|
@ -3219,21 +3158,6 @@
|
||||||
"streamx": "^2.15.0"
|
"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": {
|
"node_modules/text-decoder": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||||
|
|
@ -3273,12 +3197,6 @@
|
||||||
"node": ">=0.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": {
|
"node_modules/touch": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
||||||
|
|
@ -3435,15 +3353,6 @@
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4"
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,7 @@
|
||||||
"maxmind": "^4.3.25",
|
"maxmind": "^4.3.25",
|
||||||
"pm2": "^5.3.0",
|
"pm2": "^5.3.0",
|
||||||
"string-dsa": "^2.1.0",
|
"string-dsa": "^2.1.0",
|
||||||
"tar": "^7.4.3",
|
|
||||||
"tar-stream": "^3.1.7",
|
"tar-stream": "^3.1.7",
|
||||||
"toml": "^3.0.0",
|
|
||||||
"ws": "^8.16.0"
|
"ws": "^8.16.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
||||||
|
|
@ -118,16 +118,6 @@ function proxyMiddleware() {
|
||||||
return { middleware: router };
|
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) {
|
if (enabled) {
|
||||||
registerPlugin('proxy', proxyMiddleware());
|
registerPlugin('proxy', proxyMiddleware());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,9 @@ async function handleStatsPage(req, res) {
|
||||||
const url = new URL(`${req.protocol}://${req.get('host')}${req.originalUrl}`);
|
const url = new URL(`${req.protocol}://${req.get('host')}${req.originalUrl}`);
|
||||||
if (url.pathname !== statsUIPath) return false;
|
if (url.pathname !== statsUIPath) return false;
|
||||||
try {
|
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);
|
res.status(200).type('html').send(html);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue