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