1
0
mirror of https://github.com/quay/quay.git synced 2026-01-27 18:42:52 +03:00
Files
quay/storage/cloudflarestorage.py
Syed Ahmed 93c816b2b7 cdn: add namespace and username to CDN redirect for usage calculation (PROJQUAY-5939) (#2616)
* cdn: add namespace and username to CDN redirect for usage calculation (PROJQUAY-5939)

We use the referer header to infer the namespace but that
is not always reliable as some clients don't add that header
when sending the request to the CDN
2024-01-18 10:39:18 -05:00

113 lines
4.0 KiB
Python

import base64
import logging
import urllib.parse
from datetime import datetime, timedelta
from functools import lru_cache
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
logger = logging.getLogger(__name__)
from storage.cloud import S3Storage
class CloudFlareS3Storage(S3Storage):
"""
CloudFlare CDN backed by S3 storage
"""
def __init__(
self,
context,
cloudflare_domain,
cloudflare_privatekey_filename,
storage_path,
s3_bucket,
s3_region,
*args,
**kwargs,
):
super(CloudFlareS3Storage, self).__init__(
context, storage_path, s3_bucket, s3_region=s3_region, *args, **kwargs
)
self.cloudflare_domain = cloudflare_domain
self.cloudflare_privatekey = self._load_private_key(cloudflare_privatekey_filename)
self.region = s3_region
def get_direct_download_url(
self, path, request_ip=None, expires_in=60, requires_cors=False, head=False, **kwargs
):
# If CloudFront could not be loaded, fall back to normal S3.
s3_presigned_url = super(CloudFlareS3Storage, self).get_direct_download_url(
path, request_ip, expires_in, requires_cors, head
)
logger.debug(f"s3 presigned_url: {s3_presigned_url}")
if self.cloudflare_privatekey is None or request_ip is None:
return s3_presigned_url
logger.debug('Got direct download request for path "%s" with IP "%s"', path, request_ip)
s3_url_parsed = urllib.parse.urlparse(s3_presigned_url)
cf_url_parsed = s3_url_parsed._replace(netloc=self.cloudflare_domain)
expire_date = datetime.now() + timedelta(seconds=expires_in)
signed_url = self._cf_sign_url(cf_url_parsed, date_less_than=expire_date, **kwargs)
logger.debug(
'Returning CloudFlare URL for path "%s" with IP "%s": %s',
path,
request_ip,
signed_url,
)
return signed_url
def _cf_sign_url(self, cf_url_parsed, date_less_than, **kwargs):
expiry_ts = date_less_than.timestamp()
sign_data = "%s@%d" % (cf_url_parsed.path, expiry_ts)
signature = self.cloudflare_privatekey.sign(
sign_data.encode("utf8"), padding.PKCS1v15(), hashes.SHA256()
)
signature_b64 = base64.b64encode(signature)
return self._build_signed_url(cf_url_parsed, signature_b64, date_less_than, **kwargs)
def _build_signed_url(self, cf_url_parsed, signature, expiry_date, **kwargs):
query = cf_url_parsed.query
url_dict = dict(urllib.parse.parse_qsl(query))
params = {
"cf_sign": signature,
"cf_expiry": "%d" % expiry_date.timestamp(),
"region": self.region,
}
# Additional params for usage calculation. They are removed
# from the URL before sending to S3 by the CloudFlare worker
if kwargs.get("namespace"):
params["namespace"] = kwargs.get("namespace")
if kwargs.get("username"):
params["username"] = kwargs.get("username")
url_dict.update(params)
url_new_query = urllib.parse.urlencode(url_dict)
url_parse = cf_url_parsed._replace(query=url_new_query)
return urllib.parse.urlunparse(url_parse)
@lru_cache(maxsize=1)
def _load_private_key(self, cloudfront_privatekey_filename):
"""
Returns the private key, loaded from the config provider, used to sign direct download URLs
to CloudFront.
"""
if self._context.config_provider is None:
return None
with self._context.config_provider.get_volume_file(
cloudfront_privatekey_filename,
mode="rb",
) as key_file:
return serialization.load_pem_private_key(
key_file.read(), password=None, backend=default_backend()
)