322 lines
10 KiB
Python
322 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
|