feat: implement Xtream proxy handler with M3U8 URL rewriting, SSRF protection, and content type handling.

This commit is contained in:
Michael committed 2026-01-07 11:31:40 +01:00
1 parent 755367018a
commit 758cbe4c43
1 file changed
+22 -12
+22 -12
View File
@@ -88,21 +88,31 @@ class ProxyHandler {
final targetUrl = Uri.parse(fullUrl); final targetUrl = Uri.parse(fullUrl);
// SSRF Protection via Domain Allowlist // SSRF Protection - but allow images/static assets from any host
final playlist = await _getPlaylist(request); // Xtream providers often use separate CDN servers for picons/images
if (playlist == null) { final isStaticAsset = targetUrl.path.endsWith('.png') ||
return Response.forbidden('No active playlist configuration found to validate request'); targetUrl.path.endsWith('.jpg') ||
} targetUrl.path.endsWith('.jpeg') ||
targetUrl.path.endsWith('.gif') ||
targetUrl.path.endsWith('.webp') ||
targetUrl.path.endsWith('.ico') ||
targetUrl.path.contains('/picons/') ||
targetUrl.path.contains('/logos/');
// Normalize domains for comparison if (!isStaticAsset) {
final targetHost = targetUrl.host.toLowerCase(); // For API calls, enforce domain allowlist
final allowedHost = Uri.parse(playlist.dns).host.toLowerCase(); final playlist = await _getPlaylist(request);
if (playlist == null) {
return Response.forbidden('No active playlist configuration found to validate request');
}
bool isAllowed = targetHost == allowedHost; final targetHost = targetUrl.host.toLowerCase();
final allowedHost = Uri.parse(playlist.dns).host.toLowerCase();
if (!isAllowed) { if (targetHost != allowedHost) {
print('[Proxy] Blocked SSRF attempt to $targetHost (Allowed: $allowedHost)'); print('[Proxy] Blocked SSRF attempt to $targetHost (Allowed: $allowedHost)');
return Response.forbidden('Access to this domain is forbidden by policy'); return Response.forbidden('Access to this domain is forbidden by policy');
}
} }
final proxyHeaders = { final proxyHeaders = {