| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| Expand Up | @@ -8,7 +8,6 @@ | |
| import logging | ||
| import secrets | ||
| import ssl | ||
| import time | ||
| from enum import Enum, auto | ||
| from typing import TYPE_CHECKING, Any, Dict, cast | ||
|
|
||
| Expand All | @@ -29,7 +28,7 @@ | |
| from ..httpclient import HttpClient | ||
| from ..json import dumps as json_dumps | ||
| from ..json import loads as json_loads | ||
| from ..protocol import BaseTransport | ||
| from ..protocol import DEFAULT_CREDENTIALS, BaseTransport, get_default_credentials | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
| Expand Down Expand Up | @@ -71,7 +70,6 @@ class SslAesTransport(BaseTransport): | |
| "Accept": "application/json", | ||
| "Accept-Encoding": "gzip, deflate", | ||
| "User-Agent": "Tapo CameraClient Android", | ||
| "Connection": "close", | ||
| } | ||
| CIPHERS = ":".join( | ||
| [ | ||
| Expand All | @@ -96,7 +94,9 @@ def __init__( | |
| not self._credentials or self._credentials.username is None | ||
| ) and not self._credentials_hash: | ||
| self._credentials = Credentials() | ||
| self._default_credentials: Credentials | None = None | ||
| self._default_credentials: Credentials = get_default_credentials( | ||
| DEFAULT_CREDENTIALS["TAPOCAMERA"] | ||
| ) | ||
|
|
||
| if not config.timeout: | ||
| config.timeout = self.DEFAULT_TIMEOUT | ||
| Expand Down Expand Up | @@ -149,7 +149,7 @@ def credentials_hash(self) -> str | None: | |
| return base64.b64encode(json_dumps(ch).encode()).decode() | ||
| return None | ||
|
|
||
| def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None: | ||
| def _get_response_error(self, resp_dict: Any) -> SmartErrorCode: | ||
| error_code_raw = resp_dict.get("error_code") | ||
| try: | ||
| error_code = SmartErrorCode.from_int(error_code_raw) | ||
| Expand All | @@ -158,6 +158,10 @@ def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None: | |
| "Device %s received unknown error code: %s", self._host, error_code_raw | ||
| ) | ||
| error_code = SmartErrorCode.INTERNAL_UNKNOWN_ERROR | ||
| return error_code | ||
|
|
||
| def _handle_response_error_code(self, resp_dict: Any, msg: str) -> None: | ||
| error_code = self._get_response_error(resp_dict) | ||
| if error_code is SmartErrorCode.SUCCESS: | ||
| return | ||
| msg = f"{msg}: {self._host}: {error_code.name}({error_code.value})" | ||
| Expand Down Expand Up | @@ -325,6 +329,8 @@ async def perform_handshake2(self, local_nonce, server_nonce, pwd_hash) -> None: | |
| + f"status code {status_code} to handshake2" | ||
| ) | ||
| resp_dict = cast(dict, resp_dict) | ||
| self._handle_response_error_code(resp_dict, "Error in handshake2") | ||
|
|
||
| self._seq = resp_dict["result"]["start_seq"] | ||
| stok = resp_dict["result"]["stok"] | ||
| self._token_url = URL(f"{str(self._app_url)}/stok={stok}/ds") | ||
| Expand All | @@ -337,42 +343,41 @@ async def perform_handshake2(self, local_nonce, server_nonce, pwd_hash) -> None: | |
| _LOGGER.debug("Handshake2 complete ...") | ||
|
|
||
| async def perform_handshake1(self) -> tuple[str, str, str]: | ||
| """Perform the handshake.""" | ||
| _LOGGER.debug("Will perform handshaking...") | ||
|
|
||
| if not self._username: | ||
| raise KasaException("Cannot connect to device with no credentials") | ||
| local_nonce = secrets.token_bytes(8).hex().upper() | ||
| # Device needs the content length or it will response with 500 | ||
| body = { | ||
| "method": "login", | ||
| "params": { | ||
| "cnonce": local_nonce, | ||
| "encrypt_type": "3", | ||
| "username": self._username, | ||
| }, | ||
| } | ||
| http_client = self._http_client | ||
| """Perform the handshake1.""" | ||
| resp_dict = None | ||
| if self._username: | ||
| local_nonce = secrets.token_bytes(8).hex().upper() | ||
| resp_dict = await self.try_send_handshake1(self._username, local_nonce) | ||
|
|
||
| status_code, resp_dict = await http_client.post( | ||
| self._app_url, | ||
| json=body, | ||
| headers=self._headers, | ||
| ssl=await self._get_ssl_context(), | ||
| ) | ||
|
|
||
| _LOGGER.debug("Device responded with: %s", resp_dict) | ||
|
|
||
| if status_code != 200: | ||
| raise KasaException( | ||
| f"{self._host} responded with an unexpected " | ||
| + f"status code {status_code} to handshake1" | ||
| # Try the default username. If it fails raise the original error_code | ||
| if ( | ||
| not resp_dict | ||
| or (error_code := self._get_response_error(resp_dict)) | ||
| is not SmartErrorCode.INVALID_NONCE | ||
| or "nonce" not in resp_dict["result"].get("data", {}) | ||
| ): | ||
| local_nonce = secrets.token_bytes(8).hex().upper() | ||
| default_resp_dict = await self.try_send_handshake1( | ||
| self._default_credentials.username, local_nonce | ||
| ) | ||
| if ( | ||
| default_error_code := self._get_response_error(default_resp_dict) | ||
| ) is SmartErrorCode.INVALID_NONCE and "nonce" in default_resp_dict[ | ||
| "result" | ||
| ].get("data", {}): | ||
| _LOGGER.debug("Connected to {self._host} with default username") | ||
| self._username = self._default_credentials.username | ||
| error_code = default_error_code | ||
| resp_dict = default_resp_dict | ||
|
|
||
| resp_dict = cast(dict, resp_dict) | ||
| error_code = SmartErrorCode.from_int(resp_dict["error_code"]) | ||
| if error_code != SmartErrorCode.INVALID_NONCE: | ||
| self._handle_response_error_code(resp_dict, "Unable to complete handshake") | ||
| if not self._username: | ||
| raise AuthenticationError( | ||
| "Credentials must be supplied to connect to {self._host}" | ||
| ) | ||
| if error_code is not SmartErrorCode.INVALID_NONCE or ( | ||
| resp_dict and "nonce" not in resp_dict["result"].get("data", {}) | ||
| ): | ||
| raise AuthenticationError("Error trying handshake1: {resp_dict}") | ||
|
|
||
| if TYPE_CHECKING: | ||
| resp_dict = cast(Dict[str, Any], resp_dict) | ||
| Expand All | @@ -381,10 +386,10 @@ async def perform_handshake1(self) -> tuple[str, str, str]: | |
| device_confirm = resp_dict["result"]["data"]["device_confirm"] | ||
| if self._credentials and self._credentials != Credentials(): | ||
| pwd_hash = _sha256_hash(self._credentials.password.encode()) | ||
| elif self._username and self._password: | ||
| pwd_hash = _sha256_hash(self._password.encode()) | ||
| else: | ||
| if TYPE_CHECKING: | ||
| assert self._pwd_hash | ||
| pwd_hash = self._pwd_hash | ||
| pwd_hash = _sha256_hash(self._default_credentials.password.encode()) | ||
|
|
||
| expected_confirm_sha256 = self.generate_confirm_hash( | ||
| local_nonce, server_nonce, pwd_hash | ||
| Expand All | @@ -408,19 +413,40 @@ async def perform_handshake1(self) -> tuple[str, str, str]: | |
| _LOGGER.debug(msg) | ||
| raise AuthenticationError(msg) | ||
|
|
||
| def _handshake_session_expired(self): | ||
| """Return true if session has expired.""" | ||
| return ( | ||
| self._session_expire_at is None | ||
| or self._session_expire_at - time.time() <= 0 | ||
| async def try_send_handshake1(self, username: str, local_nonce: str) -> dict: | ||
| """Perform the handshake.""" | ||
| _LOGGER.debug("Will to send handshake1...") | ||
|
|
||
| body = { | ||
| "method": "login", | ||
| "params": { | ||
|
Comment thread
Copy link
Copy Markdown
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityProbably not worth assigning if this is used only once?
Sorry, something went wrong.
All reactions
|
||
| "cnonce": local_nonce, | ||
| "encrypt_type": "3", | ||
| "username": self._username, | ||
| }, | ||
| } | ||
| http_client = self._http_client | ||
|
|
||
| status_code, resp_dict = await http_client.post( | ||
| self._app_url, | ||
| json=body, | ||
| headers=self._headers, | ||
| ssl=await self._get_ssl_context(), | ||
| ) | ||
|
|
||
| _LOGGER.debug("Device responded with: %s", resp_dict) | ||
|
|
||
| if status_code != 200: | ||
| raise KasaException( | ||
| f"{self._host} responded with an unexpected " | ||
| + f"status code {status_code} to handshake1" | ||
| ) | ||
|
|
||
| return cast(dict, resp_dict) | ||
|
|
||
| async def send(self, request: str) -> dict[str, Any]: | ||
| """Send the request.""" | ||
| if ( | ||
| self._state is TransportState.HANDSHAKE_REQUIRED | ||
| or self._handshake_session_expired() | ||
|
Comment thread
rytilahti marked this conversation as resolved.
|
||
| ): | ||
| if self._state is TransportState.HANDSHAKE_REQUIRED: | ||
| await self.perform_handshake() | ||
|
|
||
| return await self.send_secure_passthrough(request) | ||
| Expand Down | ||
| Back | FazBrowse Home | New Git URL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low QualityWould it be better to invert the check, and bail out early on different errors?
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.