Handle authentication errors better

This commit is contained in:
Michaël Arnauts 2020-03-22 10:30:23 +01:00
parent 57cf842ca7
commit 5a19cf02a9
10 changed files with 76 additions and 58 deletions

View File

@ -96,6 +96,10 @@ msgctxt "#30701"
msgid "To watch a video, you need to enter your credentials. Do you want to enter them now?" msgid "To watch a video, you need to enter your credentials. Do you want to enter them now?"
msgstr "" msgstr ""
msgctxt "#30702"
msgid "An error occurred while authenticating: {error}."
msgstr ""
msgctxt "#30709" msgctxt "#30709"
msgid "Geo-blocked video" msgid "Geo-blocked video"
msgstr "" msgstr ""

View File

@ -97,6 +97,10 @@ msgctxt "#30701"
msgid "To watch a video, you need to enter your credentials. Do you want to enter them now?" 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?" 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" msgctxt "#30709"
msgid "Geo-blocked video" msgid "Geo-blocked video"
msgstr "Video is geografisch geblokkeerd" msgstr "Video is geografisch geblokkeerd"

View File

@ -9,7 +9,6 @@ from resources.lib import kodiutils
from resources.lib.kodiutils import TitleItem from resources.lib.kodiutils import TitleItem
from resources.lib.modules.menu import Menu from resources.lib.modules.menu import Menu
from resources.lib.viervijfzes import CHANNELS from resources.lib.viervijfzes import CHANNELS
from resources.lib.viervijfzes.auth import AuthApi
from resources.lib.viervijfzes.content import ContentApi, UnavailableException from resources.lib.viervijfzes.content import ContentApi, UnavailableException
_LOGGER = logging.getLogger('catalog') _LOGGER = logging.getLogger('catalog')
@ -20,8 +19,7 @@ class Catalog:
def __init__(self): def __init__(self):
""" Initialise object """ """ Initialise object """
self._auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) self._api = ContentApi()
self._api = ContentApi(self._auth.get_token())
self._menu = Menu() self._menu = Menu()
def show_catalog(self): def show_catalog(self):

View File

@ -7,6 +7,7 @@ import logging
from resources.lib import kodiutils from resources.lib import kodiutils
from resources.lib.viervijfzes.auth import AuthApi 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 from resources.lib.viervijfzes.content import ContentApi, UnavailableException, GeoblockedException
_LOGGER = logging.getLogger('player') _LOGGER = logging.getLogger('player')
@ -38,15 +39,22 @@ class Player:
try: try:
# Check if we have credentials # Check if we have credentials
if not kodiutils.get_setting('username') or not kodiutils.get_setting('password'): 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: if confirm:
kodiutils.open_settings() kodiutils.open_settings()
kodiutils.end_of_directory() kodiutils.end_of_directory()
return return
# Fetch an auth token now # Fetch an auth token now
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) try:
token = auth.get_token() 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 # Get stream information
resolved_stream = ContentApi(token).get_stream(channel, item) resolved_stream = ContentApi(token).get_stream(channel, item)

View File

@ -8,7 +8,7 @@ import logging
import os import os
import time 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') _LOGGER = logging.getLogger('auth-api')
@ -25,15 +25,15 @@ class AuthApi:
""" Initialise object """ """ Initialise object """
self._username = username self._username = username
self._password = password self._password = password
self._cache = cache_dir self._cache_dir = cache_dir
self._id_token = None self._id_token = None
self._expiry = 0 self._expiry = 0
self._refresh_token = None self._refresh_token = None
if self._cache: if self._cache_dir:
# Load tokens from cache # Load tokens from cache
try: 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()) data_json = json.loads(f.read())
self._id_token = data_json.get('id_token') self._id_token = data_json.get('id_token')
self._refresh_token = data_json.get('refresh_token') self._refresh_token = data_json.get('refresh_token')
@ -53,25 +53,33 @@ class AuthApi:
if self._refresh_token: if self._refresh_token:
# We have a valid refresh token, use that to refresh our id 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. # 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) _LOGGER.debug('Getting an id token by refreshing')
if self._id_token: try:
self._id_token = self._refresh(self._refresh_token)
self._expiry = now + 3600 self._expiry = now + 3600
_LOGGER.debug('Got an id token by refreshing: %s', self._id_token) _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: if not self._id_token:
# We have no tokens, or they are all invalid, do a login # 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) id_token, refresh_token = self._authenticate(self._username, self._password)
self._id_token = id_token self._id_token = id_token
self._refresh_token = refresh_token self._refresh_token = refresh_token
self._expiry = now + 3600 self._expiry = now + 3600
_LOGGER.debug('Got an id token by logging in: %s', self._id_token) _LOGGER.debug('Got an id token by logging in: %s', self._id_token)
if self._cache: if self._cache_dir:
if not os.path.isdir(self._cache): if not os.path.isdir(self._cache_dir):
os.mkdir(self._cache) os.mkdir(self._cache_dir)
# Store new tokens in cache # 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( data = json.dumps(dict(
id_token=self._id_token, id_token=self._id_token,
refresh_token=self._refresh_token, refresh_token=self._refresh_token,
@ -83,11 +91,11 @@ class AuthApi:
def clear_cache(self): def clear_cache(self):
""" Remove the cached tokens. """ """ Remove the cached tokens. """
if not self._cache: if not self._cache_dir:
return return
# Remove cache # Remove cache
os.remove(self._cache + self.TOKEN_FILE) os.remove(self._cache_dir + self.TOKEN_FILE)
# Clear tokens in memory # Clear tokens in memory
self._id_token = None self._id_token = None

View File

@ -20,6 +20,16 @@ import six
_LOGGER = logging.getLogger('auth-awsidp') _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: class AwsIdp:
""" AWS Identity Provider """ """ AWS Identity Provider """
@ -86,9 +96,7 @@ class AwsIdp:
challenge_name = auth_response_json.get("ChallengeName") challenge_name = auth_response_json.get("ChallengeName")
if not challenge_name == "PASSWORD_VERIFIER": if not challenge_name == "PASSWORD_VERIFIER":
message = auth_response_json.get("message") raise AuthenticationException(auth_response_json.get("message"))
_LOGGER.error("Cannot start authentication challenge: %s", message or None)
return None
# Step 2: Respond to the Challenge with a valid ChallengeResponse # Step 2: Respond to the Challenge with a valid ChallengeResponse
challenge_request = self.__get_challenge_response_request(challenge_parameters, password) challenge_request = self.__get_challenge_response_request(challenge_parameters, password)
@ -102,8 +110,7 @@ class AwsIdp:
_LOGGER.debug("Got response: %s", auth_response_json) _LOGGER.debug("Got response: %s", auth_response_json)
if "message" in auth_response_json: if "message" in auth_response_json:
_LOGGER.error("Error logging in: %s", auth_response_json.get("message")) raise InvalidLoginException(auth_response_json.get("message"))
return None, None
id_token = auth_response_json.get("AuthenticationResult", {}).get("IdToken") id_token = auth_response_json.get("AuthenticationResult", {}).get("IdToken")
refresh_token = auth_response_json.get("AuthenticationResult", {}).get("RefreshToken") refresh_token = auth_response_json.get("AuthenticationResult", {}).get("RefreshToken")
@ -134,8 +141,7 @@ class AwsIdp:
refresh_json = json.loads(refresh_response.content) refresh_json = json.loads(refresh_response.content)
if "message" in refresh_json: if "message" in refresh_json:
_LOGGER.error("Error refreshing: %s", refresh_json.get("message")) raise AuthenticationException(refresh_json.get("message"))
return None
id_token = refresh_json.get("AuthenticationResult", {}).get("IdToken") id_token = refresh_json.get("AuthenticationResult", {}).get("IdToken")
return id_token return id_token

View File

@ -8,8 +8,8 @@ import logging
import re import re
from datetime import datetime from datetime import datetime
import requests
from six.moves.html_parser import HTMLParser from six.moves.html_parser import HTMLParser
import requests
from resources.lib.viervijfzes import CHANNELS from resources.lib.viervijfzes import CHANNELS

View File

@ -18,19 +18,16 @@ _LOGGER = logging.getLogger('test-api')
class TestApi(unittest.TestCase): class TestApi(unittest.TestCase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TestApi, self).__init__(*args, **kwargs) super(TestApi, self).__init__(*args, **kwargs)
self._api = ContentApi()
def test_programs(self): def test_programs(self):
api = ContentApi()
for channel in ['vier', 'vijf', 'zes']: for channel in ['vier', 'vijf', 'zes']:
channels = api.get_programs(channel) channels = self._api.get_programs(channel)
self.assertIsInstance(channels, list) self.assertIsInstance(channels, list)
def test_episodes(self): def test_episodes(self):
api = ContentApi()
for channel, program in [('vier', 'auwch'), ('vijf', 'zo-man-zo-vrouw')]: 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, Program)
self.assertIsInstance(program.seasons, dict) self.assertIsInstance(program.seasons, dict)
# self.assertIsInstance(program.seasons[0], Season) # self.assertIsInstance(program.seasons[0], Season)
@ -39,13 +36,12 @@ class TestApi(unittest.TestCase):
_LOGGER.info('Got program: %s', program) _LOGGER.info('Got program: %s', program)
def test_get_stream(self): def test_get_stream(self):
auth = AuthApi(kodiutils.get_setting('username'), kodiutils.get_setting('password'), kodiutils.get_tokens_path()) program = self._api.get_program('vier', 'auwch')
token = auth.get_token()
api = ContentApi(token)
program = api.get_program('vier', 'auwch')
episode = program.episodes[0] 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) self.assertTrue(video)
_LOGGER.info('Got video URL: %s', video) _LOGGER.info('Got video URL: %s', video)

View File

@ -20,47 +20,43 @@ _LOGGER = logging.getLogger('test-epg')
class TestEpg(unittest.TestCase): class TestEpg(unittest.TestCase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TestEpg, self).__init__(*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): def test_vier_today(self):
epg = EpgApi() programs = self._epg.get_epg('vier', date.today().strftime('%Y-%m-%d'))
programs = epg.get_epg('vier', date.today().strftime('%Y-%m-%d'))
self.assertIsInstance(programs, list) self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], EpgProgram) self.assertIsInstance(programs[0], EpgProgram)
def test_vijf_today(self): def test_vijf_today(self):
epg = EpgApi() programs = self._epg.get_epg('vijf', date.today().strftime('%Y-%m-%d'))
programs = epg.get_epg('vijf', date.today().strftime('%Y-%m-%d'))
self.assertIsInstance(programs, list) self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], EpgProgram) self.assertIsInstance(programs[0], EpgProgram)
def test_zes_today(self): def test_zes_today(self):
epg = EpgApi() programs = self._epg.get_epg('zes', date.today().strftime('%Y-%m-%d'))
programs = epg.get_epg('zes', date.today().strftime('%Y-%m-%d'))
self.assertIsInstance(programs, list) self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], EpgProgram) self.assertIsInstance(programs[0], EpgProgram)
def test_unknown_today(self): def test_unknown_today(self):
epg = EpgApi()
with self.assertRaises(Exception): 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): def test_vier_out_of_range(self):
epg = EpgApi() programs = self._epg.get_epg('vier', '2020-01-01')
programs = epg.get_epg('vier', '2020-01-01')
self.assertEqual(programs, []) self.assertEqual(programs, [])
def test_play_video_from_epg(self): def test_play_video_from_epg(self):
epg = EpgApi() epg_programs = self._epg.get_epg('vier', date.today().strftime('%Y-%m-%d'))
epg_programs = epg.get_epg('vier', date.today().strftime('%Y-%m-%d'))
epg_program = [program for program in epg_programs if program.video_url][0] epg_program = [program for program in epg_programs if program.video_url][0]
# Lookup the Episode data since we don't have an UUID # 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) episode = api.get_episode(epg_program.channel, epg_program.video_url)
self.assertIsInstance(episode, Episode) self.assertIsInstance(episode, Episode)
# Get stream based on the Episode's UUID # 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) video = api.get_stream(episode.channel, episode.uuid)
self.assertTrue(video) self.assertTrue(video)

View File

@ -17,21 +17,19 @@ _LOGGER = logging.getLogger('test-search')
class TestSearch(unittest.TestCase): class TestSearch(unittest.TestCase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(TestSearch, self).__init__(*args, **kwargs) super(TestSearch, self).__init__(*args, **kwargs)
self._search = SearchApi()
def test_search(self): def test_search(self):
search = SearchApi() programs = self._search.search('de mol')
programs = search.search('de mol')
self.assertIsInstance(programs, list) self.assertIsInstance(programs, list)
self.assertIsInstance(programs[0], Program) self.assertIsInstance(programs[0], Program)
def test_search_empty(self): def test_search_empty(self):
search = SearchApi() programs = self._search.search('')
programs = search.search('')
self.assertIsInstance(programs, list) self.assertIsInstance(programs, list)
def test_search_space(self): def test_search_space(self):
search = SearchApi() programs = self._search.search(' ')
programs = search.search(' ')
self.assertIsInstance(programs, list) self.assertIsInstance(programs, list)