diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c7e8ed4b..154d5fae40 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Updated `Query::create` PHPDoc to include supported types and propagate it to callers by @franmomu [#2088](https://github.com/ruflin/Elastica/pull/2088) * Update some iterable types in PHPDoc to be more specific by @franmomu [#2092](https://github.com/ruflin/Elastica/pull/2092) +* Sanitize host name for AWS requests before signing in AWSAuthV4 transport ### Deprecated * Deprecated `Elastica\Reindex::WAIT_FOR_COMPLETION_FALSE`, use a boolean as parameter instead by @franmomu [#2070](https://github.com/ruflin/Elastica/pull/2070) * Passing anything else than a boolean as 1st argument to `Reindex::setWaitForCompletion`, pass a boolean instead by @franmomu [#2070](https://github.com/ruflin/Elastica/pull/2070) diff --git a/src/Transport/AwsAuthV4.php b/src/Transport/AwsAuthV4.php old mode 100755 new mode 100644 index da25ed112a..640ea82e13 --- a/src/Transport/AwsAuthV4.php +++ b/src/Transport/AwsAuthV4.php @@ -10,6 +10,7 @@ use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7\Utils; use Psr\Http\Message\RequestInterface; class AwsAuthV4 extends Guzzle @@ -42,15 +43,28 @@ private function getSigningMiddleware(): callable : \getenv('AWS_REGION'); $signer = new SignatureV4('es', $region); $credProvider = $this->getCredentialProvider(); + $transport = $this; return Middleware::mapRequest(static function (RequestInterface $req) use ( $signer, - $credProvider + $credProvider, + $transport ) { - return $signer->signRequest($req, $credProvider()->wait()); + return $signer->signRequest($transport->sanitizeRequest($req), $credProvider()->wait()); }); } + private function sanitizeRequest(RequestInterface $request): RequestInterface + { + // Trailing dots are valid parts of DNS host names (see RFC 1034), + // but interferes with header signing where AWS expects a stripped host name. + if ('.' === \substr($request->getHeader('host')[0], -1)) { + $request = Utils::modifyRequest($request, ['set_headers' => ['host' => \rtrim($request->getHeader('host')[0], '.')]]); + } + + return $request; + } + private function getCredentialProvider(): callable { $connection = $this->getConnection(); diff --git a/tests/Transport/AwsAuthV4Test.php b/tests/Transport/AwsAuthV4Test.php index bd5991569b..40a058b971 100644 --- a/tests/Transport/AwsAuthV4Test.php +++ b/tests/Transport/AwsAuthV4Test.php @@ -198,4 +198,35 @@ public function testSignsWithEnvironmentalCredentials(): void ); } } + + /** + * @group unit + * @depends testSignsWithProvidedCredentials + */ + public function testStripsTrailingDotInHost(): void + { + $host = $this->_getHost(); + $hostWithTrailingDot = $host.'.'; + + $config = [ + 'persistent' => false, + 'transport' => 'AwsAuthV4', + 'aws_access_key_id' => 'foo', + 'aws_secret_access_key' => 'bar', + 'aws_session_token' => 'baz', + 'aws_region' => 'us-east-1', + 'host' => $hostWithTrailingDot, + ]; + $client = $this->_getClient($config); + + try { + $client->request('_stats'); + } catch (GuzzleException $e) { + $guzzleException = $e->getGuzzleException(); + /** @var \Psr\Http\Message\RequestInterface $request */ + $request = $guzzleException->getRequest(); + $this->assertSame($host, $request->getHeader('host')[0]); + $this->assertSame($hostWithTrailingDot, $request->getUri()->getHost()); + } + } }