From b6921dcbde64369507b0d70e1ed52c7eb9a1a688 Mon Sep 17 00:00:00 2001 From: Rim Date: Wed, 16 Apr 2025 16:16:32 -0400 Subject: [PATCH] chore: separate utility code --- app.js | 198 ++-------------------------------------- src/js/serverUtils.js | 204 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+), 190 deletions(-) create mode 100644 src/js/serverUtils.js diff --git a/app.js b/app.js index c7c3df1..033e8a3 100644 --- a/app.js +++ b/app.js @@ -9,6 +9,14 @@ 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); @@ -29,196 +37,6 @@ app.use( ); app.use(demoTracker.demoModeMiddleware); -// 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()), - }); -}; - -// 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; -} - // Set up demo mode cleanup interval setInterval( () => demoTracker.demoModeRequestTracker.cleanupExpiredEntries(), diff --git a/src/js/serverUtils.js b/src/js/serverUtils.js new file mode 100644 index 0000000..0f4da16 --- /dev/null +++ b/src/js/serverUtils.js @@ -0,0 +1,204 @@ +const { logger } = require('./logger.js'); +const API = require('./index.js'); +const path = require('path'); +const fs = require('fs'); +require('./utils.js'); + +// Initialize key replacements +let keyReplacements = {}; + +try { + const replacementsPath = path.join( + __dirname, + '..', + '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()), + }); +}; + +// 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; +} + +module.exports = { + activeSessions, + timeoutPromise, + ensureLogin, + handleApiError, + sanitizeHeaders, + processJsonOutput +}; \ No newline at end of file