diff --git a/src/HttpRequest.php b/src/HttpRequest.php index ce74cc5..12f8e44 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 */ + protected 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; } /** @@ -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; + } } diff --git a/src/HttpResponse.php b/src/HttpResponse.php index 17ad457..856ec55 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->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"; } diff --git a/src/MiddlewareInterface.php b/src/MiddlewareInterface.php new file mode 100644 index 0000000..bfc5729 --- /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->setProxiedToUri($originalUri); + } +} 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..b13a001 --- /dev/null +++ b/tests/ProxyMiddlewareTest.php @@ -0,0 +1,38 @@ +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() + ); + } +} 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"]); - } -}