MovixOpenSource/API/Mainapi/routes/proxy.js
Movix 1401dc20f3 tird release
mon compte github a été détecté comme du spam je crois
2026-04-28 17:55:00 +02:00

313 lines
12 KiB
JavaScript

/**
* Proxy route.
* Extracted from server.js -- raw HTML proxy with domain-specific headers.
* Mount point: app.use('/proxy', router)
*/
const express = require('express');
const router = express.Router();
const axios = require('axios');
const crypto = require('crypto');
const path = require('path');
const fsp = require('fs').promises;
const { safeWriteJsonFile } = require('../utils/safeFile');
const { CACHE_DIR } = require('../utils/cacheManager');
// === Routes ===
// GET /* - Proxy route for raw HTML (replaces old /proxy route)
router.get(/^\/(.*)/, async (req, res) => {
try {
// Extract the target URL after /
let targetUrl = req.url.slice(1); // Remove leading '/'
// Decode the URL recursively if it's encoded (handles double/triple encoding)
try {
let decoded = targetUrl;
let previousDecoded = '';
// Keep decoding until the URL doesn't change anymore (handles multiple encodings)
while (decoded !== previousDecoded) {
previousDecoded = decoded;
try {
decoded = decodeURIComponent(decoded);
} catch (e) {
// If decoding fails, break the loop
break;
}
}
targetUrl = decoded;
} catch (decodeError) {
// If decoding fails, use the original URL
console.warn('Failed to decode URL:', targetUrl, decodeError.message);
}
// Fix recursive proxying issue - remove any localhost/proxy/ patterns
const localhostProxyPattern = /localhost(:\d+)?\/proxy\//i;
if (localhostProxyPattern.test(targetUrl)) {
console.log(`Detected recursive proxy request in: ${targetUrl}`);
targetUrl = targetUrl.replace(localhostProxyPattern, '');
console.log(`Corrected to: ${targetUrl}`);
}
// Check if the URL starts with http(s)://, if not, prepend https://
if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
targetUrl = 'https://' + targetUrl;
}
// === TVDIRECT SPECIAL HANDLING ===
if (targetUrl.includes('tvdirect.ddns')) {
try {
const cacheKey = crypto.createHash('md5').update(targetUrl).digest('hex');
// Ensure directory exists
await fsp.mkdir(CACHE_DIR.TVDIRECT, { recursive: true });
const cacheFile = path.join(CACHE_DIR.TVDIRECT, `${cacheKey}.json`);
// Helper to fetch external resource
const fetchTvDirect = async () => {
console.log(`[TVDIRECT] Fetching ${targetUrl}`);
const resp = await axios.get(targetUrl, {
headers: {
'User-Agent': 'stremio',
'Accept': '*/*'
},
responseType: 'text',
timeout: 15000
});
return {
data: resp.data,
headers: resp.headers,
timestamp: Date.now()
};
};
// Try to read from cache
let cachedEntry = null;
try {
const fileContent = await fsp.readFile(cacheFile, 'utf8');
cachedEntry = JSON.parse(fileContent);
} catch (e) { /* No cache or invalid */ }
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
const now = Date.now();
if (cachedEntry && cachedEntry.data) {
// Serve cached response
if (cachedEntry.headers && cachedEntry.headers['content-type']) {
res.setHeader('Content-Type', cachedEntry.headers['content-type']);
}
res.setHeader('Access-Control-Allow-Origin', '*');
res.send(cachedEntry.data);
// Check if stale for background update
if (now - cachedEntry.timestamp > CACHE_TTL) {
console.log(`[TVDIRECT] Cache stale for ${targetUrl}, updating in background...`);
// Background update (no await)
fetchTvDirect().then(async (newData) => {
await safeWriteJsonFile(cacheFile, newData);
console.log(`[TVDIRECT] Background update success for ${targetUrl}`);
}).catch(err => console.error(`[TVDIRECT] Background update failed: ${err.message}`));
}
return; // Exit route
}
// No cache, fetch synchronously
const newData = await fetchTvDirect();
await safeWriteJsonFile(cacheFile, newData);
if (newData.headers && newData.headers['content-type']) {
res.setHeader('Content-Type', newData.headers['content-type']);
}
res.setHeader('Access-Control-Allow-Origin', '*');
res.send(newData.data);
return; // Exit route
} catch (error) {
console.error(`[TVDIRECT] Error handling request:`, error);
return res.status(502).json({ error: 'TVDirect proxy failed', details: error.message });
}
}
// Préparer les headers à forwarder
let refererOrigin;
let targetHost;
try {
const urlObj = new URL(targetUrl);
refererOrigin = urlObj.origin;
targetHost = urlObj.host;
} catch (urlError) {
// Si l'URL est invalide, utiliser une valeur par défaut
console.warn('Invalid URL for referer:', targetUrl, urlError.message);
refererOrigin = 'https://vmwesa.online';
targetHost = 'vmwesa.online';
}
// Headers spécifiques pour vmwesa/vidmoly et certains CDN (ex: getromes.space)
const isVmwesa = /vmwesa\.online|vidmoly|getromes\.space/i.test(targetUrl);
// Headers spécifiques pour dropcdn
const isDropcdn = /dropcdn/i.test(targetUrl);
// Headers spécifiques pour serversicuro
const isServersicuro = /serversicuro/i.test(targetUrl);
// Headers spécifiques pour coflix
const isCoflix = /coflix\.(bet|si|boo|io)/i.test(targetUrl);
const headers = isCoflix ? {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr;q=0.7',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Priority': 'u=0, i',
'Referer': 'https://coflix.click/',
'Sec-CH-UA': '"Brave";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
'Sec-CH-UA-Mobile': '?0',
'Sec-CH-UA-Platform': '"Windows"',
'Sec-Fetch-Dest': 'iframe',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'cross-site',
'Sec-Fetch-Storage-Access': 'none',
'Sec-GPC': '1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
} : isVmwesa ? {
'Accept': 'application/vnd.apple.mpegurl,application/x-mpegURL,*/*',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Accept-Language': 'fr-FR,fr;q=0.8',
'Connection': 'keep-alive',
'Host': targetHost,
'Origin': 'https://vidmoly.net',
'Referer': 'https://vidmoly.net/',
'Sec-CH-UA': '"Not;A=Brand";v="99", "Brave";v="139", "Chromium";v="139"',
'Sec-CH-UA-Mobile': '?0',
'Sec-CH-UA-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'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/139.0.0.0 Safari/537.36'
} : isDropcdn ? {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Accept-Language': 'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
'Connection': 'keep-alive',
'Host': targetHost,
'Origin': 'https://dropload.tv',
'Referer': 'https://dropload.tv/',
'Sec-CH-UA': '"Not;A=Brand";v="99", "Brave";v="139", "Chromium";v="139"',
'Sec-CH-UA-Mobile': '?0',
'Sec-CH-UA-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'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/139.0.0.0 Safari/537.36'
} : isServersicuro ? {
'Accept': '*/*',
'Accept-Encoding': 'gzip, deflate, br, zstd',
'Accept-Language': 'fr-FR,fr;q=0.8',
'Connection': 'keep-alive',
'Host': targetHost,
'Origin': 'https://supervideo.cc',
'Referer': 'https://supervideo.cc/',
'Sec-CH-UA': '"Not;A=Brand";v="99", "Brave";v="139", "Chromium";v="139"',
'Sec-CH-UA-Mobile': '?0',
'Sec-CH-UA-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'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/139.0.0.0 Safari/537.36'
} : {
'User-Agent': req.headers['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',
'Referer': refererOrigin,
'Accept': req.headers['accept'] || '*/*',
'Accept-Encoding': req.headers['accept-encoding'] || 'gzip, deflate, br',
'Connection': 'keep-alive',
};
// Forward Range header if present (important for video streaming)
if (req.headers['range']) {
headers['Range'] = req.headers['range'];
}
// Détecter si c'est un .m3u8 (playlist HLS)
const isM3U8 = targetUrl.toLowerCase().includes('.m3u8') || (req.headers.accept && req.headers.accept.includes('application/vnd.apple.mpegurl'));
// Faire la requête distante avec l'agent de streaming pour le proxy
const response = await axios({
method: 'get',
url: targetUrl,
responseType: isM3U8 ? 'text' : 'stream',
headers,
timeout: 30000,
maxRedirects: 5,
validateStatus: status => true, // On gère nous-même les codes d'erreur
decompress: true // Disable automatic decompression for better performance
});
// Copier les headers utiles
Object.entries(response.headers).forEach(([key, value]) => {
// Éviter certains headers problématiques
if (!['transfer-encoding', 'connection', 'content-encoding'].includes(key.toLowerCase())) {
res.setHeader(key, value);
}
});
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS', 'DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Range');
// Si c'est un .m3u8, retourner le contenu original sans modification
if (isM3U8 && typeof response.data === 'string') {
const contentType = response.headers['content-type'] || '';
const bodyText = response.data;
// Retourner le contenu M3U8 original sans modification
if (contentType) res.setHeader('content-type', contentType);
res.status(response.status).send(bodyText);
return;
}
// Gestion du code de retour (206 pour Range, sinon code d'origine)
res.status(response.status);
// Détruire le stream upstream si le client se déconnecte pour éviter les fuites mémoire
const upstream = response.data;
res.on('close', () => {
if (!upstream.destroyed) {
upstream.destroy();
}
});
upstream.on('error', (err) => {
console.error('Proxy upstream error:', err.message);
if (!res.headersSent) {
res.status(502).json({ error: 'Upstream stream error', message: err.message });
}
if (!upstream.destroyed) upstream.destroy();
});
upstream.pipe(res);
} catch (error) {
console.error('Proxy error:', error.message);
if (error.response) {
res.status(error.response.status).json({
error: `Target server responded with ${error.response.status}`,
message: error.message
});
} else if (error.request) {
res.status(504).json({
error: 'Gateway Timeout',
message: 'No response received from target server'
});
} else {
res.status(500).json({
error: 'Internal Server Error',
message: error.message
});
}
}
});
module.exports = router;