UNPKG

@samuraitruong/php-cookie-challenge

Version:

Axios wrapper with automatic cookie challenge detection and processing

245 lines (215 loc) 8.46 kB
/** * Detects if a response contains a cookie challenge * @param {Object} response - Axios response object * @returns {boolean} True if cookie challenge is detected */ function detectCookieChallenge(response) { if (!response || !response.data) { return false; } // Check if response is HTML containing the challenge script const data = typeof response.data === 'string' ? response.data : ''; // Look for the challenge indicators: // 1. Contains slowAES.decrypt // 2. Contains document.cookie with __test // 3. Contains location.href redirect const hasSlowAES = data.includes('slowAES.decrypt'); const hasTestCookie = data.includes('__test='); const hasLocationRedirect = data.includes('location.href'); const hasAesJs = data.includes('/aes.js'); return hasSlowAES && hasTestCookie && hasLocationRedirect && hasAesJs; } /** * Extracts encrypted values from the challenge HTML * @param {string} html - HTML content containing the challenge script * @returns {Object|null} Object with encrypted values (a, b, c) or null if not found */ function extractEncryptedValues(html) { // Extract the encrypted values from the script // Pattern: var a=toNumbers("..."),b=toNumbers("..."),c=toNumbers("..."); // The values might be on one line or multiple lines, with or without spaces const aMatch = html.match(/var\s+a\s*=\s*toNumbers\s*\(\s*"([a-fA-F0-9]+)"\s*\)/); const bMatch = html.match(/var\s+b\s*=\s*toNumbers\s*\(\s*"([a-fA-F0-9]+)"\s*\)/); const cMatch = html.match(/var\s+c\s*=\s*toNumbers\s*\(\s*"([a-fA-F0-9]+)"\s*\)/); if (!aMatch || !bMatch || !cMatch) { // Try alternative pattern - all on one line: var a=toNumbers("..."),b=toNumbers("..."),c=toNumbers("..."); const allMatch = html.match(/var\s+a\s*=\s*toNumbers\s*\(\s*"([a-fA-F0-9]+)"\s*\)\s*,\s*b\s*=\s*toNumbers\s*\(\s*"([a-fA-F0-9]+)"\s*\)\s*,\s*c\s*=\s*toNumbers\s*\(\s*"([a-fA-F0-9]+)"\s*\)/); if (allMatch) { return { a: allMatch[1], b: allMatch[2], c: allMatch[3], }; } return null; } return { a: aMatch[1], b: bMatch[1], c: cMatch[1], }; } /** * Converts hex string to array of numbers * @param {string} hex - Hex string * @returns {Array<number>} Array of numbers */ function toNumbers(hex) { const result = []; hex.replace(/(..)/g, (match) => { result.push(parseInt(match, 16)); }); return result; } /** * Converts array of numbers to hex string * @param {Array<number>} numbers - Array of numbers * @returns {string} Hex string */ function toHex(numbers) { let result = ''; for (let i = 0; i < numbers.length; i++) { result += (numbers[i] < 16 ? '0' : '') + numbers[i].toString(16); } return result.toLowerCase(); } /** * Loads slowAES from the server * @param {Object} client - Axios client instance * @param {string} baseURL - Base URL of the server * @returns {Promise<Object>} slowAES object */ async function loadSlowAES(client, baseURL) { try { const response = await client.get('/aes.js', { baseURL: baseURL, responseType: 'text', }); // Evaluate the slowAES code // The slowAES code defines a global slowAES object // We need to execute it and return the slowAES object const slowAES = {}; // Create a function that executes the code and returns slowAES // The code defines slowAES as a global, so we pass it as a parameter const code = ` ${response.data} return typeof slowAES !== 'undefined' ? slowAES : {}; `; const func = new Function('slowAES', code); const result = func(slowAES); // Return the slowAES object (it should have decrypt method) return result && typeof result.decrypt === 'function' ? result : slowAES; } catch (error) { // If loading fails, try to use a fallback or throw throw new Error(`Failed to load slowAES: ${error.message}`); } } /** * Processes a cookie challenge by making necessary requests and updating cookies * This function will make sequential API calls to get cookies and then retry the original request * @param {Object} response - Axios response object containing the challenge * @param {Object} client - Axios client instance to make follow-up requests * @returns {Promise<Object>} Retried response with cookies handled */ async function processCookieChallenge(response, client) { const originalRequest = response.config; const html = typeof response.data === 'string' ? response.data : String(response.data); // Extract encrypted values from HTML const encrypted = extractEncryptedValues(html); if (!encrypted) { throw new Error('Could not extract encrypted values from challenge'); } // Get base URL from client config or construct from request URL let baseURL = client.defaults?.baseURL; if (!baseURL && originalRequest.url) { try { const url = new URL(originalRequest.url); baseURL = `${url.protocol}//${url.host}`; } catch (e) { // If URL is relative, try to get from request if (originalRequest.baseURL) { baseURL = originalRequest.baseURL; } else { throw new Error('Could not determine base URL for slowAES loading'); } } } if (!baseURL) { throw new Error('Could not determine base URL for slowAES loading'); } // Load slowAES library from the server const slowAES = await loadSlowAES(client, baseURL); // Convert hex strings to number arrays const a = toNumbers(encrypted.a); const b = toNumbers(encrypted.b); const c = toNumbers(encrypted.c); // Decrypt using slowAES (mode 2 = CBC) // The decrypt function signature: decrypt(ciphertext, mode, key, iv) // Check if decrypt exists on slowAES or slowAES.modeOfOperation let decrypted; if (typeof slowAES.decrypt === 'function') { decrypted = slowAES.decrypt(c, 2, a, b); } else if (slowAES.modeOfOperation && typeof slowAES.modeOfOperation.decrypt === 'function') { decrypted = slowAES.modeOfOperation.decrypt(c, 2, a, b); } else { throw new Error('slowAES.decrypt function not found'); } // Convert decrypted array to hex string const cookieValue = toHex(decrypted); // Build the retry URL with ?i=1 parameter const originalUrl = originalRequest.url || ''; let retryUrl; try { const url = new URL(originalUrl, baseURL); url.searchParams.set('i', '1'); retryUrl = url.pathname + url.search; } catch (e) { // If URL parsing fails, append ?i=1 manually retryUrl = originalUrl + (originalUrl.includes('?') ? '&' : '?') + 'i=1'; } // Retry the original request with the cookie and ?i=1 parameter const retryConfig = { ...originalRequest, url: retryUrl, headers: { ...originalRequest.headers, Cookie: `__test=${cookieValue}`, }, }; // Make the retry request const retryResponse = await client.request(retryConfig); return retryResponse; } /** * Creates a cookie challenge interceptor function that can be used with client.interceptors.use() * @param {Object} client - Axios client instance (will be used for sequential API calls and retry) * @param {Function} detectFn - Optional custom detection function (defaults to detectCookieChallenge) * @param {Function} processFn - Optional custom processing function (defaults to processCookieChallenge) * @returns {Function} Interceptor function for use with axios interceptors */ function createCookieChallengeInterceptor(client, detectFn = detectCookieChallenge, processFn = processCookieChallenge) { return async (response) => { // Check if response contains a cookie challenge const isChallenge = detectFn(response); if (isChallenge) { try { // Process the cookie challenge (makes sequential API calls and retries) // The client is captured from closure and will be used for: // 1. Making sequential API calls to get cookies // 2. Retrying the original request with new cookies const processedResponse = await processFn(response, client); return processedResponse; } catch (error) { // If processing fails, log error and rethrow so caller knows it failed console.error('Cookie challenge processing failed:', error.message || error); throw error; } } return response; }; } export { detectCookieChallenge, processCookieChallenge, createCookieChallengeInterceptor, };