Skip to content

Commit 9233d5c

Browse files
fix: parse HTTP Range header correctly in output_file() fixes ZoneMinder#4777
The previous implementation used `str_replace($range, '-', $range)` which is a no-op (wrong arg order, return value discarded), then cast the raw Range value (e.g. "12345-67890" or "-500") to int. The function then ignored the requested END entirely and always streamed from the parsed start to EOF. For a suffix range like `Range: bytes=-500` -- which Chrome's media stack sends to locate the moov atom in many HEVC mp4s -- (int)"-500" is -500, producing Content-Length = filesize + 500. fseek with SEEK_SET fails for negative offsets, so the body delivered was filesize bytes against an inflated Content-Length, triggering ERR_CONTENT_LENGTH_MISMATCH in the browser and blocking HEVC playback in the files view. Parse `bytes=start-end`, `bytes=start-`, and `bytes=-suffix` per RFC 7233, clamp the end to file size, return 416 for unsatisfiable ranges, set Content-Length to the actual byte count served, and stop reading once that many bytes have been emitted. Guard ob_flush() with ob_get_level() so it does not warn when no buffer is active. Verified on pseudo by loading an HEVC mp4 in Chrome -- the ERR_CONTENT_LENGTH_MISMATCH is gone, the browser parses metadata (duration, dimensions) and buffers playback data normally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 97dd74d commit 9233d5c

1 file changed

Lines changed: 36 additions & 15 deletions

File tree

web/includes/functions.php

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2367,22 +2367,38 @@ function output_file($path, $chunkSize=1024) {
23672367
header("Content-Disposition: $contentDisposition;filename=\"$file\"");
23682368

23692369
header('Accept-Ranges: bytes');
2370-
$range = 0;
23712370
$size = filesize($path);
2371+
$start = 0;
2372+
$end = $size - 1;
23722373

23732374
if (isset($_SERVER['HTTP_RANGE'])) {
2374-
list($a, $range) = explode('=', $_SERVER['HTTP_RANGE']);
2375-
str_replace($range, '-', $range);
2376-
$range = (int)$range; #fseek etc require integers not strings
2377-
$size2 = $size - 1;
2378-
$new_length = $size - $range;
2375+
# RFC 7233: bytes=start-end | bytes=start- | bytes=-suffix
2376+
if (preg_match('/^bytes=(\d*)-(\d*)$/', trim($_SERVER['HTTP_RANGE']), $m)
2377+
and ($m[1] !== '' or $m[2] !== '')) {
2378+
if ($m[1] === '') {
2379+
# Suffix range: last N bytes
2380+
$suffix = (int)$m[2];
2381+
if ($suffix > $size) $suffix = $size;
2382+
$start = $size - $suffix;
2383+
} else {
2384+
$start = (int)$m[1];
2385+
if ($m[2] !== '') $end = (int)$m[2];
2386+
}
2387+
if ($end > $size - 1) $end = $size - 1;
2388+
}
2389+
if ($start > $end or $start >= $size) {
2390+
header('HTTP/1.1 416 Range Not Satisfiable');
2391+
header("Content-Range: bytes */$size");
2392+
return false;
2393+
}
2394+
$length = $end - $start + 1;
23792395
header('HTTP/1.1 206 Partial Content');
2380-
header("Content-Length: $new_length");
2381-
header("Content-Range: bytes $range-$size2/$size");
2396+
header("Content-Length: $length");
2397+
header("Content-Range: bytes $start-$end/$size");
23822398
} else {
2383-
$size2 = $size - 1;
2384-
header("Content-Range: bytes 0-$size2/$size");
2385-
header('Content-Length: ' . $size);
2399+
$length = $size;
2400+
header("Content-Range: bytes 0-$end/$size");
2401+
header("Content-Length: $size");
23862402
}
23872403

23882404
if ($size == 0) {
@@ -2391,13 +2407,18 @@ function output_file($path, $chunkSize=1024) {
23912407
@ini_set('magic_quotes_runtime', 0);
23922408
$fp = fopen($path, 'rb');
23932409

2394-
fseek($fp, $range);
2410+
fseek($fp, $start);
23952411

2396-
while (!feof($fp) and (connection_status() == 0)) {
2412+
$remaining = $length;
2413+
$buffer = 1024 * $chunkSize;
2414+
while ($remaining > 0 and !feof($fp) and (connection_status() == 0)) {
23972415
set_time_limit(0);
2398-
print(@fread($fp, 1024*$chunkSize));
2416+
$data = @fread($fp, min($buffer, $remaining));
2417+
if ($data === false or $data === '') break;
2418+
print($data);
23992419
flush();
2400-
ob_flush();
2420+
if (ob_get_level() > 0) ob_flush();
2421+
$remaining -= strlen($data);
24012422
}
24022423
fclose($fp);
24032424

0 commit comments

Comments
 (0)