]> source.dussan.org Git - nextcloud-server.git/commitdiff
add option to use legacy v2 auth with s3 7758/head
authorRobin Appelman <robin@icewind.nl>
Tue, 19 Dec 2017 10:54:55 +0000 (11:54 +0100)
committerMorris Jobke <hey@morrisjobke.de>
Tue, 9 Jan 2018 17:21:47 +0000 (18:21 +0100)
Signed-off-by: Robin Appelman <robin@icewind.nl>
apps/files_external/lib/Lib/Backend/AmazonS3.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/Files/ObjectStore/S3ConnectionTrait.php
lib/private/Files/ObjectStore/S3Signature.php [new file with mode: 0644]

index c975901512199ad92980a16d661721d26ea46f3c..5625805601cb0aa60931f848ac4427e96307ba02 100644 (file)
@@ -53,6 +53,8 @@ class AmazonS3 extends Backend {
                                        ->setType(DefinitionParameter::VALUE_BOOLEAN),
                                (new DefinitionParameter('use_path_style', $l->t('Enable Path Style')))
                                        ->setType(DefinitionParameter::VALUE_BOOLEAN),
+                               (new DefinitionParameter('legacy_auth', $l->t('Legacy (v2) authentication')))
+                                       ->setType(DefinitionParameter::VALUE_BOOLEAN),
                        ])
                        ->addAuthScheme(AccessKey::SCHEME_AMAZONS3_ACCESSKEY)
                        ->setLegacyAuthMechanism($legacyAuth)
index 61df49cd5100031a9dfab0e0d38b7c29ddf7f4b7..93e792a7f4c18916fb44d4b97a6925db54e49477 100644 (file)
@@ -594,6 +594,7 @@ return array(
     'OC\\Files\\ObjectStore\\S3' => $baseDir . '/lib/private/Files/ObjectStore/S3.php',
     'OC\\Files\\ObjectStore\\S3ConnectionTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php',
     'OC\\Files\\ObjectStore\\S3ObjectTrait' => $baseDir . '/lib/private/Files/ObjectStore/S3ObjectTrait.php',
+    'OC\\Files\\ObjectStore\\S3Signature' => $baseDir . '/lib/private/Files/ObjectStore/S3Signature.php',
     'OC\\Files\\ObjectStore\\StorageObjectStore' => $baseDir . '/lib/private/Files/ObjectStore/StorageObjectStore.php',
     'OC\\Files\\ObjectStore\\Swift' => $baseDir . '/lib/private/Files/ObjectStore/Swift.php',
     'OC\\Files\\Search\\SearchBinaryOperator' => $baseDir . '/lib/private/Files/Search/SearchBinaryOperator.php',
index aa5990466fc06da058b844df4ca8d111b2f4811e..88bb9c65ffa688d0b947c7849eb2aca48c8e4100 100644 (file)
@@ -624,6 +624,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\Files\\ObjectStore\\S3' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3.php',
         'OC\\Files\\ObjectStore\\S3ConnectionTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ConnectionTrait.php',
         'OC\\Files\\ObjectStore\\S3ObjectTrait' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3ObjectTrait.php',
+        'OC\\Files\\ObjectStore\\S3Signature' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/S3Signature.php',
         'OC\\Files\\ObjectStore\\StorageObjectStore' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/StorageObjectStore.php',
         'OC\\Files\\ObjectStore\\Swift' => __DIR__ . '/../../..' . '/lib/private/Files/ObjectStore/Swift.php',
         'OC\\Files\\Search\\SearchBinaryOperator' => __DIR__ . '/../../..' . '/lib/private/Files/Search/SearchBinaryOperator.php',
index fdda19ff700fe21e994b220bec8d340511d05193..e6ae0f6cb54ee52d8a2be9d5dedc1c5c3ed9a936 100644 (file)
@@ -21,6 +21,7 @@
 
 namespace OC\Files\ObjectStore;
 
+use Aws\ClientResolver;
 use Aws\S3\Exception\S3Exception;
 use Aws\S3\S3Client;
 
@@ -83,11 +84,15 @@ trait S3ConnectionTrait {
                        ],
                        'endpoint' => $base_url,
                        'region' => $this->params['region'],
-                       'use_path_style_endpoint' => isset($this->params['use_path_style']) ? $this->params['use_path_style'] : false
+                       'use_path_style_endpoint' => isset($this->params['use_path_style']) ? $this->params['use_path_style'] : false,
+                       'signature_provider' => \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider())
                ];
                if (isset($this->params['proxy'])) {
                        $options['request.options'] = ['proxy' => $this->params['proxy']];
                }
+               if (isset($this->params['legacy_auth']) && $this->params['legacy_auth']) {
+                       $options['signature_version'] = 'v2';
+               }
                $this->connection = new S3Client($options);
 
                if (!S3Client::isBucketDnsCompatible($this->bucket)) {
@@ -117,4 +122,14 @@ trait S3ConnectionTrait {
                        sleep($this->timeout);
                }
        }
+
+       public static function legacySignatureProvider($version, $service, $region) {
+               switch ($version) {
+                       case 'v2':
+                       case 's3':
+                               return new S3Signature();
+                       default:
+                               return null;
+               }
+       }
 }
diff --git a/lib/private/Files/ObjectStore/S3Signature.php b/lib/private/Files/ObjectStore/S3Signature.php
new file mode 100644 (file)
index 0000000..d5bfbf4
--- /dev/null
@@ -0,0 +1,204 @@
+<?php
+
+namespace OC\Files\ObjectStore;
+
+use Aws\Credentials\CredentialsInterface;
+use Aws\S3\S3Client;
+use Aws\S3\S3UriParser;
+use Aws\Signature\SignatureInterface;
+use GuzzleHttp\Psr7;
+use Psr\Http\Message\RequestInterface;
+
+/**
+ * Legacy Amazon S3 signature implementation
+ */
+class S3Signature implements SignatureInterface
+{
+       /** @var array Query string values that must be signed */
+       private $signableQueryString = [
+               'acl', 'cors', 'delete', 'lifecycle', 'location', 'logging',
+               'notification', 'partNumber', 'policy', 'requestPayment',
+               'response-cache-control', 'response-content-disposition',
+               'response-content-encoding', 'response-content-language',
+               'response-content-type', 'response-expires', 'restore', 'tagging',
+               'torrent', 'uploadId', 'uploads', 'versionId', 'versioning',
+               'versions', 'website'
+       ];
+
+       /** @var array Sorted headers that must be signed */
+       private $signableHeaders = ['Content-MD5', 'Content-Type'];
+
+       /** @var \Aws\S3\S3UriParser S3 URI parser */
+       private $parser;
+
+       public function __construct()
+       {
+               $this->parser = new S3UriParser();
+               // Ensure that the signable query string parameters are sorted
+               sort($this->signableQueryString);
+       }
+
+       public function signRequest(
+               RequestInterface $request,
+               CredentialsInterface $credentials
+       ) {
+               $request = $this->prepareRequest($request, $credentials);
+               $stringToSign = $this->createCanonicalizedString($request);
+               $auth = 'AWS '
+                       . $credentials->getAccessKeyId() . ':'
+                       . $this->signString($stringToSign, $credentials);
+
+               return $request->withHeader('Authorization', $auth);
+       }
+
+       public function presign(
+               RequestInterface $request,
+               CredentialsInterface $credentials,
+               $expires
+       ) {
+               $query = [];
+               // URL encoding already occurs in the URI template expansion. Undo that
+               // and encode using the same encoding as GET object, PUT object, etc.
+               $uri = $request->getUri();
+               $path = S3Client::encodeKey(rawurldecode($uri->getPath()));
+               $request = $request->withUri($uri->withPath($path));
+
+               // Make sure to handle temporary credentials
+               if ($token = $credentials->getSecurityToken()) {
+                       $request = $request->withHeader('X-Amz-Security-Token', $token);
+                       $query['X-Amz-Security-Token'] = $token;
+               }
+
+               if ($expires instanceof \DateTime) {
+                       $expires = $expires->getTimestamp();
+               } elseif (!is_numeric($expires)) {
+                       $expires = strtotime($expires);
+               }
+
+               // Set query params required for pre-signed URLs
+               $query['AWSAccessKeyId'] = $credentials->getAccessKeyId();
+               $query['Expires'] = $expires;
+               $query['Signature'] = $this->signString(
+                       $this->createCanonicalizedString($request, $expires),
+                       $credentials
+               );
+
+               // Move X-Amz-* headers to the query string
+               foreach ($request->getHeaders() as $name => $header) {
+                       $name = strtolower($name);
+                       if (strpos($name, 'x-amz-') === 0) {
+                               $query[$name] = implode(',', $header);
+                       }
+               }
+
+               $queryString = http_build_query($query, null, '&', PHP_QUERY_RFC3986);
+
+               return $request->withUri($request->getUri()->withQuery($queryString));
+       }
+
+       /**
+        * @param RequestInterface     $request
+        * @param CredentialsInterface $creds
+        *
+        * @return RequestInterface
+        */
+       private function prepareRequest(
+               RequestInterface $request,
+               CredentialsInterface $creds
+       ) {
+               $modify = [
+                       'remove_headers' => ['X-Amz-Date'],
+                       'set_headers'    => ['Date' => gmdate(\DateTime::RFC2822)]
+               ];
+
+               // Add the security token header if one is being used by the credentials
+               if ($token = $creds->getSecurityToken()) {
+                       $modify['set_headers']['X-Amz-Security-Token'] = $token;
+               }
+
+               return Psr7\modify_request($request, $modify);
+       }
+
+       private function signString($string, CredentialsInterface $credentials)
+       {
+               return base64_encode(
+                       hash_hmac('sha1', $string, $credentials->getSecretKey(), true)
+               );
+       }
+
+       private function createCanonicalizedString(
+               RequestInterface $request,
+               $expires = null
+       ) {
+               $buffer = $request->getMethod() . "\n";
+
+               // Add the interesting headers
+               foreach ($this->signableHeaders as $header) {
+                       $buffer .= $request->getHeaderLine($header) . "\n";
+               }
+
+               $date = $expires ?: $request->getHeaderLine('date');
+               $buffer .= "{$date}\n"
+                       . $this->createCanonicalizedAmzHeaders($request)
+                       . $this->createCanonicalizedResource($request);
+
+               return $buffer;
+       }
+
+       private function createCanonicalizedAmzHeaders(RequestInterface $request)
+       {
+               $headers = [];
+               foreach ($request->getHeaders() as $name => $header) {
+                       $name = strtolower($name);
+                       if (strpos($name, 'x-amz-') === 0) {
+                               $value = implode(',', $header);
+                               if (strlen($value) > 0) {
+                                       $headers[$name] = $name . ':' . $value;
+                               }
+                       }
+               }
+
+               if (!$headers) {
+                       return '';
+               }
+
+               ksort($headers);
+
+               return implode("\n", $headers) . "\n";
+       }
+
+       private function createCanonicalizedResource(RequestInterface $request)
+       {
+               $data = $this->parser->parse($request->getUri());
+               $buffer = '/';
+
+               if ($data['bucket']) {
+                       $buffer .= $data['bucket'];
+                       if (!empty($data['key']) || !$data['path_style']) {
+                               $buffer .= '/' . $data['key'];
+                       }
+               }
+
+               // Add sub resource parameters if present.
+               $query = $request->getUri()->getQuery();
+
+               if ($query) {
+                       $params = Psr7\parse_query($query);
+                       $first = true;
+                       foreach ($this->signableQueryString as $key) {
+                               if (array_key_exists($key, $params)) {
+                                       $value = $params[$key];
+                                       $buffer .= $first ? '?' : '&';
+                                       $first = false;
+                                       $buffer .= $key;
+                                       // Don't add values for empty sub-resources
+                                       if (strlen($value)) {
+                                               $buffer .= "={$value}";
+                                       }
+                               }
+                       }
+               }
+
+               return $buffer;
+       }
+}