From 11d0f264b0915f193484a91878674606a3c313d7 Mon Sep 17 00:00:00 2001 From: John Flatness Date: Tue, 5 Sep 2023 21:41:33 -0400 Subject: [PATCH] Add CloudFront support for S3 storage --- .../Omeka/Storage/Adapter/ZendS3.php | 2 +- .../Storage/Adapter/ZendS3Cloudfront.php | 128 ++++++++++++++++++ 2 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 application/libraries/Omeka/Storage/Adapter/ZendS3Cloudfront.php diff --git a/application/libraries/Omeka/Storage/Adapter/ZendS3.php b/application/libraries/Omeka/Storage/Adapter/ZendS3.php index 5a45724f2..d7d56eaf8 100644 --- a/application/libraries/Omeka/Storage/Adapter/ZendS3.php +++ b/application/libraries/Omeka/Storage/Adapter/ZendS3.php @@ -236,7 +236,7 @@ private function _getObjectName($path) * * @return int */ - private function _getExpiration() + protected function _getExpiration() { $expiration = (int) @$this->_options[self::EXPIRATION_OPTION]; return $expiration > 0 ? $expiration : 0; diff --git a/application/libraries/Omeka/Storage/Adapter/ZendS3Cloudfront.php b/application/libraries/Omeka/Storage/Adapter/ZendS3Cloudfront.php new file mode 100644 index 000000000..a6d29ba89 --- /dev/null +++ b/application/libraries/Omeka/Storage/Adapter/ZendS3Cloudfront.php @@ -0,0 +1,128 @@ +_cloudfrontDomain = $options[self::CLOUDFRONT_DOMAIN]; + } else { + throw new Omeka_Storage_Exception('The cloudfrontDomain storage option is required'); + } + + if (isset($options[self::CLOUDFRONT_KEY_ID])) { + $this->_cloudfrontKeyId = $options[self::CLOUDFRONT_KEY_ID]; + } + + if (isset($options[self::CLOUDFRONT_KEY_PATH])) { + $this->_cloudfrontKeyPath = $options[self::CLOUDFRONT_KEY_PATH]; + } + + if (isset($options[self::CLOUDFRONT_KEY_PASSPHRASE])) { + $this->_cloudfrontKeyPassphrase = $options[self::CLOUDFRONT_KEY_PASSPHRASE]; + } + + if ($this->_getExpiration() && !($this->_cloudfrontKeyId && $this->_cloudfrontKeyPath)) { + throw new Omeka_Storage_Exception('The cloudfrontKeyId and cloudfrontKeyPath storage options are required when enabling expiration'); + } + } + + /** + * Get a URI to a stored file from CloudFront + */ + public function getUri($path) + { + $object = str_replace('%2F', '/', rawurlencode($path)); + $uri = "https://{$this->_cloudfrontDomain}/{$object}"; + + if ($expiration = $this->_getExpiration()) { + $timestamp = time(); + $expirationSeconds = $expiration * 60; + $expires = $timestamp + $expirationSeconds; + // "Chunk" expirations to allow browser caching + $expires = $expires + $expirationSeconds - ($expires % $expirationSeconds); + + $statement = json_encode(array( + 'Statement' => array( + array( + 'Resource' => $uri, + 'Condition' => array( + 'DateLessThan' => array( + 'AWS:EpochTime' => $expires, + ), + ), + ), + ), + ), JSON_UNESCAPED_SLASHES); + + $key = openssl_pkey_get_private('file://' . $this->_cloudfrontKeyPath, $this->_cloudfrontKeyPassphrase); + if (!$key) { + throw new Omeka_Storage_Exception("Unable to load key for Cloudfront\n\n" . $this->_getOpensslErrors()); + } + + $result = openssl_sign($statement, $signature, $key); + if (!$result) { + throw new Omeka_Storage_Exception("Failed to create Cloudfront URI signature\n\n" . $this->_getOpensslErrors()); + } + + unset($key); + + $signatureParam = strtr(base64_encode($signature), '+=/', '-_~'); + + $query['Expires'] = $expires; + $query['Signature'] = $signatureParam; + $query['Key-Pair-Id'] = $this->_cloudfrontKeyId; + + $queryString = http_build_query($query); + + $uri .= "?$queryString"; + } + + return $uri; + } + + private function _getOpensslErrors() + { + $errors = array(); + while (($error = openssl_error_string()) !== false) { + $errors[] = $error; + } + return implode("\n", $errors); + } +}