mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-08-01 18:25:57 +00:00
313 lines
12 KiB
JavaScript
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;
|