From 588ffe7ac0f25f3ffda9338df02c0c594ceeb4b0 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 25 Sep 2019 18:59:20 +0200 Subject: [PATCH 01/63] Update aiofreepybox.py: Add freebox host port and api_version auto detection support --- aiofreepybox/aiofreepybox.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index e2ca8ef0..70568daa 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -41,14 +41,14 @@ class Freepybox: - def __init__(self, app_desc=app_desc, token_file=token_file, api_version='v3', timeout=10): + def __init__(self, app_desc=app_desc, token_file=token_file, api_version='auto', timeout=10): self.token_file = token_file self.api_version = api_version self.timeout = timeout self.app_desc = app_desc self._access = None - async def open(self, host, port): + async def open(self, host='auto', port='auto'): ''' Open a session to the freebox, get a valid access module and instantiate freebox modules @@ -63,6 +63,24 @@ async def open(self, host, port): conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) self._session = aiohttp.ClientSession(connector=conn) + # Detect host, port and api_version + r = await self._session.get('http://mafreebox.freebox.fr/api_version', timeout=self.timeout) + resp = await r.json() + if host == 'auto': + host = resp['api_domain'] + logger.debug('host set to {0}'.format(host)) + if port == 'auto': + port = resp['https_port'] + logger.debug('port set to {0}'.format(port)) + if self.api_version == 'auto': + self.api_version = 'v{0}'.format(resp['api_version'][:1]) + logger.debug('api version set to {0}'.format(host)) + elif resp['api_version'][:1] > self.api_version[1:]: + logger.warning('Freebox server support a newer api version: v{0}, check api_version ({1})'.format(resp['api_version'][:1], self.api_version)) + elif resp['api_version'][:1] < self.api_version[1:]: + logger.warning('Freebox server does not support this version ({0}), downgrading to v{1}'.format(self.api_version, resp['api_version'][:1])) + self.api_version = 'v{0}'.format(resp['api_version'][:1]) + self._access = await self._get_freebox_access(host, port, self.api_version, self.token_file, self.app_desc, self.timeout) # Instantiate freebox modules From 8862c41f6df13fd2bf590783c23770b5d0193588 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 25 Sep 2019 19:01:18 +0200 Subject: [PATCH 02/63] Update example.py: use autodetection --- example.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example.py b/example.py index 8fc0397e..55550de1 100755 --- a/example.py +++ b/example.py @@ -15,12 +15,12 @@ async def demo(): fbx = Freepybox() # To find out the HTTPS host and port of your freebox, go to - # http://mafreebox.freebox.fr/api_version + # http://mafreebox.freebox.fr/api_version or let auto detect do it for you # Connect to the freebox # Be ready to authorize the application on the Freebox if you use this # example for the first time - await fbx.open(host='abcdefgh.fbxos.fr', port=1234) + await fbx.open() # Dump freebox configuration using system API # Extract temperature and mac address From e03a73f42865e88a5a0fc3f53fec702f0df32494 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 25 Sep 2019 21:59:14 +0200 Subject: [PATCH 03/63] Update aiofreepybox.py: fix log --- aiofreepybox/aiofreepybox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 70568daa..a3891478 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -74,7 +74,7 @@ async def open(self, host='auto', port='auto'): logger.debug('port set to {0}'.format(port)) if self.api_version == 'auto': self.api_version = 'v{0}'.format(resp['api_version'][:1]) - logger.debug('api version set to {0}'.format(host)) + logger.debug('api version set to {0}'.format(self.api_version)) elif resp['api_version'][:1] > self.api_version[1:]: logger.warning('Freebox server support a newer api version: v{0}, check api_version ({1})'.format(resp['api_version'][:1], self.api_version)) elif resp['api_version'][:1] < self.api_version[1:]: From a6a99667dad1f84a54733d0fde82aa61c7c0dbf4 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 25 Sep 2019 22:05:29 +0200 Subject: [PATCH 04/63] Update aiofreepybox.py: only detect if any parameter is set to auto --- aiofreepybox/aiofreepybox.py | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index a3891478..e89f0ace 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -64,22 +64,23 @@ async def open(self, host='auto', port='auto'): self._session = aiohttp.ClientSession(connector=conn) # Detect host, port and api_version - r = await self._session.get('http://mafreebox.freebox.fr/api_version', timeout=self.timeout) - resp = await r.json() - if host == 'auto': - host = resp['api_domain'] - logger.debug('host set to {0}'.format(host)) - if port == 'auto': - port = resp['https_port'] - logger.debug('port set to {0}'.format(port)) - if self.api_version == 'auto': - self.api_version = 'v{0}'.format(resp['api_version'][:1]) - logger.debug('api version set to {0}'.format(self.api_version)) - elif resp['api_version'][:1] > self.api_version[1:]: - logger.warning('Freebox server support a newer api version: v{0}, check api_version ({1})'.format(resp['api_version'][:1], self.api_version)) - elif resp['api_version'][:1] < self.api_version[1:]: - logger.warning('Freebox server does not support this version ({0}), downgrading to v{1}'.format(self.api_version, resp['api_version'][:1])) - self.api_version = 'v{0}'.format(resp['api_version'][:1]) + if host == 'auto' or port == 'auto' or self.api_version == 'auto': + r = await self._session.get('http://mafreebox.freebox.fr/api_version', timeout=self.timeout) + resp = await r.json() + if host == 'auto': + host = resp['api_domain'] + logger.debug('host set to {0}'.format(host)) + if port == 'auto': + port = resp['https_port'] + logger.debug('port set to {0}'.format(port)) + if self.api_version == 'auto': + self.api_version = 'v{0}'.format(resp['api_version'][:1]) + logger.debug('api version set to {0}'.format(self.api_version)) + elif resp['api_version'][:1] > self.api_version[1:]: + logger.warning('Freebox server support a newer api version: v{0}, check api_version ({1})'.format(resp['api_version'][:1], self.api_version)) + elif resp['api_version'][:1] < self.api_version[1:]: + logger.warning('Freebox server does not support this version ({0}), downgrading to v{1}'.format(self.api_version, resp['api_version'][:1])) + self.api_version = 'v{0}'.format(resp['api_version'][:1]) self._access = await self._get_freebox_access(host, port, self.api_version, self.token_file, self.app_desc, self.timeout) From 7789e2348ac91a3577616b3d48e33fca9dc062cc Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 25 Sep 2019 22:35:42 +0200 Subject: [PATCH 05/63] Update aiofreebox.py: improve detect and host logic --- aiofreepybox/aiofreepybox.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index e89f0ace..911fd506 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -64,8 +64,13 @@ async def open(self, host='auto', port='auto'): self._session = aiohttp.ClientSession(connector=conn) # Detect host, port and api_version - if host == 'auto' or port == 'auto' or self.api_version == 'auto': - r = await self._session.get('http://mafreebox.freebox.fr/api_version', timeout=self.timeout) + detect = [host, port, self.api_version] + if 'auto' in detect: + default_host = '212.27.38.253' + host_list = ['auto', 'mafreebox.freebox.fr', default_host] + if host not in host_list: + default_host = host + r = await self._session.get('http://{0}/api_version'.format(default_host), timeout=self.timeout) resp = await r.json() if host == 'auto': host = resp['api_domain'] From 52b7aef7c8bef66cfc316d836704d65e083473e4 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 25 Sep 2019 23:42:52 +0200 Subject: [PATCH 06/63] Update aiofreepybox.py: formatting, small refactoring --- aiofreepybox/aiofreepybox.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 911fd506..cd6d1152 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -64,11 +64,19 @@ async def open(self, host='auto', port='auto'): self._session = aiohttp.ClientSession(connector=conn) # Detect host, port and api_version - detect = [host, port, self.api_version] + detect = [ + host, + port, + self.api_version + ] if 'auto' in detect: default_host = '212.27.38.253' - host_list = ['auto', 'mafreebox.freebox.fr', default_host] - if host not in host_list: + default_host_list = [ + 'auto', + 'mafreebox.freebox.fr', + default_host + ] + if host not in default_host_list: default_host = host r = await self._session.get('http://{0}/api_version'.format(default_host), timeout=self.timeout) resp = await r.json() @@ -78,14 +86,16 @@ async def open(self, host='auto', port='auto'): if port == 'auto': port = resp['https_port'] logger.debug('port set to {0}'.format(port)) + server_version = resp['api_version'][:1] + short_api_version = self.api_version[1:] if self.api_version == 'auto': - self.api_version = 'v{0}'.format(resp['api_version'][:1]) + self.api_version = 'v{0}'.format(server_version) logger.debug('api version set to {0}'.format(self.api_version)) - elif resp['api_version'][:1] > self.api_version[1:]: - logger.warning('Freebox server support a newer api version: v{0}, check api_version ({1})'.format(resp['api_version'][:1], self.api_version)) - elif resp['api_version'][:1] < self.api_version[1:]: - logger.warning('Freebox server does not support this version ({0}), downgrading to v{1}'.format(self.api_version, resp['api_version'][:1])) - self.api_version = 'v{0}'.format(resp['api_version'][:1]) + elif server_version > short_api_version: + logger.warning('Freebox server support a newer api version: v{0}, check api_version ({1})'.format(server_version, self.api_version)) + elif server_version < short_api_version: + logger.warning('Freebox server does not support this version ({0}), downgrading to v{1}'.format(self.api_version, server_version)) + self.api_version = 'v{0}'.format(server_version) self._access = await self._get_freebox_access(host, port, self.api_version, self.token_file, self.app_desc, self.timeout) From d6d01ab07227d4abb6cac3ee248abcba64f0d786 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Thu, 26 Sep 2019 21:51:17 +0200 Subject: [PATCH 07/63] Update aiofreepybox.py: use f-strings --- aiofreepybox/aiofreepybox.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index cd6d1152..5aee1e1f 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -78,24 +78,24 @@ async def open(self, host='auto', port='auto'): ] if host not in default_host_list: default_host = host - r = await self._session.get('http://{0}/api_version'.format(default_host), timeout=self.timeout) + r = await self._session.get(f'http://{default_host}/api_version', timeout=self.timeout) resp = await r.json() if host == 'auto': host = resp['api_domain'] - logger.debug('host set to {0}'.format(host)) + logger.debug(f'host set to {host}') if port == 'auto': port = resp['https_port'] - logger.debug('port set to {0}'.format(port)) + logger.debug(f'port set to {port}') server_version = resp['api_version'][:1] short_api_version = self.api_version[1:] if self.api_version == 'auto': - self.api_version = 'v{0}'.format(server_version) - logger.debug('api version set to {0}'.format(self.api_version)) + self.api_version = f'v{server_version}' + logger.debug(f'api version set to {self.api_version}') elif server_version > short_api_version: - logger.warning('Freebox server support a newer api version: v{0}, check api_version ({1})'.format(server_version, self.api_version)) + logger.warning(f'Freebox server support a newer api version: v{server_version}, check api_version ({self.api_version})') elif server_version < short_api_version: - logger.warning('Freebox server does not support this version ({0}), downgrading to v{1}'.format(self.api_version, server_version)) - self.api_version = 'v{0}'.format(server_version) + logger.warning(f'Freebox server does not support this version ({self.api_version}), downgrading to v{server_version}') + self.api_version = f'v{server_version}' self._access = await self._get_freebox_access(host, port, self.api_version, self.token_file, self.app_desc, self.timeout) @@ -182,7 +182,7 @@ async def _get_freebox_access(self, host, port, api_version, token_file, app_des # Store application token in file self._writefile_app_token(app_token, track_id, app_desc, token_file) - logger.info('Application token file was generated: {0}'.format(token_file)) + logger.info(f'Application token file was generated: {token_file}') # Create freebox http access module fbx_access = Access(self._session, base_url, app_token, app_desc['app_id'], timeout) @@ -199,7 +199,7 @@ async def _get_authorization_status(self, base_url, track_id, timeout): granted the app_token is valid and can be used to open a session denied the user denied the authorization request ''' - url = urljoin(base_url, 'login/authorize/{0}'.format(track_id)) + url = urljoin(base_url, f'login/authorize/{track_id}') r = await self._session.get(url, timeout=timeout) resp = await r.json() return resp['result']['status'] @@ -217,7 +217,7 @@ async def _get_app_token(self, base_url, app_desc, timeout=10): # raise exception if resp.success != True if not resp.get('success'): - raise AuthorizationError('Authorization failed (APIResponse: {0})' + raise AuthorizationError('Authorization failed (APIResponse: {})' .format(json.dumps(resp))) app_token = resp['result']['app_token'] @@ -255,7 +255,7 @@ def _get_base_url(self, host, port, freebox_api_version): Returns base url for HTTPS requests :return: ''' - return 'https://{0}:{1}/api/{2}/'.format(host, port, freebox_api_version) + return f'https://{host}:{port}/api/{freebox_api_version}/' def _is_app_desc_valid(self, app_desc): ''' From 0908f5cb35f33fa4b762b6a7982c07289b8d5f95 Mon Sep 17 00:00:00 2001 From: foreignsub Date: Fri, 27 Sep 2019 19:21:11 +0200 Subject: [PATCH 08/63] Update aiofreepybox.py: improve detection logic --- aiofreepybox/aiofreepybox.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 5aee1e1f..913df174 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -74,10 +74,18 @@ async def open(self, host='auto', port='auto'): default_host_list = [ 'auto', 'mafreebox.freebox.fr', + '192.168.0.254', default_host ] if host not in default_host_list: default_host = host + logger.debug(f'host set to {host}') + if port == 'auto': + logger.warning('Port is set to auto, but host is not in default host list, checking port 80') + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + result = sock.connect_ex((default_host,80)) + if result != 0: + raise HttpRequestError('port 80 is closed, cannot detect freebox') r = await self._session.get(f'http://{default_host}/api_version', timeout=self.timeout) resp = await r.json() if host == 'auto': From 560ba50d34fd6b62c40e4865896d1363178c37c9 Mon Sep 17 00:00:00 2001 From: foreignsub Date: Fri, 27 Sep 2019 19:23:59 +0200 Subject: [PATCH 09/63] Update aiofreepybox.py: cosmetic fix --- aiofreepybox/aiofreepybox.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 913df174..5fcba7d1 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -79,26 +79,26 @@ async def open(self, host='auto', port='auto'): ] if host not in default_host_list: default_host = host - logger.debug(f'host set to {host}') + logger.debug(f'Host set to {host}') if port == 'auto': logger.warning('Port is set to auto, but host is not in default host list, checking port 80') sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((default_host,80)) if result != 0: - raise HttpRequestError('port 80 is closed, cannot detect freebox') + raise HttpRequestError('Port 80 is closed, cannot detect freebox') r = await self._session.get(f'http://{default_host}/api_version', timeout=self.timeout) resp = await r.json() if host == 'auto': host = resp['api_domain'] - logger.debug(f'host set to {host}') + logger.debug(f'Host set to {host}') if port == 'auto': port = resp['https_port'] - logger.debug(f'port set to {port}') + logger.debug(f'Port set to {port}') server_version = resp['api_version'][:1] short_api_version = self.api_version[1:] if self.api_version == 'auto': self.api_version = f'v{server_version}' - logger.debug(f'api version set to {self.api_version}') + logger.debug(f'API version set to {self.api_version}') elif server_version > short_api_version: logger.warning(f'Freebox server support a newer api version: v{server_version}, check api_version ({self.api_version})') elif server_version < short_api_version: From 7edd9755200dc678348258c29f952e44b2951955 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 1 Oct 2019 00:52:23 +0200 Subject: [PATCH 10/63] Update aiofreepybox.py: split api version, use https by default for detection --- aiofreepybox/aiofreepybox.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 5fcba7d1..7c4bd331 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -70,23 +70,24 @@ async def open(self, host='auto', port='auto'): self.api_version ] if 'auto' in detect: - default_host = '212.27.38.253' + default_host = 'mafreebox.freebox.fr' default_host_list = [ 'auto', - 'mafreebox.freebox.fr', - '192.168.0.254', + 'freeplayer.freebox.fr', default_host ] + secure = 's' if host not in default_host_list: default_host = host logger.debug(f'Host set to {host}') + secure = '' if port == 'auto': logger.warning('Port is set to auto, but host is not in default host list, checking port 80') sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((default_host,80)) if result != 0: raise HttpRequestError('Port 80 is closed, cannot detect freebox') - r = await self._session.get(f'http://{default_host}/api_version', timeout=self.timeout) + r = await self._session.get(f'http{secure}://{default_host}/api_version', timeout=self.timeout) resp = await r.json() if host == 'auto': host = resp['api_domain'] @@ -94,7 +95,7 @@ async def open(self, host='auto', port='auto'): if port == 'auto': port = resp['https_port'] logger.debug(f'Port set to {port}') - server_version = resp['api_version'][:1] + server_version = resp['api_version'].split('.')[0] short_api_version = self.api_version[1:] if self.api_version == 'auto': self.api_version = f'v{server_version}' From 0fd91a89cffba07d2f5fefc030e7dd81b333e3f1 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sat, 5 Oct 2019 00:23:42 +0200 Subject: [PATCH 11/63] Update aiofreebox.py: pep257, small fix --- aiofreepybox/aiofreepybox.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 7c4bd331..308a5aa6 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -49,10 +49,10 @@ def __init__(self, app_desc=app_desc, token_file=token_file, api_version='auto', self._access = None async def open(self, host='auto', port='auto'): - ''' + """ Open a session to the freebox, get a valid access module and instantiate freebox modules - ''' + """ if not self._is_app_desc_valid(self.app_desc): raise InvalidTokenError('Invalid application descriptor') @@ -84,7 +84,7 @@ async def open(self, host='auto', port='auto'): if port == 'auto': logger.warning('Port is set to auto, but host is not in default host list, checking port 80') sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = sock.connect_ex((default_host,80)) + result = sock.connect_ex((default_host, 80)) if result != 0: raise HttpRequestError('Port 80 is closed, cannot detect freebox') r = await self._session.get(f'http{secure}://{default_host}/api_version', timeout=self.timeout) @@ -120,9 +120,9 @@ async def open(self, host='auto', port='auto'): self.nat = Nat(self._access) async def close(self): - ''' + """ Close the freebox session - ''' + """ if self._access is None: raise NotOpenError('Freebox is not open') @@ -130,7 +130,7 @@ async def close(self): await self._session.close() async def get_permissions(self): - ''' + """ Returns the permissions for this app. The permissions are returned as a dictionary key->boolean where the @@ -142,16 +142,16 @@ async def get_permissions(self): opened. If they have been changed in the meantime, they may be outdated until the session token is refreshed. If the session has not been opened yet, returns None. - ''' + """ if self._access: return await self._access.get_permissions() else: return None async def _get_freebox_access(self, host, port, api_version, token_file, app_desc, timeout=10): - ''' + """ Returns an access object used for HTTP requests. - ''' + """ base_url = self._get_base_url(host, port, api_version) @@ -199,7 +199,7 @@ async def _get_freebox_access(self, host, port, api_version, token_file, app_des return fbx_access async def _get_authorization_status(self, base_url, track_id, timeout): - ''' + """ Get authorization status of the application token Returns: unknown the app_token is invalid or has been revoked @@ -207,7 +207,7 @@ async def _get_authorization_status(self, base_url, track_id, timeout): timeout the user did not confirmed the authorization within the given time granted the app_token is valid and can be used to open a session denied the user denied the authorization request - ''' + """ url = urljoin(base_url, f'login/authorize/{track_id}') r = await self._session.get(url, timeout=timeout) resp = await r.json() @@ -260,14 +260,14 @@ def _readfile_app_token(self, file): return (None, None, None) def _get_base_url(self, host, port, freebox_api_version): - ''' + """ Returns base url for HTTPS requests :return: - ''' + """ return f'https://{host}:{port}/api/{freebox_api_version}/' def _is_app_desc_valid(self, app_desc): - ''' + """ Check validity of the application descriptor - ''' + """ return all(k in app_desc for k in ('app_id', 'app_name', 'app_version', 'device_name')) From 25c56387b0e879682c80cae7164354935f669e36 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 8 Oct 2019 01:55:47 +0200 Subject: [PATCH 12/63] Update aiofreepybox.py: add hardcoded api_version_target, cleanup --- aiofreepybox/aiofreepybox.py | 37 +++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 308a5aa6..2c424bda 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -44,6 +44,7 @@ class Freepybox: def __init__(self, app_desc=app_desc, token_file=token_file, api_version='auto', timeout=10): self.token_file = token_file self.api_version = api_version + self.api_version_target = 'v6' self.timeout = timeout self.app_desc = app_desc self._access = None @@ -68,42 +69,64 @@ async def open(self, host='auto', port='auto'): host, port, self.api_version - ] + ] + if 'auto' in detect: default_host = 'mafreebox.freebox.fr' default_host_list = [ 'auto', 'freeplayer.freebox.fr', default_host - ] + ] secure = 's' + if host not in default_host_list: default_host = host logger.debug(f'Host set to {host}') secure = '' + if port == 'auto': logger.warning('Port is set to auto, but host is not in default host list, checking port 80') sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((default_host, 80)) if result != 0: raise HttpRequestError('Port 80 is closed, cannot detect freebox') + r = await self._session.get(f'http{secure}://{default_host}/api_version', timeout=self.timeout) resp = await r.json() + if host == 'auto': host = resp['api_domain'] logger.debug(f'Host set to {host}') + if port == 'auto': port = resp['https_port'] logger.debug(f'Port set to {port}') + server_version = resp['api_version'].split('.')[0] - short_api_version = self.api_version[1:] + short_api_version_target = self.api_version_target[1:] + + # Check auto api version if self.api_version == 'auto': + # Check server version + if server_version >= short_api_version_target: + self.api_version = self.api_version_target + logger.debug(f'API version set to target version {self.api_version}') + else: + # This should never happen unless time is going backward + self.api_version = f'v{server_version}' + logger.warning(f'Target API version not supported ({self.api_version_target}), downgrading to server version {self.api_version}') + # Check server api version + elif self.api_version == 'server': self.api_version = f'v{server_version}' - logger.debug(f'API version set to {self.api_version}') - elif server_version > short_api_version: - logger.warning(f'Freebox server support a newer api version: v{server_version}, check api_version ({self.api_version})') + logger.debug(f'API version set to server version {self.api_version}') + + short_api_version = self.api_version[1:] + + if server_version > short_api_version: + logger.debug(f'Freebox server supports a newer api version: v{server_version}, check api_version ({self.api_version}) for support.') elif server_version < short_api_version: - logger.warning(f'Freebox server does not support this version ({self.api_version}), downgrading to v{server_version}') + logger.warning(f'Freebox server does not support this version ({self.api_version}), downgrading to v{server_version}.') self.api_version = f'v{server_version}' self._access = await self._get_freebox_access(host, port, self.api_version, self.token_file, self.app_desc, self.timeout) From 2864a1c60c5b7cbb1b8d1130a3f8a955bdf598d4 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 8 Oct 2019 02:42:23 +0200 Subject: [PATCH 13/63] Update aiofreepybox.py: rework logic --- aiofreepybox/aiofreepybox.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 2c424bda..673759cb 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -105,6 +105,7 @@ async def open(self, host='auto', port='auto'): server_version = resp['api_version'].split('.')[0] short_api_version_target = self.api_version_target[1:] + short_api_version = self.api_version[1:] # Check auto api version if self.api_version == 'auto': @@ -120,10 +121,8 @@ async def open(self, host='auto', port='auto'): elif self.api_version == 'server': self.api_version = f'v{server_version}' logger.debug(f'API version set to server version {self.api_version}') - - short_api_version = self.api_version[1:] - - if server_version > short_api_version: + # Check user api version + elif server_version > short_api_version: logger.debug(f'Freebox server supports a newer api version: v{server_version}, check api_version ({self.api_version}) for support.') elif server_version < short_api_version: logger.warning(f'Freebox server does not support this version ({self.api_version}), downgrading to v{server_version}.') From 297ae8c37c234e5150475f46079164f5ab114f74 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 8 Oct 2019 13:39:58 +0200 Subject: [PATCH 14/63] Update aiofreepybox.py: fix api_version logic --- aiofreepybox/aiofreepybox.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 673759cb..6ad1e526 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -109,24 +109,24 @@ async def open(self, host='auto', port='auto'): # Check auto api version if self.api_version == 'auto': + self.api_version = self.api_version_target # Check server version - if server_version >= short_api_version_target: - self.api_version = self.api_version_target - logger.debug(f'API version set to target version {self.api_version}') - else: - # This should never happen unless time is going backward - self.api_version = f'v{server_version}' - logger.warning(f'Target API version not supported ({self.api_version_target}), downgrading to server version {self.api_version}') + if server_version > short_api_version_target: + logger.debug(f'Freebox server supports a newer api version: v{server_version}, check api_version ({self.api_version}) for support.') # Check server api version elif self.api_version == 'server': self.api_version = f'v{server_version}' logger.debug(f'API version set to server version {self.api_version}') + if server_version > short_api_version_target: + logger.warning(f'Using new API version {self.api_version}, results may vary ') # Check user api version - elif server_version > short_api_version: - logger.debug(f'Freebox server supports a newer api version: v{server_version}, check api_version ({self.api_version}) for support.') + elif short_api_version < short_api_version_target and int(short_api_version) > 0: + logger.warning(f'Using deprecated API version {self.api_version}, results may vary ') elif server_version < short_api_version: - logger.warning(f'Freebox server does not support this version ({self.api_version}), downgrading to v{server_version}.') + logger.warning(f'Freebox server does not support this API version ({self.api_version}), downgrading to v{server_version}.') self.api_version = f'v{server_version}' + elif short_api_version != short_api_version_target: + logger.warning(f'User defined API version set to {self.api_version}, results may vary') self._access = await self._get_freebox_access(host, port, self.api_version, self.token_file, self.app_desc, self.timeout) From 9d5b8495595fdac0478543b4ef333c83c5957ea4 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 16 Oct 2019 06:33:03 +0200 Subject: [PATCH 15/63] Update aiofreepybox.py: rework detection code --- aiofreepybox/aiofreepybox.py | 117 ++++++++++++++++------------------- 1 file changed, 53 insertions(+), 64 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 6ad1e526..e06e4b83 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -47,6 +47,7 @@ def __init__(self, app_desc=app_desc, token_file=token_file, api_version='auto', self.api_version_target = 'v6' self.timeout = timeout self.app_desc = app_desc + self.fbx_desc = {} self._access = None async def open(self, host='auto', port='auto'): @@ -57,6 +58,27 @@ async def open(self, host='auto', port='auto'): if not self._is_app_desc_valid(self.app_desc): raise InvalidTokenError('Invalid application descriptor') + default_host = 'mafreebox.freebox.fr' + default_port = 443 + secure = 's' + + # Detect host, port and api_version + if host != 'auto': + default_host = host + + if port != 'auto': + default_port = port + if default_port == 80: + secure = '' + + # Checking host and port + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + result = sock.connect_ex((default_host, default_port)) + if result != 0: + raise HttpRequestError(f'Port {default_port} is closed, cannot detect freebox') + else: + sock.close() + cert_path = os.path.join(os.path.dirname(__file__), 'freebox_certificates.pem') ssl_ctx = ssl.create_default_context() ssl_ctx.load_verify_locations(cafile=cert_path) @@ -64,69 +86,35 @@ async def open(self, host='auto', port='auto'): conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) self._session = aiohttp.ClientSession(connector=conn) - # Detect host, port and api_version - detect = [ - host, - port, - self.api_version - ] - - if 'auto' in detect: - default_host = 'mafreebox.freebox.fr' - default_host_list = [ - 'auto', - 'freeplayer.freebox.fr', - default_host - ] - secure = 's' - - if host not in default_host_list: - default_host = host - logger.debug(f'Host set to {host}') - secure = '' - - if port == 'auto': - logger.warning('Port is set to auto, but host is not in default host list, checking port 80') - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = sock.connect_ex((default_host, 80)) - if result != 0: - raise HttpRequestError('Port 80 is closed, cannot detect freebox') - - r = await self._session.get(f'http{secure}://{default_host}/api_version', timeout=self.timeout) - resp = await r.json() - - if host == 'auto': - host = resp['api_domain'] - logger.debug(f'Host set to {host}') - - if port == 'auto': - port = resp['https_port'] - logger.debug(f'Port set to {port}') - - server_version = resp['api_version'].split('.')[0] - short_api_version_target = self.api_version_target[1:] - short_api_version = self.api_version[1:] - - # Check auto api version - if self.api_version == 'auto': - self.api_version = self.api_version_target - # Check server version - if server_version > short_api_version_target: - logger.debug(f'Freebox server supports a newer api version: v{server_version}, check api_version ({self.api_version}) for support.') - # Check server api version - elif self.api_version == 'server': - self.api_version = f'v{server_version}' - logger.debug(f'API version set to server version {self.api_version}') - if server_version > short_api_version_target: - logger.warning(f'Using new API version {self.api_version}, results may vary ') - # Check user api version - elif short_api_version < short_api_version_target and int(short_api_version) > 0: - logger.warning(f'Using deprecated API version {self.api_version}, results may vary ') - elif server_version < short_api_version: - logger.warning(f'Freebox server does not support this API version ({self.api_version}), downgrading to v{server_version}.') - self.api_version = f'v{server_version}' - elif short_api_version != short_api_version_target: - logger.warning(f'User defined API version set to {self.api_version}, results may vary') + r = await self._session.get(f'http{secure}://{default_host}:{default_port}/api_version', timeout=self.timeout) + self.fbx_desc = await r.json() + + if host == 'auto': + host = self.fbx_desc['api_domain'] + + if port == 'auto': + port = self.fbx_desc['https_port'] + + server_version = self.fbx_desc['api_version'].split('.')[0] + short_api_version_target = self.api_version_target[1:] + short_api_version = self.api_version[1:] + + # Check auto api version + if self.api_version == 'auto': + self.api_version = self.api_version_target + # Check server api version + elif self.api_version == 'server': + self.api_version = f'v{server_version}' + if server_version > short_api_version_target: + logger.warning(f'Using new API version {self.api_version}, results may vary ') + # Check user api version + elif short_api_version < short_api_version_target and int(short_api_version) > 0: + logger.warning(f'Using deprecated API version {self.api_version}, results may vary ') + elif server_version < short_api_version: + logger.warning(f'Freebox server does not support this API version ({self.api_version}), downgrading to v{server_version}.') + self.api_version = f'v{server_version}' + elif short_api_version != short_api_version_target: + logger.warning(f'User defined API version set to {self.api_version}, results may vary') self._access = await self._get_freebox_access(host, port, self.api_version, self.token_file, self.app_desc, self.timeout) @@ -286,7 +274,8 @@ def _get_base_url(self, host, port, freebox_api_version): Returns base url for HTTPS requests :return: """ - return f'https://{host}:{port}/api/{freebox_api_version}/' + abu = self.fbx_desc['api_base_url'] + return f'https://{host}:{port}{abu}{freebox_api_version}/' def _is_app_desc_valid(self, app_desc): """ From 7023229b3afc74af761615ebf2e026be48885d96 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 16 Oct 2019 17:06:09 +0200 Subject: [PATCH 16/63] Update aiofreepybox.py: cleanup and fixes --- aiofreepybox/aiofreepybox.py | 212 ++++++++++++++++++++--------------- 1 file changed, 124 insertions(+), 88 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index c9f62838..6aaa78de 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -35,98 +35,119 @@ from aiofreepybox.api.upnpigd import Upnpigd # Token file default location -token_filename = 'app_auth' +token_filename = "app_auth" token_dir = os.path.dirname(os.path.abspath(__file__)) token_file = os.path.join(token_dir, token_filename) # Default application descriptor app_desc = { - 'app_id': 'aiofpbx', - 'app_name': 'aiofreepybox', - 'app_version': aiofreepybox.__version__, - 'device_name': socket.gethostname() - } + "app_id": "aiofpbx", + "app_name": "aiofreepybox", + "app_version": aiofreepybox.__version__, + "device_name": socket.gethostname() +} logger = logging.getLogger(__name__) class Freepybox: - def __init__(self, app_desc=app_desc, token_file=token_file, api_version='auto', timeout=10): - self.token_file = token_file + """ + This python library is implementing the freebox OS API. + It handles the authentication process and provides a raw access to the freebox API in an asynchronous manner. + """ + + def __init__( + self, app_desc=app_desc, token_file=token_file, api_version="auto", timeout=10 + ): + self._access = None self.api_version = api_version - self.api_version_target = 'v6' - self.timeout = timeout + self.api_version_target = "v6" self.app_desc = app_desc self.fbx_desc = {} - self._access = None + self.timeout = timeout + self.token_file = token_file - async def open(self, host='auto', port='auto'): + async def open(self, host="auto", port="auto"): """ Open a session to the freebox, get a valid access module and instantiate freebox modules """ if not self._is_app_desc_valid(self.app_desc): - raise InvalidTokenError('Invalid application descriptor') - - default_host = 'mafreebox.freebox.fr' - default_port = 443 - secure = 's' + raise InvalidTokenError("Invalid application descriptor") # Detect host, port and api_version - if host != 'auto': - default_host = host - - if port != 'auto': - default_port = port - if default_port == 80: - secure = '' + if host != "auto" and port == "auto": + # Fallback to http + port = 80 + default_host = host if host != "auto" else "mafreebox.freebox.fr" + default_port = port if port != "auto" else 443 + s = "s" if default_port != 80 else "" # Checking host and port sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((default_host, default_port)) if result != 0: - raise HttpRequestError(f'Port {default_port} is closed, cannot detect freebox') + raise HttpRequestError( + f"Port {default_port} is closed, cannot detect freebox" + ) else: sock.close() - cert_path = os.path.join(os.path.dirname(__file__), 'freebox_certificates.pem') + # Session setup + cert_path = os.path.join(os.path.dirname(__file__), "freebox_certificates.pem") ssl_ctx = ssl.create_default_context() ssl_ctx.load_verify_locations(cafile=cert_path) - conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) self._session = aiohttp.ClientSession(connector=conn) - - r = await self._session.get(f'http{secure}://{default_host}:{default_port}/api_version', timeout=self.timeout) + r = await self._session.get( + f"http{s}://{default_host}:{default_port}/api_version", timeout=self.timeout + ) self.fbx_desc = await r.json() - if host == 'auto': - host = self.fbx_desc['api_domain'] + # Setting host and port + host = ( + self.fbx_desc["api_domain"] + if host == "auto" or port == 80 + else default_host + ) + port = self.fbx_desc["https_port"] if port in ["auto", 80] else default_port - if port == 'auto': - port = self.fbx_desc['https_port'] - - server_version = self.fbx_desc['api_version'].split('.')[0] + # Check auto and server api version + server_version = self.fbx_desc["api_version"].split(".")[0] short_api_version_target = self.api_version_target[1:] - short_api_version = self.api_version[1:] + self.api_version = ( + self.api_version_target + if self.api_version == "auto" + else f"v{server_version}" + if self.api_version == "server" + else self.api_version + ) - # Check auto api version - if self.api_version == 'auto': - self.api_version = self.api_version_target - # Check server api version - elif self.api_version == 'server': - self.api_version = f'v{server_version}' - if server_version > short_api_version_target: - logger.warning(f'Using new API version {self.api_version}, results may vary ') # Check user api version - elif short_api_version < short_api_version_target and int(short_api_version) > 0: - logger.warning(f'Using deprecated API version {self.api_version}, results may vary ') - elif server_version < short_api_version: - logger.warning(f'Freebox server does not support this API version ({self.api_version}), downgrading to v{server_version}.') - self.api_version = f'v{server_version}' - elif short_api_version != short_api_version_target: - logger.warning(f'User defined API version set to {self.api_version}, results may vary') + short_api_version = self.api_version[1:] + if ( + short_api_version_target < server_version + and short_api_version == server_version + ): + logger.warning( + f"Using new API version {self.api_version}, results may vary " + ) + elif ( + short_api_version < short_api_version_target and int(short_api_version) > 0 + ): + logger.warning( + f"Using deprecated API version {self.api_version}, results may vary " + ) + elif short_api_version > server_version: + logger.warning( + f"Freebox server does not support this API version ({self.api_version}), downgrading to {self.api_version_target}." + ) + self.api_version = self.api_version_target - self._access = await self._get_freebox_access(host, port, self.api_version, self.token_file, self.app_desc, self.timeout) + # Get API access + self._access = await self._get_freebox_access( + host, port, self.api_version, self.token_file, self.app_desc, self.timeout + ) # Instantiate freebox modules self.tv = Tv(self._access) @@ -155,9 +176,9 @@ async def close(self): Close the freebox session """ if self._access is None: - raise NotOpenError('Freebox is not open') + raise NotOpenError("Freebox is not open") - await self._access.post('login/logout') + await self._access.post("login/logout") await self._session.close() async def get_permissions(self): @@ -179,7 +200,9 @@ async def get_permissions(self): else: return None - async def _get_freebox_access(self, host, port, api_version, token_file, app_desc, timeout=10): + async def _get_freebox_access( + self, host, port, api_version, token_file, app_desc, timeout=10 + ): """ Returns an access object used for HTTP requests. """ @@ -187,12 +210,12 @@ async def _get_freebox_access(self, host, port, api_version, token_file, app_des base_url = self._get_base_url(host, port, api_version) # Read stored application token - logger.info('Read application authorization file') + logger.info("Read application authorization file") app_token, track_id, file_app_desc = self._readfile_app_token(token_file) # If no valid token is stored then request a token to freebox api - Only for LAN connection if app_token is None or file_app_desc != app_desc: - logger.info('No valid authorization file found') + logger.info("No valid authorization file found") # Get application token from the freebox app_token, track_id = await self._get_app_token(base_url, app_desc, timeout) @@ -200,32 +223,38 @@ async def _get_freebox_access(self, host, port, api_version, token_file, app_des # Check the authorization status out_msg_flag = False status = None - while(status != 'granted'): - status = await self._get_authorization_status(base_url, track_id, timeout) + while status != "granted": + status = await self._get_authorization_status( + base_url, track_id, timeout + ) # denied status = authorization failed - if status == 'denied': - raise AuthorizationError('The app token is invalid or has been revoked') + if status == "denied": + raise AuthorizationError( + "The app token is invalid or has been revoked" + ) # Pending status : user must accept the app request on the freebox - elif status == 'pending': + elif status == "pending": if not out_msg_flag: out_msg_flag = True - print('Please confirm the authentification on the freebox') + print("Please confirm the authentification on the freebox") await asyncio.sleep(1) # timeout = authorization failed - elif status == 'timeout': - raise AuthorizationError('Authorization timed out') + elif status == "timeout": + raise AuthorizationError("Authorization timed out") - logger.info('Application authorization granted') + logger.info("Application authorization granted") # Store application token in file self._writefile_app_token(app_token, track_id, app_desc, token_file) - logger.info(f'Application token file was generated: {token_file}') + logger.info(f"Application token file was generated: {token_file}") # Create freebox http access module - fbx_access = Access(self._session, base_url, app_token, app_desc['app_id'], timeout) + fbx_access = Access( + self._session, base_url, app_token, app_desc["app_id"], timeout + ) return fbx_access @@ -239,10 +268,10 @@ async def _get_authorization_status(self, base_url, track_id, timeout): granted the app_token is valid and can be used to open a session denied the user denied the authorization request """ - url = urljoin(base_url, f'login/authorize/{track_id}') + url = urljoin(base_url, f"login/authorize/{track_id}") r = await self._session.get(url, timeout=timeout) resp = await r.json() - return resp['result']['status'] + return resp["result"]["status"] async def _get_app_token(self, base_url, app_desc, timeout=10): """ @@ -250,28 +279,29 @@ async def _get_app_token(self, base_url, app_desc, timeout=10): Returns (app_token, track_id) """ # Get authentification token - url = urljoin(base_url, 'login/authorize/') + url = urljoin(base_url, "login/authorize/") data = json.dumps(app_desc) r = await self._session.post(url, data=data, timeout=timeout) resp = await r.json() # raise exception if resp.success != True - if not resp.get('success'): - raise AuthorizationError('Authorization failed (APIResponse: {})' - .format(json.dumps(resp))) + if not resp.get("success"): + raise AuthorizationError( + "Authorization failed (APIResponse: {})".format(json.dumps(resp)) + ) - app_token = resp['result']['app_token'] - track_id = resp['result']['track_id'] + app_token = resp["result"]["app_token"] + track_id = resp["result"]["track_id"] - return(app_token, track_id) + return app_token, track_id def _writefile_app_token(self, app_token, track_id, app_desc, file): """ Store the application token in g_app_auth_file file """ - d = {**app_desc, 'app_token': app_token, 'track_id': track_id} + d = {**app_desc, "app_token": app_token, "track_id": track_id} - with open(file, 'w') as f: + with open(file, "w") as f: json.dump(d, f) def _readfile_app_token(self, file): @@ -280,26 +310,32 @@ def _readfile_app_token(self, file): Returns (app_token, track_id, app_desc) """ try: - with open(file, 'r') as f: + with open(file, "r") as f: d = json.load(f) - app_token = d['app_token'] - track_id = d['track_id'] - app_desc = {k: d[k] for k in ('app_id', 'app_name', 'app_version', 'device_name') if k in d} - return (app_token, track_id, app_desc) + app_token = d["app_token"] + track_id = d["track_id"] + app_desc = { + k: d[k] + for k in ("app_id", "app_name", "app_version", "device_name") + if k in d + } + return app_token, track_id, app_desc except FileNotFoundError: - return (None, None, None) + return None, None, None def _get_base_url(self, host, port, freebox_api_version): """ Returns base url for HTTPS requests :return: """ - abu = self.fbx_desc['api_base_url'] - return f'https://{host}:{port}{abu}{freebox_api_version}/' + abu = self.fbx_desc["api_base_url"] + return f"https://{host}:{port}{abu}{freebox_api_version}/" def _is_app_desc_valid(self, app_desc): """ Check validity of the application descriptor """ - return all(k in app_desc for k in ('app_id', 'app_name', 'app_version', 'device_name')) + return all( + k in app_desc for k in ("app_id", "app_name", "app_version", "device_name") + ) From 96f4a0d4c2d8e5a2b904af6eb5e07fe608d2750e Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 16 Oct 2019 18:08:49 +0200 Subject: [PATCH 17/63] Update aiofreepybox.py: update docstrings, some fixes --- aiofreepybox/aiofreepybox.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 6aaa78de..d5d09879 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -54,6 +54,9 @@ class Freepybox: """ This python library is implementing the freebox OS API. It handles the authentication process and provides a raw access to the freebox API in an asynchronous manner. + + api_version : "auto", "server" or "v(1-7)" + , Default to "auto" """ def __init__( @@ -71,17 +74,25 @@ async def open(self, host="auto", port="auto"): """ Open a session to the freebox, get a valid access module and instantiate freebox modules + + host : `str` + , Default to "auto" + port : `str` + , Default to "auto" """ if not self._is_app_desc_valid(self.app_desc): raise InvalidTokenError("Invalid application descriptor") - # Detect host, port and api_version - if host != "auto" and port == "auto": - # Fallback to http - port = 80 + # Detect host, port and API version default_host = host if host != "auto" else "mafreebox.freebox.fr" - default_port = port if port != "auto" else 443 - s = "s" if default_port != 80 else "" + default_port = ( + port + if port != "auto" + else 80 + if host != "auto" and port == "auto" + else 443 + ) + s = "" if default_port == 80 else "s" # Checking host and port sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -90,8 +101,7 @@ async def open(self, host="auto", port="auto"): raise HttpRequestError( f"Port {default_port} is closed, cannot detect freebox" ) - else: - sock.close() + sock.close() # Session setup cert_path = os.path.join(os.path.dirname(__file__), "freebox_certificates.pem") @@ -107,12 +117,12 @@ async def open(self, host="auto", port="auto"): # Setting host and port host = ( self.fbx_desc["api_domain"] - if host == "auto" or port == 80 + if host == "auto" or default_port == 80 else default_host ) port = self.fbx_desc["https_port"] if port in ["auto", 80] else default_port - # Check auto and server api version + # Check auto and server API version server_version = self.fbx_desc["api_version"].split(".")[0] short_api_version_target = self.api_version_target[1:] self.api_version = ( From 4d0950b05bc879fcb1cc713f8edac266a89e6df9 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Thu, 17 Oct 2019 03:21:43 +0200 Subject: [PATCH 18/63] Update aiofreepybox.py: small cleanup --- aiofreepybox/aiofreepybox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index d5d09879..bccffa58 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -89,7 +89,7 @@ async def open(self, host="auto", port="auto"): port if port != "auto" else 80 - if host != "auto" and port == "auto" + if host != "auto" else 443 ) s = "" if default_port == 80 else "s" From 2efa0c5c76766b810b802a048f7fc44cbed5233a Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Fri, 18 Oct 2019 19:45:30 +0200 Subject: [PATCH 19/63] Update aiofreepybox.py: more cleanup, add fbx_url --- aiofreepybox/aiofreepybox.py | 142 +++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 65 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index bccffa58..42c434e9 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -67,6 +67,7 @@ def __init__( self.api_version_target = "v6" self.app_desc = app_desc self.fbx_desc = {} + self.fbx_url = "" self.timeout = timeout self.token_file = token_file @@ -121,41 +122,12 @@ async def open(self, host="auto", port="auto"): else default_host ) port = self.fbx_desc["https_port"] if port in ["auto", 80] else default_port + self.fbx_url = self._get_base_url(host, port) - # Check auto and server API version - server_version = self.fbx_desc["api_version"].split(".")[0] - short_api_version_target = self.api_version_target[1:] - self.api_version = ( - self.api_version_target - if self.api_version == "auto" - else f"v{server_version}" - if self.api_version == "server" - else self.api_version - ) - - # Check user api version - short_api_version = self.api_version[1:] - if ( - short_api_version_target < server_version - and short_api_version == server_version - ): - logger.warning( - f"Using new API version {self.api_version}, results may vary " - ) - elif ( - short_api_version < short_api_version_target and int(short_api_version) > 0 - ): - logger.warning( - f"Using deprecated API version {self.api_version}, results may vary " - ) - elif short_api_version > server_version: - logger.warning( - f"Freebox server does not support this API version ({self.api_version}), downgrading to {self.api_version_target}." - ) - self.api_version = self.api_version_target + self._check_api_version() # Get API access - self._access = await self._get_freebox_access( + self._access = await self._get_app_access( host, port, self.api_version, self.token_file, self.app_desc, self.timeout ) @@ -210,7 +182,40 @@ async def get_permissions(self): else: return None - async def _get_freebox_access( + def _check_api_version(self): + # Check auto and server API version + server_version = self.fbx_desc["api_version"].split(".")[0] + short_api_version_target = self.api_version_target[1:] + self.api_version = ( + self.api_version_target + if self.api_version == "auto" + else f"v{server_version}" + if self.api_version == "server" + else self.api_version + ) + + # Check user api version + short_api_version = self.api_version[1:] + if ( + short_api_version_target < server_version + and short_api_version == server_version + ): + logger.warning( + f"Using new API version {self.api_version}, results may vary " + ) + elif ( + short_api_version < short_api_version_target and int(short_api_version) > 0 + ): + logger.warning( + f"Using deprecated API version {self.api_version}, results may vary " + ) + elif short_api_version > server_version: + logger.warning( + f"Freebox server does not support this API version ({self.api_version}), downgrading to {self.api_version_target}." + ) + self.api_version = self.api_version_target + + async def _get_app_access( self, host, port, api_version, token_file, app_desc, timeout=10 ): """ @@ -268,21 +273,6 @@ async def _get_freebox_access( return fbx_access - async def _get_authorization_status(self, base_url, track_id, timeout): - """ - Get authorization status of the application token - Returns: - unknown the app_token is invalid or has been revoked - pending the user has not confirmed the authorization request yet - timeout the user did not confirmed the authorization within the given time - granted the app_token is valid and can be used to open a session - denied the user denied the authorization request - """ - url = urljoin(base_url, f"login/authorize/{track_id}") - r = await self._session.get(url, timeout=timeout) - resp = await r.json() - return resp["result"]["status"] - async def _get_app_token(self, base_url, app_desc, timeout=10): """ Get the application token from the freebox @@ -305,14 +295,43 @@ async def _get_app_token(self, base_url, app_desc, timeout=10): return app_token, track_id - def _writefile_app_token(self, app_token, track_id, app_desc, file): + async def _get_authorization_status(self, base_url, track_id, timeout): """ - Store the application token in g_app_auth_file file + Get authorization status of the application token + Returns: + unknown the app_token is invalid or has been revoked + pending the user has not confirmed the authorization request yet + timeout the user did not confirmed the authorization within the given time + granted the app_token is valid and can be used to open a session + denied the user denied the authorization request """ - d = {**app_desc, "app_token": app_token, "track_id": track_id} + url = urljoin(base_url, f"login/authorize/{track_id}") + r = await self._session.get(url, timeout=timeout) + resp = await r.json() + return resp["result"]["status"] - with open(file, "w") as f: - json.dump(d, f) + def _get_base_url(self, host, port, freebox_api_version=None): + """ + Returns base url for HTTPS requests + + host : `str` + port : `str` + freebox_api_version : `str` + , Default to `None` + """ + if freebox_api_version is None: + return f"https://{host}:{port}" + else: + abu = self.fbx_desc["api_base_url"] + return f"https://{host}:{port}{abu}{freebox_api_version}/" + + def _is_app_desc_valid(self, app_desc): + """ + Check validity of the application descriptor + """ + return all( + k in app_desc for k in ("app_id", "app_name", "app_version", "device_name") + ) def _readfile_app_token(self, file): """ @@ -334,18 +353,11 @@ def _readfile_app_token(self, file): except FileNotFoundError: return None, None, None - def _get_base_url(self, host, port, freebox_api_version): + def _writefile_app_token(self, app_token, track_id, app_desc, file): """ - Returns base url for HTTPS requests - :return: + Store the application token in g_app_auth_file file """ - abu = self.fbx_desc["api_base_url"] - return f"https://{host}:{port}{abu}{freebox_api_version}/" + d = {**app_desc, "app_token": app_token, "track_id": track_id} - def _is_app_desc_valid(self, app_desc): - """ - Check validity of the application descriptor - """ - return all( - k in app_desc for k in ("app_id", "app_name", "app_version", "device_name") - ) + with open(file, "w") as f: + json.dump(d, f) From ba72a58b496e5b659273b9b9c29d444686f172ac Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 20 Oct 2019 12:34:54 +0200 Subject: [PATCH 20/63] Update aiofreepybox.py: remove unused import time, cleanup --- aiofreepybox/aiofreepybox.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 42c434e9..eed40aa0 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -1,15 +1,13 @@ +import aiohttp import asyncio import ipaddress -import os import json import logging +import os import socket import ssl -import time from urllib.parse import urljoin -import aiohttp - import aiofreepybox from aiofreepybox.exceptions import * from aiofreepybox.access import Access From be2b0f72cd889ba05597c4eeaccca590a8d9522e Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 20 Oct 2019 17:15:35 +0200 Subject: [PATCH 21/63] Update aiofreepybox.py: more cleanup --- aiofreepybox/aiofreepybox.py | 39 +++++++++++++++--------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index eed40aa0..de697f26 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -33,19 +33,19 @@ from aiofreepybox.api.upnpigd import Upnpigd # Token file default location -token_filename = "app_auth" -token_dir = os.path.dirname(os.path.abspath(__file__)) -token_file = os.path.join(token_dir, token_filename) +TOKEN_FILENAME = "app_auth" +TOKEN_DIR = os.path.dirname(os.path.abspath(__file__)) +TOKEN_FILE = os.path.join(TOKEN_DIR, TOKEN_FILENAME) # Default application descriptor -app_desc = { +APP_DESC = { "app_id": "aiofpbx", "app_name": "aiofreepybox", "app_version": aiofreepybox.__version__, - "device_name": socket.gethostname() + "device_name": socket.gethostname(), } -logger = logging.getLogger(__name__) +_LOGGER = logging.getLogger(__name__) class Freepybox: @@ -58,7 +58,7 @@ class Freepybox: """ def __init__( - self, app_desc=app_desc, token_file=token_file, api_version="auto", timeout=10 + self, app_desc=APP_DESC, token_file=TOKEN_FILE, api_version="auto", timeout=10 ): self._access = None self.api_version = api_version @@ -84,13 +84,7 @@ async def open(self, host="auto", port="auto"): # Detect host, port and API version default_host = host if host != "auto" else "mafreebox.freebox.fr" - default_port = ( - port - if port != "auto" - else 80 - if host != "auto" - else 443 - ) + default_port = port if port != "auto" else 80 if host != "auto" else 443 s = "" if default_port == 80 else "s" # Checking host and port @@ -198,17 +192,17 @@ def _check_api_version(self): short_api_version_target < server_version and short_api_version == server_version ): - logger.warning( + _LOGGER.warning( f"Using new API version {self.api_version}, results may vary " ) elif ( short_api_version < short_api_version_target and int(short_api_version) > 0 ): - logger.warning( + _LOGGER.warning( f"Using deprecated API version {self.api_version}, results may vary " ) elif short_api_version > server_version: - logger.warning( + _LOGGER.warning( f"Freebox server does not support this API version ({self.api_version}), downgrading to {self.api_version_target}." ) self.api_version = self.api_version_target @@ -223,12 +217,12 @@ async def _get_app_access( base_url = self._get_base_url(host, port, api_version) # Read stored application token - logger.info("Read application authorization file") + _LOGGER.info("Read application authorization file") app_token, track_id, file_app_desc = self._readfile_app_token(token_file) # If no valid token is stored then request a token to freebox api - Only for LAN connection if app_token is None or file_app_desc != app_desc: - logger.info("No valid authorization file found") + _LOGGER.info("No valid authorization file found") # Get application token from the freebox app_token, track_id = await self._get_app_token(base_url, app_desc, timeout) @@ -258,17 +252,16 @@ async def _get_app_access( elif status == "timeout": raise AuthorizationError("Authorization timed out") - logger.info("Application authorization granted") + _LOGGER.info("Application authorization granted") # Store application token in file self._writefile_app_token(app_token, track_id, app_desc, token_file) - logger.info(f"Application token file was generated: {token_file}") + _LOGGER.info(f"Application token file was generated: {token_file}") - # Create freebox http access module + # Create and return freebox http access module fbx_access = Access( self._session, base_url, app_token, app_desc["app_id"], timeout ) - return fbx_access async def _get_app_token(self, base_url, app_desc, timeout=10): From e159d1f0dab0ef302be165b630f6b204cd431571 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 22 Oct 2019 12:20:39 +0200 Subject: [PATCH 22/63] Update aiofreepybox.py: None is the new auto, refactor, cleanup, format, add discover --- aiofreepybox/aiofreepybox.py | 259 ++++++++++++++++++++++++----------- 1 file changed, 177 insertions(+), 82 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index de697f26..95d913e5 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -32,11 +32,6 @@ from aiofreepybox.api.upnpav import Upnpav from aiofreepybox.api.upnpigd import Upnpigd -# Token file default location -TOKEN_FILENAME = "app_auth" -TOKEN_DIR = os.path.dirname(os.path.abspath(__file__)) -TOKEN_FILE = os.path.join(TOKEN_DIR, TOKEN_FILENAME) - # Default application descriptor APP_DESC = { "app_id": "aiofpbx", @@ -45,6 +40,20 @@ "device_name": socket.gethostname(), } +# Token file default location +TOKEN_FILENAME = "app_auth" +TOKEN_DIR = os.path.dirname(os.path.abspath(__file__)) +TOKEN_FILE = os.path.join(TOKEN_DIR, TOKEN_FILENAME) + +# App defaults +_DEFAULT_API_VERSION = "v6" +_DEFAULT_CERT = "freebox_certificates.pem" +_DEFAULT_HOST = "mafreebox.freebox.fr" +_DEFAULT_HTTP_PORT = "80" +_DEFAULT_HTTPS_PORT = "443" +_DEFAULT_SSL = True +_DEFAULT_TIMEOUT = 10 + _LOGGER = logging.getLogger(__name__) @@ -57,66 +66,33 @@ class Freepybox: , Default to "auto" """ - def __init__( - self, app_desc=APP_DESC, token_file=TOKEN_FILE, api_version="auto", timeout=10 - ): - self._access = None - self.api_version = api_version - self.api_version_target = "v6" - self.app_desc = app_desc - self.fbx_desc = {} + def __init__(self, app_desc=None, token_file=None, api_version=None, timeout=None): + self.api_version = ( + api_version if api_version is not None else _DEFAULT_API_VERSION + ) + self.app_desc = app_desc if app_desc is not None else APP_DESC + self.fbx_desc = None self.fbx_url = "" - self.timeout = timeout - self.token_file = token_file + self.timeout = timeout if timeout is not None else _DEFAULT_TIMEOUT + self.token_file = token_file if token_file is not None else TOKEN_FILE + self._access = None + self._session = None - async def open(self, host="auto", port="auto"): + async def open(self, host=None, port=None): """ Open a session to the freebox, get a valid access module and instantiate freebox modules host : `str` - , Default to "auto" + , Default to `None` port : `str` - , Default to "auto" + , Default to `None` """ + if not self._is_app_desc_valid(self.app_desc): raise InvalidTokenError("Invalid application descriptor") - # Detect host, port and API version - default_host = host if host != "auto" else "mafreebox.freebox.fr" - default_port = port if port != "auto" else 80 if host != "auto" else 443 - s = "" if default_port == 80 else "s" - - # Checking host and port - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = sock.connect_ex((default_host, default_port)) - if result != 0: - raise HttpRequestError( - f"Port {default_port} is closed, cannot detect freebox" - ) - sock.close() - - # Session setup - cert_path = os.path.join(os.path.dirname(__file__), "freebox_certificates.pem") - ssl_ctx = ssl.create_default_context() - ssl_ctx.load_verify_locations(cafile=cert_path) - conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) - self._session = aiohttp.ClientSession(connector=conn) - r = await self._session.get( - f"http{s}://{default_host}:{default_port}/api_version", timeout=self.timeout - ) - self.fbx_desc = await r.json() - - # Setting host and port - host = ( - self.fbx_desc["api_domain"] - if host == "auto" or default_port == 80 - else default_host - ) - port = self.fbx_desc["https_port"] if port in ["auto", 80] else default_port - self.fbx_url = self._get_base_url(host, port) - - self._check_api_version() + host, port = await self._check_and_validate_parameters(host, port) # Get API access self._access = await self._get_app_access( @@ -149,11 +125,77 @@ async def close(self): """ Close the freebox session """ - if self._access is None: - raise NotOpenError("Freebox is not open") + + if self._access is None or self._session.closed: + _LOGGER.warning(f"Closing but freebox is not connected") + return await self._access.post("login/logout") await self._session.close() + await asyncio.sleep(0.250) + + async def discover(self, default_host=None, default_port=None): + """ + Discover a freebox on the network + """ + + # Setup host and port + if default_host is None: + default_host = _DEFAULT_HOST + + default_port = ( + _DEFAULT_HTTPS_PORT + if default_port is None and _DEFAULT_SSL + else default_port + if _DEFAULT_SSL + else _DEFAULT_HTTP_PORT + ) + s = "" if default_port == _DEFAULT_HTTP_PORT or not _DEFAULT_SSL else "s" + + # Check session + if ( + self.fbx_desc is not None + and self._session is not None + and not self._session.closed + ): + conns = list(self._session._connector._conns.keys())[0] + if default_host != conns.host or default_port != conns.port: + self.fbx_desc = None + await self._session.close() + await asyncio.sleep(0.250) + return await self.discover(default_host, default_port) + else: + return self.fbx_desc + + # Try to connect + if ( + self._session is None + or not isinstance(self._session, aiohttp.ClientSession) + or self._session.closed + ): + + # Checking host and port + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + result = sock.connect_ex((default_host, int(default_port))) + if result != 0: + return None + sock.close() + + try: + cert_path = os.path.join(os.path.dirname(__file__), _DEFAULT_CERT) + ssl_ctx = ssl.create_default_context() + ssl_ctx.load_verify_locations(cafile=cert_path) + conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) + self._session = aiohttp.ClientSession(connector=conn) + except aiohttp.client_exceptions.ClientConnectorError or aiohttp.client_exceptions.ClientConnectorCertificateError or ssl.SSLCertVerificationError: + return None + + # Found freebox + r = await self._session.get( + f"http{s}://{default_host}:{default_port}/api_version", timeout=self.timeout + ) + self.fbx_desc = await r.json() + return self.fbx_desc async def get_permissions(self): """ @@ -169,46 +211,90 @@ async def get_permissions(self): until the session token is refreshed. If the session has not been opened yet, returns None. """ + if self._access: return await self._access.get_permissions() else: return None + async def _check_and_validate_parameters(self, target_host, target_port): + """ + Validate host and port + + target_host : `str` + target_port : `str` + """ + + # Discover + if await self.discover(target_host, target_port) is None: + if self._session is not None: + await self._session.close() + await asyncio.sleep(0.250) + raise HttpRequestError( + f"Cannot detect freebox on the network, please check your configuration." + ) + + # Setting host and port + host = ( + self.fbx_desc["api_domain"] + if ( + (target_host is None or target_port == _DEFAULT_HTTP_PORT) + or (target_host is not None and target_port is None) + ) + and _DEFAULT_SSL + else _DEFAULT_HOST + if target_host is None and not _DEFAULT_SSL + else target_host + ) + port = ( + self.fbx_desc["https_port"] + if (target_port is None or target_port == _DEFAULT_HTTP_PORT) + and _DEFAULT_SSL + else target_port + if _DEFAULT_SSL + else _DEFAULT_HTTP_PORT + ) + + await self.discover(host, port) + self._check_api_version() + self.fbx_url = self._get_base_url(host, port) + + return host, port + def _check_api_version(self): - # Check auto and server API version + """ + Check api version + """ + + # Set API version if needed server_version = self.fbx_desc["api_version"].split(".")[0] - short_api_version_target = self.api_version_target[1:] - self.api_version = ( - self.api_version_target - if self.api_version == "auto" - else f"v{server_version}" - if self.api_version == "server" - else self.api_version - ) + short_api_version_target = _DEFAULT_API_VERSION[1:] + if self.api_version == "server": + self.api_version = f"v{server_version}" - # Check user api version + # Check user API version short_api_version = self.api_version[1:] if ( short_api_version_target < server_version and short_api_version == server_version ): _LOGGER.warning( - f"Using new API version {self.api_version}, results may vary " + f"Using new API version {self.api_version}, results may vary." ) elif ( short_api_version < short_api_version_target and int(short_api_version) > 0 ): _LOGGER.warning( - f"Using deprecated API version {self.api_version}, results may vary " + f"Using deprecated API version {self.api_version}, results may vary." ) - elif short_api_version > server_version: + elif short_api_version > server_version or int(short_api_version) < 1: _LOGGER.warning( - f"Freebox server does not support this API version ({self.api_version}), downgrading to {self.api_version_target}." + f"Freebox server does not support this API version ({self.api_version}), resetting to {_DEFAULT_API_VERSION}." ) - self.api_version = self.api_version_target + self.api_version = _DEFAULT_API_VERSION async def _get_app_access( - self, host, port, api_version, token_file, app_desc, timeout=10 + self, host, port, api_version, token_file, app_desc, timeout=_DEFAULT_TIMEOUT ): """ Returns an access object used for HTTP requests. @@ -217,12 +303,14 @@ async def _get_app_access( base_url = self._get_base_url(host, port, api_version) # Read stored application token - _LOGGER.info("Read application authorization file") + _LOGGER.debug("Reading application authorization file.") app_token, track_id, file_app_desc = self._readfile_app_token(token_file) # If no valid token is stored then request a token to freebox api - Only for LAN connection if app_token is None or file_app_desc != app_desc: - _LOGGER.info("No valid authorization file found") + _LOGGER.warning( + "No valid authorization file found, requesting authorization." + ) # Get application token from the freebox app_token, track_id = await self._get_app_token(base_url, app_desc, timeout) @@ -238,25 +326,25 @@ async def _get_app_access( # denied status = authorization failed if status == "denied": raise AuthorizationError( - "The app token is invalid or has been revoked" + "The app token is invalid or has been revoked." ) # Pending status : user must accept the app request on the freebox elif status == "pending": if not out_msg_flag: out_msg_flag = True - print("Please confirm the authentification on the freebox") + print("Please confirm the authentification on the freebox.") await asyncio.sleep(1) # timeout = authorization failed elif status == "timeout": - raise AuthorizationError("Authorization timed out") + raise AuthorizationError("Authorization timed out.") - _LOGGER.info("Application authorization granted") + _LOGGER.info("Application authorization granted.") # Store application token in file self._writefile_app_token(app_token, track_id, app_desc, token_file) - _LOGGER.info(f"Application token file was generated: {token_file}") + _LOGGER.info(f"Application token file was generated: {token_file}.") # Create and return freebox http access module fbx_access = Access( @@ -269,6 +357,7 @@ async def _get_app_token(self, base_url, app_desc, timeout=10): Get the application token from the freebox Returns (app_token, track_id) """ + # Get authentification token url = urljoin(base_url, "login/authorize/") data = json.dumps(app_desc) @@ -278,7 +367,7 @@ async def _get_app_token(self, base_url, app_desc, timeout=10): # raise exception if resp.success != True if not resp.get("success"): raise AuthorizationError( - "Authorization failed (APIResponse: {})".format(json.dumps(resp)) + "Authorization failed (APIResponse: {}).".format(json.dumps(resp)) ) app_token = resp["result"]["app_token"] @@ -296,6 +385,7 @@ async def _get_authorization_status(self, base_url, track_id, timeout): granted the app_token is valid and can be used to open a session denied the user denied the authorization request """ + url = urljoin(base_url, f"login/authorize/{track_id}") r = await self._session.get(url, timeout=timeout) resp = await r.json() @@ -303,23 +393,26 @@ async def _get_authorization_status(self, base_url, track_id, timeout): def _get_base_url(self, host, port, freebox_api_version=None): """ - Returns base url for HTTPS requests + Returns base url for HTTP(S) requests host : `str` port : `str` freebox_api_version : `str` , Default to `None` """ + + s = "s" if _DEFAULT_SSL else "" if freebox_api_version is None: - return f"https://{host}:{port}" + return f"http{s}://{host}:{port}" else: abu = self.fbx_desc["api_base_url"] - return f"https://{host}:{port}{abu}{freebox_api_version}/" + return f"http{s}://{host}:{port}{abu}{freebox_api_version}/" def _is_app_desc_valid(self, app_desc): """ Check validity of the application descriptor """ + return all( k in app_desc for k in ("app_id", "app_name", "app_version", "device_name") ) @@ -329,6 +422,7 @@ def _readfile_app_token(self, file): Read the application token in the authentication file. Returns (app_token, track_id, app_desc) """ + try: with open(file, "r") as f: d = json.load(f) @@ -348,6 +442,7 @@ def _writefile_app_token(self, app_token, track_id, app_desc, file): """ Store the application token in g_app_auth_file file """ + d = {**app_desc, "app_token": app_token, "track_id": track_id} with open(file, "w") as f: From ae293fe3cba75aaf498e7b989431312f49522eba Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 22 Oct 2019 16:00:11 +0200 Subject: [PATCH 23/63] Update aiofreepybox.py: update docstring --- aiofreepybox/aiofreepybox.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 95d913e5..48b8f479 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -62,8 +62,14 @@ class Freepybox: This python library is implementing the freebox OS API. It handles the authentication process and provides a raw access to the freebox API in an asynchronous manner. - api_version : "auto", "server" or "v(1-7)" - , Default to "auto" + app_desc : `dict` + , Default to APP_DESC + token_file : `str` + , Default to TOKEN_FILE + api_version : `str`, "server" or "v(1-7)" + , Default to _DEFAULT_API_VERSION + timeout : `int` + , Default to _DEFAULT_TIMEOUT """ def __init__(self, app_desc=None, token_file=None, api_version=None, timeout=None): From 680e7eea290ed081cadd770791ea1303b94b78ed Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 22 Oct 2019 17:17:37 +0200 Subject: [PATCH 24/63] Update aiofreepybox.py: update error message and improve detection --- aiofreepybox/aiofreepybox.py | 39 ++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 48b8f479..16f294d6 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -148,6 +148,11 @@ async def discover(self, default_host=None, default_port=None): # Setup host and port if default_host is None: default_host = _DEFAULT_HOST + elif self._is_ipv4(default_host) and default_port is None: + default_port = _DEFAULT_HTTP_PORT + elif self._is_ipv6(default_host): + _LOGGER.error(f"IPv6 is not supported") + return None default_port = ( _DEFAULT_HTTPS_PORT @@ -184,14 +189,21 @@ async def discover(self, default_host=None, default_port=None): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((default_host, int(default_port))) if result != 0: - return None + if default_port != _DEFAULT_HTTP_PORT: + return await self.discover(default_host, _DEFAULT_HTTP_PORT) + else: + return None sock.close() try: - cert_path = os.path.join(os.path.dirname(__file__), _DEFAULT_CERT) - ssl_ctx = ssl.create_default_context() - ssl_ctx.load_verify_locations(cafile=cert_path) - conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) + if s == "s": + cert_path = os.path.join(os.path.dirname(__file__), _DEFAULT_CERT) + ssl_ctx = ssl.create_default_context() + ssl_ctx.load_verify_locations(cafile=cert_path) + conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) + else: + conn = aiohttp.TCPConnector() + self._session = aiohttp.ClientSession(connector=conn) except aiohttp.client_exceptions.ClientConnectorError or aiohttp.client_exceptions.ClientConnectorCertificateError or ssl.SSLCertVerificationError: return None @@ -236,8 +248,9 @@ async def _check_and_validate_parameters(self, target_host, target_port): if self._session is not None: await self._session.close() await asyncio.sleep(0.250) + unknown = "?" raise HttpRequestError( - f"Cannot detect freebox on the network, please check your configuration." + f"Cannot detect freebox at {target_host if target_host is not None else unknown}:{target_port if target_port is not None else unknown}, please check your configuration." ) # Setting host and port @@ -423,6 +436,20 @@ def _is_app_desc_valid(self, app_desc): k in app_desc for k in ("app_id", "app_name", "app_version", "device_name") ) + def _is_ipv4(self, string): + try: + ipaddress.IPv4Network(string) + return True + except ValueError: + return False + + def _is_ipv6(self, string): + try: + ipaddress.IPv6Network(string) + return True + except ValueError: + return False + def _readfile_app_token(self, file): """ Read the application token in the authentication file. From 818f54ab1830d1760d9d3d75ff4869c67ea9cca2 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 22 Oct 2019 17:50:29 +0200 Subject: [PATCH 25/63] Update aiofreepybox.py: fix error message --- aiofreepybox/aiofreepybox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 16f294d6..06b08d3a 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -151,7 +151,7 @@ async def discover(self, default_host=None, default_port=None): elif self._is_ipv4(default_host) and default_port is None: default_port = _DEFAULT_HTTP_PORT elif self._is_ipv6(default_host): - _LOGGER.error(f"IPv6 is not supported") + _LOGGER.error(f"{default_host} : IPv6 is not supported") return None default_port = ( From 7803eed270767482bce5ac5a8173e3b34984bfa9 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 22 Oct 2019 22:56:57 +0200 Subject: [PATCH 26/63] Update aiofreepybox.py: update docstring, improve error detection --- aiofreepybox/aiofreepybox.py | 88 +++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 16 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 06b08d3a..ff7d244c 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -9,7 +9,13 @@ from urllib.parse import urljoin import aiofreepybox -from aiofreepybox.exceptions import * +from aiofreepybox.exceptions import ( + AuthorizationError, + HttpRequestError, + InsufficientPermissionsError, + InvalidTokenError, + NotOpenError, +) from aiofreepybox.access import Access from aiofreepybox.api.tv import Tv from aiofreepybox.api.system import System @@ -143,6 +149,11 @@ async def close(self): async def discover(self, default_host=None, default_port=None): """ Discover a freebox on the network + + default_host : `str` + , Default to None + default_port : `str` + , Default to None """ # Setup host and port @@ -209,10 +220,16 @@ async def discover(self, default_host=None, default_port=None): return None # Found freebox - r = await self._session.get( - f"http{s}://{default_host}:{default_port}/api_version", timeout=self.timeout - ) - self.fbx_desc = await r.json() + try: + r = await self._session.get( + f"http{s}://{default_host}:{default_port}/api_version", + timeout=self.timeout, + ) + self.fbx_desc = await r.json() + except aiohttp.client_exceptions.ClientResponseError: + raise HttpRequestError( + f"A network error occurred while reading from the freebox at {default_host}:{default_port}" + ) return self.fbx_desc async def get_permissions(self): @@ -249,7 +266,7 @@ async def _check_and_validate_parameters(self, target_host, target_port): await self._session.close() await asyncio.sleep(0.250) unknown = "?" - raise HttpRequestError( + raise NotOpenError( f"Cannot detect freebox at {target_host if target_host is not None else unknown}:{target_port if target_port is not None else unknown}, please check your configuration." ) @@ -316,7 +333,14 @@ async def _get_app_access( self, host, port, api_version, token_file, app_desc, timeout=_DEFAULT_TIMEOUT ): """ - Returns an access object used for HTTP requests. + Returns an access object used for HTTP(S) requests. + + host : `str` + port : `str` + api_version : `str` + token_file : `str` + app_desc : `dict` + timeout : `int` """ base_url = self._get_base_url(host, port, api_version) @@ -371,10 +395,15 @@ async def _get_app_access( ) return fbx_access - async def _get_app_token(self, base_url, app_desc, timeout=10): + async def _get_app_token(self, base_url, app_desc, timeout=_DEFAULT_TIMEOUT): """ Get the application token from the freebox - Returns (app_token, track_id) + + base_url : `str` + app_desc : `dict` + timeout : `int` + + Returns app_token, track_id """ # Get authentification token @@ -394,9 +423,16 @@ async def _get_app_token(self, base_url, app_desc, timeout=10): return app_token, track_id - async def _get_authorization_status(self, base_url, track_id, timeout): + async def _get_authorization_status( + self, base_url, track_id, timeout=_DEFAULT_TIMEOUT + ): """ Get authorization status of the application token + + base_url : `str` + track_id : `str` + timeout : `int` + Returns: unknown the app_token is invalid or has been revoked pending the user has not confirmed the authorization request yet @@ -430,22 +466,34 @@ def _get_base_url(self, host, port, freebox_api_version=None): def _is_app_desc_valid(self, app_desc): """ Check validity of the application descriptor + + app_desc : `dict` """ return all( k in app_desc for k in ("app_id", "app_name", "app_version", "device_name") ) - def _is_ipv4(self, string): + def _is_ipv4(self, ip_address): + """ + Check ip version for v4 + + ip_address : `str` + """ try: - ipaddress.IPv4Network(string) + ipaddress.IPv4Network(ip_address) return True except ValueError: return False - def _is_ipv6(self, string): + def _is_ipv6(self, ip_address): + """ + Check ip version for v6 + + ip_address : `str` + """ try: - ipaddress.IPv6Network(string) + ipaddress.IPv6Network(ip_address) return True except ValueError: return False @@ -453,7 +501,10 @@ def _is_ipv6(self, string): def _readfile_app_token(self, file): """ Read the application token in the authentication file. - Returns (app_token, track_id, app_desc) + + file : `str` + + Returns app_token, track_id, app_desc """ try: @@ -473,7 +524,12 @@ def _readfile_app_token(self, file): def _writefile_app_token(self, app_token, track_id, app_desc, file): """ - Store the application token in g_app_auth_file file + Store the application token in a TOKEN_FILE file + + app_token : `str` + track_id : `str` + app_desc : `dict` + file : `str` """ d = {**app_desc, "app_token": app_token, "track_id": track_id} From 1041bdfb668d0a17b090f62939c51b88d9f218d4 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Fri, 25 Oct 2019 12:17:59 +0200 Subject: [PATCH 27/63] Update aiofreepybox.py: fix socket close, refactor --- aiofreepybox/aiofreepybox.py | 38 +++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index ff7d244c..bf888294 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -59,6 +59,7 @@ _DEFAULT_HTTPS_PORT = "443" _DEFAULT_SSL = True _DEFAULT_TIMEOUT = 10 +_DEFAULT_UKNOWN = "?" _LOGGER = logging.getLogger(__name__) @@ -156,6 +157,16 @@ async def discover(self, default_host=None, default_port=None): , Default to None """ + async def _close_to_return(self): + """Close session""" + + if self.fbx_desc is not None: + self.fbx_desc = None + if self._session is not None and not self._session.closed(): + await self._session.close() + await asyncio.sleep(0.250) + return None + # Setup host and port if default_host is None: default_host = _DEFAULT_HOST @@ -163,7 +174,7 @@ async def discover(self, default_host=None, default_port=None): default_port = _DEFAULT_HTTP_PORT elif self._is_ipv6(default_host): _LOGGER.error(f"{default_host} : IPv6 is not supported") - return None + return await _close_to_return() default_port = ( _DEFAULT_HTTPS_PORT @@ -182,15 +193,13 @@ async def discover(self, default_host=None, default_port=None): ): conns = list(self._session._connector._conns.keys())[0] if default_host != conns.host or default_port != conns.port: - self.fbx_desc = None - await self._session.close() - await asyncio.sleep(0.250) + await _close_to_return() return await self.discover(default_host, default_port) else: return self.fbx_desc - # Try to connect - if ( + # Connect if session is closed + elif ( self._session is None or not isinstance(self._session, aiohttp.ClientSession) or self._session.closed @@ -199,13 +208,14 @@ async def discover(self, default_host=None, default_port=None): # Checking host and port sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock.connect_ex((default_host, int(default_port))) + sock.close() if result != 0: if default_port != _DEFAULT_HTTP_PORT: return await self.discover(default_host, _DEFAULT_HTTP_PORT) else: - return None - sock.close() + return await _close_to_return() + # Connect try: if s == "s": cert_path = os.path.join(os.path.dirname(__file__), _DEFAULT_CERT) @@ -214,10 +224,10 @@ async def discover(self, default_host=None, default_port=None): conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) else: conn = aiohttp.TCPConnector() - self._session = aiohttp.ClientSession(connector=conn) + except aiohttp.client_exceptions.ClientConnectorError or aiohttp.client_exceptions.ClientConnectorCertificateError or ssl.SSLCertVerificationError: - return None + return await _close_to_return() # Found freebox try: @@ -226,10 +236,13 @@ async def discover(self, default_host=None, default_port=None): timeout=self.timeout, ) self.fbx_desc = await r.json() + except aiohttp.client_exceptions.ClientResponseError: + await _close_to_return() raise HttpRequestError( f"A network error occurred while reading from the freebox at {default_host}:{default_port}" ) + return self.fbx_desc async def get_permissions(self): @@ -262,10 +275,7 @@ async def _check_and_validate_parameters(self, target_host, target_port): # Discover if await self.discover(target_host, target_port) is None: - if self._session is not None: - await self._session.close() - await asyncio.sleep(0.250) - unknown = "?" + unknown = _DEFAULT_UNKNOWN raise NotOpenError( f"Cannot detect freebox at {target_host if target_host is not None else unknown}:{target_port if target_port is not None else unknown}, please check your configuration." ) From 262e09845456531f904f712c4c0b9769e49185c8 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Fri, 25 Oct 2019 12:19:37 +0200 Subject: [PATCH 28/63] Update aiofreepybox.py: fix default --- aiofreepybox/aiofreepybox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index bf888294..9f2b4bd4 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -59,7 +59,7 @@ _DEFAULT_HTTPS_PORT = "443" _DEFAULT_SSL = True _DEFAULT_TIMEOUT = 10 -_DEFAULT_UKNOWN = "?" +_DEFAULT_UNKNOWN = "?" _LOGGER = logging.getLogger(__name__) From 18e93cdd71be8154dab899a8ab1a517ece605576 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sat, 26 Oct 2019 12:52:37 +0200 Subject: [PATCH 29/63] Update aiofreepybox.py: format fixes refactor --- aiofreepybox/aiofreepybox.py | 152 ++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 74 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 9f2b4bd4..f2dd3ab3 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -39,7 +39,7 @@ from aiofreepybox.api.upnpigd import Upnpigd # Default application descriptor -APP_DESC = { +_APP_DESC = { "app_id": "aiofpbx", "app_name": "aiofreepybox", "app_version": aiofreepybox.__version__, @@ -47,19 +47,20 @@ } # Token file default location -TOKEN_FILENAME = "app_auth" -TOKEN_DIR = os.path.dirname(os.path.abspath(__file__)) -TOKEN_FILE = os.path.join(TOKEN_DIR, TOKEN_FILENAME) +_TOKEN_FILENAME = "app_auth" +_TOKEN_DIR = os.path.dirname(os.path.abspath(__file__)) +_TOKEN_FILE = os.path.join(_TOKEN_DIR, _TOKEN_FILENAME) # App defaults _DEFAULT_API_VERSION = "v6" _DEFAULT_CERT = "freebox_certificates.pem" +_DEFAULT_DEVICE_NAME = "Freebox Server" _DEFAULT_HOST = "mafreebox.freebox.fr" _DEFAULT_HTTP_PORT = "80" _DEFAULT_HTTPS_PORT = "443" _DEFAULT_SSL = True _DEFAULT_TIMEOUT = 10 -_DEFAULT_UNKNOWN = "?" +_DEFAULT_UNKNOWN = "None" _LOGGER = logging.getLogger(__name__) @@ -70,9 +71,9 @@ class Freepybox: It handles the authentication process and provides a raw access to the freebox API in an asynchronous manner. app_desc : `dict` - , Default to APP_DESC + , Default to _APP_DESC token_file : `str` - , Default to TOKEN_FILE + , Default to _TOKEN_FILE api_version : `str`, "server" or "v(1-7)" , Default to _DEFAULT_API_VERSION timeout : `int` @@ -83,11 +84,11 @@ def __init__(self, app_desc=None, token_file=None, api_version=None, timeout=Non self.api_version = ( api_version if api_version is not None else _DEFAULT_API_VERSION ) - self.app_desc = app_desc if app_desc is not None else APP_DESC + self.app_desc = app_desc if app_desc is not None else _APP_DESC self.fbx_desc = None self.fbx_url = "" self.timeout = timeout if timeout is not None else _DEFAULT_TIMEOUT - self.token_file = token_file if token_file is not None else TOKEN_FILE + self.token_file = token_file if token_file is not None else _TOKEN_FILE self._access = None self._session = None @@ -157,63 +158,55 @@ async def discover(self, default_host=None, default_port=None): , Default to None """ - async def _close_to_return(self): + async def _close_to_return(): """Close session""" if self.fbx_desc is not None: self.fbx_desc = None - if self._session is not None and not self._session.closed(): + if self._session is not None and not self._session.closed: await self._session.close() await asyncio.sleep(0.250) return None # Setup host and port - if default_host is None: + if not default_host: default_host = _DEFAULT_HOST - elif self._is_ipv4(default_host) and default_port is None: + if self._is_ipv4(default_host): default_port = _DEFAULT_HTTP_PORT elif self._is_ipv6(default_host): _LOGGER.error(f"{default_host} : IPv6 is not supported") return await _close_to_return() - default_port = ( - _DEFAULT_HTTPS_PORT - if default_port is None and _DEFAULT_SSL - else default_port - if _DEFAULT_SSL - else _DEFAULT_HTTP_PORT - ) - s = "" if default_port == _DEFAULT_HTTP_PORT or not _DEFAULT_SSL else "s" + if not default_port: + default_port = _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else _DEFAULT_HTTP_PORT + + s = "s" if default_port != _DEFAULT_HTTP_PORT and _DEFAULT_SSL else "" # Check session - if ( - self.fbx_desc is not None - and self._session is not None - and not self._session.closed - ): + if self.fbx_desc and self._session is not None and not self._session.closed: conns = list(self._session._connector._conns.keys())[0] if default_host != conns.host or default_port != conns.port: - await _close_to_return() + await self._session.close() + await asyncio.sleep(0.250) return await self.discover(default_host, default_port) else: return self.fbx_desc # Connect if session is closed - elif ( - self._session is None - or not isinstance(self._session, aiohttp.ClientSession) - or self._session.closed + elif any( + [ + self._session is None, + not isinstance(self._session, aiohttp.ClientSession), + (self._session and self._session.closed), + ] ): - # Checking host and port sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(_DEFAULT_TIMEOUT) result = sock.connect_ex((default_host, int(default_port))) sock.close() if result != 0: - if default_port != _DEFAULT_HTTP_PORT: - return await self.discover(default_host, _DEFAULT_HTTP_PORT) - else: - return await _close_to_return() + return await _close_to_return() # Connect try: @@ -226,7 +219,7 @@ async def _close_to_return(self): conn = aiohttp.TCPConnector() self._session = aiohttp.ClientSession(connector=conn) - except aiohttp.client_exceptions.ClientConnectorError or aiohttp.client_exceptions.ClientConnectorCertificateError or ssl.SSLCertVerificationError: + except ssl.SSLCertVerificationError: return await _close_to_return() # Found freebox @@ -235,13 +228,20 @@ async def _close_to_return(self): f"http{s}://{default_host}:{default_port}/api_version", timeout=self.timeout, ) - self.fbx_desc = await r.json() + if r.content_type != 'application/json': + return await _close_to_return() + else: + self.fbx_desc = await r.json() - except aiohttp.client_exceptions.ClientResponseError: + except ssl.SSLCertVerificationError as e: await _close_to_return() - raise HttpRequestError( - f"A network error occurred while reading from the freebox at {default_host}:{default_port}" - ) + raise HttpRequestError(f"{e}") + # Only for testing + #except Exception as e: + #raise e + + if self.fbx_desc["device_name"] != _DEFAULT_DEVICE_NAME: + return await _close_to_return() return self.fbx_desc @@ -273,40 +273,43 @@ async def _check_and_validate_parameters(self, target_host, target_port): target_port : `str` """ + fbx_host = fbx_port = None # Discover - if await self.discover(target_host, target_port) is None: - unknown = _DEFAULT_UNKNOWN - raise NotOpenError( - f"Cannot detect freebox at {target_host if target_host is not None else unknown}:{target_port if target_port is not None else unknown}, please check your configuration." - ) - - # Setting host and port - host = ( - self.fbx_desc["api_domain"] - if ( - (target_host is None or target_port == _DEFAULT_HTTP_PORT) - or (target_host is not None and target_port is None) + if await self.discover(target_host, target_port): + # Setting host and port + fbx_ssl = self.fbx_desc["https_available"] if _DEFAULT_SSL else _DEFAULT_SSL + fbx_host, fbx_port = ( + target_host + if not not target_host + and ( + any([(not self._is_ipv4(target_host)), target_port]) + and target_port != _DEFAULT_HTTPS_PORT + ) + else (self.fbx_desc["api_domain"] if fbx_ssl else _DEFAULT_HOST), + target_port + if target_port is not None + and target_port.isdigit() + and not not target_host + and not self._is_ipv4(target_host) + or (target_host and not self._is_ipv4(target_host)) + else (self.fbx_desc["https_port"] if fbx_ssl else _DEFAULT_HTTP_PORT), ) - and _DEFAULT_SSL - else _DEFAULT_HOST - if target_host is None and not _DEFAULT_SSL - else target_host + if await self.discover(fbx_host, fbx_port): + self._check_api_version() + self.fbx_url = self._get_base_url(fbx_host, fbx_port) + return fbx_host, fbx_port + + unk = _DEFAULT_UNKNOWN + host, port = ( + next(v for v in [target_host, fbx_host, unk] if v), + next(v for v in [target_port, fbx_port, unk] if v), ) - port = ( - self.fbx_desc["https_port"] - if (target_port is None or target_port == _DEFAULT_HTTP_PORT) - and _DEFAULT_SSL - else target_port - if _DEFAULT_SSL - else _DEFAULT_HTTP_PORT + raise NotOpenError( + "Cannot detect freebox at " + f"{host}:{port}" + ", please check your configuration." ) - await self.discover(host, port) - self._check_api_version() - self.fbx_url = self._get_base_url(host, port) - - return host, port - def _check_api_version(self): """ Check api version @@ -335,7 +338,8 @@ def _check_api_version(self): ) elif short_api_version > server_version or int(short_api_version) < 1: _LOGGER.warning( - f"Freebox server does not support this API version ({self.api_version}), resetting to {_DEFAULT_API_VERSION}." + "Freebox server does not support this API version (" + f"{self.api_version}), resetting to {_DEFAULT_API_VERSION}." ) self.api_version = _DEFAULT_API_VERSION @@ -466,7 +470,7 @@ def _get_base_url(self, host, port, freebox_api_version=None): , Default to `None` """ - s = "s" if _DEFAULT_SSL else "" + s = "s" if self.fbx_desc["https_available"] or _DEFAULT_SSL else "" if freebox_api_version is None: return f"http{s}://{host}:{port}" else: @@ -534,7 +538,7 @@ def _readfile_app_token(self, file): def _writefile_app_token(self, app_token, track_id, app_desc, file): """ - Store the application token in a TOKEN_FILE file + Store the application token in a _TOKEN_FILE file app_token : `str` track_id : `str` From 506e550f883e12c6dfcd6cfa05475d83246e2170 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 27 Oct 2019 00:10:05 +0200 Subject: [PATCH 30/63] Update aiofreepybox.py: refactor --- aiofreepybox/aiofreepybox.py | 130 ++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 62 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index f2dd3ab3..2b09fe48 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -148,13 +148,13 @@ async def close(self): await self._session.close() await asyncio.sleep(0.250) - async def discover(self, default_host=None, default_port=None): + async def discover(self, host=None, port=None): """ Discover a freebox on the network - default_host : `str` + host : `str` , Default to None - default_port : `str` + port : `str` , Default to None """ @@ -169,26 +169,30 @@ async def _close_to_return(): return None # Setup host and port - if not default_host: - default_host = _DEFAULT_HOST - if self._is_ipv4(default_host): - default_port = _DEFAULT_HTTP_PORT - elif self._is_ipv6(default_host): - _LOGGER.error(f"{default_host} : IPv6 is not supported") + if self._is_ipv6(host): + _LOGGER.error(f"{host} : IPv6 is not supported") return await _close_to_return() - if not default_port: - default_port = _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else _DEFAULT_HTTP_PORT + s = "s" if _DEFAULT_SSL and port != _DEFAULT_HTTP_PORT else "" - s = "s" if default_port != _DEFAULT_HTTP_PORT and _DEFAULT_SSL else "" + if host is None and port is None: + host, port = ( + _DEFAULT_HOST, + _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else _DEFAULT_HTTP_PORT, + ) + elif port is None: + port = _DEFAULT_HTTP_PORT + s = "" + elif host is None: + host = _DEFAULT_HOST # Check session if self.fbx_desc and self._session is not None and not self._session.closed: conns = list(self._session._connector._conns.keys())[0] - if default_host != conns.host or default_port != conns.port: + if host != conns.host or port != conns.port: await self._session.close() await asyncio.sleep(0.250) - return await self.discover(default_host, default_port) + return await self.discover(host, port) else: return self.fbx_desc @@ -203,7 +207,7 @@ async def _close_to_return(): # Checking host and port sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(_DEFAULT_TIMEOUT) - result = sock.connect_ex((default_host, int(default_port))) + result = sock.connect_ex((host, int(port))) sock.close() if result != 0: return await _close_to_return() @@ -225,20 +229,19 @@ async def _close_to_return(): # Found freebox try: r = await self._session.get( - f"http{s}://{default_host}:{default_port}/api_version", - timeout=self.timeout, + f"http{s}://{host}:{port}/api_version", timeout=self.timeout ) - if r.content_type != 'application/json': - return await _close_to_return() - else: - self.fbx_desc = await r.json() - except ssl.SSLCertVerificationError as e: await _close_to_return() raise HttpRequestError(f"{e}") # Only for testing - #except Exception as e: - #raise e + # except Exception as e: + # raise e + + if r.content_type != "application/json": + return await _close_to_return() + else: + self.fbx_desc = await r.json() if self.fbx_desc["device_name"] != _DEFAULT_DEVICE_NAME: return await _close_to_return() @@ -265,50 +268,47 @@ async def get_permissions(self): else: return None - async def _check_and_validate_parameters(self, target_host, target_port): + async def _check_and_validate_parameters(self, host, port): """ Validate host and port - target_host : `str` - target_port : `str` + host : `str` + port : `str` """ - fbx_host = fbx_port = None - # Discover - if await self.discover(target_host, target_port): - # Setting host and port - fbx_ssl = self.fbx_desc["https_available"] if _DEFAULT_SSL else _DEFAULT_SSL - fbx_host, fbx_port = ( - target_host - if not not target_host - and ( - any([(not self._is_ipv4(target_host)), target_port]) - and target_port != _DEFAULT_HTTPS_PORT + try: + if await self.discover(host, port) is None: + raise ValueError + + if _DEFAULT_SSL and self.fbx_desc["https_available"]: + host, port = ( + self.fbx_desc["api_domain"] if host is None else host, + self.fbx_desc["https_port"] if port is None else port, + ) + else: + host, port = ( + _DEFAULT_HOST if host is None else host, + _DEFAULT_HTTP_PORT if port is None else port, ) - else (self.fbx_desc["api_domain"] if fbx_ssl else _DEFAULT_HOST), - target_port - if target_port is not None - and target_port.isdigit() - and not not target_host - and not self._is_ipv4(target_host) - or (target_host and not self._is_ipv4(target_host)) - else (self.fbx_desc["https_port"] if fbx_ssl else _DEFAULT_HTTP_PORT), + + if await self.discover(host, port) is None: + raise ValueError + + except (ValueError, HttpRequestError): + unk = _DEFAULT_UNKNOWN + host, port = ( + next(v for v in [host, unk] if v), + next(v for v in [port, unk] if v), + ) + raise NotOpenError( + "Cannot detect freebox at " + f"{host}:{port}" + ", please check your configuration." ) - if await self.discover(fbx_host, fbx_port): - self._check_api_version() - self.fbx_url = self._get_base_url(fbx_host, fbx_port) - return fbx_host, fbx_port - - unk = _DEFAULT_UNKNOWN - host, port = ( - next(v for v in [target_host, fbx_host, unk] if v), - next(v for v in [target_port, fbx_port, unk] if v), - ) - raise NotOpenError( - "Cannot detect freebox at " - f"{host}:{port}" - ", please check your configuration." - ) + + self._check_api_version() + self.fbx_url = self._get_base_url(host, port) + return host, port def _check_api_version(self): """ @@ -470,7 +470,13 @@ def _get_base_url(self, host, port, freebox_api_version=None): , Default to `None` """ - s = "s" if self.fbx_desc["https_available"] or _DEFAULT_SSL else "" + s = ( + "s" + if _DEFAULT_SSL + and self.fbx_desc["https_available"] + and port != _DEFAULT_HTTP_PORT + else "" + ) if freebox_api_version is None: return f"http{s}://{host}:{port}" else: From 77f152b4d5b7427aa462f550eb273da08dd8960e Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 27 Oct 2019 12:34:16 +0100 Subject: [PATCH 31/63] Update aiofreepybox.py: refactor --- aiofreepybox/aiofreepybox.py | 244 +++++++++++++++++++---------------- 1 file changed, 133 insertions(+), 111 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 2b09fe48..2bbef16d 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -106,11 +106,13 @@ async def open(self, host=None, port=None): if not self._is_app_desc_valid(self.app_desc): raise InvalidTokenError("Invalid application descriptor") - host, port = await self._check_and_validate_parameters(host, port) - # Get API access self._access = await self._get_app_access( - host, port, self.api_version, self.token_file, self.app_desc, self.timeout + *await self._open_init(host, port), + self.api_version, + self.token_file, + self.app_desc, + self.timeout, ) # Instantiate freebox modules @@ -158,73 +160,26 @@ async def discover(self, host=None, port=None): , Default to None """ - async def _close_to_return(): - """Close session""" - - if self.fbx_desc is not None: - self.fbx_desc = None - if self._session is not None and not self._session.closed: - await self._session.close() - await asyncio.sleep(0.250) - return None - - # Setup host and port if self._is_ipv6(host): _LOGGER.error(f"{host} : IPv6 is not supported") - return await _close_to_return() - - s = "s" if _DEFAULT_SSL and port != _DEFAULT_HTTP_PORT else "" - - if host is None and port is None: - host, port = ( - _DEFAULT_HOST, - _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else _DEFAULT_HTTP_PORT, - ) - elif port is None: - port = _DEFAULT_HTTP_PORT - s = "" - elif host is None: - host = _DEFAULT_HOST + return await self._disc_close_to_return() # Check session - if self.fbx_desc and self._session is not None and not self._session.closed: - conns = list(self._session._connector._conns.keys())[0] - if host != conns.host or port != conns.port: - await self._session.close() - await asyncio.sleep(0.250) - return await self.discover(host, port) - else: - return self.fbx_desc + sess = await self._disc_check_session(*self._disc_set_host_and_port(host, port)) + if not isinstance(sess, tuple): + return sess + else: + host, port, s = sess # Connect if session is closed - elif any( + if any( [ self._session is None, not isinstance(self._session, aiohttp.ClientSession), (self._session and self._session.closed), ] - ): - # Checking host and port - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(_DEFAULT_TIMEOUT) - result = sock.connect_ex((host, int(port))) - sock.close() - if result != 0: - return await _close_to_return() - - # Connect - try: - if s == "s": - cert_path = os.path.join(os.path.dirname(__file__), _DEFAULT_CERT) - ssl_ctx = ssl.create_default_context() - ssl_ctx.load_verify_locations(cafile=cert_path) - conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) - else: - conn = aiohttp.TCPConnector() - self._session = aiohttp.ClientSession(connector=conn) - - except ssl.SSLCertVerificationError: - return await _close_to_return() + ) and not await self._disc_connect(host, port, s): + return None # Found freebox try: @@ -232,19 +187,19 @@ async def _close_to_return(): f"http{s}://{host}:{port}/api_version", timeout=self.timeout ) except ssl.SSLCertVerificationError as e: - await _close_to_return() + await self._disc_close_to_return() raise HttpRequestError(f"{e}") # Only for testing # except Exception as e: # raise e if r.content_type != "application/json": - return await _close_to_return() + return await self._disc_close_to_return() else: self.fbx_desc = await r.json() if self.fbx_desc["device_name"] != _DEFAULT_DEVICE_NAME: - return await _close_to_return() + return await self._disc_close_to_return() return self.fbx_desc @@ -268,48 +223,6 @@ async def get_permissions(self): else: return None - async def _check_and_validate_parameters(self, host, port): - """ - Validate host and port - - host : `str` - port : `str` - """ - - try: - if await self.discover(host, port) is None: - raise ValueError - - if _DEFAULT_SSL and self.fbx_desc["https_available"]: - host, port = ( - self.fbx_desc["api_domain"] if host is None else host, - self.fbx_desc["https_port"] if port is None else port, - ) - else: - host, port = ( - _DEFAULT_HOST if host is None else host, - _DEFAULT_HTTP_PORT if port is None else port, - ) - - if await self.discover(host, port) is None: - raise ValueError - - except (ValueError, HttpRequestError): - unk = _DEFAULT_UNKNOWN - host, port = ( - next(v for v in [host, unk] if v), - next(v for v in [port, unk] if v), - ) - raise NotOpenError( - "Cannot detect freebox at " - f"{host}:{port}" - ", please check your configuration." - ) - - self._check_api_version() - self.fbx_url = self._get_base_url(host, port) - return host, port - def _check_api_version(self): """ Check api version @@ -343,6 +256,74 @@ def _check_api_version(self): ) self.api_version = _DEFAULT_API_VERSION + async def _disc_check_session(self, host, port, s): + """Check discovery session""" + if self.fbx_desc and self._session is not None and not self._session.closed: + conns = list(self._session._connector._conns.keys())[0] + if ( + conns.host == host + and conns.port == int(port) + and conns.is_ssl == (not not s) + ): + return self.fbx_desc + elif await self._disc_close_to_return() is None: + return await self.discover(host, port) + return host, port, s + + async def _disc_close_to_return(self): + """Close discovery session""" + + if self.fbx_desc is not None: + self.fbx_desc = None + if self._session is not None and not self._session.closed: + await self._session.close() + await asyncio.sleep(0.250) + return None + + async def _disc_connect(self, host, port, s): + """Connect for discovery""" + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(_DEFAULT_TIMEOUT) + result = sock.connect_ex((host, int(port))) + sock.close() + if result != 0: + return await self._disc_close_to_return() + + # Connect + try: + if s == "s": + cert_path = os.path.join(os.path.dirname(__file__), _DEFAULT_CERT) + ssl_ctx = ssl.create_default_context() + ssl_ctx.load_verify_locations(cafile=cert_path) + conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) + else: + conn = aiohttp.TCPConnector() + self._session = aiohttp.ClientSession(connector=conn) + + except ssl.SSLCertVerificationError: + return await self._disc_close_to_return() + + return True + + def _disc_set_host_and_port(self, host, port): + """Set discovery host and port""" + + s = "s" if _DEFAULT_SSL and port != _DEFAULT_HTTP_PORT else "" + + if not host and not port: + host, port = ( + _DEFAULT_HOST, + _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else _DEFAULT_HTTP_PORT, + ) + elif not port: + port = _DEFAULT_HTTP_PORT + s = "" + elif not host: + host = _DEFAULT_HOST + + return host, port, s + async def _get_app_access( self, host, port, api_version, token_file, app_desc, timeout=_DEFAULT_TIMEOUT ): @@ -470,13 +451,7 @@ def _get_base_url(self, host, port, freebox_api_version=None): , Default to `None` """ - s = ( - "s" - if _DEFAULT_SSL - and self.fbx_desc["https_available"] - and port != _DEFAULT_HTTP_PORT - else "" - ) + s = "s" if list(self._session._connector._conns.keys())[0].is_ssl else "" if freebox_api_version is None: return f"http{s}://{host}:{port}" else: @@ -518,6 +493,53 @@ def _is_ipv6(self, ip_address): except ValueError: return False + async def _open_init(self, host, port): + """Init host and port for open""" + + try: + if await self.discover(host, port) is None: + raise ValueError + + host, port = self._open_setup(host, port) + + if await self.discover(host, port) is None: + raise ValueError + + except (ValueError, HttpRequestError): + unk = _DEFAULT_UNKNOWN + host, port = ( + next(v for v in [host, unk] if v), + next(v for v in [port, unk] if v), + ) + raise NotOpenError( + "Cannot detect freebox at " + f"{host}:{port}" + ", please check your configuration." + ) + + self._check_api_version() + self.fbx_url = self._get_base_url(host, port) + return host, port + + def _open_setup(self, host, port): + """Setup host and port value for open""" + + if _DEFAULT_SSL and self.fbx_desc["https_available"]: + host, port = ( + self.fbx_desc["api_domain"] + if host is None or self._is_ipv4(host) + else host, + self.fbx_desc["https_port"] + if port is None or port == _DEFAULT_HTTP_PORT + else port, + ) + else: + host, port = ( + _DEFAULT_HOST if host is None else host, + _DEFAULT_HTTP_PORT if port is None else port, + ) + return host, port + def _readfile_app_token(self, file): """ Read the application token in the authentication file. From 05148d1c17c13b9509df8b2588e5569e10e9db68 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 27 Oct 2019 15:16:07 +0100 Subject: [PATCH 32/63] Update aiofreepybox.py: cleanup --- aiofreepybox/aiofreepybox.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 2bbef16d..9ce5eaf1 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -165,11 +165,12 @@ async def discover(self, host=None, port=None): return await self._disc_close_to_return() # Check session - sess = await self._disc_check_session(*self._disc_set_host_and_port(host, port)) - if not isinstance(sess, tuple): - return sess - else: - host, port, s = sess + try: + host, port, s = await self._disc_check_session( + *self._disc_set_host_and_port(host, port) + ) + except ValueError as err: + return err.args[0] # Connect if session is closed if any( @@ -195,8 +196,8 @@ async def discover(self, host=None, port=None): if r.content_type != "application/json": return await self._disc_close_to_return() - else: - self.fbx_desc = await r.json() + + self.fbx_desc = await r.json() if self.fbx_desc["device_name"] != _DEFAULT_DEVICE_NAME: return await self._disc_close_to_return() @@ -220,8 +221,8 @@ async def get_permissions(self): if self._access: return await self._access.get_permissions() - else: - return None + + return None def _check_api_version(self): """ @@ -265,9 +266,9 @@ async def _disc_check_session(self, host, port, s): and conns.port == int(port) and conns.is_ssl == (not not s) ): - return self.fbx_desc + raise ValueError(self.fbx_desc) elif await self._disc_close_to_return() is None: - return await self.discover(host, port) + raise ValueError(await self.discover(host, port)) return host, port, s async def _disc_close_to_return(self): @@ -454,9 +455,9 @@ def _get_base_url(self, host, port, freebox_api_version=None): s = "s" if list(self._session._connector._conns.keys())[0].is_ssl else "" if freebox_api_version is None: return f"http{s}://{host}:{port}" - else: - abu = self.fbx_desc["api_base_url"] - return f"http{s}://{host}:{port}{abu}{freebox_api_version}/" + + abu = self.fbx_desc["api_base_url"] + return f"http{s}://{host}:{port}{abu}{freebox_api_version}/" def _is_app_desc_valid(self, app_desc): """ From 2d7648431a3547aef990babd5bc677130a280ee2 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 6 Nov 2019 22:23:30 +0100 Subject: [PATCH 33/63] Update aiofreepybox: cleanup --- aiofreepybox/aiofreepybox.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 9ce5eaf1..a46914c0 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -68,7 +68,8 @@ class Freepybox: """ This python library is implementing the freebox OS API. - It handles the authentication process and provides a raw access to the freebox API in an asynchronous manner. + It handles the authentication process and provides a raw access + to the freebox API in an asynchronous manner. app_desc : `dict` , Default to _APP_DESC @@ -190,9 +191,6 @@ async def discover(self, host=None, port=None): except ssl.SSLCertVerificationError as e: await self._disc_close_to_return() raise HttpRequestError(f"{e}") - # Only for testing - # except Exception as e: - # raise e if r.content_type != "application/json": return await self._disc_close_to_return() @@ -259,6 +257,7 @@ def _check_api_version(self): async def _disc_check_session(self, host, port, s): """Check discovery session""" + if self.fbx_desc and self._session is not None and not self._session.closed: conns = list(self._session._connector._conns.keys())[0] if ( @@ -269,6 +268,7 @@ async def _disc_check_session(self, host, port, s): raise ValueError(self.fbx_desc) elif await self._disc_close_to_return() is None: raise ValueError(await self.discover(host, port)) + return host, port, s async def _disc_close_to_return(self): @@ -279,6 +279,7 @@ async def _disc_close_to_return(self): if self._session is not None and not self._session.closed: await self._session.close() await asyncio.sleep(0.250) + return None async def _disc_connect(self, host, port, s): @@ -301,7 +302,6 @@ async def _disc_connect(self, host, port, s): else: conn = aiohttp.TCPConnector() self._session = aiohttp.ClientSession(connector=conn) - except ssl.SSLCertVerificationError: return await self._disc_close_to_return() @@ -414,8 +414,7 @@ async def _get_app_token(self, base_url, app_desc, timeout=_DEFAULT_TIMEOUT): "Authorization failed (APIResponse: {}).".format(json.dumps(resp)) ) - app_token = resp["result"]["app_token"] - track_id = resp["result"]["track_id"] + app_token, track_id = resp["result"]["app_token"], resp["result"]["track_id"] return app_token, track_id @@ -440,6 +439,7 @@ async def _get_authorization_status( url = urljoin(base_url, f"login/authorize/{track_id}") r = await self._session.get(url, timeout=timeout) resp = await r.json() + return resp["result"]["status"] def _get_base_url(self, host, port, freebox_api_version=None): @@ -465,7 +465,6 @@ def _is_app_desc_valid(self, app_desc): app_desc : `dict` """ - return all( k in app_desc for k in ("app_id", "app_name", "app_version", "device_name") ) @@ -476,6 +475,7 @@ def _is_ipv4(self, ip_address): ip_address : `str` """ + try: ipaddress.IPv4Network(ip_address) return True @@ -488,6 +488,7 @@ def _is_ipv6(self, ip_address): ip_address : `str` """ + try: ipaddress.IPv6Network(ip_address) return True @@ -505,7 +506,6 @@ async def _open_init(self, host, port): if await self.discover(host, port) is None: raise ValueError - except (ValueError, HttpRequestError): unk = _DEFAULT_UNKNOWN host, port = ( @@ -520,6 +520,7 @@ async def _open_init(self, host, port): self._check_api_version() self.fbx_url = self._get_base_url(host, port) + return host, port def _open_setup(self, host, port): @@ -539,6 +540,7 @@ def _open_setup(self, host, port): _DEFAULT_HOST if host is None else host, _DEFAULT_HTTP_PORT if port is None else port, ) + return host, port def _readfile_app_token(self, file): @@ -561,7 +563,6 @@ def _readfile_app_token(self, file): if k in d } return app_token, track_id, app_desc - except FileNotFoundError: return None, None, None @@ -576,6 +577,5 @@ def _writefile_app_token(self, app_token, track_id, app_desc, file): """ d = {**app_desc, "app_token": app_token, "track_id": track_id} - with open(file, "w") as f: json.dump(d, f) From f83ef09240155bf63b96076b7ceef5ea963e11c3 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Thu, 7 Nov 2019 22:48:32 +0100 Subject: [PATCH 34/63] Update aiofreepybox.py: docstring --- aiofreepybox/aiofreepybox.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index a46914c0..a3725b78 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -71,13 +71,13 @@ class Freepybox: It handles the authentication process and provides a raw access to the freebox API in an asynchronous manner. - app_desc : `dict` + app_desc : `dict` , optional , Default to _APP_DESC - token_file : `str` + token_file : `str` , optional , Default to _TOKEN_FILE - api_version : `str`, "server" or "v(1-7)" + api_version : `str`, "server" or "v(1-7)" , optional , Default to _DEFAULT_API_VERSION - timeout : `int` + timeout : `int` , optional , Default to _DEFAULT_TIMEOUT """ @@ -98,9 +98,9 @@ async def open(self, host=None, port=None): Open a session to the freebox, get a valid access module and instantiate freebox modules - host : `str` + host : `str` , optional , Default to `None` - port : `str` + port : `str` , optional , Default to `None` """ @@ -155,9 +155,9 @@ async def discover(self, host=None, port=None): """ Discover a freebox on the network - host : `str` + host : `str` , optional , Default to None - port : `str` + port : `str` , optional , Default to None """ @@ -336,7 +336,8 @@ async def _get_app_access( api_version : `str` token_file : `str` app_desc : `dict` - timeout : `int` + timeout : `int` , optional + , Default to _DEFAULT_TIMEOUT """ base_url = self._get_base_url(host, port, api_version) @@ -397,7 +398,8 @@ async def _get_app_token(self, base_url, app_desc, timeout=_DEFAULT_TIMEOUT): base_url : `str` app_desc : `dict` - timeout : `int` + timeout : `int` , optional + , Default to _DEFAULT_TIMEOUT Returns app_token, track_id """ @@ -426,7 +428,8 @@ async def _get_authorization_status( base_url : `str` track_id : `str` - timeout : `int` + timeout : `int` , optional + , Default to _DEFAULT_TIMEOUT Returns: unknown the app_token is invalid or has been revoked @@ -448,7 +451,7 @@ def _get_base_url(self, host, port, freebox_api_version=None): host : `str` port : `str` - freebox_api_version : `str` + freebox_api_version : `str` , optional , Default to `None` """ From edf2c7dac122c0510633f6213d288260643c88aa Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Fri, 8 Nov 2019 01:00:17 +0100 Subject: [PATCH 35/63] Update aiofreepybox.py: cleanup --- aiofreepybox/aiofreepybox.py | 68 +++++++++++++++--------------------- 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index a3725b78..048a7ebd 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -185,19 +185,20 @@ async def discover(self, host=None, port=None): # Found freebox try: - r = await self._session.get( + async with self._session.get( f"http{s}://{host}:{port}/api_version", timeout=self.timeout - ) + ) as r: + if r.content_type != "application/json": + return await self._disc_close_to_return() + self.fbx_desc = await r.json() except ssl.SSLCertVerificationError as e: await self._disc_close_to_return() raise HttpRequestError(f"{e}") - if r.content_type != "application/json": - return await self._disc_close_to_return() - - self.fbx_desc = await r.json() - - if self.fbx_desc["device_name"] != _DEFAULT_DEVICE_NAME: + if not "device_name" in self.fbx_desc or ( + "device_name" in self.fbx_desc + and self.fbx_desc["device_name"] != _DEFAULT_DEVICE_NAME + ): return await self._disc_close_to_return() return self.fbx_desc @@ -219,7 +220,6 @@ async def get_permissions(self): if self._access: return await self._access.get_permissions() - return None def _check_api_version(self): @@ -228,27 +228,22 @@ def _check_api_version(self): """ # Set API version if needed - server_version = self.fbx_desc["api_version"].split(".")[0] - short_api_version_target = _DEFAULT_API_VERSION[1:] + s_fbx_version = self.fbx_desc["api_version"].split(".")[0] + s_default_api_version = _DEFAULT_API_VERSION[1:] if self.api_version == "server": - self.api_version = f"v{server_version}" + self.api_version = f"v{s_fbx_version}" # Check user API version - short_api_version = self.api_version[1:] - if ( - short_api_version_target < server_version - and short_api_version == server_version - ): + s_api_version = self.api_version[1:] + if s_default_api_version < s_fbx_version and s_api_version == s_fbx_version: _LOGGER.warning( f"Using new API version {self.api_version}, results may vary." ) - elif ( - short_api_version < short_api_version_target and int(short_api_version) > 0 - ): + elif 0 < int(s_api_version) and s_api_version < s_default_api_version: _LOGGER.warning( f"Using deprecated API version {self.api_version}, results may vary." ) - elif short_api_version > server_version or int(short_api_version) < 1: + elif 1 > int(s_api_version) or s_api_version > s_fbx_version: _LOGGER.warning( "Freebox server does not support this API version (" f"{self.api_version}), resetting to {_DEFAULT_API_VERSION}." @@ -259,12 +254,8 @@ async def _disc_check_session(self, host, port, s): """Check discovery session""" if self.fbx_desc and self._session is not None and not self._session.closed: - conns = list(self._session._connector._conns.keys())[0] - if ( - conns.host == host - and conns.port == int(port) - and conns.is_ssl == (not not s) - ): + c = list(self._session._connector._conns.keys())[0] + if c.host == host and c.port == int(port) and c.is_ssl == (not not s): raise ValueError(self.fbx_desc) elif await self._disc_close_to_return() is None: raise ValueError(await self.discover(host, port)) @@ -274,11 +265,10 @@ async def _disc_check_session(self, host, port, s): async def _disc_close_to_return(self): """Close discovery session""" - if self.fbx_desc is not None: - self.fbx_desc = None if self._session is not None and not self._session.closed: await self._session.close() await asyncio.sleep(0.250) + self.fbx_desc = None if self.fbx_desc is not None else self.fbx_desc return None @@ -407,8 +397,8 @@ async def _get_app_token(self, base_url, app_desc, timeout=_DEFAULT_TIMEOUT): # Get authentification token url = urljoin(base_url, "login/authorize/") data = json.dumps(app_desc) - r = await self._session.post(url, data=data, timeout=timeout) - resp = await r.json() + async with self._session.post(url, data=data, timeout=timeout) as r: + resp = await r.json() # raise exception if resp.success != True if not resp.get("success"): @@ -417,7 +407,6 @@ async def _get_app_token(self, base_url, app_desc, timeout=_DEFAULT_TIMEOUT): ) app_token, track_id = resp["result"]["app_token"], resp["result"]["track_id"] - return app_token, track_id async def _get_authorization_status( @@ -440,27 +429,26 @@ async def _get_authorization_status( """ url = urljoin(base_url, f"login/authorize/{track_id}") - r = await self._session.get(url, timeout=timeout) - resp = await r.json() - - return resp["result"]["status"] + async with self._session.get(url, timeout=timeout) as r: + resp = await r.json() + return resp["result"]["status"] - def _get_base_url(self, host, port, freebox_api_version=None): + def _get_base_url(self, host, port, fbx_api_version=None): """ Returns base url for HTTP(S) requests host : `str` port : `str` - freebox_api_version : `str` , optional + fbx_api_version : `str` , optional , Default to `None` """ s = "s" if list(self._session._connector._conns.keys())[0].is_ssl else "" - if freebox_api_version is None: + if fbx_api_version is None: return f"http{s}://{host}:{port}" abu = self.fbx_desc["api_base_url"] - return f"http{s}://{host}:{port}{abu}{freebox_api_version}/" + return f"http{s}://{host}:{port}{abu}{fbx_api_version}/" def _is_app_desc_valid(self, app_desc): """ From 1dceb664eaa1189ebfe60fd3b24e468fcaea32e3 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sat, 9 Nov 2019 00:18:55 +0100 Subject: [PATCH 36/63] Update example.py: fix home examples --- example.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example.py b/example.py index 303d2f30..f45eb71c 100755 --- a/example.py +++ b/example.py @@ -27,11 +27,14 @@ async def demo(): if fbx.api_version == 'v6': # Get a jpg snapshot from a camera fbx_cam_jpg = await fbx.home.get_camera_snapshot() + fbx_cam_jpg.close() # Get a TS stream from a camera r = await fbx.home.get_camera_stream_m3u8() m3u8_obj = m3u8.loads(await r.text()) + r.close() fbx_ts = await fbx.home.get_camera_ts(m3u8_obj.files[0]) + fbx_ts.close() # Dump freebox configuration using system API # Extract temperature and mac address From 9b6f56f2ef87594f7b9c4b70ada68d6631e51acf Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sat, 9 Nov 2019 05:18:48 +0100 Subject: [PATCH 37/63] Update aiofreepybox.py: typing --- aiofreepybox/aiofreepybox.py | 215 +++++++++++++++++++++-------------- 1 file changed, 131 insertions(+), 84 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 048a7ebd..d5e54cfd 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -39,6 +39,8 @@ from aiofreepybox.api.upnpigd import Upnpigd # Default application descriptor +from typing import Dict, Optional, Tuple, Union + _APP_DESC = { "app_id": "aiofpbx", "app_name": "aiofreepybox", @@ -81,19 +83,27 @@ class Freepybox: , Default to _DEFAULT_TIMEOUT """ - def __init__(self, app_desc=None, token_file=None, api_version=None, timeout=None): - self.api_version = ( + def __init__( + self, + app_desc: Optional[Dict[str, str]] = None, + token_file: Optional[str] = None, + api_version: Optional[str] = None, + timeout: Optional[int] = None, + ) -> None: + self.api_version: str = ( api_version if api_version is not None else _DEFAULT_API_VERSION ) - self.app_desc = app_desc if app_desc is not None else _APP_DESC - self.fbx_desc = None - self.fbx_url = "" - self.timeout = timeout if timeout is not None else _DEFAULT_TIMEOUT - self.token_file = token_file if token_file is not None else _TOKEN_FILE - self._access = None - self._session = None - - async def open(self, host=None, port=None): + self.app_desc: Dict[str, str] = app_desc if app_desc is not None else _APP_DESC + self._fbx_desc: dict = {} + self._fbx_url: str = "" + self.timeout: int = timeout if timeout is not None else _DEFAULT_TIMEOUT + self.token_file: str = token_file if token_file is not None else _TOKEN_FILE + self._access: Optional[Access] = None + self._session: Optional[aiohttp.ClientSession] = None + + async def open( + self, host: Optional[str] = None, port: Optional[str] = None + ) -> None: """ Open a session to the freebox, get a valid access module and instantiate freebox modules @@ -109,10 +119,10 @@ async def open(self, host=None, port=None): # Get API access self._access = await self._get_app_access( - *await self._open_init(host, port), self.api_version, self.token_file, self.app_desc, + *await self._open_init(host, port), self.timeout, ) @@ -138,20 +148,22 @@ async def open(self, host=None, port=None): self.upnpav = Upnpav(self._access) self.upnpigd = Upnpigd(self._access) - async def close(self): + async def close(self) -> None: """ Close the freebox session """ - if self._access is None or self._session.closed: + if self._access is None or self._session.closed: # type: ignore # noqa _LOGGER.warning(f"Closing but freebox is not connected") return await self._access.post("login/logout") - await self._session.close() + await self._session.close() # type: ignore # noqa await asyncio.sleep(0.250) - async def discover(self, host=None, port=None): + async def discover( + self, host_in: Optional[str] = None, port_in: Optional[str] = None + ) -> Optional[dict]: """ Discover a freebox on the network @@ -161,14 +173,14 @@ async def discover(self, host=None, port=None): , Default to None """ - if self._is_ipv6(host): - _LOGGER.error(f"{host} : IPv6 is not supported") + if host_in and self._is_ipv6(host_in): + _LOGGER.error(f"{host_in} : IPv6 is not supported") return await self._disc_close_to_return() # Check session try: host, port, s = await self._disc_check_session( - *self._disc_set_host_and_port(host, port) + *self._disc_set_host_and_port(host_in, port_in) ) except ValueError as err: return err.args[0] @@ -185,25 +197,22 @@ async def discover(self, host=None, port=None): # Found freebox try: - async with self._session.get( + async with self._session.get( # type: ignore # noqa f"http{s}://{host}:{port}/api_version", timeout=self.timeout ) as r: if r.content_type != "application/json": return await self._disc_close_to_return() - self.fbx_desc = await r.json() + self._fbx_desc = await r.json() except ssl.SSLCertVerificationError as e: await self._disc_close_to_return() raise HttpRequestError(f"{e}") - if not "device_name" in self.fbx_desc or ( - "device_name" in self.fbx_desc - and self.fbx_desc["device_name"] != _DEFAULT_DEVICE_NAME - ): + if self._fbx_desc.get("device_name", None) != _DEFAULT_DEVICE_NAME: return await self._disc_close_to_return() - return self.fbx_desc + return self._fbx_desc - async def get_permissions(self): + async def get_permissions(self) -> Optional[dict]: """ Returns the permissions for this app. @@ -222,13 +231,13 @@ async def get_permissions(self): return await self._access.get_permissions() return None - def _check_api_version(self): + def _check_api_version(self) -> None: """ Check api version """ # Set API version if needed - s_fbx_version = self.fbx_desc["api_version"].split(".")[0] + s_fbx_version = self._fbx_desc["api_version"].split(".")[0] s_default_api_version = _DEFAULT_API_VERSION[1:] if self.api_version == "server": self.api_version = f"v{s_fbx_version}" @@ -250,29 +259,35 @@ def _check_api_version(self): ) self.api_version = _DEFAULT_API_VERSION - async def _disc_check_session(self, host, port, s): + async def _disc_check_session( + self, host: str, port: str, s: str + ) -> Tuple[str, str, str]: """Check discovery session""" - if self.fbx_desc and self._session is not None and not self._session.closed: - c = list(self._session._connector._conns.keys())[0] + if ( + self._session + and not self._session.closed + and self._session._connector._conns # type: ignore # noqa + ): + c = list(self._session._connector._conns.keys())[0] # type: ignore # noqa if c.host == host and c.port == int(port) and c.is_ssl == (not not s): - raise ValueError(self.fbx_desc) + raise ValueError(self._fbx_desc) elif await self._disc_close_to_return() is None: raise ValueError(await self.discover(host, port)) return host, port, s - async def _disc_close_to_return(self): + async def _disc_close_to_return(self) -> None: """Close discovery session""" if self._session is not None and not self._session.closed: await self._session.close() await asyncio.sleep(0.250) - self.fbx_desc = None if self.fbx_desc is not None else self.fbx_desc + self._fbx_desc = {} if not self._fbx_desc.__len__ else self._fbx_desc return None - async def _disc_connect(self, host, port, s): + async def _disc_connect(self, host: str, port: str, s: str) -> bool: """Connect for discovery""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -280,7 +295,8 @@ async def _disc_connect(self, host, port, s): result = sock.connect_ex((host, int(port))) sock.close() if result != 0: - return await self._disc_close_to_return() + await self._disc_close_to_return() + return False # Connect try: @@ -293,31 +309,35 @@ async def _disc_connect(self, host, port, s): conn = aiohttp.TCPConnector() self._session = aiohttp.ClientSession(connector=conn) except ssl.SSLCertVerificationError: - return await self._disc_close_to_return() + await self._disc_close_to_return() + return False return True - def _disc_set_host_and_port(self, host, port): + def _disc_set_host_and_port( + self, host_in: Optional[str] = None, port_in: Optional[str] = None + ) -> Tuple[str, str, str]: """Set discovery host and port""" + host, port = ( + host_in if host_in else _DEFAULT_HOST, + port_in if port_in else _DEFAULT_HTTP_PORT, + ) + if not host_in and not port_in: + port = _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else port s = "s" if _DEFAULT_SSL and port != _DEFAULT_HTTP_PORT else "" - if not host and not port: - host, port = ( - _DEFAULT_HOST, - _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else _DEFAULT_HTTP_PORT, - ) - elif not port: - port = _DEFAULT_HTTP_PORT - s = "" - elif not host: - host = _DEFAULT_HOST - return host, port, s async def _get_app_access( - self, host, port, api_version, token_file, app_desc, timeout=_DEFAULT_TIMEOUT - ): + self, + api_version: str, + token_file: str, + app_desc: Dict[str, str], + host: str, + port: str, + timeout: int = _DEFAULT_TIMEOUT, + ) -> Access: """ Returns an access object used for HTTP(S) requests. @@ -326,7 +346,7 @@ async def _get_app_access( api_version : `str` token_file : `str` app_desc : `dict` - timeout : `int` , optional + timeout : `int` , Default to _DEFAULT_TIMEOUT """ @@ -382,13 +402,15 @@ async def _get_app_access( ) return fbx_access - async def _get_app_token(self, base_url, app_desc, timeout=_DEFAULT_TIMEOUT): + async def _get_app_token( + self, base_url: str, app_desc: Dict[str, str], timeout: int = _DEFAULT_TIMEOUT + ): """ Get the application token from the freebox base_url : `str` app_desc : `dict` - timeout : `int` , optional + timeout : `int` , Default to _DEFAULT_TIMEOUT Returns app_token, track_id @@ -397,7 +419,9 @@ async def _get_app_token(self, base_url, app_desc, timeout=_DEFAULT_TIMEOUT): # Get authentification token url = urljoin(base_url, "login/authorize/") data = json.dumps(app_desc) - async with self._session.post(url, data=data, timeout=timeout) as r: + async with self._session.post( # type: ignore # noqa + url, data=data, timeout=timeout + ) as r: resp = await r.json() # raise exception if resp.success != True @@ -410,30 +434,32 @@ async def _get_app_token(self, base_url, app_desc, timeout=_DEFAULT_TIMEOUT): return app_token, track_id async def _get_authorization_status( - self, base_url, track_id, timeout=_DEFAULT_TIMEOUT + self, base_url: str, track_id: str, timeout: int = _DEFAULT_TIMEOUT ): """ Get authorization status of the application token base_url : `str` track_id : `str` - timeout : `int` , optional + timeout : `int` , Default to _DEFAULT_TIMEOUT Returns: - unknown the app_token is invalid or has been revoked - pending the user has not confirmed the authorization request yet - timeout the user did not confirmed the authorization within the given time - granted the app_token is valid and can be used to open a session - denied the user denied the authorization request + unknown the app_token is invalid or has been revoked + pending the user has not confirmed the authorization request yet + timeout the user did not confirmed the authorization within the given time + granted the app_token is valid and can be used to open a session + denied the user denied the authorization request """ url = urljoin(base_url, f"login/authorize/{track_id}") - async with self._session.get(url, timeout=timeout) as r: + async with self._session.get(url, timeout=timeout) as r: # type: ignore # noqa resp = await r.json() return resp["result"]["status"] - def _get_base_url(self, host, port, fbx_api_version=None): + def _get_base_url( + self, host: str, port: str, fbx_api_version: Optional[str] = None + ) -> str: """ Returns base url for HTTP(S) requests @@ -443,14 +469,20 @@ def _get_base_url(self, host, port, fbx_api_version=None): , Default to `None` """ - s = "s" if list(self._session._connector._conns.keys())[0].is_ssl else "" + s = ( + "s" + if list(self._session._connector._conns.keys())[ # type: ignore # noqa + 0 + ].is_ssl + else "" + ) if fbx_api_version is None: return f"http{s}://{host}:{port}" - abu = self.fbx_desc["api_base_url"] + abu = self._fbx_desc["api_base_url"] return f"http{s}://{host}:{port}{abu}{fbx_api_version}/" - def _is_app_desc_valid(self, app_desc): + def _is_app_desc_valid(self, app_desc: Dict[str, str]) -> bool: """ Check validity of the application descriptor @@ -460,7 +492,7 @@ def _is_app_desc_valid(self, app_desc): k in app_desc for k in ("app_id", "app_name", "app_version", "device_name") ) - def _is_ipv4(self, ip_address): + def _is_ipv4(self, ip_address: str) -> bool: """ Check ip version for v4 @@ -473,7 +505,7 @@ def _is_ipv4(self, ip_address): except ValueError: return False - def _is_ipv6(self, ip_address): + def _is_ipv6(self, ip_address: str) -> bool: """ Check ip version for v6 @@ -486,22 +518,26 @@ def _is_ipv6(self, ip_address): except ValueError: return False - async def _open_init(self, host, port): + async def _open_init( + self, host_in: Optional[str] = None, port_in: Optional[str] = None + ) -> Tuple[str, str]: """Init host and port for open""" try: - if await self.discover(host, port) is None: + if await self.discover(host_in, port_in) is None: raise ValueError - host, port = self._open_setup(host, port) + host, port = self._open_setup(host_in, port_in) if await self.discover(host, port) is None: raise ValueError + self._check_api_version() + self._fbx_url = self._get_base_url(host, port) except (ValueError, HttpRequestError): unk = _DEFAULT_UNKNOWN host, port = ( - next(v for v in [host, unk] if v), - next(v for v in [port, unk] if v), + next(v for v in [host_in, host, unk] if v), + next(v for v in [port_in, port, unk] if v), ) raise NotOpenError( "Cannot detect freebox at " @@ -509,20 +545,19 @@ async def _open_init(self, host, port): ", please check your configuration." ) - self._check_api_version() - self.fbx_url = self._get_base_url(host, port) - return host, port - def _open_setup(self, host, port): + def _open_setup( + self, host: Optional[str] = None, port: Optional[str] = None + ) -> Tuple[str, str]: """Setup host and port value for open""" - if _DEFAULT_SSL and self.fbx_desc["https_available"]: + if _DEFAULT_SSL and self._fbx_desc["https_available"]: host, port = ( - self.fbx_desc["api_domain"] + self._fbx_desc["api_domain"] if host is None or self._is_ipv4(host) else host, - self.fbx_desc["https_port"] + self._fbx_desc["https_port"] if port is None or port == _DEFAULT_HTTP_PORT else port, ) @@ -534,7 +569,7 @@ def _open_setup(self, host, port): return host, port - def _readfile_app_token(self, file): + def _readfile_app_token(self, file: str): """ Read the application token in the authentication file. @@ -557,7 +592,9 @@ def _readfile_app_token(self, file): except FileNotFoundError: return None, None, None - def _writefile_app_token(self, app_token, track_id, app_desc, file): + def _writefile_app_token( + self, app_token: str, track_id: str, app_desc: Dict[str, str], file: str + ): """ Store the application token in a _TOKEN_FILE file @@ -570,3 +607,13 @@ def _writefile_app_token(self, app_token, track_id, app_desc, file): d = {**app_desc, "app_token": app_token, "track_id": track_id} with open(file, "w") as f: json.dump(d, f) + + @property + def fbx_desc(self) -> Optional[dict]: + """Freebox description.""" + return self._fbx_desc + + @property + def fbx_url(self) -> Optional[str]: + """Freebox url.""" + return self._fbx_url From 22416072a26d123cb1495aa6ec8d57cd190fdfc7 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sat, 9 Nov 2019 17:17:06 +0100 Subject: [PATCH 38/63] Update aiofreepybox.py: add missing types --- aiofreepybox/aiofreepybox.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index d5e54cfd..1a24975a 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -39,7 +39,7 @@ from aiofreepybox.api.upnpigd import Upnpigd # Default application descriptor -from typing import Dict, Optional, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union _APP_DESC = { "app_id": "aiofpbx", @@ -404,7 +404,7 @@ async def _get_app_access( async def _get_app_token( self, base_url: str, app_desc: Dict[str, str], timeout: int = _DEFAULT_TIMEOUT - ): + ) -> Tuple[str, str]: """ Get the application token from the freebox @@ -435,7 +435,7 @@ async def _get_app_token( async def _get_authorization_status( self, base_url: str, track_id: str, timeout: int = _DEFAULT_TIMEOUT - ): + ) -> str: """ Get authorization status of the application token @@ -569,7 +569,7 @@ def _open_setup( return host, port - def _readfile_app_token(self, file: str): + def _readfile_app_token(self, file: str) -> Tuple[Any, Any, Any]: """ Read the application token in the authentication file. @@ -594,7 +594,7 @@ def _readfile_app_token(self, file: str): def _writefile_app_token( self, app_token: str, track_id: str, app_desc: Dict[str, str], file: str - ): + ) -> None: """ Store the application token in a _TOKEN_FILE file From 5571415c54e9bbe5b30cae42a2216365daad36f4 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Mon, 11 Nov 2019 17:52:30 +0100 Subject: [PATCH 39/63] Update aiofreepybox.py: catch socket error, cleanup --- aiofreepybox/aiofreepybox.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 1a24975a..fdbb302f 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -94,11 +94,11 @@ def __init__( api_version if api_version is not None else _DEFAULT_API_VERSION ) self.app_desc: Dict[str, str] = app_desc if app_desc is not None else _APP_DESC - self._fbx_desc: dict = {} - self._fbx_url: str = "" self.timeout: int = timeout if timeout is not None else _DEFAULT_TIMEOUT self.token_file: str = token_file if token_file is not None else _TOKEN_FILE self._access: Optional[Access] = None + self._fbx_desc: dict = {} + self._fbx_url: str = "" self._session: Optional[aiohttp.ClientSession] = None async def open( @@ -193,7 +193,7 @@ async def discover( (self._session and self._session.closed), ] ) and not await self._disc_connect(host, port, s): - return None + raise ValueError # Found freebox try: @@ -290,10 +290,13 @@ async def _disc_close_to_return(self) -> None: async def _disc_connect(self, host: str, port: str, s: str) -> bool: """Connect for discovery""" - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(_DEFAULT_TIMEOUT) - result = sock.connect_ex((host, int(port))) - sock.close() + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(_DEFAULT_TIMEOUT) + result = sock.connect_ex((host, int(port))) + sock.close() + except socket.gaierror: + result = 1 if result != 0: await self._disc_close_to_return() return False @@ -523,14 +526,12 @@ async def _open_init( ) -> Tuple[str, str]: """Init host and port for open""" + host = None + port = None try: - if await self.discover(host_in, port_in) is None: - raise ValueError - + await self.discover(host_in, port_in) host, port = self._open_setup(host_in, port_in) - - if await self.discover(host, port) is None: - raise ValueError + await self.discover(host, port) self._check_api_version() self._fbx_url = self._get_base_url(host, port) except (ValueError, HttpRequestError): From ec8efece2daf0c00df99fa71e1680d2e481733bc Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Mon, 11 Nov 2019 19:31:54 +0100 Subject: [PATCH 40/63] Update aiofrepybox.py: add _fbx_api_url, refactor --- aiofreepybox/aiofreepybox.py | 51 +++++++++++++++--------------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index fdbb302f..333ff1d1 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -98,6 +98,7 @@ def __init__( self.token_file: str = token_file if token_file is not None else _TOKEN_FILE self._access: Optional[Access] = None self._fbx_desc: dict = {} + self._fbx_api_url: str = "" self._fbx_url: str = "" self._session: Optional[aiohttp.ClientSession] = None @@ -118,12 +119,9 @@ async def open( raise InvalidTokenError("Invalid application descriptor") # Get API access + await self._open_init(host, port) self._access = await self._get_app_access( - self.api_version, - self.token_file, - self.app_desc, - *await self._open_init(host, port), - self.timeout, + self.token_file, self.app_desc, self.timeout ) # Instantiate freebox modules @@ -333,13 +331,7 @@ def _disc_set_host_and_port( return host, port, s async def _get_app_access( - self, - api_version: str, - token_file: str, - app_desc: Dict[str, str], - host: str, - port: str, - timeout: int = _DEFAULT_TIMEOUT, + self, token_file: str, app_desc: Dict[str, str], timeout: int = _DEFAULT_TIMEOUT ) -> Access: """ Returns an access object used for HTTP(S) requests. @@ -353,8 +345,6 @@ async def _get_app_access( , Default to _DEFAULT_TIMEOUT """ - base_url = self._get_base_url(host, port, api_version) - # Read stored application token _LOGGER.debug("Reading application authorization file.") app_token, track_id, file_app_desc = self._readfile_app_token(token_file) @@ -366,15 +356,13 @@ async def _get_app_access( ) # Get application token from the freebox - app_token, track_id = await self._get_app_token(base_url, app_desc, timeout) + app_token, track_id = await self._get_app_token(app_desc, timeout) # Check the authorization status out_msg_flag = False status = None while status != "granted": - status = await self._get_authorization_status( - base_url, track_id, timeout - ) + status = await self._get_authorization_status(track_id, timeout) # denied status = authorization failed if status == "denied": @@ -401,17 +389,16 @@ async def _get_app_access( # Create and return freebox http access module fbx_access = Access( - self._session, base_url, app_token, app_desc["app_id"], timeout + self._session, self._fbx_api_url, app_token, app_desc["app_id"], timeout ) return fbx_access async def _get_app_token( - self, base_url: str, app_desc: Dict[str, str], timeout: int = _DEFAULT_TIMEOUT + self, app_desc: Dict[str, str], timeout: int = _DEFAULT_TIMEOUT ) -> Tuple[str, str]: """ Get the application token from the freebox - base_url : `str` app_desc : `dict` timeout : `int` , Default to _DEFAULT_TIMEOUT @@ -420,7 +407,7 @@ async def _get_app_token( """ # Get authentification token - url = urljoin(base_url, "login/authorize/") + url = urljoin(self._fbx_api_url, "login/authorize/") data = json.dumps(app_desc) async with self._session.post( # type: ignore # noqa url, data=data, timeout=timeout @@ -437,12 +424,11 @@ async def _get_app_token( return app_token, track_id async def _get_authorization_status( - self, base_url: str, track_id: str, timeout: int = _DEFAULT_TIMEOUT + self, track_id: str, timeout: int = _DEFAULT_TIMEOUT ) -> str: """ Get authorization status of the application token - base_url : `str` track_id : `str` timeout : `int` , Default to _DEFAULT_TIMEOUT @@ -455,7 +441,7 @@ async def _get_authorization_status( denied the user denied the authorization request """ - url = urljoin(base_url, f"login/authorize/{track_id}") + url = urljoin(self._fbx_api_url, f"login/authorize/{track_id}") async with self._session.get(url, timeout=timeout) as r: # type: ignore # noqa resp = await r.json() return resp["result"]["status"] @@ -523,8 +509,8 @@ def _is_ipv6(self, ip_address: str) -> bool: async def _open_init( self, host_in: Optional[str] = None, port_in: Optional[str] = None - ) -> Tuple[str, str]: - """Init host and port for open""" + ) -> None: + """Init freebox link for open""" host = None port = None @@ -532,8 +518,6 @@ async def _open_init( await self.discover(host_in, port_in) host, port = self._open_setup(host_in, port_in) await self.discover(host, port) - self._check_api_version() - self._fbx_url = self._get_base_url(host, port) except (ValueError, HttpRequestError): unk = _DEFAULT_UNKNOWN host, port = ( @@ -546,7 +530,9 @@ async def _open_init( ", please check your configuration." ) - return host, port + self._check_api_version() + self._fbx_api_url = self._get_base_url(host, port, self.api_version) + self._fbx_url = self._get_base_url(host, port) def _open_setup( self, host: Optional[str] = None, port: Optional[str] = None @@ -614,6 +600,11 @@ def fbx_desc(self) -> Optional[dict]: """Freebox description.""" return self._fbx_desc + @property + def fbx_api_url(self) -> Optional[str]: + """Freebox api url.""" + return self._fbx_api_url + @property def fbx_url(self) -> Optional[str]: """Freebox url.""" From 2693939733f54ab25d723e6ce25b7d8b9fb05675 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Thu, 14 Nov 2019 02:39:14 +0100 Subject: [PATCH 41/63] Update aiofreepybox.py: use raise on discovery errors --- aiofreepybox/aiofreepybox.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 333ff1d1..93f51b61 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -173,7 +173,7 @@ async def discover( if host_in and self._is_ipv6(host_in): _LOGGER.error(f"{host_in} : IPv6 is not supported") - return await self._disc_close_to_return() + raise ValueError # Check session try: @@ -205,8 +205,11 @@ async def discover( await self._disc_close_to_return() raise HttpRequestError(f"{e}") - if self._fbx_desc.get("device_name", None) != _DEFAULT_DEVICE_NAME: - return await self._disc_close_to_return() + fbx_dev = self._fbx_desc.get("device_name", None) + if fbx_dev != _DEFAULT_DEVICE_NAME: + _LOGGER.error(f"{fbx_dev} is not a freebox server") + await self._disc_close_to_return() + raise ValueError return self._fbx_desc @@ -278,12 +281,10 @@ async def _disc_check_session( async def _disc_close_to_return(self) -> None: """Close discovery session""" + self._fbx_desc = {} if not self._fbx_desc.__len__ else self._fbx_desc if self._session is not None and not self._session.closed: await self._session.close() await asyncio.sleep(0.250) - self._fbx_desc = {} if not self._fbx_desc.__len__ else self._fbx_desc - - return None async def _disc_connect(self, host: str, port: str, s: str) -> bool: """Connect for discovery""" From c878af85bc9d6c02ab769a8f84bb1d421b19b529 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Thu, 14 Nov 2019 18:21:18 +0100 Subject: [PATCH 42/63] Update aiofreepybox.py: include error message in raise --- aiofreepybox/aiofreepybox.py | 40 ++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 93f51b61..75ece5de 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -119,10 +119,16 @@ async def open( raise InvalidTokenError("Invalid application descriptor") # Get API access - await self._open_init(host, port) - self._access = await self._get_app_access( - self.token_file, self.app_desc, self.timeout - ) + try: + await self._open_init(host, port) + except NotOpenError: + raise + try: + self._access = await self._get_app_access( + self.token_file, self.app_desc, self.timeout + ) + except AuthorizationError: + raise # Instantiate freebox modules self.tv = Tv(self._access) @@ -172,8 +178,7 @@ async def discover( """ if host_in and self._is_ipv6(host_in): - _LOGGER.error(f"{host_in} : IPv6 is not supported") - raise ValueError + raise ValueError(f"{host_in} : IPv6 is not supported") # Check session try: @@ -191,7 +196,7 @@ async def discover( (self._session and self._session.closed), ] ) and not await self._disc_connect(host, port, s): - raise ValueError + raise ValueError("Port closed") # Found freebox try: @@ -199,17 +204,17 @@ async def discover( f"http{s}://{host}:{port}/api_version", timeout=self.timeout ) as r: if r.content_type != "application/json": - return await self._disc_close_to_return() + await self._disc_close_to_return() + raise ValueError("Invalid content type") self._fbx_desc = await r.json() - except ssl.SSLCertVerificationError as e: + except (ssl.SSLCertVerificationError, ValueError) as err: await self._disc_close_to_return() - raise HttpRequestError(f"{e}") + raise ValueError(err.args[0]) fbx_dev = self._fbx_desc.get("device_name", None) if fbx_dev != _DEFAULT_DEVICE_NAME: - _LOGGER.error(f"{fbx_dev} is not a freebox server") await self._disc_close_to_return() - raise ValueError + raise ValueError(f"{fbx_dev}: Wrong device") return self._fbx_desc @@ -273,8 +278,8 @@ async def _disc_check_session( c = list(self._session._connector._conns.keys())[0] # type: ignore # noqa if c.host == host and c.port == int(port) and c.is_ssl == (not not s): raise ValueError(self._fbx_desc) - elif await self._disc_close_to_return() is None: - raise ValueError(await self.discover(host, port)) + await self._disc_close_to_return() + raise ValueError(await self.discover(host, port)) return host, port, s @@ -297,7 +302,6 @@ async def _disc_connect(self, host: str, port: str, s: str) -> bool: except socket.gaierror: result = 1 if result != 0: - await self._disc_close_to_return() return False # Connect @@ -519,14 +523,14 @@ async def _open_init( await self.discover(host_in, port_in) host, port = self._open_setup(host_in, port_in) await self.discover(host, port) - except (ValueError, HttpRequestError): + except ValueError as err: unk = _DEFAULT_UNKNOWN host, port = ( next(v for v in [host_in, host, unk] if v), next(v for v in [port_in, port, unk] if v), ) raise NotOpenError( - "Cannot detect freebox at " + f"{err.args[0]}: Cannot detect freebox at " f"{host}:{port}" ", please check your configuration." ) @@ -598,7 +602,7 @@ def _writefile_app_token( @property def fbx_desc(self) -> Optional[dict]: - """Freebox description.""" + """Freebox API description.""" return self._fbx_desc @property From 349ef45a08d59ef6d368f3316259d015d45c1295 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 17 Nov 2019 18:12:39 +0100 Subject: [PATCH 43/63] Update aiofreepybox.py: add _fbx_db, manage connections, refactor, open by uid --- aiofreepybox/aiofreepybox.py | 523 ++++++++++++++++++++++------------- 1 file changed, 338 insertions(+), 185 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 75ece5de..8d124bbb 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -1,11 +1,14 @@ import aiohttp import asyncio +import base64 +import bz2 import ipaddress import json import logging import os import socket import ssl +from typing import Any, Dict, List, Optional, Tuple, Union from urllib.parse import urljoin import aiofreepybox @@ -39,19 +42,17 @@ from aiofreepybox.api.upnpigd import Upnpigd # Default application descriptor -from typing import Any, Dict, Optional, Tuple, Union - _APP_DESC = { "app_id": "aiofpbx", "app_name": "aiofreepybox", "app_version": aiofreepybox.__version__, "device_name": socket.gethostname(), } +_DATA_DIR = os.path.dirname(os.path.abspath(__file__)) -# Token file default location -_TOKEN_FILENAME = "app_auth" -_TOKEN_DIR = os.path.dirname(os.path.abspath(__file__)) -_TOKEN_FILE = os.path.join(_TOKEN_DIR, _TOKEN_FILENAME) +# Db file default location +_DB_FILENAME = ".db" +_DB_FILE = os.path.join(_DATA_DIR, _DB_FILENAME) # App defaults _DEFAULT_API_VERSION = "v6" @@ -63,9 +64,12 @@ _DEFAULT_SSL = True _DEFAULT_TIMEOUT = 10 _DEFAULT_UNKNOWN = "None" - _LOGGER = logging.getLogger(__name__) +# Token file default location +_TOKEN_FILENAME = ".app_auth" +_TOKEN_FILE = os.path.join(_DATA_DIR, _TOKEN_FILENAME) + class Freepybox: """ @@ -97,61 +101,10 @@ def __init__( self.timeout: int = timeout if timeout is not None else _DEFAULT_TIMEOUT self.token_file: str = token_file if token_file is not None else _TOKEN_FILE self._access: Optional[Access] = None - self._fbx_desc: dict = {} - self._fbx_api_url: str = "" - self._fbx_url: str = "" + self._fbx_db: Dict[str, Any] = {} + self._fbx_uid: str = "" self._session: Optional[aiohttp.ClientSession] = None - async def open( - self, host: Optional[str] = None, port: Optional[str] = None - ) -> None: - """ - Open a session to the freebox, get a valid access module - and instantiate freebox modules - - host : `str` , optional - , Default to `None` - port : `str` , optional - , Default to `None` - """ - - if not self._is_app_desc_valid(self.app_desc): - raise InvalidTokenError("Invalid application descriptor") - - # Get API access - try: - await self._open_init(host, port) - except NotOpenError: - raise - try: - self._access = await self._get_app_access( - self.token_file, self.app_desc, self.timeout - ) - except AuthorizationError: - raise - - # Instantiate freebox modules - self.tv = Tv(self._access) - self.system = System(self._access) - self.dhcp = Dhcp(self._access) - self.switch = Switch(self._access) - self.lan = Lan(self._access) - self.lcd = Lcd(self._access) - self.wifi = Wifi(self._access) - self.phone = Phone(self._access) - self.fs = Fs(self._access) - self.fw = Fw(self._access) - self.freeplug = Freeplug(self._access) - self.call = Call(self._access) - self.connection = Connection(self._access) - self.home = Home(self._access) - self.parental = Parental(self._access) - self.nat = Nat(self._access) - self.notifications = Notifications(self._access) - self.rrd = Rrd(self._access) - self.upnpav = Upnpav(self._access) - self.upnpigd = Upnpigd(self._access) - async def close(self) -> None: """ Close the freebox session @@ -167,7 +120,7 @@ async def close(self) -> None: async def discover( self, host_in: Optional[str] = None, port_in: Optional[str] = None - ) -> Optional[dict]: + ) -> Dict[str, Any]: """ Discover a freebox on the network @@ -182,8 +135,8 @@ async def discover( # Check session try: - host, port, s = await self._disc_check_session( - *self._disc_set_host_and_port(host_in, port_in) + fbx_addict = await self._disc_check_session( + self._disc_set_host_and_port(host_in, port_in) ) except ValueError as err: return err.args[0] @@ -195,28 +148,30 @@ async def discover( not isinstance(self._session, aiohttp.ClientSession), (self._session and self._session.closed), ] - ) and not await self._disc_connect(host, port, s): + ) and not await self._disc_connect(**(fbx_addict)): raise ValueError("Port closed") # Found freebox try: async with self._session.get( # type: ignore # noqa - f"http{s}://{host}:{port}/api_version", timeout=self.timeout + f"http{fbx_addict['s']}://{fbx_addict['host']}:{fbx_addict['port']}/api_version", + timeout=self.timeout, ) as r: if r.content_type != "application/json": await self._disc_close_to_return() raise ValueError("Invalid content type") - self._fbx_desc = await r.json() + fbx_desc = await r.json() except (ssl.SSLCertVerificationError, ValueError) as err: await self._disc_close_to_return() raise ValueError(err.args[0]) - fbx_dev = self._fbx_desc.get("device_name", None) - if fbx_dev != _DEFAULT_DEVICE_NAME: + fbx_device = fbx_desc.get("device_name", None) + if fbx_device != _DEFAULT_DEVICE_NAME: await self._disc_close_to_return() - raise ValueError(f"{fbx_dev}: Wrong device") + raise ValueError(f"{fbx_device}: Wrong device") - return self._fbx_desc + self._fbx_update_db(fbx_desc, fbx_addict) + return fbx_desc async def get_permissions(self) -> Optional[dict]: """ @@ -237,17 +192,80 @@ async def get_permissions(self) -> Optional[dict]: return await self._access.get_permissions() return None - def _check_api_version(self) -> None: + async def open( + self, + host: Optional[str] = None, + port: Optional[str] = None, + uid: Optional[str] = None, + ) -> None: + """ + Open a session to the freebox, get a valid access module + and instantiate freebox modules + + host : `str` , optional + , Default to `None` + port : `str` , optional + , Default to `None` + uid : `str` , optional + , Default to `None` + """ + + if not self._is_app_desc_valid(self.app_desc): + raise InvalidTokenError("Invalid application descriptor") + + # Get API access + if uid is None: + uid = "" + try: + uid = await self._fbx_open_init(host, port) + except NotOpenError: + raise + else: + try: + uid = await self._fbx_open_db(uid) + except NotOpenError: + raise + + try: + self._access = await self._get_app_access( + uid, self.token_file, self.app_desc, self.timeout + ) + except AuthorizationError: + raise + + # Instantiate freebox modules + self.tv = Tv(self._access) + self.system = System(self._access) + self.dhcp = Dhcp(self._access) + self.switch = Switch(self._access) + self.lan = Lan(self._access) + self.lcd = Lcd(self._access) + self.wifi = Wifi(self._access) + self.phone = Phone(self._access) + self.fs = Fs(self._access) + self.fw = Fw(self._access) + self.freeplug = Freeplug(self._access) + self.call = Call(self._access) + self.connection = Connection(self._access) + self.home = Home(self._access) + self.parental = Parental(self._access) + self.nat = Nat(self._access) + self.notifications = Notifications(self._access) + self.rrd = Rrd(self._access) + self.upnpav = Upnpav(self._access) + self.upnpigd = Upnpigd(self._access) + + def _check_api_version(self, fbx_api_version: str) -> str: """ Check api version """ # Set API version if needed - s_fbx_version = self._fbx_desc["api_version"].split(".")[0] + api_version = None + s_fbx_version = fbx_api_version.split(".")[0] s_default_api_version = _DEFAULT_API_VERSION[1:] if self.api_version == "server": - self.api_version = f"v{s_fbx_version}" - + api_version = f"v{s_fbx_version}" # Check user API version s_api_version = self.api_version[1:] if s_default_api_version < s_fbx_version and s_api_version == s_fbx_version: @@ -263,11 +281,15 @@ def _check_api_version(self) -> None: "Freebox server does not support this API version (" f"{self.api_version}), resetting to {_DEFAULT_API_VERSION}." ) - self.api_version = _DEFAULT_API_VERSION + api_version = _DEFAULT_API_VERSION - async def _disc_check_session( - self, host: str, port: str, s: str - ) -> Tuple[str, str, str]: + if api_version is None: + return self.api_version + else: + self.api_version = api_version + return api_version + + async def _disc_check_session(self, fbx_addict: Dict[str, Any]) -> Dict[str, Any]: """Check discovery session""" if ( @@ -276,17 +298,22 @@ async def _disc_check_session( and self._session._connector._conns # type: ignore # noqa ): c = list(self._session._connector._conns.keys())[0] # type: ignore # noqa - if c.host == host and c.port == int(port) and c.is_ssl == (not not s): - raise ValueError(self._fbx_desc) + if ( + c.host == fbx_addict["host"] + and c.port == int(fbx_addict["port"]) + and c.is_ssl == (not not fbx_addict["s"]) + ): + raise ValueError(self._fbx_db[self._fbx_uid]["desc"]) await self._disc_close_to_return() - raise ValueError(await self.discover(host, port)) + raise ValueError( + await self.discover(fbx_addict["host"], fbx_addict["port"]) + ) - return host, port, s + return fbx_addict async def _disc_close_to_return(self) -> None: """Close discovery session""" - self._fbx_desc = {} if not self._fbx_desc.__len__ else self._fbx_desc if self._session is not None and not self._session.closed: await self._session.close() await asyncio.sleep(0.250) @@ -294,14 +321,7 @@ async def _disc_close_to_return(self) -> None: async def _disc_connect(self, host: str, port: str, s: str) -> bool: """Connect for discovery""" - try: - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(_DEFAULT_TIMEOUT) - result = sock.connect_ex((host, int(port))) - sock.close() - except socket.gaierror: - result = 1 - if result != 0: + if not self._fbx_ping_port(host, port): return False # Connect @@ -322,7 +342,7 @@ async def _disc_connect(self, host: str, port: str, s: str) -> bool: def _disc_set_host_and_port( self, host_in: Optional[str] = None, port_in: Optional[str] = None - ) -> Tuple[str, str, str]: + ) -> Dict[str, Any]: """Set discovery host and port""" host, port = ( @@ -333,17 +353,154 @@ def _disc_set_host_and_port( port = _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else port s = "s" if _DEFAULT_SSL and port != _DEFAULT_HTTP_PORT else "" - return host, port, s + del self, host_in, port_in + return locals() + + async def _fbx_enum_conns(self, db: Dict[str, Any]) -> Dict[str, Any]: + """Try freebox connections""" + for i, conn in enumerate(db["conn"]): + try: + d = await self.discover(conn["host"], conn["port"]) + except ValueError: + pass + else: + self._fbx_db[d["uid"]]["conf"]["cc"] = i + break + if not d: + raise NotOpenError + return d + + async def _fbx_open_init( + self, host_in: Optional[str] = None, port_in: Optional[str] = None + ) -> str: + """Init freebox link for open""" + + host = None + port = None + try: + fbx_desc = await self.discover(host_in, port_in) + host, port = self._fbx_open_setup(fbx_desc, host_in, port_in) + except ValueError as err: + unk = _DEFAULT_UNKNOWN + host, port = ( + next(v for v in [host_in, host, unk] if v), + next(v for v in [port_in, port, unk] if v), + ) + raise NotOpenError( + f"{err.args[0]}: Cannot detect freebox at " + f"{host}:{port}" + ", please check your configuration." + ) + d = await self._fbx_enum_conns(self._fbx_db[fbx_desc["uid"]]) + + self._fbx_db[d["uid"]]["conf"]["api_version"] = self._check_api_version( + self._fbx_db[d["uid"]]["desc"]["api_version"] + ) + self._fbx_uid = d["uid"] + return d["uid"] + + async def _fbx_open_db(self, uid: str) -> str: + """Open freebox db""" + + db = self._readfile_fbx_db(_DB_FILE, uid) + d: Dict[str, Any] = {} + if db is None: + try: + d = await self.discover() + except ValueError: + raise NotOpenError + else: + self._fbx_db[uid] = db + d = await self._fbx_enum_conns(db) + if uid != d["uid"]: + raise NotOpenError + self._fbx_db[d["uid"]]["conf"]["api_version"] = self._check_api_version( + self._fbx_db[d["uid"]]["desc"]["api_version"] + ) + self._fbx_uid = uid + return uid + + def _fbx_open_setup( + self, + fbx_desc: Dict[str, Any], + host: Optional[str] = None, + port: Optional[str] = None, + ) -> Tuple[str, str]: + """Setup host and port value for open""" + + if _DEFAULT_SSL and fbx_desc["https_available"]: + host, port = ( + fbx_desc["api_domain"] if host is None or self._is_ipv4(host) else host, + fbx_desc["https_port"] + if port is None or port == _DEFAULT_HTTP_PORT + else port, + ) + else: + host, port = ( + _DEFAULT_HOST if host is None else host, + _DEFAULT_HTTP_PORT if port is None else port, + ) + + return host, port + + def _fbx_ping_port(self, host: str, port: str) -> bool: + """Check if a port if open""" + + result = 0 + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(_DEFAULT_TIMEOUT) + result = sock.connect_ex((host, int(port))) + sock.close() + except socket.gaierror: + result = 1 + finally: + if result != 0: + return False + return True + + def _fbx_update_db( + self, fbx_desc: Dict[str, Any], fbx_addict: Optional[Dict[str, Any]] + ) -> None: + + uid: str = fbx_desc["uid"] + try: + fbx_data: List[Dict[str, Any]] = self._fbx_db[uid]["conn"] + except KeyError: + fbx_api_add = [ + { + "host": fbx_desc["api_domain"], + "port": fbx_desc["https_port"], + "s": "s" if fbx_desc["https_available"] else "", + } + ] + fbx_conf = {"api_version": "", "cc": 0} + fbx_entry = {uid: {"conn": fbx_api_add, "conf": fbx_conf, "desc": fbx_desc}} + self._fbx_db.update(fbx_entry) + fbx_data = self._fbx_db[uid]["conn"] + if not self._readfile_fbx_db(_DB_FILE, uid): + self._writefile_fbx_db(_DB_FILE, uid) + + finally: + if fbx_addict is not None: + for k in fbx_data: + if dict(k) == fbx_addict: + return + fbx_data.append(fbx_addict) + self._fbx_db[uid]["conn"] = fbx_data + self._writefile_fbx_db(_DB_FILE, uid) async def _get_app_access( - self, token_file: str, app_desc: Dict[str, str], timeout: int = _DEFAULT_TIMEOUT + self, + uid: str, + token_file: str, + app_desc: Dict[str, str], + timeout: int = _DEFAULT_TIMEOUT, ) -> Access: """ Returns an access object used for HTTP(S) requests. - host : `str` - port : `str` - api_version : `str` + uid : `str` token_file : `str` app_desc : `dict` timeout : `int` @@ -352,7 +509,7 @@ async def _get_app_access( # Read stored application token _LOGGER.debug("Reading application authorization file.") - app_token, track_id, file_app_desc = self._readfile_app_token(token_file) + app_token, track_id, file_app_desc = self._readfile_app_token(token_file, uid) # If no valid token is stored then request a token to freebox api - Only for LAN connection if app_token is None or file_app_desc != app_desc: @@ -361,13 +518,13 @@ async def _get_app_access( ) # Get application token from the freebox - app_token, track_id = await self._get_app_token(app_desc, timeout) + app_token, track_id = await self._get_app_token(app_desc, uid, timeout) # Check the authorization status out_msg_flag = False status = None while status != "granted": - status = await self._get_authorization_status(track_id, timeout) + status = await self._get_authorization_status(track_id, uid, timeout) # denied status = authorization failed if status == "denied": @@ -389,22 +546,29 @@ async def _get_app_access( _LOGGER.info("Application authorization granted.") # Store application token in file - self._writefile_app_token(app_token, track_id, app_desc, token_file) - _LOGGER.info(f"Application token file was generated: {token_file}.") + tk_file = self._writefile_app_token( + app_token, track_id, app_desc, token_file, uid + ) + _LOGGER.info(f"Application token file was generated: {tk_file}.") # Create and return freebox http access module fbx_access = Access( - self._session, self._fbx_api_url, app_token, app_desc["app_id"], timeout + self._session, + self._get_db_base_url(uid), + app_token, + app_desc["app_id"], + timeout, ) return fbx_access async def _get_app_token( - self, app_desc: Dict[str, str], timeout: int = _DEFAULT_TIMEOUT + self, app_desc: Dict[str, str], uid: str, timeout: int = _DEFAULT_TIMEOUT ) -> Tuple[str, str]: """ Get the application token from the freebox app_desc : `dict` + uid : `str` timeout : `int` , Default to _DEFAULT_TIMEOUT @@ -412,7 +576,7 @@ async def _get_app_token( """ # Get authentification token - url = urljoin(self._fbx_api_url, "login/authorize/") + url = urljoin(self._get_db_base_url(uid), "login/authorize/") data = json.dumps(app_desc) async with self._session.post( # type: ignore # noqa url, data=data, timeout=timeout @@ -429,12 +593,13 @@ async def _get_app_token( return app_token, track_id async def _get_authorization_status( - self, track_id: str, timeout: int = _DEFAULT_TIMEOUT + self, track_id: str, uid: str, timeout: int = _DEFAULT_TIMEOUT ) -> str: """ Get authorization status of the application token track_id : `str` + uid : `str` timeout : `int` , Default to _DEFAULT_TIMEOUT @@ -446,35 +611,26 @@ async def _get_authorization_status( denied the user denied the authorization request """ - url = urljoin(self._fbx_api_url, f"login/authorize/{track_id}") + url = urljoin(self._get_db_base_url(uid), f"login/authorize/{track_id}") async with self._session.get(url, timeout=timeout) as r: # type: ignore # noqa resp = await r.json() return resp["result"]["status"] - def _get_base_url( - self, host: str, port: str, fbx_api_version: Optional[str] = None - ) -> str: + def _get_db_base_url(self, uid: str, api: Optional[bool] = True) -> str: """ Returns base url for HTTP(S) requests - host : `str` - port : `str` - fbx_api_version : `str` , optional - , Default to `None` + uid : `str` + api : `bool` , optional + , Default to `True` """ - s = ( - "s" - if list(self._session._connector._conns.keys())[ # type: ignore # noqa - 0 - ].is_ssl - else "" - ) - if fbx_api_version is None: - return f"http{s}://{host}:{port}" - - abu = self._fbx_desc["api_base_url"] - return f"http{s}://{host}:{port}{abu}{fbx_api_version}/" + conn = self._fbx_db[uid]["conn"][(self._fbx_db[uid]["conf"]["cc"])] + if api: + abu = self._fbx_db[uid]["desc"]["api_base_url"] + api_version = self._fbx_db[uid]["conf"]["api_version"] + return f"http{conn['s']}://{conn['host']}:{conn['port']}{abu}{api_version}/" + return f"http{conn['s']}://{conn['host']}:{conn['port']}" def _is_app_desc_valid(self, app_desc: Dict[str, str]) -> bool: """ @@ -512,67 +668,20 @@ def _is_ipv6(self, ip_address: str) -> bool: except ValueError: return False - async def _open_init( - self, host_in: Optional[str] = None, port_in: Optional[str] = None - ) -> None: - """Init freebox link for open""" - - host = None - port = None - try: - await self.discover(host_in, port_in) - host, port = self._open_setup(host_in, port_in) - await self.discover(host, port) - except ValueError as err: - unk = _DEFAULT_UNKNOWN - host, port = ( - next(v for v in [host_in, host, unk] if v), - next(v for v in [port_in, port, unk] if v), - ) - raise NotOpenError( - f"{err.args[0]}: Cannot detect freebox at " - f"{host}:{port}" - ", please check your configuration." - ) - - self._check_api_version() - self._fbx_api_url = self._get_base_url(host, port, self.api_version) - self._fbx_url = self._get_base_url(host, port) - - def _open_setup( - self, host: Optional[str] = None, port: Optional[str] = None - ) -> Tuple[str, str]: - """Setup host and port value for open""" - - if _DEFAULT_SSL and self._fbx_desc["https_available"]: - host, port = ( - self._fbx_desc["api_domain"] - if host is None or self._is_ipv4(host) - else host, - self._fbx_desc["https_port"] - if port is None or port == _DEFAULT_HTTP_PORT - else port, - ) - else: - host, port = ( - _DEFAULT_HOST if host is None else host, - _DEFAULT_HTTP_PORT if port is None else port, - ) - - return host, port - - def _readfile_app_token(self, file: str) -> Tuple[Any, Any, Any]: + def _readfile_app_token(self, file: str, uid: str) -> Tuple[Any, Any, Any]: """ Read the application token in the authentication file. file : `str` + uid : `str` Returns app_token, track_id, app_desc """ + fname = file + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") try: - with open(file, "r") as f: - d = json.load(f) + with bz2.open(fname, "rt", encoding="utf-8") as zf: + d = json.load(zf) app_token = d["app_token"] track_id = d["track_id"] app_desc = { @@ -584,9 +693,32 @@ def _readfile_app_token(self, file: str) -> Tuple[Any, Any, Any]: except FileNotFoundError: return None, None, None + def _readfile_fbx_db(self, file: str, uid: str) -> Optional[Dict[str, Any]]: + """ + Read the freebox db in the db file. + + file : `str` + uid : `str` + + Returns fbx_db + """ + fname = file + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + + try: + with bz2.open(fname, "rt", encoding="utf-8") as zf: + d = json.load(zf) + return d + except FileNotFoundError: + return None + def _writefile_app_token( - self, app_token: str, track_id: str, app_desc: Dict[str, str], file: str - ) -> None: + self, + app_token: str, + track_id: str, + app_desc: Dict[str, str], + file: str, + uid: str, + ) -> str: """ Store the application token in a _TOKEN_FILE file @@ -594,23 +726,44 @@ def _writefile_app_token( track_id : `str` app_desc : `dict` file : `str` + uid : `str` """ d = {**app_desc, "app_token": app_token, "track_id": track_id} - with open(file, "w") as f: - json.dump(d, f) + fname = file + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + with bz2.open(fname, "wt", encoding="utf-8") as zf: + json.dump(d, zf) + return fname + + def _writefile_fbx_db(self, file: str, uid: str) -> str: + """ + Store the freebox db in a _DB_FILE file + + file : `str` + uid : `str` + """ + + fname = file + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + with bz2.open(fname, "wt", encoding="utf-8") as zf: + json.dump(self._fbx_db[uid], zf) + return fname @property def fbx_desc(self) -> Optional[dict]: """Freebox API description.""" - return self._fbx_desc + return self._fbx_db[self._fbx_uid]["desc"] @property def fbx_api_url(self) -> Optional[str]: """Freebox api url.""" - return self._fbx_api_url + return self._get_db_base_url(self._fbx_uid) + + @property + def fbx_uid(self) -> Optional[str]: + """Freebox uid.""" + return self._fbx_uid @property def fbx_url(self) -> Optional[str]: """Freebox url.""" - return self._fbx_url + return self._get_db_base_url(self._fbx_uid, api=False) From 224a56eb3f882fe30c50d1d104a071cdf68a75a4 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 17 Nov 2019 22:30:36 +0100 Subject: [PATCH 44/63] Update .gitignore: ignore .vscode and api files --- .gitignore | 3 +++ aiofreepybox/.gitignore | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index b989be6c..a6086687 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,6 @@ venv.bak/ # mypy .mypy_cache/ + +#vscode +.vscode/ \ No newline at end of file diff --git a/aiofreepybox/.gitignore b/aiofreepybox/.gitignore index e53028cb..f15b4be9 100644 --- a/aiofreepybox/.gitignore +++ b/aiofreepybox/.gitignore @@ -1,3 +1,5 @@ /__pycache__/ *.pyc app_auth +.app_auth* +.db_* From 63f82d53cd81e87703405b7c7352692fb279d716 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 17 Nov 2019 22:50:27 +0100 Subject: [PATCH 45/63] Update aiofreepybox.py: update raise error messages --- aiofreepybox/aiofreepybox.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 8d124bbb..42684066 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -407,13 +407,19 @@ async def _fbx_open_db(self, uid: str) -> str: if db is None: try: d = await self.discover() - except ValueError: - raise NotOpenError + except ValueError as err: + raise NotOpenError( + f"{err.args[0]}: Cannot detect freebox for uid: {uid}" + ", please check your configuration." + ) else: self._fbx_db[uid] = db d = await self._fbx_enum_conns(db) if uid != d["uid"]: - raise NotOpenError + raise NotOpenError( + f"Cannot detect freebox for uid: {uid}" + ", please check your configuration." + ) self._fbx_db[d["uid"]]["conf"]["api_version"] = self._check_api_version( self._fbx_db[d["uid"]]["desc"]["api_version"] ) @@ -625,12 +631,13 @@ def _get_db_base_url(self, uid: str, api: Optional[bool] = True) -> str: , Default to `True` """ + abu = "" conn = self._fbx_db[uid]["conn"][(self._fbx_db[uid]["conf"]["cc"])] if api: abu = self._fbx_db[uid]["desc"]["api_base_url"] api_version = self._fbx_db[uid]["conf"]["api_version"] - return f"http{conn['s']}://{conn['host']}:{conn['port']}{abu}{api_version}/" - return f"http{conn['s']}://{conn['host']}:{conn['port']}" + abu = f"{abu}{api_version}/" + return f"http{conn['s']}://{conn['host']}:{conn['port']}{abu}" def _is_app_desc_valid(self, app_desc: Dict[str, str]) -> bool: """ From b089ceff7e2b726479e16ce62568ad43d8ad9ea6 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 17 Nov 2019 23:52:25 +0100 Subject: [PATCH 46/63] Update aiofreepybox.py: update raise error messages --- aiofreepybox/aiofreepybox.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 42684066..f171f4c9 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -149,7 +149,7 @@ async def discover( (self._session and self._session.closed), ] ) and not await self._disc_connect(**(fbx_addict)): - raise ValueError("Port closed") + raise ValueError("Port closed or dns failed") # Found freebox try: @@ -358,16 +358,18 @@ def _disc_set_host_and_port( async def _fbx_enum_conns(self, db: Dict[str, Any]) -> Dict[str, Any]: """Try freebox connections""" + + d = None for i, conn in enumerate(db["conn"]): try: d = await self.discover(conn["host"], conn["port"]) - except ValueError: - pass + except ValueError as err: + err_out = err else: self._fbx_db[d["uid"]]["conf"]["cc"] = i break - if not d: - raise NotOpenError + if d is None: + raise ValueError(err_out.args[0]) return d async def _fbx_open_init( @@ -409,17 +411,24 @@ async def _fbx_open_db(self, uid: str) -> str: d = await self.discover() except ValueError as err: raise NotOpenError( - f"{err.args[0]}: Cannot detect freebox for uid: {uid}" - ", please check your configuration." + f"{err.args[0]}: Cannot detect freebox for uid: {uid}" + ", please check your configuration." ) else: self._fbx_db[uid] = db - d = await self._fbx_enum_conns(db) - if uid != d["uid"]: + try: + d = await self._fbx_enum_conns(db) + except ValueError as err: + raise NotOpenError( + f"{err.args[0]}: " + f"Cannot detect freebox for uid: {uid}" + ", please check your configuration." + ) + if isinstance(d['uid'], str) and uid != d["uid"]: raise NotOpenError( - f"Cannot detect freebox for uid: {uid}" + f"{d['uid']}: Cannot detect freebox for uid: {uid}" ", please check your configuration." - ) + ) self._fbx_db[d["uid"]]["conf"]["api_version"] = self._check_api_version( self._fbx_db[d["uid"]]["desc"]["api_version"] ) @@ -636,7 +645,7 @@ def _get_db_base_url(self, uid: str, api: Optional[bool] = True) -> str: if api: abu = self._fbx_db[uid]["desc"]["api_base_url"] api_version = self._fbx_db[uid]["conf"]["api_version"] - abu = f"{abu}{api_version}/" + abu = f"{abu}{api_version}/" return f"http{conn['s']}://{conn['host']}:{conn['port']}{abu}" def _is_app_desc_valid(self, app_desc: Dict[str, str]) -> bool: From d2f223b420bcdce5d70cdf6854812d231e1bc173 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Mon, 18 Nov 2019 05:58:48 +0100 Subject: [PATCH 47/63] Update aiofreepybox.py: auto import modules, use path, update error messages --- aiofreepybox/aiofreepybox.py | 196 ++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 95 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index f171f4c9..f42425cf 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -2,15 +2,20 @@ import asyncio import base64 import bz2 +import inspect import ipaddress import json import logging -import os +import pkgutil import socket import ssl +from importlib import import_module +from os import fspath +from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union from urllib.parse import urljoin +# aiofpbx imports import aiofreepybox from aiofreepybox.exceptions import ( AuthorizationError, @@ -19,27 +24,21 @@ InvalidTokenError, NotOpenError, ) + +_API_MODS = {} +_DATA_DIR = Path(__file__).parent + from aiofreepybox.access import Access -from aiofreepybox.api.tv import Tv -from aiofreepybox.api.system import System -from aiofreepybox.api.dhcp import Dhcp -from aiofreepybox.api.switch import Switch -from aiofreepybox.api.lan import Lan -from aiofreepybox.api.lcd import Lcd -from aiofreepybox.api.wifi import Wifi -from aiofreepybox.api.phone import Phone -from aiofreepybox.api.fs import Fs -from aiofreepybox.api.fw import Fw -from aiofreepybox.api.freeplug import Freeplug -from aiofreepybox.api.call import Call -from aiofreepybox.api.connection import Connection -from aiofreepybox.api.home import Home -from aiofreepybox.api.parental import Parental -from aiofreepybox.api.nat import Nat -from aiofreepybox.api.notifications import Notifications -from aiofreepybox.api.rrd import Rrd -from aiofreepybox.api.upnpav import Upnpav -from aiofreepybox.api.upnpigd import Upnpigd + +# Import API modules +for _, name, _ in pkgutil.iter_modules([str(_DATA_DIR.joinpath("api"))]): + import_api_mod = import_module(".api." + name, package=__name__.rsplit(".", 1)[0]) + for i in dir(import_api_mod): + if inspect.isclass(getattr(import_api_mod, i)): + _API_MODS.update({name: import_api_mod}) + +# API modules extra parameters +_API_MODS_PARAMS: Dict[str, Any] = {} # {"player": {"api_version": "v6"}} # Default application descriptor _APP_DESC = { @@ -48,16 +47,12 @@ "app_version": aiofreepybox.__version__, "device_name": socket.gethostname(), } -_DATA_DIR = os.path.dirname(os.path.abspath(__file__)) - -# Db file default location -_DB_FILENAME = ".db" -_DB_FILE = os.path.join(_DATA_DIR, _DB_FILENAME) # App defaults _DEFAULT_API_VERSION = "v6" _DEFAULT_CERT = "freebox_certificates.pem" -_DEFAULT_DEVICE_NAME = "Freebox Server" +_DEFAULT_DEVICE_TYPE = "FreeboxServer" +_DEFAULT_ERR = "Error: " _DEFAULT_HOST = "mafreebox.freebox.fr" _DEFAULT_HTTP_PORT = "80" _DEFAULT_HTTPS_PORT = "443" @@ -66,9 +61,11 @@ _DEFAULT_UNKNOWN = "None" _LOGGER = logging.getLogger(__name__) -# Token file default location -_TOKEN_FILENAME = ".app_auth" -_TOKEN_FILE = os.path.join(_DATA_DIR, _TOKEN_FILENAME) +# Db file prefix +_F_DB_NAME = ".db" + +# Token file prefix +_F_TOKEN_NAME = ".app_auth" class Freepybox: @@ -79,8 +76,6 @@ class Freepybox: app_desc : `dict` , optional , Default to _APP_DESC - token_file : `str` , optional - , Default to _TOKEN_FILE api_version : `str`, "server" or "v(1-7)" , optional , Default to _DEFAULT_API_VERSION timeout : `int` , optional @@ -90,7 +85,6 @@ class Freepybox: def __init__( self, app_desc: Optional[Dict[str, str]] = None, - token_file: Optional[str] = None, api_version: Optional[str] = None, timeout: Optional[int] = None, ) -> None: @@ -99,7 +93,6 @@ def __init__( ) self.app_desc: Dict[str, str] = app_desc if app_desc is not None else _APP_DESC self.timeout: int = timeout if timeout is not None else _DEFAULT_TIMEOUT - self.token_file: str = token_file if token_file is not None else _TOKEN_FILE self._access: Optional[Access] = None self._fbx_db: Dict[str, Any] = {} self._fbx_uid: str = "" @@ -131,7 +124,7 @@ async def discover( """ if host_in and self._is_ipv6(host_in): - raise ValueError(f"{host_in} : IPv6 is not supported") + raise ValueError(f"{_DEFAULT_ERR}{host_in} : IPv6 is not supported") # Check session try: @@ -139,6 +132,8 @@ async def discover( self._disc_set_host_and_port(host_in, port_in) ) except ValueError as err: + if isinstance(err, ValueError): + raise return err.args[0] # Connect if session is closed @@ -149,26 +144,37 @@ async def discover( (self._session and self._session.closed), ] ) and not await self._disc_connect(**(fbx_addict)): - raise ValueError("Port closed or dns failed") + raise ValueError(f"{_DEFAULT_ERR}Port closed or dns failed") # Found freebox try: async with self._session.get( # type: ignore # noqa f"http{fbx_addict['s']}://{fbx_addict['host']}:{fbx_addict['port']}/api_version", timeout=self.timeout, + skip_auto_headers=["User-Agent"], ) as r: if r.content_type != "application/json": await self._disc_close_to_return() - raise ValueError("Invalid content type") + raise ValueError( + f"Invalid content type: {r.content_type}" + ) fbx_desc = await r.json() - except (ssl.SSLCertVerificationError, ValueError) as err: + except asyncio.TimeoutError: + raise ValueError(f"{_DEFAULT_ERR}Timeout") + except aiohttp.ClientSSLError: + raise ValueError(f"{_DEFAULT_ERR}SSL error") + except aiohttp.ClientConnectorError: + raise ValueError(f"{_DEFAULT_ERR}connect error") + except aiohttp.ServerDisconnectedError: + raise ValueError(f"{_DEFAULT_ERR}disconnected") + except ValueError as e: await self._disc_close_to_return() - raise ValueError(err.args[0]) + raise ValueError(f"{_DEFAULT_ERR}{e.args[0]}") - fbx_device = fbx_desc.get("device_name", None) - if fbx_device != _DEFAULT_DEVICE_NAME: + fbx_device = fbx_desc.get("device_type", None) + if _DEFAULT_DEVICE_TYPE not in fbx_device: await self._disc_close_to_return() - raise ValueError(f"{fbx_device}: Wrong device") + raise ValueError(f"{_DEFAULT_ERR}{fbx_device}: Wrong device") self._fbx_update_db(fbx_desc, fbx_addict) return fbx_desc @@ -211,7 +217,7 @@ async def open( """ if not self._is_app_desc_valid(self.app_desc): - raise InvalidTokenError("Invalid application descriptor") + raise InvalidTokenError(f"{_DEFAULT_ERR}Invalid application descriptor") # Get API access if uid is None: @@ -228,32 +234,22 @@ async def open( try: self._access = await self._get_app_access( - uid, self.token_file, self.app_desc, self.timeout + uid, _DATA_DIR, self.app_desc, self.timeout ) except AuthorizationError: raise # Instantiate freebox modules - self.tv = Tv(self._access) - self.system = System(self._access) - self.dhcp = Dhcp(self._access) - self.switch = Switch(self._access) - self.lan = Lan(self._access) - self.lcd = Lcd(self._access) - self.wifi = Wifi(self._access) - self.phone = Phone(self._access) - self.fs = Fs(self._access) - self.fw = Fw(self._access) - self.freeplug = Freeplug(self._access) - self.call = Call(self._access) - self.connection = Connection(self._access) - self.home = Home(self._access) - self.parental = Parental(self._access) - self.nat = Nat(self._access) - self.notifications = Notifications(self._access) - self.rrd = Rrd(self._access) - self.upnpav = Upnpav(self._access) - self.upnpigd = Upnpigd(self._access) + kwargs: Dict[str, str] = {} + for api_mod in _API_MODS: + api_mod_class = getattr(_API_MODS[api_mod], api_mod.capitalize()) + if api_mod in _API_MODS_PARAMS and isinstance( + _API_MODS_PARAMS[api_mod], dict + ): + kwargs = _API_MODS_PARAMS[api_mod] + setattr(self, api_mod, api_mod_class(self._access, **kwargs)) + if kwargs: + kwargs = {} def _check_api_version(self, fbx_api_version: str) -> str: """ @@ -327,7 +323,7 @@ async def _disc_connect(self, host: str, port: str, s: str) -> bool: # Connect try: if s == "s": - cert_path = os.path.join(os.path.dirname(__file__), _DEFAULT_CERT) + cert_path = str(_DATA_DIR.joinpath(_DEFAULT_CERT)) ssl_ctx = ssl.create_default_context() ssl_ctx.load_verify_locations(cafile=cert_path) conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) @@ -404,7 +400,7 @@ async def _fbx_open_init( async def _fbx_open_db(self, uid: str) -> str: """Open freebox db""" - db = self._readfile_fbx_db(_DB_FILE, uid) + db = self._readfile_fbx_db(_DATA_DIR, uid) d: Dict[str, Any] = {} if db is None: try: @@ -424,9 +420,9 @@ async def _fbx_open_db(self, uid: str) -> str: f"Cannot detect freebox for uid: {uid}" ", please check your configuration." ) - if isinstance(d['uid'], str) and uid != d["uid"]: + if isinstance(d["uid"], str) and uid != d["uid"]: raise NotOpenError( - f"{d['uid']}: Cannot detect freebox for uid: {uid}" + f"{_DEFAULT_ERR}{d['uid']}: Cannot detect freebox for uid: {uid}" ", please check your configuration." ) self._fbx_db[d["uid"]]["conf"]["api_version"] = self._check_api_version( @@ -464,7 +460,7 @@ def _fbx_ping_port(self, host: str, port: str) -> bool: result = 0 try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(_DEFAULT_TIMEOUT) + sock.settimeout(self.timeout) result = sock.connect_ex((host, int(port))) sock.close() except socket.gaierror: @@ -493,8 +489,8 @@ def _fbx_update_db( fbx_entry = {uid: {"conn": fbx_api_add, "conf": fbx_conf, "desc": fbx_desc}} self._fbx_db.update(fbx_entry) fbx_data = self._fbx_db[uid]["conn"] - if not self._readfile_fbx_db(_DB_FILE, uid): - self._writefile_fbx_db(_DB_FILE, uid) + if not self._readfile_fbx_db(_DATA_DIR, uid): + self._writefile_fbx_db(_DATA_DIR, uid) finally: if fbx_addict is not None: @@ -503,12 +499,12 @@ def _fbx_update_db( return fbx_data.append(fbx_addict) self._fbx_db[uid]["conn"] = fbx_data - self._writefile_fbx_db(_DB_FILE, uid) + self._writefile_fbx_db(_DATA_DIR, uid) async def _get_app_access( self, uid: str, - token_file: str, + token_file: Path, app_desc: Dict[str, str], timeout: int = _DEFAULT_TIMEOUT, ) -> Access: @@ -516,7 +512,7 @@ async def _get_app_access( Returns an access object used for HTTP(S) requests. uid : `str` - token_file : `str` + token_file : `Path` app_desc : `dict` timeout : `int` , Default to _DEFAULT_TIMEOUT @@ -544,7 +540,7 @@ async def _get_app_access( # denied status = authorization failed if status == "denied": raise AuthorizationError( - "The app token is invalid or has been revoked." + f"{_DEFAULT_ERR}The app token is invalid or has been revoked." ) # Pending status : user must accept the app request on the freebox @@ -556,7 +552,7 @@ async def _get_app_access( # timeout = authorization failed elif status == "timeout": - raise AuthorizationError("Authorization timed out.") + raise AuthorizationError(f"{_DEFAULT_ERR}Authorization timed out.") _LOGGER.info("Application authorization granted.") @@ -594,14 +590,14 @@ async def _get_app_token( url = urljoin(self._get_db_base_url(uid), "login/authorize/") data = json.dumps(app_desc) async with self._session.post( # type: ignore # noqa - url, data=data, timeout=timeout + url, data=data, timeout=timeout, skip_auto_headers=["User-Agent"] ) as r: resp = await r.json() # raise exception if resp.success != True if not resp.get("success"): raise AuthorizationError( - "Authorization failed (APIResponse: {}).".format(json.dumps(resp)) + f"{_DEFAULT_ERR}Authorization failed (APIResponse: {json.dumps(resp)})." ) app_token, track_id = resp["result"]["app_token"], resp["result"]["track_id"] @@ -627,7 +623,9 @@ async def _get_authorization_status( """ url = urljoin(self._get_db_base_url(uid), f"login/authorize/{track_id}") - async with self._session.get(url, timeout=timeout) as r: # type: ignore # noqa + async with self._session.get( # type: ignore # noqa + url, timeout=timeout, skip_auto_headers=["User-Agent"] + ) as r: resp = await r.json() return resp["result"]["status"] @@ -684,17 +682,19 @@ def _is_ipv6(self, ip_address: str) -> bool: except ValueError: return False - def _readfile_app_token(self, file: str, uid: str) -> Tuple[Any, Any, Any]: + def _readfile_app_token(self, file: Path, uid: str) -> Tuple[Any, Any, Any]: """ Read the application token in the authentication file. - file : `str` + file : `Path` uid : `str` Returns app_token, track_id, app_desc """ - fname = file + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + fname = file.joinpath( + _F_TOKEN_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + ) try: with bz2.open(fname, "rt", encoding="utf-8") as zf: d = json.load(zf) @@ -709,17 +709,19 @@ def _readfile_app_token(self, file: str, uid: str) -> Tuple[Any, Any, Any]: except FileNotFoundError: return None, None, None - def _readfile_fbx_db(self, file: str, uid: str) -> Optional[Dict[str, Any]]: + def _readfile_fbx_db(self, file: Path, uid: str) -> Optional[Dict[str, Any]]: """ Read the freebox db in the db file. - file : `str` + file : `Path` uid : `str` Returns fbx_db """ - fname = file + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + fname = file.joinpath( + _F_DB_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + ) try: with bz2.open(fname, "rt", encoding="utf-8") as zf: d = json.load(zf) @@ -732,37 +734,41 @@ def _writefile_app_token( app_token: str, track_id: str, app_desc: Dict[str, str], - file: str, + file: Path, uid: str, ) -> str: """ - Store the application token in a _TOKEN_FILE file + Store the application token in a ``_F_TOKEN`` file app_token : `str` track_id : `str` app_desc : `dict` - file : `str` + file : `Path` uid : `str` """ d = {**app_desc, "app_token": app_token, "track_id": track_id} - fname = file + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + fname = file.joinpath( + _F_TOKEN_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + ) with bz2.open(fname, "wt", encoding="utf-8") as zf: json.dump(d, zf) - return fname + return fspath(fname) - def _writefile_fbx_db(self, file: str, uid: str) -> str: + def _writefile_fbx_db(self, file: Path, uid: str) -> str: """ - Store the freebox db in a _DB_FILE file + Store the freebox db in a ``_F_DB`` file - file : `str` + file : `Path` uid : `str` """ - fname = file + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + fname = file.joinpath( + _F_DB_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + ) with bz2.open(fname, "wt", encoding="utf-8") as zf: json.dump(self._fbx_db[uid], zf) - return fname + return fspath(fname) @property def fbx_desc(self) -> Optional[dict]: From 0134fc95f4c1936f0170cd3f1346fbc263472da9 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Mon, 18 Nov 2019 21:49:22 +0100 Subject: [PATCH 48/63] update aiofreepybox.py: add db_get, db_clean, update error messages, cleanup --- aiofreepybox/aiofreepybox.py | 111 +++++++++++++++++++++++++++-------- 1 file changed, 88 insertions(+), 23 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index f42425cf..aa7c514f 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -111,6 +111,73 @@ async def close(self) -> None: await self._session.close() # type: ignore # noqa await asyncio.sleep(0.250) + def db_clean(self, uids: Optional[list] = None, all_: bool = False) -> bool: + """ + Remove unauth fbx's from db + + uids : `list`, optional + , Default to None + all_ : `bool` + , Default to False + """ + + if uids is None: + uids = [ + base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") + for x in _DATA_DIR.glob(_F_DB_NAME + "_*") + ] + auth = [ + base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") + for x in _DATA_DIR.glob(_F_TOKEN_NAME + "_*") + ] + c = 0 + for uid in uids: + if uid not in auth or all_: + file_p = _DATA_DIR + fname = file_p.joinpath( + _F_DB_NAME + + "_" + + base64.b64encode(uid.encode("utf-8")).decode("utf-8") + ) + try: + fname.unlink() + c += 1 + except FileNotFoundError: + pass + if c != 0: + _LOGGER.debug(f"{c} db entry cleared") + return True + return False + + def db_get( + self, uids: Optional[List[str]] = None + ) -> Optional[List[Dict[str, Any]]]: + """ + Get fbx_db from uids + + uids : `list` + , Default to None + """ + + fbx_db = [] + if uids is None: + uids = [ + base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") + for x in _DATA_DIR.glob(_F_DB_NAME + "_*") + ] + for uid in uids: + try: + fbx_db.append({uid: self._fbx_db[uid]}) + except KeyError: + d = self._readfile_fbx_db(_DATA_DIR, uid) + if d is not None: + fbx_db.append({uid: d}) + + if not fbx_db: + return None + _LOGGER.debug(f"{fbx_db.__len__} uid(s) in db") + return fbx_db + async def discover( self, host_in: Optional[str] = None, port_in: Optional[str] = None ) -> Dict[str, Any]: @@ -131,10 +198,10 @@ async def discover( fbx_addict = await self._disc_check_session( self._disc_set_host_and_port(host_in, port_in) ) - except ValueError as err: - if isinstance(err, ValueError): - raise - return err.args[0] + except ValueError as e: + raise + except KeyError as e: + return e.args[0] # Connect if session is closed if any( @@ -155,9 +222,7 @@ async def discover( ) as r: if r.content_type != "application/json": await self._disc_close_to_return() - raise ValueError( - f"Invalid content type: {r.content_type}" - ) + raise ValueError(f"Invalid content type: {r.content_type}") fbx_desc = await r.json() except asyncio.TimeoutError: raise ValueError(f"{_DEFAULT_ERR}Timeout") @@ -165,6 +230,8 @@ async def discover( raise ValueError(f"{_DEFAULT_ERR}SSL error") except aiohttp.ClientConnectorError: raise ValueError(f"{_DEFAULT_ERR}connect error") + except aiohttp.ClientResponseError: + raise ValueError(f"{_DEFAULT_ERR}response error") except aiohttp.ServerDisconnectedError: raise ValueError(f"{_DEFAULT_ERR}disconnected") except ValueError as e: @@ -299,11 +366,9 @@ async def _disc_check_session(self, fbx_addict: Dict[str, Any]) -> Dict[str, Any and c.port == int(fbx_addict["port"]) and c.is_ssl == (not not fbx_addict["s"]) ): - raise ValueError(self._fbx_db[self._fbx_uid]["desc"]) + raise KeyError(self._fbx_db[self._fbx_uid]["desc"]) await self._disc_close_to_return() - raise ValueError( - await self.discover(fbx_addict["host"], fbx_addict["port"]) - ) + raise KeyError(await self.discover(fbx_addict["host"], fbx_addict["port"])) return fbx_addict @@ -682,17 +747,17 @@ def _is_ipv6(self, ip_address: str) -> bool: except ValueError: return False - def _readfile_app_token(self, file: Path, uid: str) -> Tuple[Any, Any, Any]: + def _readfile_app_token(self, file_p: Path, uid: str) -> Tuple[Any, Any, Any]: """ Read the application token in the authentication file. - file : `Path` + file_p : `Path` uid : `str` Returns app_token, track_id, app_desc """ - fname = file.joinpath( + fname = file_p.joinpath( _F_TOKEN_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") ) try: @@ -709,17 +774,17 @@ def _readfile_app_token(self, file: Path, uid: str) -> Tuple[Any, Any, Any]: except FileNotFoundError: return None, None, None - def _readfile_fbx_db(self, file: Path, uid: str) -> Optional[Dict[str, Any]]: + def _readfile_fbx_db(self, file_p: Path, uid: str) -> Optional[Dict[str, Any]]: """ Read the freebox db in the db file. - file : `Path` + file_p : `Path` uid : `str` Returns fbx_db """ - fname = file.joinpath( + fname = file_p.joinpath( _F_DB_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") ) try: @@ -734,7 +799,7 @@ def _writefile_app_token( app_token: str, track_id: str, app_desc: Dict[str, str], - file: Path, + file_p: Path, uid: str, ) -> str: """ @@ -743,27 +808,27 @@ def _writefile_app_token( app_token : `str` track_id : `str` app_desc : `dict` - file : `Path` + file_p : `Path` uid : `str` """ d = {**app_desc, "app_token": app_token, "track_id": track_id} - fname = file.joinpath( + fname = file_p.joinpath( _F_TOKEN_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") ) with bz2.open(fname, "wt", encoding="utf-8") as zf: json.dump(d, zf) return fspath(fname) - def _writefile_fbx_db(self, file: Path, uid: str) -> str: + def _writefile_fbx_db(self, file_p: Path, uid: str) -> str: """ Store the freebox db in a ``_F_DB`` file - file : `Path` + file_p : `Path` uid : `str` """ - fname = file.joinpath( + fname = file_p.joinpath( _F_DB_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") ) with bz2.open(fname, "wt", encoding="utf-8") as zf: From 87310463f530fe55a9b9e2ea364a507a92e7d26f Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 19 Nov 2019 02:42:53 +0100 Subject: [PATCH 49/63] Update aiofreepybox.py: update close, update error messages, cleanup --- aiofreepybox/aiofreepybox.py | 88 +++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index aa7c514f..8052be52 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -103,13 +103,18 @@ async def close(self) -> None: Close the freebox session """ - if self._access is None or self._session.closed: # type: ignore # noqa - _LOGGER.warning(f"Closing but freebox is not connected") - return + if self._session and self._session.closed: # type: ignore # noqa + return None - await self._access.post("login/logout") - await self._session.close() # type: ignore # noqa - await asyncio.sleep(0.250) + try: + await self._access.post("login/logout") # type: ignore # noqa + except AttributeError: + pass + try: + await self._session.close() # type: ignore # noqa + await asyncio.sleep(0.250) + except AttributeError: + pass def db_clean(self, uids: Optional[list] = None, all_: bool = False) -> bool: """ @@ -204,14 +209,10 @@ async def discover( return e.args[0] # Connect if session is closed - if any( - [ - self._session is None, - not isinstance(self._session, aiohttp.ClientSession), - (self._session and self._session.closed), - ] - ) and not await self._disc_connect(**(fbx_addict)): - raise ValueError(f"{_DEFAULT_ERR}Port closed or dns failed") + try: + await self._disc_connect(**(fbx_addict)) + except ValueError: + raise # Found freebox try: @@ -226,14 +227,14 @@ async def discover( fbx_desc = await r.json() except asyncio.TimeoutError: raise ValueError(f"{_DEFAULT_ERR}Timeout") - except aiohttp.ClientSSLError: - raise ValueError(f"{_DEFAULT_ERR}SSL error") - except aiohttp.ClientConnectorError: - raise ValueError(f"{_DEFAULT_ERR}connect error") - except aiohttp.ClientResponseError: - raise ValueError(f"{_DEFAULT_ERR}response error") - except aiohttp.ServerDisconnectedError: - raise ValueError(f"{_DEFAULT_ERR}disconnected") + except ( + aiohttp.ClientSSLError, + aiohttp.ClientConnectorError, + aiohttp.ClientOSError, + aiohttp.ClientResponseError, + aiohttp.ServerDisconnectedError, + ) as e: + raise ValueError(f"{_DEFAULT_ERR}{str(e)}") except ValueError as e: await self._disc_close_to_return() raise ValueError(f"{_DEFAULT_ERR}{e.args[0]}") @@ -375,15 +376,19 @@ async def _disc_check_session(self, fbx_addict: Dict[str, Any]) -> Dict[str, Any async def _disc_close_to_return(self) -> None: """Close discovery session""" - if self._session is not None and not self._session.closed: - await self._session.close() + try: + await self._session.close() # type: ignore # noqa await asyncio.sleep(0.250) + except AttributeError: + pass - async def _disc_connect(self, host: str, port: str, s: str) -> bool: + async def _disc_connect(self, host: str, port: str, s: str) -> None: """Connect for discovery""" - if not self._fbx_ping_port(host, port): - return False + try: + self._fbx_ping_port(host, port) + except ValueError: + raise # Connect try: @@ -395,11 +400,9 @@ async def _disc_connect(self, host: str, port: str, s: str) -> bool: else: conn = aiohttp.TCPConnector() self._session = aiohttp.ClientSession(connector=conn) - except ssl.SSLCertVerificationError: + except ssl.SSLCertVerificationError as e: await self._disc_close_to_return() - return False - - return True + raise ValueError(str(e)) def _disc_set_host_and_port( self, host_in: Optional[str] = None, port_in: Optional[str] = None @@ -424,8 +427,8 @@ async def _fbx_enum_conns(self, db: Dict[str, Any]) -> Dict[str, Any]: for i, conn in enumerate(db["conn"]): try: d = await self.discover(conn["host"], conn["port"]) - except ValueError as err: - err_out = err + except ValueError as e: + err_out = e else: self._fbx_db[d["uid"]]["conf"]["cc"] = i break @@ -443,14 +446,14 @@ async def _fbx_open_init( try: fbx_desc = await self.discover(host_in, port_in) host, port = self._fbx_open_setup(fbx_desc, host_in, port_in) - except ValueError as err: + except ValueError as e: unk = _DEFAULT_UNKNOWN host, port = ( next(v for v in [host_in, host, unk] if v), next(v for v in [port_in, port, unk] if v), ) raise NotOpenError( - f"{err.args[0]}: Cannot detect freebox at " + f"{e.args[0]}: host error for " f"{host}:{port}" ", please check your configuration." ) @@ -470,18 +473,18 @@ async def _fbx_open_db(self, uid: str) -> str: if db is None: try: d = await self.discover() - except ValueError as err: + except ValueError as e: raise NotOpenError( - f"{err.args[0]}: Cannot detect freebox for uid: {uid}" + f"{e.args[0]}: Cannot detect freebox for uid: {uid}" ", please check your configuration." ) else: self._fbx_db[uid] = db try: d = await self._fbx_enum_conns(db) - except ValueError as err: + except ValueError as e: raise NotOpenError( - f"{err.args[0]}: " + f"{e.args[0]}: " f"Cannot detect freebox for uid: {uid}" ", please check your configuration." ) @@ -528,11 +531,11 @@ def _fbx_ping_port(self, host: str, port: str) -> bool: sock.settimeout(self.timeout) result = sock.connect_ex((host, int(port))) sock.close() - except socket.gaierror: - result = 1 + except socket.gaierror as e: + raise ValueError(f"{_DEFAULT_ERR}socket error: {str(e)}") finally: if result != 0: - return False + raise ValueError(f"{_DEFAULT_ERR}socket error: {result}") return True def _fbx_update_db( @@ -613,6 +616,7 @@ async def _get_app_access( if not out_msg_flag: out_msg_flag = True print("Please confirm the authentification on the freebox.") + _LOGGER.info(f"User action required: please confirm access on freebox.") await asyncio.sleep(1) # timeout = authorization failed From ef54de6ec76fae4b82c4211d7d1032a549699d7c Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 19 Nov 2019 03:09:47 +0100 Subject: [PATCH 50/63] Update example.py: update demo --- example.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/example.py b/example.py index f45eb71c..049f97e8 100755 --- a/example.py +++ b/example.py @@ -9,6 +9,7 @@ import m3u8 from aiofreepybox import Freepybox +from aiofreepybox.exceptions import (NotOpenError, AuthorizationError) async def demo(): @@ -22,7 +23,13 @@ async def demo(): # Connect to the freebox # Be ready to authorize the application on the Freebox if you use this # example for the first time - await fbx.open() + + try: + await fbx.open() + except NotOpenError as e: + print(f"Something went wrong {e}") + except AuthorizationError as e: + print(str(e)) if fbx.api_version == 'v6': # Get a jpg snapshot from a camera From 6d67bed172e89a58674df009240d28ab04c3500c Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 19 Nov 2019 09:54:29 +0100 Subject: [PATCH 51/63] Update __init__.py: remove empty line --- aiofreepybox/api/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiofreepybox/api/__init__.py b/aiofreepybox/api/__init__.py index 13490c3e..05e3699e 100644 --- a/aiofreepybox/api/__init__.py +++ b/aiofreepybox/api/__init__.py @@ -1,4 +1,4 @@ #-*- coding:utf-8 -*- +from typing import List -__all__ = [] - +__all__: List[str] = [] From 90b277e77144ea05ff7b65f9feb69d7297f88a53 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Wed, 20 Nov 2019 18:23:12 +0100 Subject: [PATCH 52/63] Update aiofreepybox.py: cleanup discovery, update error message, improve logic --- aiofreepybox/aiofreepybox.py | 426 ++++++++++++++++++----------------- 1 file changed, 218 insertions(+), 208 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 8052be52..bc15e873 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -98,25 +98,7 @@ def __init__( self._fbx_uid: str = "" self._session: Optional[aiohttp.ClientSession] = None - async def close(self) -> None: - """ - Close the freebox session - """ - - if self._session and self._session.closed: # type: ignore # noqa - return None - - try: - await self._access.post("login/logout") # type: ignore # noqa - except AttributeError: - pass - try: - await self._session.close() # type: ignore # noqa - await asyncio.sleep(0.250) - except AttributeError: - pass - - def db_clean(self, uids: Optional[list] = None, all_: bool = False) -> bool: + def clean_db(self, uids: Optional[list] = None, all_: bool = False) -> bool: """ Remove unauth fbx's from db @@ -154,34 +136,23 @@ def db_clean(self, uids: Optional[list] = None, all_: bool = False) -> bool: return True return False - def db_get( - self, uids: Optional[List[str]] = None - ) -> Optional[List[Dict[str, Any]]]: + async def close(self) -> None: """ - Get fbx_db from uids - - uids : `list` - , Default to None + Close the freebox session """ - fbx_db = [] - if uids is None: - uids = [ - base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") - for x in _DATA_DIR.glob(_F_DB_NAME + "_*") - ] - for uid in uids: - try: - fbx_db.append({uid: self._fbx_db[uid]}) - except KeyError: - d = self._readfile_fbx_db(_DATA_DIR, uid) - if d is not None: - fbx_db.append({uid: d}) - - if not fbx_db: + if self._session and self._session.closed: # type: ignore # noqa return None - _LOGGER.debug(f"{fbx_db.__len__} uid(s) in db") - return fbx_db + + try: + await self._access.post("login/logout") # type: ignore # noqa + except AttributeError: + pass + try: + await self._session.close() # type: ignore # noqa + await asyncio.sleep(0.250) + except AttributeError: + pass async def discover( self, host_in: Optional[str] = None, port_in: Optional[str] = None @@ -200,52 +171,77 @@ async def discover( # Check session try: - fbx_addict = await self._disc_check_session( - self._disc_set_host_and_port(host_in, port_in) + fbx_addict = await self._disc_c_session( + self._disc_s_host_port(host_in, port_in) ) except ValueError as e: raise except KeyError as e: return e.args[0] - # Connect if session is closed + # Found freebox ? + fbx_desc: Dict[str, Any] = {} try: - await self._disc_connect(**(fbx_addict)) + async with self._fbx_open_session(**fbx_addict) as session: + try: + async with session.get( # type: ignore # noqa + f"http{fbx_addict['s']}://{fbx_addict['host']}:{fbx_addict['port']}" + "/api_version", + timeout=self.timeout, + skip_auto_headers=["User-Agent"], + ) as r: + if r.content_type != "application/json": + raise ValueError(f"Invalid content type: {r.content_type}") + fbx_desc = await r.json() + except asyncio.TimeoutError: + raise ValueError(f"{_DEFAULT_ERR}Timeout") + except ( + aiohttp.ClientSSLError, + aiohttp.ClientConnectorError, + aiohttp.ClientOSError, + aiohttp.ClientResponseError, + aiohttp.ServerDisconnectedError, + ) as e: + raise ValueError(f"{_DEFAULT_ERR}{str(e)}") + except ValueError as e: + raise ValueError(f"{_DEFAULT_ERR}{e.args[0]}") except ValueError: raise - - # Found freebox - try: - async with self._session.get( # type: ignore # noqa - f"http{fbx_addict['s']}://{fbx_addict['host']}:{fbx_addict['port']}/api_version", - timeout=self.timeout, - skip_auto_headers=["User-Agent"], - ) as r: - if r.content_type != "application/json": - await self._disc_close_to_return() - raise ValueError(f"Invalid content type: {r.content_type}") - fbx_desc = await r.json() - except asyncio.TimeoutError: - raise ValueError(f"{_DEFAULT_ERR}Timeout") - except ( - aiohttp.ClientSSLError, - aiohttp.ClientConnectorError, - aiohttp.ClientOSError, - aiohttp.ClientResponseError, - aiohttp.ServerDisconnectedError, - ) as e: - raise ValueError(f"{_DEFAULT_ERR}{str(e)}") - except ValueError as e: - await self._disc_close_to_return() - raise ValueError(f"{_DEFAULT_ERR}{e.args[0]}") - fbx_device = fbx_desc.get("device_type", None) if _DEFAULT_DEVICE_TYPE not in fbx_device: - await self._disc_close_to_return() raise ValueError(f"{_DEFAULT_ERR}{fbx_device}: Wrong device") - self._fbx_update_db(fbx_desc, fbx_addict) - return fbx_desc + uid = self._fbx_update_db(fbx_desc, fbx_addict) + return self._fbx_db[uid]["desc"] + + def get_db( + self, uids: Optional[List[str]] = None + ) -> Optional[List[Dict[str, Any]]]: + """ + Get fbx_db from uids + + uids : `list` + , Default to None + """ + + fbx_db = [] + if uids is None: + uids = [ + base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") + for x in _DATA_DIR.glob(_F_DB_NAME + "_*") + ] + for uid in uids: + try: + fbx_db.append({uid: self._fbx_db[uid]}) + except KeyError: + d = self._readfile_fbx_db(_DATA_DIR, uid) + if d is not None: + fbx_db.append({uid: d}) + + if not fbx_db: + return None + _LOGGER.debug(f"{fbx_db.__len__} uid(s) in db") + return fbx_db async def get_permissions(self) -> Optional[dict]: """ @@ -291,7 +287,7 @@ async def open( if uid is None: uid = "" try: - uid = await self._fbx_open_init(host, port) + uid = await self._fbx_open_disc(host, port) except NotOpenError: raise else: @@ -307,6 +303,8 @@ async def open( except AuthorizationError: raise + self._fbx_uid = uid + # Instantiate freebox modules kwargs: Dict[str, str] = {} for api_mod in _API_MODS: @@ -319,7 +317,46 @@ async def open( if kwargs: kwargs = {} - def _check_api_version(self, fbx_api_version: str) -> str: + async def _disc_c_session(self, fbx_addict: Dict[str, Any]) -> Dict[str, Any]: + """Check discovery session""" + + if ( + self._session + and not self._session.closed + and self._session._connector._conns # type: ignore # noqa + ): + c = list(self._session._connector._conns.keys())[0] # type: ignore # noqa + if ( + c.host == fbx_addict["host"] + and c.port == int(fbx_addict["port"]) + and c.is_ssl == (not not fbx_addict["s"]) + ): + raise KeyError(self._fbx_db[self._fbx_uid]["desc"]) + try: + await self._session.close() # type: ignore # noqa + await asyncio.sleep(0.250) + except AttributeError: + pass + + return fbx_addict + + def _disc_s_host_port( + self, host_in: Optional[str] = None, port_in: Optional[str] = None + ) -> Dict[str, Any]: + """Set discovery host and port""" + + host, port = ( + host_in if host_in else _DEFAULT_HOST, + port_in if port_in else _DEFAULT_HTTP_PORT, + ) + if not host_in and not port_in: + port = _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else port + s = "s" if _DEFAULT_SSL and port != _DEFAULT_HTTP_PORT else "" + + del self, host_in, port_in + return locals() + + def _fbx_c_api_version(self, fbx_api_version: str) -> str: """ Check api version """ @@ -330,6 +367,7 @@ def _check_api_version(self, fbx_api_version: str) -> str: s_default_api_version = _DEFAULT_API_VERSION[1:] if self.api_version == "server": api_version = f"v{s_fbx_version}" + # Check user API version s_api_version = self.api_version[1:] if s_default_api_version < s_fbx_version and s_api_version == s_fbx_version: @@ -353,117 +391,21 @@ def _check_api_version(self, fbx_api_version: str) -> str: self.api_version = api_version return api_version - async def _disc_check_session(self, fbx_addict: Dict[str, Any]) -> Dict[str, Any]: - """Check discovery session""" - - if ( - self._session - and not self._session.closed - and self._session._connector._conns # type: ignore # noqa - ): - c = list(self._session._connector._conns.keys())[0] # type: ignore # noqa - if ( - c.host == fbx_addict["host"] - and c.port == int(fbx_addict["port"]) - and c.is_ssl == (not not fbx_addict["s"]) - ): - raise KeyError(self._fbx_db[self._fbx_uid]["desc"]) - await self._disc_close_to_return() - raise KeyError(await self.discover(fbx_addict["host"], fbx_addict["port"])) - - return fbx_addict - - async def _disc_close_to_return(self) -> None: - """Close discovery session""" - - try: - await self._session.close() # type: ignore # noqa - await asyncio.sleep(0.250) - except AttributeError: - pass - - async def _disc_connect(self, host: str, port: str, s: str) -> None: - """Connect for discovery""" - - try: - self._fbx_ping_port(host, port) - except ValueError: - raise - - # Connect - try: - if s == "s": - cert_path = str(_DATA_DIR.joinpath(_DEFAULT_CERT)) - ssl_ctx = ssl.create_default_context() - ssl_ctx.load_verify_locations(cafile=cert_path) - conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) - else: - conn = aiohttp.TCPConnector() - self._session = aiohttp.ClientSession(connector=conn) - except ssl.SSLCertVerificationError as e: - await self._disc_close_to_return() - raise ValueError(str(e)) - - def _disc_set_host_and_port( - self, host_in: Optional[str] = None, port_in: Optional[str] = None - ) -> Dict[str, Any]: - """Set discovery host and port""" - - host, port = ( - host_in if host_in else _DEFAULT_HOST, - port_in if port_in else _DEFAULT_HTTP_PORT, - ) - if not host_in and not port_in: - port = _DEFAULT_HTTPS_PORT if _DEFAULT_SSL else port - s = "s" if _DEFAULT_SSL and port != _DEFAULT_HTTP_PORT else "" - - del self, host_in, port_in - return locals() - - async def _fbx_enum_conns(self, db: Dict[str, Any]) -> Dict[str, Any]: + def _fbx_enum_conns(self, db: Dict[str, Any]) -> None: """Try freebox connections""" - d = None + uid = db["desc"]["uid"] for i, conn in enumerate(db["conn"]): try: - d = await self.discover(conn["host"], conn["port"]) + self._fbx_ping_port(conn["host"], conn["port"]) + self._session = self._fbx_open_session(**conn) except ValueError as e: err_out = e else: - self._fbx_db[d["uid"]]["conf"]["cc"] = i + self._fbx_db[uid]["conf"]["cc"] = i break - if d is None: + if self._session is None and err_out: raise ValueError(err_out.args[0]) - return d - - async def _fbx_open_init( - self, host_in: Optional[str] = None, port_in: Optional[str] = None - ) -> str: - """Init freebox link for open""" - - host = None - port = None - try: - fbx_desc = await self.discover(host_in, port_in) - host, port = self._fbx_open_setup(fbx_desc, host_in, port_in) - except ValueError as e: - unk = _DEFAULT_UNKNOWN - host, port = ( - next(v for v in [host_in, host, unk] if v), - next(v for v in [port_in, port, unk] if v), - ) - raise NotOpenError( - f"{e.args[0]}: host error for " - f"{host}:{port}" - ", please check your configuration." - ) - d = await self._fbx_enum_conns(self._fbx_db[fbx_desc["uid"]]) - - self._fbx_db[d["uid"]]["conf"]["api_version"] = self._check_api_version( - self._fbx_db[d["uid"]]["desc"]["api_version"] - ) - self._fbx_uid = d["uid"] - return d["uid"] async def _fbx_open_db(self, uid: str) -> str: """Open freebox db""" @@ -472,33 +414,84 @@ async def _fbx_open_db(self, uid: str) -> str: d: Dict[str, Any] = {} if db is None: try: - d = await self.discover() + fbx_desc = await self.discover() except ValueError as e: raise NotOpenError( f"{e.args[0]}: Cannot detect freebox for uid: {uid}" ", please check your configuration." ) + else: + if uid != fbx_desc["uid"]: + raise NotOpenError( + f"{_DEFAULT_ERR}{d['uid']}: Cannot detect freebox for uid: {uid}" + ", please check your configuration." + ) else: self._fbx_db[uid] = db try: - d = await self._fbx_enum_conns(db) + self._fbx_enum_conns(db) except ValueError as e: raise NotOpenError( f"{e.args[0]}: " f"Cannot detect freebox for uid: {uid}" ", please check your configuration." ) - if isinstance(d["uid"], str) and uid != d["uid"]: + self._fbx_db[uid]["conf"]["api_version"] = self._fbx_c_api_version( + self._fbx_db[uid]["desc"]["api_version"] + ) + return uid + + async def _fbx_open_disc( + self, host_in: Optional[str] = None, port_in: Optional[str] = None + ) -> str: + """Init freebox link for open""" + + host = None + port = None + try: + fbx_desc = await self.discover(host_in, port_in) + host, port = self._fbx_open_setup(fbx_desc, host_in, port_in) + except ValueError as e: + unk = _DEFAULT_UNKNOWN + host, port = ( + next(v for v in [host_in, host, unk] if v), + next(v for v in [port_in, port, unk] if v), + ) raise NotOpenError( - f"{_DEFAULT_ERR}{d['uid']}: Cannot detect freebox for uid: {uid}" + f"{e.args[0]}: host error for " + f"{host}:{port}" ", please check your configuration." ) - self._fbx_db[d["uid"]]["conf"]["api_version"] = self._check_api_version( - self._fbx_db[d["uid"]]["desc"]["api_version"] + uid: str = fbx_desc["uid"] + self._fbx_enum_conns(self._fbx_db[uid]) + self._fbx_db[uid]["conf"]["api_version"] = self._fbx_c_api_version( + self._fbx_db[uid]["desc"]["api_version"] ) - self._fbx_uid = uid return uid + def _fbx_open_session(self, host: str, port: str, s: str) -> aiohttp.ClientSession: + """Connect for discovery""" + + try: + self._fbx_ping_port(host, port) + except ValueError: + raise + + # Connect + try: + if s == "s": + cert_path = str(_DATA_DIR.joinpath(_DEFAULT_CERT)) + ssl_ctx = ssl.create_default_context() + ssl_ctx.load_verify_locations(cafile=cert_path) + conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) + else: + conn = aiohttp.TCPConnector() + session = aiohttp.ClientSession(connector=conn) + except ssl.SSLCertVerificationError as e: + raise ValueError(str(e)) + else: + return session + def _fbx_open_setup( self, fbx_desc: Dict[str, Any], @@ -530,22 +523,29 @@ def _fbx_ping_port(self, host: str, port: str) -> bool: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(self.timeout) result = sock.connect_ex((host, int(port))) - sock.close() except socket.gaierror as e: - raise ValueError(f"{_DEFAULT_ERR}socket error: {str(e)}") - finally: + raise ValueError(f"socket resolve error: {str(e)}") + else: if result != 0: - raise ValueError(f"{_DEFAULT_ERR}socket error: {result}") + raise ValueError(f"socket error: {result}") return True + finally: + sock.close() def _fbx_update_db( self, fbx_desc: Dict[str, Any], fbx_addict: Optional[Dict[str, Any]] - ) -> None: + ) -> str: + fbx_conn: List[Dict[str, Any]] = [] uid: str = fbx_desc["uid"] - try: - fbx_data: List[Dict[str, Any]] = self._fbx_db[uid]["conn"] - except KeyError: + write_db = False + if uid in self._fbx_db: + fbx_conn = self._fbx_db[uid]["conn"] + if fbx_desc != self._fbx_db[uid]["desc"]: + write_db = True + self._fbx_db[uid]["desc"] = fbx_desc + write_db = True + else: fbx_api_add = [ { "host": fbx_desc["api_domain"], @@ -556,18 +556,24 @@ def _fbx_update_db( fbx_conf = {"api_version": "", "cc": 0} fbx_entry = {uid: {"conn": fbx_api_add, "conf": fbx_conf, "desc": fbx_desc}} self._fbx_db.update(fbx_entry) - fbx_data = self._fbx_db[uid]["conn"] + fbx_conn = self._fbx_db[uid]["conn"] if not self._readfile_fbx_db(_DATA_DIR, uid): - self._writefile_fbx_db(_DATA_DIR, uid) + write_db = True + + if fbx_addict is not None: + for k in fbx_conn: + if dict(k) == fbx_addict: + return uid + fbx_conn.append(fbx_addict) + self._fbx_db[uid]["conn"] = fbx_conn + write_db = True + + if write_db: + asyncio.create_task( + self._writefile_fbx_db(_DATA_DIR, uid, self._fbx_db[uid]) + ) - finally: - if fbx_addict is not None: - for k in fbx_data: - if dict(k) == fbx_addict: - return - fbx_data.append(fbx_addict) - self._fbx_db[uid]["conn"] = fbx_data - self._writefile_fbx_db(_DATA_DIR, uid) + return uid async def _get_app_access( self, @@ -616,7 +622,9 @@ async def _get_app_access( if not out_msg_flag: out_msg_flag = True print("Please confirm the authentification on the freebox.") - _LOGGER.info(f"User action required: please confirm access on freebox.") + _LOGGER.info( + f"User action required: please confirm access on freebox." + ) await asyncio.sleep(1) # timeout = authorization failed @@ -824,7 +832,9 @@ def _writefile_app_token( json.dump(d, zf) return fspath(fname) - def _writefile_fbx_db(self, file_p: Path, uid: str) -> str: + async def _writefile_fbx_db( + self, file_p: Path, uid: str, db: Dict[str, Any] + ) -> str: """ Store the freebox db in a ``_F_DB`` file @@ -836,7 +846,7 @@ def _writefile_fbx_db(self, file_p: Path, uid: str) -> str: _F_DB_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") ) with bz2.open(fname, "wt", encoding="utf-8") as zf: - json.dump(self._fbx_db[uid], zf) + json.dump(db, zf) return fspath(fname) @property From c7aafb81db65c5fe49c6e4f425d31dbc26e658a6 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Thu, 21 Nov 2019 22:18:11 +0100 Subject: [PATCH 53/63] Update aiofreepybox.py: merge try blocks --- aiofreepybox/aiofreepybox.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index bc15e873..3705ce22 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -284,17 +284,13 @@ async def open( raise InvalidTokenError(f"{_DEFAULT_ERR}Invalid application descriptor") # Get API access - if uid is None: - uid = "" - try: + try: + if uid is None: uid = await self._fbx_open_disc(host, port) - except NotOpenError: - raise - else: - try: + else: uid = await self._fbx_open_db(uid) - except NotOpenError: - raise + except NotOpenError: + raise try: self._access = await self._get_app_access( From 607d9d18bbb39767bccdd95b1cdc0342f6ff324a Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Sun, 24 Nov 2019 12:43:56 +0100 Subject: [PATCH 54/63] Update aiofreepybox.py: polish, remove duplicates (blocks, lines), more polish --- aiofreepybox/aiofreepybox.py | 110 +++++++++++++++-------------------- 1 file changed, 46 insertions(+), 64 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 3705ce22..4e4cc72a 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -120,8 +120,7 @@ def clean_db(self, uids: Optional[list] = None, all_: bool = False) -> bool: c = 0 for uid in uids: if uid not in auth or all_: - file_p = _DATA_DIR - fname = file_p.joinpath( + fname = _DATA_DIR.joinpath( _F_DB_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") @@ -155,25 +154,24 @@ async def close(self) -> None: pass async def discover( - self, host_in: Optional[str] = None, port_in: Optional[str] = None + self, host: Optional[str] = None, port: Optional[Union[int, str]] = None ) -> Dict[str, Any]: """ Discover a freebox on the network host : `str` , optional , Default to None - port : `str` , optional + port : Union[int,str] , optional , Default to None """ - if host_in and self._is_ipv6(host_in): - raise ValueError(f"{_DEFAULT_ERR}{host_in} : IPv6 is not supported") + if host and self._is_ipv6(host): + raise ValueError(f"{_DEFAULT_ERR}{host} : IPv6 is not supported") + port = str(port) if isinstance(port, int) else port # Check session try: - fbx_addict = await self._disc_c_session( - self._disc_s_host_port(host_in, port_in) - ) + fbx_addict = await self._disc_c_session(self._disc_s_host_port(host, port)) except ValueError as e: raise except KeyError as e: @@ -183,30 +181,27 @@ async def discover( fbx_desc: Dict[str, Any] = {} try: async with self._fbx_open_session(**fbx_addict) as session: - try: - async with session.get( # type: ignore # noqa - f"http{fbx_addict['s']}://{fbx_addict['host']}:{fbx_addict['port']}" - "/api_version", - timeout=self.timeout, - skip_auto_headers=["User-Agent"], - ) as r: - if r.content_type != "application/json": - raise ValueError(f"Invalid content type: {r.content_type}") - fbx_desc = await r.json() - except asyncio.TimeoutError: - raise ValueError(f"{_DEFAULT_ERR}Timeout") - except ( - aiohttp.ClientSSLError, - aiohttp.ClientConnectorError, - aiohttp.ClientOSError, - aiohttp.ClientResponseError, - aiohttp.ServerDisconnectedError, - ) as e: - raise ValueError(f"{_DEFAULT_ERR}{str(e)}") - except ValueError as e: - raise ValueError(f"{_DEFAULT_ERR}{e.args[0]}") - except ValueError: - raise + async with session.get( # type: ignore # noqa + f"http{fbx_addict['s']}://{fbx_addict['host']}:{fbx_addict['port']}" + "/api_version", + timeout=self.timeout, + skip_auto_headers=["User-Agent"], + ) as r: + if r.content_type != "application/json": + raise ValueError(f"Invalid content type: {r.content_type}") + fbx_desc = await r.json() + except asyncio.TimeoutError: + raise ValueError(f"{_DEFAULT_ERR}Timeout") + except ( + aiohttp.ClientSSLError, + aiohttp.ClientConnectorError, + aiohttp.ClientOSError, + aiohttp.ClientResponseError, + aiohttp.ServerDisconnectedError, + ) as e: + raise ValueError(f"{_DEFAULT_ERR}{str(e)}") + except ValueError as e: + raise ValueError(f"{_DEFAULT_ERR}{e.args[0]}") fbx_device = fbx_desc.get("device_type", None) if _DEFAULT_DEVICE_TYPE not in fbx_device: raise ValueError(f"{_DEFAULT_ERR}{fbx_device}: Wrong device") @@ -265,7 +260,7 @@ async def get_permissions(self) -> Optional[dict]: async def open( self, host: Optional[str] = None, - port: Optional[str] = None, + port: Optional[Union[int, str]] = None, uid: Optional[str] = None, ) -> None: """ @@ -274,7 +269,7 @@ async def open( host : `str` , optional , Default to `None` - port : `str` , optional + port : Union[`str`,`int`] , optional , Default to `None` uid : `str` , optional , Default to `None` @@ -400,6 +395,7 @@ def _fbx_enum_conns(self, db: Dict[str, Any]) -> None: else: self._fbx_db[uid]["conf"]["cc"] = i break + if self._session is None and err_out: raise ValueError(err_out.args[0]) @@ -407,43 +403,33 @@ async def _fbx_open_db(self, uid: str) -> str: """Open freebox db""" db = self._readfile_fbx_db(_DATA_DIR, uid) - d: Dict[str, Any] = {} - if db is None: - try: + try: + if db is None: fbx_desc = await self.discover() - except ValueError as e: - raise NotOpenError( - f"{e.args[0]}: Cannot detect freebox for uid: {uid}" - ", please check your configuration." - ) - else: if uid != fbx_desc["uid"]: - raise NotOpenError( - f"{_DEFAULT_ERR}{d['uid']}: Cannot detect freebox for uid: {uid}" - ", please check your configuration." - ) - else: - self._fbx_db[uid] = db - try: + raise ValueError(f"found: {fbx_desc['uid']}") + else: + self._fbx_db[uid] = db self._fbx_enum_conns(db) - except ValueError as e: - raise NotOpenError( - f"{e.args[0]}: " - f"Cannot detect freebox for uid: {uid}" - ", please check your configuration." - ) + except ValueError as e: + raise NotOpenError( + f"{e.args[0]}: Cannot detect freebox for uid: {uid}" + ", please check your configuration." + ) self._fbx_db[uid]["conf"]["api_version"] = self._fbx_c_api_version( self._fbx_db[uid]["desc"]["api_version"] ) return uid async def _fbx_open_disc( - self, host_in: Optional[str] = None, port_in: Optional[str] = None + self, host_in: Optional[str] = None, port_in: Optional[Union[int, str]] = None ) -> str: """Init freebox link for open""" host = None port = None + port_in = str(port_in) if isinstance(port_in, int) else port_in + try: fbx_desc = await self.discover(host_in, port_in) host, port = self._fbx_open_setup(fbx_desc, host_in, port_in) @@ -473,7 +459,7 @@ def _fbx_open_session(self, host: str, port: str, s: str) -> aiohttp.ClientSessi except ValueError: raise - # Connect + # Connect session try: if s == "s": cert_path = str(_DATA_DIR.joinpath(_DEFAULT_CERT)) @@ -538,7 +524,6 @@ def _fbx_update_db( if uid in self._fbx_db: fbx_conn = self._fbx_db[uid]["conn"] if fbx_desc != self._fbx_db[uid]["desc"]: - write_db = True self._fbx_db[uid]["desc"] = fbx_desc write_db = True else: @@ -556,10 +541,7 @@ def _fbx_update_db( if not self._readfile_fbx_db(_DATA_DIR, uid): write_db = True - if fbx_addict is not None: - for k in fbx_conn: - if dict(k) == fbx_addict: - return uid + if fbx_addict is not None and fbx_addict not in fbx_conn: fbx_conn.append(fbx_addict) self._fbx_db[uid]["conn"] = fbx_conn write_db = True From 90626b08ca234b289795b074da160b75fa9ef892 Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Tue, 26 Nov 2019 21:22:23 +0100 Subject: [PATCH 55/63] Update aiofreepybox.py: purepath is not path, fix possible unbound values, extra polish --- aiofreepybox/aiofreepybox.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 4e4cc72a..f81b588a 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -31,7 +31,7 @@ from aiofreepybox.access import Access # Import API modules -for _, name, _ in pkgutil.iter_modules([str(_DATA_DIR.joinpath("api"))]): +for _, name, _ in pkgutil.iter_modules([str(Path(_DATA_DIR).joinpath("api"))]): import_api_mod = import_module(".api." + name, package=__name__.rsplit(".", 1)[0]) for i in dir(import_api_mod): if inspect.isclass(getattr(import_api_mod, i)): @@ -111,16 +111,16 @@ def clean_db(self, uids: Optional[list] = None, all_: bool = False) -> bool: if uids is None: uids = [ base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") - for x in _DATA_DIR.glob(_F_DB_NAME + "_*") + for x in Path(_DATA_DIR).glob(_F_DB_NAME + "_*") ] auth = [ base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") - for x in _DATA_DIR.glob(_F_TOKEN_NAME + "_*") + for x in Path(_DATA_DIR).glob(_F_TOKEN_NAME + "_*") ] c = 0 for uid in uids: if uid not in auth or all_: - fname = _DATA_DIR.joinpath( + fname = Path(_DATA_DIR).joinpath( _F_DB_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") @@ -223,13 +223,13 @@ def get_db( if uids is None: uids = [ base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") - for x in _DATA_DIR.glob(_F_DB_NAME + "_*") + for x in Path(_DATA_DIR).glob(_F_DB_NAME + "_*") ] for uid in uids: try: fbx_db.append({uid: self._fbx_db[uid]}) except KeyError: - d = self._readfile_fbx_db(_DATA_DIR, uid) + d = self._readfile_fbx_db(Path(_DATA_DIR), uid) if d is not None: fbx_db.append({uid: d}) @@ -289,7 +289,7 @@ async def open( try: self._access = await self._get_app_access( - uid, _DATA_DIR, self.app_desc, self.timeout + uid, Path(_DATA_DIR), self.app_desc, self.timeout ) except AuthorizationError: raise @@ -385,6 +385,7 @@ def _fbx_c_api_version(self, fbx_api_version: str) -> str: def _fbx_enum_conns(self, db: Dict[str, Any]) -> None: """Try freebox connections""" + err_out: ValueError = ValueError() uid = db["desc"]["uid"] for i, conn in enumerate(db["conn"]): try: @@ -396,13 +397,13 @@ def _fbx_enum_conns(self, db: Dict[str, Any]) -> None: self._fbx_db[uid]["conf"]["cc"] = i break - if self._session is None and err_out: + if self._session is None: raise ValueError(err_out.args[0]) async def _fbx_open_db(self, uid: str) -> str: """Open freebox db""" - db = self._readfile_fbx_db(_DATA_DIR, uid) + db = self._readfile_fbx_db(Path(_DATA_DIR), uid) try: if db is None: fbx_desc = await self.discover() @@ -462,7 +463,7 @@ def _fbx_open_session(self, host: str, port: str, s: str) -> aiohttp.ClientSessi # Connect session try: if s == "s": - cert_path = str(_DATA_DIR.joinpath(_DEFAULT_CERT)) + cert_path = str(Path(_DATA_DIR).joinpath(_DEFAULT_CERT)) ssl_ctx = ssl.create_default_context() ssl_ctx.load_verify_locations(cafile=cert_path) conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) @@ -482,20 +483,21 @@ def _fbx_open_setup( ) -> Tuple[str, str]: """Setup host and port value for open""" + host_s, port_s = "", "" if _DEFAULT_SSL and fbx_desc["https_available"]: - host, port = ( + host_s, port_s = ( fbx_desc["api_domain"] if host is None or self._is_ipv4(host) else host, fbx_desc["https_port"] if port is None or port == _DEFAULT_HTTP_PORT else port, ) else: - host, port = ( + host_s, port_s = ( _DEFAULT_HOST if host is None else host, _DEFAULT_HTTP_PORT if port is None else port, ) - return host, port + return host_s, port_s def _fbx_ping_port(self, host: str, port: str) -> bool: """Check if a port if open""" @@ -505,14 +507,13 @@ def _fbx_ping_port(self, host: str, port: str) -> bool: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(self.timeout) result = sock.connect_ex((host, int(port))) + sock.close() except socket.gaierror as e: raise ValueError(f"socket resolve error: {str(e)}") else: if result != 0: raise ValueError(f"socket error: {result}") return True - finally: - sock.close() def _fbx_update_db( self, fbx_desc: Dict[str, Any], fbx_addict: Optional[Dict[str, Any]] @@ -538,7 +539,7 @@ def _fbx_update_db( fbx_entry = {uid: {"conn": fbx_api_add, "conf": fbx_conf, "desc": fbx_desc}} self._fbx_db.update(fbx_entry) fbx_conn = self._fbx_db[uid]["conn"] - if not self._readfile_fbx_db(_DATA_DIR, uid): + if not self._readfile_fbx_db(Path(_DATA_DIR), uid): write_db = True if fbx_addict is not None and fbx_addict not in fbx_conn: @@ -548,7 +549,7 @@ def _fbx_update_db( if write_db: asyncio.create_task( - self._writefile_fbx_db(_DATA_DIR, uid, self._fbx_db[uid]) + self._writefile_fbx_db(Path(_DATA_DIR), uid, self._fbx_db[uid]) ) return uid From ae472db6a2d4eade21da4d829d6c44954c6a01ae Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Thu, 28 Nov 2019 23:00:29 +0100 Subject: [PATCH 56/63] Update aiofreepybox.py: more polish, setup api module instance on access. --- aiofreepybox/aiofreepybox.py | 85 ++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 24 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index f81b588a..bdac51dc 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -2,7 +2,6 @@ import asyncio import base64 import bz2 -import inspect import ipaddress import json import logging @@ -10,6 +9,7 @@ import socket import ssl from importlib import import_module +from importlib import resources from os import fspath from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union @@ -24,19 +24,8 @@ InvalidTokenError, NotOpenError, ) - -_API_MODS = {} -_DATA_DIR = Path(__file__).parent - from aiofreepybox.access import Access -# Import API modules -for _, name, _ in pkgutil.iter_modules([str(Path(_DATA_DIR).joinpath("api"))]): - import_api_mod = import_module(".api." + name, package=__name__.rsplit(".", 1)[0]) - for i in dir(import_api_mod): - if inspect.isclass(getattr(import_api_mod, i)): - _API_MODS.update({name: import_api_mod}) - # API modules extra parameters _API_MODS_PARAMS: Dict[str, Any] = {} # {"player": {"api_version": "v6"}} @@ -48,6 +37,8 @@ "device_name": socket.gethostname(), } +_DATA_DIR = Path(__file__).parent + # App defaults _DEFAULT_API_VERSION = "v6" _DEFAULT_CERT = "freebox_certificates.pem" @@ -98,6 +89,50 @@ def __init__( self._fbx_uid: str = "" self._session: Optional[aiohttp.ClientSession] = None + def __imp_api__(self, api_l: List[str]) -> Dict[str, Any]: + """ Import API modules """ + + l = len(api_l) + api_l = list( + filter( + lambda n: resources.is_resource( + __name__.rsplit(".", 1)[0] + ".api", str(n) + ".py" + ), + api_l, + ) + ) + if len(api_l) != l: + raise KeyError + + mods = { + str(n): import_module(".api." + str(n), package=__name__.rsplit(".", 1)[0]) + for n in api_l + } + return mods + + def __getattr__(self, k): + """ Return API attribute """ + + if "__" in k: + raise AttributeError + + try: + mod = self.__imp_api__([k]) + except KeyError: + _LOGGER.error(f"Invalid API name: {k}") + raise AttributeError + else: + if self._access: + kwargs: Dict[str, str] = {} + api_mod_class = getattr(mod[k], k.capitalize()) + if k in _API_MODS_PARAMS and isinstance(_API_MODS_PARAMS[k], dict): + kwargs = _API_MODS_PARAMS[k] + setattr(self, k, api_mod_class(self._access, **kwargs)) + else: + return getattr(mod[k], k.capitalize()) + + return getattr(self, k) + def clean_db(self, uids: Optional[list] = None, all_: bool = False) -> bool: """ Remove unauth fbx's from db @@ -235,7 +270,7 @@ def get_db( if not fbx_db: return None - _LOGGER.debug(f"{fbx_db.__len__} uid(s) in db") + _LOGGER.debug(f"{len(fbx_db)} uid(s) in db") return fbx_db async def get_permissions(self) -> Optional[dict]: @@ -296,17 +331,14 @@ async def open( self._fbx_uid = uid - # Instantiate freebox modules - kwargs: Dict[str, str] = {} - for api_mod in _API_MODS: - api_mod_class = getattr(_API_MODS[api_mod], api_mod.capitalize()) - if api_mod in _API_MODS_PARAMS and isinstance( - _API_MODS_PARAMS[api_mod], dict - ): - kwargs = _API_MODS_PARAMS[api_mod] - setattr(self, api_mod, api_mod_class(self._access, **kwargs)) - if kwargs: - kwargs = {} + def _api_mods_l(self) -> List[str]: + """ Return mods list """ + return list( + map( + lambda t: t[1], + pkgutil.iter_modules([str(Path(_DATA_DIR).joinpath("api"))]), + ) + ) async def _disc_c_session(self, fbx_addict: Dict[str, Any]) -> Dict[str, Any]: """Check discovery session""" @@ -838,6 +870,11 @@ def fbx_api_url(self) -> Optional[str]: """Freebox api url.""" return self._get_db_base_url(self._fbx_uid) + @property + def fbx_api_mods(self) -> Optional[list]: + """Freebox available api modules list.""" + return self._api_mods_l() + @property def fbx_uid(self) -> Optional[str]: """Freebox uid.""" From d27b25df7e0054e48a90efcc53d3def17d1e33da Mon Sep 17 00:00:00 2001 From: foreign-sub Date: Fri, 29 Nov 2019 20:29:45 +0100 Subject: [PATCH 57/63] Update aiofreepybox.py: small polish --- aiofreepybox/aiofreepybox.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index bdac51dc..576e73c4 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -124,10 +124,11 @@ def __getattr__(self, k): else: if self._access: kwargs: Dict[str, str] = {} - api_mod_class = getattr(mod[k], k.capitalize()) if k in _API_MODS_PARAMS and isinstance(_API_MODS_PARAMS[k], dict): kwargs = _API_MODS_PARAMS[k] - setattr(self, k, api_mod_class(self._access, **kwargs)) + setattr( + self, k, getattr(mod[k], k.capitalize())(self._access, **kwargs) + ) else: return getattr(mod[k], k.capitalize()) @@ -315,10 +316,11 @@ async def open( # Get API access try: - if uid is None: - uid = await self._fbx_open_disc(host, port) - else: - uid = await self._fbx_open_db(uid) + uid = ( + await self._fbx_open_disc(host, port) + if uid is None + else await self._fbx_open_db(uid) + ) except NotOpenError: raise From fc445b03fb1fba02b93b2e7a9b74d2433742455e Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Tue, 21 Jan 2020 23:07:08 +0100 Subject: [PATCH 58/63] Update aiofreepybox.py: organize imports, add some error messages --- aiofreepybox/aiofreepybox.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 576e73c4..27e24c9e 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -1,22 +1,23 @@ -import aiohttp import asyncio import base64 import bz2 +from importlib import import_module +from importlib import resources import ipaddress import json import logging +from os import fspath +from pathlib import Path import pkgutil import socket import ssl -from importlib import import_module -from importlib import resources -from os import fspath -from pathlib import Path from typing import Any, Dict, List, Optional, Tuple, Union from urllib.parse import urljoin -# aiofpbx imports +import aiohttp + import aiofreepybox +from aiofreepybox.access import Access from aiofreepybox.exceptions import ( AuthorizationError, HttpRequestError, @@ -24,7 +25,6 @@ InvalidTokenError, NotOpenError, ) -from aiofreepybox.access import Access # API modules extra parameters _API_MODS_PARAMS: Dict[str, Any] = {} # {"player": {"api_version": "v6"}} @@ -322,6 +322,7 @@ async def open( else await self._fbx_open_db(uid) ) except NotOpenError: + _LOGGER.error(f"Cannot open freebox with uid: {uid}") raise try: @@ -329,6 +330,7 @@ async def open( uid, Path(_DATA_DIR), self.app_desc, self.timeout ) except AuthorizationError: + _LOGGER.error("Authorization error") raise self._fbx_uid = uid @@ -492,6 +494,7 @@ def _fbx_open_session(self, host: str, port: str, s: str) -> aiohttp.ClientSessi try: self._fbx_ping_port(host, port) except ValueError: + _LOGGER.error("Cannot open freebox port") raise # Connect session From 91ba569c4eff4ef8cb9ace5a2108550e47bcd0bc Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Thu, 6 Feb 2020 16:29:00 +0100 Subject: [PATCH 59/63] Update aiofreepybox.py: fix possible unbound session --- aiofreepybox/aiofreepybox.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 27e24c9e..899832ba 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -656,13 +656,16 @@ async def _get_app_access( _LOGGER.info(f"Application token file was generated: {tk_file}.") # Create and return freebox http access module - fbx_access = Access( - self._session, - self._get_db_base_url(uid), - app_token, - app_desc["app_id"], - timeout, - ) + if self._session is None: + raise AuthorizationError(f"{_DEFAULT_ERR}Session error") + else: + fbx_access = Access( + self._session, + self._get_db_base_url(uid), + app_token, + app_desc["app_id"], + timeout, + ) return fbx_access async def _get_app_token( From b651ea829347d016df3535ed0cf0d783284b884c Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Thu, 6 Feb 2020 22:27:14 +0100 Subject: [PATCH 60/63] Update aiofreepybox.py: Allow users to store settings files on an custom path. --- aiofreepybox/aiofreepybox.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 899832ba..85de01f9 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -37,7 +37,7 @@ "device_name": socket.gethostname(), } -_DATA_DIR = Path(__file__).parent +_SELF_DIR = Path(__file__).parent # App defaults _DEFAULT_API_VERSION = "v6" @@ -53,10 +53,10 @@ _LOGGER = logging.getLogger(__name__) # Db file prefix -_F_DB_NAME = ".db" +_F_DB_NAME = ".fbx_db" # Token file prefix -_F_TOKEN_NAME = ".app_auth" +_F_TOKEN_NAME = ".fbx_app_auth" class Freepybox: @@ -69,6 +69,8 @@ class Freepybox: , Default to _APP_DESC api_version : `str`, "server" or "v(1-7)" , optional , Default to _DEFAULT_API_VERSION + data_dir : `str`, optional + , Default to _SELF_DIR timeout : `int` , optional , Default to _DEFAULT_TIMEOUT """ @@ -77,12 +79,14 @@ def __init__( self, app_desc: Optional[Dict[str, str]] = None, api_version: Optional[str] = None, + data_dir: Optional[str] = None, timeout: Optional[int] = None, ) -> None: self.api_version: str = ( api_version if api_version is not None else _DEFAULT_API_VERSION ) self.app_desc: Dict[str, str] = app_desc if app_desc is not None else _APP_DESC + self.data_dir: Path = Path(data_dir) if data_dir is not None else _SELF_DIR self.timeout: int = timeout if timeout is not None else _DEFAULT_TIMEOUT self._access: Optional[Access] = None self._fbx_db: Dict[str, Any] = {} @@ -147,16 +151,16 @@ def clean_db(self, uids: Optional[list] = None, all_: bool = False) -> bool: if uids is None: uids = [ base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") - for x in Path(_DATA_DIR).glob(_F_DB_NAME + "_*") + for x in Path(self.data_dir).glob(_F_DB_NAME + "_*") ] auth = [ base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") - for x in Path(_DATA_DIR).glob(_F_TOKEN_NAME + "_*") + for x in Path(self.data_dir).glob(_F_TOKEN_NAME + "_*") ] c = 0 for uid in uids: if uid not in auth or all_: - fname = Path(_DATA_DIR).joinpath( + fname = Path(self.data_dir).joinpath( _F_DB_NAME + "_" + base64.b64encode(uid.encode("utf-8")).decode("utf-8") @@ -259,13 +263,13 @@ def get_db( if uids is None: uids = [ base64.b64decode(x.name.rsplit("_", 1)[1]).decode("utf-8") - for x in Path(_DATA_DIR).glob(_F_DB_NAME + "_*") + for x in Path(self.data_dir).glob(_F_DB_NAME + "_*") ] for uid in uids: try: fbx_db.append({uid: self._fbx_db[uid]}) except KeyError: - d = self._readfile_fbx_db(Path(_DATA_DIR), uid) + d = self._readfile_fbx_db(Path(self.data_dir), uid) if d is not None: fbx_db.append({uid: d}) @@ -327,7 +331,7 @@ async def open( try: self._access = await self._get_app_access( - uid, Path(_DATA_DIR), self.app_desc, self.timeout + uid, Path(self.data_dir), self.app_desc, self.timeout ) except AuthorizationError: _LOGGER.error("Authorization error") @@ -340,7 +344,7 @@ def _api_mods_l(self) -> List[str]: return list( map( lambda t: t[1], - pkgutil.iter_modules([str(Path(_DATA_DIR).joinpath("api"))]), + pkgutil.iter_modules([str(Path(_SELF_DIR).joinpath("api"))]), ) ) @@ -439,7 +443,7 @@ def _fbx_enum_conns(self, db: Dict[str, Any]) -> None: async def _fbx_open_db(self, uid: str) -> str: """Open freebox db""" - db = self._readfile_fbx_db(Path(_DATA_DIR), uid) + db = self._readfile_fbx_db(Path(self.data_dir), uid) try: if db is None: fbx_desc = await self.discover() @@ -500,7 +504,7 @@ def _fbx_open_session(self, host: str, port: str, s: str) -> aiohttp.ClientSessi # Connect session try: if s == "s": - cert_path = str(Path(_DATA_DIR).joinpath(_DEFAULT_CERT)) + cert_path = str(Path(_SELF_DIR).joinpath(_DEFAULT_CERT)) ssl_ctx = ssl.create_default_context() ssl_ctx.load_verify_locations(cafile=cert_path) conn = aiohttp.TCPConnector(ssl_context=ssl_ctx) @@ -576,7 +580,7 @@ def _fbx_update_db( fbx_entry = {uid: {"conn": fbx_api_add, "conf": fbx_conf, "desc": fbx_desc}} self._fbx_db.update(fbx_entry) fbx_conn = self._fbx_db[uid]["conn"] - if not self._readfile_fbx_db(Path(_DATA_DIR), uid): + if not self._readfile_fbx_db(Path(self.data_dir), uid): write_db = True if fbx_addict is not None and fbx_addict not in fbx_conn: @@ -586,7 +590,7 @@ def _fbx_update_db( if write_db: asyncio.create_task( - self._writefile_fbx_db(Path(_DATA_DIR), uid, self._fbx_db[uid]) + self._writefile_fbx_db(Path(self.data_dir), uid, self._fbx_db[uid]) ) return uid From b8fe7202bd26a4e5733107efc754c86cccf5ef16 Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Thu, 6 Feb 2020 22:34:14 +0100 Subject: [PATCH 61/63] Update .gitignore --- aiofreepybox/.gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aiofreepybox/.gitignore b/aiofreepybox/.gitignore index f15b4be9..a647c157 100644 --- a/aiofreepybox/.gitignore +++ b/aiofreepybox/.gitignore @@ -1,5 +1,4 @@ /__pycache__/ *.pyc app_auth -.app_auth* -.db_* +.fbx_* From 11b8815041b5c1db03548d4ff0a8dfc9b6f5f07b Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Fri, 7 Feb 2020 20:52:06 +0100 Subject: [PATCH 62/63] Update aiofreepybox.py: fix get_permissions return type --- aiofreepybox/aiofreepybox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 85de01f9..920a3e6b 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -278,7 +278,7 @@ def get_db( _LOGGER.debug(f"{len(fbx_db)} uid(s) in db") return fbx_db - async def get_permissions(self) -> Optional[dict]: + async def get_permissions(self) -> Optional[Dict[str, bool]]: """ Returns the permissions for this app. From 7f309b76cbf10e16695f05240e16bedb52399298 Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Sat, 8 Feb 2020 11:48:30 +0100 Subject: [PATCH 63/63] Update aiofreepybox.py: fix, use "or" instead of "if" for str --- aiofreepybox/aiofreepybox.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/aiofreepybox/aiofreepybox.py b/aiofreepybox/aiofreepybox.py index 920a3e6b..b71fe4b9 100644 --- a/aiofreepybox/aiofreepybox.py +++ b/aiofreepybox/aiofreepybox.py @@ -82,9 +82,7 @@ def __init__( data_dir: Optional[str] = None, timeout: Optional[int] = None, ) -> None: - self.api_version: str = ( - api_version if api_version is not None else _DEFAULT_API_VERSION - ) + self.api_version: str = api_version or _DEFAULT_API_VERSION self.app_desc: Dict[str, str] = app_desc if app_desc is not None else _APP_DESC self.data_dir: Path = Path(data_dir) if data_dir is not None else _SELF_DIR self.timeout: int = timeout if timeout is not None else _DEFAULT_TIMEOUT