From 3f48c080489466041b73af1050d6afe8e98d17bb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 21:57:47 -1000 Subject: [PATCH 1/8] Renew the KLAP handshake session 20 minutes before we think it will expire Currently we assumed the clocks were perfectly aligned and the handshake session lasted 20 hours. We now add a 20 minute buffer --- kasa/klaptransport.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/kasa/klaptransport.py b/kasa/klaptransport.py index c678e4483..738ff680e 100644 --- a/kasa/klaptransport.py +++ b/kasa/klaptransport.py @@ -63,6 +63,10 @@ _LOGGER = logging.getLogger(__name__) +ON_DAY_SECONDS = 86400 +SESSION_EXPIRE_BUFFER_SECONDS = 60 * 20 + + def _sha256(payload: bytes) -> bytes: digest = hashes.Hash(hashes.SHA256()) # noqa: S303 digest.update(payload) @@ -278,7 +282,12 @@ async def perform_handshake(self) -> Any: # The device returns a TIMEOUT cookie on handshake1 which # it doesn't like to get back so we store the one we want - self._session_expire_at = time.time() + 86400 + # There is a 24 hour timeout on the session cookie + # but the clock on the device is not always accurate + # so we set the expiry to 24 hours from now minus a buffer + self._session_expire_at = ( + time.time() + ON_DAY_SECONDS - SESSION_EXPIRE_BUFFER_SECONDS + ) self._encryption_session = await self.perform_handshake2( local_seed, remote_seed, auth_hash ) From 7c1450136068494aaf1c0b5b5c67d9713ca67d76 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 21:58:52 -1000 Subject: [PATCH 2/8] Renew the KLAP handshake session 20 minutes before we think it will expire Currently we assumed the clocks were perfectly aligned and the handshake session lasted 20 hours. We now add a 20 minute buffer --- kasa/klaptransport.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kasa/klaptransport.py b/kasa/klaptransport.py index 738ff680e..4e8db4f47 100644 --- a/kasa/klaptransport.py +++ b/kasa/klaptransport.py @@ -63,7 +63,7 @@ _LOGGER = logging.getLogger(__name__) -ON_DAY_SECONDS = 86400 +ONE_DAY_SECONDS = 86400 SESSION_EXPIRE_BUFFER_SECONDS = 60 * 20 @@ -286,7 +286,7 @@ async def perform_handshake(self) -> Any: # but the clock on the device is not always accurate # so we set the expiry to 24 hours from now minus a buffer self._session_expire_at = ( - time.time() + ON_DAY_SECONDS - SESSION_EXPIRE_BUFFER_SECONDS + time.time() + ONE_DAY_SECONDS - SESSION_EXPIRE_BUFFER_SECONDS ) self._encryption_session = await self.perform_handshake2( local_seed, remote_seed, auth_hash From f99661842d49de607b76c1ac58f6f3ffb1699e03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 22:00:16 -1000 Subject: [PATCH 3/8] Renew the KLAP handshake session 20 minutes before we think it will expire Currently we assumed the clocks were perfectly aligned and the handshake session lasted 20 hours. We now add a 20 minute buffer --- kasa/aestransport.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/kasa/aestransport.py b/kasa/aestransport.py index 73d02b0ee..6f3c0bdbd 100644 --- a/kasa/aestransport.py +++ b/kasa/aestransport.py @@ -35,6 +35,11 @@ _LOGGER = logging.getLogger(__name__) +ONE_DAY_SECONDS = 86400 +SESSION_EXPIRE_BUFFER_SECONDS = 60 * 20 + + + def _sha1(payload: bytes) -> str: sha1_algo = hashlib.sha1() # noqa: S324 sha1_algo.update(payload) @@ -281,7 +286,12 @@ async def perform_handshake(self): ): self._session_cookie = {self.SESSION_COOKIE_NAME: cookie} - self._session_expire_at = time.time() + 86400 + # There is a 24 hour timeout on the session cookie + # but the clock on the device is not always accurate + # so we set the expiry to 24 hours from now minus a buffer + self._session_expire_at = ( + time.time() + ONE_DAY_SECONDS - SESSION_EXPIRE_BUFFER_SECONDS + ) if TYPE_CHECKING: assert self._key_pair is not None # pragma: no cover self._encryption_session = AesEncyptionSession.create_from_keypair( From 03fe3d326cda0feb95b0c1453112d9b10c982c16 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 22:00:24 -1000 Subject: [PATCH 4/8] Renew the KLAP handshake session 20 minutes before we think it will expire Currently we assumed the clocks were perfectly aligned and the handshake session lasted 20 hours. We now add a 20 minute buffer --- kasa/aestransport.py | 5 +---- kasa/modules/emeter.py | 2 +- kasa/smartdevice.py | 4 +++- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/kasa/aestransport.py b/kasa/aestransport.py index 6f3c0bdbd..ebbb716f0 100644 --- a/kasa/aestransport.py +++ b/kasa/aestransport.py @@ -39,7 +39,6 @@ SESSION_EXPIRE_BUFFER_SECONDS = 60 * 20 - def _sha1(payload: bytes) -> str: sha1_algo = hashlib.sha1() # noqa: S324 sha1_algo.update(payload) @@ -280,9 +279,7 @@ async def perform_handshake(self): self.SESSION_COOKIE_NAME ) ) or ( - cookie := self._http_client.get_cookie( # type: ignore - "SESSIONID" - ) + cookie := self._http_client.get_cookie("SESSIONID") # type: ignore ): self._session_cookie = {self.SESSION_COOKIE_NAME: cookie} diff --git a/kasa/modules/emeter.py b/kasa/modules/emeter.py index a205396ed..11eed48f8 100644 --- a/kasa/modules/emeter.py +++ b/kasa/modules/emeter.py @@ -63,7 +63,7 @@ def _convert_stat_data( self, data: List[Dict[str, Union[int, float]]], entry_key: str, - kwh: bool=True, + kwh: bool = True, key: Optional[int] = None, ) -> Dict[Union[int, float], Union[int, float]]: """Return emeter information keyed with the day/month. diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index 31418afcc..c007165d2 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -694,7 +694,9 @@ async def _scan(target): return [WifiNetwork(**x) for x in info["ap_list"]] - async def wifi_join(self, ssid: str, password: str, keytype: str = "3"): # noqa: D202 + async def wifi_join( + self, ssid: str, password: str, keytype: str = "3" + ): # noqa: D202 """Join the given wifi network. If joining the network fails, the device will return to AP mode after a while. From d1ea3aee86aea7908501e799890410c116360f7c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 22:00:50 -1000 Subject: [PATCH 5/8] Renew the KLAP handshake session 20 minutes before we think it will expire Currently we assumed the clocks were perfectly aligned and the handshake session lasted 20 hours. We now add a 20 minute buffer --- kasa/modules/emeter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kasa/modules/emeter.py b/kasa/modules/emeter.py index 11eed48f8..a205396ed 100644 --- a/kasa/modules/emeter.py +++ b/kasa/modules/emeter.py @@ -63,7 +63,7 @@ def _convert_stat_data( self, data: List[Dict[str, Union[int, float]]], entry_key: str, - kwh: bool = True, + kwh: bool=True, key: Optional[int] = None, ) -> Dict[Union[int, float], Union[int, float]]: """Return emeter information keyed with the day/month. From 665a5fc590457baac924518900493aba2ca54bb1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 22:01:07 -1000 Subject: [PATCH 6/8] Renew the KLAP handshake session 20 minutes before we think it will expire Currently we assumed the clocks were perfectly aligned and the handshake session lasted 20 hours. We now add a 20 minute buffer --- kasa/smartdevice.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/kasa/smartdevice.py b/kasa/smartdevice.py index c007165d2..31418afcc 100755 --- a/kasa/smartdevice.py +++ b/kasa/smartdevice.py @@ -694,9 +694,7 @@ async def _scan(target): return [WifiNetwork(**x) for x in info["ap_list"]] - async def wifi_join( - self, ssid: str, password: str, keytype: str = "3" - ): # noqa: D202 + async def wifi_join(self, ssid: str, password: str, keytype: str = "3"): # noqa: D202 """Join the given wifi network. If joining the network fails, the device will return to AP mode after a while. From df8ca80c8b0034f10abb136ab4aa33d7e3596c70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 22:38:24 -1000 Subject: [PATCH 7/8] use timeout cookie when available --- kasa/aestransport.py | 8 +++++--- kasa/klaptransport.py | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/kasa/aestransport.py b/kasa/aestransport.py index ebbb716f0..9fd74e1bf 100644 --- a/kasa/aestransport.py +++ b/kasa/aestransport.py @@ -54,6 +54,7 @@ class AesTransport(BaseTransport): DEFAULT_PORT: int = 80 SESSION_COOKIE_NAME = "TP_SESSIONID" + TIMEOUT_COOKIE_NAME = "TIMEOUT" COMMON_HEADERS = { "Content-Type": "application/json", "requestByApp": "true", @@ -283,12 +284,13 @@ async def perform_handshake(self): ): self._session_cookie = {self.SESSION_COOKIE_NAME: cookie} + timeout = ( + self._http_client.get_cookie(self.TIMEOUT_COOKIE_NAME) or ONE_DAY_SECONDS + ) # There is a 24 hour timeout on the session cookie # but the clock on the device is not always accurate # so we set the expiry to 24 hours from now minus a buffer - self._session_expire_at = ( - time.time() + ONE_DAY_SECONDS - SESSION_EXPIRE_BUFFER_SECONDS - ) + self._session_expire_at = time.time() + timeout - SESSION_EXPIRE_BUFFER_SECONDS if TYPE_CHECKING: assert self._key_pair is not None # pragma: no cover self._encryption_session = AesEncyptionSession.create_from_keypair( diff --git a/kasa/klaptransport.py b/kasa/klaptransport.py index 4e8db4f47..e8fed56bf 100644 --- a/kasa/klaptransport.py +++ b/kasa/klaptransport.py @@ -90,6 +90,7 @@ class KlapTransport(BaseTransport): DEFAULT_PORT: int = 80 DISCOVERY_QUERY = {"system": {"get_sysinfo": None}} SESSION_COOKIE_NAME = "TP_SESSIONID" + TIMEOUT_COOKIE_NAME = "TIMEOUT" def __init__( self, @@ -282,12 +283,13 @@ async def perform_handshake(self) -> Any: # The device returns a TIMEOUT cookie on handshake1 which # it doesn't like to get back so we store the one we want + timeout = int( + self._http_client.get_cookie(self.TIMEOUT_COOKIE_NAME) or ONE_DAY_SECONDS + ) # There is a 24 hour timeout on the session cookie # but the clock on the device is not always accurate # so we set the expiry to 24 hours from now minus a buffer - self._session_expire_at = ( - time.time() + ONE_DAY_SECONDS - SESSION_EXPIRE_BUFFER_SECONDS - ) + self._session_expire_at = time.time() + timeout - SESSION_EXPIRE_BUFFER_SECONDS self._encryption_session = await self.perform_handshake2( local_seed, remote_seed, auth_hash ) From c6a3d6c64bb766598ae22b7c64c83259c659408a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 23 Jan 2024 22:40:42 -1000 Subject: [PATCH 8/8] use timeout cookie when available --- kasa/aestransport.py | 13 +++++++------ kasa/klaptransport.py | 8 +++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/kasa/aestransport.py b/kasa/aestransport.py index 9fd74e1bf..765137d4b 100644 --- a/kasa/aestransport.py +++ b/kasa/aestransport.py @@ -256,7 +256,9 @@ async def perform_handshake(self): **self.COMMON_HEADERS, self.CONTENT_LENGTH: str(self.KEY_PAIR_CONTENT_LENGTH), } - status_code, resp_dict = await self._http_client.post( + http_client = self._http_client + + status_code, resp_dict = await http_client.post( url, json=self._generate_key_pair_payload(), headers=headers, @@ -272,20 +274,19 @@ async def perform_handshake(self): ) self._handle_response_error_code(resp_dict, "Unable to complete handshake") - handshake_key = resp_dict["result"]["key"] if ( - cookie := self._http_client.get_cookie( # type: ignore + cookie := http_client.get_cookie( # type: ignore self.SESSION_COOKIE_NAME ) ) or ( - cookie := self._http_client.get_cookie("SESSIONID") # type: ignore + cookie := http_client.get_cookie("SESSIONID") # type: ignore ): self._session_cookie = {self.SESSION_COOKIE_NAME: cookie} - timeout = ( - self._http_client.get_cookie(self.TIMEOUT_COOKIE_NAME) or ONE_DAY_SECONDS + timeout = int( + http_client.get_cookie(self.TIMEOUT_COOKIE_NAME) or ONE_DAY_SECONDS ) # There is a 24 hour timeout on the session cookie # but the clock on the device is not always accurate diff --git a/kasa/klaptransport.py b/kasa/klaptransport.py index e8fed56bf..cd0e3de6b 100644 --- a/kasa/klaptransport.py +++ b/kasa/klaptransport.py @@ -276,15 +276,13 @@ async def perform_handshake(self) -> Any: self._session_cookie = None local_seed, remote_seed, auth_hash = await self.perform_handshake1() - if cookie := self._http_client.get_cookie( # type: ignore - self.SESSION_COOKIE_NAME - ): + http_client = self._http_client + if cookie := http_client.get_cookie(self.SESSION_COOKIE_NAME): # type: ignore self._session_cookie = {self.SESSION_COOKIE_NAME: cookie} # The device returns a TIMEOUT cookie on handshake1 which # it doesn't like to get back so we store the one we want - timeout = int( - self._http_client.get_cookie(self.TIMEOUT_COOKIE_NAME) or ONE_DAY_SECONDS + http_client.get_cookie(self.TIMEOUT_COOKIE_NAME) or ONE_DAY_SECONDS ) # There is a 24 hour timeout on the session cookie # but the clock on the device is not always accurate