mirror of
https://github.com/quay/quay.git
synced 2026-01-27 18:42:52 +03:00
* 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
113 lines
4.0 KiB
Python
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()
|
|
)
|