diff --git a/bin/aws-s3 b/bin/aws-s3 new file mode 120000 --- /dev/null +++ b/bin/aws-s3 @@ -0,0 +1 @@ +../scripts/utils/aws-s3.php \ No newline at end of file diff --git a/scripts/utils/aws-s3.php b/scripts/utils/aws-s3.php new file mode 100755 --- /dev/null +++ b/scripts/utils/aws-s3.php @@ -0,0 +1,22 @@ +#!/usr/bin/env php +setTagline(pht('AWS CLI Client for S3')); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilClassMapQuery()) + ->setAncestorClass('PhutilAWSS3ManagementWorkflow') + ->execute(); + +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); 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 @@ -85,7 +85,10 @@ 'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php', 'PhutilAWSException' => 'future/aws/PhutilAWSException.php', 'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php', + 'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php', 'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php', + 'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php', + 'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php', 'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php', 'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php', 'PhutilAggregateException' => 'error/PhutilAggregateException.php', @@ -603,7 +606,10 @@ 'PhutilAWSEC2Future' => 'PhutilAWSFuture', 'PhutilAWSException' => 'Exception', 'PhutilAWSFuture' => 'FutureProxy', + 'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhutilAWSS3Future' => 'PhutilAWSFuture', + 'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow', + 'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow', 'PhutilAWSv4Signature' => 'Phobject', 'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase', 'PhutilAggregateException' => 'Exception', 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 @@ -3,11 +3,13 @@ abstract class PhutilAWSFuture extends FutureProxy { private $future; - private $awsAccessKey; - private $awsPrivateKey; - private $awsRegion; - private $builtRequest; - private $params; + private $accessKey; + private $secretKey; + private $region; + private $httpMethod = 'GET'; + private $path = '/'; + private $params = array(); + private $endpoint; abstract public function getServiceName(); @@ -15,73 +17,101 @@ parent::__construct(null); } - public function setAWSKeys($access, $private) { - $this->awsAccessKey = $access; - $this->awsPrivateKey = $private; + public function setAccessKey($access_key) { + $this->accessKey = $access_key; return $this; } - public function getAWSAccessKey() { - return $this->awsAccessKey; + public function getAccessKey() { + return $this->accessKey; } - public function getAWSPrivateKey() { - return $this->awsPrivateKey; + public function setSecretKey(PhutilOpaqueEnvelope $secret_key) { + $this->secretKey = $secret_key; + return $this; + } + + public function getSecretKey() { + return $this->secretKey; } - public function getAWSRegion() { - return $this->awsRegion; + public function getRegion() { + return $this->region; } - public function setAWSRegion($region) { - $this->awsRegion = $region; + public function setRegion($region) { + $this->region = $region; return $this; } - public function getHost() { - $host = $this->getServiceName().'.'.$this->awsRegion.'.amazonaws.com'; - return $host; + public function setEndpoint($endpoint) { + $this->endpoint = $endpoint; + return $this; } - public function setRawAWSQuery($action, array $params = array()) { - $this->params = $params; - $this->params['Action'] = $action; + public function getEndpoint() { + return $this->endpoint; + } + + public function setHTTPMethod($method) { + $this->httpMethod = $method; return $this; } - protected function getProxiedFuture() { - if (!$this->future) { - $params = $this->params; + public function getHTTPMethod() { + return $this->httpMethod; + } - if (!$this->params) { - throw new Exception( - pht( - 'You must %s!', - 'setRawAWSQuery()')); - } + public function setPath($path) { + $this->path = $path; + return $this; + } - if (!$this->getAWSAccessKey()) { - throw new Exception( - pht( - 'You must %s!', - 'setAWSKeys()')); - } + public function getPath() { + return $this->path; + } - $params['AWSAccessKeyId'] = $this->getAWSAccessKey(); - $params['Version'] = '2013-10-15'; - $params['Timestamp'] = date('c'); + protected function getParameters() { + $params = $this->params; + return $params; + } - $params = $this->sign($params); + protected function getProxiedFuture() { + if (!$this->future) { + $params = $this->getParameters(); + $method = $this->getHTTPMethod(); + $host = $this->getEndpoint(); + $path = $this->getPath(); - $uri = new PhutilURI('http://'.$this->getHost().'/'); - $uri->setQueryParams($params); + $uri = id(new PhutilURI("https://{$host}/")) + ->setPath($path) + ->setQueryParams($params); - $this->future = new HTTPFuture($uri); + $future = id(new HTTPSFuture($uri)) + ->setMethod($method); + + $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) + ->signRequest($future); + } + protected function didReceiveResult($result) { list($status, $body, $headers) = $result; @@ -101,7 +131,8 @@ ); if ($xml) { $params['RequestID'] = $xml->RequestID[0]; - foreach ($xml->Errors[0] as $error) { + $errors = array($xml->Error); + foreach ($errors as $error) { $params['Errors'][] = array($error->Code, $error->Message); } } @@ -112,36 +143,4 @@ return $xml; } - /** - * http://bit.ly/wU0JFh - */ - private function sign(array $params) { - - $params['SignatureMethod'] = 'HmacSHA256'; - $params['SignatureVersion'] = '2'; - - ksort($params); - - $pstr = array(); - foreach ($params as $key => $value) { - $pstr[] = rawurlencode($key).'='.rawurlencode($value); - } - $pstr = implode('&', $pstr); - - $sign = "GET"."\n". - strtolower($this->getHost())."\n". - "/"."\n". - $pstr; - - $hash = hash_hmac( - 'sha256', - $sign, - $this->getAWSPrivateKey(), - $raw_ouput = true); - - $params['Signature'] = base64_encode($hash); - - return $params; - } - } 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,42 @@ final class PhutilAWSS3Future extends PhutilAWSFuture { + private $bucket; + public function getServiceName() { return 's3'; } + public function setBucket($bucket) { + $this->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; + } + + protected function didReceiveResult($result) { + list($status, $body, $headers) = $result; + + if (!$status->isError()) { + return $body; + } + + if ($status->getStatusCode() === 404) { + return null; + } + + return parent::didReceiveResult($result); + } + } diff --git a/src/future/aws/PhutilAWSv4Signature.php b/src/future/aws/PhutilAWSv4Signature.php --- a/src/future/aws/PhutilAWSv4Signature.php +++ b/src/future/aws/PhutilAWSv4Signature.php @@ -28,7 +28,7 @@ public function getDate() { if ($this->date === null) { - $this->date = date('c'); + $this->date = gmdate('Ymd\THis\Z', time()); } return $this->date; } diff --git a/src/future/aws/management/PhutilAWSManagementWorkflow.php b/src/future/aws/management/PhutilAWSManagementWorkflow.php new file mode 100644 --- /dev/null +++ b/src/future/aws/management/PhutilAWSManagementWorkflow.php @@ -0,0 +1,69 @@ +getArgv(); + + $access_key = $argv->getArg('access-key'); + $secret_key = $argv->getArg('secret-key'); + + $has_root = (strlen($access_key) || strlen($secret_key)); + if ($has_root) { + if (!strlen($access_key) || !strlen($secret_key)) { + throw new PhutilArgumentUsageException( + pht( + 'When specifying AWS credentials with --access-key and '. + '--secret-key, you must provide both keys.')); + } + + $template->setAccessKey($access_key); + $template->setSecretKey(new PhutilOpaqueEnvelope($secret_key)); + } + + $has_any = ($has_root); + if (!$has_any) { + throw new PhutilArgumentUsageException( + pht( + 'You must specify AWS credentials. Use --access-key and '. + '--secret-key to provide root credentials.')); + } + + $region = $argv->getArg('region'); + if (!strlen($region)) { + throw new PhutilArgumentUsageException( + pht( + 'You must specify an AWS region with --region.')); + } + + $template->setRegion($region); + + return $template; + } + + protected function getAWSArguments() { + return array( + array( + 'name' => 'access-key', + 'param' => 'key', + 'help' => pht('AWS access key.'), + ), + array( + 'name' => 'secret-key', + 'param' => 'file', + 'help' => pht('AWS secret key.'), + ), + array( + 'name' => 'region', + 'param' => 'region', + 'help' => pht('AWS region.'), + ), + ); + } + +} diff --git a/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php new file mode 100644 --- /dev/null +++ b/src/future/aws/management/PhutilAWSS3GetManagementWorkflow.php @@ -0,0 +1,57 @@ +setName('get') + ->setExamples( + '**get** --key __key__') + ->setSynopsis(pht('Download content from S3.')) + ->setArguments( + array_merge( + $this->getAWSArguments(), + $this->getAWSS3BucketArguments(), + array( + array( + 'name' => 'key', + 'param' => 'key', + 'help' => pht('Specify a key to retrieve.'), + ), + ))); + } + + public function execute(PhutilArgumentParser $args) { + $bucket = $args->getArg('bucket'); + if (!strlen($bucket)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify an AWS S3 bucket to access with --bucket.')); + } + + $endpoint = $args->getArg('endpoint'); + if (!strlen($endpoint)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify an AWS S3 endpoint with --endpoint.')); + } + + $key = $args->getArg('key'); + if (!strlen($key)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify an AWS S3 object key to access with --key.')); + } + + $future = $this->newAWSFuture(new PhutilAWSS3Future()) + ->setBucket($bucket) + ->setEndpoint($endpoint) + ->setParametersForGetObject($key); + + echo $future->resolve(); + + return 0; + } + +} diff --git a/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php b/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php new file mode 100644 --- /dev/null +++ b/src/future/aws/management/PhutilAWSS3ManagementWorkflow.php @@ -0,0 +1,21 @@ + 'bucket', + 'param' => 'bucket', + 'help' => pht('Name of the S3 bucket to access.'), + ), + array( + 'name' => 'endpoint', + 'param' => 'endpoint', + 'help' => pht('Name of the AWS region to access.'), + ), + ); + } + +} diff --git a/src/parser/argument/PhutilArgumentParser.php b/src/parser/argument/PhutilArgumentParser.php --- a/src/parser/argument/PhutilArgumentParser.php +++ b/src/parser/argument/PhutilArgumentParser.php @@ -402,7 +402,9 @@ $this->parse($workflow->getArguments()); } + if ($workflow->isExecutable()) { + $workflow->setArgv($this); $err = $workflow->execute($this); exit($err); } else { diff --git a/src/parser/argument/workflow/PhutilArgumentWorkflow.php b/src/parser/argument/workflow/PhutilArgumentWorkflow.php --- a/src/parser/argument/workflow/PhutilArgumentWorkflow.php +++ b/src/parser/argument/workflow/PhutilArgumentWorkflow.php @@ -80,6 +80,7 @@ private $specs = array(); private $examples; private $help; + private $argv; final public function __construct() { $this->didConstruct(); @@ -154,6 +155,15 @@ return $this->specs; } + final public function setArgv(PhutilArgumentParser $argv) { + $this->argv = $argv; + return $this; + } + + final public function getArgv() { + return $this->argv; + } + protected function didConstruct() { return null; }