Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 48 additions & 6 deletions src/HttpRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,16 @@ class HttpRequest extends HttpMessage implements \JsonSerializable, RequestInter

/**
* @var string The URL of the request.
* @deprecated Use $uri instead.
*/
protected $url;

/** @var UriInterface */
protected UriInterface $uri;

/** @var UriInterface|null */
protected UriInterface|null $proxiedToUri = null;

/**
* @var array
*/
Expand Down Expand Up @@ -71,7 +78,7 @@ class HttpRequest extends HttpMessage implements \JsonSerializable, RequestInter
* Initialize an instance of the {@link HttpRequest} class.
*
* @param string $method The HTTP method of the request.
* @param string $url The URL where the request will be sent.
* @param string|UriInterface $url The URL where the request will be sent.
* @param string|array $body The body of the request.
* @param array $headers An array of http headers to be sent with the request.
* @param array $options An array of extra options.
Expand All @@ -81,9 +88,13 @@ class HttpRequest extends HttpMessage implements \JsonSerializable, RequestInter
* - auth: A username/password used to send basic HTTP authentication with the request.
* - timeout: The number of seconds to wait before the request times out. A value of zero means no timeout.
*/
public function __construct(string $method = self::METHOD_GET, string $url = '', $body = '', array $headers = [], array $options = []) {
public function __construct(string $method = self::METHOD_GET, string|UriInterface $url = '', $body = '', array $headers = [], array $options = []) {
$this->setMethod(strtoupper($method));
$this->setUrl($url);
if ($url instanceof UriInterface) {
$this->setUri($url);
} else {
$this->setUrl($url);
}
$this->setBody($body);
$this->setHeaders($headers);

Expand Down Expand Up @@ -184,7 +195,23 @@ public function getUrl(): string {
* @return HttpRequest Returns `$this` for fluent calls.
*/
public function setUrl(string $url) {
$uriFactory = new UriFactory();
$uri = $uriFactory->createUri($url);
$this->url = $url;
$this->uri = $uri;
return $this;
}

/**
* Set the URI of the request.
*
* @param UriInterface $uri The new URI.
*
* @return static Returns `$this` for fluent calls.
*/
public function setUri(UriInterface $uri): static {
$this->uri = $uri;
$this->url = (string) $uri;
return $this;
}

Expand Down Expand Up @@ -271,7 +298,8 @@ public function getOptions(): array {
public function jsonSerialize(): array {
return [
"url" => $this->getUrl(),
"host" => $this->getHeader("host") ?: $this->getUri()->getHost(),
"proxiedToUrl" => $this->proxiedToUri !== null ? (string) $this->proxiedToUri : null,
"host" => $this->proxiedToUri !== null ? $this->proxiedToUri->getHost() : $this->getUri()->getHost(),
"method" => $this->getMethod(),
];
}
Expand Down Expand Up @@ -326,8 +354,7 @@ public function withMethod($method) {
* @inheritDoc
*/
public function getUri(): UriInterface {
$uriFactory = new UriFactory();
return $uriFactory->createUri($this->getUrl());
return $this->uri;
}

/**
Expand All @@ -338,4 +365,19 @@ public function withUri(UriInterface $uri, $preserveHost = false) {
$cloned->setUrl((string) $uri);
return $cloned;
}

/**
* @return UriInterface|null
*/
public function getProxiedToUri(): ?UriInterface {
return $this->proxiedToUri;
}

/**
* @param UriInterface|null $proxiedToUri
* @return void
*/
public function setProxiedToUri(?UriInterface $proxiedToUri): void {
$this->proxiedToUri = $proxiedToUri;
}
}
14 changes: 13 additions & 1 deletion src/HttpResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,20 @@ public function setRequest(HttpRequest $request = null)
public function asException(): HttpResponseException
{
$request = $this->getRequest();

if ($request !== null) {
$requestID = "Request \"{$request->getMethod()} {$request->getUrl()}\"";
$proxiedToUri = $request->getProxiedToUri();
$actualUri = $request->getUri();

$proxiedToUrl = $proxiedToUri !== null ? (string) $proxiedToUri : null;
$proxiedThroughUrl = $proxiedToUrl !== null ? (string) $actualUri : null;
$mainUrl = $proxiedToUrl ?? (string) $actualUri;

if ($proxiedThroughUrl !== null) {
$requestID = "Request \"{$request->getMethod()} {$mainUrl} (proxied through {$proxiedThroughUrl})\"";
} else {
$requestID = "Request \"{$request->getMethod()} {$mainUrl}\"";
}
} else {
$requestID = "Unknown request";
}
Expand Down
21 changes: 21 additions & 0 deletions src/MiddlewareInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
namespace Garden\Http;

/**
* An interface for middleware that processes HTTP requests and responses.
*
* Middleware can modify the request before passing it to the next handler,
* or modify the response after it has been processed by the next handler.
*/
interface MiddlewareInterface {

/**
* Call next to execute the next middleware or handler in the chain (next returns the response).
*
* @param HttpRequest $request
* @param callable(HttpRequest): HttpResponse $next
*
* @return HttpResponse
*/
public function __invoke(HttpRequest $request, callable $next): HttpResponse;
}
50 changes: 50 additions & 0 deletions src/ProxyMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
namespace Garden\Http;

use Slim\Psr7\Uri;

/**
* Middleware to proxy requests through a proxy.
*/
class ProxyMiddleware implements MiddlewareInterface {


/**
* @param string $proxyHostname The hostname of the proxy to use.
* @param bool $downgradeScheme Whether to downgrade the scheme of the request from https to http.
*/
public function __construct(protected string $proxyHostname, protected bool $downgradeScheme = true) { }

/**
* @inheritdoc
*/
public function __invoke(HttpRequest $request, callable $next): HttpResponse
{
$this->alterRequest($request);
return $next($request, $next);
}

/**
* Given a url, try to replace it's base url so it routes with the cluster router.
*
* @param string $url
*
* @return void
*/
protected function alterRequest(HttpRequest $request): void
{
/** @var Uri $uri */
$originalUri = $request->getUri();

$uri = $originalUri;
if ($this->downgradeScheme && $uri->getScheme() === "https") {
$uri = $uri->withScheme("http");
}

$requestUri = $uri->withHost($this->proxyHostname);

$request->setUrl($requestUri);
$request->setHeader("Host", $originalUri->getHost());
$request->setProxiedToUri($originalUri);
}
}
2 changes: 2 additions & 0 deletions tests/HttpResponseExceptionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public function testJsonSerialize(): void {
'url' => 'https://somesite.com/some/path',
"host" => "somesite.com",
'method' => 'POST',
'proxiedToUrl' => null,
],
"response" => [
'statusCode' => 501,
Expand All @@ -52,6 +53,7 @@ public function testHostSiteOverrideSerialize() {
"url" => "https://proxy-server.com/some/path",
"host" => "proxy-server.com",
"method" => "GET",
'proxiedToUrl' => null,
], $serialized);
}

Expand Down
38 changes: 38 additions & 0 deletions tests/ProxyMiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
namespace Garden\Http\Tests;

use Garden\Http\HttpClient;
use Garden\Http\ProxyMiddleware;
use PHPUnit\Framework\TestCase;

/**
* Tests for using the HttpClient with {@link ProxyMiddleware}
*/
class ProxyMiddlewareTest extends TestCase {

/**
* Test that the proxied URI is applied correctly.
*
* @return void
*/
public function testProxiedUriIsApplied(): void {
$proxyHostname = "0.0.0.0:8091";

$client = new HttpClient("https://example.com/base-url");
$client->addMiddleware(new ProxyMiddleware($proxyHostname, downgradeScheme: true));
$response = $client->get("/api?query#hash");

$request = $response->getRequest();
$this->assertEquals("http://$proxyHostname/base-url/api?query#hash", $request->getUrl());
$this->assertEquals("example.com", $request->getHeader("Host"));

// If we have a response exception it's message will indicate the proxied URL
$this->assertEquals(
<<<MESSAGE
Request "GET https://example.com/base-url/api?query#hash (proxied through http://0.0.0.0:8091/base-url/api?query#hash)" failed with a response code of 404 and a standard message of "Not Found"
MESSAGE
,
$response->asException()->getMessage()
);
}
}
91 changes: 0 additions & 91 deletions tests/ReadmeTest.php

This file was deleted.