From 328e631bac12a57a509ecf9f8b23a9485680868e Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 3 Sep 2026 05:45:16 +0000 Subject: [PATCH 1/4] ext/standard: add tests for HTTP fopen wrapper --- .../tests/http/http_auth_userinfo.phpt | 54 +++++++++++++ ext/standard/tests/http/http_auto_decode.phpt | 63 +++++++++++++++ .../tests/http/http_location_realloc.phpt | 35 ++++++++ .../tests/http/http_proxy_auth_removed.phpt | 77 ++++++++++++++++++ .../tests/http/http_proxy_ssl_connect_01.phpt | 48 +++++++++++ .../tests/http/http_proxy_ssl_connect_02.phpt | 49 ++++++++++++ .../tests/http/http_proxy_ssl_connect_03.phpt | 51 ++++++++++++ .../http_redirect_removes_post_headers.phpt | 79 +++++++++++++++++++ .../tests/http/http_relative_redirect.phpt | 61 ++++++++++++++ .../http_sets_default_request_headers.phpt | 40 ++++++++++ .../http/http_user_header_precedence.phpt | 54 +++++++++++++ 11 files changed, 611 insertions(+) create mode 100644 ext/standard/tests/http/http_auth_userinfo.phpt create mode 100644 ext/standard/tests/http/http_auto_decode.phpt create mode 100644 ext/standard/tests/http/http_location_realloc.phpt create mode 100644 ext/standard/tests/http/http_proxy_auth_removed.phpt create mode 100644 ext/standard/tests/http/http_proxy_ssl_connect_01.phpt create mode 100644 ext/standard/tests/http/http_proxy_ssl_connect_02.phpt create mode 100644 ext/standard/tests/http/http_proxy_ssl_connect_03.phpt create mode 100644 ext/standard/tests/http/http_redirect_removes_post_headers.phpt create mode 100644 ext/standard/tests/http/http_relative_redirect.phpt create mode 100644 ext/standard/tests/http/http_sets_default_request_headers.phpt create mode 100644 ext/standard/tests/http/http_user_header_precedence.phpt diff --git a/ext/standard/tests/http/http_auth_userinfo.phpt b/ext/standard/tests/http/http_auth_userinfo.phpt new file mode 100644 index 000000000000..74f52067a031 --- /dev/null +++ b/ext/standard/tests/http/http_auth_userinfo.phpt @@ -0,0 +1,54 @@ +--TEST-- +http(s) wrapper attaches an Authorization: Basic header when the URL contains userinfo +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- +" when no Authorization header +// was supplied explicitly. +function probe(string $label, string $userinfo, string $expected): void +{ + $responses = [ + "data://text/plain,HTTP/1.0 200 Ok\r\n\r\n", + ]; + + ['pid' => $pid, 'uri' => $uri] = http_server($responses, $output); + + // Inject the userinfo (e.g. "user:password") just before the host. + $url = preg_replace('(^http://)', 'http://' . $userinfo . '@', $uri); + file_get_contents($url); + + fseek($output, 0, SEEK_SET); + $raw = stream_get_contents($output); + + http_server_kill($pid); + + $token = preg_match('(^Authorization:\s*Basic\s+(\S+))mi', $raw, $m) ? $m[1] : null; + + echo "=== {$label} ===", PHP_EOL; + // The exact base64 token the wrapper attached. + var_dump($token); + // The decoded credential string (user:password, or "user:" when no password). + var_dump($token === null ? null : base64_decode($token)); + // Does the attached token match the expected credential string? + var_dump($token === base64_encode($expected)); +} + +probe('user:password', 'user:password', 'user:password'); +probe('user (no password)', 'user', 'user:'); +?> +--EXPECT-- +=== user:password === +string(20) "dXNlcjpwYXNzd29yZA==" +string(13) "user:password" +bool(true) +=== user (no password) === +string(8) "dXNlcjo=" +string(5) "user:" +bool(true) diff --git a/ext/standard/tests/http/http_auto_decode.phpt b/ext/standard/tests/http/http_auto_decode.phpt new file mode 100644 index 000000000000..2904370a5036 --- /dev/null +++ b/ext/standard/tests/http/http_auto_decode.phpt @@ -0,0 +1,63 @@ +--TEST-- +http.auto_decode stream context option controls chunked response decoding +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $pid, 'uri' => $uri] = http_server($responses); + +// $set_auto_decode: whether to set the http.auto_decode option at all. +// $auto_decode: the value to set (only used when $set_auto_decode is true). +function fetch($uri, $set_auto_decode, $auto_decode) { + $http = ['protocol_version' => '1.1', 'header' => 'Connection: Close']; + if ($set_auto_decode) { + $http['auto_decode'] = $auto_decode; + } + $ctx = stream_context_create(['http' => $http]); + $body = file_get_contents($uri, false, $ctx); + $has_te = false; + foreach (http_get_last_response_headers() as $h) { + if (stripos($h, 'Transfer-Encoding:') === 0) { + $has_te = true; + break; + } + } + // Escape CR/LF so the raw body prints on a single line. + return [addcslashes($body, "\r\n"), $has_te]; +} + +echo "default (auto_decode unset):\n"; +[$body, $has_te] = fetch($uri, false, null); +var_dump($body, $has_te); + +echo "auto_decode = true:\n"; +[$body, $has_te] = fetch($uri, true, true); +var_dump($body, $has_te); + +echo "auto_decode = false:\n"; +[$body, $has_te] = fetch($uri, true, false); +var_dump($body, $has_te); + +http_server_kill($pid); +?> +--EXPECT-- +default (auto_decode unset): +string(4) "abcd" +bool(false) +auto_decode = true: +string(4) "abcd" +bool(false) +auto_decode = false: +string(31) "2\r\nab\r\n2\r\ncd\r\n0\r\n\r\n" +bool(true) diff --git a/ext/standard/tests/http/http_location_realloc.phpt b/ext/standard/tests/http/http_location_realloc.phpt new file mode 100644 index 000000000000..df4e1057c337 --- /dev/null +++ b/ext/standard/tests/http/http_location_realloc.phpt @@ -0,0 +1,35 @@ +--TEST-- +HTTP response with two Location headers (second longer) triggers erealloc +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + +$ctx = stream_context_create(['http' => ['follow_location' => 0]]); +$result = file_get_contents("$uri/", false, $ctx); +var_dump($result); +var_dump(http_get_last_response_headers()); + +http_server_kill($pid); +?> +--EXPECT-- +string(0) "" +array(4) { + [0]=> + string(30) "HTTP/1.1 301 Moved Permanently" + [1]=> + string(16) "Location: /short" + [2]=> + string(40) "Location: /a_much_longer_path_than_short" + [3]=> + string(17) "Content-Length: 0" +} diff --git a/ext/standard/tests/http/http_proxy_auth_removed.phpt b/ext/standard/tests/http/http_proxy_auth_removed.phpt new file mode 100644 index 000000000000..85d8ac7ff7cc --- /dev/null +++ b/ext/standard/tests/http/http_proxy_auth_removed.phpt @@ -0,0 +1,77 @@ +--TEST-- +Proxy-Authorization header removed from request after CONNECT tunnel +--DESCRIPTION-- +Tests the Proxy-Authorization removal code (lines 743-761 of +http_fopen_wrapper.c) that strips the header from the actual HTTP request +after the CONNECT tunnel is established. The header is used for proxy +authentication during CONNECT but must not be forwarded to the target. +--EXTENSIONS-- +openssl +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + [ + 'proxy' => "tcp://$host:$port", + 'header' => [ + "Proxy-Authorization: Basic Zm9vOmJhcg==", + ], + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]); + +file_get_contents("https://www.php.net/", false, $ctx); + +http_server_kill($server['pid']); +?> +--EXPECT-- +CONNECT contains Proxy-Authorization: bool(true) +Proxied request contains Proxy-Authorization: bool(false) diff --git a/ext/standard/tests/http/http_proxy_ssl_connect_01.phpt b/ext/standard/tests/http/http_proxy_ssl_connect_01.phpt new file mode 100644 index 000000000000..0d647b79d02f --- /dev/null +++ b/ext/standard/tests/http/http_proxy_ssl_connect_01.phpt @@ -0,0 +1,48 @@ +--TEST-- +HTTP proxy SSL CONNECT with Proxy-Authorization header (string, multi-line) +--EXTENSIONS-- +openssl +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + +$host = parse_url($uri, PHP_URL_HOST); +$port = parse_url($uri, PHP_URL_PORT); + +$ctx = stream_context_create([ + 'http' => [ + 'proxy' => "tcp://$host:$port", + 'header' => "X-Custom: test\r\nProxy-Authorization: Basic dXNlcjpwYXNz", + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]); +@$result = file_get_contents("https://www.php.net/test", false, $ctx); +var_dump($result); + +http_server_kill($pid); + +rewind($output); +$request = stream_get_contents($output); +var_dump(str_contains($request, 'CONNECT www.php.net:443 HTTP/1.0')); +var_dump(str_contains($request, 'Proxy-Authorization: Basic dXNlcjpwYXNz')); +var_dump(str_contains($request, 'X-Custom: test')); +?> +--EXPECT-- +bool(false) +bool(true) +bool(true) +bool(false) diff --git a/ext/standard/tests/http/http_proxy_ssl_connect_02.phpt b/ext/standard/tests/http/http_proxy_ssl_connect_02.phpt new file mode 100644 index 000000000000..0ff293c57841 --- /dev/null +++ b/ext/standard/tests/http/http_proxy_ssl_connect_02.phpt @@ -0,0 +1,49 @@ +--TEST-- +HTTP proxy SSL CONNECT without Proxy-Authorization header (FAILURE path) +--EXTENSIONS-- +openssl +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + +$host = parse_url($uri, PHP_URL_HOST); +$port = parse_url($uri, PHP_URL_PORT); + +$ctx = stream_context_create([ + 'http' => [ + 'proxy' => "tcp://$host:$port", + 'header' => "X-Custom: test\r\nX-Other: value", + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]); +@$result = file_get_contents("https://www.php.net/test", false, $ctx); +var_dump($result); + +http_server_kill($pid); + +rewind($output); +$request = stream_get_contents($output); +var_dump(str_contains($request, 'CONNECT www.php.net:443 HTTP/1.0')); +var_dump(str_contains($request, 'Proxy-Authorization')); +?> +===DONE=== + +--EXPECT-- +bool(false) +bool(true) +bool(false) +===DONE=== diff --git a/ext/standard/tests/http/http_proxy_ssl_connect_03.phpt b/ext/standard/tests/http/http_proxy_ssl_connect_03.phpt new file mode 100644 index 000000000000..c83c4b9a9136 --- /dev/null +++ b/ext/standard/tests/http/http_proxy_ssl_connect_03.phpt @@ -0,0 +1,51 @@ +--TEST-- +HTTP proxy SSL CONNECT with Proxy-Authorization header (array) +--EXTENSIONS-- +openssl +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + +$host = parse_url($uri, PHP_URL_HOST); +$port = parse_url($uri, PHP_URL_PORT); + +$ctx = stream_context_create([ + 'http' => [ + 'proxy' => "tcp://$host:$port", + 'header' => ["X-Custom: test", "Proxy-Authorization: Basic abc123"], + ], + 'ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ], +]); +@$result = file_get_contents("https://www.php.net/test", false, $ctx); +var_dump($result); + +http_server_kill($pid); + +rewind($output); +$request = stream_get_contents($output); +var_dump(str_contains($request, 'CONNECT www.php.net:443 HTTP/1.0')); +var_dump(str_contains($request, 'Proxy-Authorization: Basic abc123')); +var_dump(str_contains($request, 'X-Custom')); +?> +===DONE=== + +--EXPECT-- +bool(false) +bool(true) +bool(true) +bool(false) +===DONE=== diff --git a/ext/standard/tests/http/http_redirect_removes_post_headers.phpt b/ext/standard/tests/http/http_redirect_removes_post_headers.phpt new file mode 100644 index 000000000000..7cd989b58bc4 --- /dev/null +++ b/ext/standard/tests/http/http_redirect_removes_post_headers.phpt @@ -0,0 +1,79 @@ +--TEST-- +POST Content-Type and Content-Length headers removed on redirect except for 307/308 +--DESCRIPTION-- +Tests that user-supplied Content-Type and Content-Length headers from the +http.header stream context option are removed when a POST request is redirected +to a GET request (301/302/303), but preserved for 307/308 where the request +method and body must be retained. +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + [ + 'method' => 'POST', + 'content' => 'test=data', + 'follow_location' => 1, + 'max_redirects' => 3, + 'header' => + "Content-Type: application/x-www-form-urlencoded\r\n" . + "Content-Length: 9\r\n", + ], +]); + +foreach ($status_codes as $code) { + file_get_contents($server['uri'], false, $context); +} + +http_server_kill($server['pid']); + +rewind($output); +$contents = stream_get_contents($output); + +foreach ($status_codes as $code) { + if (!preg_match("~(GET|POST) /$code-redirected .*?\r\n\r\n~s", $contents, $matches)) { + die("fail redirect request for $code not found\n"); + } + echo "Redirect request for $code has Content-Type: "; + var_dump(stripos($matches[0], 'content-type') !== false); + echo "Redirect request for $code has Content-Length: "; + var_dump(stripos($matches[0], 'content-length') !== false); +} + +?> +--EXPECT-- +Redirect request for 301 has Content-Type: bool(false) +Redirect request for 301 has Content-Length: bool(false) +Redirect request for 302 has Content-Type: bool(false) +Redirect request for 302 has Content-Length: bool(false) +Redirect request for 303 has Content-Type: bool(false) +Redirect request for 303 has Content-Length: bool(false) +Redirect request for 307 has Content-Type: bool(true) +Redirect request for 307 has Content-Length: bool(true) +Redirect request for 308 has Content-Type: bool(true) +Redirect request for 308 has Content-Length: bool(true) diff --git a/ext/standard/tests/http/http_relative_redirect.phpt b/ext/standard/tests/http/http_relative_redirect.phpt new file mode 100644 index 000000000000..b2290820ffe0 --- /dev/null +++ b/ext/standard/tests/http/http_relative_redirect.phpt @@ -0,0 +1,61 @@ +--TEST-- +http(s) wrapper resolves a relative redirect Location against the request path +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + + $ctx = stream_context_create(['http' => ['follow_location' => true]]); + $body = file_get_contents($uri . $req_path, false, $ctx); + + fseek($output, 0, SEEK_SET); + $raw = stream_get_contents($output); + + http_server_kill($pid); + + preg_match_all('#^(?:GET|HEAD|POST) (\S+) HTTP/\d#mi', $raw, $m); + $paths = $m[1] ?? []; + + echo "=== {$label} ===\n"; + echo " body = ", $body, "\n"; + echo " paths = ", implode(' | ', $paths), "\n"; +} + +// Subdirectory path + multi-char relative location: +// "/dir/page" + "other" -> dir part "/dir/" joined with "other". +probe('subdir + relative', '/dir/page', 'other', 'SUBDIR'); + +// Root path + relative location: +// "/" + "other" -> "/other". +probe('root + relative', '/', 'other', 'ROOT'); + +// Subdirectory path + single-char relative location (location_len <= 1): +// "/dir/page" + "a" -> "/a". +probe('subdir + 1-char relative', '/dir/page', 'a', 'ONELONG'); +?> +--EXPECT-- +=== subdir + relative === + body = SUBDIR + paths = /dir/page | /dir//other +=== root + relative === + body = ROOT + paths = / | /other +=== subdir + 1-char relative === + body = ONELONG + paths = /dir/page | /a diff --git a/ext/standard/tests/http/http_sets_default_request_headers.phpt b/ext/standard/tests/http/http_sets_default_request_headers.phpt new file mode 100644 index 000000000000..c86cea6cfae1 --- /dev/null +++ b/ext/standard/tests/http/http_sets_default_request_headers.phpt @@ -0,0 +1,40 @@ +--TEST-- +Test from and user-agent headers are set from ini and ctx +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +from=ini_from@php.net +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + +$ctx = stream_context_create([ + 'http' => [ + 'user_agent' => 'SomeAgent', + 'content' => 'payload', + ], +]); +file_get_contents($uri, false, $ctx); + +http_server_kill($pid); + +rewind($output); +$request = stream_get_contents($output); + +var_dump(str_contains($request, 'User-Agent: SomeAgent')); +var_dump(str_contains($request, 'From: ini_from@php.net')); +var_dump(str_contains($request, 'Content-Type: application/x-www-form-urlencoded')); +var_dump(str_contains($request, 'Content-Length: 7')); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/http/http_user_header_precedence.phpt b/ext/standard/tests/http/http_user_header_precedence.phpt new file mode 100644 index 000000000000..b5e562972bb4 --- /dev/null +++ b/ext/standard/tests/http/http_user_header_precedence.phpt @@ -0,0 +1,54 @@ +--TEST-- +Header precedence when headers are supplied in multiple ways +--SKIPIF-- + +--INI-- +allow_url_fopen=1 +from=ini_from@example.com +--FILE-- + $pid, 'uri' => $uri] = http_server($responses, $output); + +$ctx = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => "User-Agent: MyCustomUA\r\n" + . "From: me@example.com\r\n" + . "Content-Length: 5\r\n" + . "Content-Type: application/json\r\n" + . "X-Custom: test", + 'user_agent' => 'AutoGeneratedUA', + 'content' => '0123456789', + ], +]); +file_get_contents($uri, false, $ctx); + +http_server_kill($pid); + +rewind($output); +$request = stream_get_contents($output); + +var_dump(str_contains($request, 'User-Agent: MyCustomUA')); +var_dump(str_contains($request, 'AutoGeneratedUA')); +var_dump(substr_count($request, 'User-Agent:')); +var_dump(str_contains($request, 'From: me@example.com')); +var_dump(str_contains($request, 'ini_from@example.com')); +var_dump(substr_count($request, 'From:')); +var_dump(str_contains($request, 'Content-Length: 5')); +var_dump(substr_count($request, 'Content-Length:')); +?> +--EXPECT-- +bool(true) +bool(false) +int(1) +bool(true) +bool(false) +int(1) +bool(true) +int(1) From 095608de7b305222877b9b631f2ea736a96d78a9 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Thu, 3 Sep 2026 20:26:28 +0000 Subject: [PATCH 2/4] Clean up tests, add case to redirect test --- .../tests/http/http_auth_userinfo.phpt | 62 ++--- ext/standard/tests/http/http_auto_decode.phpt | 58 +++-- .../tests/http/http_proxy_auth_removed.phpt | 5 - .../tests/http/http_proxy_ssl_connect_02.phpt | 3 - .../tests/http/http_proxy_ssl_connect_03.phpt | 3 - .../http_redirect_removes_post_headers.phpt | 5 - .../tests/http/http_relative_redirect.phpt | 215 ++++++++++++++---- 7 files changed, 221 insertions(+), 130 deletions(-) diff --git a/ext/standard/tests/http/http_auth_userinfo.phpt b/ext/standard/tests/http/http_auth_userinfo.phpt index 74f52067a031..56ca3e8ac841 100644 --- a/ext/standard/tests/http/http_auth_userinfo.phpt +++ b/ext/standard/tests/http/http_auth_userinfo.phpt @@ -1,54 +1,38 @@ --TEST-- -http(s) wrapper attaches an Authorization: Basic header when the URL contains userinfo +http wrapper attaches an Authorization: Basic header when the URL contains userinfo --SKIPIF-- - + --INI-- allow_url_fopen=1 --FILE-- " when no Authorization header -// was supplied explicitly. -function probe(string $label, string $userinfo, string $expected): void -{ - $responses = [ - "data://text/plain,HTTP/1.0 200 Ok\r\n\r\n", - ]; +$responses = [ + "data://text/plain,HTTP/1.0 204 No Content\r\n\r\n", + "data://text/plain,HTTP/1.0 204 No Content\r\n\r\n", +]; +['pid' => $pid, 'uri' => $uri] = http_server($responses, $output); - ['pid' => $pid, 'uri' => $uri] = http_server($responses, $output); +$host = parse_url($uri, PHP_URL_HOST); +$port = parse_url($uri, PHP_URL_PORT); - // Inject the userinfo (e.g. "user:password") just before the host. - $url = preg_replace('(^http://)', 'http://' . $userinfo . '@', $uri); - file_get_contents($url); +file_get_contents("http://user@$host:$port/path"); +file_get_contents("http://user:pass@$host:$port/path"); - fseek($output, 0, SEEK_SET); - $raw = stream_get_contents($output); - - http_server_kill($pid); - - $token = preg_match('(^Authorization:\s*Basic\s+(\S+))mi', $raw, $m) ? $m[1] : null; +rewind($output); +$requests = stream_get_contents($output); +if (!preg_match_all('~Authorization: .*~', $requests, $matches)) { + die('fail No authorization headers found'); +} - echo "=== {$label} ===", PHP_EOL; - // The exact base64 token the wrapper attached. - var_dump($token); - // The decoded credential string (user:password, or "user:" when no password). - var_dump($token === null ? null : base64_decode($token)); - // Does the attached token match the expected credential string? - var_dump($token === base64_encode($expected)); +foreach ($matches[0] as $header) { + echo $header . "\n"; } -probe('user:password', 'user:password', 'user:password'); -probe('user (no password)', 'user', 'user:'); +http_server_kill($pid); + ?> --EXPECT-- -=== user:password === -string(20) "dXNlcjpwYXNzd29yZA==" -string(13) "user:password" -bool(true) -=== user (no password) === -string(8) "dXNlcjo=" -string(5) "user:" -bool(true) +Authorization: Basic dXNlcjo= +Authorization: Basic dXNlcjpwYXNz diff --git a/ext/standard/tests/http/http_auto_decode.phpt b/ext/standard/tests/http/http_auto_decode.phpt index 2904370a5036..c1fa2a64011e 100644 --- a/ext/standard/tests/http/http_auto_decode.phpt +++ b/ext/standard/tests/http/http_auto_decode.phpt @@ -8,8 +8,6 @@ allow_url_fopen=1 $pid, 'uri' => $uri] = http_server($responses); -// $set_auto_decode: whether to set the http.auto_decode option at all. -// $auto_decode: the value to set (only used when $set_auto_decode is true). -function fetch($uri, $set_auto_decode, $auto_decode) { - $http = ['protocol_version' => '1.1', 'header' => 'Connection: Close']; - if ($set_auto_decode) { - $http['auto_decode'] = $auto_decode; +function test_auto_decode($auto_decode) { + global $uri; + $ctx = null; + if ($auto_decode !== null) { + $ctx = stream_context_create(['http' => ['auto_decode' => $auto_decode]]); } - $ctx = stream_context_create(['http' => $http]); $body = file_get_contents($uri, false, $ctx); + $has_te = false; foreach (http_get_last_response_headers() as $h) { if (stripos($h, 'Transfer-Encoding:') === 0) { @@ -33,31 +30,32 @@ function fetch($uri, $set_auto_decode, $auto_decode) { break; } } - // Escape CR/LF so the raw body prints on a single line. + return [addcslashes($body, "\r\n"), $has_te]; } -echo "default (auto_decode unset):\n"; -[$body, $has_te] = fetch($uri, false, null); -var_dump($body, $has_te); - -echo "auto_decode = true:\n"; -[$body, $has_te] = fetch($uri, true, true); -var_dump($body, $has_te); - -echo "auto_decode = false:\n"; -[$body, $has_te] = fetch($uri, true, false); -var_dump($body, $has_te); +var_dump(test_auto_decode(null)); +var_dump(test_auto_decode(true)); +var_dump(test_auto_decode(false)); http_server_kill($pid); ?> --EXPECT-- -default (auto_decode unset): -string(4) "abcd" -bool(false) -auto_decode = true: -string(4) "abcd" -bool(false) -auto_decode = false: -string(31) "2\r\nab\r\n2\r\ncd\r\n0\r\n\r\n" -bool(true) +array(2) { + [0]=> + string(4) "abcd" + [1]=> + bool(false) +} +array(2) { + [0]=> + string(4) "abcd" + [1]=> + bool(false) +} +array(2) { + [0]=> + string(31) "2\r\nab\r\n2\r\ncd\r\n0\r\n\r\n" + [1]=> + bool(true) +} diff --git a/ext/standard/tests/http/http_proxy_auth_removed.phpt b/ext/standard/tests/http/http_proxy_auth_removed.phpt index 85d8ac7ff7cc..5c541e299505 100644 --- a/ext/standard/tests/http/http_proxy_auth_removed.phpt +++ b/ext/standard/tests/http/http_proxy_auth_removed.phpt @@ -1,10 +1,5 @@ --TEST-- Proxy-Authorization header removed from request after CONNECT tunnel ---DESCRIPTION-- -Tests the Proxy-Authorization removal code (lines 743-761 of -http_fopen_wrapper.c) that strips the header from the actual HTTP request -after the CONNECT tunnel is established. The header is used for proxy -authentication during CONNECT but must not be forwarded to the target. --EXTENSIONS-- openssl --SKIPIF-- diff --git a/ext/standard/tests/http/http_proxy_ssl_connect_02.phpt b/ext/standard/tests/http/http_proxy_ssl_connect_02.phpt index 0ff293c57841..bff063e2a0ae 100644 --- a/ext/standard/tests/http/http_proxy_ssl_connect_02.phpt +++ b/ext/standard/tests/http/http_proxy_ssl_connect_02.phpt @@ -40,10 +40,7 @@ $request = stream_get_contents($output); var_dump(str_contains($request, 'CONNECT www.php.net:443 HTTP/1.0')); var_dump(str_contains($request, 'Proxy-Authorization')); ?> -===DONE=== - --EXPECT-- bool(false) bool(true) bool(false) -===DONE=== diff --git a/ext/standard/tests/http/http_proxy_ssl_connect_03.phpt b/ext/standard/tests/http/http_proxy_ssl_connect_03.phpt index c83c4b9a9136..90a19798f6df 100644 --- a/ext/standard/tests/http/http_proxy_ssl_connect_03.phpt +++ b/ext/standard/tests/http/http_proxy_ssl_connect_03.phpt @@ -41,11 +41,8 @@ var_dump(str_contains($request, 'CONNECT www.php.net:443 HTTP/1.0')); var_dump(str_contains($request, 'Proxy-Authorization: Basic abc123')); var_dump(str_contains($request, 'X-Custom')); ?> -===DONE=== - --EXPECT-- bool(false) bool(true) bool(true) bool(false) -===DONE=== diff --git a/ext/standard/tests/http/http_redirect_removes_post_headers.phpt b/ext/standard/tests/http/http_redirect_removes_post_headers.phpt index 7cd989b58bc4..6e0927016ec6 100644 --- a/ext/standard/tests/http/http_redirect_removes_post_headers.phpt +++ b/ext/standard/tests/http/http_redirect_removes_post_headers.phpt @@ -1,10 +1,5 @@ --TEST-- POST Content-Type and Content-Length headers removed on redirect except for 307/308 ---DESCRIPTION-- -Tests that user-supplied Content-Type and Content-Length headers from the -http.header stream context option are removed when a POST request is redirected -to a GET request (301/302/303), but preserved for 307/308 where the request -method and body must be retained. --SKIPIF-- + --INI-- allow_url_fopen=1 --FILE-- $pid, 'uri' => $uri] = http_server($responses, $output); - - $ctx = stream_context_create(['http' => ['follow_location' => true]]); - $body = file_get_contents($uri . $req_path, false, $ctx); +foreach ($uri_parsers as $uri_parser) { + foreach ($froms as $from) { + foreach ($tos as $to) { + $responses[] = "data://text/plain,HTTP/1.0 302 Found\r\nLocation: $to\r\n\r\n"; + $responses[] = "data://text/plain,HTTP/1.0 204 No Content\r\n\r\n"; + } + } +} - fseek($output, 0, SEEK_SET); - $raw = stream_get_contents($output); +['pid' => $pid, 'uri' => $uri] = http_server($responses, $output); - http_server_kill($pid); +foreach ($uri_parsers as $uri_parser) { + echo "# URI parser: $uri_parser\n"; + foreach ($froms as $from) { + foreach ($tos as $to) { + ftruncate($output, 0); - preg_match_all('#^(?:GET|HEAD|POST) (\S+) HTTP/\d#mi', $raw, $m); - $paths = $m[1] ?? []; + $ctx = stream_context_create(['http' => [ + 'follow_location' => true, + 'uri_parser_class' => $uri_parser, + ]]); + $body = @file_get_contents($uri . $from, false, $ctx); + rewind($output); - echo "=== {$label} ===\n"; - echo " body = ", $body, "\n"; - echo " paths = ", implode(' | ', $paths), "\n"; + if ($body === false) { + $result = "failed"; + // Remove request from responses queue + file_get_contents($uri); + } else { + $requests = stream_get_contents($output); + preg_match_all('~GET (.*) HTTP/1.~', $requests, $matches); + $result = $matches[1][1]; + } + echo "Redirect from '$from' to '$to': $result\n"; + } + } } -// Subdirectory path + multi-char relative location: -// "/dir/page" + "other" -> dir part "/dir/" joined with "other". -probe('subdir + relative', '/dir/page', 'other', 'SUBDIR'); - -// Root path + relative location: -// "/" + "other" -> "/other". -probe('root + relative', '/', 'other', 'ROOT'); - -// Subdirectory path + single-char relative location (location_len <= 1): -// "/dir/page" + "a" -> "/a". -probe('subdir + 1-char relative', '/dir/page', 'a', 'ONELONG'); +http_server_kill($pid); ?> --EXPECT-- -=== subdir + relative === - body = SUBDIR - paths = /dir/page | /dir//other -=== root + relative === - body = ROOT - paths = / | /other -=== subdir + 1-char relative === - body = ONELONG - paths = /dir/page | /a +# URI parser: +Redirect from '/dir/page' to '/dir/page': /dir/page +Redirect from '/dir/page' to 'dir/page': /dir//dir/page +Redirect from '/dir/page' to 'a': /a +Redirect from '/dir/page' to 'other': /dir//other +Redirect from '/dir/page' to '': / +Redirect from '/dir/page' to '../../../foo': /dir//../../../foo +Redirect from '/dir/page' to 'space bar': /dir//space bar +Redirect from '/a' to '/dir/page': /dir/page +Redirect from '/a' to 'dir/page': /dir/page +Redirect from '/a' to 'a': /a +Redirect from '/a' to 'other': /other +Redirect from '/a' to '': / +Redirect from '/a' to '../../../foo': /../../../foo +Redirect from '/a' to 'space bar': /space bar +Redirect from '/other' to '/dir/page': /dir/page +Redirect from '/other' to 'dir/page': /dir/page +Redirect from '/other' to 'a': /a +Redirect from '/other' to 'other': /other +Redirect from '/other' to '': / +Redirect from '/other' to '../../../foo': /../../../foo +Redirect from '/other' to 'space bar': /space bar +Redirect from '' to '/dir/page': /dir/page +Redirect from '' to 'dir/page': /dir/page +Redirect from '' to 'a': /a +Redirect from '' to 'other': /other +Redirect from '' to '': / +Redirect from '' to '../../../foo': /../../../foo +Redirect from '' to 'space bar': /space bar +Redirect from '/../../../foo' to '/dir/page': /dir/page +Redirect from '/../../../foo' to 'dir/page': /../../..//dir/page +Redirect from '/../../../foo' to 'a': /a +Redirect from '/../../../foo' to 'other': /../../..//other +Redirect from '/../../../foo' to '': / +Redirect from '/../../../foo' to '../../../foo': /../../..//../../../foo +Redirect from '/../../../foo' to 'space bar': /../../..//space bar +Redirect from '/space bar' to '/dir/page': /dir/page +Redirect from '/space bar' to 'dir/page': /dir/page +Redirect from '/space bar' to 'a': /a +Redirect from '/space bar' to 'other': /other +Redirect from '/space bar' to '': / +Redirect from '/space bar' to '../../../foo': /../../../foo +Redirect from '/space bar' to 'space bar': /space bar +# URI parser: Uri\Rfc3986\Uri +Redirect from '/dir/page' to '/dir/page': /dir/page +Redirect from '/dir/page' to 'dir/page': /dir//dir/page +Redirect from '/dir/page' to 'a': /a +Redirect from '/dir/page' to 'other': /dir//other +Redirect from '/dir/page' to '': / +Redirect from '/dir/page' to '../../../foo': /dir//../../../foo +Redirect from '/dir/page' to 'space bar': failed +Redirect from '/a' to '/dir/page': /dir/page +Redirect from '/a' to 'dir/page': /dir/page +Redirect from '/a' to 'a': /a +Redirect from '/a' to 'other': /other +Redirect from '/a' to '': / +Redirect from '/a' to '../../../foo': /../../../foo +Redirect from '/a' to 'space bar': failed +Redirect from '/other' to '/dir/page': /dir/page +Redirect from '/other' to 'dir/page': /dir/page +Redirect from '/other' to 'a': /a +Redirect from '/other' to 'other': /other +Redirect from '/other' to '': / +Redirect from '/other' to '../../../foo': /../../../foo +Redirect from '/other' to 'space bar': failed +Redirect from '' to '/dir/page': /dir/page +Redirect from '' to 'dir/page': /dir/page +Redirect from '' to 'a': /a +Redirect from '' to 'other': /other +Redirect from '' to '': / +Redirect from '' to '../../../foo': /../../../foo +Redirect from '' to 'space bar': failed +Redirect from '/../../../foo' to '/dir/page': /dir/page +Redirect from '/../../../foo' to 'dir/page': /../../..//dir/page +Redirect from '/../../../foo' to 'a': /a +Redirect from '/../../../foo' to 'other': /../../..//other +Redirect from '/../../../foo' to '': / +Redirect from '/../../../foo' to '../../../foo': /../../..//../../../foo +Redirect from '/../../../foo' to 'space bar': failed +Redirect from '/space bar' to '/dir/page': failed +Redirect from '/space bar' to 'dir/page': failed +Redirect from '/space bar' to 'a': failed +Redirect from '/space bar' to 'other': failed +Redirect from '/space bar' to '': failed +Redirect from '/space bar' to '../../../foo': failed +Redirect from '/space bar' to 'space bar': failed +# URI parser: Uri\WhatWg\Url +Redirect from '/dir/page' to '/dir/page': /dir/page +Redirect from '/dir/page' to 'dir/page': /dir//dir/page +Redirect from '/dir/page' to 'a': /a +Redirect from '/dir/page' to 'other': /dir//other +Redirect from '/dir/page' to '': / +Redirect from '/dir/page' to '../../../foo': /foo +Redirect from '/dir/page' to 'space bar': /dir//space%20bar +Redirect from '/a' to '/dir/page': /dir/page +Redirect from '/a' to 'dir/page': /dir/page +Redirect from '/a' to 'a': /a +Redirect from '/a' to 'other': /other +Redirect from '/a' to '': / +Redirect from '/a' to '../../../foo': /foo +Redirect from '/a' to 'space bar': /space%20bar +Redirect from '/other' to '/dir/page': /dir/page +Redirect from '/other' to 'dir/page': /dir/page +Redirect from '/other' to 'a': /a +Redirect from '/other' to 'other': /other +Redirect from '/other' to '': / +Redirect from '/other' to '../../../foo': /foo +Redirect from '/other' to 'space bar': /space%20bar +Redirect from '' to '/dir/page': /dir/page +Redirect from '' to 'dir/page': /dir/page +Redirect from '' to 'a': /a +Redirect from '' to 'other': /other +Redirect from '' to '': / +Redirect from '' to '../../../foo': /foo +Redirect from '' to 'space bar': /space%20bar +Redirect from '/../../../foo' to '/dir/page': /dir/page +Redirect from '/../../../foo' to 'dir/page': /dir/page +Redirect from '/../../../foo' to 'a': /a +Redirect from '/../../../foo' to 'other': /other +Redirect from '/../../../foo' to '': / +Redirect from '/../../../foo' to '../../../foo': /foo +Redirect from '/../../../foo' to 'space bar': /space%20bar +Redirect from '/space bar' to '/dir/page': /dir/page +Redirect from '/space bar' to 'dir/page': /dir/page +Redirect from '/space bar' to 'a': /a +Redirect from '/space bar' to 'other': /other +Redirect from '/space bar' to '': / +Redirect from '/space bar' to '../../../foo': /foo +Redirect from '/space bar' to 'space bar': /space%20bar From d00d29ed249f27f18b401d29f16b357e7333a7f4 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Fri, 4 Sep 2026 08:52:54 +0000 Subject: [PATCH 3/4] Add missing space --- ext/standard/tests/http/http_relative_redirect.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/standard/tests/http/http_relative_redirect.phpt b/ext/standard/tests/http/http_relative_redirect.phpt index 621099c6a643..9c3036da5a11 100644 --- a/ext/standard/tests/http/http_relative_redirect.phpt +++ b/ext/standard/tests/http/http_relative_redirect.phpt @@ -55,7 +55,7 @@ foreach ($uri_parsers as $uri_parser) { http_server_kill($pid); ?> --EXPECT-- -# URI parser: +# URI parser: Redirect from '/dir/page' to '/dir/page': /dir/page Redirect from '/dir/page' to 'dir/page': /dir//dir/page Redirect from '/dir/page' to 'a': /a From 7656770ca2038ceb9a49dfebd9a2504983bb1985 Mon Sep 17 00:00:00 2001 From: Sjoerd Langkemper Date: Fri, 4 Sep 2026 09:52:20 +0000 Subject: [PATCH 4/4] Remove test that doesn't increase coverage --- .../tests/http/http_auth_userinfo.phpt | 38 ------------------- 1 file changed, 38 deletions(-) delete mode 100644 ext/standard/tests/http/http_auth_userinfo.phpt diff --git a/ext/standard/tests/http/http_auth_userinfo.phpt b/ext/standard/tests/http/http_auth_userinfo.phpt deleted file mode 100644 index 56ca3e8ac841..000000000000 --- a/ext/standard/tests/http/http_auth_userinfo.phpt +++ /dev/null @@ -1,38 +0,0 @@ ---TEST-- -http wrapper attaches an Authorization: Basic header when the URL contains userinfo ---SKIPIF-- - ---INI-- -allow_url_fopen=1 ---FILE-- - $pid, 'uri' => $uri] = http_server($responses, $output); - -$host = parse_url($uri, PHP_URL_HOST); -$port = parse_url($uri, PHP_URL_PORT); - -file_get_contents("http://user@$host:$port/path"); -file_get_contents("http://user:pass@$host:$port/path"); - -rewind($output); -$requests = stream_get_contents($output); -if (!preg_match_all('~Authorization: .*~', $requests, $matches)) { - die('fail No authorization headers found'); -} - -foreach ($matches[0] as $header) { - echo $header . "\n"; -} - -http_server_kill($pid); - -?> ---EXPECT-- -Authorization: Basic dXNlcjo= -Authorization: Basic dXNlcjpwYXNz