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?"
msgstr ""
msgctxt "#30702"
msgid "An error occurred while authenticating: {error}."
msgstr ""
msgctxt "#30709"
msgid "Geo-blocked video"
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?"
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"

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)