From 46089158bdfb49c69770a08da2c9756b11619fb2 Mon Sep 17 00:00:00 2001 From: myhomeiot Date: Sat, 27 Apr 2024 09:01:05 +0300 Subject: [PATCH] feat: support for vodafone sercomm h300s Tested with: Firmware: Vodafone-H-300s-v1.2.02.03 and Vodafone-H-300s-v1.2.02.05 Hardware: V2 --- src/aiovodafone/api.py | 106 +++++++++++++++++++++++++++------------ src/aiovodafone/const.py | 2 + 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/src/aiovodafone/api.py b/src/aiovodafone/api.py index ba2f518..974a206 100644 --- a/src/aiovodafone/api.py +++ b/src/aiovodafone/api.py @@ -81,18 +81,26 @@ async def get_device_type( response_json = await response.json() if "data" in response_json and "ModelName" in response_json["data"]: return DeviceType.TECHNICOLOR - async with session.get( - f"https://{host}/login.html", - headers=HEADERS, - ssl=False, - ) as response: - # To identify the Sercomm devices before the login - # There's no other sure way to identify a Sercomm device without login - if ( - response.status == HTTPStatus.OK - and "var csrf_token = " in await response.text() - ): - return DeviceType.SERCOMM + + for protocol in ["https", "http"]: + try: + async with session.get( + f"{protocol}://{host}/login.html", + headers=HEADERS, + ssl=False, + ) as response: + # To identify the Sercomm devices before the login + # There's no other sure way to identify a Sercomm device + # without login + if ( + response.status == HTTPStatus.OK + and "var csrf_token = " in await response.text() + ): + return DeviceType.SERCOMM + except aiohttp.client_exceptions.ClientConnectorSSLError: + _LOGGER.debug("Unable to login using protocol %s", protocol) + continue + return None def __init__(self, host: str, username: str, password: str) -> None: @@ -431,6 +439,19 @@ async def _encrypt_string(self, credential: str) -> str: digestmod=hashlib.sha256, ).hexdigest() + async def _encrypt_with_challenge(self, challenge: str) -> str: + """Encrypt password with challenge for login.""" + return hashlib.sha256( + bytes(self.password + challenge, "utf-8"), + ).hexdigest() + + async def _get_challenge(self) -> str: + """Return challenge or login.""" + return_dict = await self._get_sercomm_page("/data/login.json") + challenge: str = return_dict["challenge"] + _LOGGER.debug("challenge: <%s>", challenge) + return challenge + async def _reset(self) -> bool: """Reset page content before loading.""" payload = {"chk_sys_busy": ""} @@ -440,22 +461,23 @@ async def _reset(self) -> bool: return False - async def _login_json(self, username: str, password: str) -> bool: + async def _login_json(self, payload: dict[str, Any]) -> bool: """Login via json page.""" - payload = { - "LoginName": username, - "LoginPWD": password, - } reply_json = await self._post_sercomm_page("/data/login.json", payload) - _LOGGER.debug("Login result: %s[%s]", LOGIN[int(str(reply_json))], reply_json) + reply_str = str(reply_json) + _LOGGER.debug( + "Login result: %s[%s]", + LOGIN[int(reply_str)] if 0 <= int(reply_str) < len(LOGIN) else "unknown", + reply_json, + ) - if reply_json == "1": + if reply_str == "1": return True - if reply_json == "2": + if reply_str == "2": raise AlreadyLogged - if reply_json in ["3", "4", "5"]: + if reply_str in ["3", "4", "5", "7"]: raise CannotAuthenticate raise GenericLoginError @@ -553,20 +575,38 @@ async def login(self) -> bool: await self._set_cookie() await self._reset() - # First try with both username and password encrypted - # Second try with plain username and password encrypted - try: - _LOGGER.debug("Login first try: username[encrypted], password[encrypted]") - logged = await self._login_json( - await self._encrypt_string(self.username), - await self._encrypt_string(self.password), - ) - except CannotAuthenticate: - _LOGGER.debug("Login second try: username[plain], password[encrypted]") + if not self.encryption_key: + _LOGGER.debug("Login: username[plain], password[challenge encrypted]") + + challenge = await self._get_challenge() logged = await self._login_json( - self.username, - await self._encrypt_string(self.password), + { + "LoginName": self.username, + "LoginPWD": await self._encrypt_with_challenge(challenge), + "challenge": challenge, + }, ) + else: + # First try with both username and password encrypted + # Second try with plain username and password encrypted + try: + _LOGGER.debug( + "Login first try: username[encrypted], password[encrypted]", + ) + logged = await self._login_json( + { + "LoginName": await self._encrypt_string(self.username), + "LoginPWD": await self._encrypt_string(self.password), + }, + ) + except CannotAuthenticate: + _LOGGER.debug("Login second try: username[plain], password[encrypted]") + logged = await self._login_json( + { + "LoginName": self.username, + "LoginPWD": await self._encrypt_string(self.password), + }, + ) return logged diff --git a/src/aiovodafone/const.py b/src/aiovodafone/const.py index a04f861..51ef5f3 100644 --- a/src/aiovodafone/const.py +++ b/src/aiovodafone/const.py @@ -20,6 +20,8 @@ "credential error", "credential error", "password mismatch", + "incorrect challenge", + "password mismatch", ] USER_ALREADY_LOGGED_IN = "MSG_LOGIN_150"