diff --git a/.changes/nextrelease/feature-user-agent-v2.json b/.changes/nextrelease/feature-user-agent-v2.json new file mode 100644 index 0000000000..62675ca95f --- /dev/null +++ b/.changes/nextrelease/feature-user-agent-v2.json @@ -0,0 +1,7 @@ +[ + { + "type": "feature", + "category": "", + "description": "User agent header updated to include info on OS and language version" + } +] diff --git a/src/ClientResolver.php b/src/ClientResolver.php index 0c451e04f2..8c46611a5f 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -668,44 +668,74 @@ public static function _apply_http_handler($value, array &$args, HandlerList $li ); } - public static function _apply_user_agent($value, array &$args, HandlerList $list) + public static function _apply_user_agent($inputUserAgent, array &$args, HandlerList $list) { - if (!is_array($value)) { - $value = [$value]; - } - - $value = array_map('strval', $value); + //Add SDK version + $xAmzUserAgent = ['aws-sdk-php/' . Sdk::VERSION]; + //If on HHVM add the HHVM version if (defined('HHVM_VERSION')) { - array_unshift($value, 'HHVM/' . HHVM_VERSION); + $xAmzUserAgent []= 'HHVM/' . HHVM_VERSION; } + //Set up the updated user agent + $legacyUserAgent = $xAmzUserAgent; + + //Add OS version $disabledFunctions = explode(',', ini_get('disable_functions')); - if (!ini_get('safe_mode') - && function_exists('php_uname') + if (function_exists('php_uname') && !in_array('php_uname', $disabledFunctions, true) ) { $osName = "OS/" . php_uname('s') . '/' . php_uname('r'); if (!empty($osName)) { - array_unshift($value, $osName); + $legacyUserAgent []= $osName; } } - array_unshift($value, 'aws-sdk-php/' . Sdk::VERSION); - $args['ua_append'] = $value; + //Add the language version + $legacyUserAgent []= 'lang/php/' . phpversion(); - $list->appendBuild(static function (callable $handler) use ($value) { + //Add exec environment if present + if ($executionEnvironment = getenv('AWS_EXECUTION_ENV')) { + $legacyUserAgent []= $executionEnvironment; + } + + //Add the input to the end + if ($inputUserAgent){ + if (!is_array($inputUserAgent)) { + $inputUserAgent = [$inputUserAgent]; + } + $inputUserAgent = array_map('strval', $inputUserAgent); + $legacyUserAgent = array_merge($legacyUserAgent, $inputUserAgent); + $xAmzUserAgent = array_merge($xAmzUserAgent, $inputUserAgent); + } + + $args['ua_append'] = $legacyUserAgent; + + $list->appendBuild(static function (callable $handler) use ( + $xAmzUserAgent, + $legacyUserAgent + ) { return function ( CommandInterface $command, RequestInterface $request - ) use ($handler, $value) { - return $handler($command, $request->withHeader( - 'User-Agent', - implode(' ', array_merge( - $value, - $request->getHeader('User-Agent') - )) - )); + ) use ($handler, $legacyUserAgent, $xAmzUserAgent) { + return $handler( + $command, + $request->withHeader( + 'X-Amz-User-Agent', + implode(' ', array_merge( + $xAmzUserAgent, + $request->getHeader('X-Amz-User-Agent') + )) + )->withHeader( + 'User-Agent', + implode(' ', array_merge( + $legacyUserAgent, + $request->getHeader('User-Agent') + )) + ) + ); }; }); } diff --git a/src/S3/Crypto/S3EncryptionClient.php b/src/S3/Crypto/S3EncryptionClient.php index 187606b23d..34d63d8300 100644 --- a/src/S3/Crypto/S3EncryptionClient.php +++ b/src/S3/Crypto/S3EncryptionClient.php @@ -53,7 +53,7 @@ public function __construct( S3Client $client, $instructionFileSuffix = null ) { - $this->appendUserAgent($client, 'S3CryptoV' . self::CRYPTO_VERSION); + $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); $this->client = $client; $this->instructionFileSuffix = $instructionFileSuffix; } diff --git a/src/S3/Crypto/S3EncryptionClientV2.php b/src/S3/Crypto/S3EncryptionClientV2.php index 94ec8af1cb..e88a89855b 100644 --- a/src/S3/Crypto/S3EncryptionClientV2.php +++ b/src/S3/Crypto/S3EncryptionClientV2.php @@ -105,7 +105,7 @@ public function __construct( S3Client $client, $instructionFileSuffix = null ) { - $this->appendUserAgent($client, 'S3CryptoV' . self::CRYPTO_VERSION); + $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); $this->client = $client; $this->instructionFileSuffix = $instructionFileSuffix; $this->legacyWarningCount = 0; diff --git a/src/S3/Crypto/S3EncryptionMultipartUploader.php b/src/S3/Crypto/S3EncryptionMultipartUploader.php index 9359f5e0b8..0653276cbd 100644 --- a/src/S3/Crypto/S3EncryptionMultipartUploader.php +++ b/src/S3/Crypto/S3EncryptionMultipartUploader.php @@ -107,7 +107,7 @@ public function __construct( $source, array $config = [] ) { - $this->appendUserAgent($client, 'S3CryptoV' . self::CRYPTO_VERSION); + $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); $this->client = $client; $config['params'] = []; if (!empty($config['bucket'])) { diff --git a/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php b/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php index 7e71863b8d..0273313f8c 100644 --- a/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php +++ b/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php @@ -114,7 +114,7 @@ public function __construct( $source, array $config = [] ) { - $this->appendUserAgent($client, 'S3CryptoV' . self::CRYPTO_VERSION); + $this->appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); $this->client = $client; $config['params'] = []; if (!empty($config['bucket'])) { diff --git a/src/Signature/SignatureV4.php b/src/Signature/SignatureV4.php index dd0f4f398e..c521ae444b 100644 --- a/src/Signature/SignatureV4.php +++ b/src/Signature/SignatureV4.php @@ -55,6 +55,7 @@ private function getHeaderBlacklist() 'from' => true, 'referer' => true, 'user-agent' => true, + 'X-Amz-User-Agent' => true, 'x-amzn-trace-id' => true, 'aws-sdk-invocation-id' => true, 'aws-sdk-retry' => true, @@ -364,6 +365,9 @@ private function convertExpires($expiresTimestamp, $startTimestamp) private function moveHeadersToQuery(array $parsedRequest) { + //x-amz-user-agent shouldn't be put in a query param + unset($parsedRequest['headers']['X-Amz-User-Agent']); + foreach ($parsedRequest['headers'] as $name => $header) { $lname = strtolower($name); if (substr($lname, 0, 5) == 'x-amz') { diff --git a/tests/ClientResolverTest.php b/tests/ClientResolverTest.php index 053afb7e05..1e83f9aff1 100644 --- a/tests/ClientResolverTest.php +++ b/tests/ClientResolverTest.php @@ -715,19 +715,33 @@ public function testUserAgentAlwaysStartsWithSdkAgentString() ->disableOriginalConstructor() ->getMock(); - $request->expects($this->once()) + $request->expects($this->at(0)) + ->method('getHeader') + ->with('X-Amz-User-Agent') + ->willReturn(["MockBuilder"]); + + $request->expects($this->at(1)) + ->method('withHeader') + ->with( + 'X-Amz-User-Agent', + new \PHPUnit\Framework\Constraint\RegularExpression( + '/aws-sdk-php\/' . Sdk::VERSION . '.* MockBuilder/' + ) + )->willReturn($request); + + $request->expects($this->at(2)) ->method('getHeader') ->with('User-Agent') ->willReturn(['MockBuilder']); - $request->expects($this->once()) + $request->expects($this->at(3)) ->method('withHeader') ->with( 'User-Agent', new \PHPUnit\Framework\Constraint\RegularExpression( '/aws-sdk-php\/' . Sdk::VERSION . '.* MockBuilder/' ) - ); + )->willReturn($request); $args = []; $list = new HandlerList(function () {}); @@ -735,6 +749,7 @@ public function testUserAgentAlwaysStartsWithSdkAgentString() call_user_func($list->resolve(), $command, $request); } + /** * @dataProvider statValueProvider * @param bool|array $userValue diff --git a/tests/S3/Crypto/S3EncryptionClientTest.php b/tests/S3/Crypto/S3EncryptionClientTest.php index b175d457e8..fc052720d9 100644 --- a/tests/S3/Crypto/S3EncryptionClientTest.php +++ b/tests/S3/Crypto/S3EncryptionClientTest.php @@ -782,7 +782,7 @@ public function testAddsCryptoUserAgent() 'version' => 'latest', 'http_handler' => function (RequestInterface $req) use ($provider) { $this->assertContains( - 'S3CryptoV' . S3EncryptionClient::CRYPTO_VERSION, + 'feat/s3-encrypt/' . S3EncryptionClient::CRYPTO_VERSION, $req->getHeaderLine('User-Agent') ); return Promise\promise_for(new Response( diff --git a/tests/S3/Crypto/S3EncryptionClientV2Test.php b/tests/S3/Crypto/S3EncryptionClientV2Test.php index 5af3a22d8d..3536c434a3 100644 --- a/tests/S3/Crypto/S3EncryptionClientV2Test.php +++ b/tests/S3/Crypto/S3EncryptionClientV2Test.php @@ -1059,7 +1059,7 @@ public function testAddsCryptoUserAgent() 'version' => 'latest', 'http_handler' => function (RequestInterface $req) use ($provider) { $this->assertContains( - 'S3CryptoV' . S3EncryptionClientV2::CRYPTO_VERSION, + 'feat/s3-encrypt/' . S3EncryptionClientV2::CRYPTO_VERSION, $req->getHeaderLine('User-Agent') ); return Promise\promise_for(new Response( diff --git a/tests/S3/Crypto/S3EncryptionMultipartUploaderTest.php b/tests/S3/Crypto/S3EncryptionMultipartUploaderTest.php index 675768b65c..6b84ea1f2e 100644 --- a/tests/S3/Crypto/S3EncryptionMultipartUploaderTest.php +++ b/tests/S3/Crypto/S3EncryptionMultipartUploaderTest.php @@ -452,7 +452,7 @@ public function testAddsCryptoUserAgent() $list = $s3->getHandlerList(); $list->appendSign(Middleware::tap(function($cmd, $req) { $this->assertContains( - 'S3CryptoV' . S3EncryptionMultipartUploader::CRYPTO_VERSION, + 'feat/s3-encrypt/' . S3EncryptionMultipartUploader::CRYPTO_VERSION, $req->getHeaderLine('User-Agent') ); })); diff --git a/tests/S3/Crypto/S3EncryptionMultipartUploaderV2Test.php b/tests/S3/Crypto/S3EncryptionMultipartUploaderV2Test.php index 0cc04ddfeb..0cc61b8428 100644 --- a/tests/S3/Crypto/S3EncryptionMultipartUploaderV2Test.php +++ b/tests/S3/Crypto/S3EncryptionMultipartUploaderV2Test.php @@ -503,7 +503,7 @@ public function testAddsCryptoUserAgent() $list = $s3->getHandlerList(); $list->appendSign(Middleware::tap(function($cmd, $req) { $this->assertContains( - 'S3CryptoV' . S3EncryptionMultipartUploaderV2::CRYPTO_VERSION, + 'feat/s3-encrypt/' . S3EncryptionMultipartUploaderV2::CRYPTO_VERSION, $req->getHeaderLine('User-Agent') ); })); diff --git a/tests/Signature/SignatureV4Test.php b/tests/Signature/SignatureV4Test.php index abb43231e7..baf72e782f 100644 --- a/tests/Signature/SignatureV4Test.php +++ b/tests/Signature/SignatureV4Test.php @@ -304,6 +304,7 @@ public function testPresignBlacklistedHeaders() ]); $presigned = $sig->presign($req, $creds, '+5 minutes'); $this->assertNotContains('user-agent', (string)$presigned->getUri()); + $this->assertNotContains('X-Amz-User-Agent', (string)$presigned->getUri()); $this->assertNotContains('content-length', (string)$presigned->getUri()); $this->assertNotContains('Content-Type', (string)$presigned->getUri()); }