Skip to content

Commit

Permalink
WIP: Create the PSR-18 PeclHttp client from original Request/Response…
Browse files Browse the repository at this point in the history
… classes.
  • Loading branch information
ralflang committed Jul 23, 2021
1 parent dcc9587 commit ae70119
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 9 deletions.
147 changes: 138 additions & 9 deletions src/Client/Peclhttp.php
Expand Up @@ -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;
Expand All @@ -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
*
Expand All @@ -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;
Expand All @@ -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;
}
}
42 changes: 42 additions & 0 deletions src/Request/Psr7ToPeclHttp.php
@@ -0,0 +1,42 @@
<?php
namespace Horde\Http\Request;
use \Psr\Http\Message\RequestInterface as Psr7Request;
use \Psr\Http\Message\StreamInterface as Psr7Stream;
/**
* Convert a PSR-7 request message to a pecl/Http native message
* Split off from the PeclHttp Client implementation
*/
trait Psr7ToPeclHttp
{
/**
* Convert to native format
*
* @param Psr7Request $request The PSR request to convert
*
* @return \http\Client\Request
*/
private function convertPsr7RequestToPeclHttp(Psr7Request $request): \http\Client\Request
{
$extHttpReqBody = new \http\Message\Body();
// Mind: Do we need to support special formEncoding? The PSR request should return the correct content anyway.
/* if (is_array($data)) {
$body->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;
}
}
47 changes: 47 additions & 0 deletions src/Response/PeclHttpToPsr7.php
@@ -0,0 +1,47 @@
<?php
namespace Horde\Http\Response;
use \Psr\Http\Message\ResponseFactoryInterface;
use \Psr\Http\Message\ResponseInterface;
use \Psr\Http\Message\StreamFactoryInterface;
use \Psr\Http\Message\StreamInterface;
use http\Client\Response;

/**
* Convert a pecl/Http native message to a PSR7 response
* Split off from the PeclHttp Client implementation
*/
trait PeclHttpToPsr7
{
/**
* Convert to PSR-7 format
*
* @param http\Client\Response $response The httpClient response
*
* @return ResponseInterface The PSR-7 equivalent
*/
private function convertPeclHttpResponseToPsr7(Response $httpResponse
): ResponseInterface
{
try {
$info = $httpResponse->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;
}
}
26 changes: 26 additions & 0 deletions src/StreamUtils.php
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Horde\Http;
use \Psr\Http\Message\StreamInterface;
use InvalidArgumentException;
/**
* Static utilities for PSR-7 Stream objects
*/
class StreamUtils
{
/**
*
*/
const MB16 = 16777216;

public static function copyStreamToResource(StreamInterface $stream, $resource, int $buffer = self::MB16)
{
if (!is_resource($resource)) {
throw new InvalidArgumentException('Second Parameter $resource must be resource');
}
while (!$stream->eof()) {
fwrite($resource, $stream->read($buffer));
}
return $resource;
}
}

0 comments on commit ae70119

Please sign in to comment.