mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-07-27 00:52:08 +00:00
1936 lines
56 KiB
JavaScript
1936 lines
56 KiB
JavaScript
/**
|
|
* Proxy management utilities.
|
|
* Extracted from server.js — centralizes all proxy configuration, agent caching,
|
|
* Cloudflare Workers proxy rotation, CORS fallback, and site-specific request helpers.
|
|
*/
|
|
|
|
const axios = require("axios");
|
|
const http = require("http");
|
|
const https = require("https");
|
|
const { SocksProxyAgent } = require("socks-proxy-agent");
|
|
const { HttpProxyAgent } = require("http-proxy-agent");
|
|
const { HttpsProxyAgent } = require("https-proxy-agent");
|
|
const tough = require("tough-cookie");
|
|
const initCycleTLS = require("cycletls");
|
|
|
|
// === PROXY AGENT CACHES ===
|
|
const proxyAgentCache = new Map();
|
|
const darkinoProxyAgentCache = new Map();
|
|
const proxyRotationState = new Map();
|
|
|
|
// Initialize global agent keep-alive with socket limits
|
|
http.globalAgent.keepAlive = true;
|
|
http.globalAgent.maxSockets = 128;
|
|
http.globalAgent.maxFreeSockets = 32;
|
|
https.globalAgent.keepAlive = true;
|
|
https.globalAgent.maxSockets = 128;
|
|
https.globalAgent.maxFreeSockets = 32;
|
|
|
|
// === PROXY CONFIGURATION FLAGS ===
|
|
const ENABLE_DARKINO_PROXY = true; // Passe \u00e0 false pour d\u00e9sactiver le proxy pour Darkino
|
|
const ENABLE_COFLIX_PROXY = true; // Passe \u00e0 false pour d\u00e9sactiver le proxy pour Coflix
|
|
const ENABLE_FRENCH_STREAM_PROXY = true; // Active/d\u00e9sactive le proxy pour French-Stream
|
|
const ENABLE_LECTEURVIDEO_PROXY = true; // Active/d\u00e9sactive le proxy pour LecteurVideo
|
|
const ENABLE_FSTREAM_PROXY = true; // Active/d\u00e9sactive le proxy pour FStream
|
|
const ENABLE_ANIME_PROXY = true; // Active/d\u00e9sactive le proxy pour AnimeSama (via Cloudflare Workers)
|
|
const ENABLE_WIFLIX_PROXY = true; // Active/d\u00e9sactive le proxy pour Wiflix
|
|
const MAX_LECTEURVIDEO_PROXY_ATTEMPTS = 2;
|
|
|
|
// Constante pour l'enhancement Darkino
|
|
const darkiworld_premium = false; // Passe \u00e0 false pour d\u00e9sactiver l'enhancement Darkino
|
|
|
|
// === DARKINO 403 COOLDOWN ===
|
|
// Cooldown de 5 minutes apr\u00e8s une erreur 403 (Cloudflare challenge)
|
|
const DARKINO_403_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
let darkino403CooldownUntil = 0; // Timestamp jusqu'auquel on ne fait plus de requ\u00eates
|
|
|
|
// === CPASMAL CONFIGURATION ===
|
|
const CPASMAL_BASE_URL = "https://www.cpasmal.rip";
|
|
|
|
const cpasmalJar = new tough.CookieJar(null, { rejectPublicSuffixes: false });
|
|
|
|
// Cache pour les agents SOCKS5 Cpasmal (Keep-Alive)
|
|
const cpasmalAgentCache = new Map();
|
|
|
|
function getCpasmalAgent(proxy) {
|
|
if (!proxy) return null;
|
|
const cacheKey = `${proxy.type}:${proxy.host}:${proxy.port}:${proxy.auth}`;
|
|
|
|
if (cpasmalAgentCache.has(cacheKey)) {
|
|
return cpasmalAgentCache.get(cacheKey);
|
|
}
|
|
|
|
// Configuration Keep-Alive optimis\u00e9e
|
|
const agentOpts = {
|
|
keepAlive: true,
|
|
keepAliveMsecs: 15000, // 15s keep-alive
|
|
maxSockets: 50,
|
|
maxFreeSockets: 10,
|
|
timeout: 30000,
|
|
};
|
|
|
|
let agent;
|
|
if (proxy.type === "socks5" || proxy.type === "socks5h") {
|
|
const info = {
|
|
hostname: proxy.host,
|
|
port: proxy.port,
|
|
protocol: "socks:",
|
|
tls: { rejectUnauthorized: false },
|
|
...agentOpts,
|
|
};
|
|
|
|
// Auth handling
|
|
if (proxy.auth) {
|
|
const parts = proxy.auth.split(":");
|
|
info.username = parts[0]; // Correction: userId -> username
|
|
info.password = parts[1];
|
|
}
|
|
|
|
agent = new SocksProxyAgent(info);
|
|
}
|
|
|
|
if (agent) {
|
|
cpasmalAgentCache.set(cacheKey, agent);
|
|
}
|
|
return agent;
|
|
}
|
|
|
|
// Fonction pour faire des requ\u00eates vers Cpasmal avec rotation de proxies SOCKS5
|
|
async function axiosCpasmalRequest(config) {
|
|
const urlStr = config.url || "";
|
|
|
|
// Constante pour Cpasmal Base URL si relative
|
|
const baseURL = CPASMAL_BASE_URL;
|
|
|
|
// Headers par d\u00e9faut
|
|
const defaultHeaders = {
|
|
"User-Agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
|
Accept:
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
"Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8",
|
|
"Accept-Encoding": "gzip, deflate, br",
|
|
Connection: "keep-alive",
|
|
...(config.headers || {}),
|
|
};
|
|
|
|
// Gestion des cookies (r\u00e9cup\u00e9ration manuelle pour compatibilit\u00e9 avec proxy agent)
|
|
if (urlStr || config.baseURL) {
|
|
try {
|
|
// Reconstitution approximative de l'URL pour les cookies
|
|
const targetUrl = urlStr.startsWith("http")
|
|
? urlStr
|
|
: (config.baseURL || baseURL) + urlStr;
|
|
const cookieString = await cpasmalJar.getCookieString(targetUrl);
|
|
if (cookieString) {
|
|
defaultHeaders["Cookie"] = cookieString;
|
|
}
|
|
} catch (err) {
|
|
console.error("[Cpasmal CookieJar] Error getting cookies:", err);
|
|
}
|
|
}
|
|
|
|
// Déterminer les agents proxy à utiliser :
|
|
// 1. _cpasmalAgents fournis (session scopée avec DARKINO_PROXIES HTTP)
|
|
// 2. _cpasmalProxy fourni (SOCKS5 override)
|
|
// 3. Sinon : proxy SOCKS5 aléatoire (legacy)
|
|
let httpAgent, httpsAgent, proxyLabel;
|
|
|
|
if (config._cpasmalAgents) {
|
|
httpAgent = config._cpasmalAgents.httpAgent;
|
|
httpsAgent = config._cpasmalAgents.httpsAgent;
|
|
proxyLabel = config._cpasmalAgents._label || "HTTP proxy";
|
|
} else {
|
|
const proxy =
|
|
config._cpasmalProxy !== undefined
|
|
? config._cpasmalProxy
|
|
: pickRandomProxyOrNone();
|
|
const agent = getCpasmalAgent(proxy);
|
|
httpAgent = agent;
|
|
httpsAgent = agent;
|
|
proxyLabel = proxy ? `${proxy.host}:${proxy.port}` : "Direct";
|
|
}
|
|
|
|
// Nettoyer les champs internes avant de passer à axios
|
|
const { _cpasmalProxy, _cpasmalAgents, ...cleanConfig } = config;
|
|
|
|
try {
|
|
if (process.env.DEBUG_CPASMAL === "true")
|
|
console.time(`[Cpasmal] Request ${urlStr} (Proxy: ${proxyLabel})`);
|
|
|
|
const response = await axios({
|
|
...cleanConfig,
|
|
headers: defaultHeaders,
|
|
httpAgent,
|
|
httpsAgent,
|
|
proxy: false,
|
|
timeout: config.timeout || 5000,
|
|
decompress: true,
|
|
});
|
|
|
|
if (process.env.DEBUG_CPASMAL === "true")
|
|
console.timeEnd(`[Cpasmal] Request ${urlStr} (Proxy: ${proxyLabel})`);
|
|
|
|
// Gestion des cookies (sauvegarde manuelle)
|
|
if (response.headers["set-cookie"]) {
|
|
const cookies = response.headers["set-cookie"];
|
|
const url = response.config.url;
|
|
try {
|
|
if (Array.isArray(cookies)) {
|
|
for (const cookie of cookies) {
|
|
await cpasmalJar.setCookie(cookie, url);
|
|
}
|
|
} else {
|
|
await cpasmalJar.setCookie(cookies, url);
|
|
}
|
|
} catch (err) {
|
|
console.error("[Cpasmal CookieJar] Error setting cookies:", err);
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
// Ajouter infos debug
|
|
error.cpasmalUrl = urlStr;
|
|
error.cpasmalProxy = proxyLabel;
|
|
|
|
// Propager l'erreur immédiatement
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// === CLOUDFLARE WORKERS PROXIES CONFIGURATION ===
|
|
const CLOUDFLARE_WORKERS_PROXIES = (
|
|
process.env.CLOUDFLARE_WORKERS_PROXIES || ""
|
|
)
|
|
.split(",")
|
|
.map((proxy) => proxy.trim())
|
|
.filter(Boolean);
|
|
|
|
// === CACHE POUR LES PROXIES CLOUDFLARE EN ERREUR ===
|
|
// Cache pour m\u00e9moriser les proxies en erreur (429, 500, timeout, etc.)
|
|
// Les proxies en erreur seront ignor\u00e9s pendant PROXY_ERROR_COOLDOWN_MS millisecondes
|
|
const proxyErrorCache = new Map(); // Map<proxyUrl, { errorTime: timestamp, errorCode: number|string, errorCount: number }>
|
|
const PROXY_ERROR_COOLDOWN_MS = 60000; // 60 secondes de cooldown pour un proxy en erreur
|
|
const PROXY_ERROR_COOLDOWN_429_MS = 120000; // 2 minutes de cooldown sp\u00e9cifique pour erreur 429 (rate limit)
|
|
const PROXY_ERROR_COOLDOWN_5XX_MS = 90000; // 1.5 minutes pour les erreurs serveur (500, 502, 503, 504)
|
|
const MAX_CONSECUTIVE_ERRORS = 3; // Nombre max d'erreurs cons\u00e9cutives avant cooldown prolong\u00e9
|
|
const PROXY_EXTENDED_COOLDOWN_MS = 300000; // 5 minutes de cooldown prolong\u00e9 si trop d'erreurs cons\u00e9cutives
|
|
|
|
/**
|
|
* Marque un proxy comme \u00e9tant en erreur
|
|
* @param {string} proxyUrl - URL du proxy
|
|
* @param {number|string} errorCode - Code d'erreur (429, 500, 'timeout', etc.)
|
|
*/
|
|
function markProxyAsErrored(proxyUrl, errorCode) {
|
|
const existing = proxyErrorCache.get(proxyUrl);
|
|
const errorCount = existing ? existing.errorCount + 1 : 1;
|
|
|
|
proxyErrorCache.set(proxyUrl, {
|
|
errorTime: Date.now(),
|
|
errorCode,
|
|
errorCount,
|
|
});
|
|
|
|
// Log seulement si DEBUG activ\u00e9
|
|
if (process.env.DEBUG_PROXY) {
|
|
console.log(
|
|
`[PROXY CACHE] Proxy marqu\u00e9 en erreur: ${proxyUrl} (code: ${errorCode}, count: ${errorCount})`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* V\u00e9rifie si un proxy est actuellement en cooldown (\u00e0 \u00e9viter)
|
|
* @param {string} proxyUrl - URL du proxy
|
|
* @returns {boolean} - true si le proxy doit \u00eatre ignor\u00e9
|
|
*/
|
|
function isProxyInCooldown(proxyUrl) {
|
|
const errorInfo = proxyErrorCache.get(proxyUrl);
|
|
if (!errorInfo) return false;
|
|
|
|
const now = Date.now();
|
|
const timeSinceError = now - errorInfo.errorTime;
|
|
|
|
// D\u00e9terminer le cooldown appropri\u00e9 selon le type d'erreur et le nombre d'erreurs
|
|
let cooldownMs;
|
|
if (errorInfo.errorCount >= MAX_CONSECUTIVE_ERRORS) {
|
|
cooldownMs = PROXY_EXTENDED_COOLDOWN_MS;
|
|
} else if (errorInfo.errorCode === 429) {
|
|
cooldownMs = PROXY_ERROR_COOLDOWN_429_MS;
|
|
} else if (errorInfo.errorCode >= 500 && errorInfo.errorCode < 600) {
|
|
cooldownMs = PROXY_ERROR_COOLDOWN_5XX_MS;
|
|
} else {
|
|
cooldownMs = PROXY_ERROR_COOLDOWN_MS;
|
|
}
|
|
|
|
// Si le cooldown est pass\u00e9, supprimer l'entr\u00e9e du cache
|
|
if (timeSinceError >= cooldownMs) {
|
|
proxyErrorCache.delete(proxyUrl);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Retourne la liste des proxies disponibles (non en cooldown)
|
|
* @param {string[]} allProxies - Liste de tous les proxies
|
|
* @returns {string[]} - Liste des proxies disponibles
|
|
*/
|
|
function getAvailableProxies(allProxies) {
|
|
const available = allProxies.filter((proxy) => !isProxyInCooldown(proxy));
|
|
|
|
// Si tous les proxies sont en cooldown, on r\u00e9initialise le cache et on retourne tous les proxies
|
|
// pour \u00e9viter de bloquer compl\u00e8tement le service
|
|
if (available.length === 0) {
|
|
if (process.env.DEBUG_PROXY) {
|
|
console.log(
|
|
"[PROXY CACHE] Tous les proxies sont en cooldown, r\u00e9initialisation du cache",
|
|
);
|
|
}
|
|
proxyErrorCache.clear();
|
|
return allProxies;
|
|
}
|
|
|
|
return available;
|
|
}
|
|
|
|
/**
|
|
* R\u00e9initialise le compteur d'erreurs d'un proxy apr\u00e8s un succ\u00e8s
|
|
* @param {string} proxyUrl - URL du proxy
|
|
*/
|
|
function markProxyAsHealthy(proxyUrl) {
|
|
proxyErrorCache.delete(proxyUrl);
|
|
}
|
|
|
|
// Nettoyage p\u00e9riodique du proxyErrorCache — supprime les entr\u00e9es dont le cooldown a expir\u00e9
|
|
setInterval(
|
|
() => {
|
|
const now = Date.now();
|
|
for (const [proxyUrl, errorInfo] of proxyErrorCache) {
|
|
const maxCooldown = PROXY_EXTENDED_COOLDOWN_MS; // 5 min = cooldown le plus long
|
|
if (now - errorInfo.errorTime > maxCooldown) {
|
|
proxyErrorCache.delete(proxyUrl);
|
|
}
|
|
}
|
|
},
|
|
5 * 60 * 1000,
|
|
).unref(); // Toutes les 5 minutes — unref to not prevent process exit
|
|
|
|
/**
|
|
* Construit l'URL proxy en fonction du type de proxy
|
|
* Les proxies se terminant par '/' attendent l'URL encod\u00e9e
|
|
* Les proxies se terminant par '?' attendent l'URL non-encod\u00e9e
|
|
* @param {string} proxyUrl - URL du proxy Cloudflare
|
|
* @param {string} targetUrl - URL cible \u00e0 proxyer
|
|
* @returns {string} - URL finale \u00e0 appeler
|
|
*/
|
|
function buildProxiedUrl(proxyUrl, targetUrl) {
|
|
if (proxyUrl.endsWith("/")) {
|
|
// Proxy qui attend l'URL encod\u00e9e (ex: cors-worker-1)
|
|
return proxyUrl + encodeURIComponent(targetUrl);
|
|
} else {
|
|
// Proxy qui attend l'URL en query string (ex: ?url=...)
|
|
return proxyUrl + targetUrl;
|
|
}
|
|
}
|
|
|
|
// Fonction pour faire une requ\u00eate avec fallback CORS en cas d'erreur 429
|
|
async function makeRequestWithCorsFallback(targetUrl, options = {}) {
|
|
const {
|
|
timeout = 7000,
|
|
headers = {},
|
|
decompress = true,
|
|
method: requestMethod,
|
|
data: requestData,
|
|
...otherOptions
|
|
} = options;
|
|
|
|
const method = (requestMethod || 'GET').toUpperCase();
|
|
const defaultHeaders = {
|
|
accept:
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
"accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
priority: "u=1, i",
|
|
"sec-ch-ua":
|
|
'"Chromium";v="140", "Not=A?Brand";v="24", "Brave";v="140"',
|
|
"sec-ch-ua-mobile": "?0",
|
|
"sec-ch-ua-platform": '"Windows"',
|
|
"sec-fetch-dest": "document",
|
|
"sec-fetch-mode": "navigate",
|
|
"sec-fetch-site": "cross-site",
|
|
"sec-gpc": "1",
|
|
"user-agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
|
|
...headers,
|
|
};
|
|
|
|
// Filtrer les proxies disponibles (non en cooldown)
|
|
const availableProxies = getAvailableProxies(CLOUDFLARE_WORKERS_PROXIES);
|
|
|
|
if (availableProxies.length === 0) {
|
|
return axios({
|
|
url: targetUrl,
|
|
method,
|
|
headers: defaultHeaders,
|
|
timeout,
|
|
decompress,
|
|
...(requestData !== undefined ? { data: requestData } : {}),
|
|
...otherOptions,
|
|
});
|
|
}
|
|
|
|
// Utiliser directement les proxies Cloudflare Workers disponibles
|
|
let lastError = null;
|
|
|
|
for (let i = 0; i < availableProxies.length; i++) {
|
|
const currentProxy = availableProxies[i];
|
|
try {
|
|
const finalProxyUrl = buildProxiedUrl(currentProxy, targetUrl);
|
|
|
|
const response = await axios({
|
|
url: finalProxyUrl,
|
|
method,
|
|
headers: defaultHeaders,
|
|
timeout,
|
|
decompress,
|
|
...(requestData !== undefined ? { data: requestData } : {}),
|
|
...otherOptions,
|
|
});
|
|
|
|
// Succ\u00e8s : marquer le proxy comme sain
|
|
markProxyAsHealthy(currentProxy);
|
|
return response;
|
|
} catch (proxyError) {
|
|
lastError = proxyError;
|
|
const statusCode = proxyError.response?.status;
|
|
const errorCode = statusCode || proxyError.code || "unknown";
|
|
|
|
// En cas d'erreur 400 ou 403, arr\u00eat imm\u00e9diat sans r\u00e9essayer avec d'autres proxies
|
|
if (statusCode === 400 || statusCode === 403) {
|
|
throw proxyError;
|
|
}
|
|
|
|
// Marquer le proxy en erreur pour les codes 429, 5xx, timeout, etc.
|
|
if (
|
|
statusCode === 429 ||
|
|
(statusCode >= 500 && statusCode < 600) ||
|
|
proxyError.code === "ECONNABORTED" ||
|
|
proxyError.code === "ETIMEDOUT"
|
|
) {
|
|
markProxyAsErrored(currentProxy, errorCode);
|
|
}
|
|
|
|
// Si c'est le dernier proxy et qu'on a une erreur, throw l'erreur
|
|
if (i === availableProxies.length - 1) {
|
|
throw proxyError;
|
|
}
|
|
// Sinon continuer avec le prochain proxy (429, etc.)
|
|
}
|
|
}
|
|
|
|
// Si on arrive ici, tous les proxies ont \u00e9chou\u00e9
|
|
throw lastError || new Error("Tous les proxies ont \u00e9chou\u00e9");
|
|
}
|
|
|
|
// Fonction pour faire une requ\u00eate Coflix avec rotation Cloudflare Workers.
|
|
async function makeCoflixRequest(targetUrl, options = {}) {
|
|
const {
|
|
timeout = 15000,
|
|
headers = {},
|
|
decompress = true,
|
|
...otherOptions
|
|
} = options;
|
|
|
|
// Nettoyer l'URL pour \u00e9viter les espaces ind\u00e9sirables
|
|
const cleanTargetUrl = targetUrl.trim();
|
|
|
|
// Headers pour les requ\u00eates Coflix via proxy
|
|
const coflixProxyHeaders = {
|
|
accept:
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
"accept-encoding": "gzip, deflate, br",
|
|
"accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
"cache-control": "no-cache",
|
|
pragma: "no-cache",
|
|
"sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Brave";v="140"',
|
|
"sec-ch-ua-mobile": "?0",
|
|
"sec-ch-ua-platform": '"Windows"',
|
|
"sec-fetch-dest": "document",
|
|
"sec-fetch-mode": "navigate",
|
|
"sec-fetch-site": "cross-site",
|
|
"sec-gpc": "1",
|
|
"user-agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
|
|
...headers,
|
|
};
|
|
|
|
// Supprimer les headers li\u00e9s \u00e0 l'IP d'origine pour \u00e9viter de les transmettre
|
|
const headersToRemove = [
|
|
"X-Forwarded-For",
|
|
"X-Real-IP",
|
|
"X-Client-IP",
|
|
"CF-Connecting-IP",
|
|
"True-Client-IP",
|
|
"X-Original-Forwarded-For",
|
|
];
|
|
const cleanHeaders = { ...coflixProxyHeaders };
|
|
headersToRemove.forEach((header) => {
|
|
delete cleanHeaders[header];
|
|
delete cleanHeaders[header.toLowerCase()];
|
|
});
|
|
|
|
const proxyCandidates = getAvailableProxies(CLOUDFLARE_WORKERS_PROXIES);
|
|
let lastError = null;
|
|
|
|
if (proxyCandidates.length === 0) {
|
|
return axios({
|
|
url: cleanTargetUrl,
|
|
method: otherOptions.method || "GET",
|
|
headers: cleanHeaders,
|
|
timeout,
|
|
decompress,
|
|
responseType: "text",
|
|
responseEncoding: "utf8",
|
|
...otherOptions,
|
|
});
|
|
}
|
|
|
|
// Un 403/429 peut d\u00e9pendre du worker utilise : on tente chaque worker avant d'abandonner.
|
|
for (let i = 0; i < proxyCandidates.length; i++) {
|
|
const cloudflareProxy = proxyCandidates[i];
|
|
const proxiedUrl = buildProxiedUrl(cloudflareProxy, cleanTargetUrl);
|
|
|
|
try {
|
|
const response = await axios({
|
|
url: proxiedUrl,
|
|
method: otherOptions.method || "GET",
|
|
headers: cleanHeaders,
|
|
timeout,
|
|
decompress,
|
|
responseType: "text",
|
|
responseEncoding: "utf8",
|
|
...otherOptions,
|
|
});
|
|
|
|
markProxyAsHealthy(cloudflareProxy);
|
|
return response;
|
|
} catch (error) {
|
|
lastError = error;
|
|
const statusCode = error.response?.status;
|
|
const errorCode = error.code || "unknown";
|
|
|
|
error.coflixUrl = cleanTargetUrl;
|
|
error.coflixProxy = cloudflareProxy;
|
|
error.coflixProxiedUrl = proxiedUrl;
|
|
|
|
if (statusCode === 403 || statusCode === 429) {
|
|
markProxyAsErrored(cloudflareProxy, statusCode);
|
|
continue;
|
|
}
|
|
|
|
if (statusCode >= 500 && statusCode < 600) {
|
|
markProxyAsErrored(cloudflareProxy, statusCode);
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
errorCode === "ECONNABORTED" ||
|
|
errorCode === "ETIMEDOUT" ||
|
|
errorCode === "ECONNRESET" ||
|
|
errorCode === "EHOSTUNREACH" ||
|
|
errorCode === "ENETUNREACH"
|
|
) {
|
|
markProxyAsErrored(cloudflareProxy, errorCode);
|
|
continue;
|
|
}
|
|
|
|
// Pour les erreurs fonctionnelles (400, 404, parsing, etc.), arr\u00eater imm\u00e9diatement.
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
if (lastError) throw lastError;
|
|
throw new Error(
|
|
"Tous les Cloudflare Workers proxies ont \u00e9chou\u00e9 (Coflix)",
|
|
);
|
|
}
|
|
|
|
// Fonction pour faire une requete LecteurVideo avec CycleTLS.
|
|
// On limite volontairement la rotation a 2 proxies max pour eviter de balayer tout le pool.
|
|
// Instance CycleTLS partagee (singleton)
|
|
let cycleTLSInstance = null;
|
|
let cycleTLSInitializing = false;
|
|
const cycleTLSWaiters = [];
|
|
|
|
async function getCycleTLS() {
|
|
if (cycleTLSInstance) return cycleTLSInstance;
|
|
if (cycleTLSInitializing) {
|
|
return new Promise((resolve) => cycleTLSWaiters.push(resolve));
|
|
}
|
|
cycleTLSInitializing = true;
|
|
try {
|
|
cycleTLSInstance = await initCycleTLS();
|
|
cycleTLSInitializing = false;
|
|
for (const waiter of cycleTLSWaiters) waiter(cycleTLSInstance);
|
|
cycleTLSWaiters.length = 0;
|
|
return cycleTLSInstance;
|
|
} catch (err) {
|
|
cycleTLSInitializing = false;
|
|
// Reject all waiters so they don't leak forever
|
|
for (const waiter of cycleTLSWaiters) waiter(Promise.reject(err));
|
|
cycleTLSWaiters.length = 0;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// Graceful shutdown: kill CycleTLS Go subprocess
|
|
async function shutdownCycleTLS() {
|
|
if (cycleTLSInstance) {
|
|
try {
|
|
await cycleTLSInstance.exit();
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
cycleTLSInstance = null;
|
|
}
|
|
}
|
|
|
|
// JA3 fingerprint Chrome 120+
|
|
const CHROME_JA3 =
|
|
"771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0";
|
|
const CHROME_UA =
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
|
|
function pickLecteurVideoProxyCandidates() {
|
|
const httpPool =
|
|
HTTP_PROXIES.length > 0
|
|
? HTTP_PROXIES
|
|
: parseProxyPayload(process.env.HTTP_PROXIES, "http")
|
|
.map((entry) => sanitizeProxyEntry(entry, "http"))
|
|
.filter(Boolean);
|
|
|
|
const socksPool =
|
|
PROXIES.length > 0
|
|
? PROXIES
|
|
: parseProxyPayload(process.env.SOCKS5_PROXIES, "socks5")
|
|
.map((entry) => sanitizeProxyEntry(entry, "socks5"))
|
|
.filter(Boolean);
|
|
|
|
const preferredPool =
|
|
httpPool.length > 0
|
|
? { proxies: httpPool, useSocks: false, label: "HTTP_PROXIES" }
|
|
: { proxies: socksPool, useSocks: true, label: "SOCKS5_PROXIES" };
|
|
|
|
if (!preferredPool.proxies || preferredPool.proxies.length === 0) {
|
|
return {
|
|
proxies: [],
|
|
useSocks: false,
|
|
label: "aucun",
|
|
totalAvailable: 0,
|
|
};
|
|
}
|
|
|
|
const selectedProxies = [...preferredPool.proxies]
|
|
.sort(() => Math.random() - 0.5)
|
|
.slice(0, MAX_LECTEURVIDEO_PROXY_ATTEMPTS);
|
|
|
|
return {
|
|
proxies: selectedProxies,
|
|
useSocks: preferredPool.useSocks,
|
|
label: preferredPool.label,
|
|
totalAvailable: preferredPool.proxies.length,
|
|
};
|
|
}
|
|
|
|
async function makeLecteurVideoRequest(targetUrl, options = {}) {
|
|
const { headers = {} } = options;
|
|
const cleanTargetUrl = targetUrl.trim();
|
|
|
|
const {
|
|
proxies,
|
|
useSocks,
|
|
label: proxyPoolLabel,
|
|
totalAvailable,
|
|
} = pickLecteurVideoProxyCandidates();
|
|
|
|
if (!proxies || proxies.length === 0) {
|
|
const cycleTLS = await getCycleTLS();
|
|
const response = await cycleTLS(
|
|
cleanTargetUrl,
|
|
{
|
|
body: "",
|
|
ja3: CHROME_JA3,
|
|
userAgent: CHROME_UA,
|
|
headers: {
|
|
Referer: "https://coflix.click/",
|
|
Origin: "https://coflix.click",
|
|
Accept:
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
"Accept-Language": "fr-FR,fr;q=0.9",
|
|
...headers,
|
|
},
|
|
timeout: 15,
|
|
},
|
|
"get",
|
|
);
|
|
|
|
const body =
|
|
typeof response.body === "string"
|
|
? response.body
|
|
: JSON.stringify(response.body);
|
|
return {
|
|
data: body,
|
|
status: response.status,
|
|
headers: response.headers || {},
|
|
};
|
|
}
|
|
|
|
const cycleTLS = await getCycleTLS();
|
|
let lastError = null;
|
|
|
|
console.log(
|
|
`[LECTEURVIDEO] ${proxies.length} proxy(s) retenu(s) sur ${totalAvailable} depuis ${proxyPoolLabel}`,
|
|
);
|
|
|
|
for (let i = 0; i < proxies.length; i++) {
|
|
const proxy = proxies[i];
|
|
const auth = proxy.auth ? `${proxy.auth}@` : "";
|
|
const proxyUrl = useSocks
|
|
? `socks5h://${auth}${proxy.host}:${proxy.port}`
|
|
: `http://${auth}${proxy.host}:${proxy.port}`;
|
|
|
|
console.log(
|
|
`[LECTEURVIDEO] tentative ${i + 1}/${proxies.length} via cycletls + ${proxy.host}:${proxy.port} (${proxyPoolLabel})`,
|
|
);
|
|
|
|
try {
|
|
const response = await cycleTLS(
|
|
cleanTargetUrl,
|
|
{
|
|
body: "",
|
|
ja3: CHROME_JA3,
|
|
userAgent: CHROME_UA,
|
|
proxy: proxyUrl,
|
|
headers: {
|
|
Referer: "https://coflix.click/",
|
|
Origin: "https://coflix.click",
|
|
Accept:
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
"Accept-Language": "fr-FR,fr;q=0.9",
|
|
...headers,
|
|
},
|
|
timeout: 15,
|
|
},
|
|
"get",
|
|
);
|
|
|
|
const body =
|
|
typeof response.body === "string"
|
|
? response.body
|
|
: JSON.stringify(response.body);
|
|
const title = body.match(/<title>(.*?)<\/title>/i)?.[1] || "no title";
|
|
console.log(
|
|
`[LECTEURVIDEO] ${proxy.host}:${proxy.port} -> ${response.status} | title: ${title} | ${body.length} chars`,
|
|
);
|
|
|
|
if (
|
|
response.status === 403 &&
|
|
(body.includes("cf-wrapper") || body.includes("cloudflare"))
|
|
) {
|
|
console.warn(
|
|
`[LECTEURVIDEO] proxy ${i + 1}/${proxies.length} (${proxy.host}) bloque par Cloudflare, retry...`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
return {
|
|
data: body,
|
|
status: response.status,
|
|
headers: response.headers || {},
|
|
};
|
|
} catch (error) {
|
|
console.error(
|
|
`[LECTEURVIDEO] proxy ${i + 1}/${proxies.length} (${proxy.host}) echoue: ${error.message?.substring(0, 100)}`,
|
|
);
|
|
lastError = error;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
console.error(
|
|
`[LECTEURVIDEO] tous les proxies ont echoue pour ${cleanTargetUrl}`,
|
|
);
|
|
throw lastError || new Error("[LECTEURVIDEO] Tous les proxies ont echoue");
|
|
}
|
|
|
|
// Fonction AnimeSama avec CycleTLS (cloudscraper-style : JA3 Chrome pour bypass Cloudflare).
|
|
// Utilise le SOCKS5_PROXIES env directement (proxies dedies) -- pas le pool ProxyScrape,
|
|
// pas de fallback direct sans proxy.
|
|
const MAX_ANIMESAMA_CYCLETLS_ATTEMPTS = 2;
|
|
const ANIMESAMA_SOCKS5_PROXIES = dedupeProxyEntries(
|
|
parseProxyPayload(process.env.SOCKS5_PROXIES, "socks5")
|
|
.map((entry) => sanitizeProxyEntry(entry, "socks5"))
|
|
.filter(Boolean),
|
|
);
|
|
|
|
async function makeAnimeSamaRequest(targetUrl, options = {}) {
|
|
const {
|
|
headers = {},
|
|
timeout = 30,
|
|
method = "get",
|
|
body = "",
|
|
} = options;
|
|
const cleanTargetUrl = targetUrl.trim();
|
|
const lowerMethod = String(method).toLowerCase();
|
|
|
|
const defaultHeaders = {
|
|
Accept:
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
"Accept-Language": "fr-FR,fr;q=0.6",
|
|
...headers,
|
|
};
|
|
|
|
const pool = [...ANIMESAMA_SOCKS5_PROXIES]
|
|
.sort(() => Math.random() - 0.5)
|
|
.slice(0, MAX_ANIMESAMA_CYCLETLS_ATTEMPTS);
|
|
|
|
if (pool.length === 0) {
|
|
throw new Error(
|
|
"[ANIMESAMA CYCLETLS] aucun proxy SOCKS5 defini dans SOCKS5_PROXIES",
|
|
);
|
|
}
|
|
|
|
const cycleTLS = await getCycleTLS();
|
|
|
|
const asAxiosLike = (response) => {
|
|
const data =
|
|
typeof response.body === "string"
|
|
? response.body
|
|
: JSON.stringify(response.body);
|
|
return { data, status: response.status, headers: response.headers || {} };
|
|
};
|
|
|
|
const isCloudflareBlock = (status, bodyStr) =>
|
|
status === 403 &&
|
|
typeof bodyStr === "string" &&
|
|
(bodyStr.includes("cf-wrapper") || bodyStr.includes("cloudflare"));
|
|
|
|
let lastError = null;
|
|
|
|
for (let i = 0; i < pool.length; i++) {
|
|
const proxy = pool[i];
|
|
const auth = proxy.auth ? `${proxy.auth}@` : "";
|
|
const proxyUrl = `socks5h://${auth}${proxy.host}:${proxy.port}`;
|
|
|
|
try {
|
|
const response = await cycleTLS(
|
|
cleanTargetUrl,
|
|
{
|
|
body,
|
|
ja3: CHROME_JA3,
|
|
userAgent: CHROME_UA,
|
|
proxy: proxyUrl,
|
|
headers: defaultHeaders,
|
|
timeout,
|
|
},
|
|
lowerMethod,
|
|
);
|
|
|
|
const result = asAxiosLike(response);
|
|
if (isCloudflareBlock(result.status, result.data)) {
|
|
lastError = new Error(
|
|
`[ANIMESAMA CYCLETLS] cloudflare block via ${proxy.host}:${proxy.port}`,
|
|
);
|
|
continue;
|
|
}
|
|
return result;
|
|
} catch (err) {
|
|
lastError = err;
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error("[ANIMESAMA CYCLETLS] tous les proxies ont echoue");
|
|
}
|
|
|
|
// === SOCKS5 PROXIES (from env) ===
|
|
const parseJsonArrayEnv = (envName, fallback = []) => {
|
|
const rawValue = process.env[envName];
|
|
if (!rawValue) return fallback;
|
|
try {
|
|
const parsed = JSON.parse(rawValue);
|
|
return Array.isArray(parsed) ? parsed : fallback;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
};
|
|
|
|
const { redis } = require("../config/redis");
|
|
const { acquireRedisLock } = require("./redisLock");
|
|
|
|
const PROXY_ROTATION_REDIS_PREFIX = "proxyscrape:rotation:v1";
|
|
const PROXY_RATE_LIMIT_REDIS_PREFIX = "proxyscrape:rate-limit:v1";
|
|
const proxyRateLimitState = new Map();
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function buildProxyRateLimitKey(poolName, proxy) {
|
|
if (!proxy || !proxy.host || !proxy.port) return null;
|
|
return `${PROXY_RATE_LIMIT_REDIS_PREFIX}:${poolName}:${proxy.type || "proxy"}:${proxy.host}:${proxy.port}:${proxy.auth || ""}`;
|
|
}
|
|
|
|
function reserveLocalRateLimitedProxy(proxyKey, minIntervalMs) {
|
|
if (!proxyKey) return false;
|
|
|
|
const now = Date.now();
|
|
const nextAllowedAt = proxyRateLimitState.get(proxyKey) || 0;
|
|
if (nextAllowedAt > now) {
|
|
return false;
|
|
}
|
|
|
|
proxyRateLimitState.set(proxyKey, now + minIntervalMs);
|
|
return true;
|
|
}
|
|
|
|
async function reserveRateLimitedProxy(poolName, proxy, minIntervalMs) {
|
|
if (!proxy) return false;
|
|
|
|
const safeIntervalMs = Math.max(0, Math.floor(Number(minIntervalMs) || 0));
|
|
if (safeIntervalMs <= 0) {
|
|
return true;
|
|
}
|
|
|
|
const proxyKey = buildProxyRateLimitKey(poolName, proxy);
|
|
if (!proxyKey) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const reserved = await redis.set(
|
|
proxyKey,
|
|
String(Date.now()),
|
|
"PX",
|
|
safeIntervalMs,
|
|
"NX",
|
|
);
|
|
if (reserved === "OK") {
|
|
return true;
|
|
}
|
|
return false;
|
|
} catch {
|
|
// Fallback local si Redis n'est pas disponible.
|
|
}
|
|
|
|
return reserveLocalRateLimitedProxy(proxyKey, safeIntervalMs);
|
|
}
|
|
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [proxyKey, nextAllowedAt] of proxyRateLimitState) {
|
|
if (!Number.isFinite(nextAllowedAt) || nextAllowedAt <= now) {
|
|
proxyRateLimitState.delete(proxyKey);
|
|
}
|
|
}
|
|
}, 30 * 1000).unref();
|
|
|
|
function reserveLocalProxyWindow(poolName, poolLength, batchSize) {
|
|
const existingCursor = proxyRotationState.get(poolName);
|
|
const startIndex = Number.isInteger(existingCursor)
|
|
? existingCursor % poolLength
|
|
: Math.floor(Math.random() * poolLength);
|
|
|
|
proxyRotationState.set(poolName, (startIndex + batchSize) % poolLength);
|
|
return startIndex;
|
|
}
|
|
|
|
async function reserveProxyWindow(poolName, poolLength, batchSize = 1) {
|
|
if (!Number.isFinite(poolLength) || poolLength <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
const safeBatchSize = Math.min(
|
|
poolLength,
|
|
Math.max(1, Math.floor(Number(batchSize) || 1)),
|
|
);
|
|
|
|
try {
|
|
const rotationKey = `${PROXY_ROTATION_REDIS_PREFIX}:${poolName}`;
|
|
const nextValue = await redis.incrby(rotationKey, safeBatchSize);
|
|
const startIndex = Number(nextValue) - safeBatchSize;
|
|
return ((startIndex % poolLength) + poolLength) % poolLength;
|
|
} catch {
|
|
return reserveLocalProxyWindow(poolName, poolLength, safeBatchSize);
|
|
}
|
|
}
|
|
|
|
async function pickNextProxyBatch(pool, count = 1, poolName = "default") {
|
|
if (!Array.isArray(pool) || pool.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const safeCount = Math.min(
|
|
pool.length,
|
|
Math.max(1, Math.floor(Number(count) || 1)),
|
|
);
|
|
const startIndex = await reserveProxyWindow(poolName, pool.length, safeCount);
|
|
const selected = [];
|
|
|
|
for (let offset = 0; offset < safeCount; offset++) {
|
|
selected.push(pool[(startIndex + offset) % pool.length]);
|
|
}
|
|
|
|
return selected;
|
|
}
|
|
|
|
async function pickNextRateLimitedProxy(pool, options = {}) {
|
|
if (!Array.isArray(pool) || pool.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const {
|
|
poolName = "default",
|
|
minIntervalMs = 0,
|
|
waitTimeoutMs = 0,
|
|
retryDelayMs = 25,
|
|
} = options;
|
|
|
|
const safeMinIntervalMs = Math.max(0, Math.floor(Number(minIntervalMs) || 0));
|
|
if (safeMinIntervalMs <= 0) {
|
|
const [proxy] = await pickNextProxyBatch(pool, 1, poolName);
|
|
return proxy || null;
|
|
}
|
|
|
|
const deadline =
|
|
Date.now() + Math.max(0, Math.floor(Number(waitTimeoutMs) || 0));
|
|
|
|
while (true) {
|
|
const startIndex = await reserveProxyWindow(poolName, pool.length, 1);
|
|
|
|
for (let offset = 0; offset < pool.length; offset++) {
|
|
const proxy = pool[(startIndex + offset) % pool.length];
|
|
const reserved = await reserveRateLimitedProxy(
|
|
poolName,
|
|
proxy,
|
|
safeMinIntervalMs,
|
|
);
|
|
if (reserved) {
|
|
return proxy;
|
|
}
|
|
}
|
|
|
|
if (Date.now() >= deadline) {
|
|
return null;
|
|
}
|
|
|
|
const remainingMs = deadline - Date.now();
|
|
await sleep(Math.max(5, Math.min(retryDelayMs, remainingMs)));
|
|
}
|
|
}
|
|
|
|
async function pickNextSocks5Proxy(options = {}) {
|
|
const proxy = await pickNextRateLimitedProxy(PROXIES, {
|
|
poolName: "SOCKS5_PROXIES",
|
|
...options,
|
|
});
|
|
return proxy || null;
|
|
}
|
|
|
|
const splitCsvEnv = (envName, fallback = []) => {
|
|
const rawValue = process.env[envName];
|
|
if (!rawValue) return [...fallback];
|
|
return rawValue
|
|
.split(",")
|
|
.map((value) => value.trim())
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const parsePositiveIntEnv = (envName, fallback) => {
|
|
const rawValue = process.env[envName];
|
|
const parsed = Number.parseInt(rawValue || "", 10);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
};
|
|
|
|
function normalizeProxyType(type, fallback = "http") {
|
|
const normalized = String(type || fallback)
|
|
.toLowerCase()
|
|
.replace(/:$/, "")
|
|
.trim();
|
|
if (
|
|
normalized === "socks" ||
|
|
normalized === "socks5h" ||
|
|
normalized.startsWith("socks5")
|
|
) {
|
|
return "socks5";
|
|
}
|
|
if (normalized.startsWith("http")) {
|
|
return "http";
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
function updateArrayInPlace(target, nextValues) {
|
|
target.splice(0, target.length, ...nextValues);
|
|
return target;
|
|
}
|
|
|
|
function dedupeProxyEntries(entries = []) {
|
|
const seen = new Set();
|
|
return entries.filter((entry) => {
|
|
if (!entry || !entry.host || !entry.port) return false;
|
|
const key = `${entry.type}:${entry.host}:${entry.port}:${entry.auth || ""}`;
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function sanitizeProxyEntry(entry, fallbackType = "http") {
|
|
if (!entry || !entry.host || !entry.port) return null;
|
|
const port = Number.parseInt(entry.port, 10);
|
|
if (!Number.isFinite(port) || port <= 0) return null;
|
|
return {
|
|
host: String(entry.host).trim(),
|
|
port,
|
|
auth: entry.auth ? String(entry.auth).trim() : undefined,
|
|
type: normalizeProxyType(entry.type, fallbackType),
|
|
};
|
|
}
|
|
|
|
function parseProxyString(value, defaultType = "http") {
|
|
if (!value || typeof value !== "string") return null;
|
|
const rawValue = value.trim();
|
|
if (!rawValue) return null;
|
|
|
|
if (/^[a-z]+:\/\//i.test(rawValue)) {
|
|
try {
|
|
const parsedUrl = new URL(rawValue);
|
|
return sanitizeProxyEntry(
|
|
{
|
|
host: parsedUrl.hostname,
|
|
port: parsedUrl.port,
|
|
auth: parsedUrl.username
|
|
? `${decodeURIComponent(parsedUrl.username)}${parsedUrl.password ? `:${decodeURIComponent(parsedUrl.password)}` : ""}`
|
|
: undefined,
|
|
type: parsedUrl.protocol.replace(":", ""),
|
|
},
|
|
defaultType,
|
|
);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const authSplitIndex = rawValue.lastIndexOf("@");
|
|
let authPart = "";
|
|
let hostPortPart = rawValue;
|
|
if (authSplitIndex !== -1) {
|
|
authPart = rawValue.slice(0, authSplitIndex).trim();
|
|
hostPortPart = rawValue.slice(authSplitIndex + 1).trim();
|
|
}
|
|
|
|
const segments = hostPortPart
|
|
.split(":")
|
|
.map((segment) => segment.trim())
|
|
.filter(Boolean);
|
|
if (segments.length === 2) {
|
|
return sanitizeProxyEntry(
|
|
{
|
|
host: segments[0],
|
|
port: segments[1],
|
|
auth: authPart || undefined,
|
|
type: defaultType,
|
|
},
|
|
defaultType,
|
|
);
|
|
}
|
|
|
|
if (!authPart && segments.length === 4) {
|
|
const [a, b, c, d] = segments;
|
|
if (/^\d+$/.test(b)) {
|
|
return sanitizeProxyEntry(
|
|
{
|
|
host: a,
|
|
port: b,
|
|
auth: `${c}:${d}`,
|
|
type: defaultType,
|
|
},
|
|
defaultType,
|
|
);
|
|
}
|
|
if (/^\d+$/.test(d)) {
|
|
return sanitizeProxyEntry(
|
|
{
|
|
host: c,
|
|
port: d,
|
|
auth: `${a}:${b}`,
|
|
type: defaultType,
|
|
},
|
|
defaultType,
|
|
);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function parseProxyPayload(payload, defaultType = "http") {
|
|
if (!payload) return [];
|
|
|
|
if (Array.isArray(payload)) {
|
|
return dedupeProxyEntries(
|
|
payload
|
|
.flatMap((item) => parseProxyPayload(item, defaultType))
|
|
.filter(Boolean),
|
|
);
|
|
}
|
|
|
|
if (typeof payload === "string") {
|
|
const trimmed = payload.trim();
|
|
if (!trimmed) return [];
|
|
|
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
try {
|
|
return parseProxyPayload(JSON.parse(trimmed), defaultType);
|
|
} catch {
|
|
// Keep parsing as raw text below.
|
|
}
|
|
}
|
|
|
|
return dedupeProxyEntries(
|
|
trimmed
|
|
.split(/\r?\n/)
|
|
.map((line) => parseProxyString(line, defaultType))
|
|
.filter(Boolean),
|
|
);
|
|
}
|
|
|
|
if (typeof payload === "object") {
|
|
if (payload.host && payload.port) {
|
|
return dedupeProxyEntries(
|
|
[sanitizeProxyEntry(payload, defaultType)].filter(Boolean),
|
|
);
|
|
}
|
|
|
|
const candidateKeys = ["proxies", "data", "items", "results", "list"];
|
|
for (const key of candidateKeys) {
|
|
if (payload[key] !== undefined) {
|
|
const parsed = parseProxyPayload(payload[key], defaultType);
|
|
if (parsed.length > 0) return parsed;
|
|
}
|
|
}
|
|
|
|
const arrayValues = Object.values(payload).filter(Array.isArray);
|
|
for (const value of arrayValues) {
|
|
const parsed = parseProxyPayload(value, defaultType);
|
|
if (parsed.length > 0) return parsed;
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function proxyToUrl(proxy, forcedType) {
|
|
const safeProxy = sanitizeProxyEntry(
|
|
proxy,
|
|
forcedType || proxy?.type || "http",
|
|
);
|
|
if (!safeProxy) return null;
|
|
const type = normalizeProxyType(forcedType || safeProxy.type, "http");
|
|
const auth = safeProxy.auth ? `${safeProxy.auth}@` : "";
|
|
return `${type}://${auth}${safeProxy.host}:${safeProxy.port}`;
|
|
}
|
|
|
|
const PROXY_POOL_REDIS_KEY = "proxyscrape:mainapi:pools:v1";
|
|
const PROXYSCRAPE_API_TOKEN = (
|
|
process.env.PROXYSCRAPE_API_TOKEN ||
|
|
process.env.PROXYSCRAPE_API_KEY ||
|
|
""
|
|
).trim();
|
|
const PROXYSCRAPE_ACCOUNT_ID = (
|
|
process.env.PROXYSCRAPE_ACCOUNT_ID || ""
|
|
).trim();
|
|
const PROXYSCRAPE_ENABLED = Boolean(
|
|
PROXYSCRAPE_API_TOKEN && PROXYSCRAPE_ACCOUNT_ID,
|
|
);
|
|
const PROXY_POOL_REDIS_TTL_SECONDS = 60 * 60;
|
|
const PROXYSCRAPE_ACCOUNT_BASE_URL = "https://api.proxyscrape.com";
|
|
const PROXYSCRAPE_ACCOUNT_PATH =
|
|
"/v4/account/{accountId}/datacenter_shared/proxy-list";
|
|
const PROXYSCRAPE_TIMEOUT_MS = 12000;
|
|
const PROXYSCRAPE_REFRESH_MS = 15 * 60 * 1000;
|
|
const PROXYSCRAPE_MAX_PER_PROTOCOL = 1000;
|
|
const PROXYSCRAPE_HTTP_PROTOCOLS = ["http"];
|
|
const PROXYSCRAPE_SOCKS_PROTOCOLS = ["socks"];
|
|
const PROXYSCRAPE_ALLOW_LEGACY_FALLBACK = false;
|
|
const PROXYSCRAPE_DEBUG = false;
|
|
|
|
const legacySocksEnv =
|
|
PROXYSCRAPE_ENABLED && !PROXYSCRAPE_ALLOW_LEGACY_FALLBACK
|
|
? []
|
|
: parseJsonArrayEnv("SOCKS5_PROXIES", []);
|
|
const legacyHttpEnv =
|
|
PROXYSCRAPE_ENABLED && !PROXYSCRAPE_ALLOW_LEGACY_FALLBACK
|
|
? []
|
|
: parseJsonArrayEnv("HTTP_PROXIES", []);
|
|
|
|
const PROXIES = dedupeProxyEntries(
|
|
(legacySocksEnv.length > 0
|
|
? legacySocksEnv
|
|
: parseProxyPayload(process.env.SOCKS5_PROXIES, "socks5")
|
|
)
|
|
.map((entry) => sanitizeProxyEntry(entry, "socks5"))
|
|
.filter(Boolean),
|
|
);
|
|
|
|
const COFLIX_SOCKS5_PROXIES = dedupeProxyEntries(
|
|
parseProxyPayload(process.env.SOCKS5_PROXIES, "socks5")
|
|
.map((entry) => sanitizeProxyEntry(entry, "socks5"))
|
|
.filter(Boolean),
|
|
);
|
|
|
|
const HTTP_PROXIES = dedupeProxyEntries(
|
|
(legacyHttpEnv.length > 0
|
|
? legacyHttpEnv
|
|
: parseProxyPayload(process.env.HTTP_PROXIES, "http")
|
|
)
|
|
.map((entry) => sanitizeProxyEntry(entry, "http"))
|
|
.filter(Boolean),
|
|
);
|
|
|
|
const DARKINO_HTTP_PROXIES = [];
|
|
const DARKINO_PROXIES = [];
|
|
|
|
function syncDerivedHttpProxyPools(httpEntries) {
|
|
const normalizedHttpEntries = dedupeProxyEntries(
|
|
httpEntries
|
|
.map((entry) => sanitizeProxyEntry(entry, "http"))
|
|
.filter(Boolean),
|
|
);
|
|
|
|
updateArrayInPlace(HTTP_PROXIES, normalizedHttpEntries);
|
|
updateArrayInPlace(
|
|
DARKINO_PROXIES,
|
|
normalizedHttpEntries.map((entry) => ({ ...entry, type: "http" })),
|
|
);
|
|
updateArrayInPlace(
|
|
DARKINO_HTTP_PROXIES,
|
|
DARKINO_PROXIES.map((proxy) => proxyToUrl(proxy, "http")).filter(Boolean),
|
|
);
|
|
}
|
|
|
|
syncDerivedHttpProxyPools(HTTP_PROXIES);
|
|
|
|
const proxyScrapeState = {
|
|
refreshPromise: null,
|
|
lastRefreshAt: 0,
|
|
lastRefreshError: null,
|
|
lastSuccessMeta: {
|
|
socks: null,
|
|
http: null,
|
|
},
|
|
};
|
|
|
|
function getProxyPoolAgeMs(updatedAt = proxyScrapeState.lastRefreshAt) {
|
|
const timestamp = Number(updatedAt);
|
|
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
|
return Number.POSITIVE_INFINITY;
|
|
}
|
|
return Date.now() - timestamp;
|
|
}
|
|
|
|
function isProxyPoolFresh(updatedAt = proxyScrapeState.lastRefreshAt) {
|
|
return getProxyPoolAgeMs(updatedAt) < PROXYSCRAPE_REFRESH_MS;
|
|
}
|
|
|
|
function buildProxyScrapeResult(source) {
|
|
return {
|
|
enabled: true,
|
|
socks: PROXIES.length,
|
|
http: HTTP_PROXIES.length,
|
|
meta: proxyScrapeState.lastSuccessMeta,
|
|
source,
|
|
};
|
|
}
|
|
|
|
function limitProxyEntries(entries) {
|
|
if (
|
|
!Number.isFinite(PROXYSCRAPE_MAX_PER_PROTOCOL) ||
|
|
PROXYSCRAPE_MAX_PER_PROTOCOL <= 0
|
|
) {
|
|
return entries;
|
|
}
|
|
return entries.slice(0, PROXYSCRAPE_MAX_PER_PROTOCOL);
|
|
}
|
|
|
|
function buildProxyScrapeCandidates(protocol) {
|
|
if (!PROXYSCRAPE_ACCOUNT_ID) {
|
|
return [];
|
|
}
|
|
|
|
const accountPath = PROXYSCRAPE_ACCOUNT_PATH.replace(
|
|
"{accountId}",
|
|
PROXYSCRAPE_ACCOUNT_ID,
|
|
);
|
|
return [
|
|
{
|
|
url: `${PROXYSCRAPE_ACCOUNT_BASE_URL}${accountPath}`,
|
|
headers: { "api-token": PROXYSCRAPE_API_TOKEN },
|
|
params: {
|
|
type: "getproxies",
|
|
protocol,
|
|
format: "normal",
|
|
},
|
|
label: `v4 account ${accountPath}`,
|
|
},
|
|
];
|
|
}
|
|
|
|
function applyStoredProxyPools(storedPools) {
|
|
if (!storedPools || typeof storedPools !== "object") return false;
|
|
|
|
const socks = dedupeProxyEntries(
|
|
(storedPools.socks || [])
|
|
.map((entry) => sanitizeProxyEntry(entry, "socks5"))
|
|
.filter(Boolean),
|
|
);
|
|
const httpEntries = dedupeProxyEntries(
|
|
(storedPools.http || [])
|
|
.map((entry) => sanitizeProxyEntry(entry, "http"))
|
|
.filter(Boolean),
|
|
);
|
|
|
|
if (socks.length === 0 || httpEntries.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
updateArrayInPlace(
|
|
PROXIES,
|
|
socks.map((entry) => ({ ...entry, type: "socks5" })),
|
|
);
|
|
syncDerivedHttpProxyPools(httpEntries);
|
|
proxyAgentCache.clear();
|
|
darkinoProxyAgentCache.clear();
|
|
cpasmalAgentCache.clear();
|
|
return true;
|
|
}
|
|
|
|
async function loadProxyPoolsFromRedis() {
|
|
try {
|
|
const rawValue = await redis.get(PROXY_POOL_REDIS_KEY);
|
|
if (!rawValue) return false;
|
|
const parsed = JSON.parse(rawValue);
|
|
if (!applyStoredProxyPools(parsed)) return false;
|
|
proxyScrapeState.lastRefreshAt =
|
|
Number(parsed.updatedAt) || proxyScrapeState.lastRefreshAt;
|
|
proxyScrapeState.lastSuccessMeta =
|
|
parsed.meta || proxyScrapeState.lastSuccessMeta;
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function saveProxyPoolsToRedis(payload) {
|
|
try {
|
|
await redis.set(
|
|
PROXY_POOL_REDIS_KEY,
|
|
JSON.stringify(payload),
|
|
"EX",
|
|
PROXY_POOL_REDIS_TTL_SECONDS,
|
|
);
|
|
} catch {
|
|
// ignore Redis persistence failures
|
|
}
|
|
}
|
|
|
|
async function fetchProxyScrapeProtocol(defaultType, protocols) {
|
|
const attemptErrors = [];
|
|
|
|
for (const protocol of protocols) {
|
|
const candidates = buildProxyScrapeCandidates(protocol);
|
|
|
|
for (const candidate of candidates) {
|
|
try {
|
|
if (PROXYSCRAPE_DEBUG) {
|
|
console.log(
|
|
`[PROXYSCRAPE DEBUG] tentative ${candidate.label} | protocol=${protocol} | url=${candidate.url} | params=${JSON.stringify(candidate.params)}`,
|
|
);
|
|
}
|
|
|
|
const response = await axios.get(candidate.url, {
|
|
headers: candidate.headers,
|
|
params: candidate.params,
|
|
timeout: PROXYSCRAPE_TIMEOUT_MS,
|
|
responseType: "text",
|
|
transformResponse: [(data) => data],
|
|
});
|
|
|
|
const proxies = limitProxyEntries(
|
|
dedupeProxyEntries(parseProxyPayload(response.data, defaultType)),
|
|
);
|
|
|
|
if (proxies.length === 0) {
|
|
const message = `[PROXYSCRAPE] ${candidate.label} (${protocol}) a retourne 0 proxy`;
|
|
attemptErrors.push(message);
|
|
if (PROXYSCRAPE_DEBUG) {
|
|
console.warn(
|
|
`${message} | body=${String(response.data || "").slice(0, 300)}`,
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (PROXYSCRAPE_DEBUG) {
|
|
console.log(
|
|
`[PROXYSCRAPE DEBUG] succes ${candidate.label} (${protocol}) -> ${proxies.length} proxies`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
proxies,
|
|
meta: {
|
|
protocol,
|
|
endpoint: candidate.label,
|
|
count: proxies.length,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
const status = error.response?.status;
|
|
const message = `[PROXYSCRAPE] ${candidate.label} (${protocol}) a echoue: ${status || error.code || error.message}`;
|
|
attemptErrors.push(message);
|
|
if (PROXYSCRAPE_DEBUG) {
|
|
console.warn(
|
|
`${message} | body=${String(error.response?.data || "").slice(0, 300)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
attemptErrors.length > 0
|
|
? attemptErrors.join(" | ")
|
|
: "[PROXYSCRAPE] Aucun endpoint n a repondu",
|
|
);
|
|
}
|
|
|
|
async function refreshProxyScrapeProxies(options = {}) {
|
|
const { force = false, silent = false } = options;
|
|
|
|
if (!PROXYSCRAPE_ENABLED) {
|
|
return {
|
|
enabled: false,
|
|
socks: PROXIES.length,
|
|
http: HTTP_PROXIES.length,
|
|
};
|
|
}
|
|
|
|
if (!force && proxyScrapeState.refreshPromise) {
|
|
return proxyScrapeState.refreshPromise;
|
|
}
|
|
|
|
if (proxyScrapeState.refreshPromise) {
|
|
return proxyScrapeState.refreshPromise;
|
|
}
|
|
|
|
if (!force) {
|
|
const loadedFromRedis = await loadProxyPoolsFromRedis();
|
|
if (loadedFromRedis && isProxyPoolFresh()) {
|
|
if (!silent) {
|
|
console.log(
|
|
`[PROXYSCRAPE] Pools frais charges depuis Redis: ${PROXIES.length} SOCKS5, ${HTTP_PROXIES.length} HTTP`,
|
|
);
|
|
}
|
|
return buildProxyScrapeResult("redis-fresh");
|
|
}
|
|
}
|
|
|
|
proxyScrapeState.refreshPromise = (async () => {
|
|
const lock = await acquireRedisLock(PROXY_POOL_REDIS_KEY, {
|
|
ttl: Math.max(10, Math.ceil(PROXYSCRAPE_TIMEOUT_MS / 1000) * 2),
|
|
retries: 40,
|
|
retryDelay: 500,
|
|
});
|
|
|
|
if (!lock) {
|
|
const loadedFromRedis = await loadProxyPoolsFromRedis();
|
|
if (loadedFromRedis) {
|
|
if (!silent) {
|
|
console.log(
|
|
`[PROXYSCRAPE] Pools charges depuis Redis: ${PROXIES.length} SOCKS5, ${HTTP_PROXIES.length} HTTP`,
|
|
);
|
|
}
|
|
return buildProxyScrapeResult("redis");
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (!force) {
|
|
const loadedFromRedis = await loadProxyPoolsFromRedis();
|
|
if (loadedFromRedis && isProxyPoolFresh()) {
|
|
if (!silent) {
|
|
console.log(
|
|
`[PROXYSCRAPE] Pools frais recharges depuis Redis: ${PROXIES.length} SOCKS5, ${HTTP_PROXIES.length} HTTP`,
|
|
);
|
|
}
|
|
return buildProxyScrapeResult("redis-fresh");
|
|
}
|
|
}
|
|
|
|
const socksResult = await fetchProxyScrapeProtocol(
|
|
"socks5",
|
|
PROXYSCRAPE_SOCKS_PROTOCOLS,
|
|
);
|
|
const httpResult = await fetchProxyScrapeProtocol(
|
|
"http",
|
|
PROXYSCRAPE_HTTP_PROTOCOLS,
|
|
);
|
|
|
|
updateArrayInPlace(
|
|
PROXIES,
|
|
socksResult.proxies.map((proxy) => ({ ...proxy, type: "socks5" })),
|
|
);
|
|
syncDerivedHttpProxyPools(httpResult.proxies);
|
|
|
|
proxyAgentCache.clear();
|
|
darkinoProxyAgentCache.clear();
|
|
cpasmalAgentCache.clear();
|
|
|
|
proxyScrapeState.lastRefreshAt = Date.now();
|
|
proxyScrapeState.lastRefreshError = null;
|
|
proxyScrapeState.lastSuccessMeta = {
|
|
socks: socksResult.meta,
|
|
http: httpResult.meta,
|
|
};
|
|
|
|
await saveProxyPoolsToRedis({
|
|
updatedAt: proxyScrapeState.lastRefreshAt,
|
|
socks: PROXIES,
|
|
http: HTTP_PROXIES,
|
|
meta: proxyScrapeState.lastSuccessMeta,
|
|
});
|
|
|
|
if (!silent) {
|
|
console.log(
|
|
`[PROXYSCRAPE] Refresh OK: ${PROXIES.length} SOCKS5 via ${socksResult.meta.endpoint}, ${HTTP_PROXIES.length} HTTP via ${httpResult.meta.endpoint}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
enabled: true,
|
|
socks: PROXIES.length,
|
|
http: HTTP_PROXIES.length,
|
|
meta: proxyScrapeState.lastSuccessMeta,
|
|
source: "api",
|
|
};
|
|
} catch (error) {
|
|
proxyScrapeState.lastRefreshError = error.message;
|
|
|
|
const loadedFromRedis = await loadProxyPoolsFromRedis();
|
|
if (loadedFromRedis) {
|
|
if (!silent) {
|
|
console.warn(
|
|
`[PROXYSCRAPE] Refresh API echoue, fallback Redis utilise: ${error.message}`,
|
|
);
|
|
}
|
|
return buildProxyScrapeResult("redis-fallback");
|
|
}
|
|
|
|
if (!silent) {
|
|
console.error(`[PROXYSCRAPE] Refresh echoue: ${error.message}`);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
if (lock && typeof lock.release === "function") {
|
|
await lock.release();
|
|
}
|
|
}
|
|
})().finally(() => {
|
|
proxyScrapeState.refreshPromise = null;
|
|
});
|
|
|
|
return proxyScrapeState.refreshPromise;
|
|
}
|
|
|
|
function scheduleProxyScrapeRefresh() {
|
|
if (!PROXYSCRAPE_ENABLED) return;
|
|
|
|
setTimeout(() => {
|
|
loadProxyPoolsFromRedis().catch(() => {});
|
|
}, 0).unref();
|
|
|
|
setInterval(() => {
|
|
refreshProxyScrapeProxies({ silent: false }).catch(() => {});
|
|
}, PROXYSCRAPE_REFRESH_MS).unref();
|
|
}
|
|
|
|
scheduleProxyScrapeRefresh();
|
|
|
|
// === WIFLIX FREE PROXY LIST (ProxyScrape v4 free) ===
|
|
const WIFLIX_FREE_PROXY_URL =
|
|
"https://api.proxyscrape.com/v4/free-proxy-list/get?request=display_proxies&proxy_format=protocolipport&format=text";
|
|
const WIFLIX_FREE_PROXY_REFRESH_MS = 5 * 60 * 1000; // 5 minutes
|
|
const WIFLIX_FREE_PROXY_MAX_ATTEMPTS = 10; // Try up to 10 proxies per request
|
|
const WIFLIX_FREE_PROXY_TIMEOUT = 5000; // 5s per proxy attempt (fail fast)
|
|
const wiflixFreeProxies = []; // HTTP-only proxy URL strings
|
|
const wiflixProxyAgentCache = new Map();
|
|
const wiflixDeadProxies = new Set(); // Proxies who echoue — survit entre les requetes, reset au refresh
|
|
|
|
async function fetchWiflixFreeProxies() {
|
|
try {
|
|
const res = await axios.get(WIFLIX_FREE_PROXY_URL, { timeout: 15000 });
|
|
const text = typeof res.data === "string" ? res.data : "";
|
|
const lines = text
|
|
.split(/\r?\n/)
|
|
.map((l) => l.trim())
|
|
.filter(Boolean);
|
|
|
|
const parsed = [];
|
|
for (const line of lines) {
|
|
// Only keep http/https — socks4 free proxies are too unreliable
|
|
if (/^https?:\/\/.+:\d+$/.test(line)) {
|
|
parsed.push(line);
|
|
}
|
|
}
|
|
|
|
// Replace list in-place + reset dead list
|
|
wiflixFreeProxies.length = 0;
|
|
wiflixFreeProxies.push(...parsed);
|
|
wiflixProxyAgentCache.clear();
|
|
wiflixDeadProxies.clear();
|
|
|
|
console.log(
|
|
`[WIFLIX FREE PROXY] Refresh OK: ${wiflixFreeProxies.length} http proxies (${lines.length - parsed.length} socks ignores)`,
|
|
);
|
|
} catch (err) {
|
|
console.error(
|
|
`[WIFLIX FREE PROXY] Refresh echoue: ${err.message}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function getWiflixFreeProxyAgent(proxyUrl) {
|
|
if (wiflixProxyAgentCache.has(proxyUrl)) {
|
|
return wiflixProxyAgentCache.get(proxyUrl);
|
|
}
|
|
|
|
const agents = {
|
|
httpAgent: new HttpProxyAgent(proxyUrl),
|
|
httpsAgent: new HttpsProxyAgent(proxyUrl),
|
|
};
|
|
|
|
wiflixProxyAgentCache.set(proxyUrl, agents);
|
|
return agents;
|
|
}
|
|
|
|
/**
|
|
* Wiflix request — tries free HTTP proxies first, then falls back to Cloudflare Workers.
|
|
*/
|
|
async function makeWiflixRequest(targetUrl, options = {}) {
|
|
const {
|
|
timeout = 10000,
|
|
headers = {},
|
|
decompress = true,
|
|
method: requestMethod,
|
|
data: requestData,
|
|
...otherOptions
|
|
} = options;
|
|
|
|
const method = (requestMethod || "GET").toUpperCase();
|
|
const defaultHeaders = {
|
|
accept:
|
|
"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
|
|
"accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
|
|
"user-agent":
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
|
|
...headers,
|
|
};
|
|
|
|
// --- Phase 1: free proxy list (skip already-dead ones) ---
|
|
const alive = wiflixFreeProxies.filter((p) => !wiflixDeadProxies.has(p));
|
|
if (alive.length > 0) {
|
|
const shuffled = alive.sort(() => Math.random() - 0.5);
|
|
const candidates = shuffled.slice(0, WIFLIX_FREE_PROXY_MAX_ATTEMPTS);
|
|
|
|
for (let i = 0; i < candidates.length; i++) {
|
|
const proxyUrl = candidates[i];
|
|
try {
|
|
const agents = getWiflixFreeProxyAgent(proxyUrl);
|
|
const response = await axios({
|
|
url: targetUrl,
|
|
method,
|
|
headers: defaultHeaders,
|
|
timeout: WIFLIX_FREE_PROXY_TIMEOUT,
|
|
decompress,
|
|
...agents,
|
|
...(requestData !== undefined ? { data: requestData } : {}),
|
|
...otherOptions,
|
|
});
|
|
|
|
// Verify we got a real response (not a proxy error page)
|
|
const body = typeof response.data === "string" ? response.data : "";
|
|
if (response.status === 200 && body.length > 100) {
|
|
console.log(
|
|
`[WIFLIX FREE PROXY] OK via ${proxyUrl} (tentative ${i + 1}/${candidates.length})`,
|
|
);
|
|
return response;
|
|
}
|
|
|
|
// Bad response — mark dead
|
|
wiflixDeadProxies.add(proxyUrl);
|
|
} catch {
|
|
wiflixDeadProxies.add(proxyUrl);
|
|
wiflixProxyAgentCache.delete(proxyUrl);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`[WIFLIX FREE PROXY] ${candidates.length} echoues, ${alive.length - candidates.length} restants, fallback CF Workers`,
|
|
);
|
|
}
|
|
|
|
// --- Phase 2: fallback to Cloudflare Workers ---
|
|
return makeRequestWithCorsFallback(targetUrl, options);
|
|
}
|
|
|
|
// Initial fetch + 5-min refresh
|
|
fetchWiflixFreeProxies();
|
|
setInterval(() => {
|
|
fetchWiflixFreeProxies();
|
|
}, WIFLIX_FREE_PROXY_REFRESH_MS).unref();
|
|
|
|
// Fonction utilitaire pour choisir aléatoirement un proxy (toujours utiliser un proxy)
|
|
function pickRandomProxyOrNone() {
|
|
// Sélectionner toujours un proxy aléatoire parmi la liste
|
|
if (!PROXIES || PROXIES.length === 0) return null;
|
|
const idx = Math.floor(Math.random() * PROXIES.length);
|
|
return PROXIES[idx] || null;
|
|
}
|
|
|
|
// Sélectionne un proxy aléatoire — retourne null si la liste est vide
|
|
function pickRandomProxy() {
|
|
if (!PROXIES || PROXIES.length === 0) return null;
|
|
const idx = Math.floor(Math.random() * PROXIES.length);
|
|
return PROXIES[idx];
|
|
}
|
|
|
|
// Fonction utilitaire pour cr\u00e9er un agent proxy SOCKS5 (avec cache)
|
|
function getProxyAgent(proxy) {
|
|
if (!proxy) return null;
|
|
const auth = proxy.auth ? `${proxy.auth}@` : "";
|
|
const proxyType = normalizeProxyType(proxy.type, "socks5");
|
|
const cacheKey = `${proxyType}:${proxy.host}:${proxy.port}:${auth}`;
|
|
|
|
// V\u00e9rifier le cache d'abord
|
|
if (proxyAgentCache.has(cacheKey)) {
|
|
return proxyAgentCache.get(cacheKey);
|
|
}
|
|
|
|
const protocol = proxyType === "socks5" ? "socks5h" : proxyType;
|
|
const proxyUrl = `${protocol}://${auth}${proxy.host}:${proxy.port}`;
|
|
const agent = new SocksProxyAgent(proxyUrl);
|
|
|
|
// Mettre en cache l'agent
|
|
proxyAgentCache.set(cacheKey, agent);
|
|
return agent;
|
|
}
|
|
|
|
// Fonction utilitaire pour cr\u00e9er un agent proxy (pour Darkino) - avec cache
|
|
function getDarkinoHttpProxyAgent(proxy) {
|
|
if (!proxy) return null;
|
|
const auth = proxy.auth ? `${proxy.auth}@` : "";
|
|
const proxyType = normalizeProxyType(proxy.type, "http");
|
|
const cacheKey = `${proxyType}:${proxy.host}:${proxy.port}:${auth}`;
|
|
|
|
// V\u00e9rifier le cache d'abord
|
|
if (darkinoProxyAgentCache.has(cacheKey)) {
|
|
return darkinoProxyAgentCache.get(cacheKey);
|
|
}
|
|
|
|
let agents;
|
|
if (proxyType === "socks5") {
|
|
const proxyUrl = `socks5://${auth}${proxy.host}:${proxy.port}`;
|
|
const agent = new SocksProxyAgent(proxyUrl);
|
|
agents = {
|
|
httpAgent: agent,
|
|
httpsAgent: agent,
|
|
};
|
|
} else {
|
|
const proxyUrl = `http://${auth}${proxy.host}:${proxy.port}`;
|
|
agents = {
|
|
httpAgent: new HttpProxyAgent(proxyUrl),
|
|
httpsAgent: new HttpsProxyAgent(proxyUrl),
|
|
};
|
|
}
|
|
|
|
// Mettre en cache les agents
|
|
darkinoProxyAgentCache.set(cacheKey, agents);
|
|
return agents;
|
|
}
|
|
|
|
module.exports = {
|
|
// Proxy agent caches
|
|
proxyAgentCache,
|
|
darkinoProxyAgentCache,
|
|
|
|
// Proxy configuration flags
|
|
ENABLE_DARKINO_PROXY,
|
|
ENABLE_COFLIX_PROXY,
|
|
ENABLE_FRENCH_STREAM_PROXY,
|
|
ENABLE_LECTEURVIDEO_PROXY,
|
|
ENABLE_FSTREAM_PROXY,
|
|
ENABLE_ANIME_PROXY,
|
|
ENABLE_WIFLIX_PROXY,
|
|
darkiworld_premium,
|
|
|
|
// Darkino 403 cooldown (expose getter/setter since it's mutable)
|
|
DARKINO_403_COOLDOWN_MS,
|
|
get darkino403CooldownUntil() {
|
|
return darkino403CooldownUntil;
|
|
},
|
|
set darkino403CooldownUntil(val) {
|
|
darkino403CooldownUntil = val;
|
|
},
|
|
|
|
// Cpasmal
|
|
CPASMAL_BASE_URL,
|
|
cpasmalJar,
|
|
getCpasmalAgent,
|
|
axiosCpasmalRequest,
|
|
|
|
// Cloudflare Workers proxies
|
|
CLOUDFLARE_WORKERS_PROXIES,
|
|
proxyErrorCache,
|
|
markProxyAsErrored,
|
|
isProxyInCooldown,
|
|
getAvailableProxies,
|
|
markProxyAsHealthy,
|
|
buildProxiedUrl,
|
|
|
|
// Request helpers
|
|
makeRequestWithCorsFallback,
|
|
makeWiflixRequest,
|
|
makeCoflixRequest,
|
|
makeLecteurVideoRequest,
|
|
makeAnimeSamaRequest,
|
|
|
|
// SOCKS5 proxies
|
|
PROXIES,
|
|
HTTP_PROXIES,
|
|
DARKINO_HTTP_PROXIES,
|
|
DARKINO_PROXIES,
|
|
refreshProxyScrapeProxies,
|
|
|
|
// Agent helpers
|
|
pickRandomProxyOrNone,
|
|
pickRandomProxy,
|
|
pickNextSocks5Proxy,
|
|
getProxyAgent,
|
|
getDarkinoHttpProxyAgent,
|
|
|
|
// Cleanup
|
|
shutdownCycleTLS,
|
|
};
|