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 { logger } = require('./src/js/logger.js'); const favicon = require('serve-favicon'); const app = express(); const port = process.env.PORT || 3512; require('./src/js/utils.js'); app.set('trust proxy', true); // Middleware // app.use(bodyParser.json({ limit: '10mb' })); 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(express.raw({ type: 'application/json', limit: '10mb' })); const fs = require('fs'); let DEFAULT_SSO_TOKEN = ''; // Try to read the token from a file try { const tokenPath = path.join(__dirname, 'token.txt'); if (fs.existsSync(tokenPath)) { DEFAULT_SSO_TOKEN = fs.readFileSync(tokenPath, 'utf8').trim(); logger.info('Default SSO token loaded successfully from token.txt'); } else { logger.warn('token.txt not found, demo mode may not function correctly'); } } catch (error) { logger.error('Error loading token file:', { error: error.message }); } setInterval(() => demoModeRequestTracker.cleanupExpiredEntries(), 3600000); // Clean up every hour // Configure rate limiting for demo mode const demoModeRequestTracker = { requests: new Map(), maxRequestsPerHour: 5, // Adjust as needed resetInterval: 60 * 60 * 1000, // 1 hour in milliseconds isLimited: function (ip) { const now = Date.now(); const userRequests = this.requests.get(ip) || { count: 0, resetTime: now + this.resetInterval, }; // Reset counter if the reset time has passed if (now > userRequests.resetTime) { userRequests.count = 0; userRequests.resetTime = now + this.resetInterval; } return userRequests.count >= this.maxRequestsPerHour; }, incrementCounter: function (ip) { const now = Date.now(); const userRequests = this.requests.get(ip) || { count: 0, resetTime: now + this.resetInterval, }; // Reset counter if the reset time has passed if (now > userRequests.resetTime) { userRequests.count = 1; // Start with 1 for this request userRequests.resetTime = now + this.resetInterval; } else { userRequests.count++; } this.requests.set(ip, userRequests); return userRequests.count; }, cleanupExpiredEntries: function () { const now = Date.now(); for (const [ip, data] of this.requests.entries()) { if (now > data.resetTime) { // Either remove entries completely or reset them to 0 this.requests.delete(ip); } } }, }; // Demo mode middleware const demoModeMiddleware = (req, res, next) => { // Skip non-API routes if (!req.path.startsWith('/api/')) { return next(); } // Get client IP for tracking // const clientIP = req.headers['x-forwarded-for'] || req.ip || req.connection.remoteAddress; const clientIP = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for']?.split(',')[0] || req.ip || req.connection.remoteAddress; // Check if demo mode is active if (DEFAULT_SSO_TOKEN) { // For API endpoints, check rate limit in demo mode if (demoModeRequestTracker.isLimited(clientIP)) { const userRequests = demoModeRequestTracker.requests.get(clientIP); const resetTimeMs = userRequests.resetTime - Date.now(); // Format time as HH:MM:SS const hours = Math.floor(resetTimeMs / (60 * 60 * 1000)); const minutes = Math.floor( (resetTimeMs % (60 * 60 * 1000)) / (60 * 1000) ); const seconds = Math.floor((resetTimeMs % (60 * 1000)) / 1000); const timeFormat = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; logger.warn(`Demo mode rate limit exceeded for IP: ${clientIP}`); return res.status(429).json({ status: 'error', message: `Please try again in ${timeFormat} or use your own SSO token.`, timestamp: global.Utils.toIsoString(new Date()), }); } // Increment the counter // const count = demoModeRequestTracker.incrementCounter(clientIP); // logger.debug(`Demo mode request count for ${clientIP}: ${count}`); } next(); }; function incrementDemoCounter(req, ssoToken) { if (ssoToken === DEFAULT_SSO_TOKEN) { const clientIP = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for']?.split(',')[0] || req.ip || req.connection.remoteAddress; const count = demoModeRequestTracker.incrementCounter(clientIP); logger.debug(`Demo mode request count for ${clientIP}: ${count}`); } } app.use(demoModeMiddleware); app.use((req, res, next) => { if (req.query.demo === 'true' || req.query.demo === '1') { // Enable demo mode for this session if it's not already enabled if (!req.session) { req.session = {}; } req.session.forceDemoMode = true; logger.info('Demo mode enabled via URL parameter'); } else if (req.query.demo === 'false' || req.query.demo === '0') { // Disable demo mode for this session if (req.session) { req.session.forceDemoMode = false; } logger.info('Demo mode disabled via URL parameter'); } next(); }); // Initialize key replacements let keyReplacements = {}; try { const replacementsPath = path.join( __dirname, 'src', 'data', 'replacements.json' ); if (fs.existsSync(replacementsPath)) { const replacementsContent = fs.readFileSync(replacementsPath, 'utf8'); keyReplacements = JSON.parse(replacementsContent); // logger.debug("Replacements loaded successfully"); } else { logger.warn('replacements.json not found, key replacement disabled'); } } catch (error) { logger.error('Error loading replacements file:', { error: error.message }); } const replaceJsonKeys = (obj) => { if (!obj || typeof obj !== 'object') return obj; if (Array.isArray(obj)) { return obj.map((item) => replaceJsonKeys(item)); } const newObj = {}; Object.keys(obj).forEach((key) => { // Replace key if it exists in replacements const newKey = keyReplacements[key] || key; // DEBUG: Log replacements when they happen // if (newKey !== key) { // logger.debug(`Replacing key "${key}" with "${newKey}"`); // } // Also check if the value should be replaced (if it's a string) let value = obj[key]; if (typeof value === 'string' && keyReplacements[value]) { value = keyReplacements[value]; // logger.debug(`Replacing value "${obj[key]}" with "${value}"`); } // Process value recursively if it's an object or array newObj[newKey] = replaceJsonKeys(value); }); return newObj; }; // Utility regex function const sanitizeJsonOutput = (data) => { if (!data) return data; // Convert to string to perform regex operations const jsonString = JSON.stringify(data); // Define regex pattern that matches HTML entities const regexPattern = /<span class=".*?">|<\/span>|">/g; // Replace unwanted patterns const sanitizedString = jsonString.replace(regexPattern, ''); // Parse back to object try { return JSON.parse(sanitizedString); } catch (e) { logger.error('Error parsing sanitized JSON:', e); return data; // Return original data if parsing fails } }; // Replace the processJsonOutput function with this more efficient version const processJsonOutput = ( data, options = { sanitize: true, replaceKeys: true } ) => { // Use a more efficient deep clone approach instead of JSON.parse(JSON.stringify()) function deepClone(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map((item) => deepClone(item)); } const clone = {}; Object.keys(obj).forEach((key) => { clone[key] = deepClone(obj[key]); }); return clone; } // Create a deep copy of the data to avoid reference issues let processedData = deepClone(data); // Apply sanitization if needed if (options.sanitize) { processedData = sanitizeJsonOutput(processedData); } // Apply key replacement if needed if (options.replaceKeys) { processedData = replaceJsonKeys(processedData); } return processedData; }; // Store active sessions to avoid repeated logins const activeSessions = new Map(); // Utility function to create a timeout promise const timeoutPromise = (ms) => { return new Promise((_, reject) => { setTimeout(() => reject(new Error(`Request timed out after ${ms}ms`)), ms); }); }; // Helper function to ensure login const ensureLogin = async (ssoToken) => { if (!activeSessions.has(ssoToken)) { logger.info( `Attempting to login with SSO token: ${ssoToken.substring(0, 5)}...` ); // logger.info(`Attempting to login with SSO token: ${ssoToken}`); const loginResult = await Promise.race([ API.login(ssoToken), timeoutPromise(10000), // 10 second timeout ]); logger.debug(`Login successful: ${JSON.stringify(loginResult)}`); logger.debug(`Session created at: ${global.Utils.toIsoString(new Date())}`); activeSessions.set(ssoToken, new Date()); } else { logger.debug('Using existing session'); } }; // Helper function to handle API errors const handleApiError = (error, res) => { logger.error('API Error:', error); logger.error(`Error Stack: ${error.stack}`); logger.error(`Error Time: ${global.Utils.toIsoString(new Date())}`); // Try to extract more useful information from the error let errorMessage = error.message || 'Unknown API error'; let errorName = error.name || 'ApiError'; // Handle the specific JSON parsing error if (errorName === 'SyntaxError' && errorMessage.includes('JSON')) { logger.debug('JSON parsing error detected'); return res.status(200).json({ status: 'error', message: 'Failed to parse API response. This usually means the SSO token is invalid or expired.', error_type: 'InvalidResponseError', timestamp: global.Utils.toIsoString(new Date()), }); } // Send a more graceful response return res.status(200).json({ status: 'error', message: errorMessage, error_type: errorName, timestamp: global.Utils.toIsoString(new Date()), }); }; // 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; if (!ssoToken && DEFAULT_SSO_TOKEN) { ssoToken = DEFAULT_SSO_TOKEN; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { return res.status(200).json({ status: 'error', message: 'SSO Token is required as demo mode is not active', 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("====================="); */ // 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; 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("========================"); */ if (!username) { return res.status(400).json({ error: 'Username is required' }); } if (!ssoToken && DEFAULT_SSO_TOKEN) { ssoToken = DEFAULT_SSO_TOKEN; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { return res.status(200).json({ status: 'error', message: 'SSO Token is required as demo mode is not active', timestamp: global.Utils.toIsoString(new Date()), }); } 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; 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("=========================="); */ if (!matchId) { return res.status(400).json({ error: 'Match ID is required' }); } if (!ssoToken && DEFAULT_SSO_TOKEN) { ssoToken = DEFAULT_SSO_TOKEN; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { return res.status(200).json({ status: 'error', message: 'SSO Token is required as demo mode is not active', timestamp: global.Utils.toIsoString(new Date()), }); } 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; 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("========================="); */ // 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' }); } if (!ssoToken && DEFAULT_SSO_TOKEN) { ssoToken = DEFAULT_SSO_TOKEN; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { return res.status(200).json({ status: 'error', message: 'SSO Token is required as demo mode is not active', timestamp: global.Utils.toIsoString(new Date()), }); } 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; 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("=========================="); */ if (!username) { return res.status(400).json({ error: 'Username is required' }); } if (!ssoToken && DEFAULT_SSO_TOKEN) { ssoToken = DEFAULT_SSO_TOKEN; logger.info('Using default SSO token for demo mode'); } else if (!ssoToken && !DEFAULT_SSO_TOKEN) { return res.status(200).json({ status: 'error', message: 'SSO Token is required as demo mode is not active', timestamp: global.Utils.toIsoString(new Date()), }); } 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; 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(); }); // Helper function to remove sensitive data from headers function sanitizeHeaders(headers) { const safeHeaders = { ...headers }; // Remove potential sensitive information const sensitiveHeaders = ['authorization', 'cookie', 'set-cookie']; sensitiveHeaders.forEach((header) => { if (safeHeaders[header]) { safeHeaders[header] = '[REDACTED]'; } }); return safeHeaders; } // 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)); } */ app.get('/health', (req, res) => { // Check if demo mode is forced via URL parameter const forceDemoMode = req.session && req.session.forceDemoMode === true; const isDemoMode = !!DEFAULT_SSO_TOKEN || forceDemoMode; res.json({ status: 'ok', timestamp: global.Utils.toIsoString(new Date()), demoMode: isDemoMode, requestsRemaining: isDemoMode ? demoModeRequestTracker.maxRequestsPerHour - (demoModeRequestTracker.requests.get(req.ip)?.count || 0) : null, }); }); app.get('/demo', (req, res) => { // Set a cookie or session variable to enable demo mode if (!req.session) { req.session = {}; } req.session.forceDemoMode = true; // Redirect to the main app res.redirect('/'); }); // Serve the main HTML file app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'src', 'index.html')); }); // Start the server app.listen(port, () => { logger.info(`Server running on http://localhost:${port}`); });