From 0fec9d7ff7af0868166aebbb32eac04f045994c8 Mon Sep 17 00:00:00 2001 From: Mick Date: Fri, 9 Aug 2024 21:06:06 +0000 Subject: [PATCH 1/5] Fix nonce and add capability flag, add retry on reauth --- sagemcom_api/client.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index 111b005..b97be87 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -3,11 +3,13 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping import hashlib import json import math import random from types import TracebackType +from typing import Any import urllib.parse from aiohttp import ( @@ -51,6 +53,11 @@ from .models import Device, DeviceInfo, PortMapping +async def retry_login(invocation: Mapping[str, Any]) -> None: + """Retry login via backoff if an exception occurs.""" + await invocation["args"][0].login() + + # pylint: disable=too-many-instance-attributes class SagemcomClient: """Client to communicate with the Sagemcom API.""" @@ -118,7 +125,7 @@ async def close(self) -> None: def __generate_nonce(self): """Generate pseudo random number (nonce) to avoid replay attacks.""" - self._current_nonce = math.floor(random.randrange(0, 1) * 500000) + self._current_nonce = math.floor(random.randrange(0, 500000)) def __generate_request_id(self): """Generate sequential request ID.""" @@ -182,6 +189,12 @@ def __get_response_value(self, response, index=0): return value + @backoff.on_exception( + backoff.expo, + (AuthenticationException, LoginRetryErrorException, LoginTimeoutException), + max_tries=2, + on_backoff=retry_login, + ) @backoff.on_exception( backoff.expo, (ClientConnectorError, ClientOSError, ServerDisconnectedError), @@ -435,7 +448,9 @@ async def get_device_info(self) -> DeviceInfo: async def get_hosts(self, only_active: bool | None = False) -> list[Device]: """Retrieve hosts connected to Sagemcom F@st device.""" - data = await self.get_value_by_xpath("Device/Hosts/Hosts") + data = await self.get_value_by_xpath( + "Device/Hosts/Hosts", options={"capability-flags": {"interface": True}} + ) devices = [Device(**d) for d in data] if only_active: From 99fdc5d0a389679db905be5d274841f4992f80ae Mon Sep 17 00:00:00 2001 From: Mick Date: Fri, 9 Aug 2024 21:30:08 +0000 Subject: [PATCH 2/5] Fix nonce and add capability flag, add retry on auth issue --- sagemcom_api/client.py | 28 +++++++++++++++++----- sagemcom_api/const.py | 1 + sagemcom_api/exceptions.py | 48 ++++++++++++++++++++++---------------- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index b97be87..d58902c 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -29,6 +29,7 @@ DEFAULT_USER_AGENT, XMO_ACCESS_RESTRICTION_ERR, XMO_AUTHENTICATION_ERR, + XMO_INVALID_SESSION_ERR, XMO_LOGIN_RETRY_ERR, XMO_MAX_SESSION_COUNT_ERR, XMO_NO_ERR, @@ -42,6 +43,7 @@ AccessRestrictionException, AuthenticationException, BadRequestException, + InvalidSessionException, LoginRetryErrorException, LoginTimeoutException, MaximumSessionCountException, @@ -189,17 +191,12 @@ def __get_response_value(self, response, index=0): return value - @backoff.on_exception( - backoff.expo, - (AuthenticationException, LoginRetryErrorException, LoginTimeoutException), - max_tries=2, - on_backoff=retry_login, - ) @backoff.on_exception( backoff.expo, (ClientConnectorError, ClientOSError, ServerDisconnectedError), max_tries=5, ) + # pylint: disable=too-many-branches async def __post(self, url, data): async with self.session.post(url, data=data) as response: if response.status == 400: @@ -220,6 +217,12 @@ async def __post(self, url, data): ): return result + if error["description"] == XMO_INVALID_SESSION_ERR: + self._session_id = 0 + self._server_nonce = "" + self._request_id = -1 + raise InvalidSessionException(error) + # Error in one of the actions if error["description"] == XMO_REQUEST_ACTION_ERR: # pylint:disable=fixme @@ -254,6 +257,17 @@ async def __post(self, url, data): return result + @backoff.on_exception( + backoff.expo, + ( + AuthenticationException, + LoginRetryErrorException, + LoginTimeoutException, + InvalidSessionException, + ), + max_tries=2, + on_backoff=retry_login, + ) async def __api_request_async(self, actions, priority=False): """Build request to the internal JSON-req API.""" self.__generate_request_id() @@ -273,6 +287,8 @@ async def __api_request_async(self, actions, priority=False): } } + print(payload) + form_data = {"req": json.dumps(payload, separators=(",", ":"))} try: result = await self.__post(api_host, form_data) diff --git a/sagemcom_api/const.py b/sagemcom_api/const.py index f0add30..8a96cc3 100644 --- a/sagemcom_api/const.py +++ b/sagemcom_api/const.py @@ -7,6 +7,7 @@ XMO_ACCESS_RESTRICTION_ERR = "XMO_ACCESS_RESTRICTION_ERR" XMO_AUTHENTICATION_ERR = "XMO_AUTHENTICATION_ERR" +XMO_INVALID_SESSION_ERR = "XMO_INVALID_SESSION_ERR" XMO_NON_WRITABLE_PARAMETER_ERR = "XMO_NON_WRITABLE_PARAMETER_ERR" XMO_NO_ERR = "XMO_NO_ERR" XMO_REQUEST_ACTION_ERR = "XMO_REQUEST_ACTION_ERR" diff --git a/sagemcom_api/exceptions.py b/sagemcom_api/exceptions.py index 3906745..4fb2933 100644 --- a/sagemcom_api/exceptions.py +++ b/sagemcom_api/exceptions.py @@ -1,43 +1,51 @@ """Exceptions for the Sagemcom F@st client.""" +class BaseSagemcomException(Exception): + """Base exception for Sagemcom F@st client.""" + + +# Broad exceptions provided by this library +class BadRequestException(BaseSagemcomException): + """Bad request.""" + + +class UnauthorizedException(BaseSagemcomException): + """Unauthorized.""" + + +class UnknownException(BaseSagemcomException): + """Unknown exception.""" + + # Exceptions provided by SagemCom API -class AccessRestrictionException(Exception): +class AccessRestrictionException(BaseSagemcomException): """Raised when current user has access restrictions.""" -class AuthenticationException(Exception): +class AuthenticationException(UnauthorizedException): """Raised when authentication is not correct.""" -class LoginRetryErrorException(Exception): +class InvalidSessionException(UnauthorizedException): + """Raised when session is invalid.""" + + +class LoginRetryErrorException(BaseSagemcomException): """Raised when too many login retries are attempted.""" -class LoginTimeoutException(Exception): +class LoginTimeoutException(BaseSagemcomException): """Raised when a timeout is encountered during login.""" -class NonWritableParameterException(Exception): +class NonWritableParameterException(BaseSagemcomException): """Raised when provided parameter is not writable.""" -class UnknownPathException(Exception): +class UnknownPathException(BaseSagemcomException): """Raised when provided path does not exist.""" -class MaximumSessionCountException(Exception): +class MaximumSessionCountException(BaseSagemcomException): """Raised when the maximum session count is reached.""" - - -# Broad exceptions provided by this library -class BadRequestException(Exception): - """TODO.""" - - -class UnauthorizedException(Exception): - """TODO.""" - - -class UnknownException(Exception): - """TODO.""" From 013f8263f8d5e80624893ed01a1c8cc41bb648d7 Mon Sep 17 00:00:00 2001 From: Mick Date: Fri, 9 Aug 2024 21:33:01 +0000 Subject: [PATCH 3/5] Fix session_id should be int --- .devcontainer/devcontainer.json | 2 +- sagemcom_api/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index df2a57c..2e12186 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,7 +12,7 @@ // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "poetry config virtualenvs.in-project true && poetry install --with dev --no-interaction && . .venv/bin/activate && pre-commit install", + "postCreateCommand": "poetry config virtualenvs.in-project true && poetry install --with dev --no-interaction && . .venv/bin/activate && pre-commit install && pre-commit install-hooks", // Configure tool-specific properties. "customizations": { "vscode": { diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index d58902c..e7109f0 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -279,7 +279,7 @@ async def __api_request_async(self, actions, priority=False): payload = { "request": { "id": self._request_id, - "session-id": str(self._session_id), + "session-id": int(self._session_id), "priority": priority, "actions": actions, "cnonce": self._current_nonce, From 88a964d883b850a5f8e530440ec974a308c00a58 Mon Sep 17 00:00:00 2001 From: Mick Date: Fri, 9 Aug 2024 21:35:04 +0000 Subject: [PATCH 4/5] remove print --- sagemcom_api/client.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index e7109f0..2d12263 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -287,8 +287,6 @@ async def __api_request_async(self, actions, priority=False): } } - print(payload) - form_data = {"req": json.dumps(payload, separators=(",", ":"))} try: result = await self.__post(api_host, form_data) From 0ebde5fdb20a228dc4f5b41b6413b3910f44e9a4 Mon Sep 17 00:00:00 2001 From: Mick Date: Fri, 9 Aug 2024 21:35:55 +0000 Subject: [PATCH 5/5] Add funding.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8209473 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ["https://paypal.me/imick"]