Skip to content

Commit

Permalink
Fix header parsing in HttpUtil::loadFromUrl
Browse files Browse the repository at this point in the history
In case there was a redirection in the URL loaded, the status_code and
message returned came from the first redirection response and not from the
last response after the redirection(s). Potentially, also the content_type
could come from an intermediate result although likely the redirection
responses shouldn't define this header.

This may have caused other bugs too, but at least this broke the radio
playback in cases where the given URL redirected to another URL which was a
playlist file pointing to the actual audio stream. For example,
http://sverigesradio.se/topsy/direkt/226-hi-aac.pls is such URL.
  • Loading branch information
paulijar committed Jan 19, 2025
1 parent 4501cb5 commit bd302fd
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* Stream relaying not working on some redirecting stream URLs, depending on the headers
[#1194](https://github.com/owncloud/music/issues/1194)
* Stream playback failing when the stream URL has only the domain part without any path and no trailing '/' (like http://abc.somedomain.xyz)
* Stream playback failing when the given URL redirects to a playlist file containing the actual audio URL
* HTTP redirections not followed when parsing Icy-MetaData of the channel

## 2.1.1 - 2025-01-03
Expand Down
7 changes: 2 additions & 5 deletions lib/Http/RelayStreamResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* later. See the COPYING file.
*
* @author Pauli Järvinen <pauli.jarvinen@gmail.com>
* @copyright Pauli Järvinen 2024
* @copyright Pauli Järvinen 2024, 2025
*/

namespace OCA\Music\Http;
Expand Down Expand Up @@ -41,12 +41,9 @@ public function __construct(string $url) {
$this->context = HttpUtil::createContext(null, $reqHeaders);

// Get headers from the source and relay the important ones to our client
$sourceHeaders = HttpUtil::getUrlHeaders($url, $this->context);
$sourceHeaders = HttpUtil::getUrlHeaders($url, $this->context, /*convertKeysToLower=*/true);

if ($sourceHeaders !== null) {
// According to RFC 2616, HTTP headers are case-insensitive but we need predictable keys
$sourceHeaders = \array_change_key_case($sourceHeaders, CASE_LOWER);

if (isset($sourceHeaders['content-type'])) {
$this->addHeader('Content-Type', $sourceHeaders['content-type']);
}
Expand Down
72 changes: 40 additions & 32 deletions lib/Utility/HttpUtil.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* later. See the COPYING file.
*
* @author Pauli Järvinen <pauli.jarvinen@gmail.com>
* @copyright Pauli Järvinen 2022, 2023
* @copyright Pauli Järvinen 2022 - 2025
*/

namespace OCA\Music\Utility;
Expand Down Expand Up @@ -43,9 +43,10 @@ public static function loadFromUrl(string $url, ?int $maxLength=null, ?int $time
// It's some PHP magic that calling file_get_contents creates and populates also a local
// variable array $http_response_header, provided that the server could be reached.
if (!empty($http_response_header)) {
list($version, $status_code, $message) = \explode(' ', $http_response_header[0], 3);
$status_code = (int)$status_code;
$content_type = self::findHeader($http_response_header, 'Content-Type');
$parsedHeaders = self::parseHeaders($http_response_header, true);
$status_code = $parsedHeaders['status_code'];
$message = $parsedHeaders['status_msg'];
$content_type = $parsedHeaders['content-type'];
} else {
$message = 'The requested URL did not respond';
}
Expand All @@ -67,11 +68,14 @@ public static function createContext(?int $timeout_s=null, array $extraHeaders =

/**
* @param resource $context
* @param bool $convertKeysToLower When true, the header names used as keys of the result array are
* converted to lower case. According to RFC 2616, HTTP headers are case-insensitive.
* @return ?array The headers from the URL, after any redirections. The header names will be array keys.
* In addition to the named headers from the server, the key 'status_code' will contain
* the status code number of the HTTP request (like 200, 302, 404).
* the status code number of the HTTP request (like 200, 302, 404) and 'status_msg'
* the textual status following the code (like 'OK' or 'Not Found').
*/
public static function getUrlHeaders(string $url, $context) : ?array {
public static function getUrlHeaders(string $url, $context, bool $convertKeysToLower=false) : ?array {
$result = null;
if (self::isUrlSchemeOneOf($url, self::ALLOWED_SCHEMES)) {
// The built-in associative mode of get_headers because it mixes up the headers from the redirection
Expand All @@ -84,23 +88,40 @@ public static function getUrlHeaders(string $url, $context) : ?array {
$rawHeaders = @\get_headers($url, /** @scrutinizer ignore-type */ $associative, $context);

if ($rawHeaders !== false) {
$result = [];

foreach ($rawHeaders as $row) {
if (Util::startsWith($row, 'HTTP/', /*ignoreCase=*/true)) {
// Start of new response. If we have already parsed some headers, then those are from some
// intermediate redirect response and those should be discarded.
$result = ['status_code' => (int)(\explode(' ', $row, 3)[1] ?? 500)];
} else {
$parts = \explode(':', $row, 2);
if (\count($parts) == 2) {
list($key, $value) = $parts;
$result[\trim($key)] = \trim($value);
}
$result = self::parseHeaders($rawHeaders, $convertKeysToLower);
}
}
return $result;
}

private static function parseHeaders(array $rawHeaders, bool $convertKeysToLower) : array {
$result = [];

foreach ($rawHeaders as $row) {
if (Util::startsWith($row, 'HTTP/', /*ignoreCase=*/true)) {
// Start of new response. If we have already parsed some headers, then those are from some
// intermediate redirect response and those should be discarded.
$parts = \explode(' ', $row, 3);
if (\count($parts) == 3) {
list(, $status_code, $status_msg) = $parts;
} else {
$status_code = 500;
$status_msg = 'Bad response status header';
}
$result = ['status_code' => (int)$status_code, 'status_msg' => $status_msg];
} else {
// All other lines besides the initial status line should have the format "key: value"
$parts = \explode(':', $row, 2);
if (\count($parts) == 2) {
list($key, $value) = $parts;
if ($convertKeysToLower) {
$key = \mb_strtolower($key);
}
$result[\trim($key)] = \trim($value);
}
}
}

return $result;
}

Expand All @@ -124,19 +145,6 @@ private static function contextOptions(array $extraHeaders = []) : array {
return $opts;
}

private static function findHeader(array $headers, string $headerKey) : ?string {
// According to RFC 2616, HTTP headers are case-insensitive
$headerKey = \mb_strtolower($headerKey);
foreach ($headers as $header) {
$header = \mb_strtolower($header); // note that this converts also the header value to lower case
$find = \strstr($header, $headerKey . ':');
if ($find !== false) {
return \trim(\substr($find, \strlen($headerKey)+1));
}
}
return null;
}

private static function isUrlSchemeOneOf(string $url, array $schemes) : bool {
$url = \mb_strtolower($url);

Expand Down

0 comments on commit bd302fd

Please sign in to comment.