Files
tryton/trytond/wsgi.py
T
2021-02-12 10:01:22 +01:00

247 lines
8.8 KiB
Python

# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import base64
import http.client
import logging
import os
import posixpath
import sys
import traceback
import urllib.parse
try:
from http import HTTPStatus
except ImportError:
from http import client as HTTPStatus
from werkzeug.wrappers import Response
from werkzeug.routing import Map, Rule, BaseConverter
from werkzeug.exceptions import abort, HTTPException, InternalServerError
try:
from werkzeug.middleware.proxy_fix import ProxyFix
def NumProxyFix(app, num_proxies):
return ProxyFix(app,
x_for=num_proxies, x_proto=num_proxies, x_host=num_proxies,
x_port=num_proxies, x_prefix=num_proxies)
except ImportError:
from werkzeug.contrib.fixers import ProxyFix as NumProxyFix
try:
from werkzeug.security import safe_join
except ImportError:
safe_join = posixpath.join
try:
from werkzeug.middleware.shared_data import SharedDataMiddleware
except ImportError:
from werkzeug.wsgi import SharedDataMiddleware
import wrapt
from trytond.config import config
from trytond.protocols.wrappers import Request
from trytond.protocols.jsonrpc import JSONProtocol
from trytond.protocols.xmlrpc import XMLProtocol
from trytond.tools import resolve
__all__ = ['TrytondWSGI', 'app']
logger = logging.getLogger(__name__)
class Base64Converter(BaseConverter):
def to_python(self, value):
return base64.urlsafe_b64decode(value).decode('utf-8')
def to_url(self, value):
return base64.urlsafe_b64encode(value.encode('utf-8'))
class TrytondWSGI(object):
def __init__(self):
self.url_map = Map([], converters={
'base64': Base64Converter,
})
self.protocols = [JSONProtocol, XMLProtocol]
self.error_handlers = []
def route(self, string, methods=None, defaults=None):
def decorator(func):
self.url_map.add(Rule(
string, endpoint=func, methods=methods, defaults=defaults))
return func
return decorator
def error_handler(self, handler):
self.error_handlers.append(handler)
return handler
@wrapt.decorator
def auth_required(self, wrapped, instance, args, kwargs):
request = args[0]
if request.user_id:
return wrapped(*args, **kwargs)
else:
headers = {}
if request.headers.get('X-Requested-With') != 'XMLHttpRequest':
headers['WWW-Authenticate'] = 'Basic realm="Tryton"'
response = Response(None, http.client.UNAUTHORIZED, headers)
abort(http.client.UNAUTHORIZED, response=response)
def check_request_size(self, request, size=None):
if request.method not in {'POST', 'PUT', 'PATCH'}:
return
if size is None:
if request.user_id:
max_size = config.getint(
'request', 'max_size_authenticated')
else:
max_size = config.getint(
'request', 'max_size')
else:
max_size = size
if max_size:
content_length = request.content_length
if content_length is None:
abort(http.client.LENGTH_REQUIRED)
elif content_length > max_size:
abort(http.client.REQUEST_ENTITY_TOO_LARGE)
def dispatch_request(self, request):
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, request.view_args = adapter.match()
max_request_size = getattr(endpoint, 'max_request_size', None)
self.check_request_size(request, max_request_size)
return endpoint(request, **request.view_args)
except HTTPException as e:
logger.debug(
"Exception when processing %s", request, exc_info=True)
return e
except Exception as e:
logger.debug(
"Exception when processing %s", request, exc_info=True)
tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
for path in sys.path:
tb_s = tb_s.replace(path, '')
e.__format_traceback__ = tb_s
response = e
for error_handler in self.error_handlers:
rv = error_handler(self, request, e)
if isinstance(rv, Response):
response = rv
return response
def make_response(self, request, data):
for cls in self.protocols:
for mimetype, _ in request.accept_mimetypes:
if cls.content_type in mimetype:
response = cls.response(data, request)
break
else:
continue
break
else:
for cls in self.protocols:
if cls.content_type in request.environ.get('CONTENT_TYPE', ''):
response = cls.response(data, request)
break
else:
if isinstance(data, Exception):
try:
response = InternalServerError(original_exception=data)
except TypeError:
response = InternalServerError(data)
else:
response = Response(data)
return response
def wsgi_app(self, environ, start_response):
for cls in self.protocols:
if cls.content_type in environ.get('CONTENT_TYPE', ''):
request = cls.request(environ)
break
else:
request = Request(environ)
origin = request.headers.get('Origin')
origin_host = urllib.parse.urlparse(origin).netloc if origin else ''
host = request.headers.get('Host')
if origin and origin_host != host:
cors = filter(
None, config.get('web', 'cors', default='').splitlines())
if origin not in cors:
abort(HTTPStatus.FORBIDDEN)
data = self.dispatch_request(request)
if not isinstance(data, (Response, HTTPException)):
response = self.make_response(request, data)
else:
response = data
if origin and isinstance(response, Response):
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Vary'] = 'Origin'
method = request.headers.get('Access-Control-Request-Method')
if method:
response.headers['Access-Control-Allow-Methods'] = method
headers = request.headers.get('Access-Control-Request-Headers')
if headers:
response.headers['Access-Control-Allow-Headers'] = headers
response.headers['Access-Control-Max-Age'] = config.getint(
'web', 'cache_timeout')
return response(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
class SharedDataMiddlewareIndex(SharedDataMiddleware):
def __call__(self, environ, start_response):
if environ['REQUEST_METHOD'] not in {'GET', 'HEAD'}:
return self.app(environ, start_response)
return super(SharedDataMiddlewareIndex, self).__call__(
environ, start_response)
def get_directory_loader(self, directory):
def loader(path):
if path is not None:
path = safe_join(directory, path)
else:
path = directory
if path is not None:
if os.path.isdir(path):
path = posixpath.join(path, 'index.html')
if os.path.isfile(path):
return os.path.basename(path), self._opener(path)
return None, None
return loader
app = TrytondWSGI()
if config.get('web', 'root'):
static_files = {
'/': config.get('web', 'root'),
}
app.wsgi_app = SharedDataMiddlewareIndex(
app.wsgi_app, static_files,
cache_timeout=config.getint('web', 'cache_timeout'))
num_proxies = config.getint('web', 'num_proxies')
if num_proxies:
app.wsgi_app = NumProxyFix(app.wsgi_app, num_proxies)
if config.has_section('wsgi middleware'):
for middleware in config.options('wsgi middleware'):
Middleware = resolve(config.get('wsgi middleware', middleware))
args, kwargs = (), {}
section = 'wsgi %s' % middleware
if config.has_section(section):
if config.has_option(section, 'args'):
args = eval(config.get(section, 'args'))
if config.has_option(section, 'kwargs'):
kwargs = eval(config.get(section, 'kwargs'))
app.wsgi_app = Middleware(app.wsgi_app, *args, **kwargs)
import trytond.protocols.dispatcher # noqa: E402,F401
import trytond.bus # noqa: E402,F401