mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 06:51:47 -03:00
Consistent order across all files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
801 lines
24 KiB
JavaScript
801 lines
24 KiB
JavaScript
/**
|
|
* Debug Wrapper Entry Point
|
|
* Wraps the existing hytale-auth-server with debug capabilities
|
|
*/
|
|
|
|
const http = require('http');
|
|
const https = require('https');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
// Only load debug functionality if DEBUG_MODE is enabled
|
|
const DEBUG_MODE = process.env.DEBUG_MODE === 'true';
|
|
|
|
const DATA_DIR = process.env.DATA_DIR || '/app/data';
|
|
const LOG_FILE = path.join(DATA_DIR, 'debug-requests.jsonl');
|
|
const SSL_DIR = path.join(DATA_DIR, 'ssl');
|
|
const SSL_KEY = path.join(SSL_DIR, 'server.key');
|
|
const SSL_CERT = path.join(SSL_DIR, 'server.crt');
|
|
const HTTPS_PORT = parseInt(process.env.HTTPS_PORT || '3443');
|
|
|
|
/**
|
|
* Generate self-signed SSL certificates if they don't exist
|
|
*/
|
|
function ensureSSLCertificates() {
|
|
if (!fs.existsSync(SSL_DIR)) {
|
|
fs.mkdirSync(SSL_DIR, { recursive: true });
|
|
}
|
|
|
|
if (!fs.existsSync(SSL_KEY) || !fs.existsSync(SSL_CERT)) {
|
|
console.log('Generating self-signed SSL certificates...');
|
|
try {
|
|
execSync(`openssl req -x509 -newkey rsa:2048 -keyout "${SSL_KEY}" -out "${SSL_CERT}" -days 365 -nodes -subj "/CN=localhost" 2>/dev/null`);
|
|
console.log('SSL certificates generated successfully');
|
|
} catch (err) {
|
|
console.error('Failed to generate SSL certificates:', err.message);
|
|
console.error('HTTPS will not be available. Install openssl or provide certificates manually.');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
try {
|
|
return {
|
|
key: fs.readFileSync(SSL_KEY),
|
|
cert: fs.readFileSync(SSL_CERT)
|
|
};
|
|
} catch (err) {
|
|
console.error('Failed to read SSL certificates:', err.message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// In-memory request storage for debug dashboard
|
|
const capturedRequests = [];
|
|
const MAX_CAPTURED = 500;
|
|
|
|
// Color codes for terminal output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
green: '\x1b[32m',
|
|
blue: '\x1b[34m',
|
|
yellow: '\x1b[33m',
|
|
magenta: '\x1b[35m',
|
|
cyan: '\x1b[36m',
|
|
red: '\x1b[31m'
|
|
};
|
|
|
|
function extractSubdomain(host) {
|
|
if (!host) return null;
|
|
const hostname = host.split(':')[0];
|
|
const match = hostname.match(/^(sessions|account-data|telemetry|tools|oauth\.accounts|accounts)\./);
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
function getSubdomainColor(subdomain) {
|
|
const subColors = {
|
|
'sessions': colors.green,
|
|
'account-data': colors.blue,
|
|
'telemetry': colors.yellow,
|
|
'tools': colors.magenta
|
|
};
|
|
return subColors[subdomain] || colors.cyan;
|
|
}
|
|
|
|
/**
|
|
* Serve the debug dashboard HTML
|
|
*/
|
|
function serveDebugDashboard(res) {
|
|
const html = `<!DOCTYPE html>
|
|
<html><head><title>Debug Dashboard - Local Dev</title>
|
|
<style>
|
|
body{font-family:monospace;background:#1a1a2e;color:#eee;padding:20px;margin:0}
|
|
h1{color:#00d4ff;margin-bottom:10px}
|
|
.info{color:#888;font-size:12px;margin-bottom:20px}
|
|
.section{background:#16213e;padding:15px;margin:10px 0;border-radius:8px}
|
|
.req{background:#0f3460;padding:10px;margin:5px 0;border-radius:4px;font-size:12px;border-left:3px solid #00d4ff}
|
|
.req.error{border-left-color:#f87171}
|
|
.GET{color:#4ade80}.POST{color:#60a5fa}.DELETE{color:#f87171}.PUT{color:#fbbf24}
|
|
.sub{color:#c084fc;font-weight:bold}
|
|
.status{padding:2px 6px;border-radius:3px;font-size:11px}
|
|
.status.ok{background:#166534;color:#4ade80}
|
|
.status.error{background:#7f1d1d;color:#f87171}
|
|
pre{background:#0a0a1a;padding:10px;overflow-x:auto;font-size:11px;border-radius:4px;max-height:300px;overflow-y:auto}
|
|
button{background:#00d4ff;color:#000;border:none;padding:8px 16px;cursor:pointer;margin:4px;border-radius:4px;font-weight:bold}
|
|
button:hover{background:#00b8e6}
|
|
button.danger{background:#dc2626}
|
|
button.danger:hover{background:#b91c1c}
|
|
button.paused{background:#fbbf24;color:#000}
|
|
select{padding:8px;background:#0f3460;border:1px solid #00d4ff;color:#fff;border-radius:4px}
|
|
details summary{cursor:pointer;color:#60a5fa;padding:5px 0}
|
|
.timestamp{color:#666;font-size:10px}
|
|
.duration{color:#fbbf24}
|
|
.path{color:#fff}
|
|
#count{margin-left:10px;color:#4ade80}
|
|
#pauseStatus{margin-left:10px;color:#fbbf24;font-weight:bold}
|
|
.controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
</style></head><body>
|
|
<h1>Debug Dashboard - Local Development</h1>
|
|
<div class="info">Capturing all requests to auth server for research purposes</div>
|
|
|
|
<div class="section">
|
|
<div class="controls">
|
|
<button onclick="refresh()">Refresh</button>
|
|
<button id="pauseBtn" onclick="togglePause()">Pause</button>
|
|
<button class="danger" onclick="clearReqs()">Clear All</button>
|
|
<select id="filter" onchange="refresh()">
|
|
<option value="">All Subdomains</option>
|
|
<option value="sessions">sessions</option>
|
|
<option value="account-data">account-data</option>
|
|
<option value="telemetry">telemetry</option>
|
|
<option value="tools">tools</option>
|
|
<option value="main">main (no subdomain)</option>
|
|
</select>
|
|
<span id="count"></span>
|
|
<span id="pauseStatus"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>Subdomain Summary</h2>
|
|
<div id="subs"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>Recent Requests</h2>
|
|
<div id="reqs" style="max-height:600px;overflow-y:auto"></div>
|
|
</div>
|
|
|
|
<script>
|
|
let isPaused = false;
|
|
let refreshInterval = null;
|
|
|
|
async function refresh() {
|
|
const f = document.getElementById('filter').value;
|
|
const r = await fetch('/debug/requests' + (f ? '?subdomain=' + f : ''));
|
|
const d = await r.json();
|
|
document.getElementById('count').textContent = d.total + ' total, ' + d.filtered + ' shown';
|
|
|
|
document.getElementById('reqs').innerHTML = d.requests.map((r, i) => {
|
|
const statusClass = r.response?.statusCode >= 400 ? 'error' : 'ok';
|
|
const reqClass = r.response?.statusCode >= 400 ? 'error' : '';
|
|
return \`<div class="req \${reqClass}">
|
|
<div>
|
|
<span class="timestamp">\${r.timestamp}</span>
|
|
<span class="\${r.method}">\${r.method}</span>
|
|
<span class="sub">\${r.subdomain || 'main'}</span>
|
|
<span class="path">\${r.path}</span>
|
|
<span class="status \${statusClass}">\${r.response?.statusCode || '?'}</span>
|
|
<span class="duration">\${r.response?.duration || 0}ms</span>
|
|
</div>
|
|
<details ontoggle="handleDetailsToggle(this)">
|
|
<summary>Request Details</summary>
|
|
<pre>\${JSON.stringify({
|
|
host: r.host,
|
|
query: r.query,
|
|
body: r.body,
|
|
headers: r.headers
|
|
}, null, 2)}</pre>
|
|
</details>
|
|
<details ontoggle="handleDetailsToggle(this)">
|
|
<summary>Response</summary>
|
|
<pre>\${JSON.stringify(r.response, null, 2)}</pre>
|
|
</details>
|
|
</div>\`;
|
|
}).join('');
|
|
|
|
const s = await fetch('/debug/subdomains');
|
|
const sd = await s.json();
|
|
document.getElementById('subs').innerHTML = Object.entries(sd)
|
|
.map(([k, v]) => '<span class="sub">' + k + '</span>: ' + v.count)
|
|
.join(' | ') || '<em>No requests yet</em>';
|
|
}
|
|
|
|
function handleDetailsToggle(el) {
|
|
// Auto-pause when any details block is opened
|
|
if (el.open && !isPaused) {
|
|
togglePause();
|
|
}
|
|
}
|
|
|
|
function togglePause() {
|
|
isPaused = !isPaused;
|
|
const btn = document.getElementById('pauseBtn');
|
|
const status = document.getElementById('pauseStatus');
|
|
if (isPaused) {
|
|
btn.textContent = 'Resume';
|
|
btn.classList.add('paused');
|
|
status.textContent = '(auto-refresh paused)';
|
|
if (refreshInterval) {
|
|
clearInterval(refreshInterval);
|
|
refreshInterval = null;
|
|
}
|
|
} else {
|
|
btn.textContent = 'Pause';
|
|
btn.classList.remove('paused');
|
|
status.textContent = '';
|
|
refreshInterval = setInterval(autoRefresh, 3000);
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
function autoRefresh() {
|
|
if (!isPaused) {
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
async function clearReqs() {
|
|
if (confirm('Clear all captured requests?')) {
|
|
await fetch('/debug/requests', { method: 'DELETE' });
|
|
refresh();
|
|
}
|
|
}
|
|
|
|
refreshInterval = setInterval(autoRefresh, 3000);
|
|
refresh();
|
|
</script>
|
|
</body></html>`;
|
|
|
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
res.end(html);
|
|
}
|
|
|
|
/**
|
|
* Handle debug API endpoints
|
|
*/
|
|
function handleDebugApi(req, res, urlPath, url) {
|
|
if (urlPath === '/debug' || urlPath === '/debug/') {
|
|
serveDebugDashboard(res);
|
|
return true;
|
|
}
|
|
|
|
if (urlPath === '/debug/requests') {
|
|
if (req.method === 'DELETE') {
|
|
capturedRequests.length = 0;
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ cleared: true }));
|
|
return true;
|
|
}
|
|
|
|
let filtered = capturedRequests;
|
|
const subdomain = url.searchParams.get('subdomain');
|
|
if (subdomain) {
|
|
if (subdomain === 'main') {
|
|
filtered = filtered.filter(r => !r.subdomain);
|
|
} else {
|
|
filtered = filtered.filter(r => r.subdomain === subdomain);
|
|
}
|
|
}
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({
|
|
total: capturedRequests.length,
|
|
filtered: filtered.length,
|
|
requests: filtered.slice(0, 100)
|
|
}));
|
|
return true;
|
|
}
|
|
|
|
if (urlPath === '/debug/subdomains') {
|
|
const summary = {};
|
|
for (const r of capturedRequests) {
|
|
const sub = r.subdomain || 'main';
|
|
if (!summary[sub]) summary[sub] = { count: 0 };
|
|
summary[sub].count++;
|
|
}
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(summary));
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Capture request/response for debugging
|
|
*/
|
|
function captureRequest(req, res, url, startTime) {
|
|
const subdomain = extractSubdomain(req.headers.host);
|
|
const originalEnd = res.end.bind(res);
|
|
let responseBody = null;
|
|
|
|
// Intercept res.end to capture response
|
|
res.end = function(chunk, encoding, callback) {
|
|
if (chunk) {
|
|
try {
|
|
const str = chunk.toString();
|
|
if (str.length > 2000) {
|
|
responseBody = { _truncated: true, _length: str.length, _preview: str.substring(0, 500) };
|
|
} else {
|
|
try {
|
|
responseBody = JSON.parse(str);
|
|
} catch {
|
|
responseBody = str;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
responseBody = { _error: 'Could not capture response' };
|
|
}
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
// Log with colors
|
|
const subColor = getSubdomainColor(subdomain);
|
|
const statusColor = res.statusCode >= 400 ? colors.red : colors.green;
|
|
console.log(`${subColor}[${subdomain || 'main'}]${colors.reset} ${req.method} ${url.pathname} ${statusColor}${res.statusCode}${colors.reset} (${duration}ms)`);
|
|
|
|
// Store in memory
|
|
const entry = {
|
|
timestamp: new Date().toISOString(),
|
|
method: req.method,
|
|
path: url.pathname,
|
|
host: req.headers.host,
|
|
subdomain,
|
|
query: Object.fromEntries(url.searchParams),
|
|
headers: sanitizeHeaders(req.headers),
|
|
response: {
|
|
statusCode: res.statusCode,
|
|
duration,
|
|
body: responseBody
|
|
}
|
|
};
|
|
|
|
capturedRequests.unshift(entry);
|
|
if (capturedRequests.length > MAX_CAPTURED) {
|
|
capturedRequests.pop();
|
|
}
|
|
|
|
// Also write to file
|
|
try {
|
|
fs.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
|
|
} catch (e) {
|
|
// Ignore file write errors
|
|
}
|
|
|
|
return originalEnd(chunk, encoding, callback);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sanitize headers for logging (remove sensitive data)
|
|
*/
|
|
function sanitizeHeaders(headers) {
|
|
const sanitized = { ...headers };
|
|
// Keep Authorization header but might want to truncate in future
|
|
return sanitized;
|
|
}
|
|
|
|
/**
|
|
* Main entry point - wraps the existing server
|
|
*/
|
|
async function main() {
|
|
if (DEBUG_MODE) {
|
|
console.log('\x1b[33m=== DEBUG MODE ENABLED ===\x1b[0m');
|
|
console.log(`Debug dashboard: http://localhost:${process.env.PORT || 3000}/debug`);
|
|
console.log(`Request log: ${LOG_FILE}`);
|
|
console.log('');
|
|
}
|
|
|
|
// Load the original app module
|
|
const app = require('./app');
|
|
|
|
// If not debug mode, just run normally
|
|
if (!DEBUG_MODE) {
|
|
app.run();
|
|
return;
|
|
}
|
|
|
|
// In debug mode, we need to wrap the request handler
|
|
// Re-implement the server startup with debug wrapper
|
|
const config = require('./config');
|
|
const { connect: connectRedis, isConnected } = require('./services/redis');
|
|
const assets = require('./services/assets');
|
|
|
|
console.log('=== Hytale Auth Server (Debug Mode) ===');
|
|
console.log(`Domain: ${config.domain}`);
|
|
console.log(`Data directory: ${config.dataDir}`);
|
|
|
|
// Pre-load cosmetics
|
|
assets.preloadCosmetics();
|
|
|
|
// Connect to Redis
|
|
await connectRedis();
|
|
|
|
// Create the original request handler reference
|
|
const originalHandler = require('./app').handleRequest || null;
|
|
|
|
// Since handleRequest is not exported, we need a different approach
|
|
// Load all the modules manually
|
|
const middleware = require('./middleware');
|
|
const routes = require('./routes');
|
|
const { sendJson } = require('./utils/response');
|
|
const auth = require('./services/auth');
|
|
const crypto = require('crypto');
|
|
|
|
// Recreate the request handler with debug wrapping
|
|
async function debugWrappedHandler(req, res) {
|
|
const startTime = Date.now();
|
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
const urlPath = url.pathname;
|
|
|
|
// Skip debug endpoints from capture (but still handle them)
|
|
if (urlPath.startsWith('/debug')) {
|
|
if (handleDebugApi(req, res, urlPath, url)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Skip favicon and health from debug capture
|
|
const skipCapture = urlPath === '/favicon.ico' || urlPath === '/health';
|
|
|
|
// Capture the request/response if not skipped
|
|
if (!skipCapture) {
|
|
captureRequest(req, res, url, startTime);
|
|
}
|
|
|
|
// Now delegate to original handling logic
|
|
// CORS headers
|
|
middleware.corsHeaders(res);
|
|
|
|
if (req.method === 'OPTIONS') {
|
|
middleware.handleOptions(req, res);
|
|
return;
|
|
}
|
|
|
|
// Handle binary uploads
|
|
const headCacheMatch = urlPath.match(/^\/avatar\/([^/]+)\/head-cache$/);
|
|
if (headCacheMatch && req.method === 'POST') {
|
|
routes.avatar.handleAvatarRoutes(req, res, urlPath, {});
|
|
return;
|
|
}
|
|
|
|
// Parse JSON body
|
|
const body = await middleware.parseBody(req);
|
|
|
|
// Store body for debug capture
|
|
if (!skipCapture && capturedRequests.length > 0 && !capturedRequests[0].body) {
|
|
capturedRequests[0].body = body;
|
|
}
|
|
|
|
// Extract user context
|
|
const { uuid, name, tokenScope } = middleware.extractUserContext(body, req.headers);
|
|
|
|
// Route the request (copied from app.js to avoid modification)
|
|
await routeRequestDebug(req, res, url, urlPath, body, uuid, name, tokenScope, middleware, routes, sendJson, auth, crypto);
|
|
}
|
|
|
|
// Create HTTP server with debug wrapper
|
|
const server = http.createServer(debugWrappedHandler);
|
|
server.listen(config.port, '0.0.0.0', () => {
|
|
console.log(`HTTP Server running on port ${config.port}`);
|
|
console.log(`Redis: ${isConnected() ? 'connected' : 'NOT CONNECTED'}`);
|
|
console.log(`\n\x1b[36mDebug Dashboard: http://localhost:${config.port}/debug\x1b[0m`);
|
|
});
|
|
|
|
// Create HTTPS server if certificates are available
|
|
const sslOptions = ensureSSLCertificates();
|
|
if (sslOptions) {
|
|
const httpsServer = https.createServer(sslOptions, debugWrappedHandler);
|
|
httpsServer.listen(HTTPS_PORT, '0.0.0.0', () => {
|
|
console.log(`\x1b[32mHTTPS Server running on port ${HTTPS_PORT}\x1b[0m`);
|
|
console.log(`\x1b[33mNote: Self-signed cert - add to keychain or use NODE_TLS_REJECT_UNAUTHORIZED=0\x1b[0m\n`);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Route request - copied from app.js to avoid modifying original
|
|
*/
|
|
async function routeRequestDebug(req, res, url, urlPath, body, uuid, name, tokenScope, middleware, routes, sendJson, auth, crypto) {
|
|
const headers = req.headers;
|
|
|
|
// Avatar viewer routes
|
|
if (urlPath.startsWith('/avatar/')) {
|
|
await routes.avatar.handleAvatarRoutes(req, res, urlPath, body);
|
|
return;
|
|
}
|
|
|
|
// Customizer route
|
|
if (urlPath.startsWith('/customizer')) {
|
|
routes.avatar.handleCustomizerRoute(req, res, urlPath);
|
|
return;
|
|
}
|
|
|
|
// Cosmetics list API
|
|
if (urlPath === '/cosmetics/list') {
|
|
routes.assets.handleCosmeticsList(req, res);
|
|
return;
|
|
}
|
|
|
|
// Single cosmetic item data API
|
|
if (urlPath.startsWith('/cosmetics/item/')) {
|
|
routes.assets.handleCosmeticItem(req, res, urlPath);
|
|
return;
|
|
}
|
|
|
|
// Static assets route
|
|
if (urlPath.startsWith('/assets/')) {
|
|
routes.assets.handleStaticAssets(req, res, urlPath);
|
|
return;
|
|
}
|
|
|
|
// Asset extraction route
|
|
if (urlPath.startsWith('/asset/')) {
|
|
routes.assets.handleAssetRoute(req, res, urlPath);
|
|
return;
|
|
}
|
|
|
|
// Download route
|
|
if (urlPath.startsWith('/download/')) {
|
|
routes.assets.handleDownload(req, res, urlPath);
|
|
return;
|
|
}
|
|
|
|
// Health check
|
|
if (urlPath === '/health' || urlPath === '/') {
|
|
routes.health.handleHealth(req, res);
|
|
return;
|
|
}
|
|
|
|
// Favicon
|
|
if (urlPath === '/favicon.ico') {
|
|
res.writeHead(204);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// JWKS endpoint
|
|
if (urlPath === '/.well-known/jwks.json' || urlPath === '/jwks.json') {
|
|
routes.health.handleJwks(req, res);
|
|
return;
|
|
}
|
|
|
|
// Server auto-auth
|
|
if (urlPath === '/server/auto-auth') {
|
|
routes.server.handleServerAutoAuth(req, res, body);
|
|
return;
|
|
}
|
|
|
|
// Server game profiles
|
|
if (urlPath === '/server/game-profiles' || urlPath === '/game-profiles') {
|
|
routes.server.handleServerGameProfiles(req, res, headers);
|
|
return;
|
|
}
|
|
|
|
// OAuth device authorization
|
|
if (urlPath === '/oauth2/device/auth') {
|
|
routes.server.handleOAuthDeviceAuth(req, res, body);
|
|
return;
|
|
}
|
|
|
|
// OAuth device verification
|
|
if (urlPath === '/oauth2/device/verify') {
|
|
const query = Object.fromEntries(url.searchParams);
|
|
routes.server.handleOAuthDeviceVerify(req, res, query);
|
|
return;
|
|
}
|
|
|
|
// OAuth token endpoint
|
|
if (urlPath === '/oauth2/token') {
|
|
routes.server.handleOAuthToken(req, res, body);
|
|
return;
|
|
}
|
|
|
|
// Game session endpoints
|
|
if (urlPath === '/game-session/new') {
|
|
routes.session.handleGameSessionNew(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/game-session/refresh') {
|
|
await routes.session.handleGameSessionRefresh(req, res, body, uuid, name, headers);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/game-session/child' || urlPath.includes('/game-session/child')) {
|
|
routes.session.handleGameSessionChild(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
// Authorization grant
|
|
if (urlPath === '/game-session/authorize' || urlPath.includes('/authorize') || urlPath.includes('/auth-grant')) {
|
|
routes.session.handleAuthorizationGrant(req, res, body, uuid, name, headers);
|
|
return;
|
|
}
|
|
|
|
// Token exchange
|
|
if (urlPath === '/server-join/auth-token' || urlPath === '/game-session/exchange' || urlPath.includes('/auth-token')) {
|
|
routes.session.handleTokenExchange(req, res, body, uuid, name, headers);
|
|
return;
|
|
}
|
|
|
|
// Session/Auth endpoints
|
|
if ((urlPath.includes('/session') || urlPath.includes('/child')) && !urlPath.startsWith('/admin')) {
|
|
routes.session.handleSession(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
if (urlPath.includes('/auth')) {
|
|
routes.session.handleAuth(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
if (urlPath.includes('/token')) {
|
|
routes.session.handleToken(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
if (urlPath.includes('/validate') || urlPath.includes('/verify')) {
|
|
routes.session.handleValidate(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
if (urlPath.includes('/refresh')) {
|
|
routes.session.handleRefresh(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
// Account data endpoints
|
|
if (urlPath === '/my-account/game-profile' || urlPath.includes('/game-profile')) {
|
|
await routes.account.handleGameProfile(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/my-account/skin') {
|
|
await routes.account.handleSkin(req, res, body, uuid, name, routes.avatar.invalidateHeadCache);
|
|
return;
|
|
}
|
|
|
|
// Account-data skin endpoint
|
|
if (urlPath.startsWith('/account-data/skin/')) {
|
|
const skinUuid = urlPath.replace('/account-data/skin/', '');
|
|
await routes.account.handleSkin(req, res, body, skinUuid, name, routes.avatar.invalidateHeadCache);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/my-account/cosmetics' || urlPath.includes('/my-account/cosmetics')) {
|
|
routes.account.handleCosmetics(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/my-account/get-launcher-data') {
|
|
routes.account.handleLauncherData(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/my-account/get-profiles') {
|
|
routes.account.handleGetProfiles(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
// Bug reports and feedback
|
|
if (urlPath === '/bugs/create' || urlPath === '/feedback/create') {
|
|
res.writeHead(204);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
// Game session delete
|
|
if (urlPath === '/game-session' && req.method === 'DELETE') {
|
|
await routes.session.handleGameSessionDelete(req, res, headers);
|
|
return;
|
|
}
|
|
|
|
// Admin login
|
|
if (urlPath === '/admin/login' && req.method === 'POST') {
|
|
await routes.admin.handleAdminLogin(req, res, body);
|
|
return;
|
|
}
|
|
|
|
// Admin verify
|
|
if (urlPath === '/admin/verify') {
|
|
const token = headers['x-admin-token'] || url.searchParams.get('token');
|
|
await routes.admin.handleAdminVerify(req, res, token);
|
|
return;
|
|
}
|
|
|
|
// Admin dashboard
|
|
if (urlPath === '/admin' || urlPath === '/admin/') {
|
|
routes.admin.handleAdminDashboard(req, res);
|
|
return;
|
|
}
|
|
|
|
// Test page for head embed
|
|
if (urlPath === '/test/head') {
|
|
routes.avatar.handleTestHeadPage(req, res);
|
|
return;
|
|
}
|
|
|
|
// Protected admin routes
|
|
if (urlPath.startsWith('/admin/')) {
|
|
const validToken = await middleware.verifyAdminAuth(headers);
|
|
if (!validToken) {
|
|
sendJson(res, 401, { error: 'Unauthorized. Please login at /admin' });
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Admin API endpoints
|
|
if (urlPath === '/admin/sessions' || urlPath === '/sessions/active') {
|
|
await routes.admin.handleActiveSessions(req, res);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/admin/stats') {
|
|
await routes.admin.handleAdminStats(req, res);
|
|
return;
|
|
}
|
|
|
|
if (urlPath.startsWith('/admin/servers')) {
|
|
await routes.admin.handleAdminServers(req, res, url);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/admin/search') {
|
|
await routes.admin.handlePlayerSearch(req, res, url);
|
|
return;
|
|
}
|
|
|
|
if (urlPath === '/admin/prerender-queue') {
|
|
await routes.admin.handlePrerenderQueue(req, res);
|
|
return;
|
|
}
|
|
|
|
// Profile lookup by UUID
|
|
if (urlPath.startsWith('/profile/uuid/')) {
|
|
const lookupUuid = urlPath.replace('/profile/uuid/', '');
|
|
await routes.account.handleProfileLookupByUuid(req, res, lookupUuid, headers);
|
|
return;
|
|
}
|
|
|
|
// Profile lookup by username
|
|
if (urlPath.startsWith('/profile/username/')) {
|
|
const lookupUsername = decodeURIComponent(urlPath.replace('/profile/username/', ''));
|
|
await routes.account.handleProfileLookupByUsername(req, res, lookupUsername, headers);
|
|
return;
|
|
}
|
|
|
|
// Profile endpoint
|
|
if (urlPath.includes('/profile') || urlPath.includes('/user') || urlPath.includes('/me')) {
|
|
routes.account.handleProfile(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
// Cosmetics endpoint
|
|
if (urlPath.includes('/cosmetic') || urlPath.includes('/unlocked') || urlPath.includes('/inventory')) {
|
|
routes.account.handleCosmetics(req, res, body, uuid, name);
|
|
return;
|
|
}
|
|
|
|
// Telemetry endpoint
|
|
if (urlPath.includes('/telemetry') || urlPath.includes('/analytics') || urlPath.includes('/event')) {
|
|
sendJson(res, 200, { success: true, received: true });
|
|
return;
|
|
}
|
|
|
|
// Catch-all for unknown endpoints - important for research!
|
|
console.log(`\x1b[31m[UNKNOWN ENDPOINT]\x1b[0m ${req.method} ${urlPath}`);
|
|
const requestHost = req.headers.host;
|
|
const authGrant = auth.generateAuthorizationGrant(uuid, name, crypto.randomUUID(), null, requestHost);
|
|
const accessToken = auth.generateIdentityToken(uuid, name, null, ['game.base'], requestHost);
|
|
sendJson(res, 200, {
|
|
debug: true,
|
|
message: 'Unknown endpoint captured for research',
|
|
endpoint: urlPath,
|
|
method: req.method,
|
|
identityToken: accessToken,
|
|
sessionToken: auth.generateSessionToken(uuid, requestHost),
|
|
authorizationGrant: authGrant,
|
|
accessToken: accessToken,
|
|
tokenType: 'Bearer',
|
|
user: { uuid, name, premium: true }
|
|
});
|
|
}
|
|
|
|
main().catch(err => {
|
|
console.error('Failed to start debug server:', err);
|
|
process.exit(1);
|
|
});
|