feat: return raw CDN URLs for streams (browser loads directly via CORS *); keep subtitle proxy for third-party CDNs

This commit is contained in:
Sʜɪɴᴇɪ Nᴏᴜᴢᴇɴ
2026-06-25 08:22:34 +05:30
parent 76c47368a7
commit 6cd2b9a19e
2 changed files with 88 additions and 44 deletions
+69 -20
View File
@@ -3,13 +3,36 @@ const axios = require("axios");
const PROXY_KEY = Buffer.from("a54d389c18527d9fd3e7f0643e27edbe", "hex"); const PROXY_KEY = Buffer.from("a54d389c18527d9fd3e7f0643e27edbe", "hex");
const PRU_BASE = "https://pru.ultracloud.cc/"; const PRU_BASE = "https://pru.ultracloud.cc/";
function encodeForPru(rawUrl) { function base64urlXor(str) {
const urlBytes = Buffer.from(rawUrl, "utf-8"); const buf = Buffer.from(str, "utf-8");
const xored = Buffer.alloc(urlBytes.length); const xored = Buffer.alloc(buf.length);
for (let i = 0; i < urlBytes.length; i++) { for (let i = 0; i < buf.length; i++) {
xored[i] = urlBytes[i] ^ PROXY_KEY[i % PROXY_KEY.length]; xored[i] = buf[i] ^ PROXY_KEY[i % PROXY_KEY.length];
} }
return PRU_BASE + xored.toString("base64url"); return xored.toString("base64url");
}
function decodePruUrl(pruUrl) {
try {
const path = new URL(pruUrl).pathname.replace(/^\//, "").replace(/\/pl\.m3u8$/, "").replace(/\/seg\.jpg$/, "");
const tildeIdx = path.indexOf("~");
if (tildeIdx === -1) return null;
const urlPart = path.substring(0, tildeIdx);
const refPart = path.substring(tildeIdx + 1);
const urlBuf = Buffer.from(urlPart, "base64url");
const url = Buffer.from(Array.from(urlBuf).map((b, i) => b ^ PROXY_KEY[i % PROXY_KEY.length])).toString("utf-8");
const refBuf = Buffer.from(refPart, "base64url");
const referer = Buffer.from(Array.from(refBuf).map((b, i) => b ^ PROXY_KEY[i % PROXY_KEY.length])).toString("utf-8");
return { url, referer };
} catch {
return null;
}
}
function encodeForPru(rawUrl, referer) {
const urlEnc = base64urlXor(rawUrl);
const refEnc = base64urlXor(referer || new URL(rawUrl).origin + "/");
return `${PRU_BASE}${urlEnc}~${refEnc}/pl.m3u8`;
} }
module.exports = async function handler(req, res) { module.exports = async function handler(req, res) {
@@ -23,13 +46,31 @@ module.exports = async function handler(req, res) {
if (!url) return res.status(400).json({ success: false, message: "url query parameter is required" }); if (!url) return res.status(400).json({ success: false, message: "url query parameter is required" });
try { try {
const headers = { const decoded = decodePruUrl(url);
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", const isPru = !!decoded;
Referer: referer || "https://vidtube.site/", const targetUrl = decoded ? decoded.url : url;
}; const targetReferer = decoded ? decoded.referer : (referer || "https://www.miruro.tv/");
const response = await axios.get(url, { let fetchUrl;
headers, let fetchHeaders;
if (isPru) {
fetchUrl = encodeForPru(targetUrl, targetReferer);
fetchHeaders = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
Referer: "https://www.miruro.tv/",
Origin: "https://www.miruro.tv",
};
} else {
fetchUrl = targetUrl;
fetchHeaders = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
Referer: targetReferer,
};
}
const response = await axios.get(fetchUrl, {
headers: fetchHeaders,
timeout: 15000, timeout: 15000,
responseType: "arraybuffer", responseType: "arraybuffer",
validateStatus: (s) => s < 500, validateStatus: (s) => s < 500,
@@ -38,26 +79,34 @@ module.exports = async function handler(req, res) {
const contentType = response.headers["content-type"] || ""; const contentType = response.headers["content-type"] || "";
res.setHeader("Cache-Control", "public, max-age=300"); res.setHeader("Cache-Control", "public, max-age=300");
if (contentType.includes("mpegurl") || url.endsWith(".m3u8") || contentType.includes("x-mpegURL")) { if (isPru && (contentType.includes("mpegurl") || targetUrl.endsWith(".m3u8") || contentType.includes("x-mpegURL"))) {
let m3u8 = Buffer.from(response.data).toString("utf-8"); let m3u8 = Buffer.from(response.data).toString("utf-8");
const baseUrl = new URL(url);
// Rewrite all URLs to go through pru.ultracloud.cc proxy
m3u8 = m3u8.replace(/^(https?:\/\/[^\s]+)$/gm, (match) => { m3u8 = m3u8.replace(/^(https?:\/\/[^\s]+)$/gm, (match) => {
return encodeForPru(match); if (match.includes("pru.ultracloud.cc")) {
const dec = decodePruUrl(match);
if (dec) return `/api/proxy?url=${encodeURIComponent(dec.url)}&referer=${encodeURIComponent(dec.referer)}`;
}
return `/api/proxy?url=${encodeURIComponent(match)}&referer=${encodeURIComponent(targetReferer)}`;
}); });
// Rewrite relative URLs
m3u8 = m3u8.replace(/^([^\s#<][^\s]*\.(m3u8|ts|m4s|m4v|mp4|cmfv|cmfa|vtt|srt|ass|json|aac|mp3))(?:\?[^\s]*)?$/gm, (match) => { m3u8 = m3u8.replace(/^([^\s#<][^\s]*\.(m3u8|ts|m4s|m4v|mp4|cmfv|cmfa|vtt|srt|ass|json|aac|mp3))(?:\?[^\s]*)?$/gm, (match) => {
const absolute = new URL(match, baseUrl).href; try {
return encodeForPru(absolute); const absolute = new URL(match, new URL(targetUrl)).href;
return `/api/proxy?url=${encodeURIComponent(absolute)}&referer=${encodeURIComponent(targetReferer)}`;
} catch {
return match;
}
}); });
res.setHeader("Content-Type", "application/vnd.apple.mpegurl"); res.setHeader("Content-Type", "application/vnd.apple.mpegurl");
return res.send(m3u8); return res.send(m3u8);
} }
// For segments/subtitles, pass through directly (they have CORS)
res.setHeader("Content-Type", contentType); res.setHeader("Content-Type", contentType);
if (response.headers["content-length"]) {
res.setHeader("Content-Length", response.headers["content-length"]);
}
res.send(Buffer.from(response.data)); res.send(Buffer.from(response.data));
} catch (err) { } catch (err) {
res.status(500).json({ success: false, message: "Proxy failed: " + err.message }); res.status(500).json({ success: false, message: "Proxy failed: " + err.message });
+19 -24
View File
@@ -94,54 +94,49 @@ const decodePipeResponse = (encodedStr) => {
} }
}; };
// ---- FEATURE: Proxy URL encoding through ultracloud CDN ---- // ---- FEATURE: Subtitle proxy for CORS-safe browser access ----
/** /**
* Proxies raw CDN URLs through our API endpoint for CORS-safe browser access. * CDN streams (mt.nekostream.site) have CORS * — browsers load them directly.
* The /api/proxy endpoint rewrites M3U8 content and relays segments. * Subtitles use third-party CDNs that may block cross-origin — proxy those.
* *
* @type {string} * @type {string}
*/ */
const PROXY_BASE = "/api/proxy?url="; const PROXY_BASE = "/api/proxy?url=";
/** /**
* Encodes a raw CDN URL into a proxied URL through our API. * Proxies a URL through /api/proxy for CORS bypass.
* The proxy endpoint handles M3U8 rewriting and CORS headers.
* *
* @param {string} url - Raw CDN URL * @param {string} url - Raw URL
* @returns {string} Proxied URL through /api/proxy * @param {string} [referer] - Optional referer to pass
* @returns {string} Proxied URL
*/ */
const proxyUrl = (url) => { const proxyUrl = (url, referer) => {
if (!url || !url.startsWith("http")) return url; if (!url || !url.startsWith("http")) return url;
try { try {
return PROXY_BASE + encodeURIComponent(url); const ref = referer ? "&referer=" + encodeURIComponent(referer) : "";
return PROXY_BASE + encodeURIComponent(url) + ref;
} catch { } catch {
return url; return url;
} }
}; };
/** /**
* Proxies all stream URLs in a sources response. * Streams are returned as-is (raw CDN URLs).
* Replaces raw CDN URLs with proxied API URLs. * The browser loads them directly — CDN has CORS * and serves real content to residential IPs.
* Server-side requests get decoy PNG images, so proxying through /api/proxy defeats the purpose.
* *
* @param {object} sources - Decoded sources with streams array * @param {object} sources - Decoded sources with streams array
* @returns {object} Sources with proxied stream URLs * @returns {object} Sources with raw stream URLs
*/ */
const proxyStreams = (sources) => { const proxyStreams = (sources) => {
if (!sources?.streams) return sources; // NOTE: Intentionally return raw CDN URLs — the CDN serves real video to browsers
sources.streams = sources.streams.map((s) => { // but decoy PNG images to server-side requests. The browser's residential IP is required.
if (s.type === "embed") return s;
const ref = s.referer || "https://vidtube.site/";
return {
...s,
url: proxyUrl(s.url) + "&referer=" + encodeURIComponent(ref),
referer: "https://mirurotvapp.vercel.app/",
};
});
return sources; return sources;
}; };
/** /**
* Proxies all subtitle URLs in a sources response. * Proxies subtitle URLs through /api/proxy for CORS bypass.
* Subtitles come from third-party CDNs that may not have CORS headers.
* *
* @param {object} sources - Decoded sources with subtitles array * @param {object} sources - Decoded sources with subtitles array
* @returns {object} Sources with proxied subtitle URLs * @returns {object} Sources with proxied subtitle URLs
@@ -151,7 +146,7 @@ const proxySubtitles = (sources) => {
sources.subtitles = sources.subtitles.map((s) => { sources.subtitles = sources.subtitles.map((s) => {
const raw = s.url || s.file; const raw = s.url || s.file;
if (!raw || !raw.startsWith("http")) return s; if (!raw || !raw.startsWith("http")) return s;
const proxied = proxyUrl(raw) + "&referer=" + encodeURIComponent("https://www.miruro.tv/"); const proxied = proxyUrl(raw, "https://www.miruro.tv/");
return { ...s, url: proxied, file: proxied }; return { ...s, url: proxied, file: proxied };
}); });
return sources; return sources;