From 1ff99d8c39481eea263c3b36fd2711d9738616ff Mon Sep 17 00:00:00 2001 From: Arhitector Date: Sat, 5 Mar 2016 21:15:33 +0300 Subject: [PATCH 1/5] Fix Client::createCurlOptions Not replace CURLOPT_READFUNCTION. Avoid full loading large or unknown size body into memory. You can specify any method you'd like, including a custom method that might not be part of RFC 7231 (like "MOVE"). --- src/Client.php | 86 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/src/Client.php b/src/Client.php index 444596a..f7d7ca6 100644 --- a/src/Client.php +++ b/src/Client.php @@ -165,39 +165,73 @@ public function sendAsyncRequest(RequestInterface $request) * @return array */ private function createCurlOptions(RequestInterface $request) - { - $options = $this->options; + { + // Invalid overwrite Curl options. + $options = array_diff_key($this->options, array_flip([ + CURLOPT_HTTPGET, + CURLOPT_POST, + CURLOPT_UPLOAD, + CURLOPT_CUSTOMREQUEST, + CURLOPT_HTTPHEADER, + CURLOPT_INFILE, + CURLOPT_INFILESIZE + ])); - $options[CURLOPT_HEADER] = true; - $options[CURLOPT_RETURNTRANSFER] = true; - $options[CURLOPT_FOLLOWLOCATION] = false; + $options[CURLOPT_HEADER] = true; + $options[CURLOPT_RETURNTRANSFER] = true; + $options[CURLOPT_FOLLOWLOCATION] = false; + $options[CURLOPT_HTTP_VERSION] = $this->getProtocolVersion($request->getProtocolVersion()); + $options[CURLOPT_URL] = (string) $request->getUri(); - $options[CURLOPT_HTTP_VERSION] = $this->getProtocolVersion($request->getProtocolVersion()); - $options[CURLOPT_URL] = (string) $request->getUri(); + // These methods do not transfer body. + // You can specify any method you'd like, including a custom method that might not be part of RFC 7231 (like "MOVE"). + if (in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE', 'CONNECT'])) { + // Make cancellation CURLOPT_WRITEFUNCTION, CURLOPT_READFUNCTION ? I have not tested. + if ($request->getMethod() == 'HEAD') { + $options[CURLOPT_NOBODY] = true; + } + } else { // Allow custom methods with body transfer (PUT, PROPFIND and other.) + $body = clone $request->getBody(); + $size = $body->getSize(); - if (in_array($request->getMethod(), ['OPTIONS', 'POST', 'PUT'], true)) { - // cURL allows request body only for these methods. - $body = (string) $request->getBody(); - if ('' !== $body) { - $options[CURLOPT_POSTFIELDS] = $body; - } - } + // Send the body if the size is more than 1MB OR if the. + // The file to PUT must be set with CURLOPT_INFILE and CURLOPT_INFILESIZE. + if ($size === null || $size > 1024 * 1024) { + $options[CURLOPT_UPLOAD] = true; - if ($request->getMethod() === 'HEAD') { - $options[CURLOPT_NOBODY] = true; - } elseif ($request->getMethod() !== 'GET') { - // GET is a default method. Other methods should be specified explicitly. - $options[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); - } + // Note that using this option will not stop libcurl from sending more data, + // as exactly what is sent depends on CURLOPT_READFUNCTION. + if ($size !== null) { + $options[CURLOPT_INFILESIZE] = $size; + } - $options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options); + $body->rewind(); - if ($request->getUri()->getUserInfo()) { - $options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo(); - } + // Avoid full loading large or unknown size body into memory. Not replace CURLOPT_READFUNCTION. + $options[CURLOPT_INFILE] = $body->detach(); + } else { + // Send the body as a string if the size is less than 1MB. + $options[CURLOPT_POSTFIELDS] = (string)$request->getBody(); + } + } - return $options; - } + // GET is a default method. Other methods should be specified explicitly. + if ($request->getMethod() != 'GET') { + $options[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); + } + + // For PUT and POST need Content-Length see RFC 7230 section 3.3.2 + $options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options); + + if ($request->getUri() + ->getUserInfo() + ) { + $options[CURLOPT_USERPWD] = $request->getUri() + ->getUserInfo(); + } + + return $options; + } /** * Return cURL constant for specified HTTP version From b37f2d6a607cfdef1b7b67ee29cda0c8c77ee9e7 Mon Sep 17 00:00:00 2001 From: Arhitector Date: Sun, 6 Mar 2016 23:30:10 +0300 Subject: [PATCH 2/5] replace property visibility for a possibility of "extends" --- src/Client.php | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Client.php b/src/Client.php index f7d7ca6..2ac30d7 100644 --- a/src/Client.php +++ b/src/Client.php @@ -18,6 +18,11 @@ * * @author Михаил Красильников * @author Blake Williams + * + * @TODO + * не работают фильтры потоков Http\Message\Encoding т.к. используют копию php://memory, + * что очень плохо потому что это приводит к неконтролируемому расходу памяти и это противоречит PSR-7. + * Лучшее решение это отказаться от clue и использовать stream_copy_to_stream($stream, fopen('php://temp', 'wb')) * * @api * @since 1.0 @@ -29,28 +34,28 @@ class Client implements HttpClient, HttpAsyncClient * * @var array */ - private $options; + protected $options; /** * cURL response parser * * @var ResponseParser */ - private $responseParser; + protected $responseParser; /** * cURL synchronous requests handle * * @var resource|null */ - private $handle = null; + protected $handle = null; /** * Simultaneous requests runner * * @var MultiRunner|null */ - private $multiRunner = null; + protected $multiRunner = null; /** * Create new client @@ -164,7 +169,7 @@ public function sendAsyncRequest(RequestInterface $request) * * @return array */ - private function createCurlOptions(RequestInterface $request) + protected function createCurlOptions(RequestInterface $request) { // Invalid overwrite Curl options. $options = array_diff_key($this->options, array_flip([ @@ -211,7 +216,7 @@ private function createCurlOptions(RequestInterface $request) $options[CURLOPT_INFILE] = $body->detach(); } else { // Send the body as a string if the size is less than 1MB. - $options[CURLOPT_POSTFIELDS] = (string)$request->getBody(); + $options[CURLOPT_POSTFIELDS] = (string) $request->getBody(); } } @@ -223,9 +228,7 @@ private function createCurlOptions(RequestInterface $request) // For PUT and POST need Content-Length see RFC 7230 section 3.3.2 $options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options); - if ($request->getUri() - ->getUserInfo() - ) { + if ($request->getUri()->getUserInfo()) { $options[CURLOPT_USERPWD] = $request->getUri() ->getUserInfo(); } @@ -242,7 +245,7 @@ private function createCurlOptions(RequestInterface $request) * * @return int */ - private function getProtocolVersion($requestVersion) + protected function getProtocolVersion($requestVersion) { switch ($requestVersion) { case '1.0': @@ -266,7 +269,7 @@ private function getProtocolVersion($requestVersion) * * @return string[] */ - private function createHeaders(RequestInterface $request, array $options) + protected function createHeaders(RequestInterface $request, array $options) { $curlHeaders = []; $headers = array_keys($request->getHeaders()); From abbf8fa509763c4fc5b9c460b1f9763364689667 Mon Sep 17 00:00:00 2001 From: Arhitector Date: Thu, 10 Mar 2016 13:31:03 +0300 Subject: [PATCH 3/5] fix out of memory. (issues #15) not correctly, leads to hang the server. fix inheritance support. fix custom reading function (CURLOPT_READFUNCTION). fix custom methods. fix PATCH with send body. fix support native PHP stream filters. fix support follow location (CURLOPT_FOLLOWLOCATION). fix compatible stream with PSR-7 specifications. fix out of memory when a large body send of response. fix "Expect" and "Accept" header. fix out of memory with sendAsyncRequest in some cases. --- src/Client.php | 163 +++++++++++++++++++----------------- src/ResponseParser.php | 106 +++++++++++++++++++---- src/Tools/HeadersParser.php | 3 +- 3 files changed, 178 insertions(+), 94 deletions(-) diff --git a/src/Client.php b/src/Client.php index 2ac30d7..a61f3a9 100644 --- a/src/Client.php +++ b/src/Client.php @@ -1,4 +1,5 @@ * @author Blake Williams - * - * @TODO - * не работают фильтры потоков Http\Message\Encoding т.к. используют копию php://memory, - * что очень плохо потому что это приводит к неконтролируемому расходу памяти и это противоречит PSR-7. - * Лучшее решение это отказаться от clue и использовать stream_copy_to_stream($stream, fopen('php://temp', 'wb')) + * @author Dmitry Arhitector * * @api * @since 1.0 */ class Client implements HttpClient, HttpAsyncClient { - /** + + /** * cURL options * * @var array @@ -57,6 +55,7 @@ class Client implements HttpClient, HttpAsyncClient */ protected $multiRunner = null; + /** * Create new client * @@ -73,29 +72,17 @@ class Client implements HttpClient, HttpAsyncClient * * @since 1.0 */ - public function __construct( - MessageFactory $messageFactory, - StreamFactory $streamFactory, - array $options = [] - ) { + public function __construct(MessageFactory $messageFactory, StreamFactory $streamFactory, array $options = []) + { $this->responseParser = new ResponseParser($messageFactory, $streamFactory); $this->options = $options; } - /** - * Release resources if still active - */ - public function __destruct() - { - if (is_resource($this->handle)) { - curl_close($this->handle); - } - } - /** * Sends a PSR-7 request. * * @param RequestInterface $request + * @param array $options custom curl options * * @return ResponseInterface * @@ -104,27 +91,24 @@ public function __destruct() * * @since 1.0 */ - public function sendRequest(RequestInterface $request) + public function sendRequest(RequestInterface $request, array $options = []) { - $options = $this->createCurlOptions($request); - if (is_resource($this->handle)) { curl_reset($this->handle); } else { $this->handle = curl_init(); } + $options = $this->createCurlOptions($request, $options); + curl_setopt_array($this->handle, $options); - $raw = curl_exec($this->handle); - if (curl_errno($this->handle) > 0) { + if ( ! curl_exec($this->handle)) { throw new RequestException(curl_error($this->handle), $request); } - $info = curl_getinfo($this->handle); - try { - $response = $this->responseParser->parse($raw, $info); + $response = $this->responseParser->parse($options[CURLOPT_FILE], curl_getinfo($this->handle)); } catch (\Exception $e) { throw new RequestException($e->getMessage(), $request, $e); } @@ -135,6 +119,7 @@ public function sendRequest(RequestInterface $request) * Sends a PSR-7 request in an asynchronous way. * * @param RequestInterface $request + * @param array $options custom curl options * * @return Promise * @@ -143,94 +128,107 @@ public function sendRequest(RequestInterface $request) * * @since 1.0 */ - public function sendAsyncRequest(RequestInterface $request) + public function sendAsyncRequest(RequestInterface $request, array $options = []) { - if (!$this->multiRunner instanceof MultiRunner) { + if ( ! $this->multiRunner instanceof MultiRunner) { $this->multiRunner = new MultiRunner($this->responseParser); } $handle = curl_init(); - $options = $this->createCurlOptions($request); + $options = $this->createCurlOptions($request, $options); + curl_setopt_array($handle, $options); $core = new PromiseCore($request, $handle); $promise = new CurlPromise($core, $this->multiRunner); + $this->multiRunner->add($core); return $promise; } + /** + * Get parser + * + * @return ResponseParser + */ + public function getResponseParser() + { + return $this->responseParser; + } + + /** + * Release resources if still active + */ + public function __destruct() + { + if (is_resource($this->handle)) { + curl_close($this->handle); + } + } + /** * Generates cURL options * * @param RequestInterface $request + * @param array $options custom curl options * * @throws \UnexpectedValueException if unsupported HTTP version requested * * @return array */ - protected function createCurlOptions(RequestInterface $request) + protected function createCurlOptions(RequestInterface $request, array $options = []) { - // Invalid overwrite Curl options. - $options = array_diff_key($this->options, array_flip([ - CURLOPT_HTTPGET, - CURLOPT_POST, - CURLOPT_UPLOAD, - CURLOPT_CUSTOMREQUEST, - CURLOPT_HTTPHEADER, - CURLOPT_INFILE, - CURLOPT_INFILESIZE - ])); - - $options[CURLOPT_HEADER] = true; - $options[CURLOPT_RETURNTRANSFER] = true; - $options[CURLOPT_FOLLOWLOCATION] = false; - $options[CURLOPT_HTTP_VERSION] = $this->getProtocolVersion($request->getProtocolVersion()); - $options[CURLOPT_URL] = (string) $request->getUri(); - - // These methods do not transfer body. - // You can specify any method you'd like, including a custom method that might not be part of RFC 7231 (like "MOVE"). + // Invalid overwrite Curl options. + $options = array_diff_key($options + $this->options, array_flip([CURLOPT_INFILE, CURLOPT_INFILESIZE])); + $options[CURLOPT_HTTP_VERSION] = $this->getProtocolVersion($request->getProtocolVersion()); + $options[CURLOPT_HEADERFUNCTION] = [$this->getResponseParser(), 'headerHandler']; + $options[CURLOPT_URL] = (string) $request->getUri(); + $options[CURLOPT_RETURNTRANSFER] = false; + $options[CURLOPT_FILE] = $this->getResponseParser()->getTemporaryStream(); + $options[CURLOPT_HEADER] = false; + + // These methods do not transfer body. + // You can specify any method you'd like, including a custom method that might not be part of RFC 7231 (like "MOVE"). if (in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE', 'CONNECT'])) { - // Make cancellation CURLOPT_WRITEFUNCTION, CURLOPT_READFUNCTION ? I have not tested. if ($request->getMethod() == 'HEAD') { $options[CURLOPT_NOBODY] = true; + + unset($options[CURLOPT_READFUNCTION], $options[CURLOPT_WRITEFUNCTION]); } - } else { // Allow custom methods with body transfer (PUT, PROPFIND and other.) + } else { $body = clone $request->getBody(); $size = $body->getSize(); - // Send the body if the size is more than 1MB OR if the. - // The file to PUT must be set with CURLOPT_INFILE and CURLOPT_INFILESIZE. - if ($size === null || $size > 1024 * 1024) { - $options[CURLOPT_UPLOAD] = true; - - // Note that using this option will not stop libcurl from sending more data, - // as exactly what is sent depends on CURLOPT_READFUNCTION. - if ($size !== null) { - $options[CURLOPT_INFILESIZE] = $size; - } + if ($size === null || $size > 1048576) { + $body->rewind(); + $options[CURLOPT_UPLOAD] = true; - $body->rewind(); - - // Avoid full loading large or unknown size body into memory. Not replace CURLOPT_READFUNCTION. - $options[CURLOPT_INFILE] = $body->detach(); + // Avoid full loading large or unknown size body into memory. Not replace CURLOPT_READFUNCTION. + if (isset($options[CURLOPT_READFUNCTION]) && is_callable($options[CURLOPT_READFUNCTION])) { + $body = $body->detach(); + $options[CURLOPT_READFUNCTION] = function ($curlHandler, $handler, $length) use ($body, $options) { + return call_user_func($options[CURLOPT_READFUNCTION], $curlHandler, $body, $length); + }; + } else { + $options[CURLOPT_READFUNCTION] = function ($curl, $handler, $length) use ($body) { + return $body->read($length); + }; + } } else { - // Send the body as a string if the size is less than 1MB. + // Send the body as a string if the size is less than 1MB. $options[CURLOPT_POSTFIELDS] = (string) $request->getBody(); } } - // GET is a default method. Other methods should be specified explicitly. if ($request->getMethod() != 'GET') { $options[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); } - // For PUT and POST need Content-Length see RFC 7230 section 3.3.2 $options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options); if ($request->getUri()->getUserInfo()) { - $options[CURLOPT_USERPWD] = $request->getUri() - ->getUserInfo(); + $options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo(); } return $options; @@ -256,8 +254,10 @@ protected function getProtocolVersion($requestVersion) if (defined('CURL_HTTP_VERSION_2_0')) { return CURL_HTTP_VERSION_2_0; } + throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support'); } + return CURL_HTTP_VERSION_NONE; } @@ -273,19 +273,32 @@ protected function createHeaders(RequestInterface $request, array $options) { $curlHeaders = []; $headers = array_keys($request->getHeaders()); + + if ( ! $request->hasHeader('Expect')) { + $curlHeaders[] = 'Expect:'; + } + + if ( ! $request->hasHeader('Accept')) { + $curlHeaders[] = 'Accept: */*'; + } + foreach ($headers as $name) { if (strtolower($name) === 'content-length') { $values = [0]; + if (array_key_exists(CURLOPT_POSTFIELDS, $options)) { $values = [strlen($options[CURLOPT_POSTFIELDS])]; } } else { $values = $request->getHeader($name); } + foreach ($values as $value) { - $curlHeaders[] = $name . ': ' . $value; + $curlHeaders[] = $name. ': '.$value; } } + return $curlHeaders; } -} + +} \ No newline at end of file diff --git a/src/ResponseParser.php b/src/ResponseParser.php index 714f832..0fade3b 100644 --- a/src/ResponseParser.php +++ b/src/ResponseParser.php @@ -1,4 +1,5 @@ streamFactory = $streamFactory; } + /** + * Get factory + * + * @return MessageFactory + */ + public function getMessageFactory() + { + return $this->messageFactory; + } + + /** + * Get factory + * + * @return StreamFactory + */ + public function getStreamFactory() + { + return $this->streamFactory; + } + + /** + * Temporary body (fix out of memory) + * + * @return resource + */ + public function getTemporaryStream() + { + if ( ! is_resource($this->temporaryStream)) + { + $this->temporaryStream = fopen('php://temp', 'w+'); + } + + return $this->temporaryStream; + } + /** * Parse cURL response * - * @param string $raw raw response - * @param array $info cURL response info + * @param resource $raw raw response + * @param array $info cURL response info * * @return ResponseInterface * * @throws \InvalidArgumentException * @throws \RuntimeException */ - public function parse($raw, array $info) + public function parse($raw = null, array $info) { - $response = $this->messageFactory->createResponse(); - - $headerSize = $info['header_size']; - $rawHeaders = substr($raw, 0, $headerSize); + if (empty($raw)) // fix promise out of memory + { + $raw = $this->getTemporaryStream(); + } $parser = new HeadersParser(); - $response = $parser->parseString($rawHeaders, $response); - /* - * substr can return boolean value for empty string. But createStream does not support - * booleans. Converting to string. - */ - $content = (string) substr($raw, $headerSize); - $stream = $this->streamFactory->createStream($content); - $response = $response->withBody($stream); + $response = $this->messageFactory->createResponse(); + $response = $parser->parseArray($this->headers, $response); + $response = $response->withBody($this->streamFactory->createStream($raw)); return $response; } -} + + public function headerHandler($handler, $header) + { + $this->headers[] = $header; + + if ($this->followLocation) { + $this->followLocation = false; + $this->headers = [$header]; + } else if ( ! trim($header)) { + $this->followLocation = true; + //$this->parse(); + } + + return strlen($header); + } + +} \ No newline at end of file diff --git a/src/Tools/HeadersParser.php b/src/Tools/HeadersParser.php index 8644570..11b0c44 100644 --- a/src/Tools/HeadersParser.php +++ b/src/Tools/HeadersParser.php @@ -1,4 +1,5 @@ parseArray(explode("\r\n", $headers), $response); } -} +} \ No newline at end of file From 32ab33fcbdd90764d710da3dc9235aebb337c726 Mon Sep 17 00:00:00 2001 From: Arhitector Date: Thu, 10 Mar 2016 15:26:35 +0300 Subject: [PATCH 4/5] fix cloning stream --- src/ResponseParser.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ResponseParser.php b/src/ResponseParser.php index 0fade3b..1762f1d 100644 --- a/src/ResponseParser.php +++ b/src/ResponseParser.php @@ -124,6 +124,8 @@ public function parse($raw = null, array $info) $response = $parser->parseArray($this->headers, $response); $response = $response->withBody($this->streamFactory->createStream($raw)); + $this->temporaryStream = null; + return $response; } @@ -142,4 +144,4 @@ public function headerHandler($handler, $header) return strlen($header); } -} \ No newline at end of file +} From 0e5cfd0ec3ce8d3f8cfce82320cb29e0e0ac48ef Mon Sep 17 00:00:00 2001 From: Arhitector Date: Thu, 10 Mar 2016 21:57:39 +0300 Subject: [PATCH 5/5] description added --- src/ResponseParser.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/ResponseParser.php b/src/ResponseParser.php index 1762f1d..73f740f 100644 --- a/src/ResponseParser.php +++ b/src/ResponseParser.php @@ -13,6 +13,7 @@ * @license http://opensource.org/licenses/MIT MIT * * @author Михаил Красильников + * @author Dmitry Arhitector */ class ResponseParser { @@ -39,7 +40,7 @@ class ResponseParser protected $streamFactory; /** - * + * Temporary resource * * @var resource */ @@ -120,8 +121,7 @@ public function parse($raw = null, array $info) $parser = new HeadersParser(); - $response = $this->messageFactory->createResponse(); - $response = $parser->parseArray($this->headers, $response); + $response = $parser->parseArray($this->headers, $this->messageFactory->createResponse()); $response = $response->withBody($this->streamFactory->createStream($raw)); $this->temporaryStream = null; @@ -129,6 +129,14 @@ public function parse($raw = null, array $info) return $response; } + /** + * Save the response headers + * + * @param resource $handler curl handler + * @param string $header raw header + * + * @return integer + */ public function headerHandler($handler, $header) { $this->headers[] = $header; @@ -138,7 +146,7 @@ public function headerHandler($handler, $header) $this->headers = [$header]; } else if ( ! trim($header)) { $this->followLocation = true; - //$this->parse(); + //$this->parse(null, []); } return strlen($header);