| |
| |
| |
|
|
|
|
| import express from 'express';
|
| import fetch from 'node-fetch';
|
|
|
| const app = express();
|
| const PORT = process.env.PORT || 3000;
|
|
|
|
|
| const CDN_RULES = [
|
| { test: h => h.endsWith('.otakuu.se') || h === 'otakuu.se',
|
| referer: 'https://animex.one/', origin: 'https://animex.one', secSite: 'cross-site' },
|
| { test: h => h === 'vibeplayer.site' || h.endsWith('.vibeplayer.site'),
|
| referer: 'https://vibeplayer.site/', origin: 'https://vibeplayer.site', secSite: 'same-origin' },
|
| { test: h => h.endsWith('.mofl.pro') || h === 'mofl.pro',
|
| referer: 'https://kem.clvd.xyz/', origin: 'https://kem.clvd.xyz', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.vidhosters.com') || h === 'vidhosters.com',
|
| referer: 'https://kem.clvd.xyz/', origin: 'https://kem.clvd.xyz', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.burntburst45.store') || h === 'burntburst45.store',
|
| referer: null, origin: 'https://play2.echovideo.ru', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.streamzone1.site') || h === 'streamzone1.site',
|
| referer: 'https://megaplay.buzz/', origin: 'https://megaplay.buzz', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.zencloudz.cc') || h === 'zencloudz.cc',
|
| referer: 'https://aniwave.at/', origin: 'https://aniwave.at', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.cinewave2.site') || h === 'cinewave2.site',
|
| referer: 'https://megaplay.buzz/', origin: 'https://megaplay.buzz', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.watching.onl') || h === 'watching.onl',
|
| referer: 'https://vidwish.live/', origin: 'https://vidwish.live', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.krussdomi.com') || h === 'krussdomi.com',
|
| referer: 'https://krussdomi.com/', origin: 'https://krussdomi.com', secSite: 'same-origin' },
|
| { test: h => h.endsWith('.owocdn.top') || h === 'owocdn.top',
|
| referer: 'https://kwik.cx/', origin: 'https://kwik.cx', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.anime-dunya.com') || h === 'anime-dunya.com',
|
| referer: 'https://anime-dunya.com/', origin: 'https://anime-dunya.com', secSite: 'same-origin' },
|
| { test: h => h.startsWith('rrr.'),
|
| referer: 'https://megaup.nl/', origin: 'https://megaup.nl', secSite: 'cross-site' },
|
| { test: h => h === 'megaup.nl' || h.endsWith('.megaup.nl') || h === 'hub26link.site' || h.endsWith('.hub26link.site'),
|
| referer: 'https://megaup.nl/', origin: 'https://megaup.nl', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.mewstream.buzz') || h === 'mewstream.buzz',
|
| referer: 'https://megaplay.buzz/', origin: 'https://megaplay.buzz', secSite: 'cross-site' },
|
| { test: h => h.endsWith('.vid-cdn.xyz') || h === 'vid-cdn.xyz',
|
| referer: 'https://anizone.to/', origin: 'https://anizone.to', secSite: 'cross-site' },
|
| ];
|
|
|
|
|
| function b64uEncode(str) {
|
| return Buffer.from(str).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
| }
|
|
|
| function b64uDecode(b64u) {
|
| try {
|
| const b64 = b64u.replace(/-/g, '+').replace(/_/g, '/');
|
| const padded = b64 + '='.repeat((4 - b64.length % 4) % 4);
|
| return Buffer.from(padded, 'base64').toString();
|
| } catch {
|
| return null;
|
| }
|
| }
|
|
|
| function encodePayload(url, referer) {
|
| return b64uEncode(url + '\0' + (referer || ''));
|
| }
|
|
|
| function decodePayload(b64u) {
|
| const plain = b64uDecode(b64u);
|
| if (!plain) return null;
|
| const idx = plain.indexOf('\0');
|
| if (idx === -1) return { url: plain, ref: null };
|
| return { url: plain.slice(0, idx), ref: plain.slice(idx + 1) || null };
|
| }
|
|
|
|
|
| function browserHeaders(referer, origin, secSite) {
|
| return {
|
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
| 'Accept': '*/*',
|
| 'Accept-Language': 'en-US,en;q=0.9',
|
| 'Accept-Encoding': 'gzip, deflate, br',
|
| 'Referer': referer,
|
| 'Origin': origin,
|
| 'Sec-Fetch-Dest': 'empty',
|
| 'Sec-Fetch-Mode': 'cors',
|
| 'Sec-Fetch-Site': secSite || 'cross-site',
|
| 'Sec-CH-UA': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"',
|
| 'Sec-CH-UA-Mobile': '?0',
|
| 'Sec-CH-UA-Platform': '"Windows"',
|
| 'Connection': 'keep-alive',
|
| 'Cache-Control': 'no-cache',
|
| 'Pragma': 'no-cache',
|
| };
|
| }
|
|
|
|
|
| function corsHeaders() {
|
| return {
|
| 'Access-Control-Allow-Origin': '*',
|
| 'Access-Control-Allow-Methods': 'GET, HEAD, OPTIONS',
|
| 'Access-Control-Allow-Headers': 'Range, Content-Type',
|
| 'Access-Control-Expose-Headers': 'Content-Length, Content-Range, Content-Type, Accept-Ranges',
|
| 'Accept-Ranges': 'bytes',
|
| };
|
| }
|
|
|
|
|
| function resolveUrl(rel, base) {
|
| if (/^https?:\/\//i.test(rel)) return rel;
|
| try { return new URL(rel, base).href; } catch { return rel; }
|
| }
|
|
|
|
|
| function rewriteM3u8(text, baseUrl, referer, serverBase) {
|
| const lines = text.split('\n');
|
| return lines.map(raw => {
|
| const line = raw.trim();
|
|
|
| if (line.startsWith('#') && line.includes('URI="')) {
|
| return line.replace(/URI="([^"]+)"/g, (_, uri) => {
|
| const abs = resolveUrl(uri, baseUrl);
|
| return `URI="${serverBase}/p/${encodePayload(abs, referer)}"`;
|
| });
|
| }
|
|
|
| if (line && !line.startsWith('#')) {
|
| const abs = resolveUrl(line, baseUrl);
|
| return `${serverBase}/p/${encodePayload(abs, referer)}`;
|
| }
|
|
|
| return raw;
|
| }).join('\n');
|
| }
|
|
|
|
|
| async function proxyTarget(targetUrl, refParam, request, serverBase) {
|
| let parsedTarget;
|
| try {
|
| parsedTarget = new URL(targetUrl);
|
| if (parsedTarget.protocol !== 'https:') throw new Error('https only');
|
| } catch {
|
| return { status: 400, json: { error: 'Invalid target URL β must be https' } };
|
| }
|
|
|
| const targetHost = parsedTarget.hostname.toLowerCase();
|
| const overrideReferer = refParam || null;
|
|
|
| const rule = CDN_RULES.find(r => r.test(targetHost));
|
| let effectiveReferer, effectiveOrigin, effectiveSecSite;
|
|
|
| if (rule) {
|
| effectiveReferer = overrideReferer || rule.referer || `https://${targetHost}/`;
|
| effectiveOrigin = rule.origin || `https://${targetHost}`;
|
| effectiveSecSite = rule.secSite || 'cross-site';
|
| } else if (overrideReferer) {
|
| try {
|
| const refUrl = new URL(overrideReferer);
|
| effectiveReferer = overrideReferer;
|
| effectiveOrigin = refUrl.origin;
|
| effectiveSecSite = 'cross-site';
|
| } catch {
|
| effectiveReferer = overrideReferer;
|
| effectiveOrigin = `https://${targetHost}`;
|
| effectiveSecSite = 'cross-site';
|
| }
|
| } else {
|
| return { status: 403, json: { error: `Host not in CDN_RULES and no referer provided: ${targetHost}` } };
|
| }
|
|
|
| const headers = browserHeaders(effectiveReferer, effectiveOrigin, effectiveSecSite);
|
| const rangeHeader = request.headers.get ? request.headers.get('Range') : request.headers.range;
|
| if (rangeHeader) headers['Range'] = rangeHeader;
|
|
|
| let upstreamResp;
|
| try {
|
| upstreamResp = await fetch(targetUrl, {
|
| method: request.method === 'HEAD' ? 'HEAD' : 'GET',
|
| headers,
|
| redirect: 'follow',
|
| });
|
| } catch (err) {
|
| return { status: 502, json: { error: 'Upstream fetch failed', detail: String(err) } };
|
| }
|
|
|
| if (!upstreamResp.ok && upstreamResp.status !== 206) {
|
| return { status: upstreamResp.status, json: { error: 'Upstream error', status: upstreamResp.status } };
|
| }
|
|
|
| const contentType = (upstreamResp.headers.get('content-type') || '').toLowerCase();
|
| const isM3u8 = contentType.includes('mpegurl') || contentType.includes('x-mpegurl')
|
| || targetUrl.split('?')[0].endsWith('.m3u8');
|
|
|
| if (request.method === 'HEAD') {
|
| return { status: upstreamResp.status, headers: {
|
| 'Content-Type': upstreamResp.headers.get('content-type') || 'application/octet-stream',
|
| ...corsHeaders(),
|
| }, body: null };
|
| }
|
|
|
| if (isM3u8) {
|
| const text = await upstreamResp.text();
|
| const rewritten = rewriteM3u8(text, targetUrl, effectiveReferer, serverBase);
|
| return { status: upstreamResp.status, headers: {
|
| 'Content-Type': 'application/vnd.apple.mpegurl',
|
| 'Cache-Control': 'no-cache',
|
| ...corsHeaders(),
|
| }, body: rewritten };
|
| }
|
|
|
| const passHeaders = {
|
| 'Content-Type': upstreamResp.headers.get('content-type') || 'application/octet-stream',
|
| 'Cache-Control': 'public, max-age=86400, immutable',
|
| ...corsHeaders(),
|
| };
|
| const cl = upstreamResp.headers.get('content-length');
|
| if (cl) passHeaders['Content-Length'] = cl;
|
| const cr = upstreamResp.headers.get('content-range');
|
| if (cr) passHeaders['Content-Range'] = cr;
|
|
|
| return { status: upstreamResp.status, headers: passHeaders, body: upstreamResp.body };
|
| }
|
|
|
|
|
| app.use((req, res, next) => {
|
| Object.assign(res.corsHeaders = {}, corsHeaders());
|
| res.set(res.corsHeaders);
|
| if (req.method === 'OPTIONS') return res.status(204).end();
|
| next();
|
| });
|
|
|
| app.get('/health', (req, res) => {
|
| res.json({ ok: true, worker: 'yumezone-nodejs', ts: Date.now() });
|
| });
|
|
|
| app.get('/p/:b64u', async (req, res) => {
|
| const decoded = decodePayload(req.params.b64u);
|
| if (!decoded) return res.status(400).json({ error: 'Invalid payload' });
|
|
|
| const serverBase = `${req.protocol}://${req.get('host')}`;
|
| const result = await proxyTarget(decoded.url, decoded.ref, req, serverBase);
|
|
|
| if (result.json) return res.status(result.status).set(result.headers || {}).json(result.json);
|
| res.status(result.status).set(result.headers || {});
|
| if (result.body) res.send(result.body);
|
| else res.end();
|
| });
|
|
|
| app.get('/proxy', async (req, res) => {
|
| const targetRaw = req.query.url;
|
| if (!targetRaw) return res.status(400).json({ error: 'Missing ?url= parameter' });
|
|
|
| let targetUrl;
|
| try { targetUrl = decodeURIComponent(targetRaw); } catch {
|
| return res.status(400).json({ error: 'Bad URL encoding' });
|
| }
|
|
|
| const refParam = req.query.ref;
|
| const serverBase = `${req.protocol}://${req.get('host')}`;
|
| const result = await proxyTarget(targetUrl, refParam, req, serverBase);
|
|
|
| if (result.json) return res.status(result.status).set(result.headers || {}).json(result.json);
|
| res.status(result.status).set(result.headers || {});
|
| if (result.body) res.send(result.body);
|
| else res.end();
|
| });
|
|
|
| app.use((req, res) => res.status(404).send('Not found'));
|
|
|
| app.listen(PORT, '0.0.0.0', () => {
|
| console.log(`π YumeZone Proxy running on http://0.0.0.0:${PORT}`);
|
| });
|
|
|