PlexKodiConnect/resources/lib/plexnet/http.py
2018-09-30 13:16:17 +02:00

321 lines
10 KiB
Python

import sys
import os
import re
import traceback
import requests
import socket
import threadutils
import urllib
import mimetypes
import plexobjects
from defusedxml import ElementTree
import asyncadapter
import callback
import util
codes = requests.codes
status_codes = requests.status_codes._codes
DEFAULT_TIMEOUT = asyncadapter.AsyncTimeout(10).setConnectTimeout(10)
def GET(*args, **kwargs):
return requests.get(*args, headers=util.BASE_HEADERS.copy(), timeout=util.TIMEOUT, **kwargs)
def POST(*args, **kwargs):
return requests.post(*args, headers=util.BASE_HEADERS.copy(), timeout=util.TIMEOUT, **kwargs)
def Session():
s = asyncadapter.Session()
s.headers = util.BASE_HEADERS.copy()
s.timeout = util.TIMEOUT
return s
class RequestContext(dict):
def __getattr__(self, attr):
return self.get(attr)
def __setattr__(self, attr, value):
self[attr] = value
class HttpRequest(object):
_cancel = False
def __init__(self, url, method=None, forceCertificate=False):
self.server = None
self.path = None
self.hasParams = '?' in url
self.ignoreResponse = False
self.session = asyncadapter.Session()
self.session.headers = util.BASE_HEADERS.copy()
self.currentResponse = None
self.method = method
self.url = url
self.thread = None
# Use our specific plex.direct CA cert if applicable to improve performance
# if forceCertificate or url[:5] == "https": # TODO: ---------------------------------------------------------------------------------IMPLEMENT
# certsPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'certs')
# if "plex.direct" in url:
# self.session.cert = os.path.join(certsPath, 'plex-bundle.crt')
# else:
# self.session.cert = os.path.join(certsPath, 'ca-bundle.crt')
def removeAsPending(self):
import plexapp
plexapp.APP.delRequest(self)
def startAsync(self, *args, **kwargs):
self.thread = threadutils.KillableThread(target=self._startAsync, args=args, kwargs=kwargs, name='HTTP-ASYNC:{0}'.format(self.url))
self.thread.start()
return True
def _startAsync(self, body=None, contentType=None, context=None):
timeout = context and context.timeout or DEFAULT_TIMEOUT
self.logRequest(body, timeout)
if self._cancel:
return
try:
if self.method == 'PUT':
res = self.session.put(self.url, timeout=timeout, stream=True)
elif self.method == 'DELETE':
res = self.session.delete(self.url, timeout=timeout, stream=True)
elif self.method == 'HEAD':
res = self.session.head(self.url, timeout=timeout, stream=True)
elif self.method == 'POST' or body is not None:
if not contentType:
self.session.headers["Content-Type"] = "application/x-www-form-urlencoded"
else:
self.session.headers["Content-Type"] = mimetypes.guess_type(contentType)
res = self.session.post(self.url, data=body or None, timeout=timeout, stream=True)
else:
res = self.session.get(self.url, timeout=timeout, stream=True)
self.currentResponse = res
if self._cancel:
return
except asyncadapter.TimeoutException:
import plexapp
plexapp.APP.onRequestTimeout(context)
self.removeAsPending()
return
except Exception, e:
util.ERROR('Request failed {0}'.format(util.cleanToken(self.url)), e)
if not hasattr(e, 'response'):
return
res = e.response
self.onResponse(res, context)
self.removeAsPending()
def getWithTimeout(self, seconds=DEFAULT_TIMEOUT):
return HttpObjectResponse(self.getPostWithTimeout(seconds), self.path, self.server)
def postWithTimeout(self, seconds=DEFAULT_TIMEOUT, body=None):
self.method = 'POST'
return HttpObjectResponse(self.getPostWithTimeout(seconds, body), self.path, self.server)
def getToStringWithTimeout(self, seconds=DEFAULT_TIMEOUT):
res = self.getPostWithTimeout(seconds)
if not res:
return ''
return res.text.encode('utf8')
def postToStringWithTimeout(self, body=None, seconds=DEFAULT_TIMEOUT):
self.method = 'POST'
res = self.getPostWithTimeout(seconds, body)
if not res:
return ''
return res.text.encode('utf8')
def getPostWithTimeout(self, seconds=DEFAULT_TIMEOUT, body=None):
if self._cancel:
return
self.logRequest(body, seconds, False)
try:
if self.method == 'PUT':
res = self.session.put(self.url, timeout=seconds, stream=True)
elif self.method == 'DELETE':
res = self.session.delete(self.url, timeout=seconds, stream=True)
elif self.method == 'HEAD':
res = self.session.head(self.url, timeout=seconds, stream=True)
elif self.method == 'POST' or body is not None:
res = self.session.post(self.url, data=body, timeout=seconds, stream=True)
else:
res = self.session.get(self.url, timeout=seconds, stream=True)
self.currentResponse = res
if self._cancel:
return None
util.LOG("Got a {0} from {1}".format(res.status_code, util.cleanToken(self.url)))
# self.event = msg
return res
except Exception, e:
info = traceback.extract_tb(sys.exc_info()[2])[-1]
util.WARN_LOG(
"Request errored out - URL: {0} File: {1} Line: {2} Msg: {3}".format(util.cleanToken(self.url), os.path.basename(info[0]), info[1], e.message)
)
return None
def wasOK(self):
return self.currentResponse and self.currentResponse.ok
def wasNotFound(self):
return self.currentResponse is not None and self.currentResponse.status_code == requests.codes.not_found
def getIdentity(self):
return str(id(self))
def getUrl(self):
return self.url
def getRelativeUrl(self):
url = self.getUrl()
m = re.match('^\w+:\/\/.+?(\/.+)', url)
if m:
return m.group(1)
return url
def killSocket(self):
if not self.currentResponse:
return
try:
socket.fromfd(self.currentResponse.raw.fileno(), socket.AF_INET, socket.SOCK_STREAM).shutdown(socket.SHUT_RDWR)
return
except AttributeError:
pass
except Exception, e:
util.ERROR(err=e)
try:
self.currentResponse.raw._fp.fp._sock.shutdown(socket.SHUT_RDWR)
except AttributeError:
pass
except Exception, e:
util.ERROR(err=e)
def cancel(self):
self._cancel = True
self.session.cancel()
self.removeAsPending()
self.killSocket()
def addParam(self, encodedName, value):
if self.hasParams:
self.url += "&" + encodedName + "=" + urllib.quote_plus(value)
else:
self.hasParams = True
self.url += "?" + encodedName + "=" + urllib.quote_plus(value)
def addHeader(self, name, value):
self.session.headers[name] = value
def createRequestContext(self, requestType, callback_=None):
context = RequestContext()
context.requestType = requestType
context.timeout = DEFAULT_TIMEOUT
if callback_:
context.callback = callback.Callable(self.onResponse)
context.completionCallback = callback_
context.callbackCtx = callback_.context
return context
def onResponse(self, event, context):
if context.completionCallback:
response = HttpResponse(event)
context.completionCallback(self, response, context)
def logRequest(self, body, timeout=None, async=True):
# Log the real request method
method = self.method
if not method:
method = body is not None and "POST" or "GET"
util.LOG(
"Starting request: {0} {1} (async={2} timeout={3})".format(method, util.cleanToken(self.url), async, timeout)
)
class HttpResponse(object):
def __init__(self, event):
self.event = event
if not self.event is None:
self.event.content # force data to be read
self.event.close()
def isSuccess(self):
if not self.event:
return False
return self.event.status_code >= 200 and self.event.status_code < 300
def isError(self):
return not self.isSuccess()
def getStatus(self):
if self.event is None:
return 0
return self.event.status_code
def getBodyString(self):
if self.event is None:
return ''
return self.event.text.encode('utf-8')
def getErrorString(self):
if self.event is None:
return ''
return self.event.reason
def getBodyXml(self):
if not self.event is None:
return ElementTree.fromstring(self.getBodyString())
return None
def getResponseHeader(self, name):
if self.event is None:
return None
return self.event.headers.get(name)
class HttpObjectResponse(HttpResponse, plexobjects.PlexContainer):
def __init__(self, response, path, server=None):
self.event = response
if self.event:
self.event.content # force data to be read
self.event.close()
data = self.getBodyXml()
plexobjects.PlexContainer.__init__(self, data, initpath=path, server=server, address=path)
self.container = self
self.items = plexobjects.listItems(server, path, data=data, container=self)
def addRequestHeaders(transferObj, headers=None):
if isinstance(headers, dict):
for header in headers:
transferObj.addHeader(header, headers[header])
util.DEBUG_LOG("Adding header to {0}: {1}: {2}".format(transferObj, header, headers[header]))
def addUrlParam(url, param):
return url + ('?' in url and '&' or '?') + param