mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 08:01:46 -03:00
Fix community link order: TG Group > TG Channel > Chat
Consistent order across all files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
800
local-dev/debug-wrapper.js
Normal file
800
local-dev/debug-wrapper.js
Normal file
@@ -0,0 +1,800 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user