'latest', 'region' => 'eu-east-1', 'http_handler' => function (RequestInterface $request) use (&$triedDefaultRegion) { if (strpos($request->getUri()->getHost(), 'eu-east-1')) { $triedDefaultRegion = true; return new Response(301, ['X-Amz-Bucket-Region' => 'us-west-2']); } elseif (strpos($request->getUri()->getHost(), 'us-west-2')) { return new Response(200, [], 'Success!'); } $this->fail('Failed to determine the correct region.'); }, ]); $object = $instance->getObject(['Bucket' => 'bucket', 'Key' => 'key']); $this->assertSame('Success!', (string) $object['Body']); $this->assertTrue($triedDefaultRegion); } private function getAuthHeaderMalformedXml() { return << AuthorizationHeaderMalformed The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'us-west-2' us-west-2 656c76696e6727732072657175657374 Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg== EOXML; } private function getAuthHeaderMalformedXmlWithoutRegion() { return << AuthorizationHeaderMalformed The authorization header is malformed; 656c76696e6727732072657175657374 Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg== EOXML; } private function getSocketTimeoutResponse() { return << RequestTimeout Your socket connection to the server was not read from or written to within the timeout period. Idle connections will be closed. REQUEST_ID HOST_ID EOXML; } public function testCreatesPresignedRequestsForCorrectRegion() { $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [], $this->getAuthHeaderMalformedXml()), ]); } return Promise\Create::promiseFor(new Response); }, ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $url = (string)$client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertStringStartsWith('https://foo.s3.us-west-2.amazonaws.com/bar?', $url); $this->assertStringContainsString('X-Amz-Expires=', $url); $this->assertStringContainsString('X-Amz-Credential=', $url); $this->assertStringContainsString('X-Amz-Signature=', $url); } public function testRethrowsOnNoResponseException() { $this->expectExceptionMessageMatches("/AWS HTTP error/"); $this->expectException(\Aws\Exception\AwsException::class); $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), ]); } return Promise\Create::promiseFor(new Response); }, ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $client->createPresignedRequest($command, 1342138769)->getUri(); } public function testRethrowsOnAuthHeaderMalformedWithoutRegion() { $this->expectExceptionMessageMatches("/The authorization header is malformed/"); $this->expectException(\Aws\Exception\AwsException::class); $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [], $this->getAuthHeaderMalformedXmlWithoutRegion()), ]); } return Promise\Create::promiseFor(new Response); }, ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $client->createPresignedRequest($command, 1342138769)->getUri(); } public function testRedirectsOnNonRedirectExceptionWithHeader() { $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response( 400, ['X-Amz-Bucket-Region' => 'us-west-2'], $this->getSocketTimeoutResponse() ) ]); } return Promise\Create::promiseFor(new Response); } ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $url = (string)$client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertStringStartsWith('https://foo.s3.us-west-2.amazonaws.com/bar?', $url); $this->assertStringContainsString('X-Amz-Expires=', $url); $this->assertStringContainsString('X-Amz-Credential=', $url); $this->assertStringContainsString('X-Amz-Signature=', $url); } public function testCreatesPresignedRequestsForCorrectRegionWithPathStyle() { $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 's3.amazonaws.com') { return Promise\Create::promiseFor(new Response(301, ['X-Amz-Bucket-Region' => 'us-west-2'])); } return Promise\Create::promiseFor(new Response); }, 'use_path_style_endpoint' => true ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $url = (string)$client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertStringStartsWith('https://s3.us-west-2.amazonaws.com/foo/bar?', $url); $this->assertStringContainsString('X-Amz-Expires=', $url); $this->assertStringContainsString('X-Amz-Credential=', $url); $this->assertStringContainsString('X-Amz-Signature=', $url); } public function testCreatesObjectUrlsForCorrectRegion() { $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [], $this->getAuthHeaderMalformedXml()), ]); } return Promise\Create::promiseFor(new Response); }, ]); $url = $client->getObjectUrl('foo', 'bar'); $this->assertSame('https://foo.s3.us-west-2.amazonaws.com/bar', $url); } public function testCreatesObjectUrlsForCorrectRegionWithPathStyle() { $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 's3.amazonaws.com') { return Promise\Create::promiseFor(new Response(301, ['X-Amz-Bucket-Region' => 'us-west-2'])); } return Promise\Create::promiseFor(new Response); }, 'use_path_style_endpoint' => true ]); $url = $client->getObjectUrl('foo', 'bar'); $this->assertSame('https://s3.us-west-2.amazonaws.com/foo/bar', $url); } public function testCachesBucketLocation() { $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [], $this->getAuthHeaderMalformedXml()), ]); } return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); $this->assertSame('us-west-2', $this->getPropertyValue($client, 'cache')->get('aws:s3:foo:location')); } public function testRethrowsAwsExceptionViaMiddleware() { $this->expectExceptionMessageMatches("/Your socket connection to the server was not read from or written to within the timeout period./"); $this->expectException(\Aws\Exception\AwsException::class); $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [], $this->getSocketTimeoutResponse()), ]); } return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); } public function testRethrowsOnAuthHeaderMalformedWithoutRegionViaMiddleware() { $this->expectExceptionMessageMatches("/The authorization header is malformed/"); $this->expectException(\Aws\Exception\AwsException::class); $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [], $this->getAuthHeaderMalformedXmlWithoutRegion()), ]); } return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); } public function testCachesBucketLocationAfterLookupWithPathStyle() { $redirected = false; $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) use (&$redirected) { if ($request->getUri()->getHost() === 's3.amazonaws.com') { if (!$redirected) { $redirected = true; return Promise\Create::promiseFor(new Response(301)); } return Promise\Create::promiseFor(new Response(301, [ 'X-Amz-Bucket-Region' => 'us-west-2', ])); } return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, 'use_path_style_endpoint' => true ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); $this->assertSame('us-west-2', $this->getPropertyValue($client, 'cache')->get('aws:s3:foo:location')); } public function testCachesBucketLocationWithPathStyle() { $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 's3.amazonaws.com') { return Promise\Create::promiseFor(new Response(301, [ 'X-Amz-Bucket-Region' => 'us-west-2', ])); } return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, 'use_path_style_endpoint' => true ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); $this->assertSame('us-west-2', $this->getPropertyValue($client, 'cache')->get('aws:s3:foo:location')); } public function testReadsBucketLocationFromCache() { $cache = $this->getMockBuilder(CacheInterface::class)->getMock(); $cache->expects($this->once()) ->method('get') ->with('aws:s3:foo:location') ->willReturn('us-west-2'); $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'bucket_region_cache' => $cache, 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.amazonaws.com') { $this->fail('The us-east-1 endpoint should never have been called.'); } return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); } public function testReadsBucketLocationFromCacheWithPathStyle() { $cache = $this->getMockBuilder(CacheInterface::class)->getMock(); $cache->expects($this->once()) ->method('get') ->with('aws:s3:foo:location') ->willReturn('us-west-2'); $client = new S3MultiRegionClient([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'bucket_region_cache' => $cache, 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 's3.amazonaws.com') { $this->fail('The us-east-1 endpoint should never have been called.'); } return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, 'use_path_style_endpoint' => true ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); } public function testCorrectsErroneousEntriesInCache() { $cache = new LruArrayCache; $cache->set('aws:s3:foo:location', 'us-east-1'); $client = new S3MultiRegionClient([ 'region' => 'eu-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'bucket_region_cache' => $cache, 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 'foo.s3.us-west-2.amazonaws.com') { return Promise\Create::promiseFor(new Response(200, [], 'object!')); } return new RejectedPromise([ 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [], $this->getAuthHeaderMalformedXml()), ]); }, ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); $this->assertSame('us-west-2', $cache->get('aws:s3:foo:location')); } public function testCorrectsErroneousEntriesInCacheWithPathStyle() { $cache = new LruArrayCache; $cache->set('aws:s3:foo:location', 'us-east-1'); $client = new S3MultiRegionClient([ 'region' => 'eu-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'bucket_region_cache' => $cache, 'http_handler' => function (RequestInterface $request) { if ($request->getUri()->getHost() === 's3.us-west-2.amazonaws.com') { return Promise\Create::promiseFor(new Response(200, [], 'object!')); } return Promise\Create::promiseFor(new Response(301, [ 'X-Amz-Bucket-Region' => 'us-west-2', ])); }, 'use_path_style_endpoint' => true ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); $this->assertSame('us-west-2', $cache->get('aws:s3:foo:location')); } public function testWillDefaultToRegionInPartition() { $client = new S3MultiRegionClient([ 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'partition' => new Partition([ 'defaults' => [ 'hostname' => '{service}.{region}.{dnsSuffix}', 'protocols' => ['https'], 'signatureVersions' => ['v4'], ], 'partition' => 'aws_test', // no longer used in endpoint resolution 'dnsSuffix' => 'amazonaws.test', 'regions' => [ 'foo-region' => [ 'description' => 'A description', ], ], 'services' => [ 'service' => [ 'endpoints' => [ 'foo-region' => [], ], ], ], ]), 'http_handler' => function (RequestInterface $request) { $this->assertSame('https', $request->getUri()->getScheme()); $this->assertSame('foo.s3.foo-region.amazonaws.com', $request->getUri()->getHost()); return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); } public function testWillDefaultToRegionInPartitionWithPathStyle() { $client = new S3MultiRegionClient([ 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'partition' => new Partition([ 'defaults' => [ 'hostname' => '{service}.{region}.{dnsSuffix}', 'protocols' => ['https'], 'signatureVersions' => ['v4'], ], 'partition' => 'aws_test', // no longer used in endpoint resolution 'dnsSuffix' => 'amazonaws.test', 'regions' => [ 'foo-region' => [ 'description' => 'A description', ], ], 'services' => [ 'service' => [ 'endpoints' => [ 'foo-region' => [], ], ], ], ]), 'http_handler' => function (RequestInterface $request) { $this->assertSame('https', $request->getUri()->getScheme()); $this->assertSame('s3.foo-region.amazonaws.com', $request->getUri()->getHost()); return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, 'use_path_style_endpoint' => true, 'endpoint_v1' => true ]); $client->getObject(['Bucket' => 'foo', 'Key' => 'bar']); } /** * @dataProvider booleanProvider * * @param bool $regionalized */ public function testCallbacksAttachedToCommandHandlerListsAreInvoked($regionalized) { /** @var S3ClientInterface $client */ $client = new S3MultiRegionClient([ 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) { return Promise\Create::promiseFor(new Response(200, [], 'object!')); }, ]); $command = $client->getCommand('GetObject', [ 'Bucket' => 'bucket', 'Key' => 'key', ] + ($regionalized ? ['@region' => 'us-east-1'] : [])); $command->getHandlerList() ->appendSign(function (callable $handler) { return function (CommandInterface $c) use ($handler) { return $handler($c)->then(function (ResultInterface $result) { $result['Body'] = Psr7\Utils::streamFor(str_repeat($result['Body'], 2)); return $result; }); }; }, 'body_doubler'); $result = $client->execute($command); $this->assertSame('object!object!', (string) $result['Body']); } public function booleanProvider() { return [[true], [false]]; } }