From 03efbfb4eb8bf437f545b195a375bfa37d16d85f Mon Sep 17 00:00:00 2001 From: Alex Erohin Date: Wed, 6 Nov 2024 11:27:50 +0300 Subject: [PATCH] fix for C5400X --- README.md | 15 +++ setup.py | 2 +- tplinkrouterc6u/__init__.py | 1 + tplinkrouterc6u/client.py | 221 ++++++++++++++++++++--------------- tplinkrouterc6u/exception.py | 4 + 5 files changed, 151 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index 52d6142..209c171 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ from tplinkrouterc6u import ( TplinkRouterProvider, TplinkRouter, TplinkC1200Router, + TplinkC5400XRouter, TPLinkMRClient, TPLinkDecoClient, Connection @@ -31,6 +32,10 @@ router = TplinkRouterProvider.get_client('http://192.168.0.1', 'password') # router = TplinkRouter('http://192.168.0.1', 'password') # You may also pass username if it is different and a logger to log errors as # router = TplinkRouter('http://192.168.0.1','password','admin2', Logger('test')) +# If you have the TP-link C5400X or similar, you can use the TplinkC5400XRouter class instead of the TplinkRouter class. +# Remember that the password for this router is different, here you need to use the web encrypted password. +# To get web encrypted password, read Web Encrypted Password section +# router = TplinkC5400XRouter('http://192.168.0.1','WebEncryptedPassword', Logger('test')) try: router.authorize() # authorizing @@ -60,6 +65,16 @@ finally: The TP-Link Web Interface only supports upto 1 user logged in at a time (for security reasons, apparently). So before action you need to authorize and after logout +### Web Encrypted Password +If you got exception - `use web encrypted password instead. Check the documentation!` +or you have TP-link C5400X or similar router you need to get web encrypted password by these actions: +1. Go to the login page of your router. (default: 192.168.0.1). +2. Type in the password you use to login into the password field. +3. Click somewhere else on the page so that the password field is not selected anymore. +4. Open the JavaScript console of your browser (usually by pressing F12 and then clicking on "Console"). +5. Type `document.getElementById("login-password").value;` +6. Copy the returned value as password and use it. + ## Functions | Function | Args | Description | Return | |---|---|---|---| diff --git a/setup.py b/setup.py index 0e8d805..587f6fc 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="tplinkrouterc6u", - version="5.0.0", + version="5.0.1", author="Alex Erohin", author_email="alexanderErohin@yandex.ru", description="TP-Link Router API", diff --git a/tplinkrouterc6u/__init__.py b/tplinkrouterc6u/__init__.py index 20ce1bd..a61ac04 100644 --- a/tplinkrouterc6u/__init__.py +++ b/tplinkrouterc6u/__init__.py @@ -2,6 +2,7 @@ TplinkRouter, TplinkRouterProvider, TplinkC1200Router, + TplinkC5400XRouter, TPLinkMRClient, AbstractRouter, TPLinkDecoClient, diff --git a/tplinkrouterc6u/client.py b/tplinkrouterc6u/client.py index db0e3db..2c7bda6 100644 --- a/tplinkrouterc6u/client.py +++ b/tplinkrouterc6u/client.py @@ -14,7 +14,7 @@ from tplinkrouterc6u.encryption import EncryptionWrapper, EncryptionWrapperMR from tplinkrouterc6u.package_enum import Connection from tplinkrouterc6u.dataclass import Firmware, Status, Device, IPv4Reservation, IPv4DHCPLease, IPv4Status -from tplinkrouterc6u.exception import ClientException, ClientError +from tplinkrouterc6u.exception import ClientException, ClientError, AuthorizeError from abc import ABC, abstractmethod @@ -108,7 +108,7 @@ def request(self, path: str, data: str, ignore_response: bool = False, ignore_er error = ('TplinkRouter - {} - Response with error; Request {} - Response {}' .format(self.__class__.__name__, path, data)) if not error else error if self._logger: - self._logger.error(error) + self._logger.debug(error) raise ClientError(error) def _is_valid_response(self, data: dict) -> bool: @@ -178,7 +178,7 @@ def authorize(self) -> None: error = ("TplinkRouter - {} - Cannot authorize! Error - {}; Response - {}" .format(self.__class__.__name__, e, data)) if self._logger: - self._logger.error(error) + self._logger.debug(error) raise ClientException(error) def _request_pwd(self) -> None: @@ -203,7 +203,7 @@ def _request_pwd(self) -> None: error = ('TplinkRouter - {} - Unknown error for pwd! Error - {}; Response - {}' .format(self.__class__.__name__, e, response.text)) if self._logger: - self._logger.error(error) + self._logger.debug(error) raise ClientException(error) def _request_seq(self) -> None: @@ -230,7 +230,7 @@ def _request_seq(self) -> None: error = ('TplinkRouter - {} - Unknown error for seq! Error - {}; Response - {}' .format(self.__class__.__name__, e, response.text)) if self._logger: - self._logger.error(error) + self._logger.debug(error) raise ClientException(error) def _try_login(self) -> Response: @@ -658,7 +658,7 @@ def supports(self) -> bool: return False if response.status_code == 401 and response.text.startswith('00'): raise ClientException(('Your router is not supported. Please add your router support to ' - 'https://github.com/AlexandrErohin/TP-Link-Archer-C6U' + 'https://github.com/AlexandrErohin/TP-Link-Archer-C6U ' 'by implementing methods for TplinkC6V4Router class' )) return False @@ -682,7 +682,111 @@ def set_wifi(self, wifi: Connection, enable: bool) -> None: raise ClientException('Not Implemented') -class TplinkC1200Router(TplinkBaseRouter): +class TplinkC5400XRouter(TplinkBaseRouter): + def supports(self) -> bool: + return len(self.password) >= 200 + + def authorize(self) -> None: + if len(self.password) < 200: + raise Exception('You need to use web encrypted password instead. Check the documentation!') + + url = '{}/cgi-bin/luci/;stok=/login?form=login'.format(self.host) + + response = post( + url, + params={'operation': 'login', 'username': self.username, 'password': self.password}, + headers=self._headers_login, + timeout=self.timeout, + verify=self._verify_ssl, + ) + + try: + self._stok = response.json().get('data').get('stok') + regex_result = search('sysauth=(.*);', response.headers['set-cookie']) + self._sysauth = regex_result.group(1) + self._logged = True + self._smart_network = False + + except Exception as e: + error = "TplinkRouter - C5400X - Cannot authorize! Error - {}; Response - {}".format(e, response.text) + if self._logger: + self._logger.debug(error) + raise ClientException(error) + + def set_led(self, enable: bool) -> None: + current_state = (self.request('admin/ledgeneral?form=setting&operation=read', 'operation=read') + .get('enable', 'off') == 'on') + if current_state != enable: + self.request('admin/ledgeneral?form=setting&operation=write', 'operation=write') + + def get_led(self) -> bool: + + data = self.request('admin/ledgeneral?form=setting&operation=read', 'operation=read') + led_status = data.get('enable') if 'enable' in data else None + if led_status == 'on': + return True + elif led_status == 'off': + return False + else: + return None + + def set_wifi(self, wifi: Connection, enable: bool = None, ssid: str = None, hidden: str = None, + encryption: str = None, psk_version: str = None, psk_cipher: str = None, psk_key: str = None, + hwmode: str = None, htmode: str = None, channel: int = None, txpower: str = None, + disabled_all: str = None) -> None: + values = { + Connection.HOST_2G: 'wireless_2g', + Connection.HOST_5G: 'wireless_5g', + Connection.HOST_6G: 'wireless_6g', + Connection.GUEST_2G: 'guest_2g', + Connection.GUEST_5G: 'guest_5g', + Connection.GUEST_6G: 'guest_6g', + Connection.IOT_2G: 'iot_2g', + Connection.IOT_5G: 'iot_5g', + Connection.IOT_6G: 'iot_6g', + } + + value = values.get(wifi) + if not value: + raise ValueError(f"Invalid Wi-Fi connection type: {wifi}") + + if all(v is None for v in [enable, ssid, hidden, encryption, psk_version, psk_cipher, psk_key, hwmode, + htmode, channel, txpower, disabled_all]): + raise ValueError("At least one wireless setting must be provided") + + data = "operation=write" + + if enable is not None: + data += f"&enable={'on' if enable else 'off'}" + if ssid is not None: + data += f"&ssid={ssid}" + if hidden is not None: + data += f"&hidden={hidden}" + if encryption is not None: + data += f"&encryption={encryption}" + if psk_version is not None: + data += f"&psk_version={psk_version}" + if psk_cipher is not None: + data += f"&psk_cipher={psk_cipher}" + if psk_key is not None: + data += f"&psk_key={psk_key}" + if hwmode is not None: + data += f"&hwmode={hwmode}" + if htmode is not None: + data += f"&htmode={htmode}" + if channel is not None: + data += f"&channel={channel}" + if txpower is not None: + data += f"&txpower={txpower}" + if disabled_all is not None: + data += f"&disabled_all={disabled_all}" + + path = f"admin/wireless?form={value}&{data}" + + self.request(path, data) + + +class TplinkC1200Router(TplinkC5400XRouter): username = '' password = '' _pwdNN = '' @@ -729,10 +833,11 @@ def authorize(self) -> None: self._logged = True except Exception as e: - error = ("TplinkRouter - C1200 - {} - Cannot authorize! Error - {}; Response - {}" - .format(self.__class__.__name__, e, data)) + error = ("TplinkRouter - C1200 - Cannot authorize! Error - {}; Response - {}".format(e, data)) if self._logger: - self._logger.error(error) + self._logger.debug(error) + if 'data' in vars() and data.get('errorcode') == 'login failed': + raise AuthorizeError(error) raise ClientException(error) def _request_pwd(self) -> None: @@ -755,7 +860,7 @@ def _request_pwd(self) -> None: error = ('TplinkRouter - C1200 - {} - Unknown error for pwd! Error - {}; Response - {}' .format(self.__class__.__name__, e, response.text)) if self._logger: - self._logger.error(error) + self._logger.debug(error) raise ClientException(error) def _try_login(self) -> Response: @@ -777,78 +882,6 @@ def _try_login(self) -> Response: def _get_login_data(crypted_pwd: str) -> str: return 'operation=login&password={}'.format(crypted_pwd) - def set_led(self, enable: bool) -> None: - current_state = (self.request('admin/ledgeneral?form=setting&operation=read', 'operation=read') - .get('enable', 'off') == 'on') - if current_state != enable: - self.request('admin/ledgeneral?form=setting&operation=write', 'operation=write') - - def get_led(self) -> bool: - - data = self.request('admin/ledgeneral?form=setting&operation=read', 'operation=read') - led_status = data.get('enable') if 'enable' in data else None - if led_status == 'on': - return True - elif led_status == 'off': - return False - else: - return None - - def set_wifi(self, wifi: Connection, enable: bool = None, ssid: str = None, hidden: str = None, - encryption: str = None, psk_version: str = None, psk_cipher: str = None, psk_key: str = None, - hwmode: str = None, htmode: str = None, channel: int = None, txpower: str = None, - disabled_all: str = None) -> None: - values = { - Connection.HOST_2G: 'wireless_2g', - Connection.HOST_5G: 'wireless_5g', - Connection.HOST_6G: 'wireless_6g', - Connection.GUEST_2G: 'guest_2g', - Connection.GUEST_5G: 'guest_5g', - Connection.GUEST_6G: 'guest_6g', - Connection.IOT_2G: 'iot_2g', - Connection.IOT_5G: 'iot_5g', - Connection.IOT_6G: 'iot_6g', - } - - value = values.get(wifi) - if not value: - raise ValueError(f"Invalid Wi-Fi connection type: {wifi}") - - if all(v is None for v in [enable, ssid, hidden, encryption, psk_version, psk_cipher, psk_key, hwmode, - htmode, channel, txpower, disabled_all]): - raise ValueError("At least one wireless setting must be provided") - - data = "operation=write" - - if enable is not None: - data += f"&enable={'on' if enable else 'off'}" - if ssid is not None: - data += f"&ssid={ssid}" - if hidden is not None: - data += f"&hidden={hidden}" - if encryption is not None: - data += f"&encryption={encryption}" - if psk_version is not None: - data += f"&psk_version={psk_version}" - if psk_cipher is not None: - data += f"&psk_cipher={psk_cipher}" - if psk_key is not None: - data += f"&psk_key={psk_key}" - if hwmode is not None: - data += f"&hwmode={hwmode}" - if htmode is not None: - data += f"&htmode={htmode}" - if channel is not None: - data += f"&channel={channel}" - if txpower is not None: - data += f"&txpower={txpower}" - if disabled_all is not None: - data += f"&disabled_all={disabled_all}" - - path = f"admin/wireless?form={value}&{data}" - - self.request(path, data) - class TPLinkMRClient(AbstractRouter): REQUEST_RETRIES = 3 @@ -1168,7 +1201,7 @@ def req_act(self, acts: list): if code != 200: error = 'TplinkRouter - MR - Response with error; Request {} - Response {}'.format(data, response) if self._logger: - self._logger.error(error) + self._logger.debug(error) raise ClientError(error) result = self._merge_response(response) @@ -1319,7 +1352,7 @@ def _req_login(self) -> None: if error: if self._logger: - self._logger.error(error) + self._logger.debug(error) raise ClientException(error) def _request(self, url, method='POST', data_str=None, encrypt=False): @@ -1398,13 +1431,19 @@ def _prepare_data(self, data: str, is_login: bool) -> tuple[str, str]: class TplinkRouterProvider: @staticmethod def get_client(host: str, password: str, username: str = 'admin', logger: Logger = None, - verify_ssl: bool = True, timeout: int = 30) -> AbstractRouter | None: - for client in [TPLinkMRClient, TplinkC6V4Router, TPLinkDecoClient, TplinkRouter, TplinkC1200Router]: + verify_ssl: bool = True, timeout: int = 30) -> AbstractRouter: + for client in [TplinkC5400XRouter, TPLinkMRClient, TplinkC6V4Router, TPLinkDecoClient, TplinkRouter]: router = client(host, password, username, logger, verify_ssl, timeout) if router.supports(): return router - raise ClientException(('Your router is not supported. Please add your router support to ' - 'https://github.com/AlexandrErohin/TP-Link-Archer-C6U' - 'by implementing methods for AbstractRouter class' - )) + router = TplinkC1200Router(host, password, username, logger, verify_ssl, timeout) + try: + router.authorize() + except AuthorizeError as e: + logger.error(e.__str__()) + raise ClientException(('Login failed! Please check if your router local password is correct or ' + 'try to use web encrypted password instead. Check the documentation!' + )) + + raise ClientException('You need to use web encrypted password instead. Check the documentation!') diff --git a/tplinkrouterc6u/exception.py b/tplinkrouterc6u/exception.py index 9fff529..8d77649 100644 --- a/tplinkrouterc6u/exception.py +++ b/tplinkrouterc6u/exception.py @@ -4,3 +4,7 @@ class ClientException(Exception): class ClientError(ClientException): pass + + +class AuthorizeError(ClientException): + pass