From 6cd2b9a19e8a5777d8d42d42dc5a49cccf901b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CA=9C=C9=AA=C9=B4=E1=B4=87=C9=AA=20N=E1=B4=8F=E1=B4=9C?= =?UTF-8?q?=E1=B4=A2=E1=B4=87=C9=B4?= <157171073+Shineii86@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:22:34 +0530 Subject: [PATCH] feat: return raw CDN URLs for streams (browser loads directly via CORS *); keep subtitle proxy for third-party CDNs --- api/proxy.js | 89 +++++++++++++++++++++++++++++++++++---------- src/helpers/pipe.js | 43 ++++++++++------------ 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/api/proxy.js b/api/proxy.js index 7fad166..96f3064 100644 --- a/api/proxy.js +++ b/api/proxy.js @@ -3,13 +3,36 @@ const axios = require("axios"); const PROXY_KEY = Buffer.from("a54d389c18527d9fd3e7f0643e27edbe", "hex"); const PRU_BASE = "https://pru.ultracloud.cc/"; -function encodeForPru(rawUrl) { - const urlBytes = Buffer.from(rawUrl, "utf-8"); - const xored = Buffer.alloc(urlBytes.length); - for (let i = 0; i < urlBytes.length; i++) { - xored[i] = urlBytes[i] ^ PROXY_KEY[i % PROXY_KEY.length]; +function base64urlXor(str) { + const buf = Buffer.from(str, "utf-8"); + const xored = Buffer.alloc(buf.length); + for (let i = 0; i < buf.length; i++) { + 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) { @@ -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" }); try { - const headers = { - "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: referer || "https://vidtube.site/", - }; + const decoded = decodePruUrl(url); + const isPru = !!decoded; + const targetUrl = decoded ? decoded.url : url; + const targetReferer = decoded ? decoded.referer : (referer || "https://www.miruro.tv/"); - const response = await axios.get(url, { - headers, + let fetchUrl; + 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, responseType: "arraybuffer", validateStatus: (s) => s < 500, @@ -38,26 +79,34 @@ module.exports = async function handler(req, res) { const contentType = response.headers["content-type"] || ""; 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"); - const baseUrl = new URL(url); - // Rewrite all URLs to go through pru.ultracloud.cc proxy 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) => { - const absolute = new URL(match, baseUrl).href; - return encodeForPru(absolute); + try { + 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"); return res.send(m3u8); } - // For segments/subtitles, pass through directly (they have CORS) res.setHeader("Content-Type", contentType); + if (response.headers["content-length"]) { + res.setHeader("Content-Length", response.headers["content-length"]); + } res.send(Buffer.from(response.data)); } catch (err) { res.status(500).json({ success: false, message: "Proxy failed: " + err.message }); diff --git a/src/helpers/pipe.js b/src/helpers/pipe.js index 33e2758..350df96 100644 --- a/src/helpers/pipe.js +++ b/src/helpers/pipe.js @@ -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. - * The /api/proxy endpoint rewrites M3U8 content and relays segments. + * CDN streams (mt.nekostream.site) have CORS * — browsers load them directly. + * Subtitles use third-party CDNs that may block cross-origin — proxy those. * * @type {string} */ const PROXY_BASE = "/api/proxy?url="; /** - * Encodes a raw CDN URL into a proxied URL through our API. - * The proxy endpoint handles M3U8 rewriting and CORS headers. + * Proxies a URL through /api/proxy for CORS bypass. * - * @param {string} url - Raw CDN URL - * @returns {string} Proxied URL through /api/proxy + * @param {string} url - Raw URL + * @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; try { - return PROXY_BASE + encodeURIComponent(url); + const ref = referer ? "&referer=" + encodeURIComponent(referer) : ""; + return PROXY_BASE + encodeURIComponent(url) + ref; } catch { return url; } }; /** - * Proxies all stream URLs in a sources response. - * Replaces raw CDN URLs with proxied API URLs. + * Streams are returned as-is (raw CDN 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 - * @returns {object} Sources with proxied stream URLs + * @returns {object} Sources with raw stream URLs */ const proxyStreams = (sources) => { - if (!sources?.streams) return sources; - sources.streams = sources.streams.map((s) => { - 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/", - }; - }); + // NOTE: Intentionally return raw CDN URLs — the CDN serves real video to browsers + // but decoy PNG images to server-side requests. The browser's residential IP is required. 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 * @returns {object} Sources with proxied subtitle URLs @@ -151,7 +146,7 @@ const proxySubtitles = (sources) => { sources.subtitles = sources.subtitles.map((s) => { const raw = s.url || s.file; 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 sources;