From 5c663690463a7e7b794a549f67944d6ffe90478c Mon Sep 17 00:00:00 2001 From: mt Date: Wed, 21 Oct 2026 23:03:13 +0100 Subject: [PATCH] Stop encryption method detection when the router locks logins XMO_LOGIN_RETRY_ERR is the router's "too many attempts, login locked" response (the stock web UI shows a lock countdown for it). Detection treated it like a wrong method and kept trying, adding failed logins to an active lock and finally returning None as if no method were supported. It now raises LoginRetryErrorException after the first locked response. --- README.md | 2 ++ sagemcom_api/client.py | 7 ++++-- tests/conftest.py | 6 ++++++ tests/fixtures/login_retry_error.json | 22 +++++++++++++++++++ tests/unit/test_encryption_method.py | 31 +++++++++++++++++++++++++++ 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/login_retry_error.json create mode 100644 tests/unit/test_encryption_method.py diff --git a/README.md b/README.md index 4e0bc9a..fc66845 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,8 @@ If you are not sure which encryption method to use, you can leave it empty or pa This function will throw a `LoginTimeoutException` when no match is found, since this is still a HTTP Time Out. This could caused by the wrong encryption method, but also by trying to connect to an inaccessible host. +It will throw a `LoginRetryErrorException` when the router has locked logins after too many attempts. Wait for the lock to expire before trying again. + ### Handle exceptions Some functions may cause an error when an attempt is made to execute it. These exceptions are thrown by the client and need to be [handled in your Python program](https://docs.python.org/3/tutorial/errors.html#handling-exceptions). Best practice is to catch some specific exceptions and handle them gracefully. diff --git a/sagemcom_api/client.py b/sagemcom_api/client.py index e2c6081..6c10ded 100644 --- a/sagemcom_api/client.py +++ b/sagemcom_api/client.py @@ -364,7 +364,11 @@ async def logout(self): self._request_id = -1 async def get_encryption_method(self): - """Determine which encryption method to use for authentication and set it directly.""" + """Determine which encryption method to use for authentication and set it directly. + + :raises LoginRetryErrorException: the router has locked logins after too many attempts. + Probing further would only extend the lock, so detection stops here. + """ for encryption_method in EncryptionMethod: try: self.authentication_method = encryption_method @@ -380,7 +384,6 @@ async def get_encryption_method(self): except ( LoginTimeoutException, AuthenticationException, - LoginRetryErrorException, ): pass diff --git a/tests/conftest.py b/tests/conftest.py index 5b39653..edf39a9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,6 +40,12 @@ def login_auth_error_response() -> dict[str, Any]: return load_fixture("login_auth_error.json") +@pytest.fixture +def login_retry_error_response() -> dict[str, Any]: + """Mock response for a login locked after too many attempts.""" + return load_fixture("login_retry_error.json") + + @pytest.fixture def login_invalid_session_response() -> dict[str, Any]: """Mock response for invalid session error.""" diff --git a/tests/fixtures/login_retry_error.json b/tests/fixtures/login_retry_error.json new file mode 100644 index 0000000..f0e0697 --- /dev/null +++ b/tests/fixtures/login_retry_error.json @@ -0,0 +1,22 @@ +{ + "reply": { + "uid": 0, + "id": 0, + "error": { + "code": 16777236, + "description": "XMO_REQUEST_ACTION_ERR" + }, + "actions": [ + { + "uid": 1, + "id": 0, + "error": { + "code": 16777224, + "description": "XMO_LOGIN_RETRY_ERR" + }, + "callbacks": [] + } + ], + "events": [] + } +} diff --git a/tests/unit/test_encryption_method.py b/tests/unit/test_encryption_method.py new file mode 100644 index 0000000..32ec1d8 --- /dev/null +++ b/tests/unit/test_encryption_method.py @@ -0,0 +1,31 @@ +"""Tests for automatic encryption method detection.""" + +import pytest + +from sagemcom_api.client import SagemcomClient +from sagemcom_api.enums import EncryptionMethod +from sagemcom_api.exceptions import LoginRetryErrorException + + +@pytest.mark.asyncio +async def test_detection_stops_when_router_locks_login(mock_session_factory, login_retry_error_response): + """XMO_LOGIN_RETRY_ERR means the router has locked logins; probing further only extends the lock.""" + mock_session = mock_session_factory([login_retry_error_response] * 3) + client = SagemcomClient("192.168.1.1", "admin", "admin", session=mock_session) + + with pytest.raises(LoginRetryErrorException): + await client.get_encryption_method() + + assert mock_session.post.call_count == 1 + + +@pytest.mark.asyncio +async def test_detection_moves_on_after_authentication_error(mock_session_factory, login_auth_error_response, login_success_response): + """A plain authentication error still means "wrong method", so the next method is tried.""" + mock_session = mock_session_factory([login_auth_error_response, login_success_response]) + client = SagemcomClient("192.168.1.1", "admin", "admin", session=mock_session) + + method = await client.get_encryption_method() + + assert method == EncryptionMethod.MD5_NONCE + assert mock_session.post.call_count == 2