Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15420053
D19011.id45798.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
10 KB
Referenced Files
None
Subscribers
None
D19011.id45798.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -257,6 +257,9 @@
'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php',
'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php',
+ 'PhutilHTTPResponse' => 'parser/http/PhutilHTTPResponse.php',
+ 'PhutilHTTPResponseParser' => 'parser/http/PhutilHTTPResponseParser.php',
+ 'PhutilHTTPResponseParserTestCase' => 'parser/http/__tests__/PhutilHTTPResponseParserTestCase.php',
'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php',
'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php',
@@ -890,6 +893,9 @@
'PhutilGitURITestCase' => 'PhutilTestCase',
'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilHTTPEngineExtension' => 'Phobject',
+ 'PhutilHTTPResponse' => 'Phobject',
+ 'PhutilHTTPResponseParser' => 'Phobject',
+ 'PhutilHTTPResponseParserTestCase' => 'PhutilTestCase',
'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
'PhutilHashingIterator' => array(
'PhutilProxyIterator',
diff --git a/src/parser/http/PhutilHTTPResponse.php b/src/parser/http/PhutilHTTPResponse.php
new file mode 100644
--- /dev/null
+++ b/src/parser/http/PhutilHTTPResponse.php
@@ -0,0 +1,40 @@
+<?php
+
+final class PhutilHTTPResponse
+ extends Phobject {
+
+ private $headers = array();
+ private $body;
+ private $status;
+
+ public function __construct() {
+ $this->body = new PhutilRope();
+ }
+
+ public function setHeaders(array $headers) {
+ $this->headers = $headers;
+ return $this;
+ }
+
+ public function getHeaders() {
+ return $this->headers;
+ }
+
+ public function setStatus(HTTPFutureResponseStatus $status) {
+ $this->status = $status;
+ return $this;
+ }
+
+ public function getStatus() {
+ return $this->status;
+ }
+
+ public function appendBody($bytes) {
+ $this->body->append($bytes);
+ }
+
+ public function getBody() {
+ return $this->body->getAsString();
+ }
+
+}
diff --git a/src/parser/http/PhutilHTTPResponseParser.php b/src/parser/http/PhutilHTTPResponseParser.php
new file mode 100644
--- /dev/null
+++ b/src/parser/http/PhutilHTTPResponseParser.php
@@ -0,0 +1,174 @@
+<?php
+
+final class PhutilHTTPResponseParser extends Phobject {
+
+ private $followLocationHeaders;
+ private $responses = array();
+ private $response;
+ private $buffer;
+ private $state = 'headers';
+
+ public function setFollowLocationHeaders($follow_location_headers) {
+ $this->followLocationHeaders = $follow_location_headers;
+ return $this;
+ }
+
+ public function getFollowLocationHeaders() {
+ return $this->followLocationHeaders;
+ }
+
+ public function readBytes($bytes) {
+ if ($this->state == 'discard') {
+ return $this;
+ }
+
+ $this->buffer .= $bytes;
+
+ while (true) {
+ if ($this->state == 'headers') {
+ $matches = null;
+
+ $ok = preg_match(
+ "/(\r?\n\r?\n)/",
+ $this->buffer,
+ $matches,
+ PREG_OFFSET_CAPTURE);
+ if (!$ok) {
+ break;
+ }
+
+ $headers_len = $matches[1][1];
+ $boundary_len = strlen($matches[1][0]);
+ $raw_headers = substr($this->buffer, 0, $headers_len);
+ $this->buffer = substr($this->buffer, $headers_len + $boundary_len);
+
+ $header_lines = phutil_split_lines($raw_headers);
+ $first_line = array_shift($header_lines);
+ $response_valid = true;
+
+ $matches = null;
+ $ok = preg_match(
+ '(^HTTP/\S+\s+(\d+)\s+(.*)$)i',
+ $first_line,
+ $matches);
+
+ if ($ok) {
+ $http_code = (int)$matches[1];
+ $http_status = phutil_utf8_strtolower($matches[2]);
+ } else {
+ $response_valid = false;
+ }
+
+ $header_list = array();
+ $saw_location = false;
+ foreach ($header_lines as $header_line) {
+ $pos = strpos($header_line, ':');
+ if ($pos === false) {
+ $response_valid = false;
+ break;
+ }
+
+ $name = substr($header_line, 0, $pos);
+ $value = ltrim(substr($header_line, $pos + 1), ' ');
+
+ if (phutil_utf8_strtolower($name) == 'location') {
+ $saw_location = true;
+ }
+
+ $header_list[] = array(
+ $name,
+ $value,
+ );
+ }
+
+ // If the response didn't start with a properly formatted "HTTP/..."
+ // line, or any of the header lines were not formatted correctly, add
+ // a malformed response to the response list and discard anything else
+ // we're given.
+ if (!$response_valid) {
+ $malformed = new HTTPFutureParseResponseStatus(
+ HTTPFutureParseResponseStatus::ERROR_MALFORMED_RESPONSE,
+ $raw_headers);
+
+ $this->newHTTPRepsonse()
+ ->setStatus($malformed);
+
+ $this->buffer = '';
+ $this->state = 'discard';
+ break;
+ }
+
+ // Otherwise, we have a valid set of response headers.
+ $response_status = new HTTPFutureHTTPResponseStatus(
+ $http_code,
+ null,
+ $header_list);
+
+ $this->newHTTPResponse()
+ ->setStatus($response_status)
+ ->setHeaders($header_list);
+
+ $is_https_proxy =
+ ($http_code === 200) &&
+ ($http_status === 'connection established');
+
+ if ($http_code === 100) {
+ // If this is "HTTP/1.1 100 Continue", this is just the server
+ // telling us that everything is okay. This response won't have
+ // a body associated with it.
+ $more_headers = true;
+ } else if ($is_https_proxy) {
+ // If this is "HTTP/1.1 200 Connection Established", this is a
+ // response to a CONNECT request made automatically by cURL to
+ // an HTTPS proxy. This response won't have a body associated
+ // with it, and the real body will follow later.
+ $more_headers = true;
+ } else if ($saw_location && $this->followLocationHeaders) {
+ // If we're following location headers and this response had
+ // a location header, cURL will automatically follow it. This
+ // response shouldn't have a body.
+ $more_headers = true;
+ } else {
+ $more_headers = false;
+ }
+
+ // If we're expecting more headers, we're going to stay in the
+ // "headers" state and parse another set of headers. Otherwise,
+ // we transition to the "body" state and look for a body.
+ if (!$more_headers) {
+ $this->state = 'body';
+ }
+
+ continue;
+ }
+
+ if ($this->state == 'body') {
+ if (strlen($this->buffer)) {
+ $this->response->appendBody($this->buffer);
+ $this->buffer = '';
+ }
+ break;
+ }
+ }
+
+ return $this;
+ }
+
+ public function getResponses() {
+ if ($this->state !== 'body') {
+ throw new HTTPFutureParseResponseStatus(
+ HTTPFutureParseResponseStatus::ERROR_MALFORMED_RESPONSE,
+ $this->buffer);
+ }
+
+ return $this->responses;
+ }
+
+ private function newHTTPResponse() {
+ $response = new PhutilHTTPResponse();
+ $this->responses[] = $response;
+ $this->response = $response;
+ return $response;
+ }
+
+}
diff --git a/src/parser/http/__tests__/PhutilHTTPResponseParserTestCase.php b/src/parser/http/__tests__/PhutilHTTPResponseParserTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/parser/http/__tests__/PhutilHTTPResponseParserTestCase.php
@@ -0,0 +1,124 @@
+<?php
+
+final class PhutilHTTPResponseParserTestCase
+ extends PhutilTestCase {
+
+ public function testSimpleParsing() {
+ $input = <<<EORESPONSE
+HTTP/1.0 200 OK
+Duck: Quack
+
+I am the very model of a modern major general.
+EORESPONSE;
+
+ $this->assertParse(
+ array(
+ array(
+ 'headers' => array(
+ array('Duck', 'Quack'),
+ ),
+ 'body' => 'I am the very model of a modern major general.',
+ ),
+ ),
+ $input);
+
+ $input = <<<EORESPONSE
+HTTP/1.0 200 Connection Established
+X-I-Am-A-Proxy-Server: Hello
+
+HTTP/1.0 100 Continue
+X-Everything-Is-Fine: true
+
+HTTP/1.1 302 Found
+Location: Over-There
+
+HTTP/1.0 404 Not Found
+
+Not all who wander are lost.
+EORESPONSE;
+
+ $this->assertParse(
+ array(
+ array(
+ 'code' => 200,
+ 'headers' => array(
+ array('X-I-Am-A-Proxy-Server', 'Hello'),
+ ),
+ 'body' => '',
+ ),
+ array(
+ 'code' => 100,
+ 'headers' => array(
+ array('X-Everything-Is-Fine', 'true'),
+ ),
+ 'body' => '',
+ ),
+ array(
+ 'code' => 302,
+ 'headers' => array(
+ array('Location', 'Over-There'),
+ ),
+ 'body' => '',
+ ),
+ array(
+ 'code' => 404,
+ 'headers' => array(),
+ 'body' => 'Not all who wander are lost.',
+ ),
+ ),
+ $input,
+ id(new PhutilHTTPResponseParser())
+ ->setFollowLocationHeaders(true));
+ }
+
+ private function assertParse(array $expect, $input, $parser = null) {
+ if ($parser === null) {
+ $parser = new PhutilHTTPResponseParser();
+ }
+
+ // Submit the input in little bits to try to catch any weird parser bugs.
+ $length = strlen($input);
+ $pos = 0;
+ while ($pos < $length) {
+ $next_len = mt_rand(1, 32);
+ $piece = substr($input, $pos, $next_len);
+ $pos = $pos + $next_len;
+
+ $parser->readBytes($piece);
+ }
+
+ $responses = $parser->getResponses();
+
+ $this->assertEqual(count($expect), count($responses));
+
+ $expect = array_values($expect);
+ $responses = array_values($responses);
+
+ for ($ii = 0; $ii < count($expect); $ii++) {
+ $expect_map = $expect[$ii];
+ $actual = $responses[$ii];
+
+ foreach ($expect_map as $key => $spec) {
+ switch ($key) {
+ case 'headers':
+ $this->assertEqual($spec, $actual->getHeaders());
+ break;
+ case 'body':
+ $this->assertEqual($spec, $actual->getBody());
+ break;
+ case 'code':
+ $status = $actual->getStatus();
+ $this->assertTrue($status instanceof HTTPFutureHTTPResponseStatus);
+ $this->assertEqual($spec, $status->getStatusCode());
+ break;
+ default:
+ throw new Exception(
+ pht(
+ 'Unknown HTTPResponseParser test ("%s").',
+ $key));
+ }
+ }
+ }
+ }
+
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mar 22 2025, 10:55 AM (4 w, 6 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7707506
Default Alt Text
D19011.id45798.diff (10 KB)
Attached To
Mode
D19011: Provide a streaming HTTP response parser
Attached
Detach File
Event Timeline
Log In to Comment