diff --git a/src/future/aws/PhutilAWSEC2Future.php b/src/future/aws/PhutilAWSEC2Future.php index 2ad81d7..19a56fa 100644 --- a/src/future/aws/PhutilAWSEC2Future.php +++ b/src/future/aws/PhutilAWSEC2Future.php @@ -1,9 +1,20 @@ parameters = $parameters; + return $this; + } + + protected function getParameters() { + return $this->parameters; + } + public function getServiceName() { return 'ec2'; } } diff --git a/src/future/aws/PhutilAWSFuture.php b/src/future/aws/PhutilAWSFuture.php index 54432d5..176a8e7 100644 --- a/src/future/aws/PhutilAWSFuture.php +++ b/src/future/aws/PhutilAWSFuture.php @@ -1,168 +1,171 @@ accessKey = $access_key; return $this; } public function getAccessKey() { return $this->accessKey; } public function setSecretKey(PhutilOpaqueEnvelope $secret_key) { $this->secretKey = $secret_key; return $this; } public function getSecretKey() { return $this->secretKey; } public function getRegion() { return $this->region; } public function setRegion($region) { $this->region = $region; return $this; } public function setEndpoint($endpoint) { $this->endpoint = $endpoint; return $this; } public function getEndpoint() { return $this->endpoint; } public function setHTTPMethod($method) { $this->httpMethod = $method; return $this; } public function getHTTPMethod() { return $this->httpMethod; } public function setPath($path) { $this->path = $path; return $this; } public function getPath() { return $this->path; } public function setData($data) { $this->data = $data; return $this; } public function getData() { return $this->data; } protected function getParameters() { - $params = $this->params; - return $params; + return array(); } public function addHeader($key, $value) { $this->headers[] = array($key, $value); return $this; } protected function getProxiedFuture() { if (!$this->future) { $params = $this->getParameters(); $method = $this->getHTTPMethod(); $host = $this->getEndpoint(); $path = $this->getPath(); $data = $this->getData(); $uri = id(new PhutilURI("https://{$host}/")) ->setPath($path) ->setQueryParams($params); $future = id(new HTTPSFuture($uri, $data)) ->setMethod($method); foreach ($this->headers as $header) { list($key, $value) = $header; $future->addHeader($key, $value); } $this->signRequest($future); $this->future = $future; } return $this->future; } protected function signRequest(HTTPSFuture $future) { $access_key = $this->getAccessKey(); $secret_key = $this->getSecretKey(); $region = $this->getRegion(); id(new PhutilAWSv4Signature()) ->setRegion($region) ->setService($this->getServiceName()) ->setAccessKey($access_key) ->setSecretKey($secret_key) + ->setSignContent($this->shouldSignContent()) ->signRequest($future); } + protected function shouldSignContent() { + return false; + } + protected function didReceiveResult($result) { list($status, $body, $headers) = $result; try { $xml = @(new SimpleXMLElement($body)); } catch (Exception $ex) { $xml = null; } if ($status->isError() || !$xml) { if (!($status instanceof HTTPFutureHTTPResponseStatus)) { throw $status; } $params = array( 'body' => $body, ); if ($xml) { $params['RequestID'] = $xml->RequestID[0]; $errors = array($xml->Error); foreach ($errors as $error) { $params['Errors'][] = array($error->Code, $error->Message); } } throw new PhutilAWSException($status->getStatusCode(), $params); } return $xml; } } diff --git a/src/future/aws/PhutilAWSS3Future.php b/src/future/aws/PhutilAWSS3Future.php index 6941829..de00a50 100644 --- a/src/future/aws/PhutilAWSS3Future.php +++ b/src/future/aws/PhutilAWSS3Future.php @@ -1,66 +1,70 @@ bucket = $bucket; return $this; } public function getBucket() { return $this->bucket; } public function setParametersForGetObject($key) { $bucket = $this->getBucket(); $this->setHTTPMethod('GET'); $this->setPath($bucket.'/'.$key); return $this; } public function setParametersForPutObject($key, $value) { $bucket = $this->getBucket(); $this->setHTTPMethod('PUT'); $this->setPath($bucket.'/'.$key); $this->addHeader('X-Amz-ACL', 'private'); $this->addHeader('Content-Type', 'application/octet-stream'); $this->setData($value); return $this; } public function setParametersForDeleteObject($key) { $bucket = $this->getBucket(); $this->setHTTPMethod('DELETE'); $this->setPath($bucket.'/'.$key); return $this; } protected function didReceiveResult($result) { list($status, $body, $headers) = $result; if (!$status->isError()) { return $body; } if ($status->getStatusCode() === 404) { return null; } return parent::didReceiveResult($result); } + protected function shouldSignContent() { + return true; + } + } diff --git a/src/future/aws/PhutilAWSv4Signature.php b/src/future/aws/PhutilAWSv4Signature.php index 50248b0..343b581 100644 --- a/src/future/aws/PhutilAWSv4Signature.php +++ b/src/future/aws/PhutilAWSv4Signature.php @@ -1,256 +1,269 @@ accessKey = $access_key; return $this; } public function setSecretKey(PhutilOpaqueEnvelope $secret_key) { $this->secretKey = $secret_key; return $this; } public function setDate($date) { $this->date = $date; return $this; } public function getDate() { if ($this->date === null) { $this->date = gmdate('Ymd\THis\Z', time()); } return $this->date; } public function setRegion($region) { $this->region = $region; return $this; } public function getRegion() { return $this->region; } public function setService($service) { $this->service = $service; return $this; } public function getService() { return $this->service; } public function setSigningKey($signing_key) { $this->signingKey = $signing_key; return $this; } public function getSigningKey() { if ($this->signingKey === null) { $this->signingKey = $this->computeSigningKey(); } return $this->signingKey; } private function getAlgorithm() { return 'AWS4-HMAC-SHA256'; } + public function setSignContent($sign_content) { + $this->signContent = $sign_content; + return $this; + } + + public function getSignContent() { + return $this->signContent; + } + private function getHost(HTTPSFuture $future) { $uri = new PhutilURI($future->getURI()); return $uri->getDomain(); } private function getPath(HTTPSFuture $future) { $uri = new PhutilURI($future->getURI()); return $uri->getPath(); } public function signRequest(HTTPSFuture $future) { $body_signature = $this->getBodySignature($future); - $future->addHeader('X-Amz-Content-sha256', $body_signature); + if ($this->getSignContent()) { + $future->addHeader('X-Amz-Content-sha256', $body_signature); + } + $future->addHeader('X-Amz-Date', $this->getDate()); $request_signature = $this->getCanonicalRequestSignature( $future, $body_signature); $string_to_sign = $this->getStringToSign($request_signature); $signing_key = $this->getSigningKey(); $signature = hash_hmac('sha256', $string_to_sign, $signing_key); $algorithm = $this->getAlgorithm(); $credential = $this->getCredential(); $signed_headers = $this->getSignedHeaderList($future); $authorization = $algorithm.' '. 'Credential='.$credential.','. 'SignedHeaders='.$signed_headers.','. 'Signature='.$signature; $future->addHeader('Authorization', $authorization); return $future; } private function getBodySignature(HTTPSFuture $future) { $http_body = $future->getData(); if (is_array($http_body)) { $http_body = ''; } return hash('sha256', $http_body); } private function getCanonicalRequestSignature( HTTPSFuture $future, $body_signature) { $http_method = $future->getMethod(); $path = $this->getPath($future); $path = rawurlencode($path); $path = str_replace('%2F', '/', $path); $canonical_parameters = $this->getCanonicalParameterList($future); $canonical_headers = $this->getCanonicalHeaderList($future); $signed_headers = $this->getSignedHeaderList($future); $canonical_request = $http_method."\n". $path."\n". $canonical_parameters."\n". $canonical_headers."\n". "\n". $signed_headers."\n". $body_signature; return hash('sha256', $canonical_request); } private function getStringToSign($request_signature) { $algorithm = $this->getAlgorithm(); $date = $this->getDate(); $scope_parts = $this->getScopeParts(); $scope = implode('/', $scope_parts); $string_to_sign = $algorithm."\n". $date."\n". $scope."\n". $request_signature; return $string_to_sign; } private function getScopeParts() { return array( substr($this->getDate(), 0, 8), $this->getRegion(), $this->getService(), 'aws4_request', ); } private function computeSigningKey() { $secret_key = $this->secretKey; if (!$secret_key) { throw new Exception( pht( 'You must either provide a signing key with setSigningKey(), or '. 'provide a secret key with setSecretKey().')); } // NOTE: This part of the algorithm uses the raw binary hashes, and the // result is not human-readable. $raw_hash = true; $signing_key = 'AWS4'.$secret_key->openEnvelope(); $scope_parts = $this->getScopeParts(); foreach ($scope_parts as $scope_part) { $signing_key = hash_hmac('sha256', $scope_part, $signing_key, $raw_hash); } return $signing_key; } private function getCanonicalHeaderList(HTTPSFuture $future) { $headers = $this->getCanonicalHeaderMap($future); $canonical_headers = array(); foreach ($headers as $header => $header_value) { $canonical_headers[] = $header.':'.trim($header_value); } return implode("\n", $canonical_headers); } private function getCanonicalHeaderMap(HTTPSFuture $future) { $headers = $future->getHeaders(); $headers[] = array( 'Host', $this->getHost($future), ); $header_map = array(); foreach ($headers as $header) { list($key, $value) = $header; $key = phutil_utf8_strtolower($key); $header_map[$key] = $value; } ksort($header_map); return $header_map; } private function getSignedHeaderList(HTTPSFuture $future) { $headers = $this->getCanonicalHeaderMap($future); return implode(';', array_keys($headers)); } private function getCanonicalParameterList(HTTPSFuture $future) { $uri = new PhutilURI($future->getURI()); $params = $uri->getQueryParams(); ksort($params); $canonical_parameters = array(); foreach ($params as $key => $value) { $canonical_parameters[] = rawurlencode($key).'='.rawurlencode($value); } $canonical_parameters = implode('&', $canonical_parameters); return $canonical_parameters; } private function getCredential() { $access_key = $this->accessKey; if (!strlen($access_key)) { throw new PhutilInvalidStateException('setAccessKey'); } $parts = $this->getScopeParts(); array_unshift($parts, $access_key); return implode('/', $parts); } } diff --git a/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php b/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php index f09dbf1..b6b56b5 100644 --- a/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php +++ b/src/future/aws/__tests__/PhutilAWSv4SignatureTestCase.php @@ -1,159 +1,195 @@ setMethod($method) ->addHeader('Range', 'bytes=0-9'); $signature = id(new PhutilAWSv4Signature()) ->setAccessKey($access_key) ->setSecretKey(new PhutilOpaqueEnvelope($secret_key)) + ->setSignContent(true) ->setDate($date) ->setRegion($region) ->setService($service); $signature->signRequest($future); $expect = <<assertSignature($expect, $future); } public function testAWSv4SignaturesS3PutObject() { $access_key = 'AKIAIOSFODNN7EXAMPLE'; $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'; $date = '20130524T000000Z'; $region = 'us-east-1'; $service = 's3'; $uri = 'https://examplebucket.s3.amazonaws.com/test$file.text'; $method = 'PUT'; $body = 'Welcome to Amazon S3.'; $future = id(new HTTPSFuture($uri, $body)) ->setMethod($method) ->addHeader('X-Amz-Storage-Class', 'REDUCED_REDUNDANCY') ->addHeader('Date', 'Fri, 24 May 2013 00:00:00 GMT'); $signature = id(new PhutilAWSv4Signature()) ->setAccessKey($access_key) ->setSecretKey(new PhutilOpaqueEnvelope($secret_key)) + ->setSignContent(true) ->setDate($date) ->setRegion($region) ->setService($service); $signature->signRequest($future); $expect = <<assertSignature($expect, $future); } public function testAWSv4SignaturesS3GetBucketLifecycle() { $access_key = 'AKIAIOSFODNN7EXAMPLE'; $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'; $date = '20130524T000000Z'; $region = 'us-east-1'; $service = 's3'; $uri = 'https://examplebucket.s3.amazonaws.com/?lifecycle'; $method = 'GET'; $future = id(new HTTPSFuture($uri)) ->setMethod($method); $signature = id(new PhutilAWSv4Signature()) ->setAccessKey($access_key) ->setSecretKey(new PhutilOpaqueEnvelope($secret_key)) + ->setSignContent(true) ->setDate($date) ->setRegion($region) ->setService($service); $signature->signRequest($future); $expect = <<assertSignature($expect, $future); } public function testAWSv4SignaturesS3GetBucket() { $access_key = 'AKIAIOSFODNN7EXAMPLE'; $secret_key = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'; $date = '20130524T000000Z'; $region = 'us-east-1'; $service = 's3'; $uri = 'https://examplebucket.s3.amazonaws.com/?max-keys=2&prefix=J'; $method = 'GET'; $future = id(new HTTPSFuture($uri)) ->setMethod($method); $signature = id(new PhutilAWSv4Signature()) ->setAccessKey($access_key) ->setSecretKey(new PhutilOpaqueEnvelope($secret_key)) + ->setSignContent(true) ->setDate($date) ->setRegion($region) ->setService($service); $signature->signRequest($future); $expect = <<assertSignature($expect, $future); } + public function testAWSv4SignaturesVanillaQuery() { + $access_key = 'AKIDEXAMPLE'; + $secret_key = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'; + $date = '20150830T123600Z'; + $region = 'us-east-1'; + $service = 'service'; + $uri = 'https://example.amazonaws.com/?Param2=value2&Param1=value1'; + $method = 'GET'; + + $future = id(new HTTPSFuture($uri)) + ->setMethod($method); + + $signature = id(new PhutilAWSv4Signature()) + ->setAccessKey($access_key) + ->setSecretKey(new PhutilOpaqueEnvelope($secret_key)) + ->setSignContent(false) + ->setDate($date) + ->setRegion($region) + ->setService($service); + + $signature->signRequest($future); + + $expect = <<assertSignature($expect, $future); + } private function assertSignature($expect, HTTPSFuture $signed) { $authorization = null; foreach ($signed->getHeaders() as $header) { list($key, $value) = $header; if (phutil_utf8_strtolower($key) === 'authorization') { $authorization = $value; break; } } $expect = str_replace("\n\n", ' ', $expect); $expect = str_replace("\n", '', $expect); $this->assertEqual($expect, $authorization); } }