Improve proxy redirect handling and avoid double-fetching media segments
- validate redirect chains more robustly, including scheme-relative, query-only, fragment, and normalized relative paths - resolve media segment redirects with HEAD before streaming to avoid downloading the same segment twice - keep strict host allowlisting in place while making HLS/CDN redirect handling more reliable
This commit is contained in:
@@ -31,22 +31,72 @@ function is_allowed_url(string $url, array $allowed_hosts): bool {
|
||||
return $host !== '' && in_array($host, $allowed_hosts, true);
|
||||
}
|
||||
|
||||
function normalize_path(string $path): string {
|
||||
$segments = explode('/', $path);
|
||||
$normalized = [];
|
||||
|
||||
foreach ($segments as $segment) {
|
||||
if ($segment === '' || $segment === '.') {
|
||||
continue;
|
||||
}
|
||||
if ($segment === '..') {
|
||||
array_pop($normalized);
|
||||
continue;
|
||||
}
|
||||
$normalized[] = $segment;
|
||||
}
|
||||
|
||||
return '/' . implode('/', $normalized);
|
||||
}
|
||||
|
||||
function resolve_url(string $base, string $location): string {
|
||||
if (preg_match('#^https?://#i', $location)) return $location;
|
||||
|
||||
$parts = parse_url($base);
|
||||
$origin = $parts['scheme'] . '://' . $parts['host'];
|
||||
if (!empty($parts['port'])) $origin .= ':' . $parts['port'];
|
||||
if ($location[0] === '/') return $origin . $location;
|
||||
return $origin . rtrim(dirname($parts['path'] ?? '/'), '/') . '/' . $location;
|
||||
|
||||
if (strpos($location, '//') === 0) {
|
||||
return $parts['scheme'] . ':' . $location;
|
||||
}
|
||||
|
||||
if ($location === '') {
|
||||
return $base;
|
||||
}
|
||||
|
||||
if ($location[0] === '#') {
|
||||
$without_fragment = preg_replace('/#.*$/', '', $base);
|
||||
return $without_fragment . $location;
|
||||
}
|
||||
|
||||
if ($location[0] === '?') {
|
||||
$path = $parts['path'] ?? '/';
|
||||
return $origin . $path . $location;
|
||||
}
|
||||
|
||||
$location_parts = parse_url($location);
|
||||
$path = $location_parts['path'] ?? '';
|
||||
$query = isset($location_parts['query']) ? '?' . $location_parts['query'] : '';
|
||||
$fragment = isset($location_parts['fragment']) ? '#' . $location_parts['fragment'] : '';
|
||||
|
||||
if ($path !== '' && $path[0] === '/') {
|
||||
return $origin . normalize_path($path) . $query . $fragment;
|
||||
}
|
||||
|
||||
$base_dir = dirname($parts['path'] ?? '/');
|
||||
if ($base_dir === '\\' || $base_dir === '.') {
|
||||
$base_dir = '/';
|
||||
}
|
||||
$combined = rtrim($base_dir, '/') . '/' . $path;
|
||||
|
||||
return $origin . normalize_path($combined) . $query . $fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch with manual redirect handling so every Location header is checked
|
||||
* against the allowlist before following it.
|
||||
* $stream = true: stream the response chunk by chunk (video segments)
|
||||
* $stream = false: return the full body (m3u8 playlists)
|
||||
*/
|
||||
function fetch_with_checked_redirects(string $url, array $allowed_hosts, bool $stream = false): array {
|
||||
function fetch_with_checked_redirects(string $url, array $allowed_hosts, bool $head_only = false): array {
|
||||
$max_redirects = 5;
|
||||
|
||||
for ($i = 0; $i <= $max_redirects; $i++) {
|
||||
@@ -60,6 +110,7 @@ function fetch_with_checked_redirects(string $url, array $allowed_hosts, bool $s
|
||||
CURLOPT_FOLLOWLOCATION => false,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_NOBODY => $head_only,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_USERAGENT => $ua,
|
||||
CURLOPT_HTTPHEADER => ['Accept: */*'],
|
||||
@@ -114,9 +165,8 @@ if ($is_segment) {
|
||||
|
||||
if (ob_get_level()) ob_end_clean();
|
||||
|
||||
// For segments, resolve redirects first and only stream once the final URL
|
||||
// has already been validated.
|
||||
[$code, , , $final_url] = fetch_with_checked_redirects($url, $allowed_hosts, false);
|
||||
// Resolve redirects with HEAD first so we do not download the segment twice.
|
||||
[$code, , , $final_url] = fetch_with_checked_redirects($url, $allowed_hosts, true);
|
||||
|
||||
if ($code >= 400) { http_response_code($code); die(); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user