Skip to content

Commit e655732

Browse files
authored
Merge pull request #7570 from nextcloud/s3-legacy-auth
add option to use legacy v2 auth with s3
2 parents d984c4c + 34ced4d commit e655732

File tree

3 files changed

+222
-1
lines changed

3 files changed

+222
-1
lines changed

apps/files_external/lib/Lib/Backend/AmazonS3.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public function __construct(IL10N $l, AccessKey $legacyAuth) {
5353
->setType(DefinitionParameter::VALUE_BOOLEAN),
5454
(new DefinitionParameter('use_path_style', $l->t('Enable Path Style')))
5555
->setType(DefinitionParameter::VALUE_BOOLEAN),
56+
(new DefinitionParameter('legacy_auth', $l->t('Legacy (v2) authentication')))
57+
->setType(DefinitionParameter::VALUE_BOOLEAN),
5658
])
5759
->addAuthScheme(AccessKey::SCHEME_AMAZONS3_ACCESSKEY)
5860
->setLegacyAuthMechanism($legacyAuth)

lib/private/Files/ObjectStore/S3ConnectionTrait.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
namespace OC\Files\ObjectStore;
2626

27+
use Aws\ClientResolver;
2728
use Aws\S3\Exception\S3Exception;
2829
use Aws\S3\S3Client;
2930

@@ -86,11 +87,15 @@ protected function getConnection() {
8687
],
8788
'endpoint' => $base_url,
8889
'region' => $this->params['region'],
89-
'use_path_style_endpoint' => isset($this->params['use_path_style']) ? $this->params['use_path_style'] : false
90+
'use_path_style_endpoint' => isset($this->params['use_path_style']) ? $this->params['use_path_style'] : false,
91+
'signature_provider' => \Aws\or_chain([self::class, 'legacySignatureProvider'], ClientResolver::_default_signature_provider())
9092
];
9193
if (isset($this->params['proxy'])) {
9294
$options['request.options'] = ['proxy' => $this->params['proxy']];
9395
}
96+
if (isset($this->params['legacy_auth']) && $this->params['legacy_auth']) {
97+
$options['signature_version'] = 'v2';
98+
}
9499
$this->connection = new S3Client($options);
95100

96101
if (!$this->connection->isBucketDnsCompatible($this->bucket)) {
@@ -120,4 +125,14 @@ private function testTimeout() {
120125
sleep($this->timeout);
121126
}
122127
}
128+
129+
public static function legacySignatureProvider($version, $service, $region) {
130+
switch ($version) {
131+
case 'v2':
132+
case 's3':
133+
return new S3Signature();
134+
default:
135+
return null;
136+
}
137+
}
123138
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<?php
2+
3+
namespace OC\Files\ObjectStore;
4+
5+
use Aws\Credentials\CredentialsInterface;
6+
use Aws\S3\S3Client;
7+
use Aws\S3\S3UriParser;
8+
use Aws\Signature\SignatureInterface;
9+
use GuzzleHttp\Psr7;
10+
use Psr\Http\Message\RequestInterface;
11+
12+
/**
13+
* Legacy Amazon S3 signature implementation
14+
*/
15+
class S3Signature implements SignatureInterface
16+
{
17+
/** @var array Query string values that must be signed */
18+
private $signableQueryString = [
19+
'acl', 'cors', 'delete', 'lifecycle', 'location', 'logging',
20+
'notification', 'partNumber', 'policy', 'requestPayment',
21+
'response-cache-control', 'response-content-disposition',
22+
'response-content-encoding', 'response-content-language',
23+
'response-content-type', 'response-expires', 'restore', 'tagging',
24+
'torrent', 'uploadId', 'uploads', 'versionId', 'versioning',
25+
'versions', 'website'
26+
];
27+
28+
/** @var array Sorted headers that must be signed */
29+
private $signableHeaders = ['Content-MD5', 'Content-Type'];
30+
31+
/** @var \Aws\S3\S3UriParser S3 URI parser */
32+
private $parser;
33+
34+
public function __construct()
35+
{
36+
$this->parser = new S3UriParser();
37+
// Ensure that the signable query string parameters are sorted
38+
sort($this->signableQueryString);
39+
}
40+
41+
public function signRequest(
42+
RequestInterface $request,
43+
CredentialsInterface $credentials
44+
) {
45+
$request = $this->prepareRequest($request, $credentials);
46+
$stringToSign = $this->createCanonicalizedString($request);
47+
$auth = 'AWS '
48+
. $credentials->getAccessKeyId() . ':'
49+
. $this->signString($stringToSign, $credentials);
50+
51+
return $request->withHeader('Authorization', $auth);
52+
}
53+
54+
public function presign(
55+
RequestInterface $request,
56+
CredentialsInterface $credentials,
57+
$expires
58+
) {
59+
$query = [];
60+
// URL encoding already occurs in the URI template expansion. Undo that
61+
// and encode using the same encoding as GET object, PUT object, etc.
62+
$uri = $request->getUri();
63+
$path = S3Client::encodeKey(rawurldecode($uri->getPath()));
64+
$request = $request->withUri($uri->withPath($path));
65+
66+
// Make sure to handle temporary credentials
67+
if ($token = $credentials->getSecurityToken()) {
68+
$request = $request->withHeader('X-Amz-Security-Token', $token);
69+
$query['X-Amz-Security-Token'] = $token;
70+
}
71+
72+
if ($expires instanceof \DateTime) {
73+
$expires = $expires->getTimestamp();
74+
} elseif (!is_numeric($expires)) {
75+
$expires = strtotime($expires);
76+
}
77+
78+
// Set query params required for pre-signed URLs
79+
$query['AWSAccessKeyId'] = $credentials->getAccessKeyId();
80+
$query['Expires'] = $expires;
81+
$query['Signature'] = $this->signString(
82+
$this->createCanonicalizedString($request, $expires),
83+
$credentials
84+
);
85+
86+
// Move X-Amz-* headers to the query string
87+
foreach ($request->getHeaders() as $name => $header) {
88+
$name = strtolower($name);
89+
if (strpos($name, 'x-amz-') === 0) {
90+
$query[$name] = implode(',', $header);
91+
}
92+
}
93+
94+
$queryString = http_build_query($query, null, '&', PHP_QUERY_RFC3986);
95+
96+
return $request->withUri($request->getUri()->withQuery($queryString));
97+
}
98+
99+
/**
100+
* @param RequestInterface $request
101+
* @param CredentialsInterface $creds
102+
*
103+
* @return RequestInterface
104+
*/
105+
private function prepareRequest(
106+
RequestInterface $request,
107+
CredentialsInterface $creds
108+
) {
109+
$modify = [
110+
'remove_headers' => ['X-Amz-Date'],
111+
'set_headers' => ['Date' => gmdate(\DateTime::RFC2822)]
112+
];
113+
114+
// Add the security token header if one is being used by the credentials
115+
if ($token = $creds->getSecurityToken()) {
116+
$modify['set_headers']['X-Amz-Security-Token'] = $token;
117+
}
118+
119+
return Psr7\modify_request($request, $modify);
120+
}
121+
122+
private function signString($string, CredentialsInterface $credentials)
123+
{
124+
return base64_encode(
125+
hash_hmac('sha1', $string, $credentials->getSecretKey(), true)
126+
);
127+
}
128+
129+
private function createCanonicalizedString(
130+
RequestInterface $request,
131+
$expires = null
132+
) {
133+
$buffer = $request->getMethod() . "\n";
134+
135+
// Add the interesting headers
136+
foreach ($this->signableHeaders as $header) {
137+
$buffer .= $request->getHeaderLine($header) . "\n";
138+
}
139+
140+
$date = $expires ?: $request->getHeaderLine('date');
141+
$buffer .= "{$date}\n"
142+
. $this->createCanonicalizedAmzHeaders($request)
143+
. $this->createCanonicalizedResource($request);
144+
145+
return $buffer;
146+
}
147+
148+
private function createCanonicalizedAmzHeaders(RequestInterface $request)
149+
{
150+
$headers = [];
151+
foreach ($request->getHeaders() as $name => $header) {
152+
$name = strtolower($name);
153+
if (strpos($name, 'x-amz-') === 0) {
154+
$value = implode(',', $header);
155+
if (strlen($value) > 0) {
156+
$headers[$name] = $name . ':' . $value;
157+
}
158+
}
159+
}
160+
161+
if (!$headers) {
162+
return '';
163+
}
164+
165+
ksort($headers);
166+
167+
return implode("\n", $headers) . "\n";
168+
}
169+
170+
private function createCanonicalizedResource(RequestInterface $request)
171+
{
172+
$data = $this->parser->parse($request->getUri());
173+
$buffer = '/';
174+
175+
if ($data['bucket']) {
176+
$buffer .= $data['bucket'];
177+
if (!empty($data['key']) || !$data['path_style']) {
178+
$buffer .= '/' . $data['key'];
179+
}
180+
}
181+
182+
// Add sub resource parameters if present.
183+
$query = $request->getUri()->getQuery();
184+
185+
if ($query) {
186+
$params = Psr7\parse_query($query);
187+
$first = true;
188+
foreach ($this->signableQueryString as $key) {
189+
if (array_key_exists($key, $params)) {
190+
$value = $params[$key];
191+
$buffer .= $first ? '?' : '&';
192+
$first = false;
193+
$buffer .= $key;
194+
// Don't add values for empty sub-resources
195+
if (strlen($value)) {
196+
$buffer .= "={$value}";
197+
}
198+
}
199+
}
200+
}
201+
202+
return $buffer;
203+
}
204+
}

0 commit comments

Comments
 (0)