'us-standard', 'version' => 'latest', 'endpoint' => 'http://test.domain.com', 'bucket_endpoint' => true ]); $this->assertSame( 'http://test.domain.com/key', $c->getObjectUrl('test', 'key') ); } public function bucketNameProvider() { return [ ['.bucket', false], ['bucket.', false], ['192.168.1.1', false], ['1.1.1.100', false], ['test@42!@$5_', false], ['ab', false], ['12', false], ['bucket_name', false], ['bucket-name', true], ['bucket', true], ['my.bucket.com', true], ['test-fooCaps', false], ['w-w', true], ['w------', false], ['', false], [null,false], [false,false], [true,false], [1,false], [[],false], [new \stdClass(),false] ]; } /** * @dataProvider bucketNameProvider */ public function testValidatesDnsBucketNames($bucket, $valid) { $this->assertEquals($valid, S3Client::isBucketDnsCompatible($bucket)); } public function testCreatesPresignedRequests() { /** @var S3Client $client */ $client = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => ['key' => 'foo', 'secret' => 'bar'] ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertStringStartsWith('https://foo.s3.amazonaws.com/bar?', $url); $this->assertStringContainsString('X-Amz-Expires=', $url); $this->assertStringContainsString('X-Amz-Credential=', $url); $this->assertStringContainsString('X-Amz-Signature=', $url); } public function testCreatesPresignedRequestsWithAccessPointArn() { /** @var S3Client $client */ $client = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => ['key' => 'foo', 'secret' => 'bar'] ]); $command = $client->getCommand( 'GetObject', [ 'Bucket' => 'arn:aws:s3:us-east-1:123456789012:accesspoint:myendpoint', 'Key' => 'bar' ] ); $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertStringStartsWith('https://myendpoint-123456789012.s3-accesspoint.us-east-1.amazonaws.com/bar?', $url); $this->assertStringContainsString('X-Amz-Expires=', $url); $this->assertStringContainsString('X-Amz-Credential=', $url); $this->assertStringContainsString('X-Amz-Signature=', $url); } public function testCreatesPresignedRequestsWithStartTime() { /** @var S3Client $client */ $client = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => ['key' => 'foo', 'secret' => 'bar'] ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $url = (string) $client->createPresignedRequest( $command, '+20 minutes', ['start_time' => 1562349366] )->getUri(); $this->assertStringStartsWith('https://foo.s3.amazonaws.com/bar?', $url); $this->assertStringContainsString('X-Amz-Expires=1200', $url); $this->assertStringContainsString('X-Amz-Credential=', $url); $this->assertStringContainsString('X-Amz-Signature=61a9940ecdd901be8e36833f6d47123c0c719fc6aa82042144a6c5cf44a25988', $url); } public function testCreatesPresignedRequestsWithPathStyleFallback() { /** @var S3Client $client */ $client = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => ['key' => 'foo.baz', 'secret' => 'bar'] ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo.baz', 'Key' => 'bar']); $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertStringStartsWith('https://s3.amazonaws.com/foo.baz/bar?', $url); $this->assertStringContainsString('X-Amz-Expires=', $url); $this->assertStringContainsString('X-Amz-Credential=', $url); $this->assertStringContainsString('X-Amz-Signature=', $url); } public function testCreatesPresignedRequestsWithPathStyle() { /** @var S3Client $client */ $client = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'use_path_style_endpoint' => true ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertStringStartsWith('https://s3.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 testCreatingPresignedUrlDoesNotPermanentlyRemoveSigner() { $sent = false; $client = new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'http_handler' => function (RequestInterface $request) use (&$sent) { $sent = true; foreach (['X-Amz-Date', 'Authorization'] as $signatureHeader) { $this->assertTrue($request->hasHeader($signatureHeader)); } return Promise\Create::promiseFor(new Response); }, ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $client->createPresignedRequest($command, 1342138769)->getUri(); $client->execute($command); $this->assertTrue($sent); } public function testCreatesPresignedUrlsWithSpecialCharactersWithPathStyleFallback() { $client = new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'] ]); $command = $client->getCommand('GetObject', [ 'Bucket' => 'foobar.test.abc', 'Key' => '+%.a' ]); $url = $client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertSame('/foobar.test.abc/%2B%25.a', $url->getPath()); $query = Psr7\Query::parse($url->getQuery()); $this->assertArrayHasKey('X-Amz-Credential', $query); $this->assertArrayHasKey('X-Amz-Signature', $query); } public function testCreatesPresignedUrlsWithSpecialCharactersWithPathStyle() { $client = new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 'credentials' => ['key' => 'foo', 'secret' => 'bar'], 'use_path_style_endpoint' => true, ]); $command = $client->getCommand('GetObject', [ 'Bucket' => 'foobar.test.abc', 'Key' => '+%.a' ]); $url = $client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertSame('/foobar.test.abc/%2B%25.a', $url->getPath()); $query = Psr7\Query::parse($url->getQuery()); $this->assertArrayHasKey('X-Amz-Credential', $query); $this->assertArrayHasKey('X-Amz-Signature', $query); } public function testCreatesPresignedRequestsForObjectLambdaService() { /** @var S3Client $client */ $client = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => ['key' => 'foo', 'secret' => 'bar'] ]); $command = $client->getCommand( 'GetObject', [ 'Bucket' => 'arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/lambda-access-point', 'Key' => 'bar' ] ); $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri(); $this->assertStringStartsWith('https://lambda-access-point-123456789012.s3-object-lambda.us-east-1.amazonaws.com/bar?', $url); $this->assertStringContainsString('X-Amz-Expires=', $url); $this->assertStringContainsString('X-Amz-Credential=', $url); $this->assertStringContainsString('X-Amz-Signature=', $url); } public function testRegistersStreamWrapper() { $s3 = $this->getTestClient('S3', ['region' => 'us-east-1']); $s3->registerStreamWrapper(); $this->assertContains('s3', stream_get_wrappers()); stream_wrapper_unregister('s3'); } public function doesExistProvider() { $redirectException = new PermanentRedirectException( '', new Command('mockCommand'), ['response' => new Response(301)] ); $deleteMarkerMock = $this->getS3ErrorMock('Foo', 404, true); return [ ['foo', null, true, []], ['foo', 'bar', true, []], ['foo', null, true, $this->getS3ErrorMock('AccessDenied', 403)], ['foo', 'bar', true, $this->getS3ErrorMock('AccessDenied', 403)], ['foo', null, false, $this->getS3ErrorMock('Foo', 401)], ['foo', 'bar', false, $this->getS3ErrorMock('Foo', 401)], ['foo', null, -1, $this->getS3ErrorMock('Foo', 500)], ['foo', 'bar', -1, $this->getS3ErrorMock('Foo', 500)], ['foo', null, true, [], true], ['foo', 'bar', true, [] , true], ['foo', null, false, $this->getS3ErrorMock('Foo', 404), true], ['foo', 'bar', false, $this->getS3ErrorMock('Foo', 404), true], ['foo', null, -1, $this->getS3ErrorMock('Forbidden', 403), true], ['foo', 'bar', -1, $this->getS3ErrorMock('Forbidden', 403), true], ['foo', null, true, $this->getS3ErrorMock('Forbidden', 403), true, true], ['foo', 'bar', true, $deleteMarkerMock, true, false, true], ['foo', 'bar', false, $deleteMarkerMock, true, false, false], ['foo', null, true, $redirectException, true], ]; } private function getS3ErrorMock( $errCode, $statusCode, $deleteMarker = false ) { $response = new Response($statusCode); $deleteMarker && $response = $response->withHeader( 'x-amz-delete-marker', 'true' ); $context = [ 'code' => $errCode, 'response' => $response, ]; return new S3Exception('', new Command('mockCommand'), $context); } /** * @dataProvider doesExistProvider */ public function testsIfExists( $bucket, $key, $exists, $result, $V2 = false, $accept403 = false, $acceptDeleteMarkers = false ) { /** @var S3Client $s3 */ $s3 = $this->getTestClient('S3', ['region' => 'us-east-1']); $this->addMockResults($s3, [$result]); try { if ($V2) { if ($key) { $this->assertSame($exists, $s3->doesObjectExistV2($bucket, $key, $acceptDeleteMarkers)); } else { $this->assertSame($exists, $s3->doesBucketExistV2($bucket, $accept403)); } } else { if ($key) { $this->assertSame($exists, $s3->doesObjectExist($bucket, $key)); } else { $this->assertSame($exists, $s3->doesBucketExist($bucket)); } } } catch (\Exception $e) { $this->assertSame(-1, $exists); } } public function testReturnsObjectUrl() { $s3 = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => false ]); $this->assertSame( 'https://foo.s3.amazonaws.com/bar', $s3->getObjectUrl('foo', 'bar') ); } public function testReturnsObjectUrlWithPathStyleFallback() { $s3 = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => false, ]); $this->assertSame( 'https://s3.amazonaws.com/foo.baz/bar', $s3->getObjectUrl('foo.baz', 'bar') ); } public function testReturnsObjectUrlWithPathStyle() { $s3 = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => false, 'use_path_style_endpoint' => true ]); $this->assertSame( 'https://s3.amazonaws.com/foo/bar', $s3->getObjectUrl('foo', 'bar') ); } public function testReturnsObjectUrlViaPath() { /** @var S3Client $s3 */ $s3 = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => false ]); $this->assertSame( 'https://foo.s3.amazonaws.com/bar', $s3->getObjectUrl('foo', 'bar') ); } public function testReturnsObjectUrlViaPathWithPathStyleFallback() { /** @var S3Client $s3 */ $s3 = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => false ]); $this->assertSame( 'https://s3.amazonaws.com/foo.baz/bar', $s3->getObjectUrl('foo.baz', 'bar') ); } public function testReturnsObjectUrlViaPathWithPathStyle() { /** @var S3Client $s3 */ $s3 = $this->getTestClient('S3', [ 'region' => 'us-east-1', 'credentials' => false, 'use_path_style_endpoint' => true ]); $this->assertSame( 'https://s3.amazonaws.com/foo.baz/bar', $s3->getObjectUrl('foo.baz', 'bar') ); } public function testEnsuresMandatoryInputVariables() { $this->expectExceptionMessage("The DeleteObject operation requires non-empty parameter: Bucket"); $this->expectException(\InvalidArgumentException::class); /** @var S3Client $client */ $client = $this->getTestClient('S3'); $client->deleteObject([ 'Bucket' => "", 'Key' => "key"] ); } public function testEnsuresPrefixOrRegexSuppliedForDeleteMatchingObjects() { $this->expectException(\RuntimeException::class); /** @var S3Client $client */ $client = $this->getTestClient('S3'); $client->deleteMatchingObjects('foo'); } public function testDeletesMatchingObjectsByPrefixAndRegex() { /** @var S3Client $client */ $client = $this->getTestClient('S3'); $client->getHandlerList()->setHandler(function ($c, $r) { $this->assertSame('bucket', $c['Bucket']); return Promise\Create::promiseFor(new Result([ 'IsTruncated' => false, 'Marker' => '', 'Contents' => [ ['Key' => 'foo/bar'], ['Key' => 'foo/bar/baz'], ['Key' => 'foo/test'], ['Key' => 'foo/bar/bam'], ['Key' => 'foo/bar/001'], ['Key' => 'foo/other'] ] ])); }); $agg = []; $client->deleteMatchingObjects('bucket', 'foo/bar/', '/^foo\/bar\/[a-z]+$/', [ 'before' => function ($cmd) use (&$agg) { foreach ($cmd['Delete']['Objects'] as $k) { $agg[] = $k['Key']; } } ]); $this->assertEquals(['foo/bar/baz', 'foo/bar/bam'], $agg); } public function testProxiesToTransferObjectPut() { $this->expectExceptionMessage("Mock queue is empty. Trying to send a PutObject"); $this->expectException(\RuntimeException::class); $client = $this->getTestClient('S3'); $client->uploadDirectory(__DIR__, 'test'); } public function testProxiesToTransferObjectGet() { $this->expectExceptionMessage("Mock queue is empty. Trying to send a ListObjects"); $this->expectException(\RuntimeException::class); $client = $this->getTestClient('S3'); $client->downloadBucket(__DIR__, 'test'); } public function testProxiesToObjectUpload() { $this->expectExceptionMessage("Mock queue is empty. Trying to send a PutObject"); $this->expectException(\RuntimeException::class); $client = $this->getTestClient('S3'); $client->upload('bucket', 'key', 'body'); } public function testProxiesToObjectCopy() { $this->expectExceptionMessage("Mock queue is empty. Trying to send a HeadObject"); $this->expectException(\RuntimeException::class); $client = $this->getTestClient('S3'); $client->copy('from-bucket', 'fromKey', 'to-bucket', 'toKey'); } /** * @dataProvider getTestCasesForLocationConstraints */ public function testAddsLocationConstraintAutomatically($region, $target, $command, $contains) { $client = $this->getTestClient('S3', ['region' => $region]); $params = ['Bucket' => 'foo']; if ($region !== $target) { $params['CreateBucketConfiguration'] = ['LocationConstraint' => $target]; } $command = $client->getCommand($command, $params); $text = "{$target}"; $body = (string) \Aws\serialize($command)->getBody(); if ($contains) { $this->assertStringContainsString($text, $body); } else { $this->assertStringNotContainsString($text, $body); } } public function getTestCasesForLocationConstraints() { return [ ['us-west-2', 'us-west-2', 'CreateBucket', true], ['us-east-1', 'us-east-1', 'CreateBucket', false], ['us-west-2', 'us-west-2', 'HeadBucket', false], ['us-west-2', 'eu-west-1', 'CreateBucket', true], ['us-west-2', 'us-east-1', 'CreateBucket', false], ]; } public function testSaveAsParamAddsSink() { $client = $this->getTestClient('S3', [ 'http_handler' => function ($request, array $options) { $this->assertArrayHasKey('sink', $options); return Promise\Create::promiseFor( new Psr7\Response(200, [], 'sink=' . $options['sink']) ); } ]); $result = $client->getObject([ 'Bucket' => 'foo', 'Key' => 'bar', 'SaveAs' => 'baz', ]); $this->assertSame('sink=baz', (string) $result['Body']); } public function testRequestSucceedsWithColon() { $key = 'aaa:bbb'; $s3 = $this->getTestClient('S3', [ 'http_handler' => function (RequestInterface $request) use ($key) { $this->assertStringContainsString( urlencode($key), (string) $request->getUri() ); return Promise\Create::promiseFor(new Psr7\Response); } ]); $s3->getObject([ 'Bucket' => 'bucket', 'Key' => $key, ]); } /** * @dataProvider clientRetrySettingsProvider * * @param array $retrySettings */ public function testRetriesConnectionErrors($retrySettings) { $retries = $retrySettings['max_attempts'] - 1; $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'retries' => $retrySettings, 'http_handler' => function () use (&$retries) { if (0 === --$retries) { return new FulfilledPromise(new Response); } return new RejectedPromise([ 'connection_error' => true, 'exception' => $this->getMockBuilder(ConnectException::class) ->disableOriginalConstructor() ->getMock(), 'response' => null, ]); }, ]); $client->headBucket([ 'Bucket' => 'bucket', ]); $this->assertSame(0, $retries); } public function clientRetrySettingsProvider() { return [ [ [ 'mode' => 'legacy', 'max_attempts' => 11, ], ], [ [ 'mode' => 'standard', 'max_attempts' => 11, ], ], [ [ 'mode' => 'adaptive', 'max_attempts' => 11, ], ], ]; } /** * @dataProvider successErrorResponseProvider * * @param Response $failingSuccess * @param string $operation * @param array $payload * @param array $retryOptions */ public function testRetries200Errors( Response $failingSuccess, $operation, array $payload, $retryOptions ) { $retries = $retryOptions['max_attempts']; $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'retries' => $retryOptions, 'http_handler' => function () use (&$retries, $failingSuccess) { if (0 === --$retries) { return new FulfilledPromise(new Response( 200, [], $this->getWellFormedXml() )); } return new FulfilledPromise($failingSuccess); }, ]); $client->{$operation}($payload); $this->assertSame(0, $retries); } public function successErrorResponseProvider() { return [ [ new Response(200, [], $this->getErrorXml()), 'copyObject', [ 'Bucket' => 'foo', 'Key' => 'bar', 'CopySource' => 'baz', ], [ 'mode' => 'legacy', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getErrorXml()), 'copyObject', [ 'Bucket' => 'foo', 'Key' => 'bar', 'CopySource' => 'baz', ], [ 'mode' => 'standard', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getErrorXml()), 'copyObject', [ 'Bucket' => 'foo', 'Key' => 'bar', 'CopySource' => 'baz', ], [ 'mode' => 'adaptive', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getErrorXml()), 'uploadPartCopy', [ 'PartNumber' => 1, 'UploadId' => PHP_INT_SIZE, 'Bucket' => 'foo', 'Key' => 'bar', 'CopySource' => 'baz', ], [ 'mode' => 'legacy', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getErrorXml()), 'uploadPartCopy', [ 'PartNumber' => 1, 'UploadId' => PHP_INT_SIZE, 'Bucket' => 'foo', 'Key' => 'bar', 'CopySource' => 'baz', ], [ 'mode' => 'standard', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getErrorXml()), 'uploadPartCopy', [ 'PartNumber' => 1, 'UploadId' => PHP_INT_SIZE, 'Bucket' => 'foo', 'Key' => 'bar', 'CopySource' => 'baz', ], [ 'mode' => 'adaptive', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getErrorXml()), 'completeMultipartUpload', [ 'UploadId' => PHP_INT_SIZE, 'Bucket' => 'foo', 'Key' => 'bar', ], [ 'mode' => 'legacy', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getErrorXml()), 'completeMultipartUpload', [ 'UploadId' => PHP_INT_SIZE, 'Bucket' => 'foo', 'Key' => 'bar', ], [ 'mode' => 'standard', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getErrorXml()), 'completeMultipartUpload', [ 'UploadId' => PHP_INT_SIZE, 'Bucket' => 'foo', 'Key' => 'bar', ], [ 'mode' => 'adaptive', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getMalformedXml()), 'listObjects', [ 'Bucket' => 'foo', ], [ 'mode' => 'legacy', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getMalformedXml()), 'listObjects', [ 'Bucket' => 'foo', ], [ 'mode' => 'standard', 'max_attempts' => 11 ], ], [ new Response(200, [], $this->getMalformedXml()), 'listObjects', [ 'Bucket' => 'foo', ], [ 'mode' => 'adaptive', 'max_attempts' => 11 ], ], ]; } private function getErrorXml() { return << InternalError We encountered an internal error. Please try again. 656c76696e6727732072657175657374 Uuag1LuByRx9e6j5Onimru9pO4ZVKnJ2Qz7/C1NPcfTWAtRPfTaOFg== EOXML; } private function getMalformedXml() { return << jmtestbucket2 1000 / false < 2015-09-03T23:51:29.000Z "af1ed9909386b6116bda14403ff5f72e" 10 EOXML; } private function getWellFormedXml() { return ''; } /** * @dataProvider clientRetrySettingsProvider * * @param array $retrySettings */ public function testClientSocketTimeoutErrorsAreNotRetriedIndefinitely($retrySettings) { $this->expectExceptionMessageMatches("/Your socket connection to the server/"); $this->expectException(\Aws\S3\Exception\S3Exception::class); $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'retries' => $retrySettings, 'http_handler' => function () { return new RejectedPromise([ 'connection_error' => false, 'exception' => $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [], $this->getSocketTimeoutResponse()), ]); }, ]); $client->putObject([ 'Bucket' => 'bucket', 'Key' => 'key', 'Body' => Psr7\Utils::streamFor('x'), ]); } 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; } /** * @dataProvider clientRetrySettingsProvider * * @param array $retrySettings */ public function testNetworkingErrorsAreRetriedOnIdempotentCommands($retrySettings) { $networkingError = $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); $retries = $retrySettings['max_attempts'] - 1; $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'retries' => $retrySettings, 'http_handler' => function () use (&$retries, $networkingError) { if (0 === --$retries) { return new FulfilledPromise(new Response); } return new RejectedPromise([ 'connection_error' => false, 'exception' => $networkingError, 'response' => null, ]); }, ]); $client->putObject([ 'Bucket' => 'bucket', 'Key' => 'key', ]); $this->assertSame(0, $retries); } /** * @dataProvider clientRetrySettingsProvider * * @param array $retrySettings */ public function testNetworkingErrorsAreNotRetriedOnNonIdempotentCommands($retrySettings) { $this->expectExceptionMessageMatches("/CompleteMultipartUpload/"); $this->expectException(\Aws\S3\Exception\S3Exception::class); $networkingError = $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); $retries = $retrySettings['max_attempts']; $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'retries' => $retrySettings, 'http_handler' => function () use (&$retries, $networkingError) { if (0 === --$retries) { return new FulfilledPromise(new Response); } return new RejectedPromise([ 'connection_error' => false, 'exception' => $networkingError, 'response' => null, ]); }, ]); $client->completeMultipartUpload([ 'Bucket' => 'bucket', 'Key' => 'key', 'UploadId' => 1, ]); $this->assertSame(0, $retries); } /** * @dataProvider clientRetrySettingsProvider * * @param array $retrySettings */ public function testErrorsWithUnparseableBodiesCanBeRetried($retrySettings) { $networkingError = $this->getMockBuilder(RequestException::class) ->disableOriginalConstructor() ->setMethods([]) ->getMock(); $retries = $retrySettings['max_attempts']; $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'retries' => $retrySettings, 'http_handler' => function () use (&$retries, $networkingError) { if (0 === --$retries) { return new FulfilledPromise(new Response); } return new RejectedPromise([ 'connection_error' => false, 'exception' => $networkingError, 'response' => new Response(200, [], openssl_random_pseudo_bytes(2048)), ]); }, ]); $client->getObject([ 'Bucket' => 'bucket', 'Key' => 'key', ]); $this->assertSame(0, $retries); } /** * @dataProvider clientRetrySettingsProvider * @param $retrySettings */ public function testRetriesFailOn400Errors($retrySettings) { $retryCount = 0; $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'retries' => $retrySettings, 'http_handler' => function () use (&$retryCount) { $retryCount++; return new RejectedPromise([ 'connection_error' => false, 'exception' => $this->getMockBuilder(S3Exception::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(404, [], null), ]); }, ]); $client->getObjectAsync([ 'Bucket' => 'bucket', 'Key' => 'key' ])->otherwise(function () {})->wait(); $this->assertSame(1, $retryCount); } public function testListObjectsAppliesUrlEncodingWhenNoneSupplied() { $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => function (RequestInterface $request) { $query = Psr7\Query::parse($request->getUri()->getQuery()); $this->assertArrayHasKey('encoding-type', $query); $this->assertSame('url', $query['encoding-type']); return new FulfilledPromise(new Response); }, ]); $client->listObjects(['Bucket' => 'bucket']); } public function testListObjectsUrlDecodesEncodedKeysWhenEncodingNotSupplied() { $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => function () { return new FulfilledPromise(new Response(200, [], $this->getUrlEncodedListObjectsResponse())); }, ]); $response = $client->listObjects(['Bucket' => 'bucket']); $this->assertSame(',', $response['Delimiter']); $this->assertSame('test/yearmonth=201601/file2', $response['Marker']); $this->assertSame('test/yearmonth=201601/file2', $response['NextMarker']); $this->assertSame('test/yearmonth=201601/', $response['Prefix']); $this->assertSame('test/yearmonth=201601/file1', $response['Contents'][0]['Key']); $this->assertSame('test/yearmonth=201601/', $response['CommonPrefixes'][0]['Prefix']); } public function testListObjectsDefaultEncodingDoesNotCreateReferences() { $listObjects = $this->getUrlEncodedListObjectsResponse(); $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => function () use ($listObjects) { return new FulfilledPromise(new Response(200, [], $listObjects)); }, ]); $response = $client->listObjects(['Bucket' => 'bucket']); $this->assertSame('test/yearmonth=201601/file1', $response['Contents'][0]['Key']); $this->assertSame('test/yearmonth=201601/', $response['CommonPrefixes'][0]['Prefix']); $listObjectsCopy = $listObjects; $listObjectsCopy = str_replace('file1', 'thisisatest', $listObjectsCopy); $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => function () use ($listObjects) { return new FulfilledPromise(new Response(200, [], $listObjects)); }, ]); $response = $client->listObjects(['Bucket' => 'bucket']); $this->assertSame('test/yearmonth=201601/file1', $response['Contents'][0]['Key']); $this->assertSame('test/yearmonth=201601/', $response['CommonPrefixes'][0]['Prefix']); } public function testListObjectsDoesNotUrlDecodeEncodedKeysWhenEncodingSupplied() { $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => function () { return new FulfilledPromise(new Response(200, [], $this->getUrlEncodedListObjectsResponse())); }, ]); $response = $client->listObjects([ 'Bucket' => 'bucket', 'EncodingType' => 'url', ]); $this->assertSame('%2C', $response['Delimiter']); $this->assertSame('test/yearmonth%3D201601/file2', $response['Marker']); $this->assertSame('test/yearmonth%3D201601/file2', $response['NextMarker']); $this->assertSame('test/yearmonth%3D201601/', $response['Prefix']); $this->assertSame('test/yearmonth%3D201601/file1', $response['Contents'][0]['Key']); $this->assertSame('test/yearmonth%3D201601/', $response['CommonPrefixes'][0]['Prefix']); } private function getUrlEncodedListObjectsResponse() { return << test/yearmonth%3D201601/ test/yearmonth%3D201601/file2 test/yearmonth%3D201601/file2 %2C url test/yearmonth%3D201601/file1 test/yearmonth%3D201601/ EOXML; } public function testHeadObjectDisablesContentDecodingByDefault() { $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => function (RequestInterface $r, array $opts = []) { $this->assertArrayHasKey('decode_content', $opts); $this->assertFalse($opts['decode_content']); return Promise\Create::promiseFor(new Response); } ]); $client->headObject(['Bucket' => 'bucket', 'Key' => 'key']); } public function testContentDecodingCanBeDisabled() { $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http' => ['decode_content' => false], 'http_handler' => function (RequestInterface $r, array $opts = []) { $this->assertArrayHasKey('decode_content', $opts); $this->assertFalse($opts['decode_content']); return Promise\Create::promiseFor(new Response); } ]); $client->getObject(['Bucket' => 'bucket', 'Key' => 'key']); } public function testContentDecodingCanBeDisabledOnCommands() { $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => function (RequestInterface $r, array $opts = []) { $this->assertArrayHasKey('decode_content', $opts); $this->assertFalse($opts['decode_content']); return Promise\Create::promiseFor(new Response); } ]); $client->getObject([ 'Bucket' => 'bucket', 'Key' => 'key', '@http' => ['decode_content' => false], ]); } public function testCanDetermineRegionOfBucket() { $client = new S3Client([ 'region' => 'us-west-2', 'version' => 'latest', 'http_handler' => function () { return new FulfilledPromise(new Response(301, [ 'X-Amz-Bucket-Region' => 'alderaan-north-1', ])); }, ]); $this->assertSame('alderaan-north-1', $client->determineBucketRegion('bucket')); $client = new S3Client([ 'region' => 'us-west-2', 'version' => 'latest', 'http_handler' => function() { return new RejectedPromise([ 'connection_error' => false, 'exception' => $this->getMockBuilder(AwsException::class) ->disableOriginalConstructor() ->getMock(), 'response' => new Response(400, [ 'X-Amz-Bucket-Region' => 'us-west-2', ]), ]); }, ]); $this->assertSame('us-west-2', $client->determineBucketRegion('bucket')); } public function testDetermineBucketRegionExposeException() { $this->expectException(\Aws\Exception\AwsException::class); $client = new S3Client([ 'region' => 'us-west-2', 'version' => 'latest', 'http_handler' => function() { return new RejectedPromise([ 'connection_error' => false, 'exception' => $this->getMockBuilder(AwsException::class) ->disableOriginalConstructor() ->getMock(), 'response' => null, ]); }, ]); $client->determineBucketRegion('bucket'); } public function testAppliesS3EndpointMiddlewareDualstackAccelerate() { // test applies s3-accelerate.dualstack for valid operations $handler = function (RequestInterface $req) { $this->assertSame( 'bucket.s3-accelerate.dualstack.amazonaws.com', $req->getUri()->getHost() ); $this->assertSame( '/key', $req->getUri()->getPath() ); return Promise\Create::promiseFor(new Response); }; $accelerateClient = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'use_accelerate_endpoint' => true, 'use_dual_stack_endpoint' => true, 'http_handler' => $handler, ]); $accelerateClient->getObject([ 'Bucket' => 'bucket', 'Key' => 'key' ]); $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => $handler, ]); $client->getObject([ 'Bucket' => 'bucket', 'Key' => 'key', '@use_accelerate_endpoint' => true, '@use_dual_stack_endpoint' => true, ]); } public function testAppliesS3EndpointMiddlewareAccelerate() { // test applies s3-accelerate for valid operations $handler = function (RequestInterface $req) { $this->assertSame( 'bucket.s3-accelerate.amazonaws.com', $req->getUri()->getHost() ); return Promise\Create::promiseFor(new Response); }; $accelerateClient = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'use_accelerate_endpoint' => true, 'http_handler' => $handler, ]); $accelerateClient->getObject([ 'Bucket' => 'bucket', 'Key' => 'key' ]); $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => $handler, ]); $client->getObject([ 'Bucket' => 'bucket', 'Key' => 'key', '@use_accelerate_endpoint' => true, ]); } public function testAppliesS3EndpointMiddlewareDualstack() { // test applies dualstack $handler = function (RequestInterface $req) { $this->assertSame( 'bucket.s3.dualstack.us-west-2.amazonaws.com', $req->getUri()->getHost() ); $this->assertSame( '/', $req->getUri()->getPath() ); return Promise\Create::promiseFor(new Response); }; $dualStackClient = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'use_dual_stack_endpoint' => true, 'http_handler' => $handler, ]); $dualStackClient->createBucket([ 'Bucket' => 'bucket', ]); $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => $handler, ]); $client->createBucket([ 'Bucket' => 'bucket', '@use_dual_stack_endpoint' => true, ]); } public function testAppliesS3EndpointMiddlewareDualstackWithPathStyle() { // test applies dualstack with path style $handler = function (RequestInterface $req) { $this->assertSame( 's3.dualstack.us-west-2.amazonaws.com', $req->getUri()->getHost() ); $this->assertSame( '/bucket/', $req->getUri()->getPath() ); return Promise\Create::promiseFor(new Response); }; $dualStackClient = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'use_dual_stack_endpoint' => true, 'use_path_style_endpoint' => true, 'http_handler' => $handler, ]); $dualStackClient->createBucket([ 'Bucket' => 'bucket', ]); $client = new S3Client([ 'version' => 'latest', 'region' => 'us-west-2', 'http_handler' => $handler, ]); $client->createBucket([ 'Bucket' => 'bucket', '@use_dual_stack_endpoint' => true, '@use_path_style_endpoint' => true, ]); } public function testAddsUseArnRegionArgument() { $this->expectExceptionMessage("Invalid configuration value provided for \"use_arn_region\""); $this->expectException(\InvalidArgumentException::class); new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 'use_arn_region' => 'trigger exception' ]); } public function testAddsUseArnRegionCacheArgument() { // Create cache object $cache = new LruArrayCache(); $cache->set('aws_s3_use_arn_region_config', new UseArnRegionConfiguration(true)); // Create client using cached use_arn_region config $client = new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 'use_arn_region' => $cache, 'handler' => function (CommandInterface $cmd, RequestInterface $req) { $this->assertSame( 'myendpoint-123456789012.s3-accesspoint.us-west-2.amazonaws.com', $req->getUri()->getHost() ); $this->assertSame( '/Bar/Baz', $req->getUri()->getPath() ); return new Result([]); }, ]); $command = $client->getCommand( 'GetObject', [ 'Bucket' => 'arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint', 'Key' => 'Bar/Baz', ] ); $client->execute($command); } public function testCopyOperationCorrectlyPopulates() { $client = new S3Client([ 'region' => 'us-west-2', 'version' => 'latest', 'handler' => function (CommandInterface $cmd, RequestInterface $req) { $this->assertSame( 'myendpoint-123456789012.s3-accesspoint.us-west-2.amazonaws.com', $req->getUri()->getHost() ); $this->assertSame( '/copied-object', $req->getUri()->getPath() ); $this->assertSame( 'arn:aws:s3:us-west-2:1234567890123:accesspoint:my-my/finks-object', $req->getHeader('x-amz-copy-source')[0] ); return new Result([]); }, ]); $command = $client->getCommand( 'CopyObject', [ 'Bucket' => 'arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint', 'Key' => 'copied-object', 'CopySource' => 'arn:aws:s3:us-west-2:1234567890123:accesspoint:my-my/finks-object' ] ); $client->execute($command); } public function testAddsS3RegionalEndpointArgument() { $this->expectExceptionMessage("Configuration parameter must either be 'legacy' or 'regional'."); $this->expectException(\InvalidArgumentException::class); new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 's3_us_east_1_regional_endpoint' => 'trigger_exception' ]); } public function testAddsS3RegionalEndpointsCacheArgument() { // Create cache object $cache = new LruArrayCache(); $cache->set('aws_s3_us_east_1_regional_endpoint_config', new Configuration('regional')); // Create client using cached endpoints config $client = new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 's3_us_east_1_regional_endpoint' => $cache ]); // Get the expected Uri from the PartitionEndpointProvider $provider = PartitionEndpointProvider::defaultProvider([ 's3_us_east_1_regional_endpoint' => 'regional' ]); $endpoint = $provider([ 'service' => 's3', 'region' => 'us-east-1', ]); $uri = new Uri($endpoint['endpoint']); $this->assertSame($uri->getHost(), $client->getEndpoint()->getHost()); } /** * Tests that S3 client configuration options lead to correct endpoints * * @dataProvider optionsToEndpointsCases * @param $options * @param $host */ public function testResolvesOptionsToProperEndpoints($options, $host) { $client = new S3Client($options); $client->getHandlerList()->appendSign( Middleware::tap(function ($cmd, $req) use ($host) { $this->assertEquals($host, $req->getUri()->getHost()); }) ); $client->listBuckets(); } public function optionsToEndpointsCases() { $handler = function ($cmd, $req) { return Promise\Create::promiseFor(new Result([])); }; $data = json_decode( file_get_contents(__DIR__ . '/../Endpoint/fixtures/s3_us_east_1_regional_endpoint.json'), true ); return [ [ [ 'region' => 'us-east-1', 'version' => 'latest', 'handler' => $handler, ], 's3.amazonaws.com' ], [ [ 'region' => 'us-east-1', 'version' => 'latest', 'handler' => $handler, 's3_us_east_1_regional_endpoint' => 'regional' ], 's3.us-east-1.amazonaws.com' ], [ [ 'region' => 'us-west-2', 'version' => 'latest', 'handler' => $handler, ], 's3.us-west-2.amazonaws.com' ], [ [ 'region' => 'us-west-2', 'version' => 'latest', 'handler' => $handler, ], 's3.us-west-2.amazonaws.com' ], [ [ 'region' => 'us-east-1', 'version' => 'latest', 'handler' => $handler, 'use_dual_stack_endpoint' => true, ], 's3.dualstack.us-east-1.amazonaws.com' ], [ [ 'region' => 'us-east-1', 'version' => 'latest', 'handler' => $handler, 'use_dual_stack_endpoint' => true, ], 's3.dualstack.us-east-1.amazonaws.com' ], ]; } public function testAppliesAmbiguousSuccessParsing() { $this->expectExceptionMessage("An error connecting to the service occurred while performing the CopyObject operation"); $this->expectException(\Aws\S3\Exception\S3Exception::class); $httpHandler = function ($request, array $options) { return Promise\Create::promiseFor( new Psr7\Response(200, [], "\n\n\n") ); }; $s3 = new S3Client([ 'version' => 'latest', 'region' => 'us-east-1', 'http_handler' => $httpHandler ]); $s3->copyObject([ 'Bucket' => 'test-dest', 'Key' => 'test-key', 'CopySource' => 'test-source/key' ]); } public function testRetriesAmbiguousSuccesses() { $counter = 0; $httpHandler = function ($request, array $options) use (&$counter) { if ($counter < 2) { $body = "\n\n\n"; } else { $body = "baz"; } $counter++; return Promise\Create::promiseFor( new Psr7\Response(200, [], $body) ); }; $s3 = new S3Client([ 'version' => 'latest', 'region' => 'us-east-1', 'http_handler' => $httpHandler ]); $s3->copyObject([ 'Bucket' => 'test-dest', 'Key' => 'test-key', 'CopySource' => 'test-source/key' ]); $this->assertSame(3, $counter); } public function multiRegionSuccessProvider() { return [ ["arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap", "us-east-1", null, null, false, "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com", "x-amz-region-set:*"], ["arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap", "us-west-2", null, null, false, "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com", "x-amz-region-set:*"], ["arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap", "aws-global", null, null, false, "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com", "x-amz-region-set:*"], ["arn:aws-cn:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap", "cn-north-1", null, null, false, "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com.cn", "x-amz-region-set:*"], ["arn:aws:s3::123456789012:accesspoint:myendpoint", "us-west-2", null, null, false, "myendpoint.accesspoint.s3-global.amazonaws.com", "x-amz-region-set:*"], ["arn:aws:s3::123456789012:accesspoint:my.bucket", "us-west-2", null, null, false, "my.bucket.accesspoint.s3-global.amazonaws.com", "x-amz-region-set:*"], ]; } /** * @dataProvider multiRegionSuccessProvider */ public function testMrapParsing( $bucketFieldInput, $clientRegion, $additionalFlags, $useArnRegion, $disableMraps, $expectedEndpoint, $expectedHeaders ) { if (!extension_loaded('awscrt')) { $this->markTestSkipped(); } $client = new S3Client([ 'region' => $clientRegion, 'use_arn_region' => $useArnRegion, 'version' => 'latest', 'disable_multiregion_access_points' => $disableMraps, 'handler' => function (CommandInterface $cmd, RequestInterface $req) use ($expectedEndpoint, $expectedHeaders) { $this->assertSame( $expectedEndpoint, $req->getUri()->getHost() ); $this->assertSame( '/Bar/Baz', $req->getUri()->getPath() ); $expectedHeaders = explode(',', $expectedHeaders); foreach ($expectedHeaders as $expectedHeader) { $header = explode(':', $expectedHeader); $this->assertSame($header[1], $req->getHeader($header[0])[0]); } return new Result([]); }, ]); $command = $client->getCommand( 'GetObject', [ 'Bucket' => $bucketFieldInput, 'Key' => 'Bar/Baz', ] ); $client->execute($command); } public function mrapExceptionTestProvider() { return [ [ "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap", "us-west-2", null, null, true, "Invalid configuration: Multi-Region Access Point ARNs are disabled." ], [ "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap", "aws-global", null, null, true, "Invalid configuration: Multi-Region Access Point ARNs are disabled." ], [ "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap", "us-west-2", "dualstack", null, false, "S3 MRAP does not support dual-stack" ], [ "arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap", "us-west-2", "accelerate", null, false, "S3 MRAP does not support S3 Accelerate" ], [ "arn:aws:s3::123456789012:accesspoint:myendpoint", "us-west-2", null, null, true, "Invalid configuration: Multi-Region Access Point ARNs are disabled." ], ]; } /** * @dataProvider mrapExceptionTestProvider */ public function testMrapExceptions( $bucketFieldInput, $clientRegion, $additionalFlags, $useArnRegion, $disableMraps, $expectedException ) { $client = new S3Client([ 'region' => $clientRegion, 'use_arn_region' => $useArnRegion, 'version' => 'latest', 'disable_multiregion_access_points' => $disableMraps, 'use_dual_stack_endpoint' => !empty($additionalFlags) && $additionalFlags == 'dualstack', 'use_accelerate_endpoint' => !empty($additionalFlags) && $additionalFlags == 'accelerate', ]); $command = $client->getCommand( 'GetObject', [ 'Bucket' => $bucketFieldInput, 'Key' => 'Bar/Baz', ] ); try { $client->execute($command); self::fail("exception should have been thrown"); } catch (\Exception $e) { self::assertTrue( $e instanceof UnresolvedEndpointException || $e instanceof S3Exception ); self::assertStringContainsString($expectedException, $e->getMessage()); } } /** * @dataProvider AccessPointFailureProvider * @param $bucketFieldInput * @param $clientRegion * @param $additionalFlags * @param $useArnRegion * @param $disableMraps * @param $expectedException */ public function testAccessPointFailures ( $bucketFieldInput, $clientRegion, $additionalFlags, $useArnRegion, $disableMraps, $expectedException ){ $client = new S3Client([ 'region' => $clientRegion, 'use_arn_region' => $useArnRegion, 'version' => 'latest', 'disable_multiregion_access_points' => $disableMraps, 'use_dual_stack_endpoint' => !empty($additionalFlags) && $additionalFlags == 'dualstack', 'use_accelerate_endpoint' => !empty($additionalFlags) && $additionalFlags == 'accelerate', ]); $command = $client->getCommand( 'GetObject', [ 'Bucket' => $bucketFieldInput, 'Key' => 'Bar/Baz', ] ); try { $client->execute($command); self::fail("exception should have been thrown"); } catch (\Exception $e) { self::assertTrue( $e instanceof UnresolvedEndpointException || $e instanceof S3Exception ); self::assertStringContainsString($expectedException, $e->getMessage()); } } public function AccessPointFailureProvider() { return [ [ "arn:aws:sqs:us-west-2:123456789012:someresource", "us-west-2", null, null, null, "Invalid ARN: Unrecognized format: arn:aws:sqs:us-west-2:123456789012:someresource (type: someresource)" ], [ "arn:aws:s3:us-west-2:123456789012:bucket_name:mybucket", "us-west-2", null, null, null, "Invalid ARN: Unrecognized format: arn:aws:s3:us-west-2:123456789012:bucket_name:mybucket (type: bucket_name)" ], [ "arn:aws:s3:us-west-2::accesspoint:myendpoint", "us-west-2", null, null, null, "Invalid ARN: The account id may only contain a-z, A-Z, 0-9 and `-`. Found: ``", ], [ "arn:aws:s3:us-west-2:123.45678.9012:accesspoint:mybucket", "us-west-2", null, null, null, "Invalid ARN: The account id may only contain a-z, A-Z, 0-9 and `-`. Found: `123.45678.9012`" ], [ "arn:aws:s3:us-west-2:123456789012:accesspoint", "us-west-2", null, null, null, "Invalid ARN: Expected a resource of the format `accesspoint:` but no name was provided" ], [ "arn:aws:s3:us-west-2:123456789012:accesspoint:*", "us-west-2", null, null, null, "Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `*`" ], [ "arn:aws:s3:us-west-2:123456789012:accesspoint:my.bucket", "us-west-2", null, null, null, "Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`" ], [ "arn:aws:s3:us-west-2:123456789012:accesspoint:mybucket:object:foo", "us-west-2", null, null, null, "Invalid ARN: The ARN may only contain a single resource component after `accesspoint`." ], ]; } public function testPresignedMrapSuccess () { if (!extension_loaded('awscrt')) { $this->markTestSkipped(); } $arn = 'arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap'; $expectedEndpoint = "mfzwi23gnjvgw.mrap.accesspoint.s3-global.amazonaws.com"; $client = new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 'disable_multiregion_access_points' => false, ]); $command = $client->getCommand( 'GetObject', [ 'Bucket' => $arn, 'Key' => 'Bar/Baz', ] ); $presigned = $client->createPresignedRequest($command, time() + 10000); self::assertSame($expectedEndpoint, $presigned->getUri()->getHost()); $url = (string) $presigned->getUri(); $this->assertStringContainsString('Amz-Region-Set=%2A', $url); $this->assertStringContainsString('X-Amz-Algorithm=AWS4-ECDSA-P256-SHA256', $url); } public function testPresignedMrapFailure () { $arn = 'arn:aws:s3::123456789012:accesspoint:mfzwi23gnjvgw.mrap'; $expectedException = "Invalid configuration: Multi-Region Access Point ARNs are disabled."; $client = new S3Client([ 'region' => 'us-east-1', 'version' => 'latest', 'disable_multiregion_access_points' => true, ]); $command = $client->getCommand( 'GetObject', [ 'Bucket' => $arn, 'Key' => 'Bar/Baz', ] ); try { $client->createPresignedRequest($command, time() + 10000); self::fail("exception should have been thrown"); } catch (\Exception $e) { self::assertTrue($e instanceof UnresolvedEndpointException); self::assertStringContainsString($expectedException, $e->getMessage()); } } public function jsonCaseProvider() { return json_decode( file_get_contents(__DIR__ . '/test_cases/uri_addressing.json'), true ); } /** * @dataProvider jsonCaseProvider * * @param array $testCase */ public function testPassesCompliance( $bucket, $configuredAddressingStyle, $expectedUri, $region, $useDualstack, $useS3Accelerate ) { $key = 'key'; $client = new S3Client([ 'region' => $region, 'version' => 'latest', 'validate' => false, 'use_dual_stack_endpoint' => $useDualstack, 'use_accelerate_endpoint' => $useS3Accelerate, 'use_path_style_endpoint' => $configuredAddressingStyle === 'path', 'handler' => function ( CommandInterface $cmd, RequestInterface $req ) use ($key, $expectedUri) { $this->assertEquals($expectedUri . '/' . $key, trim($req->getUri(), '/')); return Promise\Create::promiseFor(new Result()); }, ]); $client->getObject([ 'Bucket' => $bucket, 'Key' => $key, ]); } /** * @dataProvider objectLambdasSuccessProvider * * @param $bucketFieldInput * @param $clientRegion * @param $additionalFlags * @param $useArnRegion * @param $endpointUrl * @param $expectedEndpoint */ public function testObjectLambdaArnSuccess( $bucketFieldInput, $clientRegion, $additionalFlags, $useArnRegion, $endpointUrl, $expectedEndpoint) { //additional flags is not used yet, will be in the future if dualstack support is added $clientConfig = [ 'region' => $clientRegion, 'use_arn_region' => $useArnRegion, 'version' => 'latest', 'handler' => function (CommandInterface $cmd, RequestInterface $req) use ($expectedEndpoint) { $this->assertSame( $expectedEndpoint, $req->getUri()->getHost() ); $this->assertSame( '/Bar/Baz', $req->getUri()->getPath() ); return new Result([]); }, ]; if (!empty($endpointUrl)) { $clientConfig['endpoint'] = $endpointUrl; } if (is_array($additionalFlags) && in_array('fips', $additionalFlags)) { $clientConfig['use_fips_endpoint'] = true; } $client = new S3Client($clientConfig); $command = $client->getCommand( 'GetObject', [ 'Bucket' => $bucketFieldInput, 'Key' => 'Bar/Baz', ] ); $client->execute($command); } public function objectLambdasSuccessProvider() { return [ ["arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/mybanner", "us-east-1", "none", false, null, "mybanner-123456789012.s3-object-lambda.us-east-1.amazonaws.com"], ["arn:aws:s3-object-lambda:us-west-2:123456789012:accesspoint/mybanner", "us-west-2", "none", false, null, "mybanner-123456789012.s3-object-lambda.us-west-2.amazonaws.com"], ["arn:aws:s3-object-lambda:us-west-2:123456789012:accesspoint:mybanner", "us-west-2", "none", false, null, "mybanner-123456789012.s3-object-lambda.us-west-2.amazonaws.com"], ["arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/mybanner", "us-west-2", "none", true, null, "mybanner-123456789012.s3-object-lambda.us-east-1.amazonaws.com"], ["arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/mybanner", "s3-external-1", "none", true, null, "mybanner-123456789012.s3-object-lambda.us-east-1.amazonaws.com"], ["arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/mybanner", "aws-global", "none", true, null, "mybanner-123456789012.s3-object-lambda.us-east-1.amazonaws.com"], ["arn:aws-cn:s3-object-lambda:cn-north-1:123456789012:accesspoint/mybanner", "cn-north-1", "none", true, null, "mybanner-123456789012.s3-object-lambda.cn-north-1.amazonaws.com.cn"], ["arn:aws-cn:s3-object-lambda:cn-north-1:123456789012:accesspoint/mybanner", "cn-north-1", "none", false, null, "mybanner-123456789012.s3-object-lambda.cn-north-1.amazonaws.com.cn"], ["arn:aws-cn:s3-object-lambda:cn-northwest-1:123456789012:accesspoint/mybanner", "cn-north-1", "none", true, null, "mybanner-123456789012.s3-object-lambda.cn-northwest-1.amazonaws.com.cn"], ["arn:aws-us-gov:s3-object-lambda:us-gov-east-1:123456789012:accesspoint/mybanner", "us-gov-east-1", "none", true, null, "mybanner-123456789012.s3-object-lambda.us-gov-east-1.amazonaws.com"], ["arn:aws-us-gov:s3-object-lambda:us-gov-east-1:123456789012:accesspoint/mybanner", "fips-us-gov-east-1", "none", true, null, "mybanner-123456789012.s3-object-lambda-fips.us-gov-east-1.amazonaws.com"], ["arn:aws-us-gov:s3-object-lambda:us-gov-east-1:123456789012:accesspoint/mybanner", "fips-us-gov-east-1", "none", false, null, "mybanner-123456789012.s3-object-lambda-fips.us-gov-east-1.amazonaws.com"], ["arn:aws-us-gov:s3-object-lambda:us-gov-east-1:123456789012:accesspoint/mybanner", "us-gov-east-1", ["fips"], false, null, "mybanner-123456789012.s3-object-lambda-fips.us-gov-east-1.amazonaws.com"], ["arn:aws-us-gov:s3-object-lambda:us-gov-west-1:123456789012:accesspoint/mybanner", "fips-us-gov-east-1", "none", true, null, "mybanner-123456789012.s3-object-lambda-fips.us-gov-west-1.amazonaws.com"], ["arn:aws-us-gov:s3-object-lambda:us-gov-west-1:123456789012:accesspoint/mybanner", "us-gov-east-1", ["fips"], true, null, "mybanner-123456789012.s3-object-lambda-fips.us-gov-west-1.amazonaws.com"], ]; } /** * @dataProvider objectLambdasFailureProvider * * @param $bucketFieldInput * @param $clientRegion * @param $additionalFlags * @param $useArnRegion * @param $endpointUrl * @param $expectedException */ public function testObjectLambdaArnFailures( $bucketFieldInput, $clientRegion, $additionalFlags, $useArnRegion, $endpointUrl, $expectedException) { $clientConfig = [ 'region' => $clientRegion, 'use_arn_region' => $useArnRegion, 'version' => 'latest', 'handler' => function (CommandInterface $cmd, RequestInterface $req) use ($expectedException) { $this->assertSame( $expectedException, $req->getUri()->getHost() ); $this->assertSame( '/Bar/Baz', $req->getUri()->getPath() ); return new Result([]); }, ]; if (!empty($additionalFlags) && $additionalFlags == 'dualstack') { $clientConfig['use_dual_stack_endpoint'] = true; } if (!empty($additionalFlags) && $additionalFlags == 'accelerate') { $clientConfig['use_accelerate_endpoint'] = true; } $client = new S3Client($clientConfig); $command = $client->getCommand( 'GetObject', [ 'Bucket' => $bucketFieldInput, 'Key' => 'Bar/Baz', ] ); try { $client->execute($command); $this->fail("did not catch exception: " . $expectedException); } catch (\Exception $e) { $this->assertStringContainsString($expectedException, $e->getMessage()); } } public function objectLambdasFailureProvider() { return [ [ "arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/mybanner", "us-west-2", "none", false, null, 'Invalid configuration: region from ARN `us-east-1` does not match client region `us-west-2` and UseArnRegion is `false`' ] , [ "arn:aws:s3-object-lambda:us-west-2:123456789012:accesspoint/mybanner", "us-west-2", "dualstack", true, null, 'S3 Object Lambda does not support Dual-stack' ], [ "arn:aws-cn:s3-object-lambda:cn-north-1:123456789012:accesspoint/mybanner", "us-west-2", "none", true, null, 'Client was configured for partition `aws` but ARN (`arn:aws-cn:s3-object-lambda:cn-north-1:123456789012:accesspoint/mybanner`) has `aws-cn`' ], [ "arn:aws:s3-object-lambda:us-west-2:123456789012:accesspoint/mybanner", "us-west-2", "accelerate", null, null, 'S3 Object Lambda does not support S3 Accelerate' ], [ "arn:aws:sqs:us-west-2:123456789012:someresource", "us-west-2", "n/a", null, null, 'Invalid ARN: Unrecognized format: arn:aws:sqs:us-west-2:123456789012:someresource (type: someresource)' ], [ "arn:aws:s3-object-lambda:us-west-2:123456789012:bucket_name:mybucket", "us-west-2", "n/a", null, null, 'Invalid ARN: Object Lambda ARNs only support `accesspoint` arn types, but found: `bucket_name`' ], [ "arn:aws:s3-object-lambda::123456789012:accesspoint/mybanner", "us-west-2", "none", null, null, 'Invalid ARN: bucket ARN is missing a region' ], [ "arn:aws:s3-object-lambda:us-west-2::accesspoint/mybanner", "us-west-2", "none", null, null, 'Invalid ARN: Missing account id' ], [ "arn:aws:s3-object-lambda:us-west-2:123.45678.9012:accesspoint:mybucket", "us-west-2", "n/a", null, null, 'Invalid ARN: The account id may only contain a-z, A-Z, 0-9 and `-`. Found: `123.45678.9012`' ], [ "arn:aws:s3-object-lambda:us-west-2:123456789012:accesspoint", "us-west-2", "n/a", null, null, 'Invalid ARN: Expected a resource of the format `accesspoint:` but no name was provided' ], [ "arn:aws:s3-object-lambda:us-west-2:123456789012:accesspoint:*", "us-west-2", "n/a", null, null, 'Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `*`' ], [ "arn:aws:s3-object-lambda:us-west-2:123456789012:accesspoint:my.bucket", "us-west-2", "n/a", null, null, 'Invalid ARN: The access point name may only contain a-z, A-Z, 0-9 and `-`. Found: `my.bucket`' ], [ "arn:aws:s3-object-lambda:us-west-2:123456789012:accesspoint:mybucket:object:foo", "us-west-2", "n/a", null, null, 'Invalid ARN: The ARN may only contain a single resource component after `accesspoint`.' ], [ "arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/mybanner", "s3-external-1", "none", false, null, 'Invalid configuration: region from ARN `us-east-1` does not match client region `s3-external-1` and UseArnRegion is `false`' ], [ "arn:aws:s3-object-lambda:us-east-1:123456789012:accesspoint/mybanner", "aws-global", "none", false, null, 'Invalid configuration: region from ARN `us-east-1` does not match client region `aws-global` and UseArnRegion is `false`' ], [ "arn:aws-us-gov:s3-object-lambda:us-gov-west-1:123456789012:accesspoint/mybanner", "fips-us-gov-east-1", "none", false, null, 'Invalid configuration: region from ARN `us-gov-west-1` does not match client region `us-gov-east-1` and UseArnRegion is `false`' ], ]; } /** * @dataProvider writeGetObjectResponseProvider * * @param $clientRegion * @param $route * @param $endpointUrl * @param $expectedEndpoint */ public function testWriteGetObjectResponse( $clientRegion, $route, $endpointUrl, $expectedEndpoint ) { $clientConfig = [ 'region' => $clientRegion, 'version' => 'latest', 'handler' => function (CommandInterface $cmd, RequestInterface $req) use ($expectedEndpoint) { $this->assertSame( $expectedEndpoint, $req->getUri()->getHost() ); return new Result([]); }, ]; if (!empty($endpointUrl)) { $clientConfig['endpoint'] = $endpointUrl; } $client = new S3Client($clientConfig); $command = $client->getCommand( 'WriteGetObjectResponse', [ 'RequestRoute' => $route, 'RequestToken' => 'def' ] ); $client->execute($command); } public function writeGetObjectResponseProvider() { return [ ["us-west-2", "route", null, 'route.s3-object-lambda.us-west-2.amazonaws.com'], ["us-east-1", "route", null, 'route.s3-object-lambda.us-east-1.amazonaws.com'], ]; } public function testLeadingForwardSlashIsEncoded() { $s3 = $this->getTestClient('s3'); $this->addMockResults($s3, [[]]); $command = $s3->getCommand('listObjects', ['Bucket' => 'foo', 'Prefix' => '/']); $command->getHandlerList()->appendSign( Middleware::tap(function ($cmd, $req) { $this->assertSame('foo.s3.amazonaws.com', $req->getUri()->getHost()); $this->assertSame('/?prefix=%2F&encoding-type=url', $req->getRequestTarget()); }) ); $s3->execute($command); } public function testHandlesTrailingForwardSlashInCustomEndpoint() { $s3 = $this->getTestClient('s3', ['endpoint' => 'https://test.com/']); $this->addMockResults($s3, [[]]); $command = $s3->getCommand('listObjects', ['Bucket' => 'foo', 'Prefix' => '/']); $command->getHandlerList()->appendSign( Middleware::tap(function ($cmd, $req) { $this->assertSame('foo.test.com', $req->getUri()->getHost()); $this->assertSame('/?prefix=%2F&encoding-type=url', $req->getRequestTarget()); }) ); $s3->execute($command); } public function testAddsForwardSlashIfEmptyPathAndQuery() { $s3 = $this->getTestClient('s3'); $this->addMockResults($s3, [[]]); $command = $s3->getCommand('listObjectsV2', ['Bucket' => 'foo']); $command->getHandlerList()->appendSign( Middleware::tap(function ($cmd, $req) { $this->assertSame('/', $req->getUri()->getPath()); $this->assertSame('list-type=2', $req->getUri()->getQuery()); }) ); $s3->execute($command); } public function addMD5Provider() { return [ [ ['Bucket' => 'foo', 'Key' => 'foo', 'Body' => 'test'], 'PutObject' ], [ [ 'Bucket' => 'foo', 'Key' => 'foo', 'Body' => 'test', 'PartNumber' => 1, 'UploadId' => 'foo', ], 'UploadPart' ] ]; } /** * @dataProvider addMD5Provider */ public function testAutomaticallyComputesMD5($options, $operation) { $s3 = $this->getTestClient('s3'); $this->addMockResults($s3, [[]]); $options['AddContentMD5'] = true; $command = $s3->getCommand($operation, $options); $command->getHandlerList()->appendSign( Middleware::tap(function ($cmd, $req) { $this->assertSame( 'CY9rzUYh03PK3k6DJie09g==', $req->getHeader('Content-MD5')[0] ); }) ); $s3->execute($command); } /** * @dataProvider addMD5Provider */ public function testDoesNotComputeMD5($options, $operation) { $s3 = $this->getTestClient('s3'); $this->addMockResults($s3, [[]]); $options['AddContentMD5'] = false; $command = $s3->getCommand($operation, $options); $command->getHandlerList()->appendSign( Middleware::tap(function ($cmd, $req) { $this->assertFalse( $req->hasHeader('Content-MD5') ); }) ); $s3->execute($command); } }