Spaces:
Running
Running
| const bcrypt = require('bcryptjs'); | |
| const config = require('../config/config'); | |
| const db = require('../config/database'); | |
| const authCookieDB = require('../config/auth_cookies'); | |
| // Middleware para verificar API keys y configurar reintentos | |
| function verifyAPIKey(options = {}) { | |
| // Opciones por defecto | |
| const { recordUsage = true } = options; | |
| return (req, res, next) => { | |
| // Primero verificar si hay una API key en el header Authorization | |
| const authHeader = req.headers.authorization; | |
| let apiKey = null; | |
| if (authHeader && authHeader.startsWith('Bearer ')) { | |
| apiKey = authHeader.substring(7); | |
| } | |
| // Si no hay API key, usar el comportamiento original (AUTH_COOKIE) | |
| if (!apiKey) { | |
| return next(); | |
| } | |
| // Verificar que la API key sea válida | |
| const keyData = db.getAPIKeyByValue(apiKey); | |
| if (!keyData || !keyData.enabled) { | |
| return res.status(401).json({ | |
| error: 'API key inválida o deshabilitada' | |
| }); | |
| } | |
| // Verificar si la API key está bajo rate limit | |
| const rateLimit = db.checkAndApplyRateLimit(apiKey); | |
| if (rateLimit.limited) { | |
| // Construir mensaje según el nivel de rate limit | |
| let message; | |
| switch (rateLimit.level) { | |
| case 1: | |
| message = `Has excedido el límite de solicitudes (3 en menos de 2 minutos). Por favor espera ${rateLimit.remainingMinutes} minuto antes de intentar nuevamente.`; | |
| break; | |
| case 2: | |
| message = `Has intentado hacer solicitudes durante un periodo de rate limit. Ahora debes esperar ${rateLimit.remainingMinutes} minutos antes de intentar nuevamente.`; | |
| break; | |
| case 3: | |
| message = `Debido a solicitudes excesivas, ahora debes esperar ${rateLimit.remainingMinutes} minutos antes de intentar nuevamente.`; | |
| break; | |
| default: | |
| message = `Has sido limitado por uso excesivo. Por favor espera ${rateLimit.remainingMinutes} minutos antes de intentar nuevamente.`; | |
| } | |
| return res.status(429).json({ | |
| error: 'Demasiadas solicitudes', | |
| message: message, | |
| retry_after: rateLimit.remainingMinutes * 60 // En segundos | |
| }); | |
| } | |
| // Registrar el uso de la API key solo si se especifica | |
| if (recordUsage) { | |
| db.recordUsage(apiKey); | |
| console.log(`[AUTH] Registrando uso de API key: ${keyData.name}`); | |
| } else { | |
| console.log(`[AUTH] Omitiendo registro de uso para API key: ${keyData.name} (ruta exenta)`); | |
| } | |
| // Agregar información de la key al request | |
| req.apiKey = keyData; | |
| req.isAPIKeyAuth = true; | |
| next(); | |
| }; | |
| } | |
| // Función para realizar reintentos en caso de error | |
| async function retryOnError(fn, maxRetries = 20, rawAuthToken = null) { | |
| let lastError; | |
| let currentAuthToken = rawAuthToken; | |
| const utils = require('../utils/utils.js'); | |
| // Contador de intentos totales para evitar bucles infinitos | |
| let totalAttempts = 0; | |
| const MAX_TOTAL_ATTEMPTS = 50; | |
| for (let attempt = 0; attempt < maxRetries; attempt++) { | |
| try { | |
| const response = await fn(currentAuthToken); | |
| // Verificar si es una respuesta HTTP y si es exitosa (código 200) | |
| if (response && response.status === 200 && typeof response.body !== 'undefined') { | |
| // Intentar leer el primer chunk para verificar si hay rate limit | |
| try { | |
| const reader = response.body.getReader(); | |
| const { value, done } = await reader.read(); | |
| if (!done && value) { | |
| // Convertir el chunk a string y verificar si contiene el mensaje de rate limit | |
| const chunk = Buffer.from(value).toString('hex'); | |
| const { isRateLimited } = utils.chunkToUtf8String(chunk); | |
| if (isRateLimited) { | |
| console.log('[RETRY] Detectado mensaje de rate limit en la respuesta (formato detectado)'); | |
| // Si tenemos un token de autenticación, marcar la cookie como rate-limited | |
| if (currentAuthToken) { | |
| // Buscar la cookie en la base de datos | |
| const keys = currentAuthToken.split(',').map(key => key.trim()); | |
| const processedCurrentToken = utils.processAuthToken(currentAuthToken); | |
| // Buscar la cookie en la base de datos por su valor | |
| for (const cookie of authCookieDB.getAllAuthCookies()) { | |
| const processedCookieValue = utils.processAuthToken(cookie.value); | |
| if (cookie.value === processedCurrentToken || processedCookieValue === processedCurrentToken) { | |
| console.log(`[RETRY] Marcando cookie ${cookie.name} (${cookie.id.substring(0, 8)}...) como rate-limited`); | |
| authCookieDB.markAsRateLimited(cookie.id); | |
| break; | |
| } | |
| } | |
| // Marcar el token actual como fallido | |
| utils.markTokenAsFailed(currentAuthToken); | |
| // Obtener el siguiente token disponible (con delay incorporado) | |
| currentAuthToken = await utils.getNextAuthToken(currentAuthToken); | |
| console.log(`[RETRY] Intento ${attempt + 1}/${maxRetries}: Rotando a siguiente auth_cookie después de rate limit`); | |
| // Crear un nuevo stream con el mismo contenido para devolver | |
| const newStream = new ReadableStream({ | |
| start(controller) { | |
| controller.enqueue(value); | |
| controller.close(); | |
| } | |
| }); | |
| // Incrementar contador de intentos totales | |
| totalAttempts++; | |
| // Verificar si hemos alcanzado el límite máximo de intentos totales | |
| if (totalAttempts > MAX_TOTAL_ATTEMPTS) { | |
| console.log(`[RETRY] Alcanzado el límite máximo de ${MAX_TOTAL_ATTEMPTS} intentos totales, abortando`); | |
| throw new Error('Se alcanzó el límite máximo de intentos totales'); | |
| } | |
| // Reintentar con la nueva cookie | |
| continue; | |
| } | |
| } | |
| // Si no hay rate limit, devolver la respuesta original con un nuevo stream | |
| const newStream = new ReadableStream({ | |
| start(controller) { | |
| controller.enqueue(value); | |
| // Transferir el resto del stream original al nuevo stream | |
| const pump = () => { | |
| reader.read().then(({ value, done }) => { | |
| if (done) { | |
| controller.close(); | |
| return; | |
| } | |
| controller.enqueue(value); | |
| pump(); | |
| }).catch(error => { | |
| controller.error(error); | |
| }); | |
| }; | |
| pump(); | |
| } | |
| }); | |
| // Crear una nueva respuesta con el nuevo stream | |
| return new Response(newStream, { | |
| status: response.status, | |
| statusText: response.statusText, | |
| headers: response.headers | |
| }); | |
| } | |
| } catch (streamError) { | |
| console.error('[RETRY] Error al leer el stream:', streamError); | |
| // Si hay un error al leer el stream, devolver la respuesta original | |
| } | |
| } | |
| // Si no es una respuesta HTTP o no tiene body, devolver la respuesta original | |
| return response; | |
| } catch (error) { | |
| lastError = error; | |
| // Incrementar contador de intentos totales | |
| totalAttempts++; | |
| // Verificar si hemos alcanzado el límite máximo de intentos totales | |
| if (totalAttempts > MAX_TOTAL_ATTEMPTS) { | |
| console.log(`[RETRY] Alcanzado el límite máximo de ${MAX_TOTAL_ATTEMPTS} intentos totales, abortando`); | |
| throw new Error('Se alcanzó el límite máximo de intentos totales'); | |
| } | |
| // Si tenemos un token de autenticación, intentar rotar a la siguiente cookie | |
| if (currentAuthToken) { | |
| // Marcar el token actual como fallido | |
| utils.markTokenAsFailed(currentAuthToken); | |
| // Obtener el siguiente token disponible (con delay incorporado) | |
| currentAuthToken = await utils.getNextAuthToken(currentAuthToken); | |
| console.log(`[RETRY] Intento ${attempt + 1}/${maxRetries} (total: ${totalAttempts}): Rotando a siguiente auth_cookie después de error`); | |
| } else { | |
| console.log(`[RETRY] Intento ${attempt + 1}/${maxRetries} (total: ${totalAttempts}): Reintentando después de error`); | |
| } | |
| // Esperar un poco antes de reintentar (puedes ajustar el tiempo) | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| } | |
| } | |
| // Si llegamos aquí, todos los reintentos fallaron | |
| throw lastError; | |
| } | |
| // Middleware para verificar autenticación del admin | |
| function requireAdminAuth(req, res, next) { | |
| if (!req.session.isAdminAuthenticated) { | |
| if (req.path.startsWith('/api/')) { | |
| return res.status(401).json({ error: 'Acceso no autorizado' }); | |
| } | |
| return res.redirect('/admin/login'); | |
| } | |
| next(); | |
| } | |
| // Función para verificar contraseña de admin | |
| async function verifyAdminPassword(password) { | |
| // Si la contraseña en config no está hasheada, compararla directamente | |
| if (config.adminPassword.length < 60) { | |
| return password === config.adminPassword; | |
| } | |
| // Si está hasheada, usar bcrypt | |
| return await bcrypt.compare(password, config.adminPassword); | |
| } | |
| module.exports = { | |
| verifyAPIKey, | |
| requireAdminAuth, | |
| verifyAdminPassword, | |
| retryOnError | |
| }; | |