"""Cloudfront Logic.""" import base64 import json import time from typing import Any import rsa from botocore.signers import CloudFrontSigner from cloudfront_signed_cookies.signer import Signer from video import config def set_signed_cookies( resp: Any, domain: str, resource: str, expire_at: int | None = None ) -> None: """Set Cloudfront signed cookies on the response. Args: resp (flask.Response): The response. domain (str): The domain to set the cookies for. resource (str): The resource that the cookies should give access to. expire_at (int): Timestamp in seconds when the cookies become invalid. """ expire_at = expire_at or int(time.time()) + 3600 policy_bytes, policy_base64 = generate_policy(resource, expire_at) cookies_signer = Signer( cloudfront_key_id=config.CLOUDFRONT_KEY_PAIR_ID, private_key=config.CLOUDFRONT_PRIVATE_KEY, ) cookies = cookies_signer.generate_cookies( Policy=json.loads(policy_bytes.decode("utf-8")), SecondsBeforeExpires=expire_at, ) for name, value in cookies.items(): resp.set_cookie(name, value, domain=domain, secure=True, httponly=True) def get_signed_url(url: str, expire_at: int | None = None) -> str: """Get a signed URL based on an unsigned URL. Args: url (str): The unsigned URL. expire_at (int): Timestamp in seconds when the cookies become invalid. """ expire_at = expire_at or int(time.time()) + 3600 policy_bytes, policy_base64 = generate_policy(url, expire_at) cf_signer = CloudFrontSigner(config.CLOUDFRONT_KEY_PAIR_ID, rsa_signer) return cf_signer.generate_presigned_url(url, policy=policy_bytes) def generate_policy(resource: str, expire_at: int) -> tuple[bytes, str]: """Generate Cloudfront policy to send in cookies. Args: resource (str): The resource that the cookies should give access to. expire_at (int): Timestamp in seconds when the cookies become invalid. Returns: tuple: the policy in bytes and the policy in base64. """ policy_dict = { "Statement": [ { "Resource": resource, "Condition": {"DateLessThan": {"AWS:EpochTime": expire_at}}, } ] } policy_json = json.dumps(policy_dict, separators=(",", ":")) policy_bytes = policy_json.encode("utf-8") policy_base64 = str(base64.b64encode(policy_bytes), "utf-8") policy_base64 = replace_invalid_characters(policy_base64) return policy_bytes, policy_base64 def replace_invalid_characters(s: str) -> str: """Replace invalid characters. Args: s (str): The string to replace the characters from. Returns: str: The string with the characters replaced. """ return s.replace("+", "-").replace("=", "_").replace("/", "~") def rsa_signer(message: bytes) -> bytes: """RSA signer.""" return rsa.sign( message, rsa.PrivateKey.load_pkcs1(config.CLOUDFRONT_PRIVATE_KEY.encode("utf-8")), "SHA-1", )