From 75b57ece99b833f6a42bede86a043a84248a2c56 Mon Sep 17 00:00:00 2001 From: Bladestar2105 <55372949+Bladestar2105@users.noreply.github.com> Date: Wed, 6 May 2026 08:31:40 +0200 Subject: [PATCH] Bind optimized HLS base/data segment tokens --- src/controllers/streamController.js | 18 +++++++++---- tests/performance/segment_encryption.test.js | 27 ++++++++++++++++++-- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/controllers/streamController.js b/src/controllers/streamController.js index d72ba23..f14e8e8 100644 --- a/src/controllers/streamController.js +++ b/src/controllers/streamController.js @@ -562,7 +562,8 @@ export const proxyLive = async (req, res) => { if (cookies) headersToForward['Cookie'] = cookies; // Optimization: Encrypt headers and safe-check once - const basePayload = { h: headersToForward, s: isProviderSafe }; + const bindingId = crypto.randomUUID(); + const basePayload = { h: headersToForward, s: isProviderSafe, b: bindingId }; const baseEncrypted = encrypt(JSON.stringify(basePayload)); const baseEncoded = encodeURIComponent(baseEncrypted); @@ -572,7 +573,7 @@ export const proxyLive = async (req, res) => { try { const absoluteUrl = new URL(line, baseUrl).toString(); // Only encrypt the changing URL part - const payload = { u: absoluteUrl, c: channel.name, p: channel.provider_id }; + const payload = { u: absoluteUrl, c: channel.name, p: channel.provider_id, b: bindingId }; const encrypted = encrypt(JSON.stringify(payload)); return `/live/segment/${encodeURIComponent(req.params.username)}/${encodeURIComponent(req.params.password)}/seg.ts?data=${encodeURIComponent(encrypted)}&base=${baseEncoded}${tokenParam}`; } catch (e) { @@ -582,7 +583,7 @@ export const proxyLive = async (req, res) => { try { const absoluteUrl = new URL(p1, baseUrl).toString(); // Only encrypt the changing URL part - const payload = { u: absoluteUrl, c: channel.name, p: channel.provider_id }; + const payload = { u: absoluteUrl, c: channel.name, p: channel.provider_id, b: bindingId }; const encrypted = encrypt(JSON.stringify(payload)); return `URI="/live/segment/${encodeURIComponent(req.params.username)}/${encodeURIComponent(req.params.password)}/seg.key?data=${encodeURIComponent(encrypted)}&base=${baseEncoded}${tokenParam}"`; } catch (e) { @@ -669,6 +670,8 @@ export const proxySegment = async (req, res) => { let isOriginSafe = true; + let baseBindingId = null; + // Handle 'base' param for optimized static headers/settings if (req.query.base) { try { @@ -677,6 +680,7 @@ export const proxySegment = async (req, res) => { const basePayload = JSON.parse(decryptedBase); if (basePayload.h) Object.assign(headers, basePayload.h); if (basePayload.s === false) isOriginSafe = false; + if (basePayload.b) baseBindingId = basePayload.b; } } catch(e) {} } @@ -690,6 +694,9 @@ export const proxySegment = async (req, res) => { if (payload.u) targetUrl = payload.u; if (payload.c) channelName = payload.c; if (payload.p) providerId = payload.p; + if (req.query.base) { + if (!payload.b || !baseBindingId || payload.b !== baseBindingId) return res.sendStatus(400); + } // Merge per-segment overrides (if any, legacy support) if (payload.h) Object.assign(headers, payload.h); if (payload.s !== undefined) { @@ -1289,7 +1296,8 @@ export const proxyTimeshift = async (req, res) => { if (cookies) headersToForward['Cookie'] = cookies; // Optimization: Encrypt headers and safe-check once - const basePayload = { h: headersToForward, s: isProviderSafe }; + const bindingId = crypto.randomUUID(); + const basePayload = { h: headersToForward, s: isProviderSafe, b: bindingId }; const baseEncrypted = encrypt(JSON.stringify(basePayload)); const baseEncoded = encodeURIComponent(baseEncrypted); @@ -1299,7 +1307,7 @@ export const proxyTimeshift = async (req, res) => { try { const absoluteUrl = new URL(line, baseUrl).toString(); // Only encrypt the changing URL part - const payload = { u: absoluteUrl, c: channel.name, p: channel.provider_id }; + const payload = { u: absoluteUrl, c: channel.name, p: channel.provider_id, b: bindingId }; const encrypted = encrypt(JSON.stringify(payload)); return `/live/segment/${encodeURIComponent(req.params.username)}/${encodeURIComponent(req.params.password)}/seg.ts?data=${encodeURIComponent(encrypted)}&base=${baseEncoded}${tokenParam}`; } catch (e) { diff --git a/tests/performance/segment_encryption.test.js b/tests/performance/segment_encryption.test.js index 18ffbde..7fd3a98 100644 --- a/tests/performance/segment_encryption.test.js +++ b/tests/performance/segment_encryption.test.js @@ -101,14 +101,17 @@ describe('Segment Encryption Format', () => { it('should support OPTIMIZED format (url in "data", headers in "base")', async () => { const targetUrl = 'http://example.com/optimized.ts'; + const bindingId = 'binding-id-1'; const basePayload = { h: { 'X-Custom-Header': 'OptimizedHeader' }, - s: true + s: true, + b: bindingId }; const baseEncrypted = encrypt(JSON.stringify(basePayload)); const dataPayload = { - u: targetUrl + u: targetUrl, + b: bindingId }; const dataEncrypted = encrypt(JSON.stringify(dataPayload)); @@ -123,4 +126,24 @@ describe('Segment Encryption Format', () => { expect(lastCall[0]).toBe(targetUrl); expect(lastCall[1].headers).toHaveProperty('X-Custom-Header', 'OptimizedHeader'); }); + + + it('should reject OPTIMIZED format when base/data binding does not match', async () => { + const baseEncrypted = encrypt(JSON.stringify({ + h: { 'X-Custom-Header': 'OptimizedHeader' }, + s: true, + b: 'base-binding' + })); + + const dataEncrypted = encrypt(JSON.stringify({ + u: 'http://example.com/mismatch.ts', + b: 'different-binding' + })); + + const res = await request(app) + .get(`/live/segment/${username}/password/seg.ts`) + .query({ token: userToken, data: dataEncrypted, base: baseEncrypted }); + + expect(res.status).toBe(400); + }); });