Skip to content

Commit 5146d9f

Browse files
IMSoPnikic
authored andcommitted
http_fopen_wrapper.c - Handle HTTP headers with varying white space
The stream handler assumed all HTTP headers contained exactly one space, but the standard says there may be zero or more. Should fix Bug #47021, and any other edge cases caused by a web server sending unusual spacing, e.g. the MIME type discovered from Content-Type: can no longer contain leading whitespace. We strip trailing whitespace from the headers added into $http_response_header as well.
1 parent a46bbdd commit 5146d9f

File tree

5 files changed

+212
-17
lines changed

5 files changed

+212
-17
lines changed

NEWS

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ PHP NEWS
2020

2121
- Standard:
2222
. Fixed bug #69442 (closing of fd incorrect when PTS enabled). (jaytaph)
23+
. Fixed bug #47021 (SoapClient stumbles over WSDL delivered with
24+
"Transfer-Encoding: chunked"). (Rowan Collins)
2325

2426
- ZIP:
25-
. Fixed bug #70103 (ZipArchive::addGlob ignores remove_all_path option). (cmb,
26-
Mitch Hagstrand)
27+
. Fixed bug #70103 (ZipArchive::addGlob ignores remove_all_path option). (cmb,
28+
Mitch Hagstrand)
2729

2830
19 Jan 2017 PHP 7.0.15
2931

ext/standard/http_fopen_wrapper.c

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -755,8 +755,10 @@ php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
755755

756756
while (!body && !php_stream_eof(stream)) {
757757
size_t http_header_line_length;
758+
758759
if (php_stream_get_line(stream, http_header_line, HTTP_HEADER_BLOCK_SIZE, &http_header_line_length) && *http_header_line != '\n' && *http_header_line != '\r') {
759760
char *e = http_header_line + http_header_line_length - 1;
761+
char *http_header_value;
760762
if (*e != '\n') {
761763
do { /* partial header */
762764
if (php_stream_get_line(stream, http_header_line, HTTP_HEADER_BLOCK_SIZE, &http_header_line_length) == NULL) {
@@ -770,26 +772,54 @@ php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
770772
while (*e == '\n' || *e == '\r') {
771773
e--;
772774
}
773-
http_header_line_length = e - http_header_line + 1;
774-
http_header_line[http_header_line_length] = '\0';
775775

776-
if (!strncasecmp(http_header_line, "Location: ", 10)) {
776+
/* The primary definition of an HTTP header in RFC 7230 states:
777+
* > Each header field consists of a case-insensitive field name followed
778+
* > by a colon (":"), optional leading whitespace, the field value, and
779+
* > optional trailing whitespace. */
780+
781+
/* Strip trailing whitespace */
782+
while (*e == ' ' || *e == '\t') {
783+
e--;
784+
}
785+
786+
/* Terminate header line */
787+
e++;
788+
*e = '\0';
789+
http_header_line_length = e - http_header_line;
790+
791+
http_header_value = memchr(http_header_line, ':', http_header_line_length);
792+
if (http_header_value) {
793+
http_header_value++; /* Skip ':' */
794+
795+
/* Strip leading whitespace */
796+
while (http_header_value < e
797+
&& (*http_header_value == ' ' || *http_header_value == '\t')) {
798+
http_header_value++;
799+
}
800+
}
801+
802+
if (!strncasecmp(http_header_line, "Location:", sizeof("Location:")-1)) {
777803
if (context && (tmpzval = php_stream_context_get_option(context, "http", "follow_location")) != NULL) {
778804
follow_location = zval_is_true(tmpzval);
779-
} else if (!((response_code >= 300 && response_code < 304) || 307 == response_code || 308 == response_code)) {
805+
} else if (!((response_code >= 300 && response_code < 304)
806+
|| 307 == response_code || 308 == response_code)) {
780807
/* we shouldn't redirect automatically
781808
if follow_location isn't set and response_code not in (300, 301, 302, 303 and 307)
782809
see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.1
783810
RFC 7238 defines 308: http://tools.ietf.org/html/rfc7238 */
784811
follow_location = 0;
785812
}
786-
strlcpy(location, http_header_line + 10, sizeof(location));
787-
} else if (!strncasecmp(http_header_line, "Content-Type: ", 14)) {
788-
php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, http_header_line + 14, 0);
789-
} else if (!strncasecmp(http_header_line, "Content-Length: ", 16)) {
790-
file_size = atoi(http_header_line + 16);
813+
strlcpy(location, http_header_value, sizeof(location));
814+
} else if (!strncasecmp(http_header_line, "Content-Type:", sizeof("Content-Type:")-1)) {
815+
php_stream_notify_info(context, PHP_STREAM_NOTIFY_MIME_TYPE_IS, http_header_value, 0);
816+
} else if (!strncasecmp(http_header_line, "Content-Length:", sizeof("Content-Length")-1)) {
817+
file_size = atoi(http_header_value);
791818
php_stream_notify_file_size(context, file_size, http_header_line, 0);
792-
} else if (!strncasecmp(http_header_line, "Transfer-Encoding: chunked", sizeof("Transfer-Encoding: chunked"))) {
819+
} else if (
820+
!strncasecmp(http_header_line, "Transfer-Encoding:", sizeof("Transfer-Encoding")-1)
821+
&& !strncasecmp(http_header_value, "Chunked", sizeof("Chunked")-1)
822+
) {
793823

794824
/* create filter to decode response body */
795825
if (!(options & STREAM_ONLY_GET_HEADERS)) {
@@ -808,13 +838,9 @@ php_stream *php_stream_url_wrap_http_ex(php_stream_wrapper *wrapper,
808838
}
809839
}
810840

811-
if (http_header_line[0] == '\0') {
812-
body = 1;
813-
} else {
841+
{
814842
zval http_header;
815-
816843
ZVAL_STRINGL(&http_header, http_header_line, http_header_line_length);
817-
818844
zend_hash_next_index_insert(Z_ARRVAL(response_header), &http_header);
819845
}
820846
} else {
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
--TEST--
2+
Bug #47021 (SoapClient stumbles over WSDL delivered with "Transfer-Encoding: chunked")
3+
--INI--
4+
allow_url_fopen=1
5+
--SKIPIF--
6+
<?php require 'server.inc'; http_server_skipif('tcp://127.0.0.1:12342'); ?>
7+
--FILE--
8+
<?php
9+
require 'server.inc';
10+
11+
function stream_notification_callback($notification_code, $severity, $message, $message_code, $bytes_transferred, $bytes_max) {
12+
13+
switch($notification_code) {
14+
case STREAM_NOTIFY_MIME_TYPE_IS:
15+
echo "Type='$message'\n";
16+
break;
17+
case STREAM_NOTIFY_FILE_SIZE_IS:
18+
echo "Size=$bytes_max\n";
19+
break;
20+
}
21+
}
22+
23+
function do_test($num_spaces, $leave_trailing_space=false) {
24+
// SOAPClient exhibits the bug because it forces HTTP/1.1,
25+
// whereas file_get_contents() uses HTTP/1.0 by default.
26+
$options = [
27+
'http' => [
28+
'protocol_version' => '1.1',
29+
'header' => 'Connection: Close'
30+
],
31+
];
32+
33+
$ctx = stream_context_create($options);
34+
stream_context_set_params($ctx, array("notification" => "stream_notification_callback"));
35+
36+
$spaces = str_repeat(' ', $num_spaces);
37+
$trailing = ($leave_trailing_space ? ' ' : '');
38+
$responses = [
39+
"data://text/plain,HTTP/1.1 200 OK\r\n"
40+
. "Content-Type:{$spaces}text/plain{$trailing}\r\n"
41+
. "Transfer-Encoding:{$spaces}Chunked{$trailing}\r\n\r\n"
42+
. "5\nHello\n0\n",
43+
"data://text/plain,HTTP/1.1 200 OK\r\n"
44+
. "Content-Type\r\n" // Deliberately invalid header
45+
. "Content-Length:{$spaces}5{$trailing}\r\n\r\n"
46+
. "World"
47+
];
48+
$pid = http_server('tcp://127.0.0.1:12342', $responses);
49+
50+
echo file_get_contents('http://127.0.0.1:12342/', false, $ctx);
51+
echo "\n";
52+
echo file_get_contents('http://127.0.0.1:12342/', false, $ctx);
53+
echo "\n";
54+
55+
http_server_kill($pid);
56+
}
57+
58+
// Chunked decoding should be recognised by the HTTP stream wrapper regardless of whitespace
59+
// Transfer-Encoding:Chunked
60+
do_test(0);
61+
echo "\n";
62+
// Transfer-Encoding: Chunked
63+
do_test(1);
64+
echo "\n";
65+
// Transfer-Encoding: Chunked
66+
do_test(2);
67+
echo "\n";
68+
// Trailing space at end of header
69+
do_test(1, true);
70+
echo "\n";
71+
72+
?>
73+
--EXPECT--
74+
Type='text/plain'
75+
Hello
76+
Size=5
77+
World
78+
79+
Type='text/plain'
80+
Hello
81+
Size=5
82+
World
83+
84+
Type='text/plain'
85+
Hello
86+
Size=5
87+
World
88+
89+
Type='text/plain'
90+
Hello
91+
Size=5
92+
World
93+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
$http_reponse_header (header with trailing whitespace)
3+
--SKIPIF--
4+
<?php require 'server.inc'; http_server_skipif('tcp://127.0.0.1:22349'); ?>
5+
--INI--
6+
allow_url_fopen=1
7+
allow_url_include=1
8+
--FILE--
9+
<?php
10+
require 'server.inc';
11+
12+
$responses = array(
13+
"data://text/plain,HTTP/1.0 200 Ok\r\nSome: Header \r\n\r\nBody",
14+
);
15+
16+
$pid = http_server("tcp://127.0.0.1:22349", $responses, $output);
17+
18+
function test() {
19+
$f = file_get_contents('http://127.0.0.1:22349/');
20+
var_dump($f);
21+
var_dump($http_response_header);
22+
}
23+
test();
24+
25+
http_server_kill($pid);
26+
?>
27+
==DONE==
28+
--EXPECT--
29+
string(4) "Body"
30+
array(2) {
31+
[0]=>
32+
string(15) "HTTP/1.0 200 Ok"
33+
[1]=>
34+
string(14) "Some: Header"
35+
}
36+
==DONE==
37+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
$http_reponse_header (whitespace-only "header")
3+
--SKIPIF--
4+
<?php require 'server.inc'; http_server_skipif('tcp://127.0.0.1:22350'); ?>
5+
--INI--
6+
allow_url_fopen=1
7+
allow_url_include=1
8+
--FILE--
9+
<?php
10+
require 'server.inc';
11+
12+
$responses = array(
13+
"data://text/plain,HTTP/1.0 200 Ok\r\n \r\n\r\nBody",
14+
);
15+
16+
$pid = http_server("tcp://127.0.0.1:22350", $responses, $output);
17+
18+
function test() {
19+
$f = file_get_contents('http://127.0.0.1:22350/');
20+
var_dump($f);
21+
var_dump($http_response_header);
22+
}
23+
test();
24+
25+
http_server_kill($pid);
26+
?>
27+
==DONE==
28+
--EXPECT--
29+
string(4) "Body"
30+
array(2) {
31+
[0]=>
32+
string(15) "HTTP/1.0 200 Ok"
33+
[1]=>
34+
string(0) ""
35+
}
36+
==DONE==
37+

0 commit comments

Comments
 (0)