PlexKodiConnect/resources/lib/downloadutils.py

297 lines
11 KiB
Python
Raw Normal View History

#!/usr/bin/env python
2015-12-25 07:07:00 +11:00
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
2017-12-10 00:35:08 +11:00
from logging import getLogger
import requests
2015-12-25 07:07:00 +11:00
2018-11-19 00:59:17 +11:00
from . import utils, clientinfo, app
2016-02-20 06:03:06 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
# Disable annoying requests warnings
import requests.packages.urllib3
requests.packages.urllib3.disable_warnings()
2015-12-25 07:07:00 +11:00
2018-11-19 00:59:17 +11:00
LOG = getLogger('PLEX.download')
2016-08-30 03:57:58 +10:00
2016-02-20 06:03:06 +11:00
###############################################################################
2015-12-25 07:07:00 +11:00
class DownloadUtils():
"""
Manages any up/downloads with PKC. Careful to initiate correctly
Use startSession() to initiate.
If not initiated, e.g. SSL check will fallback to False
"""
2016-02-20 06:03:06 +11:00
2015-12-25 07:07:00 +11:00
# Borg - multiple instances, shared state
2016-02-07 22:38:50 +11:00
_shared_state = {}
2015-12-25 07:07:00 +11:00
# How many failed attempts before declaring PMS dead?
connection_attempts = 1
count_error = 0
# How many 401 returns before declaring unauthorized?
unauthorized_attempts = 2
count_unauthorized = 0
2017-05-01 01:22:46 +10:00
# How long should we wait for an answer from the
timeout = 30.0
2015-12-25 07:07:00 +11:00
def __init__(self):
2016-02-07 22:38:50 +11:00
self.__dict__ = self._shared_state
2015-12-25 07:07:00 +11:00
def setSSL(self, verifySSL=None, certificate=None):
"""
verifySSL must be 'true' to enable certificate validation
certificate must be path to certificate or 'None'
"""
if verifySSL is None:
2018-11-19 00:59:17 +11:00
verifySSL = app.CONN.verify_ssl_cert
if certificate is None:
2018-11-19 00:59:17 +11:00
certificate = app.CONN.ssl_cert_path
2018-01-28 23:23:47 +11:00
# Set the session's parameters
self.s.verify = verifySSL
if certificate:
self.s.cert = certificate
2018-02-03 23:44:16 +11:00
LOG.debug("Verify SSL certificates set to: %s", verifySSL)
LOG.debug("SSL client side certificate set to: %s", certificate)
2015-12-25 07:07:00 +11:00
2016-04-13 22:34:58 +10:00
def startSession(self, reset=False):
"""
2018-11-19 00:59:17 +11:00
User should be authenticated when this method is called
"""
2015-12-25 07:07:00 +11:00
# Start session
self.s = requests.Session()
2018-06-22 03:24:37 +10:00
self.deviceId = clientinfo.getDeviceId()
# Attach authenticated header to the session
2018-06-22 03:24:37 +10:00
self.s.headers = clientinfo.getXArgsDeviceInfo()
self.s.encoding = 'utf-8'
# Set SSL settings
self.setSSL()
# Counters to declare PMS dead or unauthorized
2016-04-13 22:34:58 +10:00
if reset is True:
self.count_error = 0
self.count_unauthorized = 0
2015-12-25 07:07:00 +11:00
# Retry connections to the server
self.s.mount("http://", requests.adapters.HTTPAdapter(max_retries=1))
self.s.mount("https://", requests.adapters.HTTPAdapter(max_retries=1))
2018-11-19 00:59:17 +11:00
LOG.info("Requests session started on: %s", app.CONN.server)
2015-12-25 07:07:00 +11:00
def stopSession(self):
try:
self.s.close()
except:
2018-02-03 23:44:16 +11:00
LOG.info("Requests session already closed")
try:
del self.s
except:
pass
2018-02-03 23:44:16 +11:00
LOG.info('Request session stopped')
def getHeader(self, options=None):
2018-06-22 03:24:37 +10:00
header = clientinfo.getXArgsDeviceInfo()
if options is not None:
header.update(options)
2015-12-25 07:07:00 +11:00
return header
2016-08-30 03:57:58 +10:00
def _doDownload(self, s, action_type, **kwargs):
2016-04-26 22:02:19 +10:00
if action_type == "GET":
r = s.get(**kwargs)
2016-04-26 22:02:19 +10:00
elif action_type == "POST":
r = s.post(**kwargs)
2016-04-26 22:02:19 +10:00
elif action_type == "DELETE":
r = s.delete(**kwargs)
2016-04-26 22:02:19 +10:00
elif action_type == "OPTIONS":
r = s.options(**kwargs)
2016-04-26 22:02:19 +10:00
elif action_type == "PUT":
r = s.put(**kwargs)
return r
2016-04-26 22:02:19 +10:00
def downloadUrl(self, url, action_type="GET", postBody=None,
parameters=None, authenticate=True, headerOptions=None,
verifySSL=True, timeout=None, return_response=False,
headerOverride=None):
"""
Override SSL check with verifySSL=False
If authenticate=True, existing request session will be used/started
Otherwise, 'empty' request will be made
Returns:
2016-05-19 04:10:20 +10:00
None If an error occured
True If connection worked but no body was received
401, ... integer if PMS answered with HTTP error 401
(unauthorized) or other http error codes
xml xml etree root object, if applicable
json json() object, if applicable
<response-object> if return_response=True is set (200, 201 only)
"""
2016-07-13 05:30:39 +10:00
kwargs = {'timeout': self.timeout}
if authenticate is True:
# Get requests session
try:
s = self.s
except AttributeError:
2018-02-03 23:44:16 +11:00
LOG.info("Request session does not exist: start one")
self.startSession()
s = self.s
# Replace for the real values
2018-11-19 00:59:17 +11:00
url = url.replace("{server}", app.CONN.server)
else:
# User is not (yet) authenticated. Used to communicate with
# plex.tv and to check for PMS servers
s = requests
if not headerOverride:
headerOptions = self.getHeader(options=headerOptions)
else:
headerOptions = headerOverride
2018-11-19 00:59:17 +11:00
kwargs['verify'] = app.CONN.verify_ssl_cert
if app.CONN.ssl_cert_path:
kwargs['cert'] = app.CONN.ssl_cert_path
# Set the variables we were passed (fallback to request session
# otherwise - faster)
kwargs['url'] = url
if verifySSL is False:
kwargs['verify'] = False
if headerOptions is not None:
kwargs['headers'] = headerOptions
if postBody is not None:
kwargs['data'] = postBody
if parameters is not None:
kwargs['params'] = parameters
if timeout is not None:
kwargs['timeout'] = timeout
# ACTUAL DOWNLOAD HAPPENING HERE
2015-12-25 07:07:00 +11:00
try:
2016-08-30 03:57:58 +10:00
r = self._doDownload(s, action_type, **kwargs)
2015-12-25 07:07:00 +11:00
# THE EXCEPTIONS
except requests.exceptions.SSLError as e:
2018-02-03 23:44:16 +11:00
LOG.warn("Invalid SSL certificate for: %s", url)
LOG.warn(e)
2015-12-25 07:07:00 +11:00
except requests.exceptions.ConnectionError as e:
# Connection error
2018-02-03 23:44:16 +11:00
LOG.warn("Server unreachable at: %s", url)
LOG.warn(e)
2015-12-25 07:07:00 +11:00
except requests.exceptions.Timeout as e:
2018-02-03 23:44:16 +11:00
LOG.warn("Server timeout at: %s", url)
LOG.warn(e)
2015-12-25 07:07:00 +11:00
except requests.exceptions.HTTPError as e:
2018-02-03 23:44:16 +11:00
LOG.warn('HTTP Error at %s', url)
LOG.warn(e)
2015-12-25 07:07:00 +11:00
except requests.exceptions.TooManyRedirects as e:
2018-02-03 23:44:16 +11:00
LOG.warn("Too many redirects connecting to: %s", url)
LOG.warn(e)
2015-12-25 07:07:00 +11:00
except requests.exceptions.RequestException as e:
2018-02-03 23:44:16 +11:00
LOG.warn("Unknown error connecting to: %s", url)
LOG.warn(e)
2015-12-25 07:07:00 +11:00
2016-04-10 00:57:45 +10:00
except SystemExit:
2018-02-03 23:44:16 +11:00
LOG.info('SystemExit detected, aborting download')
2016-04-10 00:57:45 +10:00
self.stopSession()
except:
2018-02-03 23:44:16 +11:00
LOG.warn('Unknown error while downloading. Traceback:')
import traceback
2018-02-03 23:44:16 +11:00
LOG.warn(traceback.format_exc())
# THE RESPONSE #####
else:
# We COULD contact the PMS, hence it ain't dead
if authenticate is True:
self.count_error = 0
if r.status_code != 401:
self.count_unauthorized = 0
if r.status_code == 204:
# No body in the response
# But read (empty) content to release connection back to pool
# (see requests: keep-alive documentation)
r.content
return True
elif r.status_code == 401:
if authenticate is False:
# Called when checking a connect - no need for rash action
return 401
r.encoding = 'utf-8'
2018-02-03 23:44:16 +11:00
LOG.warn('HTTP error 401 from PMS %s', url)
LOG.info(r.text)
if '401 Unauthorized' in r.text:
# Truly unauthorized
self.count_unauthorized += 1
if self.count_unauthorized >= self.unauthorized_attempts:
2018-02-03 23:44:16 +11:00
LOG.warn('We seem to be truly unauthorized for PMS'
' %s ', url)
# Unauthorized access, user no longer has access
app.ACCOUNT.log_out()
utils.dialog('notification',
utils.lang(29999),
utils.lang(30017),
icon='{error}')
else:
# there might be other 401 where e.g. PMS under strain
2018-02-03 23:44:16 +11:00
LOG.info('PMS might only be under strain')
return 401
elif r.status_code in (200, 201):
# 200: OK
# 201: Created
if return_response is True:
# return the entire response object
return r
try:
# xml response
r = utils.defused_etree.fromstring(r.content)
return r
except:
r.encoding = 'utf-8'
if r.text == '':
# Answer does not contain a body
return True
try:
# UNICODE - JSON object
r = r.json()
return r
except:
if '200 OK' in r.text:
# Received fucked up OK from PMS on playstate
# update
pass
else:
2018-02-03 23:44:16 +11:00
LOG.warn("Unable to convert the response for: "
"%s", url)
LOG.warn("Received headers were: %s", r.headers)
LOG.warn('Received text: %s', r.text)
return True
elif r.status_code == 403:
# E.g. deleting a PMS item
2018-02-03 23:44:16 +11:00
LOG.warn('PMS sent 403: Forbidden error for url %s', url)
return
else:
r.encoding = 'utf-8'
2018-02-03 23:44:16 +11:00
LOG.warn('Unknown answer from PMS %s with status code %s. ',
url, r.status_code)
return True
# And now deal with the consequences of the exceptions
if authenticate is True:
# Make the addon aware of status
self.count_error += 1
if self.count_error >= self.connection_attempts:
LOG.warn('Failed to connect to %s too many times. '
'Declare PMS dead', url)
app.CONN.online = False
return