mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-05 04:35:46 -07:00
parent
09c28e434d
commit
ad2ec0e2bf
3 changed files with 260 additions and 1 deletions
255
lib/cherrypy_cors/__init__.py
Normal file
255
lib/cherrypy_cors/__init__.py
Normal file
|
@ -0,0 +1,255 @@
|
|||
import re
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import set_vary_header
|
||||
import httpagentparser
|
||||
|
||||
|
||||
CORS_ALLOW_METHODS = 'Access-Control-Allow-Methods'
|
||||
CORS_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'
|
||||
CORS_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials'
|
||||
CORS_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
|
||||
CORS_REQUEST_METHOD = 'Access-Control-Request-Method'
|
||||
CORS_REQUEST_HEADERS = 'Access-Control-Request-Headers'
|
||||
CORS_MAX_AGE = 'Access-Control-Max-Age'
|
||||
CORS_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
|
||||
PUBLIC_ORIGIN = '*'
|
||||
|
||||
|
||||
def expose(allow_credentials=False, expose_headers=None, origins=None):
|
||||
"""Adds CORS support to the resource.
|
||||
|
||||
If the resource is allowed to be exposed, the value of the
|
||||
`Access-Control-Allow-Origin`_ header in the response will echo
|
||||
the `Origin`_ request header, and `Origin` will be
|
||||
appended to the `Vary`_ response header.
|
||||
|
||||
:param allow_credentials: Use credentials to make cookies work
|
||||
(see `Access-Control-Allow-Credentials`_).
|
||||
:type allow_credentials: bool
|
||||
:param expose_headers: List of headers clients will be able to access
|
||||
(see `Access-Control-Expose-Headers`_).
|
||||
:type expose_headers: list or None
|
||||
:param origins: List of allowed origins clients must reference.
|
||||
:type origins: list or None
|
||||
|
||||
:returns: Whether the resource is being exposed.
|
||||
:rtype: bool
|
||||
|
||||
- Configuration example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
config = {
|
||||
'/static': {
|
||||
'tools.staticdir.on': True,
|
||||
'cors.expose.on': True,
|
||||
}
|
||||
}
|
||||
- Decorator example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@cherrypy_cors.tools.expose()
|
||||
def DELETE(self):
|
||||
self._delete()
|
||||
|
||||
"""
|
||||
if _get_cors().expose(allow_credentials, expose_headers, origins):
|
||||
_safe_caching_headers()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def expose_public(expose_headers=None):
|
||||
"""Adds CORS support to the resource from any origin.
|
||||
|
||||
If the resource is allowed to be exposed, the value of the
|
||||
`Access-Control-Allow-Origin`_ header in the response will be `*`.
|
||||
|
||||
:param expose_headers: List of headers clients will be able to access
|
||||
(see `Access-Control-Expose-Headers`_).
|
||||
:type expose_headers: list or None
|
||||
|
||||
:rtype: None
|
||||
"""
|
||||
_get_cors().expose_public(expose_headers)
|
||||
|
||||
|
||||
def preflight(
|
||||
allowed_methods,
|
||||
allowed_headers=None,
|
||||
allow_credentials=False,
|
||||
max_age=None,
|
||||
origins=None,
|
||||
):
|
||||
"""Adds CORS `preflight`_ support to a `HTTP OPTIONS` request.
|
||||
|
||||
:param allowed_methods: List of supported `HTTP` methods
|
||||
(see `Access-Control-Allow-Methods`_).
|
||||
:type allowed_methods: list or None
|
||||
:param allowed_headers: List of supported `HTTP` headers
|
||||
(see `Access-Control-Allow-Headers`_).
|
||||
:type allowed_headers: list or None
|
||||
:param allow_credentials: Use credentials to make cookies work
|
||||
(see `Access-Control-Allow-Credentials`_).
|
||||
:type allow_credentials: bool
|
||||
:param max_age: Seconds to cache the preflight request
|
||||
(see `Access-Control-Max-Age`_).
|
||||
:type max_age: int
|
||||
:param origins: List of allowed origins clients must reference.
|
||||
:type origins: list or None
|
||||
|
||||
:returns: Whether the preflight is allowed.
|
||||
:rtype: bool
|
||||
|
||||
- Used as a decorator with the `Method Dispatcher`_
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@cherrypy_cors.tools.preflight(
|
||||
allowed_methods=["GET", "DELETE", "PUT"])
|
||||
def OPTIONS(self):
|
||||
pass
|
||||
|
||||
- Function call with the `Object Dispatcher`_
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.allow(
|
||||
methods=["GET", "DELETE", "PUT", "OPTIONS"])
|
||||
def thing(self):
|
||||
if cherrypy.request.method == "OPTIONS":
|
||||
cherrypy_cors.preflight(
|
||||
allowed_methods=["GET", "DELETE", "PUT"])
|
||||
else:
|
||||
self._do_other_things()
|
||||
|
||||
"""
|
||||
if _get_cors().preflight(
|
||||
allowed_methods, allowed_headers, allow_credentials, max_age, origins
|
||||
):
|
||||
_safe_caching_headers()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def install():
|
||||
"""Install the toolbox such that it's available in all applications."""
|
||||
cherrypy._cptree.Application.toolboxes.update(cors=tools)
|
||||
|
||||
|
||||
class CORS:
|
||||
"""A generic CORS handler."""
|
||||
|
||||
def __init__(self, req_headers, resp_headers):
|
||||
self.req_headers = req_headers
|
||||
self.resp_headers = resp_headers
|
||||
|
||||
def expose(self, allow_credentials, expose_headers, origins):
|
||||
if self._is_valid_origin(origins):
|
||||
self._add_origin_and_credentials_headers(allow_credentials)
|
||||
self._add_expose_headers(expose_headers)
|
||||
return True
|
||||
return False
|
||||
|
||||
def expose_public(self, expose_headers):
|
||||
self._add_public_origin()
|
||||
self._add_expose_headers(expose_headers)
|
||||
|
||||
def preflight(
|
||||
self, allowed_methods, allowed_headers, allow_credentials, max_age, origins
|
||||
):
|
||||
if self._is_valid_preflight_request(allowed_headers, allowed_methods, origins):
|
||||
self._add_origin_and_credentials_headers(allow_credentials)
|
||||
self._add_prefligt_headers(allowed_methods, max_age)
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def origin(self):
|
||||
return self.req_headers.get('Origin')
|
||||
|
||||
def _is_valid_origin(self, origins):
|
||||
if origins is None:
|
||||
origins = [self.origin]
|
||||
origins = map(self._make_regex, origins)
|
||||
return self.origin is not None and any(
|
||||
origin.match(self.origin) for origin in origins
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _make_regex(pattern):
|
||||
if isinstance(pattern, str):
|
||||
pattern = re.compile(re.escape(pattern) + '$')
|
||||
return pattern
|
||||
|
||||
def _add_origin_and_credentials_headers(self, allow_credentials):
|
||||
self.resp_headers[CORS_ALLOW_ORIGIN] = self.origin
|
||||
if allow_credentials:
|
||||
self.resp_headers[CORS_ALLOW_CREDENTIALS] = 'true'
|
||||
|
||||
def _add_public_origin(self):
|
||||
self.resp_headers[CORS_ALLOW_ORIGIN] = PUBLIC_ORIGIN
|
||||
|
||||
def _add_expose_headers(self, expose_headers):
|
||||
if expose_headers:
|
||||
self.resp_headers[CORS_EXPOSE_HEADERS] = expose_headers
|
||||
|
||||
@property
|
||||
def requested_method(self):
|
||||
return self.req_headers.get(CORS_REQUEST_METHOD)
|
||||
|
||||
@property
|
||||
def requested_headers(self):
|
||||
return self.req_headers.get(CORS_REQUEST_HEADERS)
|
||||
|
||||
def _has_valid_method(self, allowed_methods):
|
||||
return self.requested_method and self.requested_method in allowed_methods
|
||||
|
||||
def _valid_headers(self, allowed_headers):
|
||||
if self.requested_headers and allowed_headers:
|
||||
for header in self.requested_headers.split(','):
|
||||
if header.strip() not in allowed_headers:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _is_valid_preflight_request(self, allowed_headers, allowed_methods, origins):
|
||||
return (
|
||||
self._is_valid_origin(origins)
|
||||
and self._has_valid_method(allowed_methods)
|
||||
and self._valid_headers(allowed_headers)
|
||||
)
|
||||
|
||||
def _add_prefligt_headers(self, allowed_methods, max_age):
|
||||
rh = self.resp_headers
|
||||
rh[CORS_ALLOW_METHODS] = ', '.join(allowed_methods)
|
||||
if max_age:
|
||||
rh[CORS_MAX_AGE] = max_age
|
||||
if self.requested_headers:
|
||||
rh[CORS_ALLOW_HEADERS] = self.requested_headers
|
||||
|
||||
|
||||
def _get_cors():
|
||||
return CORS(cherrypy.serving.request.headers, cherrypy.serving.response.headers)
|
||||
|
||||
|
||||
def _safe_caching_headers():
|
||||
"""Adds `Origin`_ to the `Vary`_ header to ensure caching works properly.
|
||||
|
||||
Except in IE because it will disable caching completely. The caching
|
||||
strategy in that case is out of the scope of this library.
|
||||
https://blogs.msdn.microsoft.com/ieinternals/2009/06/17/vary-with-care/
|
||||
"""
|
||||
uah = cherrypy.serving.request.headers.get('User-Agent', '')
|
||||
ua = httpagentparser.detect(uah)
|
||||
IE = 'Microsoft Internet Explorer'
|
||||
if ua.get('browser', {}).get('name') != IE:
|
||||
set_vary_header(cherrypy.serving.response, "Origin")
|
||||
|
||||
|
||||
tools = cherrypy._cptools.Toolbox("cors")
|
||||
tools.expose = cherrypy.Tool('before_handler', expose)
|
||||
tools.expose_public = cherrypy.Tool('before_handler', expose_public)
|
||||
tools.preflight = cherrypy.Tool('before_handler', preflight)
|
|
@ -21,6 +21,7 @@ import sys
|
|||
|
||||
import cheroot.errors
|
||||
import cherrypy
|
||||
import cherrypy_cors
|
||||
|
||||
import plexpy
|
||||
from plexpy import logger
|
||||
|
@ -62,6 +63,7 @@ def restart():
|
|||
|
||||
|
||||
def initialize(options):
|
||||
cherrypy_cors.install()
|
||||
|
||||
# HTTPS stuff stolen from sickbeard
|
||||
enable_https = options['enable_https']
|
||||
|
@ -91,7 +93,8 @@ def initialize(options):
|
|||
'server.socket_timeout': 60,
|
||||
'tools.encode.on': True,
|
||||
'tools.encode.encoding': 'utf-8',
|
||||
'tools.decode.on': True
|
||||
'tools.decode.on': True,
|
||||
'cors.expose.on': True,
|
||||
}
|
||||
|
||||
if plexpy.DEV:
|
||||
|
|
|
@ -5,6 +5,7 @@ bleach==6.2.0
|
|||
certifi==2024.8.30
|
||||
cheroot==10.0.1
|
||||
cherrypy==18.10.0
|
||||
cherrypy-cors==1.7.0
|
||||
cloudinary==1.41.0
|
||||
distro==1.9.0
|
||||
dnspython==2.7.0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue