1262 lines
38 KiB
JavaScript
1262 lines
38 KiB
JavaScript
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}`);
|
|
});
|