From ae70119ae5e840d841b59549565e2181e5751882 Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Fri, 23 Jul 2021 17:15:13 +0000 Subject: [PATCH] WIP: Create the PSR-18 PeclHttp client from original Request/Response classes. --- src/Client/Peclhttp.php | 147 ++++++++++++++++++++++++++++++-- src/Request/Psr7ToPeclHttp.php | 42 +++++++++ src/Response/PeclHttpToPsr7.php | 47 ++++++++++ src/StreamUtils.php | 26 ++++++ 4 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 src/Request/Psr7ToPeclHttp.php create mode 100644 src/Response/PeclHttpToPsr7.php create mode 100644 src/StreamUtils.php diff --git a/src/Client/Peclhttp.php b/src/Client/Peclhttp.php index d70dd20..478f693 100644 --- a/src/Client/Peclhttp.php +++ b/src/Client/Peclhttp.php @@ -14,8 +14,11 @@ declare(strict_types=1); namespace Horde\Http\Client; +use Horde\Http\Constants; use Horde\Http\ClientException; +use Horde\Http\Request\Psr7ToPeclHttp; use Horde\Http\Response; +use Horde\Http\Response\PeclHttpToPsr7; use Horde\Http\ResponseFactory; use Horde\Http\StreamFactory; use Psr\Http\Client\ClientInterface; @@ -25,6 +28,7 @@ use Psr\Http\Message\StreamInterface; use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Client\ClientExceptionInterface; +use \Horde_Support_CaseInsensitiveArray; /** * HTTP client for the pecl_http extension * @@ -34,19 +38,119 @@ * * Ported from the original Request/Response implementation */ -class PeclHttp +class PeclHttp implements ClientInterface { - protected $_httpAuthSchemes = array( - Horde_Http::AUTH_ANY => HTTP_AUTH_ANY, - Horde_Http::AUTH_BASIC => HTTP_AUTH_BASIC, - Horde_Http::AUTH_DIGEST => HTTP_AUTH_DIGEST, - Horde_Http::AUTH_GSSNEGOTIATE => HTTP_AUTH_GSSNEG, - Horde_Http::AUTH_NTLM => HTTP_AUTH_NTLM, - ); + use Psr7ToPeclHttp; + use PeclHttpToPsr7; + /** + * Map of HTTP authentication schemes from Horde_Http constants to + * implementation specific constants. + * + * @var array + */ + protected $httpAuthSchemes = [ + Constants::AUTH_ANY => \http\Client\Curl\AUTH_ANY, + Constants::AUTH_BASIC => \http\Client\Curl\AUTH_BASIC, + Constants::AUTH_DIGEST => \http\Client\Curl\AUTH_DIGEST, + Constants::AUTH_GSSNEGOTIATE => \http\Client\Curl\AUTH_GSSNEG, + Constants::AUTH_NTLM => \http\Client\Curl\AUTH_NTLM, + ]; + + /** + * Map of proxy types from Horde_Http to implementation specific constants. + * + * @var array + */ + protected $proxyTypes = [ + Constants::PROXY_SOCKS4 => \http\Client\Curl\PROXY_SOCKS4, + Constants::PROXY_SOCKS5 => \http\Client\Curl\PROXY_SOCKS5 + ]; + protected StreamFactoryInterface $streamFactory; + protected ResponseFactoryInterface $responseFactory; + protected Options $options; + + /** + * Translates a Horde_Http::AUTH_* constant to implementation specific + * constants. + * + * @param string $httpAuthScheme A Horde_Http::AUTH_* constant. + * + * @return const An implementation specific authentication scheme constant. + * @throws ClientException + */ + protected function httpAuthScheme($httpAuthScheme) + { + if (!isset($this->httpAuthSchemes[$httpAuthScheme])) { + throw new ClientException('Unsupported authentication scheme (' . $httpAuthScheme . ')'); + } + return $this->httpAuthSchemes[$httpAuthScheme]; + } + + /** + * Translates a Horde_Http::PROXY_* constant to implementation specific + * constants. + * + * @return const + * @throws ClientException + */ + protected function proxyType() + { + $proxyType = $this->proxyType; + if (!isset($this->proxyTypes[$proxyType])) { + throw new ClientException('Unsupported proxy type (' . $proxyType . ')'); + } + return $this->proxyTypes[$proxyType]; + } + + /** + * Generates the HTTP options for the request. + * + * @return array array with options + * @throws Horde_Http_Exception + */ + protected function httpOptions() + { + // Set options + $httpOptions = [ + 'headers' => $this->headers, + 'redirect' => (int)$this->options->redirects, + 'ssl' => [ + 'verifypeer' => $this->options->verifyPeer, + 'verifyhost' => $this->options->verifyPeer + ], + 'timeout' => $this->options->timeout, + 'useragent' => $this->options->userAgent + ]; + + // Proxy settings + if ($this->options->proxyServer) { + $httpOptions['proxyhost'] = $this->options->proxyServer; + if ($this->options->proxyPort) { + $httpOptions['proxyport'] = $this->options->proxyPort; + } + if ($this->options->proxyUsername && $this->options->proxyPassword) { + $httpOptions['proxyauth'] = $this->options->proxyUsername . ':' . $this->options->proxyPassword; + $httpOptions['proxyauthtype'] = $this->httpAuthScheme($this->options->proxyAuthenticationScheme); + } + if ($this->proxyType == Constants::PROXY_SOCKS4 || $this->proxyType == Constants::PROXY_SOCKS5) { + $httpOptions['proxytype'] = $this->proxyType(); + } else if ($this->options->proxyType != Constants::PROXY_HTTP) { + throw new ClientException(sprintf('Proxy type %s not supported by this request type!', $this->options->proxyType)); + } + } + + // Authentication settings + if ($this->options->username) { + $httpOptions['httpauth'] = $this->options->username . ':' . $this->options->password; + $httpOptions['httpauthtype'] = $this->httpAuthScheme($this->options->authenticationScheme); + } + + return $httpOptions; + } public function __construct(ResponseFactoryInterface $responseFactory, StreamFactoryInterface $streamFactory, Options $options) { - if (!class_exists('HttpRequest', false)) { + if (!class_exists('\http\Client', false)) { throw new ClientException('The pecl_http extension is not installed. See http://php.net/http.install'); } $this->responseFactory = $responseFactory; @@ -55,5 +159,30 @@ public function __construct(ResponseFactoryInterface $responseFactory, StreamFac // Configure curl from options } + /** + * Send this HTTP request + * + * @throws ClientException + * @return Horde_Http_Response_Base + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + // First transform a PSR-7 request to a pecl/Http request + $extHttpRequest = $this->convertPsr7RequestToPeclHttp($request); + + // at this time only the curl driver is supported + $client = new \http\Client('curl'); + $client->setOptions($this->httpOptions()); + $client->enqueue($extHttpRequest); + try { + $client->send(); + $httpResponse = $client->getResponse($extHttpRequest); + } catch (\http\Exception $e) { + throw new ClientException($e); + } + // Convert the pecl/Http response into a psr-7 response + $psr7Response = $this->convertPeclHttpResponseToPsr7($httpResponse); + return $psr7Response; + } } \ No newline at end of file diff --git a/src/Request/Psr7ToPeclHttp.php b/src/Request/Psr7ToPeclHttp.php new file mode 100644 index 0000000..5b69174 --- /dev/null +++ b/src/Request/Psr7ToPeclHttp.php @@ -0,0 +1,42 @@ +addForm($data); + } else { + $body->append($data); + }*/ + // TODO: getResource and write reasonable buffer sizes to limit memory footprint + $extHttpReqBody->append((string) $request->getBody()); + // The extHttp headers format is incompatible with PSR getHeaders format. + $extHttpReqHeaders = []; + foreach ($request->getHeaders() as $name => $values) { + $extHttpReqHeaders[$name] = $request->getHeaderLine($name); + } + $extHttpRequest = new \http\Client\Request( + $request->getMethod(), + (string) $request->getUri(), + $extHttpReqHeaders, + $extHttpReqBody + ); + return $extHttpRequest; + } +} \ No newline at end of file diff --git a/src/Response/PeclHttpToPsr7.php b/src/Response/PeclHttpToPsr7.php new file mode 100644 index 0000000..b4a8599 --- /dev/null +++ b/src/Response/PeclHttpToPsr7.php @@ -0,0 +1,47 @@ +getTransferInfo(); + } catch (\http\Exception $e) { + throw new ClientException($e); + } + try { + $uri = $info->effective_url; + } catch (\http\Exception\RuntimeException $e) { + // TODO + } + $httpVersion = $httpResponse->getHttpVersion(); + $responseCode = $info->response_code; + $headers = $httpResponse->getHeaders(); + $bodyResource = $httpResponse->getBody()->getResource(); // We can use body->getResource + $psr7Stream = $this->streamFactory->createStreamFromResource($bodyResource); + $psr7Response = $this->responseFactory->createResponse($responseCode); + $psr7Response = $psr7Response->withProtocolVersion($httpVersion)->withBody($psr7Stream); + foreach ($headers as $name => $value) { + $psr7Response = $psr7Response->withHeader($name, $value); + } + return $psr7Response; + } +} \ No newline at end of file diff --git a/src/StreamUtils.php b/src/StreamUtils.php new file mode 100644 index 0000000..db7e611 --- /dev/null +++ b/src/StreamUtils.php @@ -0,0 +1,26 @@ +eof()) { + fwrite($resource, $stream->read($buffer)); + } + return $resource; + } +} \ No newline at end of file