From 5a19cf02a90d06160816cfb863384b54a0160349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Arnauts?= Date: Sun, 22 Mar 2020 10:30:23 +0100 Subject: [PATCH] Handle authentication errors better --- .../resource.language.en_gb/strings.po | 4 +++ .../resource.language.nl_nl/strings.po | 4 +++ resources/lib/modules/catalog.py | 4 +-- resources/lib/modules/player.py | 14 ++++++-- resources/lib/viervijfzes/auth.py | 32 ++++++++++++------- resources/lib/viervijfzes/auth_awsidp.py | 20 ++++++++---- resources/lib/viervijfzes/content.py | 2 +- test/test_api.py | 20 +++++------- test/test_epg.py | 24 ++++++-------- test/test_search.py | 10 +++--- 10 files changed, 76 insertions(+), 58 deletions(-) diff --git a/resources/language/resource.language.en_gb/strings.po b/resources/language/resource.language.en_gb/strings.po index ec23f97..803dfa1 100644 --- a/resources/language/resource.language.en_gb/strings.po +++ b/resources/language/resource.language.en_gb/strings.po @@ -96,6 +96,10 @@ msgctxt "#30701" msgid "To watch a video, you need to enter your credentials. Do you want to enter them now?" msgstr "" +msgctxt "#30702" +msgid "An error occurred while authenticating: {error}." +msgstr "" + msgctxt "#30709" msgid "Geo-blocked video" msgstr "" diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po index 71e919e..ebd4766 100644 --- a/resources/language/resource.language.nl_nl/strings.po +++ b/resources/language/resource.language.nl_nl/strings.po @@ -97,6 +97,10 @@ msgctxt "#30701" msgid "To watch a video, you need to enter your credentials. Do you want to enter them now?" msgstr "Om een video te bekijken moet je je inloggegevens ingeven. Wil je dit nu doen?" +msgctxt "#30702" +msgid "An error occurred while authenticating: {error}." +msgstr "Er is een fout opgetreden tijdens het aanmelden: {error}." + msgctxt "#30709" msgid "Geo-blocked video" msgstr "Video is geografisch geblokkeerd" diff --git a/resources/lib/modules/catalog.py b/resources/lib/modules/catalog.py index 97be895..1ec1835 100644 --- a/resources/lib/modules/catalog.py +++ b/resources/lib/modules/catalog.py @@ -9,7 +9,6 @@ from resources.lib import kodiutils from resources.lib.kodiutils import TitleItem from resources.lib.modules.menu import Menu from resources.lib.viervijfzes import CHANNELS -from resources.lib.viervijfzes.auth import AuthApi from resources.lib.viervijfzes.content import ContentApi, UnavailableException _LOGGER = logging.getLogger('catalog') @@ -20,8 +19,7 @@ class Catalog: def __init__(self): """ Initialise object """ - self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) - self._api = ContentApi(self._auth.get_token()) + self._api = ContentApi() self._menu = Menu() def show_catalog(self): diff --git a/resources/lib/modules/player.py b/resources/lib/modules/player.py index 0eb86a3..b36a463 100644 --- a/resources/lib/modules/player.py +++ b/resources/lib/modules/player.py @@ -7,6 +7,7 @@ import logging from resources.lib import kodiutils from resources.lib.viervijfzes.auth import AuthApi +from resources.lib.viervijfzes.auth_awsidp import InvalidLoginException, AuthenticationException from resources.lib.viervijfzes.content import ContentApi, UnavailableException, GeoblockedException _LOGGER = logging.getLogger('player') @@ -38,15 +39,22 @@ class Player: try: # Check if we have credentials if not kodiutils.get_setting('username') or not kodiutils.get_setting('password'): - confirm = kodiutils.yesno_dialog(message=kodiutils.localize(30701)) # To watch a video, you need to enter your credentials. Do you want to enter them now? + confirm = kodiutils.yesno_dialog( + message=kodiutils.localize(30701)) # To watch a video, you need to enter your credentials. Do you want to enter them now? if confirm: kodiutils.open_settings() kodiutils.end_of_directory() return # Fetch an auth token now - auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) - token = auth.get_token() + try: + auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) + token = auth.get_token() + except (InvalidLoginException, AuthenticationException) as ex: + _LOGGER.error(ex) + kodiutils.ok_dialog(message=kodiutils.localize(30702, error=ex.message)) + kodiutils.end_of_directory() + return # Get stream information resolved_stream = ContentApi(token).get_stream(channel, item) diff --git a/resources/lib/viervijfzes/auth.py b/resources/lib/viervijfzes/auth.py index bf2e714..bffa426 100644 --- a/resources/lib/viervijfzes/auth.py +++ b/resources/lib/viervijfzes/auth.py @@ -8,7 +8,7 @@ import logging import os import time -from resources.lib.viervijfzes.auth_awsidp import AwsIdp +from resources.lib.viervijfzes.auth_awsidp import AwsIdp, InvalidLoginException, AuthenticationException _LOGGER = logging.getLogger('auth-api') @@ -25,15 +25,15 @@ class AuthApi: """ Initialise object """ self._username = username self._password = password - self._cache = cache_dir + self._cache_dir = cache_dir self._id_token = None self._expiry = 0 self._refresh_token = None - if self._cache: + if self._cache_dir: # Load tokens from cache try: - with open(self._cache + self.TOKEN_FILE, 'rb') as f: + with open(self._cache_dir + self.TOKEN_FILE, 'rb') as f: data_json = json.loads(f.read()) self._id_token = data_json.get('id_token') self._refresh_token = data_json.get('refresh_token') @@ -53,25 +53,33 @@ class AuthApi: if self._refresh_token: # We have a valid refresh token, use that to refresh our id token # The refresh token is valid for 30 days. If this refresh fails, we just continue by logging in again. - self._id_token = self._refresh(self._refresh_token) - if self._id_token: + _LOGGER.debug('Getting an id token by refreshing') + try: + self._id_token = self._refresh(self._refresh_token) self._expiry = now + 3600 _LOGGER.debug('Got an id token by refreshing: %s', self._id_token) + except (InvalidLoginException, AuthenticationException) as e: + _LOGGER.error('Error logging in: %s', e.message) + self._id_token = None + self._refresh_token = None + self._expiry = 0 + # We continue by logging in with username and password if not self._id_token: # We have no tokens, or they are all invalid, do a login + _LOGGER.debug('Getting an id token by logging in') id_token, refresh_token = self._authenticate(self._username, self._password) self._id_token = id_token self._refresh_token = refresh_token self._expiry = now + 3600 _LOGGER.debug('Got an id token by logging in: %s', self._id_token) - if self._cache: - if not os.path.isdir(self._cache): - os.mkdir(self._cache) + if self._cache_dir: + if not os.path.isdir(self._cache_dir): + os.mkdir(self._cache_dir) # Store new tokens in cache - with open(self._cache + self.TOKEN_FILE, 'wb') as f: + with open(self._cache_dir + self.TOKEN_FILE, 'wb') as f: data = json.dumps(dict( id_token=self._id_token, refresh_token=self._refresh_token, @@ -83,11 +91,11 @@ class AuthApi: def clear_cache(self): """ Remove the cached tokens. """ - if not self._cache: + if not self._cache_dir: return # Remove cache - os.remove(self._cache + self.TOKEN_FILE) + os.remove(self._cache_dir + self.TOKEN_FILE) # Clear tokens in memory self._id_token = None diff --git a/resources/lib/viervijfzes/auth_awsidp.py b/resources/lib/viervijfzes/auth_awsidp.py index 6e587bd..2208658 100644 --- a/resources/lib/viervijfzes/auth_awsidp.py +++ b/resources/lib/viervijfzes/auth_awsidp.py @@ -20,6 +20,16 @@ import six _LOGGER = logging.getLogger('auth-awsidp') +class InvalidLoginException(Exception): + """ The login credentials are invalid """ + pass + + +class AuthenticationException(Exception): + """ Something went wrong while logging in """ + pass + + class AwsIdp: """ AWS Identity Provider """ @@ -86,9 +96,7 @@ class AwsIdp: challenge_name = auth_response_json.get("ChallengeName") if not challenge_name == "PASSWORD_VERIFIER": - message = auth_response_json.get("message") - _LOGGER.error("Cannot start authentication challenge: %s", message or None) - return None + raise AuthenticationException(auth_response_json.get("message")) # Step 2: Respond to the Challenge with a valid ChallengeResponse challenge_request = self.__get_challenge_response_request(challenge_parameters, password) @@ -102,8 +110,7 @@ class AwsIdp: _LOGGER.debug("Got response: %s", auth_response_json) if "message" in auth_response_json: - _LOGGER.error("Error logging in: %s", auth_response_json.get("message")) - return None, None + raise InvalidLoginException(auth_response_json.get("message")) id_token = auth_response_json.get("AuthenticationResult", {}).get("IdToken") refresh_token = auth_response_json.get("AuthenticationResult", {}).get("RefreshToken") @@ -134,8 +141,7 @@ class AwsIdp: refresh_json = json.loads(refresh_response.content) if "message" in refresh_json: - _LOGGER.error("Error refreshing: %s", refresh_json.get("message")) - return None + raise AuthenticationException(refresh_json.get("message")) id_token = refresh_json.get("AuthenticationResult", {}).get("IdToken") return id_token diff --git a/resources/lib/viervijfzes/content.py b/resources/lib/viervijfzes/content.py index c501f40..6a155aa 100644 --- a/resources/lib/viervijfzes/content.py +++ b/resources/lib/viervijfzes/content.py @@ -8,8 +8,8 @@ import logging import re from datetime import datetime -import requests from six.moves.html_parser import HTMLParser +import requests from resources.lib.viervijfzes import CHANNELS diff --git a/test/test_api.py b/test/test_api.py index b87a864..40f8e9c 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -18,19 +18,16 @@ _LOGGER = logging.getLogger('test-api') class TestApi(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestApi, self).__init__(*args, **kwargs) + self._api = ContentApi() def test_programs(self): - api = ContentApi() - for channel in ['vier', 'vijf', 'zes']: - channels = api.get_programs(channel) + channels = self._api.get_programs(channel) self.assertIsInstance(channels, list) def test_episodes(self): - api = ContentApi() - for channel, program in [('vier', 'auwch'), ('vijf', 'zo-man-zo-vrouw')]: - program = api.get_program(channel, program) + program = self._api.get_program(channel, program) self.assertIsInstance(program, Program) self.assertIsInstance(program.seasons, dict) # self.assertIsInstance(program.seasons[0], Season) @@ -39,13 +36,12 @@ class TestApi(unittest.TestCase): _LOGGER.info('Got program: %s', program) def test_get_stream(self): - auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) - token = auth.get_token() - - api = ContentApi(token) - program = api.get_program('vier', 'auwch') + program = self._api.get_program('vier', 'auwch') episode = program.episodes[0] - video = api.get_stream(episode.channel, episode.uuid) + + auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) + api_authed = ContentApi(auth.get_token()) + video = api_authed.get_stream(episode.channel, episode.uuid) self.assertTrue(video) _LOGGER.info('Got video URL: %s', video) diff --git a/test/test_epg.py b/test/test_epg.py index 1a17f3e..ce1df2f 100644 --- a/test/test_epg.py +++ b/test/test_epg.py @@ -20,47 +20,43 @@ _LOGGER = logging.getLogger('test-epg') class TestEpg(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestEpg, self).__init__(*args, **kwargs) - self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) + self._epg = EpgApi() def test_vier_today(self): - epg = EpgApi() - programs = epg.get_epg('vier', date.today().strftime('%Y-%m-%d')) + programs = self._epg.get_epg('vier', date.today().strftime('%Y-%m-%d')) self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], EpgProgram) def test_vijf_today(self): - epg = EpgApi() - programs = epg.get_epg('vijf', date.today().strftime('%Y-%m-%d')) + programs = self._epg.get_epg('vijf', date.today().strftime('%Y-%m-%d')) self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], EpgProgram) def test_zes_today(self): - epg = EpgApi() - programs = epg.get_epg('zes', date.today().strftime('%Y-%m-%d')) + programs = self._epg.get_epg('zes', date.today().strftime('%Y-%m-%d')) self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], EpgProgram) def test_unknown_today(self): - epg = EpgApi() with self.assertRaises(Exception): - epg.get_epg('vtm', date.today().strftime('%Y-%m-%d')) + self._epg.get_epg('vtm', date.today().strftime('%Y-%m-%d')) def test_vier_out_of_range(self): - epg = EpgApi() - programs = epg.get_epg('vier', '2020-01-01') + programs = self._epg.get_epg('vier', '2020-01-01') self.assertEqual(programs, []) def test_play_video_from_epg(self): - epg = EpgApi() - epg_programs = epg.get_epg('vier', date.today().strftime('%Y-%m-%d')) + epg_programs = self._epg.get_epg('vier', date.today().strftime('%Y-%m-%d')) epg_program = [program for program in epg_programs if program.video_url][0] # Lookup the Episode data since we don't have an UUID - api = ContentApi(self._auth.get_token()) + api = ContentApi() episode = api.get_episode(epg_program.channel, epg_program.video_url) self.assertIsInstance(episode, Episode) # Get stream based on the Episode's UUID + auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) + api = ContentApi(auth.get_token()) video = api.get_stream(episode.channel, episode.uuid) self.assertTrue(video) diff --git a/test/test_search.py b/test/test_search.py index dab23f3..ef49632 100644 --- a/test/test_search.py +++ b/test/test_search.py @@ -17,21 +17,19 @@ _LOGGER = logging.getLogger('test-search') class TestSearch(unittest.TestCase): def __init__(self, *args, **kwargs): super(TestSearch, self).__init__(*args, **kwargs) + self._search = SearchApi() def test_search(self): - search = SearchApi() - programs = search.search('de mol') + programs = self._search.search('de mol') self.assertIsInstance(programs, list) self.assertIsInstance(programs[0], Program) def test_search_empty(self): - search = SearchApi() - programs = search.search('') + programs = self._search.search('') self.assertIsInstance(programs, list) def test_search_space(self): - search = SearchApi() - programs = search.search(' ') + programs = self._search.search(' ') self.assertIsInstance(programs, list)