Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/controllers/streamController.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {}
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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);

Expand All @@ -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) {
Expand Down
27 changes: 25 additions & 2 deletions tests/performance/segment_encryption.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand All @@ -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);
});
});
Loading