Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make authentication more robust #192

Merged
merged 4 commits into from
Jul 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
Thumbs.db
*~
.cache

.coverage
.tox/

# Testing
tests/home/userdata/addon_data
.env
204 changes: 91 additions & 113 deletions resources/lib/vtmgo/vtmgo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import requests

from resources.lib.vtmgo.vtmgoauth import VtmGoAuth
from resources.lib.vtmgo.vtmgoauth import VtmGoAuth, InvalidLoginException

_LOGGER = logging.getLogger('vtmgo')

Expand Down Expand Up @@ -117,8 +117,8 @@ def __repr__(self):
class Movie:
""" Defines a Movie """

def __init__(self, movie_id=None, name=None, description=None, year=None, cover=None, image=None, duration=None, remaining=None, geoblocked=None,
channel=None, legal=None, aired=None, my_list=None):
def __init__(self, movie_id=None, name=None, description=None, year=None, cover=None, image=None, duration=None,
remaining=None, geoblocked=None, channel=None, legal=None, aired=None, my_list=None):
"""
:type movie_id: str
:type name: str
Expand Down Expand Up @@ -155,8 +155,8 @@ def __repr__(self):
class Program:
""" Defines a Program """

def __init__(self, program_id=None, name=None, description=None, cover=None, image=None, seasons=None, geoblocked=None, channel=None, legal=None,
my_list=None):
def __init__(self, program_id=None, name=None, description=None, cover=None, image=None, seasons=None,
geoblocked=None, channel=None, legal=None, my_list=None):
"""
:type program_id: str
:type name: str
Expand Down Expand Up @@ -210,8 +210,9 @@ def __repr__(self):
class Episode:
""" Defines an Episode """

def __init__(self, episode_id=None, program_id=None, program_name=None, number=None, season=None, name=None, description=None, cover=None, duration=None,
remaining=None, geoblocked=None, channel=None, legal=None, aired=None, progress=None, watched=False, next_episode=None):
def __init__(self, episode_id=None, program_id=None, program_name=None, number=None, season=None, name=None,
description=None, cover=None, duration=None, remaining=None, geoblocked=None, channel=None, legal=None,
aired=None, progress=None, watched=False, next_episode=None):
"""
:type episode_id: str
:type program_id: str
Expand Down Expand Up @@ -259,25 +260,39 @@ def __repr__(self):

class VtmGo:
""" VTM GO API """
API_ENDPOINT = 'https://api.vtmgo.be'

CONTENT_TYPE_MOVIE = 'MOVIE'
CONTENT_TYPE_PROGRAM = 'PROGRAM'
CONTENT_TYPE_EPISODE = 'EPISODE'

_HEADERS = {
'x-app-version': '8',
'x-persgroep-mobile-app': 'true',
'x-persgroep-os': 'android',
'x-persgroep-os-version': '23',
}

def __init__(self, kodi):
""" Initialise object
:type kodi: resources.lib.kodiwrapper.KodiWrapper
"""
self._kodi = kodi
self._proxies = kodi.get_proxies()
self._auth = VtmGoAuth(kodi)

self._session = requests.session()
self._session.proxies = kodi.get_proxies()
self._authenticate()

def _authenticate(self):
""" Apply authentication headers in the session """
self._session.headers = {
'x-app-version': '8',
'x-persgroep-mobile-app': 'true',
'x-persgroep-os': 'android',
'x-persgroep-os-version': '23',
}
token = self._auth.get_token()
if token:
self._session.headers['x-dpp-jwt'] = token

profile = self._auth.get_profile()
if profile:
self._session.headers['x-dpp-profile'] = profile

def _mode(self):
""" Return the mode that should be used for API calls """
return 'vtmgo-kids' if self.get_product() == 'VTM_GO_KIDS' else 'vtmgo'
Expand Down Expand Up @@ -611,7 +626,8 @@ def get_program(self, program_id, cache=CACHE_AUTO):
seasons[item_season.get('index')] = Season(
number=item_season.get('index'),
episodes=episodes,
cover=item_season.get('episodes', [{}])[0].get('bigPhotoUrl') if episodes else program.get('bigPhotoUrl'),
cover=item_season.get('episodes', [{}])[0].get('bigPhotoUrl')
if episodes else program.get('bigPhotoUrl'),
geoblocked=program.get('geoBlocked'),
channel=channel,
legal=program.get('legalIcons'),
Expand Down Expand Up @@ -732,7 +748,7 @@ def get_product(self):
profile = self._kodi.get_setting('profile')
try:
return profile.split(':')[1]
except IndexError:
except (IndexError, AttributeError):
return None

@staticmethod
Expand All @@ -751,123 +767,85 @@ def _parse_channel(url):
def _get_url(self, url, params=None):
""" Makes a GET request for the specified URL.
:type url: str
:type params: dict
:rtype str
"""
headers = self._HEADERS
token = self._auth.get_token()
if token:
headers['x-dpp-jwt'] = token

profile = self._auth.get_profile()
if profile:
headers['x-dpp-profile'] = profile

_LOGGER.debug('Sending GET %s...', url)

response = requests.session().get('https://lfvp-api.dpgmedia.net' + url, params=params, headers=headers, proxies=self._proxies)

# Set encoding to UTF-8 if no charset is indicated in http headers (https:/psf/requests/issues/1604)
if not response.encoding:
response.encoding = 'utf-8'

_LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text)

if response.status_code == 404:
raise UnavailableException()

if response.status_code == 426:
raise ApiUpdateRequired()

if response.status_code not in [200, 204]:
raise Exception('Error %s.' % response.status_code)

return response.text

def _put_url(self, url):
try:
return self._request('GET', url, params=params)
except InvalidLoginException:
self._auth.clear_token()
self._authenticate()
# Retry the same request
return self._request('GET', url, params=params)

def _put_url(self, url, params=None):
""" Makes a PUT request for the specified URL.
:type url: str
:type params: dict
:rtype str
"""
headers = self._HEADERS
token = self._auth.get_token()
if token:
headers['x-dpp-jwt'] = token

profile = self._auth.get_profile()
if profile:
headers['x-dpp-profile'] = profile

_LOGGER.debug('Sending PUT %s...', url)

response = requests.session().put('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies)

_LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text)

if response.status_code == 404:
raise UnavailableException()

if response.status_code == 426:
raise ApiUpdateRequired()

if response.status_code not in [200, 204]:
raise Exception('Error %s.' % response.status_code)

return response.text

def _post_url(self, url):
try:
return self._request('PUT', url, params=params)
except InvalidLoginException:
self._auth.clear_token()
self._authenticate()
# Retry the same request
return self._request('PUT', url, params=params)

def _post_url(self, url, params=None, data=None):
""" Makes a POST request for the specified URL.
:type url: str
:type params: dict
:type data: dict
:rtype str
"""
headers = self._HEADERS
token = self._auth.get_token()
if token:
headers['x-dpp-jwt'] = token

profile = self._auth.get_profile()
if profile:
headers['x-dpp-profile'] = profile

_LOGGER.debug('Sending POST %s...', url)

response = requests.session().post('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies)

_LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text)

if response.status_code == 404:
raise UnavailableException()

if response.status_code == 426:
raise ApiUpdateRequired()

if response.status_code not in [200, 204]:
raise Exception('Error %s.' % response.status_code)

return response.text

def _delete_url(self, url):
try:
return self._request('POST', url, params=params, data=data)
except InvalidLoginException:
self._auth.clear_token()
self._authenticate()
# Retry the same request
return self._request('POST', url, params=params)

def _delete_url(self, url, params=None):
""" Makes a DELETE request for the specified URL.
:type url: str
:type params: dict
:rtype str
"""
headers = self._HEADERS
token = self._auth.get_token()
if token:
headers['x-dpp-jwt'] = token

profile = self._auth.get_profile()
if profile:
headers['x-dpp-profile'] = profile

_LOGGER.debug('Sending DELETE %s...', url)
try:
return self._request('DELETE', url, params=params)
except InvalidLoginException:
self._auth.clear_token()
self._authenticate()
# Retry the same request
return self._request('DELETE', url, params=params)

def _request(self, method, url, params=None, data=None):
""" Makes a request for the specified URL.
:type url: str
:type params: dict
:type data: dict
:rtype str
"""
_LOGGER.debug('Sending %s %s...', method, url)
response = self._session.request(method,
self.API_ENDPOINT + url,
params=params,
json=data)

response = requests.session().delete('https://api.vtmgo.be' + url, headers=headers, proxies=self._proxies)
# Set encoding to UTF-8 if no charset is indicated in http headers (https:/psf/requests/issues/1604)
if not response.encoding:
response.encoding = 'utf-8'

_LOGGER.debug('Got response (status=%s): %s', response.status_code, response.text)

if response.status_code == 404:
raise UnavailableException()

if response.status_code == 401:
raise InvalidLoginException()

if response.status_code == 426:
raise ApiUpdateRequired()

Expand Down
13 changes: 9 additions & 4 deletions resources/lib/vtmgo/vtmgoauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,17 @@ def has_credentials_changed(self):
def clear_token(self):
""" Remove the cached JWT. """
_LOGGER.debug('Clearing token cache')
self._token = None
path = os.path.join(self._kodi.get_userdata_path(), 'token.json')
if self._kodi.check_if_path_exists(path):
self._kodi.delete_file(path)
self._kodi.set_setting('profile', None)
self._token = None

def get_token(self):
""" Return a JWT that can be used to authenticate the user.
:rtype str
"""
userdata_path = self._kodi.get_userdata_path()

# Don't return a token when we have no password or username.
if not self._kodi.get_setting('username') or not self._kodi.get_setting('password'):
_LOGGER.debug('Skipping since we have no username or password')
Expand All @@ -74,7 +75,7 @@ def get_token(self):
return self._token

# Try to load from cache
path = os.path.join(self._kodi.get_userdata_path(), 'token.json')
path = os.path.join(userdata_path, 'token.json')
if self._kodi.check_if_path_exists(path):
_LOGGER.debug('Returning token from cache')

Expand All @@ -88,6 +89,10 @@ def get_token(self):
self._token = self._login()
_LOGGER.debug('Returning token from VTM GO')

# Make sure the path exists
if not self._kodi.check_if_path_exists(userdata_path):
self._kodi.mkdirs(userdata_path)

with self._kodi.open_file(path, 'w') as fdesc:
fdesc.write(from_unicode(self._token))

Expand All @@ -98,7 +103,7 @@ def get_profile(self):
profile = self._kodi.get_setting('profile')
try:
return profile.split(':')[0]
except IndexError:
except (IndexError, AttributeError):
return None

def _login(self):
Expand Down
2 changes: 1 addition & 1 deletion resources/lib/vtmgo/vtmgostream.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ def _delay_subtitles(self, subtitles, json_manifest):

import re
if not self._kodi.check_if_path_exists(temp_dir):
self._kodi.mkdir(temp_dir)
self._kodi.mkdirs(temp_dir)

ad_breaks = list()
delayed_subtitles = list()
Expand Down