From 1bb9745b1665f8806ca831cc9f86af29cdff8f9b Mon Sep 17 00:00:00 2001 From: Adam Charron <146114816+acharron-hl@users.noreply.github.com> Date: Fri, 13 Jun 2025 09:22:34 -0400 Subject: [PATCH 1/2] Add ProxyMiddleware and MiddlewareInterface --- src/HttpRequest.php | 39 +++++++++++-- src/HttpResponse.php | 14 ++++- src/MiddlewareInterface.php | 21 +++++++ src/ProxyMiddleware.php | 50 ++++++++++++++++ tests/HttpResponseExceptionTest.php | 2 + tests/ProxyMiddlewareTest.php | 39 +++++++++++++ tests/ReadmeTest.php | 91 ----------------------------- 7 files changed, 158 insertions(+), 98 deletions(-) create mode 100644 src/MiddlewareInterface.php create mode 100644 src/ProxyMiddleware.php create mode 100644 tests/ProxyMiddlewareTest.php delete mode 100644 tests/ReadmeTest.php diff --git a/src/HttpRequest.php b/src/HttpRequest.php index ce74cc5..1882421 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -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 */ + public UriInterface|null $proxiedToUri = null; + /** * @var array */ @@ -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. @@ -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); @@ -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; } @@ -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(), ]; } @@ -326,8 +354,7 @@ public function withMethod($method) { * @inheritDoc */ public function getUri(): UriInterface { - $uriFactory = new UriFactory(); - return $uriFactory->createUri($this->getUrl()); + return $this->uri; } /** diff --git a/src/HttpResponse.php b/src/HttpResponse.php index 17ad457..23a1e57 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -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->proxiedToUri; + $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"; } diff --git a/src/MiddlewareInterface.php b/src/MiddlewareInterface.php new file mode 100644 index 0000000..44ccb5c --- /dev/null +++ b/src/MiddlewareInterface.php @@ -0,0 +1,21 @@ +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->proxiedToUri = $originalUri; + } +} \ No newline at end of file diff --git a/tests/HttpResponseExceptionTest.php b/tests/HttpResponseExceptionTest.php index d38dbae..c2d3ca2 100644 --- a/tests/HttpResponseExceptionTest.php +++ b/tests/HttpResponseExceptionTest.php @@ -30,6 +30,7 @@ public function testJsonSerialize(): void { 'url' => 'https://somesite.com/some/path', "host" => "somesite.com", 'method' => 'POST', + 'proxiedToUrl' => null, ], "response" => [ 'statusCode' => 501, @@ -52,6 +53,7 @@ public function testHostSiteOverrideSerialize() { "url" => "https://proxy-server.com/some/path", "host" => "proxy-server.com", "method" => "GET", + 'proxiedToUrl' => null, ], $serialized); } diff --git a/tests/ProxyMiddlewareTest.php b/tests/ProxyMiddlewareTest.php new file mode 100644 index 0000000..3f47c9b --- /dev/null +++ b/tests/ProxyMiddlewareTest.php @@ -0,0 +1,39 @@ +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( + <<asException()->getMessage() + ); + } + +} \ No newline at end of file diff --git a/tests/ReadmeTest.php b/tests/ReadmeTest.php deleted file mode 100644 index c74aee6..0000000 --- a/tests/ReadmeTest.php +++ /dev/null @@ -1,91 +0,0 @@ - - * @copyright 2009-2018 Vanilla Forums Inc. - * @license MIT - */ - -namespace Garden\Http\Tests; - -require_once __DIR__ . "/Fixtures/HmacMiddleware.php"; - -use Garden\Http\HttpClient; -use Garden\Http\HttpResponseException; -use Garden\Http\Tests\Fixtures\HmacMiddleware; -use PHPUnit\Framework\TestCase; - -/** - * Test cases for the README. - */ -class ReadmeTest extends TestCase -{ - public function testBasicExample() - { - $api = new HttpClient("http://httpbin.org"); - $api->setThrowExceptions(true); - $api->setDefaultHeader("Content-Type", "application/json"); - - // Get some data from the API. - $response = $api->get("/get"); // requests off of base url - $data = $response->getBody(); // returns array of json decoded data - - $response = $api->post("https://httpbin.org/post", ["foo" => "bar"]); - // Access the response like an array. - $posted = $response["json"]; // should be ['foo' => 'bar'] - - if (!$response->isSuccessful()) { - $this->markTestSkipped(); - } - $this->assertIsArray($data); - $this->assertSame(["foo" => "bar"], $posted); - } - - /** - * Test that exceptions can be thrown. - */ - public function testExceptionsExample() - { - $this->expectException(HttpResponseException::class); - $this->expectExceptionCode(404); - $api = new HttpClient("https://httpbin.org"); - $api->setThrowExceptions(true); - - try { - $api->get("/status/404"); - } catch (\Exception $ex) { - $code = $ex->getCode(); // should be 404 - throw $ex; - } - } - - public function testBasicAuthentication() - { - $api = new HttpClient("https://httpbin.org"); - $api->setDefaultOption("auth", ["username", "password123"]); - - // This request is made with the default authentication set above. - $r1 = $api->get("/basic-auth/username/password123"); - - // This request overrides the basic authentication. - $r2 = $api->get( - "/basic-auth/username/password", - [], - [], - ["auth" => ["username", "password"]] - ); - - $this->assertEquals(200, $r1->getStatusCode()); - $this->assertEquals(200, $r2->getStatusCode()); - } - - public function testRequestMiddleware() - { - $api = new HttpClient("https://httpbin.org"); - $middleware = new HmacMiddleware("key", "password"); - $api->addMiddleware($middleware); - - $r = $api->get("/get"); - - $this->assertNotEmpty($r["headers"]["Authorization"]); - } -} From c51e8b5ac3ffce2c9d77904db2351162794bb4d8 Mon Sep 17 00:00:00 2001 From: Adam Charron <146114816+acharron-hl@users.noreply.github.com> Date: Fri, 13 Jun 2025 09:27:22 -0400 Subject: [PATCH 2/2] Cleanup --- src/HttpRequest.php | 17 ++++++++++++++++- src/HttpResponse.php | 2 +- src/MiddlewareInterface.php | 2 +- src/ProxyMiddleware.php | 4 ++-- tests/ProxyMiddlewareTest.php | 3 +-- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/HttpRequest.php b/src/HttpRequest.php index 1882421..12f8e44 100644 --- a/src/HttpRequest.php +++ b/src/HttpRequest.php @@ -49,7 +49,7 @@ class HttpRequest extends HttpMessage implements \JsonSerializable, RequestInter protected UriInterface $uri; /** @var UriInterface|null */ - public UriInterface|null $proxiedToUri = null; + protected UriInterface|null $proxiedToUri = null; /** * @var array @@ -365,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; + } } diff --git a/src/HttpResponse.php b/src/HttpResponse.php index 23a1e57..856ec55 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -502,7 +502,7 @@ public function asException(): HttpResponseException $request = $this->getRequest(); if ($request !== null) { - $proxiedToUri = $request->proxiedToUri; + $proxiedToUri = $request->getProxiedToUri(); $actualUri = $request->getUri(); $proxiedToUrl = $proxiedToUri !== null ? (string) $proxiedToUri : null; diff --git a/src/MiddlewareInterface.php b/src/MiddlewareInterface.php index 44ccb5c..bfc5729 100644 --- a/src/MiddlewareInterface.php +++ b/src/MiddlewareInterface.php @@ -18,4 +18,4 @@ interface MiddlewareInterface { * @return HttpResponse */ public function __invoke(HttpRequest $request, callable $next): HttpResponse; -} \ No newline at end of file +} diff --git a/src/ProxyMiddleware.php b/src/ProxyMiddleware.php index 25cc0ab..1a7c2c7 100644 --- a/src/ProxyMiddleware.php +++ b/src/ProxyMiddleware.php @@ -45,6 +45,6 @@ protected function alterRequest(HttpRequest $request): void $request->setUrl($requestUri); $request->setHeader("Host", $originalUri->getHost()); - $request->proxiedToUri = $originalUri; + $request->setProxiedToUri($originalUri); } -} \ No newline at end of file +} diff --git a/tests/ProxyMiddlewareTest.php b/tests/ProxyMiddlewareTest.php index 3f47c9b..b13a001 100644 --- a/tests/ProxyMiddlewareTest.php +++ b/tests/ProxyMiddlewareTest.php @@ -35,5 +35,4 @@ public function testProxiedUriIsApplied(): void { $response->asException()->getMessage() ); } - -} \ No newline at end of file +}