Skip to content

Commit

Permalink
url-token builder #263
Browse files Browse the repository at this point in the history
  • Loading branch information
evgkirov committed Oct 26, 2023
1 parent d3738f8 commit fa87d16
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 29 deletions.
12 changes: 10 additions & 2 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [4.1.4](https://github.com/uploadcare/pyuploadcare/compare/v4.1.3...v4.1.4) - 2023-10-27

This update introduces the ability to generate secure URLs with the same signature valid not only for the base URL of the file (e.g., `https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/`) but also for all of its transformations. This is optional by default.
This update introduces the ability to generate secure URLs with the same signature valid not only for the base URL of the file (e.g., `https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/`) but also for all of its transformations (#264). This is optional by default.

Also, `AkamaiSecureUrlBuilderWithUrlToken` class has been implemented (#263).

### Added

- For `Uploadcare`
- added optional `wildcard` parameter to `generate_secure_url` method.

- For `AkamaiSecureUrlBuilder`:
- For `AkamaiSecureUrlBuilderWithAclToken`:
- added optional `wildcard` parameter to `build` method.

- `AkamaiSecureUrlBuilderWithUrlToken` class.

### Changed

- `AkamaiSecureUrlBuilder` has been renamed to `AkamaiSecureUrlBuilderWithAclToken`. It is still available under the old name and works as before, but it will give you a deprecation warning when you use it.

## [4.1.3](https://github.com/uploadcare/pyuploadcare/compare/v4.1.2...v4.1.3) - 2023-10-05

### Added
Expand Down
28 changes: 26 additions & 2 deletions docs/core_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,12 @@ You can use your own custom domain and CDN provider for deliver files with authe
Generate secure url for file::

from pyuploadcare import Uploadcare
from pyuploadcare.secure_url import AkamaiSecureUrlBuilder
from pyuploadcare.secure_url import AkamaiSecureUrlBuilderWithAclToken

secure_url_bulder = AkamaiSecureUrlBuilder("your cdn>", "<your secret for token generation>")
secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken(
"<your cdn>",
"<your secret for token generation>"
)

uploadcare = Uploadcare(
public_key='<your public key>',
Expand All @@ -415,6 +418,27 @@ Generate secure url for file, with the same signature valid for its transformati
wildcard=True
)

Generate secure url for file by its URL (please notice the usage of a different builder class)::

from pyuploadcare import Uploadcare
from pyuploadcare.secure_url import AkamaiSecureUrlBuilderWithUrlToken

secure_url_bulder = AkamaiSecureUrlBuilderWithAUrlToken(
"<your cdn>",
"<your secret for token generation>"
)

uploadcare = Uploadcare(
public_key='<your public key>',
secret_key='<your private key>',
secure_url_builder=secure_url_bulder,
)

secure_url = uploadcare.generate_secure_url(
'https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/'
)


Useful links
------------

Expand Down
100 changes: 86 additions & 14 deletions pyuploadcare/secure_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import hashlib
import hmac
import time
import warnings
from abc import ABC, abstractmethod
from typing import Optional

Expand All @@ -12,14 +13,13 @@ def build(self, uuid: str, wildcard: bool = False) -> str:
raise NotImplementedError


class AkamaiSecureUrlBuilder(BaseSecureUrlBuilder):
class BaseAkamaiSecureUrlBuilder(BaseSecureUrlBuilder):
"""Akamai secure url builder.
See https://uploadcare.com/docs/security/secure_delivery/
for more details.
"""

template = "https://{cdn}/{uuid}/?token={token}"
field_delimeter = "~"

def __init__(
Expand All @@ -34,14 +34,36 @@ def __init__(
self.window = window
self.hash_algo = hash_algo

def _build_expire_time(self) -> int:
return int(time.time()) + self.window

def _build_signature(
self, expire: int, acl: Optional[str] = None, url: Optional[str] = None
) -> str:
assert bool(acl) != bool(url)

hash_source = [f"exp={expire}", f"acl={acl}" if acl else f"url={url}"]

signature = hmac.new(
binascii.a2b_hex(self.secret_key.encode()),
self.field_delimeter.join(hash_source).encode(),
self.hash_algo,
).hexdigest()

return signature


class AkamaiSecureUrlBuilderWithAclToken(BaseAkamaiSecureUrlBuilder):
template = "https://{cdn}/{uuid}/?token={token}"

def build(self, uuid: str, wildcard: bool = False) -> str:
uuid = uuid.lstrip("/").rstrip("/")

expire = self._build_expire_time()

acl = self._format_acl(uuid, wildcard=wildcard)

signature = self._build_signature(expire, acl)
signature = self._build_signature(expire, acl=acl)

secure_url = self._build_url(uuid, expire, acl, signature)
return secure_url
Expand Down Expand Up @@ -80,19 +102,69 @@ def _format_acl(self, uuid: str, wildcard: bool) -> str:
return f"/{uuid}/*"
return f"/{uuid}/"

def _build_expire_time(self) -> int:
return int(time.time()) + self.window

def _build_signature(self, expire: int, acl: str) -> str:
hash_source = [
class AkamaiSecureUrlBuilderWithUrlToken(BaseAkamaiSecureUrlBuilder):
template = "{url}?token={token}"

def build(self, uuid: str, wildcard: bool = False) -> str:
if wildcard:
raise ValueError(
"Wildcards are not supported in AkamaiSecureUrlBuilderWithUrlToken."
)

url = uuid

expire = self._build_expire_time()

signature = self._build_signature(expire, url=url)

secure_url = self._build_url(url, expire, signature)

return secure_url

def _build_url(
self,
url: str,
expire: int,
signature: str,
) -> str:
req_parameters = [
f"exp={expire}",
f"hmac={signature}",
]

token = self.field_delimeter.join(req_parameters)

return self.template.format(
url=url,
token=token,
)

def _build_token(self, expire: int, url: str, signature: str):
token_parts = [
f"exp={expire}",
f"acl={acl}",
f"url={url}",
f"hmac={signature}",
]
return self.field_delimeter.join(token_parts)

signature = hmac.new(
binascii.a2b_hex(self.secret_key.encode()),
self.field_delimeter.join(hash_source).encode(),
self.hash_algo,
).hexdigest()

return signature
class AkamaiSecureUrlBuilder(AkamaiSecureUrlBuilderWithAclToken):
def __init__(
self,
cdn_url: str,
secret_key: str,
window: int = 300,
hash_algo=hashlib.sha256,
):
warnings.warn(
"AkamaiSecureUrlBuilder class was renamed to AkamaiSecureUrlBuilderWithAclToken",
DeprecationWarning,
stacklevel=2,
)
super().__init__(
cdn_url=cdn_url,
secret_key=secret_key,
window=window,
hash_algo=hash_algo,
)
40 changes: 29 additions & 11 deletions tests/functional/test_secure_url.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import pytest

from pyuploadcare import Uploadcare
from pyuploadcare.secure_url import AkamaiSecureUrlBuilder
from pyuploadcare.secure_url import (
AkamaiSecureUrlBuilderWithAclToken,
AkamaiSecureUrlBuilderWithUrlToken,
)


known_secret = (
Expand All @@ -10,8 +13,8 @@


@pytest.mark.freeze_time("2021-10-12")
def test_generate_secure_url():
secure_url_bulder = AkamaiSecureUrlBuilder(
def test_generate_secure_url_acl_token():
secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken(
"cdn.yourdomain.com", known_secret
)
secure_url = secure_url_bulder.build(
Expand All @@ -26,8 +29,8 @@ def test_generate_secure_url():


@pytest.mark.freeze_time("2021-10-12")
def test_generate_secure_url_with_transformation():
secure_url_bulder = AkamaiSecureUrlBuilder(
def test_generate_secure_url_with_transformation_acl_token():
secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken(
"cdn.yourdomain.com", known_secret
)
secure_url = secure_url_bulder.build(
Expand All @@ -43,8 +46,8 @@ def test_generate_secure_url_with_transformation():


@pytest.mark.freeze_time("2021-10-12")
def test_generate_secure_url_with_wildcard():
secure_url_bulder = AkamaiSecureUrlBuilder(
def test_generate_secure_url_with_wildcard_acl_token():
secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken(
"cdn.yourdomain.com", known_secret
)
secure_url = secure_url_bulder.build(
Expand All @@ -59,8 +62,8 @@ def test_generate_secure_url_with_wildcard():


@pytest.mark.freeze_time("2021-10-12")
def test_client_generate_secure_url():
secure_url_bulder = AkamaiSecureUrlBuilder(
def test_client_generate_secure_url_acl_token():
secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken(
"cdn.yourdomain.com", known_secret
)

Expand All @@ -81,8 +84,23 @@ def test_client_generate_secure_url():


@pytest.mark.freeze_time("2021-10-12")
def test_client_generate_secure_url_with_wildcard():
secure_url_bulder = AkamaiSecureUrlBuilder(
def test_generate_secure_url_url_token():
secure_url_bulder = AkamaiSecureUrlBuilderWithUrlToken(
"cdn.yourdomain.com", known_secret
)
secure_url = secure_url_bulder.build(
"https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/"
)
assert secure_url == (
"https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/?token="
"exp=1633997100~"
"hmac=32b696b855ddc911b366f11dcecb75789adf6211a72c1dbdf234b83f22aaa368"
)


@pytest.mark.freeze_time("2021-10-12")
def test_client_generate_secure_url_with_wildcard_acl_token():
secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken(
"cdn.yourdomain.com", known_secret
)

Expand Down

0 comments on commit fa87d16

Please sign in to comment.