aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndre Basche <andre.basche@gmail.com>2023-04-11 00:59:00 +0200
committerAndre Basche <andre.basche@gmail.com>2023-04-11 00:59:00 +0200
commitb4b782c52c6f6b0bb8dbf3f2fb8f00fd0985e8e7 (patch)
tree0df587c8fbb73149d564bbee6b64d3b4bcb8a478
parente857fe91de17baaf4aa93d690db036acaf5df9fd (diff)
Improve logging for authentication errorsv0.6.4
-rw-r--r--pyhon/connection/auth.py60
-rw-r--r--pyhon/connection/handler.py11
-rw-r--r--pyhon/exceptions.py2
-rw-r--r--setup.py2
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
2import logging 2import logging
3import re 3import re
4import secrets 4import secrets
5import sys
6import urllib 5import urllib
7from pprint import pformat 6from pprint import pformat
8from urllib import parse 7from urllib import parse
@@ -10,6 +9,7 @@ from urllib import parse
10from yarl import URL 9from yarl import URL
11 10
12from pyhon import const 11from pyhon import const
12from 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
6from pyhon import const 6from pyhon import const
7from pyhon.connection.auth import HonAuth, _LOGGER 7from pyhon.connection.auth import HonAuth, _LOGGER
8from pyhon.connection.device import HonDevice 8from pyhon.connection.device import HonDevice
9from pyhon.exceptions import HonAuthenticationError
9 10
10 11
11class HonBaseConnectionHandler: 12class 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 @@
1class HonAuthenticationError(Exception):
2 pass
diff --git a/setup.py b/setup.py
index 2cc8e05..2ef3b45 100644
--- a/setup.py
+++ b/setup.py
@@ -7,7 +7,7 @@ with open("README.md", "r") as f:
7 7
8setup( 8setup(
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,