123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- <?php
- /**
- * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
- 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,
- array $options = []
- ) {
- $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 (str_starts_with($name, 'x-amz-')) {
- $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(\DateTimeInterface::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\Utils::modifyRequest($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 (str_starts_with($name, 'x-amz-')) {
- $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\Query::parse($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;
- }
- }
|