mirror of
https://github.com/quay/quay.git
synced 2025-04-19 21:42:17 +03:00
123 lines
4.4 KiB
Python
123 lines
4.4 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, is_in_network_request
|
|
|
|
|
|
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.context = context
|
|
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 CloudFlare 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
|
|
|
|
if is_in_network_request(self._context, request_ip, self.region):
|
|
if kwargs.get("cdn_specific", False):
|
|
logger.debug(
|
|
"Request came from within network but namespace is protected: %s", path
|
|
)
|
|
else:
|
|
logger.debug("Request is from within the network, returning S3 URL")
|
|
return s3_presigned_url
|
|
|
|
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")
|
|
if kwargs.get("repo_name"):
|
|
params["repo_name"] = kwargs.get("repo_name")
|
|
|
|
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()
|
|
)
|