/** * 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 = ` Debug Dashboard - Local Dev

Debug Dashboard - Local Development

Capturing all requests to auth server for research purposes

Subdomain Summary

Recent Requests

`; 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); });