const express = require('express'); const rateLimit = require('express-rate-limit'); const path = require('path'); const bodyParser = require('body-parser'); const API = require('./src/js/index.js'); const demoTracker = require('./src/js/demoTracker.js'); const { logger } = require('./src/js/logger.js'); const favicon = require('serve-favicon'); const fs = require('fs'); const app = express(); const port = process.env.PORT || 3512; const { processJsonOutput, sanitizeHeaders, timeoutPromise, ensureLogin, handleApiError, activeSessions, } = require('./src/js/serverUtils.js'); require('./src/js/utils.js'); app.set('trust proxy', true); // Middleware app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' })); app.use(express.static(__dirname)); app.use(express.static(path.join(__dirname, 'public'))); app.use('/images', express.static(path.join(__dirname, 'src/images'))); app.use(favicon(path.join(__dirname, 'src', 'images', 'favicon.ico'))); app.use( bodyParser.json({ limit: '10mb', verify: (req, res, buf) => { req.rawBody = buf.toString(); }, }) ); app.use(demoTracker.demoModeMiddleware); // Set up demo mode cleanup interval setInterval( () => demoTracker.demoModeRequestTracker.cleanupExpiredEntries(), 3600000 ); // Clean up every hour app.get('/health', (req, res) => { const clientIP = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for']?.split(',')[0] || req.ip || req.connection.remoteAddress; const isDemoMode = demoTracker.isDemoModeActive(); res.json({ status: 'ok', timestamp: global.Utils.toIsoString(new Date()), demoMode: isDemoMode, requestsRemaining: isDemoMode ? demoTracker.demoModeRequestTracker.maxRequestsPerHour - (demoTracker.demoModeRequestTracker.requests.get(clientIP)?.count || 0) : null, }); }); // Serve the main HTML file app.get('/', async (req, res) => { // Check demo mode query parameter if (req.query.demo === 'true') { await demoTracker.initializeDemoMode(true); } else if (req.query.demo === 'false') { await demoTracker.initializeDemoMode(false); } res.sendFile(path.join(__dirname, 'src', 'index.html')); }); // Demo mode route - enables demo mode when visited app.get('/demo', async (req, res) => { const success = await demoTracker.initializeDemoMode(true); if (success) { logger.info('Demo mode activated via /demo route'); res.redirect('/?demo=true'); } else { res .status(500) .send('Failed to enable demo mode. Check server logs for details.'); } }); // Demo mode deactivation route app.get('/demo/disable', async (req, res) => { await demoTracker.initializeDemoMode(false); logger.info('Demo mode deactivated via /demo/disable route'); res.redirect('/?demo=false'); }); // API endpoint to fetch stats app.post('/api/stats', async (req, res) => { logger.debug('Received request for /api/stats'); logger.debug( `Request IP: ${ req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress }` ); logger.debug( `Request JSON: ${JSON.stringify({ username: req.body.username, platform: req.body.platform, game: req.body.game, apiCall: req.body.apiCall, sanitize: req.body.sanitize, replaceKeys: req.body.replaceKeys, })}` ); try { let { username, ssoToken, platform, game, apiCall, sanitize, replaceKeys } = req.body; const defaultToken = demoTracker.getDefaultSsoToken(); if (!ssoToken && defaultToken) { ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken) { return res.status(200).json({ status: 'error', message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } /* logger.debug( `Request details - Username: ${username}, Platform: ${platform}, Game: ${game}, API Call: ${apiCall}` ); logger.debug("=== STATS REQUEST ==="); logger.debug(`Username: ${username || 'Not provided'}`); logger.debug(`Platform: ${platform}`); logger.debug(`Game: ${game}`); logger.debug(`API Call: ${apiCall}`); logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("====================="); */ /* if (!ssoToken) { return res.status(400).json({ error: 'SSO Token is required' }); } */ // For mapList, username is not required if (apiCall !== 'mapList' && !username) { return res.status(400).json({ error: 'Username is required' }); } // Clear previous session if it exists if (activeSessions.has(ssoToken)) { logger.debug('Clearing previous session'); activeSessions.delete(ssoToken); } // Login with the provided SSO token try { await ensureLogin(ssoToken); } catch (loginError) { logger.error('Login error:', loginError); return res.status(200).json({ status: 'error', error_type: 'LoginError', message: 'SSO token login failed', details: loginError.message || 'Unknown login error', timestamp: global.Utils.toIsoString(new Date()), }); } // Create a wrapped function for each API call to handle timeout const fetchWithTimeout = async (apiFn) => { return Promise.race([ apiFn(), timeoutPromise(30000), // 30 second timeout ]); }; // Check if the platform is valid for the game const requiresUno = ['mw2', 'wz2', 'mw3', 'wzm'].includes(game); if (requiresUno && platform !== 'uno' && apiCall !== 'mapList') { logger.warn(`${game} requires Uno ID`); return res.status(200).json({ status: 'error', message: `${game} requires Uno ID (numerical ID)`, timestamp: global.Utils.toIsoString(new Date()), }); } try { logger.debug( `Attempting to fetch ${game} data for ${username} on ${platform}` ); let data; if (apiCall === 'fullData') { // Fetch lifetime stats based on game switch (game) { case 'mw': data = await fetchWithTimeout(() => API.ModernWarfare.fullData(username, platform) ); break; case 'wz': data = await fetchWithTimeout(() => API.Warzone.fullData(username, platform) ); break; case 'mw2': data = await fetchWithTimeout(() => API.ModernWarfare2.fullData(username) ); break; case 'wz2': data = await fetchWithTimeout(() => API.Warzone2.fullData(username) ); break; case 'mw3': data = await fetchWithTimeout(() => API.ModernWarfare3.fullData(username) ); break; case 'cw': data = await fetchWithTimeout(() => API.ColdWar.fullData(username, platform) ); break; case 'vg': data = await fetchWithTimeout(() => API.Vanguard.fullData(username, platform) ); break; case 'wzm': data = await fetchWithTimeout(() => API.WarzoneMobile.fullData(username) ); break; default: return res.status(200).json({ status: 'error', message: 'Invalid game selected', timestamp: global.Utils.toIsoString(new Date()), }); } } else if (apiCall === 'combatHistory') { // Fetch recent match history based on game switch (game) { case 'mw': data = await fetchWithTimeout(() => API.ModernWarfare.combatHistory(username, platform) ); break; case 'wz': data = await fetchWithTimeout(() => API.Warzone.combatHistory(username, platform) ); break; case 'mw2': data = await fetchWithTimeout(() => API.ModernWarfare2.combatHistory(username) ); break; case 'wz2': data = await fetchWithTimeout(() => API.Warzone2.combatHistory(username) ); break; case 'mw3': data = await fetchWithTimeout(() => API.ModernWarfare3.combatHistory(username) ); break; case 'cw': data = await fetchWithTimeout(() => API.ColdWar.combatHistory(username, platform) ); break; case 'vg': data = await fetchWithTimeout(() => API.Vanguard.combatHistory(username, platform) ); break; case 'wzm': data = await fetchWithTimeout(() => API.WarzoneMobile.combatHistory(username) ); break; default: return res.status(200).json({ status: 'error', message: 'Invalid game selected', timestamp: global.Utils.toIsoString(new Date()), }); } } else if (apiCall === 'mapList') { // Fetch map list (only valid for MW) if (game === 'mw') { data = await fetchWithTimeout(() => API.ModernWarfare.mapList(platform) ); } else { return res.status(200).json({ status: 'error', message: 'Map list is only available for Modern Warfare', timestamp: global.Utils.toIsoString(new Date()), }); } } logger.debug('Data fetched successfully'); logger.debug(`Response Size: ~${JSON.stringify(data).length / 1024} KB`); logger.debug(`Response Time: ${global.Utils.toIsoString(new Date())}`); // Safely handle the response data if (!data) { logger.warn('No data returned from API'); return res.json({ status: 'partial_success', message: 'No data returned from API, but no error thrown', data: null, timestamp: global.Utils.toIsoString(new Date()), }); } // logger.debug("Returning data to client"); const { sanitize, replaceKeys } = req.body; demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), timestamp: global.Utils.toIsoString(new Date()), }); } catch (apiError) { return handleApiError(apiError, res); } } catch (serverError) { logger.error('Server Error:', serverError); // Return a structured error response even for unexpected errors return res.status(200).json({ status: 'server_error', message: 'The server encountered an unexpected error', error_details: serverError.message || 'Unknown server error', timestamp: global.Utils.toIsoString(new Date()), }); } }); // API endpoint to fetch recent matches app.post('/api/matches', async (req, res) => { logger.debug('Received request for /api/matches'); logger.debug( `Request IP: ${ req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress }` ); logger.debug( `Request JSON: ${JSON.stringify({ username: req.body.username, platform: req.body.platform, game: req.body.game, sanitize: req.body.sanitize, replaceKeys: req.body.replaceKeys, })}` ); try { let { username, ssoToken, platform, game, sanitize, replaceKeys } = req.body; /* logger.debug( `Request details - Username: ${username}, Platform: ${platform}, Game: ${game}` ); logger.debug("=== MATCHES REQUEST ==="); logger.debug(`Username: ${username}`); logger.debug(`Platform: ${platform}`); logger.debug(`Game: ${game}`); logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("========================"); */ const defaultToken = demoTracker.getDefaultSsoToken(); if (!ssoToken && defaultToken) { ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken) { return res.status(200).json({ status: 'error', message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } if (!username) { return res.status(400).json({ error: 'Username is required' }); } if (!username || !ssoToken) { return res .status(400) .json({ error: 'Username and SSO Token are required' }); } try { await ensureLogin(ssoToken); } catch (loginError) { return res.status(200).json({ status: 'error', error_type: 'LoginError', message: 'SSO token login failed', details: loginError.message || 'Unknown login error', timestamp: global.Utils.toIsoString(new Date()), }); } // Create a wrapped function for each API call to handle timeout const fetchWithTimeout = async (apiFn) => { return Promise.race([ apiFn(), timeoutPromise(30000), // 30 second timeout ]); }; try { logger.debug( `Attempting to fetch combat history for ${username} on ${platform}` ); let data; // Check if the platform is valid for the game const requiresUno = ['mw2', 'wz2', 'mw3', 'wzm'].includes(game); if (requiresUno && platform !== 'uno') { return res.status(200).json({ status: 'error', message: `${game} requires Uno ID (numerical ID)`, timestamp: global.Utils.toIsoString(new Date()), }); } // Fetch combat history based on game switch (game) { case 'mw': data = await fetchWithTimeout(() => API.ModernWarfare.combatHistory(username, platform) ); break; case 'wz': data = await fetchWithTimeout(() => API.Warzone.combatHistory(username, platform) ); break; case 'mw2': data = await fetchWithTimeout(() => API.ModernWarfare2.combatHistory(username) ); break; case 'wz2': data = await fetchWithTimeout(() => API.Warzone2.combatHistory(username) ); break; case 'mw3': data = await fetchWithTimeout(() => API.ModernWarfare3.combatHistory(username) ); break; case 'cw': data = await fetchWithTimeout(() => API.ColdWar.combatHistory(username, platform) ); break; case 'vg': data = await fetchWithTimeout(() => API.Vanguard.combatHistory(username, platform) ); break; case 'wzm': data = await fetchWithTimeout(() => API.WarzoneMobile.combatHistory(username) ); break; default: return res.status(200).json({ status: 'error', message: 'Invalid game selected', timestamp: global.Utils.toIsoString(new Date()), }); } const { sanitize, replaceKeys } = req.body; demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), timestamp: global.Utils.toIsoString(new Date()), }); } catch (apiError) { return handleApiError(apiError, res); } } catch (serverError) { return res.status(200).json({ status: 'server_error', message: 'The server encountered an unexpected error', error_details: serverError.message || 'Unknown server error', timestamp: global.Utils.toIsoString(new Date()), }); } }); // API endpoint to fetch match info app.post('/api/matchInfo', async (req, res) => { logger.debug('Received request for /api/matchInfo'); logger.debug( `Request IP: ${ req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress }` ); logger.debug( `Request JSON: ${JSON.stringify({ matchId: req.body.matchId, platform: req.body.platform, game: req.body.game, sanitize: req.body.sanitize, replaceKeys: req.body.replaceKeys, })}` ); try { let { matchId, ssoToken, platform, game, sanitize, replaceKeys } = req.body; /* logger.debug( `Request details - Match ID: ${matchId}, Platform: ${platform}, Game: ${game}` ); logger.debug("=== MATCH INFO REQUEST ==="); logger.debug(`Match ID: ${matchId}`); logger.debug(`Platform: ${platform}`); logger.debug(`Game: ${game}`); logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("=========================="); */ const defaultToken = demoTracker.getDefaultSsoToken(); if (!ssoToken && defaultToken) { ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken) { return res.status(200).json({ status: 'error', message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } if (!matchId) { return res.status(400).json({ error: 'Match ID is required' }); } if (!matchId || !ssoToken) { return res .status(400) .json({ error: 'Match ID and SSO Token are required' }); } try { await ensureLogin(ssoToken); } catch (loginError) { return res.status(200).json({ status: 'error', error_type: 'LoginError', message: 'SSO token login failed', details: loginError.message || 'Unknown login error', timestamp: global.Utils.toIsoString(new Date()), }); } // Create a wrapped function for each API call to handle timeout const fetchWithTimeout = async (apiFn) => { return Promise.race([ apiFn(), timeoutPromise(30000), // 30 second timeout ]); }; try { logger.debug(`Attempting to fetch match info for match ID: ${matchId}`); let data; // Fetch match info based on game switch (game) { case 'mw': data = await fetchWithTimeout(() => API.ModernWarfare.matchInfo(matchId, platform) ); break; case 'wz': data = await fetchWithTimeout(() => API.Warzone.matchInfo(matchId, platform) ); break; case 'mw2': data = await fetchWithTimeout(() => API.ModernWarfare2.matchInfo(matchId) ); break; case 'wz2': data = await fetchWithTimeout(() => API.Warzone2.matchInfo(matchId)); break; case 'mw3': data = await fetchWithTimeout(() => API.ModernWarfare3.matchInfo(matchId) ); break; case 'cw': data = await fetchWithTimeout(() => API.ColdWar.matchInfo(matchId, platform) ); break; case 'vg': data = await fetchWithTimeout(() => API.Vanguard.matchInfo(matchId, platform) ); break; case 'wzm': data = await fetchWithTimeout(() => API.WarzoneMobile.matchInfo(matchId) ); break; default: return res.status(200).json({ status: 'error', message: 'Invalid game selected', timestamp: global.Utils.toIsoString(new Date()), }); } const { sanitize, replaceKeys } = req.body; demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), timestamp: global.Utils.toIsoString(new Date()), }); } catch (apiError) { return handleApiError(apiError, res); } } catch (serverError) { return res.status(200).json({ status: 'server_error', message: 'The server encountered an unexpected error', error_details: serverError.message || 'Unknown server error', timestamp: global.Utils.toIsoString(new Date()), }); } }); // API endpoint for user-related API calls app.post('/api/user', async (req, res) => { logger.debug('Received request for /api/user'); logger.debug( `Request IP: ${ req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress }` ); logger.debug( `Request JSON: ${JSON.stringify({ username: req.body.username, platform: req.body.platform, userCall: req.body.userCall, sanitize: req.body.sanitize, replaceKeys: req.body.replaceKeys, })}` ); try { let { username, ssoToken, platform, userCall, sanitize, replaceKeys } = req.body; /* logger.debug( `Request details - Username: ${username}, Platform: ${platform}, User Call: ${userCall}` ); logger.debug("=== USER DATA REQUEST ==="); logger.debug(`Username: ${username || 'Not provided'}`); logger.debug(`Platform: ${platform}`); logger.debug(`User Call: ${userCall}`); logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("========================="); */ const defaultToken = demoTracker.getDefaultSsoToken(); if (!ssoToken && defaultToken) { ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken) { return res.status(200).json({ status: 'error', message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } // For eventFeed and identities, username is not required if ( !username && userCall !== 'eventFeed' && userCall !== 'friendFeed' && userCall !== 'identities' ) { return res .status(400) .json({ error: 'Username is required for this API call' }); } try { await ensureLogin(ssoToken); } catch (loginError) { return res.status(200).json({ status: 'error', error_type: 'LoginError', message: 'SSO token login failed', details: loginError.message || 'Unknown login error', timestamp: global.Utils.toIsoString(new Date()), }); } // Create a wrapped function for each API call to handle timeout const fetchWithTimeout = async (apiFn) => { return Promise.race([ apiFn(), timeoutPromise(30000), // 30 second timeout ]); }; try { logger.debug(`Attempting to fetch user data for ${userCall}`); let data; // Fetch user data based on userCall switch (userCall) { case 'codPoints': data = await fetchWithTimeout(() => API.Me.codPoints(username, platform) ); break; case 'connectedAccounts': data = await fetchWithTimeout(() => API.Me.connectedAccounts(username, platform) ); break; case 'eventFeed': data = await fetchWithTimeout(() => API.Me.eventFeed()); break; case 'friendFeed': data = await fetchWithTimeout(() => API.Me.friendFeed(username, platform) ); break; case 'identities': data = await fetchWithTimeout(() => API.Me.loggedInIdentities()); break; case 'friendsList': data = await fetchWithTimeout(() => API.Me.friendsList()); break; // case "settings": // data = await fetchWithTimeout(() => // API.Me.settings(username, platform) // ); // break; default: return res.status(200).json({ status: 'error', message: 'Invalid user API call selected', timestamp: global.Utils.toIsoString(new Date()), }); } const { sanitize, replaceKeys } = req.body; demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), timestamp: global.Utils.toIsoString(new Date()), }); } catch (apiError) { return handleApiError(apiError, res); } } catch (serverError) { return res.status(200).json({ status: 'server_error', message: 'The server encountered an unexpected error', error_details: serverError.message || 'Unknown server error', timestamp: global.Utils.toIsoString(new Date()), }); } }); // API endpoint for fuzzy search app.post('/api/search', async (req, res) => { logger.debug('Received request for /api/search'); logger.debug( `Request IP: ${ req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress }` ); logger.debug( `Request JSON: ${JSON.stringify({ username: req.body.username, platform: req.body.platform, sanitize: req.body.sanitize, replaceKeys: req.body.replaceKeys, })}` ); try { let { username, ssoToken, platform, sanitize, replaceKeys } = req.body; /* logger.debug( `Request details - Match ID: ${matchId}, Platform: ${platform}, Game: ${game}` ); logger.debug("=== MATCH INFO REQUEST ==="); logger.debug(`Match ID: ${matchId}`); logger.debug(`Platform: ${platform}`); logger.debug(`Game: ${game}`); logger.debug(`Processing Options - Sanitize: ${sanitize}, Replace Keys: ${replaceKeys}`); logger.debug("=========================="); */ const defaultToken = demoTracker.getDefaultSsoToken(); if (!ssoToken && defaultToken) { ssoToken = defaultToken; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken) { return res.status(200).json({ status: 'error', message: 'SSO Token is required', timestamp: global.Utils.toIsoString(new Date()), }); } if (!username) { return res.status(400).json({ error: 'Username is required' }); } if (!username || !ssoToken) { return res .status(400) .json({ error: 'Username and SSO Token are required' }); } try { await ensureLogin(ssoToken); } catch (loginError) { return res.status(200).json({ status: 'error', error_type: 'LoginError', message: 'SSO token login failed', details: loginError.message || 'Unknown login error', timestamp: global.Utils.toIsoString(new Date()), }); } // Create a wrapped function for each API call to handle timeout const fetchWithTimeout = async (apiFn) => { return Promise.race([ apiFn(), timeoutPromise(30000), // 30 second timeout ]); }; try { logger.debug( `Attempting fuzzy search for ${username} on platform ${platform}` ); const data = await fetchWithTimeout(() => API.Misc.search(username, platform) ); const { sanitize, replaceKeys } = req.body; demoTracker.incrementDemoCounter(req, ssoToken); return res.json({ // status: "success", data: processJsonOutput(data, { sanitize, replaceKeys }), timestamp: global.Utils.toIsoString(new Date()), // link: "Stats pulled using codtracker.rimmyscorner.com", }); } catch (apiError) { return handleApiError(apiError, res); } } catch (serverError) { return res.status(200).json({ status: 'server_error', message: 'The server encountered an unexpected error', error_details: serverError.message || 'Unknown server error', timestamp: global.Utils.toIsoString(new Date()), }); } }); // Improved logging endpoint app.post('/api/log', (req, res) => { const clientIP = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for']?.split(',')[0] || req.ip || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; const referer = req.headers['referer']; const origin = req.headers['origin']; let logData; try { // Handle data whether it comes as already parsed JSON or as a string if (typeof req.body === 'string') { logData = JSON.parse(req.body); } else if (req.body && typeof req.body === 'object') { logData = req.body; } else { // If no parsable data found, create a basic log entry logData = { eventType: 'unknown', timestamp: global.Utils.toIsoString(new Date()), }; } // Enrich log with server-side data const enrichedLog = { ...logData, meta: { clientIP, userAgent, referer, origin, requestHeaders: sanitizeHeaders(req.headers), serverTimestamp: global.Utils.toIsoString(new Date()), requestId: req.id || Math.random().toString(36).substring(2, 15), }, }; // Use the dedicated user activity logger logger.userActivity(enrichedLog.eventType || 'unknown', enrichedLog); } catch (error) { logger.error('Error processing log data', { error: error.message, rawBody: typeof req.body === 'object' ? '[Object]' : req.body, }); } // Always return 200 to avoid client-side errors res.status(200).send(); }); // Database storage function /* function storeLogInDatabase(logData) { // Example with MongoDB db.collection('user_logs').insertOne(logData) .catch(err => logger.error('Failed to store log in database:', err)); } */ // Start the server app.listen(port, () => { logger.info(`Server running on http://localhost:${port}`); });