Page MenuHomePhabricator

D10530.diff
No OneTemporary

D10530.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -81,6 +81,7 @@
'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
+ 'PhutilAWSS3Client' => 'future/aws/PhutilAWSS3Client.php',
'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
'PhutilAggregateException' => 'error/PhutilAggregateException.php',
'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php',
diff --git a/src/future/aws/PhutilAWSFuture.php b/src/future/aws/PhutilAWSFuture.php
--- a/src/future/aws/PhutilAWSFuture.php
+++ b/src/future/aws/PhutilAWSFuture.php
@@ -5,9 +5,13 @@
private $future;
private $awsAccessKey;
private $awsPrivateKey;
+ private $awsSessionToken;
+ private $awsSessionExpiry;
private $awsRegion;
private $builtRequest;
private $params;
+ private $useIAM = false;
+ private $useSSL = false;
abstract public function getServiceName();
@@ -15,41 +19,96 @@
parent::__construct(null);
}
- public function setAWSKeys($access, $private) {
- $this->awsAccessKey = $access;
- $this->awsPrivateKey = $private;
- return $this;
- }
+ public final function getAWSAccessKey() {
+ if ($this->useIAM) {
+ $this->refreshInstanceProfileCredentials();
+ }
- public function getAWSAccessKey() {
return $this->awsAccessKey;
}
- public function getAWSPrivateKey() {
+ public final function getAWSPrivateKey() {
+ if ($this->useIAM) {
+ $this->refreshInstanceProfileCredentials();
+ }
+
return $this->awsPrivateKey;
}
- public function getAWSRegion() {
+ public final function getAWSSessionToken() {
+ return $this->awsSessionToken;
+ }
+
+ public final function getAWSSessionExpiry() {
+ return $this->awsSessionExpiry;
+ }
+
+ public final function setAWSKeys($access, $private) {
+ $this->awsAccessKey = $access;
+ $this->awsPrivateKey = $private;
+ $this->awsSessionToken = null;
+ $this->awsSessionExpiry = null;
+ return $this;
+ }
+
+ public final function getAWSRegion() {
return $this->awsRegion;
}
- public function setAWSRegion($region) {
+ public final function setAWSRegion($region) {
$this->awsRegion = $region;
return $this;
}
public function getHost() {
- $host = $this->getServiceName().'.'.$this->awsRegion.'.amazonaws.com';
- return $host;
+ $parts = array();
+
+ $parts[] = $this->getServiceName();
+
+ if ($this->awsRegion) {
+ $parts[] = $this->awsRegion;
+ }
+
+ $parts[] = 'amazonaws.com';
+
+ return implode('.', $parts);
+ }
+
+ public function getPath() {
+ return '';
}
- public function setRawAWSQuery($action, array $params = array()) {
+ public final function setRawAWSQuery($action, array $params = array()) {
$this->params = $params;
$this->params['Action'] = $action;
return $this;
}
- protected function getProxiedFuture() {
+ /**
+ * Set whether to use instance profile credentials for authentication. See
+ * http://docs.aws.amazon.com/IAM/latest/UserGuide/instance-profiles.html for
+ * more information
+ *
+ * @param bool True to use instance profile credentials, otherwise false.
+ * @return this
+ */
+ public final function setUseIAM($use_iam) {
+ $this->useIAM = $use_iam;
+
+ if ($this->useIAM) {
+ $this->awsAccessKey = null;
+ $this->awsPrivateKey = null;
+ }
+
+ return $this;
+ }
+
+ public final function setUseSSL($use_ssl) {
+ $this->useSSL = $use_ssl;
+ return $this;
+ }
+
+ protected final function getProxiedFuture() {
if (!$this->future) {
$params = $this->params;
@@ -57,8 +116,13 @@
throw new Exception('You must setRawAWSQuery()!');
}
- if (!$this->getAWSAccessKey()) {
- throw new Exception('You must setAWSKeys()!');
+ if (!$this->getAWSAccessKey() && !$this->useIAM) {
+ throw new Exception('You must setAWSKeys() or setUseIAM(true)!');
+ }
+
+ if ($this->useIAM) {
+ $this->refreshInstanceProfileCredentials();
+ $params['Expires'] = $this->awsSessionExpiry;
}
$params['AWSAccessKeyId'] = $this->getAWSAccessKey();
@@ -67,16 +131,25 @@
$params = $this->sign($params);
- $uri = new PhutilURI('http://'.$this->getHost().'/');
- $uri->setQueryParams($params);
+ $uri = id(new PhutilURI(''))
+ ->setDomain($this->getHost())
+ ->setQueryParams($params);
+
+ if ($this->useSSL) {
+ $this->future = new HTTPSFuture($uri->setProtocol('https'));
+ } else {
+ $this->future = new HTTPFuture($uri->setProtocol('http'));
+ }
- $this->future = new HTTPFuture($uri);
+ if ($this->useIAM) {
+ $this->future->addHeader('x-amz-security-token', $this->awsSessionToken);
+ }
}
return $this->future;
}
- protected function didReceiveResult($result) {
+ protected final function didReceiveResult($result) {
list($status, $body, $headers) = $result;
try {
@@ -109,11 +182,14 @@
/**
* http://bit.ly/wU0JFh
*/
- private function sign(array $params) {
-
- $params['SignatureMethod'] = 'HmacSHA256';
- $params['SignatureVersion'] = '2';
-
+ private function sign(BaseHTTPFuture $future) {
+ $uri = new PhutilURI($future->getURI());
+
+ $params = $uri
+ ->setQueryParam('Signature', null)
+ ->setQueryParam('SignatureMethod', 'HmacSHA256')
+ ->setQueryParam('SignatureVersion', '2')
+ ->getQueryParams();
ksort($params);
$pstr = array();
@@ -122,20 +198,94 @@
}
$pstr = implode('&', $pstr);
- $sign = "GET"."\n".
- strtolower($this->getHost())."\n".
- "/"."\n".
- $pstr;
+ $amz_headers = array();
+
+ foreach ($future->getHeaders() as $header) {
+ list($key, $value) = $header;
+
+ $headers[$key] = $value;
- $hash = hash_hmac(
- 'sha256',
- $sign,
- $this->getAWSPrivateKey(),
- $raw_ouput = true);
+ if (substr(strtolower($key), 0, 6) == 'x-amz-') {
+ if (idx($amz_headers, $key, null) === null) {
+ $amz_headers[$key] = array();
+ }
+ $amz_headers[$key][] = $value;
+ }
+ }
+
+ $sign = implode("\n", array(
+ $future->getMethod(),
+ implode(',', $future->getHeaders('Content-MD5')),
+ implode(',', $future->getHeaders('Content-Type')),
+ $uri->getHost(),
+ $uri->getPath,
+ $pstr,
+ ));
+
+ $hash = hash_hmac('sha256', $sign, $this->getAWSPrivateKey(), true);
$params['Signature'] = base64_encode($hash);
return $params;
}
+ /**
+ * Returns a URI which can be used to retrieve instance metadata from an AWS
+ * EC2 instance. See
+ * http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
+ * for more information.
+ *
+ * @return PhutilURI
+ */
+ protected final function getInstanceMetadataURL() {
+ return new PhutilURI('http://169.254.169.254/latest/meta-data/');
+ }
+
+ /**
+ * Refresh credentials from the instance profile.
+ *
+ * @return this
+ */
+ private function refreshInstanceProfileCredentials() {
+ if (time() > $this->awsSessionExpiry) {
+ // Instance profile credentials have expired, retrieve a new token.
+ $this->setInstanceProfileCredentials();
+ }
+
+ return $this;
+ }
+
+ /**
+ * Set credentials from the instance profile.
+ *
+ * http://bit.ly/1mLDoQ3
+ *
+ * @return this
+ */
+ private function setInstanceProfileCredentials() {
+ $url = $this->getInstanceMetadataURL()
+ ->appendPath('/iam/security-credentials/');
+
+ // Get role.
+ $future = new HTTPFuture($url);
+ list($response, $headers) = $future->resolvex();
+ $credentials = trim($response);
+
+ // Get credentials.
+ $future = new HTTPFuture($url->appendPath($credentials));
+ list($response, $headers) = $future->resolvex();
+ $response = phutil_json_decode($response);
+
+ if ($response['Code'] !== 'Success') {
+ throw new RuntimeException(
+ pht('Unexpected response code: "%s"', $response['Code']));
+ }
+
+ $this->awsAccessKey = $response['AccessKeyId'];
+ $this->awsPrivateKey = $response['SecretAccessKey'];
+ $this->awsSessionToken = $response['Token'];
+ $this->awsSessionExpiry = strtotime($response['Expiration']);
+ return $this;
+ }
+
}
diff --git a/src/future/aws/PhutilAWSS3Client.php b/src/future/aws/PhutilAWSS3Client.php
new file mode 100644
--- /dev/null
+++ b/src/future/aws/PhutilAWSS3Client.php
@@ -0,0 +1,71 @@
+<?php
+
+final class PhutilAWSS3Client {
+
+ private $future;
+
+ public function __construct() {
+ $this->future = new PhutilAWSS3Future();
+ }
+
+ public function setAWSKeys($access, $private) {
+ $this->future->setAWSKeys($access, $private);
+ return $this;
+ }
+
+ public function setUseIAM($use_iam) {
+ $this->future->setUseIAM($use_iam);
+ return $this;
+ }
+
+ private function getFuture() {
+ $future = clone $this->future;
+ return $future;
+ }
+
+ public function deleteBucket($bucket) {
+ return $this->getFuture()
+ ->setBucket($bucket)
+ ->setMethod('DELETE')
+ ->resolvex();
+ }
+
+ public function deleteObject($bucket, $key) {
+ return $this->getFuture()
+ ->setBucket($bucket)
+ ->setKey($key)
+ ->setMethod('DELETE')
+ ->resolvex();
+ }
+
+ public function getObject($bucket, $key) {
+ return $this->getFuture()
+ ->setBucket($bucket)
+ ->setKey($key)
+ ->setMethod('GET')
+ ->resolvex();
+ }
+
+ public function listBucket($bucket) {
+ return $this->getFuture()
+ ->setBucket($bucket)
+ ->getProxiedFuture()->setMethod('GET')
+ ->setRawAWSQuery('LIST', array())
+ ->resolvex();
+ }
+
+ public function putObject($data, $bucket, $key) {
+ $params = array(
+ 'data' => $data,
+ 'size' => strlen($data),
+ 'md5sum' => base64_encode(md5($data, true)),
+ );
+
+ return $this->getFuture()
+ ->setBucket($bucket)
+ ->setKey($key)
+ ->setMethod('PUT')
+ ->resolvex();
+ }
+
+}
diff --git a/src/future/aws/PhutilAWSS3Future.php b/src/future/aws/PhutilAWSS3Future.php
--- a/src/future/aws/PhutilAWSS3Future.php
+++ b/src/future/aws/PhutilAWSS3Future.php
@@ -2,8 +2,37 @@
final class PhutilAWSS3Future extends PhutilAWSFuture {
+ private $bucket;
+ private $key;
+
public function getServiceName() {
return 's3';
}
+ public function setBucket($bucket) {
+ $this->bucket = $bucket;
+ return $this;
+ }
+
+ public function getBucket() {
+ return $this->bucket;
+ }
+
+ public function setKey($key) {
+ $this->key = $key;
+ return $this;
+ }
+
+ public function getKey() {
+ return $this->key;
+ }
+
+ public final function getHost() {
+ return $this->getBucket().'.'.parent::getHost();
+ }
+
+ public function getPath() {
+ return $this->key;
+ }
+
}

File Metadata

Mime Type
text/plain
Expires
May 22 2024, 4:01 PM (4 w, 3 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6276243
Default Alt Text
D10530.diff (10 KB)

Event Timeline