summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorMorris Jobke <hey@morrisjobke.de>2018-01-04 13:17:58 +0100
committerGitHub <noreply@github.com>2018-01-04 13:17:58 +0100
commite6557324580e4f76107a059f9fe84b03fbae2b71 (patch)
treec49ed96c51c43b4965c62413c0deb9b63d1eecbf /lib
parentd984c4cdf12d11dfb0971480386abb78913e1375 (diff)
parent34ced4dd97a9c14975f6c5ff6bc3d6dbe463d64f (diff)
downloadnextcloud-server-e6557324580e4f76107a059f9fe84b03fbae2b71.tar.gz
nextcloud-server-e6557324580e4f76107a059f9fe84b03fbae2b71.zip
Merge pull request #7570 from nextcloud/s3-legacy-auth
add option to use legacy v2 auth with s3
Diffstat (limited to 'lib')
-rw-r--r--lib/private/Files/ObjectStore/S3ConnectionTrait.php17
-rw-r--r--lib/private/Files/ObjectStore/S3Signature.php204
2 files changed, 220 insertions, 1 deletions
diff --git a/lib/private/Files/ObjectStore/S3ConnectionTrait.php b/lib/private/Files/ObjectStore/S3ConnectionTrait.php
index 6556e7b169a..d4910834962 100644
--- a/lib/private/Files/ObjectStore/S3ConnectionTrait.php
+++ b/lib/private/Files/ObjectStore/S3ConnectionTrait.php
@@ -24,6 +24,7 @@
namespace OC\Files\ObjectStore;
+use Aws\ClientResolver;
use Aws\S3\Exception\S3Exception;
use Aws\S3\S3Client;
@@ -86,11 +87,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 (!$this->connection->isBucketDnsCompatible($this->bucket)) {
@@ -120,4 +125,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
index 00000000000..d5bfbf4e53b
--- /dev/null
+++ b/lib/private/Files/ObjectStore/S3Signature.php
@@ -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;
+ }
+}