yuki / server.js
mwask's picture
Upload 6 files
aaac744 verified
Raw
History Blame Contribute Delete
12 kB
/**
* YumeZone β€” HLS Proxy Server (Node.js + Express)
* Deployable to: Hugging Face Spaces, Vercel, Railway, Heroku, etc.
*/
import express from 'express';
import fetch from 'node-fetch';
const app = express();
const PORT = process.env.PORT || 3000;
/* ─── CDN rule table ─────────────────────────────────────────────────────── */
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' },
];
/* ─── Base64url helpers ──────────────────────────────────────────────────── */
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 };
}
/* ─── Browser impersonation headers ─────────────────────────────────────── */
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',
};
}
/* ─── CORS headers ───────────────────────────────────────────────────────── */
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',
};
}
/* ─── Resolve relative URL against base ─────────────────────────────────── */
function resolveUrl(rel, base) {
if (/^https?:\/\//i.test(rel)) return rel;
try { return new URL(rel, base).href; } catch { return rel; }
}
/* ─── Rewrite M3U8: all segment/key URIs β†’ /p/<base64url> ───────────────── */
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');
}
/* ─── Core proxy logic ───────────────────────────────────────────────────── */
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 };
}
/* ─── Express Routes ───────────────────────────────────────────────────────── */
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}`);
});