diff --git a/src/server.js b/src/server.js index a37c4a6..cb3bd3a 100644 --- a/src/server.js +++ b/src/server.js @@ -5,11 +5,13 @@ const express = require('express'); const app = express(); const PORT = process.env.PORT || 3000; -// Our Bunny CDN pull zone (separate from Rok's folxplay zone) +// Our independent Bunny CDN pull zone — reads from same storage as Rok's, +// but with our own SecurityKey and our own tokenization. const BUNNY_HOST = 'https://folxlive.b-cdn.net'; const BUNNY_KEY = process.env.BUNNY_SECURITY_KEY || '492fb7b3-8a1d-4a78-8e9e-fae8c07af195'; +const TOKEN_TTL = parseInt(process.env.TOKEN_TTL || '14400', 10); // 4 hours default -function signBunnyUrl(urlPath, expiresInSeconds = 3600) { +function signBunnyUrl(urlPath, expiresInSeconds = TOKEN_TTL) { const expires = Math.floor(Date.now() / 1000) + expiresInSeconds; const hash = crypto.createHash('sha256').update(BUNNY_KEY + urlPath + expires).digest(); const token = hash.toString('base64') @@ -19,8 +21,12 @@ function signBunnyUrl(urlPath, expiresInSeconds = 3600) { return { token, expires, url: `${BUNNY_HOST}${urlPath}?token=${token}&expires=${expires}` }; } -// Default: stream1 master (linear, non-DVR) -const DEFAULT_HLS_PATH = process.env.HLS_PATH || '/live/stream1_master.m3u8'; +// Map stream number → CDN path. CDN serves rewritten/tokenized version of master. +// /live/stream1_master.m3u8 has variant URLs already pointing to folxplay.b-cdn.net (with tokens). +// We fetch this, then rewrite folxplay → folxlive with our own tokens. +function getMasterCdnPath(n) { + return `/live/stream${n}_master.m3u8`; +} app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, '..', 'views')); @@ -31,31 +37,93 @@ app.use(express.static(path.join(__dirname, '..', 'public'), { })); app.get('/', (_req, res) => { - const { url } = signBunnyUrl(DEFAULT_HLS_PATH, 3600); - res.render('index', { hlsUrl: url }); + // Use our proxy route for the player — it always returns fresh tokens + res.render('index', { hlsUrl: '/stream/1/master.m3u8' }); }); app.get('/test-embed', (_req, res) => { res.render('test-embed'); }); -// Token-signed master proxy: GET /stream/:n/master.m3u8 → signed Bunny URL -// Optional ?dvr=1 for DVR variant -app.get('/stream/:n/master.m3u8', (req, res) => { +/** + * Master proxy: fetches raw master from our Bunny zone, rewrites variant URLs to point + * to our zone with our tokens (4-hour TTL), and serves the result. + * + * GET /stream/:n/master.m3u8 + * Returns rewritten master.m3u8 with all variant URLs pointing to folxlive.b-cdn.net + * with fresh tokens (4 hours). + */ +app.get('/stream/:n/master.m3u8', async (req, res) => { const n = req.params.n; if (!/^[1-6]$/.test(n)) return res.status(400).send('Invalid stream number'); - const dvr = req.query.dvr === '1' ? '_dvr' : ''; - const streamPath = `/live/stream${n}_master${dvr}.m3u8`; - const { url } = signBunnyUrl(streamPath, 3600); + + const masterCdnPath = getMasterCdnPath(n); + const { url: signedMasterUrl } = signBunnyUrl(masterCdnPath, 600); // short TTL on internal fetch + + try { + const response = await fetch(signedMasterUrl, { + headers: { 'User-Agent': 'folx-live-proxy/1.0' }, + }); + if (!response.ok) { + console.error(`[stream/${n}] origin ${response.status}: ${signedMasterUrl}`); + return res.status(502).send(`Origin returned ${response.status}`); + } + let text = await response.text(); + + // Rewrite variant URLs from folxplay → folxlive zone, replacing CDN-edge-script tokens + // with our own tokens (4-hour TTL): + // https://folxplay.b-cdn.net/live/stream1_1080p.m3u8?token=ABC&expires=N + // → https://folxlive.b-cdn.net/live/stream1_1080p.m3u8?token=OURXYZ&expires=M + text = text.replace( + /https:\/\/folxplay\.b-cdn\.net(\/[^\s?]+\.m3u8)(?:\?[^\s]*)?/g, + (_match, urlPath) => { + const { url } = signBunnyUrl(urlPath, TOKEN_TTL); + return url; + } + ); + + res.set({ + 'Content-Type': 'application/vnd.apple.mpegurl', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Access-Control-Allow-Origin': '*', + }); + res.send(text); + } catch (err) { + console.error(`[stream/${n}] error:`, err.message); + res.status(502).send('Upstream error'); + } +}); + +/** + * Variant proxy: fetches a variant manifest (e.g. stream_1080p.m3u8) and rewrites .ts URLs + * to absolute folxlive paths (no token needed for .ts thanks to edge rule). + * + * Actually — variant manifests use relative paths so we don't need to rewrite anything. + * Player will resolve them against folxlive.b-cdn.net which is correct. + * + * So we just redirect to the signed origin URL with our token. + */ +app.get('/stream/:n/variant/:filename', (req, res) => { + const n = req.params.n; + const filename = req.params.filename; + if (!/^[1-6]$/.test(n)) return res.status(400).send('Invalid stream number'); + if (!/^stream(\d+)?_\d+p\.m3u8$/.test(filename)) return res.status(400).send('Invalid variant'); + const variantPath = `/live/${filename}`; + const { url } = signBunnyUrl(variantPath, TOKEN_TTL); res.redirect(302, url); }); app.get('/api/health', (_req, res) => { - res.json({ ok: true, host: BUNNY_HOST, defaultPath: DEFAULT_HLS_PATH, ts: Date.now() }); + res.json({ + ok: true, + host: BUNNY_HOST, + tokenTtlSeconds: TOKEN_TTL, + ts: Date.now(), + }); }); app.listen(PORT, () => { console.log(`[folx-live] listening on :${PORT}`); console.log(`[folx-live] CDN host: ${BUNNY_HOST}`); - console.log(`[folx-live] Default stream path: ${DEFAULT_HLS_PATH}`); + console.log(`[folx-live] Token TTL: ${TOKEN_TTL}s (${TOKEN_TTL / 3600}h)`); });