diff --git a/sapi/cli/php_cli_server.c b/sapi/cli/php_cli_server.c index 5df12648ca84..9f13f0647700 100644 --- a/sapi/cli/php_cli_server.c +++ b/sapi/cli/php_cli_server.c @@ -176,6 +176,7 @@ typedef struct php_cli_server_client { bool request_read; bool too_large_post; bool headers_written; + bool expect_continue; zend_string *current_header_name; zend_string *current_header_value; enum { HEADER_NONE=0, HEADER_FIELD, HEADER_VALUE } last_header_element; @@ -1794,6 +1795,13 @@ static int php_cli_server_client_read_request_on_headers_complete(php_http_parse return 2; } + zval *expect_val = zend_hash_str_find(&client->request.headers, "expect", sizeof("expect") - 1); + if (expect_val && Z_TYPE_P(expect_val) == IS_STRING + && zend_string_equals_literal_ci(Z_STR_P(expect_val), "100-continue") + && parser->http_major == 1 && parser->http_minor == 1) { + client->expect_continue = true; + } + return 0; } @@ -1901,6 +1909,23 @@ static int php_cli_server_client_read_request(php_cli_server_client *client, cha return -1; } + if (client->expect_continue && !client->request_read) { + /* Parser completed headers with Expect: 100-continue but hasn't + * finished reading the body. Send 100 Continue before the client + * sends the request body. Only supported in HTTP/1.1. */ + static const char continue_response[] = "HTTP/1.1 100 Continue\r\n\r\n"; + bool send_success = false; + client->expect_continue = false; + zend_try { + size_t sent = php_cli_server_client_send_through(client, continue_response, strlen(continue_response)); + send_success = sent == strlen(continue_response); + } zend_end_try(); + if (!send_success) { + *errstr = php_socket_strerror(php_socket_errno(), NULL, 0); + return -1; + } + } + return client->request_read ? 1: 0; } /* }}} */ @@ -1985,6 +2010,7 @@ static void php_cli_server_client_ctor(php_cli_server_client *client, php_cli_se client->request_read = false; client->too_large_post = false; client->headers_written = false; + client->expect_continue = false; client->last_header_element = HEADER_NONE; client->current_header_name = NULL; diff --git a/sapi/cli/tests/php_cli_server.inc b/sapi/cli/tests/php_cli_server.inc index feee2bbb5686..ec370753573c 100644 --- a/sapi/cli/tests/php_cli_server.inc +++ b/sapi/cli/tests/php_cli_server.inc @@ -5,6 +5,7 @@ class CliServerInfo { public function __construct( public string $docRoot, public $processHandle, + public $outputFile, ) {} } @@ -118,7 +119,7 @@ function php_cli_server_start( define("PHP_CLI_SERVER_PORT", $port); define("PHP_CLI_SERVER_ADDRESS", PHP_CLI_SERVER_HOSTNAME.":".PHP_CLI_SERVER_PORT); - return new CliServerInfo($doc_root, $handle); + return new CliServerInfo($doc_root, $handle, $output_file); } function php_cli_server_connect() { diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt new file mode 100644 index 000000000000..37886fc25e1e --- /dev/null +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_curl.phpt @@ -0,0 +1,35 @@ +--TEST-- +Expect 100-continue behavior in PHP development server (curl) +--SKIPIF-- + +--EXTENSIONS-- +curl +--FILE-- + +--EXPECT-- +int(0) +Did the PHP development server send a HTTP/1.1 100 Continue header? +bool(true) diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt new file mode 100644 index 000000000000..c792630a9305 --- /dev/null +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_iua.phpt @@ -0,0 +1,33 @@ +--TEST-- +Failure to send "100 Continue" is reported with ignore_user_abort=1 +--SKIPIF-- + +--FILE-- + 1, 'l_linger' => 0]); +fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\n\r\n"); +fclose($fp); + +$output = ''; +for ($i = 0; $i < 100 && !str_contains($output, 'Invalid request'); $i++) { + usleep(50000); + $output = file_get_contents($server->outputFile); +} + +var_dump(str_contains($output, 'Invalid request'), str_contains($output, 'Unexpected EOF')); +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(false) diff --git a/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt b/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt new file mode 100644 index 000000000000..0d386155721d --- /dev/null +++ b/sapi/cli/tests/php_cli_server_expect_100_continue_socket.phpt @@ -0,0 +1,86 @@ +--TEST-- +Expect 100-continue behavior in PHP development server (sockets) +--SKIPIF-- + +--FILE-- + 1, + 'l_linger' => 0, + ] + ); +} +stream_socket_shutdown($fp, STREAM_SHUT_RD); +fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 4\r\nConnection: close\r\n\r\n"); +fclose($fp); + +$fp = php_cli_server_connect(); +fwrite($fp, "GET / HTTP/1.1\r\nConnection: close\r\n\r\n"); +echo fgets($fp); +fclose($fp); + +echo "# GET with Expect header (no body).\n"; +$fp = php_cli_server_connect(); +fwrite($fp, "GET / HTTP/1.1\r\nExpect: 100-continue\r\nConnection: close\r\n\r\n"); +echo fgets($fp); +fclose($fp); + +echo "# POST with empty body.\n"; +$fp = php_cli_server_connect(); +fwrite($fp, "POST / HTTP/1.1\r\nExpect: 100-continue\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); +echo fgets($fp); +fclose($fp); + +echo "# Lower-case expect header.\n"; +$fp = php_cli_server_connect(); +fwrite($fp, "POST / HTTP/1.1\r\nexpect: 100-continue\r\nContent-Length: 4\r\nConnection: close\r\n\r\n"); +echo fgets($fp); +fclose($fp); +?> +--EXPECT-- +# Send Expect: 100-continue header, receive 100 Continue response. +HTTP/1.1 100 Continue + +HTTP/1.1 200 OK +# Send Expect: 100-continue header on HTTP/1.0. +int(0) +HTTP/1.0 200 OK +# Send Expect: 100-continue header and disconnect. +HTTP/1.1 200 OK +# GET with Expect header (no body). +HTTP/1.1 200 OK +# POST with empty body. +HTTP/1.1 200 OK +# Lower-case expect header. +HTTP/1.1 100 Continue