diff --git a/HISTORY.md b/HISTORY.md index 05541c6b..af2e57ed 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,7 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [4.2.3](https://github.com/uploadcare/pyuploadcare/compare/v4.2.2...v4.2.3) - unreleased +## [4.3.0](https://github.com/uploadcare/pyuploadcare/compare/v4.2.2...v4.3.0) - 2023-12-24 + +### Fixed + +- For `AkamaiSecureUrlBuilderWithAclToken` and `AkamaiSecureUrlBuilderWithUrlToken`: + - Special characters that were not previously escaped are now properly handled, as detailed in issue [#275](https://github.com/uploadcare/pyuploadcare/issues/275). + +### Changed + +- For `AkamaiSecureUrlBuilderWithAclToken` and `AkamaiSecureUrlBuilderWithUrlToken`: + - Both classes have been made more consistent and now accept a full URL, URL path, or just the UUID of a file – whichever is more convenient for you. ### Deprecated diff --git a/pyproject.toml b/pyproject.toml index 061605e6..dbf57f2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyuploadcare" -version = "4.2.2" +version = "4.3.0" description = "Python library for Uploadcare.com" authors = ["Uploadcare Inc "] readme = "README.md" diff --git a/pyuploadcare/__init__.py b/pyuploadcare/__init__.py index d9a43179..6197fc44 100644 --- a/pyuploadcare/__init__.py +++ b/pyuploadcare/__init__.py @@ -1,5 +1,5 @@ # isort: skip_file -__version__ = "4.2.2" +__version__ = "4.2.3" from pyuploadcare.resources.file import File # noqa: F401 from pyuploadcare.resources.file_group import FileGroup # noqa: F401 diff --git a/pyuploadcare/client.py b/pyuploadcare/client.py index 4ff57447..b8fd4751 100644 --- a/pyuploadcare/client.py +++ b/pyuploadcare/client.py @@ -826,25 +826,31 @@ def generate_upload_signature(self) -> Tuple[int, str]: return expire, signature def generate_secure_url( - self, uuid: Union[str, UUID], wildcard: bool = False + self, handle: Union[str, UUID], wildcard: bool = False ) -> str: - """Generate authenticated URL.""" - if isinstance(uuid, UUID): - uuid = str(uuid) + """ + Generate authenticated URL. + :param handle: Can be one of the following: UUID, UUID with transformations, full URL. + """ + if isinstance(handle, UUID): + handle = str(handle) if not self.secure_url_builder: raise ValueError("secure_url_builder must be set") - return self.secure_url_builder.build(uuid, wildcard=wildcard) + return self.secure_url_builder.build(handle, wildcard=wildcard) def generate_secure_url_token( - self, uuid: Union[str, UUID], wildcard: bool = False + self, handle: Union[str, UUID], wildcard: bool = False ) -> str: - """Generate token for authenticated URL.""" - if isinstance(uuid, UUID): - uuid = str(uuid) + """ + Generate token for authenticated URL. + :param handle: Can be one of the following: UUID, UUID with transformations, full URL. + """ + if isinstance(handle, UUID): + handle = str(handle) if not self.secure_url_builder: raise ValueError("secure_url_builder must be set") - return self.secure_url_builder.get_token(uuid, wildcard=wildcard) + return self.secure_url_builder.get_token(handle, wildcard=wildcard) diff --git a/pyuploadcare/secure_url.py b/pyuploadcare/secure_url.py index 270f21af..c92c7831 100644 --- a/pyuploadcare/secure_url.py +++ b/pyuploadcare/secure_url.py @@ -1,18 +1,26 @@ import binascii import hashlib import hmac +import re import time import warnings from abc import ABC, abstractmethod from typing import Optional +from urllib.parse import quote_plus, urlparse class BaseSecureUrlBuilder(ABC): @abstractmethod - def build(self, uuid: str, wildcard: bool = False) -> str: + def build(self, handle: str, wildcard: bool = False) -> str: + """ + :param handle: Can be one of the following: UUID, UUID with transformations, full URL. + """ raise NotImplementedError - def get_token(self, uuid: str, wildcard: bool = False) -> str: + def get_token(self, handle: str, wildcard: bool = False) -> str: + """ + :param handle: Can be one of the following: UUID, UUID with transformations, full URL. + """ raise NotImplementedError( f"{self.__class__} doesn't provide get_token()" ) @@ -25,6 +33,7 @@ class BaseAkamaiSecureUrlBuilder(BaseSecureUrlBuilder): for more details. """ + base_template = "https://{cdn}/{path}/" template = "{base}?token={token}" field_delimeter = "~" @@ -40,30 +49,46 @@ def __init__( self.window = window self.hash_algo = hash_algo - def build(self, uuid: str, wildcard: bool = False) -> str: - uuid_or_url = self._format_uuid_or_url(uuid) - token = self.get_token(uuid_or_url, wildcard=wildcard) - secure_url = self._build_url(uuid_or_url, token) + def build(self, handle: str, wildcard: bool = False) -> str: + token = self.get_token(handle, wildcard=wildcard) + secure_url = self._build_url(handle, token) return secure_url - def get_token(self, uuid: str, wildcard: bool = False) -> str: - uuid_or_url = self._format_uuid_or_url(uuid) + def get_token(self, handle: str, wildcard: bool = False) -> str: + path = self._get_path(handle) expire = self._build_expire_time() - acl = self._format_acl(uuid_or_url, wildcard=wildcard) - signature = self._build_signature(uuid_or_url, expire, acl) + acl = self._format_acl(path, wildcard=wildcard) + signature = self._build_signature(handle, expire, acl) token = self._build_token(expire, acl, signature) return token + def _prepare_path_for_url(self, path: str) -> str: + path = re.sub( + r"(%..)", + lambda match: match.group(1).lower(), + quote_plus(path, safe=","), + ) + path = self._prepare_path_for_acl(path) + return path + + def _prepare_path_for_acl(self, path: str) -> str: + for escape_char in "~": + path = path.replace( + escape_char, "%" + hex(ord(escape_char)).lower()[2:] + ) + return path + def _build_expire_time(self) -> int: return int(time.time()) + self.window def _build_signature( - self, uuid_or_url: str, expire: int, acl: Optional[str] + self, handle: str, expire: int, acl: Optional[str] ) -> str: - + path = self._get_path(handle) + path = self._prepare_path_for_url(path) hash_source = [ f"exp={expire}", - f"acl={acl}" if acl else f"url={uuid_or_url}", + f"acl={acl}" if acl else f"url={path}", ] signature = hmac.new( @@ -75,7 +100,6 @@ def _build_signature( return signature def _build_token(self, expire: int, acl: Optional[str], signature: str): - token_parts = [ f"exp={expire}", f"acl={acl}" if acl else None, @@ -86,61 +110,68 @@ def _build_token(self, expire: int, acl: Optional[str], signature: str): part for part in token_parts if part is not None ) - @abstractmethod - def _build_base_url(self, uuid_or_url: str): - raise NotImplementedError - def _build_url( self, - uuid_or_url: str, + handle: str, token: str, ) -> str: - base_url = self._build_base_url(uuid_or_url) + base_url = self._build_base_url(handle) return self.template.format( base=base_url, token=token, ) - @abstractmethod - def _format_acl(self, uuid_or_url: str, wildcard: bool) -> Optional[str]: - raise NotImplementedError + def _get_path(self, handle: str) -> str: + """ + >>> builder._get_path("fake-uuid") + /fake-uuid/ + >>> builder._get_path("https://sectest.ucarecdn.com/fake-uuid/-/resize/20x20/") + /fake-uuid/-/resize/20x20/ + """ + path = handle + parsed = urlparse(path) + if parsed.netloc: + # extract uuid with transformations from url + path = parsed.path + if not path.startswith("/"): + path = f"/{path}" + return path + + def _build_base_url(self, handle: str): + """ + >>> builder._build_base_url("fake-uuid") + https://sectest.ucarecdn.com/fake-uuid/ + >>> builder._get_path("https://sectest.ucarecdn.com/fake-uuid/-/resize/20x20/") + https://sectest.ucarecdn.com/fake-uuid/-/resize/20x20/ + """ + path = self._get_path(handle) + path = path.lstrip("/").rstrip("/") + base_url = self.base_template.format(cdn=self.cdn_url, path=path) + return base_url @abstractmethod - def _format_uuid_or_url(self, uuid_or_url: str) -> str: + def _format_acl(self, handle: str, wildcard: bool) -> Optional[str]: raise NotImplementedError class AkamaiSecureUrlBuilderWithAclToken(BaseAkamaiSecureUrlBuilder): - base_template = "https://{cdn}/{uuid}/" - - def _build_base_url(self, uuid_or_url: str): - return self.base_template.format(cdn=self.cdn_url, uuid=uuid_or_url) - - def _format_acl(self, uuid_or_url: str, wildcard: bool) -> str: + def _format_acl(self, handle: str, wildcard: bool) -> str: + path = self._get_path(handle) + path = path.lstrip("/").rstrip("/") + path = self._prepare_path_for_acl(path) if wildcard: - return f"/{uuid_or_url}/*" - return f"/{uuid_or_url}/" - - def _format_uuid_or_url(self, uuid_or_url: str) -> str: - return uuid_or_url.lstrip("/").rstrip("/") + return f"/{path}/*" + return f"/{path}/" class AkamaiSecureUrlBuilderWithUrlToken(BaseAkamaiSecureUrlBuilder): - def _build_base_url(self, uuid_or_url: str): - return uuid_or_url - - def _format_acl(self, uuid_or_url: str, wildcard: bool) -> None: + def _format_acl(self, handle: str, wildcard: bool) -> None: if wildcard: raise ValueError( "Wildcards are not supported in AkamaiSecureUrlBuilderWithUrlToken." ) return None - def _format_uuid_or_url(self, uuid_or_url: str) -> str: - if "://" not in uuid_or_url: - raise ValueError(f"{uuid_or_url} doesn't look like a URL") - return uuid_or_url - class AkamaiSecureUrlBuilder(AkamaiSecureUrlBuilderWithAclToken): def __init__( diff --git a/tests/conftest.py b/tests/conftest.py index 3d4d2706..fdbb4913 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import os +import sys from io import BytesIO from tempfile import TemporaryDirectory from typing import Tuple @@ -9,6 +10,23 @@ from pyuploadcare.client import Uploadcare +# XXX workaround for vcrpy incompability with Python 3.12 +# https://github.com/mgorny/vcrpy/commit/5f2623886dda2fd8bb6035370251fe07cec958ca +# Remove this when an updated version of vcrpy is released. +if sys.version_info >= (3, 12): + from vcr.stubs import ( + HTTPConnection, + HTTPSConnection, + VCRHTTPConnection, + VCRHTTPSConnection, + ) + + VCRHTTPConnection.debuglevel = HTTPConnection.debuglevel + VCRHTTPConnection._http_vsn = HTTPConnection._http_vsn + VCRHTTPSConnection.debuglevel = HTTPSConnection.debuglevel + VCRHTTPSConnection._http_vsn = HTTPSConnection._http_vsn + + @pytest.fixture() def temp_directory(): return TemporaryDirectory() diff --git a/tests/functional/cassettes/test_acl_token_bare_uuid.yaml b/tests/functional/cassettes/test_acl_token_bare_uuid.yaml new file mode 100644 index 00000000..5a5bb925 --- /dev/null +++ b/tests/functional/cassettes/test_acl_token_bare_uuid.yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/?token=exp=2015971200~acl=/1bd27101-6f40-460a-9358-d44c282e9d16/~hmac=0e466b666443108e92a8b8255b95f7473073c8ea14aa074de5647d092c683e1f + response: + body: + string: 'hello + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - public, max-age=31309321 + Connection: + - close + Content-Disposition: + - inline; filename=hello.txt + Content-Length: + - '6' + Content-Type: + - text/plain + Date: + - Fri, 24 Nov 2023 21:55:28 GMT + ETag: + - '"b1946ac92492d2347c6235b4d2611184"' + Last-Modified: + - Wed, 22 Nov 2023 00:53:42 GMT + Server: + - Uploadcare + Vary: + - Accept-Encoding + X-Robots-Tag: + - noindex, nofollow, nosnippet, noarchive + status: + code: 200 + message: OK +version: 1 diff --git a/tests/functional/cassettes/test_acl_token_basic_url.yaml b/tests/functional/cassettes/test_acl_token_basic_url.yaml new file mode 100644 index 00000000..859f0bd2 --- /dev/null +++ b/tests/functional/cassettes/test_acl_token_basic_url.yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/?token=exp=2015971200~acl=/1bd27101-6f40-460a-9358-d44c282e9d16/~hmac=0e466b666443108e92a8b8255b95f7473073c8ea14aa074de5647d092c683e1f + response: + body: + string: 'hello + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - public, max-age=31309319 + Connection: + - close + Content-Disposition: + - inline; filename=hello.txt + Content-Length: + - '6' + Content-Type: + - text/plain + Date: + - Fri, 24 Nov 2023 21:55:30 GMT + ETag: + - '"b1946ac92492d2347c6235b4d2611184"' + Last-Modified: + - Wed, 22 Nov 2023 00:53:42 GMT + Server: + - Uploadcare + Vary: + - Accept-Encoding + X-Robots-Tag: + - noindex, nofollow, nosnippet, noarchive + status: + code: 200 + message: OK +version: 1 diff --git a/tests/functional/cassettes/test_acl_token_group_url.yaml b/tests/functional/cassettes/test_acl_token_group_url.yaml new file mode 100644 index 00000000..1416105c --- /dev/null +++ b/tests/functional/cassettes/test_acl_token_group_url.yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/3b278cee-47bd-4276-8d7d-9cde5902b18c~1/nth/0/?token=exp=2015971200~acl=/3b278cee-47bd-4276-8d7d-9cde5902b18c%7e1/nth/0/~hmac=9df9a86460fab102dd0ecb4a36980a1f932b6ce5cf4bdc0307906451fcf8978e + response: + body: + string: 'hello + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - public, max-age=31457414 + Connection: + - close + Content-Disposition: + - inline; filename=hello.txt + Content-Length: + - '6' + Content-Type: + - text/plain + Date: + - Fri, 24 Nov 2023 21:55:30 GMT + ETag: + - '"b1946ac92492d2347c6235b4d2611184"' + Last-Modified: + - Wed, 22 Nov 2023 00:53:42 GMT + Server: + - Uploadcare + Vary: + - Accept-Encoding + X-Robots-Tag: + - noindex, nofollow, nosnippet, noarchive + status: + code: 200 + message: OK +version: 1 diff --git a/tests/functional/cassettes/test_acl_token_uuid_with_transformations.yaml b/tests/functional/cassettes/test_acl_token_uuid_with_transformations.yaml new file mode 100644 index 00000000..19feee98 --- /dev/null +++ b/tests/functional/cassettes/test_acl_token_uuid_with_transformations.yaml @@ -0,0 +1,125 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/-/crop/10x10/20,20/?token=exp=2015971200~acl=/1bd27101-6f40-460a-9358-d44c282e9d16/-/crop/10x10/20,20/~hmac=742ef029bef6b1806a2ecf1075e3df9676d9192963da54c54f4bb8e6240a1ef6 + response: + body: + string: ' + + + + + + + + 400: Bad Request + + + + + + + +

Bad Request

+ + + +

Not supported for files that are not images.

+ + + + +

+ + See CDN documentation. + +

+ + + + + + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Content-Length, Etag, X-Image-Width, X-Image-Height, X-Image-Acceptable-Original, + X-Image-Acceptable-Improved + Cache-Control: + - public, max-age=3580 + Connection: + - close + Content-Length: + - '694' + Content-Security-Policy: + - sandbox; default-src 'unsafe-inline' data:; script-src 'none' + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 24 Nov 2023 21:55:28 GMT + Server: + - Uploadcare + status: + code: 400 + message: Bad Request +version: 1 diff --git a/tests/functional/cassettes/test_acl_token_wildcard_uuid.yaml b/tests/functional/cassettes/test_acl_token_wildcard_uuid.yaml new file mode 100644 index 00000000..e615f024 --- /dev/null +++ b/tests/functional/cassettes/test_acl_token_wildcard_uuid.yaml @@ -0,0 +1,94 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/?token=exp=2015971200~acl=/1bd27101-6f40-460a-9358-d44c282e9d16/*~hmac=4f953bf9b238c59a90bd12972afd5d6fb5ec5377709f2d6b1f493d10cc08abda + response: + body: + string: 'hello + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - public, max-age=31309321 + Connection: + - close + Content-Disposition: + - inline; filename=hello.txt + Content-Length: + - '6' + Content-Type: + - text/plain + Date: + - Fri, 24 Nov 2023 21:55:28 GMT + ETag: + - '"b1946ac92492d2347c6235b4d2611184"' + Last-Modified: + - Wed, 22 Nov 2023 00:53:42 GMT + Server: + - Uploadcare + Vary: + - Accept-Encoding + X-Robots-Tag: + - noindex, nofollow, nosnippet, noarchive + status: + code: 200 + message: OK +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/hello.txt?token=exp=2015971200~acl=/1bd27101-6f40-460a-9358-d44c282e9d16/*~hmac=4f953bf9b238c59a90bd12972afd5d6fb5ec5377709f2d6b1f493d10cc08abda + response: + body: + string: 'hello + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - public, max-age=31388396 + Connection: + - close + Content-Disposition: + - inline; filename=hello.txt + Content-Length: + - '6' + Content-Type: + - text/plain + Date: + - Fri, 24 Nov 2023 21:55:28 GMT + ETag: + - '"b1946ac92492d2347c6235b4d2611184"' + Last-Modified: + - Wed, 22 Nov 2023 00:53:42 GMT + Server: + - Uploadcare + Vary: + - Accept-Encoding + X-Robots-Tag: + - noindex, nofollow, nosnippet, noarchive + status: + code: 200 + message: OK +version: 1 diff --git a/tests/functional/cassettes/test_acl_token_wildcard_uuid_with_transformations.yaml b/tests/functional/cassettes/test_acl_token_wildcard_uuid_with_transformations.yaml new file mode 100644 index 00000000..2b7f6ef3 --- /dev/null +++ b/tests/functional/cassettes/test_acl_token_wildcard_uuid_with_transformations.yaml @@ -0,0 +1,252 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/-/preview/400x400/?token=exp=2015971200~acl=/1bd27101-6f40-460a-9358-d44c282e9d16/-/preview/400x400/*~hmac=e4d3477b3c5b88cc11ec916f2f853c82f3e83aa8119e85bc18a93eb5e7d84ded + response: + body: + string: ' + + + + + + + + 400: Bad Request + + + + + + + +

Bad Request

+ + + +

Not supported for files that are not images.

+ + + + +

+ + See CDN documentation. + +

+ + + + + + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Content-Length, Etag, X-Image-Width, X-Image-Height, X-Image-Acceptable-Original, + X-Image-Acceptable-Improved + Cache-Control: + - public, max-age=3563 + Connection: + - close + Content-Length: + - '694' + Content-Security-Policy: + - sandbox; default-src 'unsafe-inline' data:; script-src 'none' + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 24 Nov 2023 21:55:29 GMT + Server: + - Uploadcare + Vary: + - accept + status: + code: 400 + message: Bad Request +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/-/preview/400x400/hello.txt?token=exp=2015971200~acl=/1bd27101-6f40-460a-9358-d44c282e9d16/-/preview/400x400/*~hmac=e4d3477b3c5b88cc11ec916f2f853c82f3e83aa8119e85bc18a93eb5e7d84ded + response: + body: + string: ' + + + + + + + + 400: Bad Request + + + + + + + +

Bad Request

+ + + +

Not supported for files that are not images.

+ + + + +

+ + See CDN documentation. + +

+ + + + + + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Content-Length, Etag, X-Image-Width, X-Image-Height, X-Image-Acceptable-Original, + X-Image-Acceptable-Improved + Cache-Control: + - public, max-age=3600 + Connection: + - close + Content-Length: + - '694' + Content-Security-Policy: + - sandbox; default-src 'unsafe-inline' data:; script-src 'none' + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 24 Nov 2023 21:55:30 GMT + Server: + - Uploadcare + Vary: + - accept + status: + code: 400 + message: Bad Request +version: 1 diff --git a/tests/functional/cassettes/test_url_token_basic_url.yaml b/tests/functional/cassettes/test_url_token_basic_url.yaml new file mode 100644 index 00000000..ca6c3c80 --- /dev/null +++ b/tests/functional/cassettes/test_url_token_basic_url.yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/?token=exp=2015971200~hmac=9d41cc5e8a03ba3d09e7ad957897c58efb35ab889a8fd473f7fe257ea413caed + response: + body: + string: 'hello + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - public, max-age=31309319 + Connection: + - close + Content-Disposition: + - inline; filename=hello.txt + Content-Length: + - '6' + Content-Type: + - text/plain + Date: + - Fri, 24 Nov 2023 21:55:30 GMT + ETag: + - '"b1946ac92492d2347c6235b4d2611184"' + Last-Modified: + - Wed, 22 Nov 2023 00:53:42 GMT + Server: + - Uploadcare + Vary: + - Accept-Encoding + X-Robots-Tag: + - noindex, nofollow, nosnippet, noarchive + status: + code: 200 + message: OK +version: 1 diff --git a/tests/functional/cassettes/test_url_token_group_url.yaml b/tests/functional/cassettes/test_url_token_group_url.yaml new file mode 100644 index 00000000..9b883e4f --- /dev/null +++ b/tests/functional/cassettes/test_url_token_group_url.yaml @@ -0,0 +1,48 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/3b278cee-47bd-4276-8d7d-9cde5902b18c~1/nth/0/?token=exp=2015971200~hmac=3d18d965fbf5d93e434c11ca209e2a7068b50b00962ebf7a19eae357a098bdd2 + response: + body: + string: 'hello + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - public, max-age=31457414 + Connection: + - close + Content-Disposition: + - inline; filename=hello.txt + Content-Length: + - '6' + Content-Type: + - text/plain + Date: + - Fri, 24 Nov 2023 21:55:30 GMT + ETag: + - '"b1946ac92492d2347c6235b4d2611184"' + Last-Modified: + - Wed, 22 Nov 2023 00:53:42 GMT + Server: + - Uploadcare + Vary: + - Accept-Encoding + X-Robots-Tag: + - noindex, nofollow, nosnippet, noarchive + status: + code: 200 + message: OK +version: 1 diff --git a/tests/functional/cassettes/test_url_token_uuid_with_transformations.yaml b/tests/functional/cassettes/test_url_token_uuid_with_transformations.yaml new file mode 100644 index 00000000..7539c057 --- /dev/null +++ b/tests/functional/cassettes/test_url_token_uuid_with_transformations.yaml @@ -0,0 +1,125 @@ +interactions: +- request: + body: null + headers: + Connection: + - close + Host: + - sectest.ucarecdn.com + User-Agent: + - Python-urllib/3.11 + method: GET + uri: https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/-/crop/10x10/20,20/?token=exp=2015971200~hmac=0bfdf8d6e104ab4228cd2321cac3e5a9951dffb1b0e235a5509915c6e78b7e76 + response: + body: + string: ' + + + + + + + + 400: Bad Request + + + + + + + +

Bad Request

+ + + +

Not supported for files that are not images.

+ + + + +

+ + See CDN documentation. + +

+ + + + + + + ' + headers: + Access-Control-Allow-Methods: + - HEAD, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - Content-Length, Etag, X-Image-Width, X-Image-Height, X-Image-Acceptable-Original, + X-Image-Acceptable-Improved + Cache-Control: + - public, max-age=3583 + Connection: + - close + Content-Length: + - '694' + Content-Security-Policy: + - sandbox; default-src 'unsafe-inline' data:; script-src 'none' + Content-Type: + - text/html; charset=UTF-8 + Date: + - Fri, 24 Nov 2023 21:55:30 GMT + Server: + - Uploadcare + status: + code: 400 + message: Bad Request +version: 1 diff --git a/tests/functional/test_secure_url.py b/tests/functional/test_secure_url.py index a7d9106c..e0bb06d6 100644 --- a/tests/functional/test_secure_url.py +++ b/tests/functional/test_secure_url.py @@ -1,3 +1,6 @@ +from urllib.error import HTTPError +from urllib.request import urlopen + import pytest from pyuploadcare import Uploadcare @@ -127,7 +130,7 @@ def test_get_url_token(): ) assert token == ( "exp=1633997100~" - "hmac=32b696b855ddc911b366f11dcecb75789adf6211a72c1dbdf234b83f22aaa368" + "hmac=25c485fd7f85c19704013673c80d2a86df1b4241fb44cdfa7b7762cb27ef3f57" ) @@ -142,7 +145,7 @@ def test_generate_secure_url_url_token(): assert secure_url == ( "https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/?token=" "exp=1633997100~" - "hmac=32b696b855ddc911b366f11dcecb75789adf6211a72c1dbdf234b83f22aaa368" + "hmac=25c485fd7f85c19704013673c80d2a86df1b4241fb44cdfa7b7762cb27ef3f57" ) @@ -166,3 +169,146 @@ def test_client_generate_secure_url_with_wildcard_acl_token(): "acl=/52da3bfc-7cd8-4861-8b05-126fef7a6994/*~" "hmac=b2c7526a29d0588b121aa78bc2b2c9399bfb6e1cad3d95397efed722fdbc5a78" ) + + +@pytest.fixture +def uploadcare_with_acl_token(): + secure_url_builder = AkamaiSecureUrlBuilderWithAclToken( + cdn_url="sectest.ucarecdn.com", + secret_key=known_secret, + window=60 * 60 * 24 * 365 * 10, + ) + + uploadcare = Uploadcare( + public_key="public", + secret_key="secret", + secure_url_builder=secure_url_builder, + ) + return uploadcare + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_acl_token_bare_uuid(uploadcare_with_acl_token: Uploadcare): + secure_url = uploadcare_with_acl_token.generate_secure_url( + "1bd27101-6f40-460a-9358-d44c282e9d16" + ) + assert urlopen(secure_url).status == 200 + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_acl_token_uuid_with_transformations( + uploadcare_with_acl_token: Uploadcare, +): + secure_url = uploadcare_with_acl_token.generate_secure_url( + "1bd27101-6f40-460a-9358-d44c282e9d16/-/crop/10x10/20,20/" + ) + with pytest.raises(HTTPError) as e: + urlopen(secure_url) + assert "are not images" in e.value.read().decode() + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_acl_token_wildcard_uuid(uploadcare_with_acl_token: Uploadcare): + secure_url = uploadcare_with_acl_token.generate_secure_url( + "1bd27101-6f40-460a-9358-d44c282e9d16", wildcard=True + ) + assert urlopen(secure_url).status == 200 + + secure_url = uploadcare_with_acl_token.generate_secure_url( + "1bd27101-6f40-460a-9358-d44c282e9d16", wildcard=True + ) + secure_url = secure_url.replace("/?token=", "/hello.txt?token=") + assert "hello.txt" in secure_url + assert urlopen(secure_url).status == 200 + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_acl_token_wildcard_uuid_with_transformations( + uploadcare_with_acl_token: Uploadcare, +): + secure_url = uploadcare_with_acl_token.generate_secure_url( + "1bd27101-6f40-460a-9358-d44c282e9d16/-/preview/400x400/", + wildcard=True, + ) + with pytest.raises(HTTPError) as e: + urlopen(secure_url) + assert "are not images" in e.value.read().decode() + + secure_url = uploadcare_with_acl_token.generate_secure_url( + "1bd27101-6f40-460a-9358-d44c282e9d16/-/preview/400x400/", + wildcard=True, + ) + secure_url = secure_url.replace("/?token=", "/hello.txt?token=") + assert "hello.txt" in secure_url + with pytest.raises(HTTPError) as e: + urlopen(secure_url) + assert "are not images" in e.value.read().decode() + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_acl_token_basic_url(uploadcare_with_acl_token: Uploadcare): + secure_url = uploadcare_with_acl_token.generate_secure_url( + "https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/" + ) + assert urlopen(secure_url).status == 200 + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_acl_token_group_url(uploadcare_with_acl_token: Uploadcare): + secure_url = uploadcare_with_acl_token.generate_secure_url( + "https://sectest.ucarecdn.com/3b278cee-47bd-4276-8d7d-9cde5902b18c~1/nth/0/" + ) + assert urlopen(secure_url).status == 200 + + +@pytest.fixture +def uploadcare_with_url_token(): + secure_url_builder = AkamaiSecureUrlBuilderWithUrlToken( + cdn_url="sectest.ucarecdn.com", + secret_key=known_secret, + window=60 * 60 * 24 * 365 * 10, + ) + + uploadcare = Uploadcare( + public_key="public", + secret_key="secret", + secure_url_builder=secure_url_builder, + ) + return uploadcare + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_url_token_basic_url(uploadcare_with_url_token: Uploadcare): + secure_url = uploadcare_with_url_token.generate_secure_url( + "https://sectest.ucarecdn.com/1bd27101-6f40-460a-9358-d44c282e9d16/" + ) + assert urlopen(secure_url).status == 200 + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_url_token_uuid_with_transformations( + uploadcare_with_url_token: Uploadcare, +): + secure_url = uploadcare_with_url_token.generate_secure_url( + "1bd27101-6f40-460a-9358-d44c282e9d16/-/crop/10x10/20,20/" + ) + with pytest.raises(HTTPError) as e: + urlopen(secure_url) + assert "are not images" in e.value.read().decode() + + +@pytest.mark.freeze_time("2023-11-22") +@pytest.mark.vcr +def test_url_token_group_url(uploadcare_with_url_token: Uploadcare): + secure_url = uploadcare_with_url_token.generate_secure_url( + "https://sectest.ucarecdn.com/3b278cee-47bd-4276-8d7d-9cde5902b18c~1/nth/0/" + ) + assert urlopen(secure_url).status == 200