mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
- Refactored `test/memory-scheduler.test.mjs` to use `mock.timers` for deterministic execution, replacing flaky `wait` logic. - Strengthened `test/lock-ops.test.mjs` with edge case tests for `parseLockEvent` handling invalid JSON and missing fields. - Fixed a security vulnerability in `src/dashboard.mjs` where sensitive configuration (e.g., rosters, auth) was exposed in `/torch-config.json`. - Updated `src/prompts/daily/test-audit-agent.md` to comply with the canonical prompt contract. - Added `reports/test-audit/test-integrity-note-2025-02-14.yaml` detailing test changes and anti-cheat measures. Co-authored-by: PR0M3TH3AN <[email protected]>
230 lines
7.8 KiB
JavaScript
230 lines
7.8 KiB
JavaScript
import fs from 'node:fs';
|
|
import fsp from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import http from 'node:http';
|
|
import crypto from 'node:crypto';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { DEFAULT_DASHBOARD_PORT } from './constants.mjs';
|
|
import { getDashboardAuth, parseTorchConfig } from './torch-config.mjs';
|
|
|
|
const SECURITY_HEADERS = {
|
|
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:; object-src 'none'; base-uri 'self';",
|
|
'X-Content-Type-Options': 'nosniff',
|
|
'X-Frame-Options': 'DENY',
|
|
'Referrer-Policy': 'strict-origin-when-cross-origin'
|
|
};
|
|
|
|
function timingSafeCompare(a, b) {
|
|
const bufA = Buffer.from(a);
|
|
const bufB = Buffer.from(b);
|
|
if (bufA.length !== bufB.length) {
|
|
// Still do a comparison to avoid some timing leaks, though length leak is hard to avoid entirely
|
|
crypto.timingSafeEqual(bufA, bufA);
|
|
return false;
|
|
}
|
|
return crypto.timingSafeEqual(bufA, bufB);
|
|
}
|
|
|
|
export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.0.1') {
|
|
// Resolve package root relative to this file (src/dashboard.mjs)
|
|
// this file is in <root>/src/dashboard.mjs, so '..' goes to <root>
|
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const auth = await getDashboardAuth();
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
try {
|
|
// Basic Auth check
|
|
if (auth) {
|
|
const authHeader = req.headers.authorization;
|
|
if (!authHeader) {
|
|
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
|
|
res.end('Authentication required');
|
|
return;
|
|
}
|
|
const parts = authHeader.split(' ');
|
|
const type = parts[0];
|
|
const credentials = parts[1];
|
|
let isValid = false;
|
|
if (type === 'Basic' && credentials) {
|
|
try {
|
|
const decoded = Buffer.from(credentials, 'base64').toString();
|
|
isValid = timingSafeCompare(decoded, auth);
|
|
} catch {
|
|
isValid = false;
|
|
}
|
|
}
|
|
|
|
if (!isValid) {
|
|
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
|
|
res.end('Invalid credentials');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// URL parsing
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
let pathname = url.pathname;
|
|
|
|
// Redirect / to /dashboard/
|
|
if (pathname === '/' || pathname === '/dashboard') {
|
|
res.writeHead(302, { ...SECURITY_HEADERS, 'Location': '/dashboard/' });
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
async function statSafe(p) {
|
|
try {
|
|
return await fsp.stat(p);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Special case: /torch-config.json
|
|
// Priority: Env Var > User's CWD > Package default
|
|
if (pathname === '/torch-config.json') {
|
|
let configContent = null;
|
|
|
|
if (process.env.TORCH_CONFIG_PATH) {
|
|
const envPath = path.resolve(process.cwd(), process.env.TORCH_CONFIG_PATH);
|
|
try {
|
|
configContent = await fsp.readFile(envPath, 'utf8');
|
|
} catch {
|
|
// Explicitly provided config path not found, do not fall back
|
|
}
|
|
} else {
|
|
const userConfigPath = path.resolve(process.cwd(), 'torch-config.json');
|
|
try {
|
|
configContent = await fsp.readFile(userConfigPath, 'utf8');
|
|
} catch {
|
|
// Try package default
|
|
const packageConfigPath = path.join(packageRoot, 'torch-config.json');
|
|
try {
|
|
configContent = await fsp.readFile(packageConfigPath, 'utf8');
|
|
} catch {
|
|
// Not found in either location
|
|
}
|
|
}
|
|
}
|
|
|
|
if (configContent) {
|
|
try {
|
|
const rawConfig = JSON.parse(configContent);
|
|
const safeConfig = parseTorchConfig(rawConfig);
|
|
|
|
// Security: Whitelist only fields required by the dashboard frontend
|
|
const publicConfig = {
|
|
dashboard: safeConfig.dashboard ? { ...safeConfig.dashboard } : {},
|
|
nostrLock: safeConfig.nostrLock
|
|
? {
|
|
namespace: safeConfig.nostrLock.namespace,
|
|
relays: safeConfig.nostrLock.relays,
|
|
}
|
|
: {},
|
|
};
|
|
|
|
// Remove sensitive dashboard auth
|
|
if (publicConfig.dashboard.auth) {
|
|
delete publicConfig.dashboard.auth;
|
|
}
|
|
|
|
res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(publicConfig, null, 2));
|
|
return;
|
|
} catch (err) {
|
|
console.error('Error parsing torch-config.json:', err);
|
|
res.writeHead(500, SECURITY_HEADERS);
|
|
res.end('Internal Server Error');
|
|
return;
|
|
}
|
|
} else {
|
|
res.writeHead(404, SECURITY_HEADERS);
|
|
res.end('Not Found');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Security check: prevent directory traversal
|
|
// Resolve path relative to packageRoot.
|
|
// We strip the leading slash from pathname (which comes from URL) to treat it as relative.
|
|
const relativePath = pathname.replace(/^\//, '');
|
|
let filePath = path.resolve(packageRoot, relativePath);
|
|
|
|
// Security check: restrict access to allowed paths
|
|
const allowedPaths = [
|
|
path.join(packageRoot, 'dashboard'),
|
|
path.join(packageRoot, 'assets'),
|
|
path.join(packageRoot, 'src', 'docs'),
|
|
path.join(packageRoot, 'src', 'constants.mjs'),
|
|
path.join(packageRoot, 'torch-config.json')
|
|
];
|
|
|
|
const isAllowed = allowedPaths.some(allowedPath => {
|
|
const rel = path.relative(allowedPath, filePath);
|
|
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
});
|
|
|
|
if (!isAllowed) {
|
|
res.writeHead(403, SECURITY_HEADERS);
|
|
res.end('Forbidden');
|
|
return;
|
|
}
|
|
|
|
// If directory, try index.html
|
|
let fileStat = await statSafe(filePath);
|
|
if (fileStat && fileStat.isDirectory()) {
|
|
filePath = path.join(filePath, 'index.html');
|
|
fileStat = await statSafe(filePath);
|
|
}
|
|
|
|
// Check if file exists and is a file
|
|
if (!fileStat || !fileStat.isFile()) {
|
|
res.writeHead(404, SECURITY_HEADERS);
|
|
res.end('Not Found');
|
|
return;
|
|
}
|
|
|
|
// MIME types
|
|
const extname = path.extname(filePath);
|
|
let contentType = 'text/plain';
|
|
switch (extname) {
|
|
case '.html': contentType = 'text/html'; break;
|
|
case '.js': contentType = 'text/javascript'; break;
|
|
case '.mjs': contentType = 'text/javascript'; break;
|
|
case '.css': contentType = 'text/css'; break;
|
|
case '.json': contentType = 'application/json'; break;
|
|
case '.png': contentType = 'image/png'; break;
|
|
case '.jpg': contentType = 'image/jpeg'; break;
|
|
case '.svg': contentType = 'image/svg+xml'; break;
|
|
case '.ico': contentType = 'image/x-icon'; break;
|
|
case '.md': contentType = 'text/markdown'; break;
|
|
}
|
|
|
|
res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': contentType });
|
|
fs.createReadStream(filePath).pipe(res);
|
|
} catch (err) {
|
|
console.error('Dashboard Server Error:', err);
|
|
if (!res.headersSent) {
|
|
res.writeHead(500, SECURITY_HEADERS);
|
|
res.end('Internal Server Error');
|
|
}
|
|
}
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
server.listen(port, host, () => {
|
|
const listenUrl = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/dashboard/`;
|
|
console.log(`Dashboard running at ${listenUrl}`);
|
|
if (auth) {
|
|
console.log('Authentication: enabled (Basic Auth)');
|
|
} else {
|
|
console.warn('Authentication: DISABLED (Dashboard is public)');
|
|
}
|
|
console.log(`Serving files from ${packageRoot}`);
|
|
console.log(`Using configuration from ${process.cwd()}`);
|
|
resolve(server);
|
|
});
|
|
server.on('error', reject);
|
|
});
|
|
}
|