diff options
author | Andre Basche <andre.basche@gmail.com> | 2023-04-11 00:59:00 +0200 |
---|---|---|
committer | Andre Basche <andre.basche@gmail.com> | 2023-04-11 00:59:00 +0200 |
commit | b4b782c52c6f6b0bb8dbf3f2fb8f00fd0985e8e7 (patch) | |
tree | 0df587c8fbb73149d564bbee6b64d3b4bcb8a478 | |
parent | e857fe91de17baaf4aa93d690db036acaf5df9fd (diff) |
Improve logging for authentication errorsv0.6.4
-rw-r--r-- | pyhon/connection/auth.py | 60 | ||||
-rw-r--r-- | pyhon/connection/handler.py | 11 | ||||
-rw-r--r-- | pyhon/exceptions.py | 2 | ||||
-rw-r--r-- | setup.py | 2 |
4 files changed, 46 insertions, 29 deletions
diff --git a/pyhon/connection/auth.py b/pyhon/connection/auth.py index 553a1f6..ac88fdc 100644 --- a/pyhon/connection/auth.py +++ b/pyhon/connection/auth.py | |||
@@ -2,7 +2,6 @@ import json | |||
2 | import logging | 2 | import logging |
3 | import re | 3 | import re |
4 | import secrets | 4 | import secrets |
5 | import sys | ||
6 | import urllib | 5 | import urllib |
7 | from pprint import pformat | 6 | from pprint import pformat |
8 | from urllib import parse | 7 | from urllib import parse |
@@ -10,6 +9,7 @@ from urllib import parse | |||
10 | from yarl import URL | 9 | from yarl import URL |
11 | 10 | ||
12 | from pyhon import const | 11 | from pyhon import const |
12 | from pyhon.exceptions import HonAuthenticationError | ||
13 | 13 | ||
14 | _LOGGER = logging.getLogger(__name__) | 14 | _LOGGER = logging.getLogger(__name__) |
15 | 15 | ||
@@ -24,6 +24,7 @@ class HonAuth: | |||
24 | self._cognito_token = "" | 24 | self._cognito_token = "" |
25 | self._id_token = "" | 25 | self._id_token = "" |
26 | self._device = device | 26 | self._device = device |
27 | self._called_urls = [] | ||
27 | 28 | ||
28 | @property | 29 | @property |
29 | def cognito_token(self): | 30 | def cognito_token(self): |
@@ -41,6 +42,16 @@ class HonAuth: | |||
41 | def refresh_token(self): | 42 | def refresh_token(self): |
42 | return self._refresh_token | 43 | return self._refresh_token |
43 | 44 | ||
45 | async def _error_logger(self, response, fail=True): | ||
46 | result = "hOn Authentication Error\n" | ||
47 | for i, (status, url) in enumerate(self._called_urls): | ||
48 | result += f" {i + 1: 2d} {status} - {url}\n" | ||
49 | result += f"ERROR - {response.status} - {response.request_info.url}\n" | ||
50 | result += f"{15 * '='} Response {15 * '='}\n{await response.text()}\n{40 * '='}" | ||
51 | _LOGGER.error(result) | ||
52 | if fail: | ||
53 | raise HonAuthenticationError("Can't login") | ||
54 | |||
44 | async def _load_login(self): | 55 | async def _load_login(self): |
45 | nonce = secrets.token_hex(16) | 56 | nonce = secrets.token_hex(16) |
46 | nonce = f"{nonce[:8]}-{nonce[8:12]}-{nonce[12:16]}-{nonce[16:20]}-{nonce[20:]}" | 57 | nonce = f"{nonce[:8]}-{nonce[8:12]}-{nonce[12:16]}-{nonce[16:20]}-{nonce[20:]}" |
@@ -58,22 +69,27 @@ class HonAuth: | |||
58 | async with self._session.get( | 69 | async with self._session.get( |
59 | f"{const.AUTH_API}/services/oauth2/authorize/expid_Login?{params}" | 70 | f"{const.AUTH_API}/services/oauth2/authorize/expid_Login?{params}" |
60 | ) as response: | 71 | ) as response: |
61 | _LOGGER.debug("%s - %s", response.status, response.request_info.url) | 72 | self._called_urls.append((response.status, response.request_info.url)) |
62 | if not (login_url := re.findall("url = '(.+?)'", await response.text())): | 73 | if not (login_url := re.findall("url = '(.+?)'", await response.text())): |
74 | await self._error_logger(response) | ||
63 | return False | 75 | return False |
64 | async with self._session.get(login_url[0], allow_redirects=False) as redirect1: | 76 | async with self._session.get(login_url[0], allow_redirects=False) as redirect1: |
65 | _LOGGER.debug("%s - %s", redirect1.status, redirect1.request_info.url) | 77 | self._called_urls.append((redirect1.status, redirect1.request_info.url)) |
66 | if not (url := redirect1.headers.get("Location")): | 78 | if not (url := redirect1.headers.get("Location")): |
79 | await self._error_logger(redirect1) | ||
67 | return False | 80 | return False |
68 | async with self._session.get(url, allow_redirects=False) as redirect2: | 81 | async with self._session.get(url, allow_redirects=False) as redirect2: |
69 | _LOGGER.debug("%s - %s", redirect2.status, redirect2.request_info.url) | 82 | self._called_urls.append((redirect2.status, redirect2.request_info.url)) |
70 | if not ( | 83 | if not ( |
71 | url := redirect2.headers.get("Location") | 84 | url := redirect2.headers.get("Location") |
72 | + "&System=IoT_Mobile_App&RegistrationSubChannel=hOn" | 85 | + "&System=IoT_Mobile_App&RegistrationSubChannel=hOn" |
73 | ): | 86 | ): |
87 | await self._error_logger(redirect2) | ||
74 | return False | 88 | return False |
75 | async with self._session.get(URL(url, encoded=True)) as login_screen: | 89 | async with self._session.get(URL(url, encoded=True)) as login_screen: |
76 | _LOGGER.debug("%s - %s", login_screen.status, login_screen.request_info.url) | 90 | self._called_urls.append( |
91 | (login_screen.status, login_screen.request_info.url) | ||
92 | ) | ||
77 | if context := re.findall( | 93 | if context := re.findall( |
78 | '"fwuid":"(.*?)","loaded":(\\{.*?})', await login_screen.text() | 94 | '"fwuid":"(.*?)","loaded":(\\{.*?})', await login_screen.text() |
79 | ): | 95 | ): |
@@ -83,6 +99,7 @@ class HonAuth: | |||
83 | "/".join(const.AUTH_API.split("/")[:-1]), "" | 99 | "/".join(const.AUTH_API.split("/")[:-1]), "" |
84 | ) | 100 | ) |
85 | return fw_uid, loaded, login_url | 101 | return fw_uid, loaded, login_url |
102 | await self._error_logger(login_screen) | ||
86 | return False | 103 | return False |
87 | 104 | ||
88 | async def _login(self, fw_uid, loaded, login_url): | 105 | async def _login(self, fw_uid, loaded, login_url): |
@@ -122,7 +139,7 @@ class HonAuth: | |||
122 | data="&".join(f"{k}={json.dumps(v)}" for k, v in data.items()), | 139 | data="&".join(f"{k}={json.dumps(v)}" for k, v in data.items()), |
123 | params=params, | 140 | params=params, |
124 | ) as response: | 141 | ) as response: |
125 | _LOGGER.debug("%s - %s", response.status, response.request_info.url) | 142 | self._called_urls.append((response.status, response.request_info.url)) |
126 | if response.status == 200: | 143 | if response.status == 200: |
127 | try: | 144 | try: |
128 | data = await response.json() | 145 | data = await response.json() |
@@ -133,35 +150,31 @@ class HonAuth: | |||
133 | _LOGGER.error( | 150 | _LOGGER.error( |
134 | "Can't get login url - %s", pformat(await response.json()) | 151 | "Can't get login url - %s", pformat(await response.json()) |
135 | ) | 152 | ) |
136 | _LOGGER.error( | 153 | await self._error_logger(response) |
137 | "Unable to login: %s\n%s", response.status, await response.text() | ||
138 | ) | ||
139 | return "" | 154 | return "" |
140 | 155 | ||
141 | async def _get_token(self, url): | 156 | async def _get_token(self, url): |
142 | async with self._session.get(url) as response: | 157 | async with self._session.get(url) as response: |
143 | _LOGGER.debug("%s - %s", response.status, response.request_info.url) | 158 | self._called_urls.append((response.status, response.request_info.url)) |
144 | if response.status != 200: | 159 | if response.status != 200: |
145 | _LOGGER.error("Unable to get token: %s", response.status) | 160 | await self._error_logger(response) |
146 | return False | 161 | return False |
147 | url = re.findall("href\\s*=\\s*[\"'](.+?)[\"']", await response.text()) | 162 | url = re.findall("href\\s*=\\s*[\"'](.+?)[\"']", await response.text()) |
148 | if not url: | 163 | if not url: |
149 | _LOGGER.error("Can't get login url - \n%s", await response.text()) | 164 | await self._error_logger(response) |
150 | raise PermissionError | 165 | return False |
151 | if "ProgressiveLogin" in url[0]: | 166 | if "ProgressiveLogin" in url[0]: |
152 | async with self._session.get(url[0]) as response: | 167 | async with self._session.get(url[0]) as response: |
153 | _LOGGER.debug("%s - %s", response.status, response.request_info.url) | 168 | self._called_urls.append((response.status, response.request_info.url)) |
154 | if response.status != 200: | 169 | if response.status != 200: |
155 | _LOGGER.error("Unable to get token: %s", response.status) | 170 | await self._error_logger(response) |
156 | return False | 171 | return False |
157 | url = re.findall("href\\s*=\\s*[\"'](.*?)[\"']", await response.text()) | 172 | url = re.findall("href\\s*=\\s*[\"'](.*?)[\"']", await response.text()) |
158 | url = "/".join(const.AUTH_API.split("/")[:-1]) + url[0] | 173 | url = "/".join(const.AUTH_API.split("/")[:-1]) + url[0] |
159 | async with self._session.get(url) as response: | 174 | async with self._session.get(url) as response: |
160 | _LOGGER.debug("%s - %s", response.status, response.request_info.url) | 175 | self._called_urls.append((response.status, response.request_info.url)) |
161 | if response.status != 200: | 176 | if response.status != 200: |
162 | _LOGGER.error( | 177 | await self._error_logger(response) |
163 | "Unable to connect to the login service: %s", response.status | ||
164 | ) | ||
165 | return False | 178 | return False |
166 | text = await response.text() | 179 | text = await response.text() |
167 | if access_token := re.findall("access_token=(.*?)&", text): | 180 | if access_token := re.findall("access_token=(.*?)&", text): |
@@ -187,11 +200,11 @@ class HonAuth: | |||
187 | async with self._session.post( | 200 | async with self._session.post( |
188 | f"{const.API_URL}/auth/v1/login", headers=post_headers, json=data | 201 | f"{const.API_URL}/auth/v1/login", headers=post_headers, json=data |
189 | ) as response: | 202 | ) as response: |
190 | _LOGGER.debug("%s - %s", response.status, response.request_info.url) | 203 | self._called_urls.append((response.status, response.request_info.url)) |
191 | try: | 204 | try: |
192 | json_data = await response.json() | 205 | json_data = await response.json() |
193 | except json.JSONDecodeError: | 206 | except json.JSONDecodeError: |
194 | _LOGGER.error("No JSON Data after POST: %s", await response.text()) | 207 | await self._error_logger(response) |
195 | return False | 208 | return False |
196 | self._cognito_token = json_data["cognitoUser"]["Token"] | 209 | self._cognito_token = json_data["cognitoUser"]["Token"] |
197 | return True | 210 | return True |
@@ -205,8 +218,9 @@ class HonAuth: | |||
205 | async with self._session.post( | 218 | async with self._session.post( |
206 | f"{const.AUTH_API}/services/oauth2/token", params=params | 219 | f"{const.AUTH_API}/services/oauth2/token", params=params |
207 | ) as response: | 220 | ) as response: |
208 | _LOGGER.debug("%s - %s", response.status, response.request_info.url) | 221 | self._called_urls.append((response.status, response.request_info.url)) |
209 | if response.status >= 400: | 222 | if response.status >= 400: |
223 | await self._error_logger(response, fail=False) | ||
210 | return False | 224 | return False |
211 | data = await response.json() | 225 | data = await response.json() |
212 | self._id_token = data["id_token"] | 226 | self._id_token = data["id_token"] |
diff --git a/pyhon/connection/handler.py b/pyhon/connection/handler.py index b16f48a..f56b4b7 100644 --- a/pyhon/connection/handler.py +++ b/pyhon/connection/handler.py | |||
@@ -6,6 +6,7 @@ import aiohttp | |||
6 | from pyhon import const | 6 | from pyhon import const |
7 | from pyhon.connection.auth import HonAuth, _LOGGER | 7 | from pyhon.connection.auth import HonAuth, _LOGGER |
8 | from pyhon.connection.device import HonDevice | 8 | from pyhon.connection.device import HonDevice |
9 | from pyhon.exceptions import HonAuthenticationError | ||
9 | 10 | ||
10 | 11 | ||
11 | class HonBaseConnectionHandler: | 12 | class HonBaseConnectionHandler: |
@@ -50,9 +51,9 @@ class HonConnectionHandler(HonBaseConnectionHandler): | |||
50 | self._email = email | 51 | self._email = email |
51 | self._password = password | 52 | self._password = password |
52 | if not self._email: | 53 | if not self._email: |
53 | raise PermissionError("Login-Error - An email address must be specified") | 54 | raise HonAuthenticationError("An email address must be specified") |
54 | if not self._password: | 55 | if not self._password: |
55 | raise PermissionError("Login-Error - A password address must be specified") | 56 | raise HonAuthenticationError("A password address must be specified") |
56 | self._request_headers = {} | 57 | self._request_headers = {} |
57 | 58 | ||
58 | @property | 59 | @property |
@@ -73,7 +74,7 @@ class HonConnectionHandler(HonBaseConnectionHandler): | |||
73 | self._request_headers["cognito-token"] = self._auth.cognito_token | 74 | self._request_headers["cognito-token"] = self._auth.cognito_token |
74 | self._request_headers["id-token"] = self._auth.id_token | 75 | self._request_headers["id-token"] = self._auth.id_token |
75 | else: | 76 | else: |
76 | raise PermissionError("Can't Login") | 77 | raise HonAuthenticationError("Can't login") |
77 | return {h: v for h, v in self._request_headers.items() if h not in headers} | 78 | return {h: v for h, v in self._request_headers.items() if h not in headers} |
78 | 79 | ||
79 | @asynccontextmanager | 80 | @asynccontextmanager |
@@ -100,7 +101,7 @@ class HonConnectionHandler(HonBaseConnectionHandler): | |||
100 | response.status, | 101 | response.status, |
101 | await response.text(), | 102 | await response.text(), |
102 | ) | 103 | ) |
103 | raise PermissionError("Login failure") | 104 | raise HonAuthenticationError("Login failure") |
104 | else: | 105 | else: |
105 | try: | 106 | try: |
106 | await response.json() | 107 | await response.json() |
@@ -123,5 +124,5 @@ class HonAnonymousConnectionHandler(HonBaseConnectionHandler): | |||
123 | kwargs["headers"] = kwargs.pop("headers", {}) | self._HEADERS | 124 | kwargs["headers"] = kwargs.pop("headers", {}) | self._HEADERS |
124 | async with method(*args, **kwargs) as response: | 125 | async with method(*args, **kwargs) as response: |
125 | if response.status == 403: | 126 | if response.status == 403: |
126 | print("Can't authorize") | 127 | _LOGGER.error("Can't authenticate anymore") |
127 | yield response | 128 | yield response |
diff --git a/pyhon/exceptions.py b/pyhon/exceptions.py new file mode 100644 index 0000000..9b6b7fa --- /dev/null +++ b/pyhon/exceptions.py | |||
@@ -0,0 +1,2 @@ | |||
1 | class HonAuthenticationError(Exception): | ||
2 | pass | ||
@@ -7,7 +7,7 @@ with open("README.md", "r") as f: | |||
7 | 7 | ||
8 | setup( | 8 | setup( |
9 | name="pyhOn", | 9 | name="pyhOn", |
10 | version="0.6.3", | 10 | version="0.6.4", |
11 | author="Andre Basche", | 11 | author="Andre Basche", |
12 | description="Control hOn devices with python", | 12 | description="Control hOn devices with python", |
13 | long_description=long_description, | 13 | long_description=long_description, |