diff --git a/Readme.md b/Readme.md index 6f26b2b..e9ad195 100644 --- a/Readme.md +++ b/Readme.md @@ -42,6 +42,12 @@ device_check = DeviceCheck( ) ``` +### Asyncio setup +```python +from devicecheck.asyncio import AsyncioDeviceCheck +``` +The rest will be the same, except for network methods must be `await`'ed + ### Validate device ```python diff --git a/devicecheck/__init__.py b/devicecheck/__init__.py index b809bd4..ea9ddb9 100644 --- a/devicecheck/__init__.py +++ b/devicecheck/__init__.py @@ -4,10 +4,10 @@ https://github.com/Kylmakalle/devicecheck """ -__version__ = "1.2.2" +__version__ = "1.3.0" __author__ = 'Sergey Akentev (@Kylmakalle)' __license__ = 'MIT' -__copyright__ = 'Copyright 2021 Sergey Akentev' +__copyright__ = 'Copyright 2023 Sergey Akentev' import logging import os @@ -207,7 +207,11 @@ def __init__(self, team_id: str, bundle_id: str, key_id: str, private_key: [str, log.warning("Using Development environment. Remember to set dev_environment=False in production!") self.retry_wrong_env_request = retry_wrong_env_request self.raise_on_error = raise_on_error - self._session = requests.Session() + self._session = self._make_session() + + @staticmethod + def _make_session() -> requests.Session: + return requests.Session() def generate_token(self, valid_time: int = 500, force_refresh=False): """ diff --git a/devicecheck/asyncio.py b/devicecheck/asyncio.py new file mode 100644 index 0000000..124a909 --- /dev/null +++ b/devicecheck/asyncio.py @@ -0,0 +1,93 @@ +import aiohttp +import certifi +import ssl +from . import DeviceCheck, get_timestamp_milliseconds, get_transaction_id, parse_apple_response, HttpAppleResponse, log, DataAppleResponse + + +class AsyncioDeviceCheck(DeviceCheck): + + @staticmethod + def _make_session() -> aiohttp.ClientSession: + return aiohttp.ClientSession( + connector=aiohttp.TCPConnector( + ssl=ssl.create_default_context(cafile=certifi.where()) + ), + ) + + async def validate_device_token(self, token: str, *args, **kwargs) -> HttpAppleResponse: + """ + Validate a device with it's token + https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_per-device_data#2929855 + :param token: Base 64–encoded representation of encrypted device information + :param args: Additional args for requests module + :param kwargs: Additional kwargs for requests module + :return: + """ + endpoint = 'v1/validate_device_token' + payload = { + 'timestamp': get_timestamp_milliseconds(), + 'transaction_id': get_transaction_id(), + 'device_token': token + } + + result = await self._request(self.get_request_url(), endpoint, payload, *args, **kwargs) + return parse_apple_response(result, self.raise_on_error) + + async def query_two_bits(self, token: str, *args, **kwargs) -> DataAppleResponse: + """ + Query two bits of device data + https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_per-device_data#2910405 + :param token: Base 64–encoded representation of encrypted device information + :param args: Additional args for requests module + :param kwargs: Additional kwargs for requests module + :return: + """ + endpoint = 'v1/query_two_bits' + payload = { + 'timestamp': get_timestamp_milliseconds(), + 'transaction_id': get_transaction_id(), + 'device_token': token + } + result = await self._request(self.get_request_url(), endpoint, payload, *args, **kwargs) + return parse_apple_response(result, self.raise_on_error) + + async def update_two_bits(self, token: str, bit_0: [bool, int] = None, bit_1: [bool, int] = None, *args, + **kwargs) -> HttpAppleResponse: + """ + Update bit(s) of a device data + https://developer.apple.com/documentation/devicecheck/accessing_and_modifying_per-device_data#2910405 + :param token: Base 64–encoded representation of encrypted device information + :param bit_0: (Optional) First bit of data `bool` + :param bit_1: (Optional) Second bit of data `bool` + :param args: Additional args for requests module + :param kwargs: Additional kwargs for requests module + :return: + """ + endpoint = 'v1/update_two_bits' + payload = { + 'timestamp': get_timestamp_milliseconds(), + 'transaction_id': get_transaction_id(), + 'device_token': token, + } + + if bit_0 is not None: + payload['bit0'] = bool(bit_0) + + if bit_1 is not None: + payload['bit1'] = bool(bit_1) + + result = await self._request(self.get_request_url(), endpoint, payload, *args, **kwargs) + return parse_apple_response(result, self.raise_on_error) + + async def _request(self, url, endpoint, payload, retrying_env=False, *args, **kwargs): + log.debug(f'Sending request to {url}/{endpoint} with data {payload}') + + async with self._session.post(f"{url}/{endpoint}", json=payload, headers={"authorization": f"Bearer {self.generate_token()}"}) as response: + result_text = await response.text() + log.debug(f"Response: {response.status} {result_text}") + + if not retrying_env and self.retry_wrong_env_request and response.status != 200: + log.info(f"Retrying request on {'production' if not self.dev_environment else 'development'} server") + return await self._request(self.get_request_url(not self.dev_environment), endpoint, payload, retrying_env=True, *args, **kwargs) + + return response.status, result_text diff --git a/devicecheck/decorators.py b/devicecheck/decorators.py index d0a8724..be5ce67 100644 --- a/devicecheck/decorators.py +++ b/devicecheck/decorators.py @@ -5,6 +5,7 @@ from functools import wraps from . import DeviceCheck, AppleException +from .asyncio import AsyncioDeviceCheck log = logging.getLogger('devicecheck:decorator') logging.basicConfig() @@ -63,13 +64,22 @@ def _is_valid_device(device_check_instance: DeviceCheck, token: str): return False # pragma: no cover -def async_validate_device(device_check_instance: DeviceCheck, +async def _is_valid_device_async(device_check_instance: AsyncioDeviceCheck, token: str): + if MOCK_DEVICE_CHECK_DECORATOR_TOKEN: + return token == MOCK_DEVICE_CHECK_DECORATOR_TOKEN + try: # pragma: no cover + return await device_check_instance.validate_device_token(token).is_ok # pragma: no cover + except AppleException: # pragma: no cover + return False # pragma: no cover + + +def async_validate_device(device_check_instance: AsyncioDeviceCheck, framework: [DCSupportedAsyncFrameworks, str] = None, on_invalid_token=('Invalid device token', 403)): """ Async Decorator that validates device token provided in `Device-Token` header or `device_token`/`deviceToken` key in json body. - :param device_check_instance: Instance of DeviceCheck module for validating + :param device_check_instance: Instance of AsyncioDeviceCheck module for validating :param framework: Name of used async framework for automated data extraction. Leave `None` to rely on a universal parser. :param on_invalid_token: Object that will be returned if validation was unsuccessful :return: on_invalid_token variable @@ -104,7 +114,7 @@ async def decorated_function(*args, **kwargs): device_token = await async_extract_device_token(request, framework) if device_token: try: - is_valid = _is_valid_device(device_check_instance, device_token) + is_valid = await _is_valid_device_async(device_check_instance, device_token) except Exception as e: # pragma: no cover log.error(f'DeviceCheck request failed. {e}') # pragma: no cover if is_valid: diff --git a/setup.py b/setup.py index 7bad6c2..4d24256 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,9 @@ def get_version(rel_path): "pyjwt>=2.0.0", "cryptography>=3.4.7" ], + extras_require={ + 'async': ['aiohttp>=3.8'] + }, packages=["devicecheck"], python_requires=">=3.6", ) diff --git a/tests/integration/asynciodevicecheck_mock.py b/tests/integration/asynciodevicecheck_mock.py new file mode 100644 index 0000000..74e80eb --- /dev/null +++ b/tests/integration/asynciodevicecheck_mock.py @@ -0,0 +1,15 @@ +from devicecheck.asyncio import AsyncioDeviceCheck + +PRIVATE_KEY = """ +-----BEGIN PRIVATE KEY----- +TEST +-----END PRIVATE KEY----- +""" + +device_check = AsyncioDeviceCheck( + team_id="XX7AN23E0Z", + bundle_id="com.akentev.app", + key_id="JSAD983ENA", + private_key=PRIVATE_KEY, + dev_environment=True +) diff --git a/tests/integration/sanic_tests/server.py b/tests/integration/sanic_tests/server.py index d9ac823..6575482 100644 --- a/tests/integration/sanic_tests/server.py +++ b/tests/integration/sanic_tests/server.py @@ -2,7 +2,7 @@ from sanic.response import text from devicecheck.decorators import async_validate_device, DCSupportedAsyncFrameworks -from tests.integration.devicecheck_mock import device_check +from tests.integration.asynciodevicecheck_mock import device_check app = Sanic(__name__) INVALID_TOKEN_RESPONSE = text('Invalid device_token', 403)