Initial import from Docker volume
This commit is contained in:
2
protocols/__init__.py
Executable file
2
protocols/__init__.py
Executable file
@@ -0,0 +1,2 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
BIN
protocols/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
BIN
protocols/__pycache__/__init__.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/__init__.cpython-311.pyc
Executable file
BIN
protocols/__pycache__/__init__.cpython-311.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/dispatcher.cpython-311.opt-1.pyc
Executable file
BIN
protocols/__pycache__/dispatcher.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/dispatcher.cpython-311.pyc
Executable file
BIN
protocols/__pycache__/dispatcher.cpython-311.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/jsonrpc.cpython-311.opt-1.pyc
Executable file
BIN
protocols/__pycache__/jsonrpc.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/jsonrpc.cpython-311.pyc
Executable file
BIN
protocols/__pycache__/jsonrpc.cpython-311.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/wrappers.cpython-311.opt-1.pyc
Executable file
BIN
protocols/__pycache__/wrappers.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/wrappers.cpython-311.pyc
Executable file
BIN
protocols/__pycache__/wrappers.cpython-311.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/xmlrpc.cpython-311.opt-1.pyc
Executable file
BIN
protocols/__pycache__/xmlrpc.cpython-311.opt-1.pyc
Executable file
Binary file not shown.
BIN
protocols/__pycache__/xmlrpc.cpython-311.pyc
Executable file
BIN
protocols/__pycache__/xmlrpc.cpython-311.pyc
Executable file
Binary file not shown.
268
protocols/dispatcher.py
Executable file
268
protocols/dispatcher.py
Executable file
@@ -0,0 +1,268 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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 logging
|
||||
import pydoc
|
||||
import time
|
||||
|
||||
from sql import Table
|
||||
|
||||
from trytond import __version__, backend, security
|
||||
from trytond.config import config, get_hostname
|
||||
from trytond.exceptions import (
|
||||
ConcurrencyException, LoginException, RateLimitException, UserError,
|
||||
UserWarning)
|
||||
from trytond.rpc import RPCReturnException
|
||||
from trytond.tools import is_instance_method
|
||||
from trytond.tools.logging import format_args
|
||||
from trytond.transaction import Transaction, TransactionError
|
||||
from trytond.worker import run_task
|
||||
from trytond.wsgi import app
|
||||
|
||||
from .wrappers import HTTPStatus, Response, abort, with_pool
|
||||
|
||||
__all__ = ['register_authentication_service']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ir_configuration = Table('ir_configuration')
|
||||
ir_lang = Table('ir_lang')
|
||||
ir_module = Table('ir_module')
|
||||
res_user = Table('res_user')
|
||||
|
||||
|
||||
@app.route('/<string:database_name>/', methods=['POST'])
|
||||
def rpc(request, database_name):
|
||||
methods = {
|
||||
'common.db.login': login,
|
||||
'common.db.logout': logout,
|
||||
'common.db.reset_password': reset_password,
|
||||
'system.listMethods': list_method,
|
||||
'system.methodHelp': help_method,
|
||||
'system.methodSignature': lambda *a: 'signatures not supported',
|
||||
}
|
||||
return methods.get(request.rpc_method, _dispatch)(
|
||||
request, database_name, *request.rpc_params)
|
||||
|
||||
|
||||
def login(request, database_name, user, parameters, language=None):
|
||||
context = {
|
||||
'language': language,
|
||||
'_request': request.context,
|
||||
}
|
||||
try:
|
||||
session = security.login(
|
||||
database_name, user, parameters, context=context)
|
||||
code = HTTPStatus.UNAUTHORIZED
|
||||
except backend.DatabaseOperationalError:
|
||||
logger.error('fail to connect to %s', database_name, exc_info=True)
|
||||
abort(HTTPStatus.NOT_FOUND)
|
||||
except RateLimitException:
|
||||
session = None
|
||||
code = HTTPStatus.TOO_MANY_REQUESTS
|
||||
if not session:
|
||||
abort(code)
|
||||
return session
|
||||
|
||||
|
||||
@app.auth_required
|
||||
def logout(request, database_name):
|
||||
auth = request.authorization
|
||||
security.logout(
|
||||
database_name, auth.get('userid'), auth.get('session'),
|
||||
context={'_request': request.context})
|
||||
|
||||
|
||||
def reset_password(request, database_name, user, language=None):
|
||||
authentications = config.get(
|
||||
'session', 'authentications', default='password').split(',')
|
||||
if not any('password' in m.split('+') for m in authentications):
|
||||
abort(HTTPStatus.FORBIDDEN)
|
||||
context = {
|
||||
'language': language,
|
||||
'_request': request.context,
|
||||
}
|
||||
try:
|
||||
security.reset_password(database_name, user, context=context)
|
||||
except backend.DatabaseOperationalError:
|
||||
logger.error('fail to connect to %s', database_name, exc_info=True)
|
||||
abort(HTTPStatus.NOT_FOUND)
|
||||
except RateLimitException:
|
||||
abort(HTTPStatus.TOO_MANY_REQUESTS)
|
||||
|
||||
|
||||
@app.route('/', methods=['POST'])
|
||||
def root(request, *args):
|
||||
methods = {
|
||||
'common.server.version': lambda *a: __version__,
|
||||
'common.db.list': db_list,
|
||||
'common.authentication.services': authentication_services,
|
||||
}
|
||||
return methods[request.rpc_method](request, *request.rpc_params)
|
||||
|
||||
|
||||
@app.route('/', methods=['OPTIONS'])
|
||||
@app.route('/<path:path>', methods=['OPTIONS'])
|
||||
def options(request, path=None):
|
||||
return Response(status=HTTPStatus.NO_CONTENT)
|
||||
|
||||
|
||||
def db_exist(request, database_name):
|
||||
try:
|
||||
backend.Database(database_name).connect()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def db_list(request, *args):
|
||||
if not config.getboolean('database', 'list'):
|
||||
abort(HTTPStatus.FORBIDDEN)
|
||||
context = {'_request': request.context}
|
||||
hostname = get_hostname(request.host)
|
||||
with Transaction().start(
|
||||
None, 0, context=context, readonly=True, close=True,
|
||||
) as transaction:
|
||||
return transaction.database.list(hostname=hostname)
|
||||
|
||||
|
||||
def authentication_services(request):
|
||||
return _AUTHENTICATION_SERVICES
|
||||
|
||||
|
||||
def register_authentication_service(name, url):
|
||||
_AUTHENTICATION_SERVICES.append((name, url))
|
||||
|
||||
|
||||
_AUTHENTICATION_SERVICES = []
|
||||
|
||||
|
||||
@app.auth_required
|
||||
@with_pool
|
||||
def list_method(request, pool):
|
||||
methods = []
|
||||
for type in ('model', 'wizard', 'report'):
|
||||
for object_name, obj in pool.iterobject(type=type):
|
||||
for method in obj.__rpc__:
|
||||
methods.append(type + '.' + object_name + '.' + method)
|
||||
return methods
|
||||
|
||||
|
||||
def get_object_method(request, pool):
|
||||
method = request.rpc_method
|
||||
type, _ = method.split('.', 1)
|
||||
name = '.'.join(method.split('.')[1:-1])
|
||||
method = method.split('.')[-1]
|
||||
return pool.get(name, type=type), method
|
||||
|
||||
|
||||
@app.auth_required
|
||||
@with_pool
|
||||
def help_method(request, pool):
|
||||
obj, method = get_object_method(request, pool)
|
||||
return pydoc.getdoc(getattr(obj, method))
|
||||
|
||||
|
||||
@app.auth_required
|
||||
@with_pool
|
||||
def _dispatch(request, pool, *args, **kwargs):
|
||||
obj, method = get_object_method(request, pool)
|
||||
if method in obj.__rpc__:
|
||||
rpc = obj.__rpc__[method]
|
||||
else:
|
||||
abort(HTTPStatus.FORBIDDEN)
|
||||
|
||||
user = request.user_id
|
||||
session = None
|
||||
if request.authorization.type == 'session':
|
||||
session = request.authorization.get('session')
|
||||
|
||||
if rpc.fresh_session and session:
|
||||
context = {'_request': request.context}
|
||||
if not security.check_timeout(
|
||||
pool.database_name, user, session, context=context):
|
||||
abort(HTTPStatus.UNAUTHORIZED)
|
||||
|
||||
log_message = '%s.%s%s from %s@%s%s in %i ms'
|
||||
username = request.authorization.username
|
||||
if isinstance(username, bytes):
|
||||
username = username.decode('utf-8')
|
||||
log_args = (
|
||||
obj.__name__, method,
|
||||
format_args(args, kwargs, logger.isEnabledFor(logging.DEBUG)),
|
||||
username, request.remote_addr, request.path)
|
||||
|
||||
def duration():
|
||||
return (time.monotonic() - started) * 1000
|
||||
started = time.monotonic()
|
||||
|
||||
retry = config.getint('database', 'retry')
|
||||
count = 0
|
||||
transaction_extras = {}
|
||||
while True:
|
||||
if count:
|
||||
time.sleep(0.02 * (retry - count))
|
||||
with Transaction().start(
|
||||
pool.database_name, user,
|
||||
readonly=rpc.readonly, timeout=rpc.timeout,
|
||||
**transaction_extras) as transaction:
|
||||
try:
|
||||
c_args, c_kwargs, transaction.context, transaction.timestamp \
|
||||
= rpc.convert(obj, *args, **kwargs)
|
||||
transaction.context['_request'] = request.context
|
||||
meth = rpc.decorate(getattr(obj, method))
|
||||
if (rpc.instantiate is None
|
||||
or not is_instance_method(obj, method)):
|
||||
result = rpc.result(meth(*c_args, **c_kwargs))
|
||||
else:
|
||||
assert rpc.instantiate == 0
|
||||
inst = c_args.pop(0)
|
||||
if hasattr(inst, method):
|
||||
result = rpc.result(meth(inst, *c_args, **c_kwargs))
|
||||
else:
|
||||
result = [rpc.result(meth(i, *c_args, **c_kwargs))
|
||||
for i in inst]
|
||||
except TransactionError as e:
|
||||
transaction.rollback()
|
||||
transaction.tasks.clear()
|
||||
e.fix(transaction_extras)
|
||||
continue
|
||||
except backend.DatabaseTimeoutError:
|
||||
logger.warning(log_message, *log_args, exc_info=True)
|
||||
abort(HTTPStatus.REQUEST_TIMEOUT)
|
||||
except backend.DatabaseOperationalError:
|
||||
if count < retry and not rpc.readonly:
|
||||
transaction.rollback()
|
||||
transaction.tasks.clear()
|
||||
count += 1
|
||||
logger.debug("Retry: %i", count)
|
||||
continue
|
||||
logger.exception(log_message, *log_args, duration())
|
||||
raise
|
||||
except RPCReturnException as e:
|
||||
transaction.rollback()
|
||||
transaction.tasks.clear()
|
||||
result = e.result()
|
||||
except (ConcurrencyException, UserError, UserWarning,
|
||||
LoginException):
|
||||
logger.info(
|
||||
log_message, *log_args, duration(),
|
||||
exc_info=logger.isEnabledFor(logging.DEBUG))
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception(log_message, *log_args, duration())
|
||||
raise
|
||||
# Need to commit to unlock SQLite database
|
||||
transaction.commit()
|
||||
while transaction.tasks:
|
||||
task_id = transaction.tasks.pop()
|
||||
run_task(pool, task_id)
|
||||
if session:
|
||||
context = {'_request': request.context}
|
||||
security.reset(pool.database_name, session, context=context)
|
||||
logger.info(log_message, *log_args, duration())
|
||||
logger.debug('Result: %r', result)
|
||||
response = app.make_response(request, result)
|
||||
if rpc.readonly and rpc.cache:
|
||||
response.headers.extend(rpc.cache.headers())
|
||||
return response
|
||||
193
protocols/jsonrpc.py
Executable file
193
protocols/jsonrpc.py
Executable file
@@ -0,0 +1,193 @@
|
||||
# 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 datetime
|
||||
import json
|
||||
from decimal import Decimal
|
||||
|
||||
from werkzeug.exceptions import (
|
||||
BadRequest, Conflict, Forbidden, InternalServerError, Locked,
|
||||
TooManyRequests)
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from trytond.exceptions import (
|
||||
ConcurrencyException, LoginException, MissingDependenciesException,
|
||||
RateLimitException, TrytonException, UserWarning)
|
||||
from trytond.protocols.wrappers import Request
|
||||
from trytond.tools import cached_property
|
||||
|
||||
|
||||
class JSONDecoder(object):
|
||||
|
||||
decoders = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, klass, decoder):
|
||||
assert klass not in cls.decoders
|
||||
cls.decoders[klass] = decoder
|
||||
|
||||
def __call__(self, dct):
|
||||
if dct.get('__class__') in self.decoders:
|
||||
return self.decoders[dct['__class__']](dct)
|
||||
return dct
|
||||
|
||||
|
||||
JSONDecoder.register('datetime',
|
||||
lambda dct: datetime.datetime(dct['year'], dct['month'], dct['day'],
|
||||
dct['hour'], dct['minute'], dct['second'], dct['microsecond']))
|
||||
JSONDecoder.register('date',
|
||||
lambda dct: datetime.date(dct['year'], dct['month'], dct['day']))
|
||||
JSONDecoder.register('time',
|
||||
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
|
||||
dct['microsecond']))
|
||||
JSONDecoder.register('timedelta',
|
||||
lambda dct: datetime.timedelta(seconds=dct['seconds']))
|
||||
|
||||
|
||||
def _bytes_decoder(dct):
|
||||
cast = bytearray if bytes == str else bytes
|
||||
return cast(base64.decodebytes(dct['base64'].encode('utf-8')))
|
||||
|
||||
|
||||
JSONDecoder.register('bytes', _bytes_decoder)
|
||||
JSONDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
|
||||
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
|
||||
serializers = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, klass, encoder):
|
||||
assert klass not in cls.serializers
|
||||
cls.serializers[klass] = encoder
|
||||
|
||||
def default(self, obj):
|
||||
marshaller = self.serializers.get(type(obj),
|
||||
super(JSONEncoder, self).default)
|
||||
return marshaller(obj)
|
||||
|
||||
|
||||
JSONEncoder.register(datetime.datetime,
|
||||
lambda o: {
|
||||
'__class__': 'datetime',
|
||||
'year': o.year,
|
||||
'month': o.month,
|
||||
'day': o.day,
|
||||
'hour': o.hour,
|
||||
'minute': o.minute,
|
||||
'second': o.second,
|
||||
'microsecond': o.microsecond,
|
||||
})
|
||||
JSONEncoder.register(datetime.date,
|
||||
lambda o: {
|
||||
'__class__': 'date',
|
||||
'year': o.year,
|
||||
'month': o.month,
|
||||
'day': o.day,
|
||||
})
|
||||
JSONEncoder.register(datetime.time,
|
||||
lambda o: {
|
||||
'__class__': 'time',
|
||||
'hour': o.hour,
|
||||
'minute': o.minute,
|
||||
'second': o.second,
|
||||
'microsecond': o.microsecond,
|
||||
})
|
||||
JSONEncoder.register(datetime.timedelta,
|
||||
lambda o: {
|
||||
'__class__': 'timedelta',
|
||||
'seconds': o.total_seconds(),
|
||||
})
|
||||
|
||||
|
||||
def _bytes_encoder(o):
|
||||
return {
|
||||
'__class__': 'bytes',
|
||||
'base64': base64.encodebytes(o).decode('utf-8'),
|
||||
}
|
||||
|
||||
|
||||
JSONEncoder.register(bytes, _bytes_encoder)
|
||||
JSONEncoder.register(bytearray, _bytes_encoder)
|
||||
JSONEncoder.register(Decimal,
|
||||
lambda o: {
|
||||
'__class__': 'Decimal',
|
||||
'decimal': str(o),
|
||||
})
|
||||
|
||||
|
||||
class JSONRequest(Request):
|
||||
parsed_content_type = 'json'
|
||||
|
||||
@cached_property
|
||||
def parsed_data(self):
|
||||
if self.parsed_content_type in self.environ.get('CONTENT_TYPE', ''):
|
||||
try:
|
||||
return json.loads(
|
||||
self.decoded_data.decode(
|
||||
getattr(self, 'charset', 'utf-8'),
|
||||
getattr(self, 'encoding_errors', 'replace')),
|
||||
object_hook=JSONDecoder())
|
||||
except Exception:
|
||||
raise BadRequest('Unable to read JSON request')
|
||||
else:
|
||||
raise BadRequest('Not a JSON request')
|
||||
|
||||
@cached_property
|
||||
def rpc_method(self):
|
||||
try:
|
||||
return self.parsed_data['method']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@cached_property
|
||||
def rpc_params(self):
|
||||
try:
|
||||
return self.parsed_data['params']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class JSONProtocol:
|
||||
content_type = 'json'
|
||||
|
||||
@classmethod
|
||||
def request(cls, environ):
|
||||
return JSONRequest(environ)
|
||||
|
||||
@classmethod
|
||||
def response(cls, data, request):
|
||||
try:
|
||||
parsed_data = request.parsed_data
|
||||
except BadRequest:
|
||||
parsed_data = {}
|
||||
if (isinstance(request, JSONRequest)
|
||||
and set(parsed_data.keys()) == {'id', 'method', 'params'}):
|
||||
response = {'id': parsed_data.get('id', 0)}
|
||||
if isinstance(data, TrytonException):
|
||||
response['error'] = data.args
|
||||
elif isinstance(data, Exception):
|
||||
# report exception back to server
|
||||
response['error'] = (str(data), data.__format_traceback__)
|
||||
else:
|
||||
response['result'] = data
|
||||
else:
|
||||
if isinstance(data, UserWarning):
|
||||
return Conflict(data)
|
||||
elif isinstance(data, LoginException):
|
||||
return Forbidden(data)
|
||||
elif isinstance(data, ConcurrencyException):
|
||||
return Locked(data)
|
||||
elif isinstance(data, RateLimitException):
|
||||
return TooManyRequests(data)
|
||||
elif isinstance(data, MissingDependenciesException):
|
||||
return InternalServerError(data)
|
||||
elif isinstance(data, TrytonException):
|
||||
return BadRequest(data)
|
||||
elif isinstance(data, Exception):
|
||||
return InternalServerError(data)
|
||||
response = data
|
||||
return Response(json.dumps(
|
||||
response, cls=JSONEncoder, separators=(',', ':')),
|
||||
content_type='application/json')
|
||||
321
protocols/wrappers.py
Executable file
321
protocols/wrappers.py
Executable file
@@ -0,0 +1,321 @@
|
||||
# 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 gzip
|
||||
import logging
|
||||
import time
|
||||
from functools import wraps
|
||||
from io import BytesIO
|
||||
|
||||
try:
|
||||
from http import HTTPStatus
|
||||
except ImportError:
|
||||
from http import client as HTTPStatus
|
||||
|
||||
from werkzeug import exceptions
|
||||
from werkzeug.datastructures import Authorization
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.utils import redirect, send_file
|
||||
from werkzeug.wrappers import Request as _Request
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from trytond import backend, security
|
||||
from trytond.config import config
|
||||
from trytond.exceptions import RateLimitException, UserError, UserWarning
|
||||
from trytond.pool import Pool
|
||||
from trytond.tools import cached_property
|
||||
from trytond.transaction import Transaction, TransactionError, check_access
|
||||
|
||||
__all__ = [
|
||||
'HTTPStatus',
|
||||
'Request',
|
||||
'Response',
|
||||
'abort',
|
||||
'allow_null_origin',
|
||||
'exceptions',
|
||||
'redirect',
|
||||
'send_file',
|
||||
'set_max_request_size',
|
||||
'user_application',
|
||||
'with_pool',
|
||||
'with_transaction',
|
||||
]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Request(_Request):
|
||||
|
||||
view_args = None
|
||||
|
||||
def __repr__(self):
|
||||
args = []
|
||||
try:
|
||||
if self.url is None or isinstance(self.url, str):
|
||||
url = self.url
|
||||
else:
|
||||
url = self.url.decode(getattr(self, 'url_charset', 'utf-8'))
|
||||
auth = self.authorization
|
||||
if auth:
|
||||
args.append("%s@%s" % (
|
||||
auth.get('userid', auth.username), self.remote_addr))
|
||||
else:
|
||||
args.append(self.remote_addr)
|
||||
args.append("'%s'" % url)
|
||||
args.append("[%s]" % self.method)
|
||||
if self.view_args:
|
||||
args.append("%s" % (self.rpc_method or ''))
|
||||
except Exception:
|
||||
args.append("(invalid WSGI environ)")
|
||||
return "<%s %s>" % (
|
||||
self.__class__.__name__, " ".join(filter(None, args)))
|
||||
|
||||
@property
|
||||
def decoded_data(self):
|
||||
if self.content_encoding == 'gzip':
|
||||
if self.user_id:
|
||||
zipfile = gzip.GzipFile(fileobj=BytesIO(self.data), mode='rb')
|
||||
return zipfile.read()
|
||||
else:
|
||||
abort(HTTPStatus.UNSUPPORTED_MEDIA_TYPE)
|
||||
else:
|
||||
return self.data
|
||||
|
||||
@property
|
||||
def parsed_data(self):
|
||||
return self.data
|
||||
|
||||
@property
|
||||
def rpc_method(self):
|
||||
return
|
||||
|
||||
@property
|
||||
def rpc_params(self):
|
||||
return
|
||||
|
||||
@cached_property
|
||||
def authorization(self):
|
||||
authorization = super(Request, self).authorization
|
||||
if authorization is None:
|
||||
header = self.headers.get('Authorization')
|
||||
return parse_authorization_header(header)
|
||||
elif authorization.type == 'session':
|
||||
# Werkzeug may parse the session as parameters
|
||||
# if the base64 uses the padding sign '='
|
||||
if authorization.token is None:
|
||||
header = self.headers.get('Authorization')
|
||||
return parse_authorization_header(header)
|
||||
else:
|
||||
return parse_session(authorization.token)
|
||||
return authorization
|
||||
|
||||
@cached_property
|
||||
def user_id(self):
|
||||
assert self.view_args is not None
|
||||
database_name = self.view_args.get('database_name')
|
||||
if not database_name:
|
||||
return None
|
||||
auth = self.authorization
|
||||
if not auth:
|
||||
return None
|
||||
context = {'_request': self.context}
|
||||
if auth.type == 'session':
|
||||
user_id = security.check(
|
||||
database_name, auth.get('userid'), auth.get('session'),
|
||||
context=context)
|
||||
elif auth.username:
|
||||
parameters = getattr(auth, 'parameters', auth)
|
||||
try:
|
||||
user_id = security.login(
|
||||
database_name, auth.username, parameters, cache=False,
|
||||
context=context)
|
||||
except RateLimitException:
|
||||
abort(HTTPStatus.TOO_MANY_REQUESTS)
|
||||
else:
|
||||
user_id = None
|
||||
return user_id
|
||||
|
||||
@cached_property
|
||||
def context(self):
|
||||
return {
|
||||
'remote_addr': self.remote_addr,
|
||||
'http_host': self.environ.get('HTTP_HOST'),
|
||||
'scheme': self.scheme,
|
||||
'is_secure': self.is_secure,
|
||||
}
|
||||
|
||||
|
||||
def parse_authorization_header(value):
|
||||
if not value:
|
||||
return
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('latin1')
|
||||
try:
|
||||
auth_type, auth_info = value.split(None, 1)
|
||||
auth_type = auth_type.lower()
|
||||
except ValueError:
|
||||
return
|
||||
if auth_type == 'session':
|
||||
return parse_session(auth_info)
|
||||
else:
|
||||
authorization = Authorization(auth_type)
|
||||
authorization.token = auth_info
|
||||
return authorization
|
||||
|
||||
|
||||
def parse_session(token):
|
||||
try:
|
||||
username, userid, session = (
|
||||
base64.b64decode(token).decode().split(':', 3))
|
||||
userid = int(userid)
|
||||
except Exception:
|
||||
return
|
||||
return Authorization('session', {
|
||||
'username': username,
|
||||
'userid': userid,
|
||||
'session': session,
|
||||
})
|
||||
|
||||
|
||||
def set_max_request_size(size):
|
||||
def decorator(func):
|
||||
func.max_request_size = size
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
def allow_null_origin(func):
|
||||
func.allow_null_origin = True
|
||||
return func
|
||||
|
||||
|
||||
def with_pool(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, database_name, *args, **kwargs):
|
||||
database_list = Pool.database_list()
|
||||
pool = Pool(database_name)
|
||||
if database_name not in database_list:
|
||||
with Transaction().start(database_name, 0, readonly=True):
|
||||
pool.init()
|
||||
|
||||
log_message = '%s in %i ms'
|
||||
|
||||
def duration():
|
||||
return (time.monotonic() - started) * 1000
|
||||
started = time.monotonic()
|
||||
|
||||
try:
|
||||
result = func(request, pool, *args, **kwargs)
|
||||
except exceptions.HTTPException:
|
||||
logger.info(
|
||||
log_message, request, duration(),
|
||||
exc_info=logger.isEnabledFor(logging.DEBUG))
|
||||
raise
|
||||
except (UserError, UserWarning) as e:
|
||||
logger.info(
|
||||
log_message, request, duration(),
|
||||
exc_info=logger.isEnabledFor(logging.DEBUG))
|
||||
if request.rpc_method:
|
||||
raise
|
||||
else:
|
||||
abort(HTTPStatus.BAD_REQUEST, e)
|
||||
except Exception as e:
|
||||
logger.exception(log_message, request, duration())
|
||||
if request.rpc_method:
|
||||
raise
|
||||
else:
|
||||
abort(HTTPStatus.INTERNAL_SERVER_ERROR, e)
|
||||
logger.info(log_message, request, duration())
|
||||
return result
|
||||
return wrapper
|
||||
|
||||
|
||||
def with_transaction(readonly=None, user=0, context=None):
|
||||
from trytond.worker import run_task
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, pool, *args, **kwargs):
|
||||
readonly_ = readonly # can not modify non local
|
||||
if readonly_ is None:
|
||||
if request.method in {'POST', 'PUT', 'DELETE', 'PATCH'}:
|
||||
readonly_ = False
|
||||
else:
|
||||
readonly_ = True
|
||||
if context is None:
|
||||
context_ = {}
|
||||
else:
|
||||
context_ = context.copy()
|
||||
context_['_request'] = request.context
|
||||
if user == 'request':
|
||||
user_ = request.user_id
|
||||
else:
|
||||
user_ = user
|
||||
retry = config.getint('database', 'retry')
|
||||
count = 0
|
||||
transaction_extras = {}
|
||||
while True:
|
||||
if count:
|
||||
time.sleep(0.02 * (retry - count))
|
||||
with Transaction().start(
|
||||
pool.database_name, user_, readonly=readonly_,
|
||||
context=context_, **transaction_extras) as transaction:
|
||||
try:
|
||||
result = func(request, pool, *args, **kwargs)
|
||||
except TransactionError as e:
|
||||
transaction.rollback()
|
||||
transaction.tasks.clear()
|
||||
e.fix(transaction_extras)
|
||||
continue
|
||||
except backend.DatabaseOperationalError:
|
||||
if count < retry and not readonly_:
|
||||
transaction.rollback()
|
||||
transaction.tasks.clear()
|
||||
count += 1
|
||||
logger.debug("Retry: %i", count)
|
||||
continue
|
||||
raise
|
||||
# Need to commit to unlock SQLite database
|
||||
transaction.commit()
|
||||
while transaction.tasks:
|
||||
task_id = transaction.tasks.pop()
|
||||
run_task(pool, task_id)
|
||||
return result
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def user_application(name, json=True):
|
||||
from .jsonrpc import JSONEncoder
|
||||
from .jsonrpc import json as json_
|
||||
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
pool = Pool()
|
||||
UserApplication = pool.get('res.user.application')
|
||||
|
||||
authorization = request.authorization
|
||||
if authorization is None:
|
||||
header = request.headers.get('Authorization')
|
||||
authorization = parse_authorization_header(header)
|
||||
if authorization is None:
|
||||
abort(HTTPStatus.UNAUTHORIZED)
|
||||
if authorization.type != 'bearer':
|
||||
abort(HTTPStatus.FORBIDDEN)
|
||||
|
||||
token = getattr(authorization, 'token', '')
|
||||
application = UserApplication.check(token, name)
|
||||
if not application:
|
||||
abort(HTTPStatus.FORBIDDEN)
|
||||
transaction = Transaction()
|
||||
# TODO language
|
||||
with transaction.set_user(application.user.id), \
|
||||
check_access():
|
||||
response = func(request, *args, **kwargs)
|
||||
if not isinstance(response, Response) and json:
|
||||
response = Response(json_.dumps(response, cls=JSONEncoder),
|
||||
content_type='application/json')
|
||||
return response
|
||||
return wrapper
|
||||
return decorator
|
||||
187
protocols/xmlrpc.py
Executable file
187
protocols/xmlrpc.py
Executable file
@@ -0,0 +1,187 @@
|
||||
# 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 datetime
|
||||
import logging
|
||||
import xmlrpc.client as client
|
||||
# convert decimal to float before marshalling:
|
||||
from decimal import Decimal
|
||||
|
||||
import defusedxml.xmlrpc
|
||||
from werkzeug.exceptions import (
|
||||
BadRequest, Conflict, Forbidden, InternalServerError, Locked,
|
||||
TooManyRequests)
|
||||
from werkzeug.wrappers import Response
|
||||
|
||||
from trytond.exceptions import (
|
||||
ConcurrencyException, LoginException, MissingDependenciesException,
|
||||
RateLimitException, TrytonException, UserWarning)
|
||||
from trytond.model.fields.dict import ImmutableDict
|
||||
from trytond.protocols.wrappers import Request
|
||||
from trytond.tools import cached_property
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
defusedxml.xmlrpc.monkey_patch()
|
||||
|
||||
|
||||
def dump_decimal(self, value, write):
|
||||
write('<value><bigdecimal>')
|
||||
write(str(Decimal(value)))
|
||||
write('</bigdecimal></value>')
|
||||
|
||||
|
||||
def dump_date(self, value, write):
|
||||
value = {'__class__': 'date',
|
||||
'year': value.year,
|
||||
'month': value.month,
|
||||
'day': value.day,
|
||||
}
|
||||
self.dump_struct(value, write)
|
||||
|
||||
|
||||
def dump_time(self, value, write):
|
||||
value = {'__class__': 'time',
|
||||
'hour': value.hour,
|
||||
'minute': value.minute,
|
||||
'second': value.second,
|
||||
'microsecond': value.microsecond,
|
||||
}
|
||||
self.dump_struct(value, write)
|
||||
|
||||
|
||||
def dump_timedelta(self, value, write):
|
||||
value = {'__class__': 'timedelta',
|
||||
'seconds': value.total_seconds(),
|
||||
}
|
||||
self.dump_struct(value, write)
|
||||
|
||||
|
||||
def dump_long(self, value, write):
|
||||
try:
|
||||
self.dump_long(value, write)
|
||||
except OverflowError:
|
||||
write('<value><biginteger>')
|
||||
write(str(int(value)))
|
||||
write('</biginteger></value>\n')
|
||||
|
||||
|
||||
client.Marshaller.dispatch[Decimal] = dump_decimal
|
||||
client.Marshaller.dispatch[datetime.date] = dump_date
|
||||
client.Marshaller.dispatch[datetime.time] = dump_time
|
||||
client.Marshaller.dispatch[datetime.timedelta] = dump_timedelta
|
||||
client.Marshaller.dispatch[int] = dump_long
|
||||
|
||||
|
||||
def dump_struct(self, value, write, escape=client.escape):
|
||||
converted_value = {}
|
||||
for k, v in value.items():
|
||||
if isinstance(k, int):
|
||||
k = str(k)
|
||||
elif isinstance(k, float):
|
||||
k = repr(k)
|
||||
converted_value[k] = v
|
||||
return self.dump_struct(converted_value, write, escape=escape)
|
||||
|
||||
|
||||
client.Marshaller.dispatch[dict] = dump_struct
|
||||
client.Marshaller.dispatch[ImmutableDict] = dump_struct
|
||||
|
||||
|
||||
class XMLRPCDecoder(object):
|
||||
|
||||
decoders = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, klass, decoder):
|
||||
assert klass not in cls.decoders
|
||||
cls.decoders[klass] = decoder
|
||||
|
||||
def __call__(self, dct):
|
||||
if dct.get('__class__') in self.decoders:
|
||||
return self.decoders[dct['__class__']](dct)
|
||||
return dct
|
||||
|
||||
|
||||
XMLRPCDecoder.register('date',
|
||||
lambda dct: datetime.date(dct['year'], dct['month'], dct['day']))
|
||||
XMLRPCDecoder.register('time',
|
||||
lambda dct: datetime.time(dct['hour'], dct['minute'], dct['second'],
|
||||
dct['microsecond']))
|
||||
XMLRPCDecoder.register('timedelta',
|
||||
lambda dct: datetime.timedelta(seconds=dct['seconds']))
|
||||
XMLRPCDecoder.register('Decimal', lambda dct: Decimal(dct['decimal']))
|
||||
|
||||
|
||||
def end_struct(self, data):
|
||||
mark = self._marks.pop()
|
||||
# map structs to Python dictionaries
|
||||
dct = {}
|
||||
items = self._stack[mark:]
|
||||
for i in range(0, len(items), 2):
|
||||
dct[items[i]] = items[i + 1]
|
||||
dct = XMLRPCDecoder()(dct)
|
||||
self._stack[mark:] = [dct]
|
||||
self._value = 0
|
||||
|
||||
|
||||
client.Unmarshaller.dispatch['struct'] = end_struct
|
||||
|
||||
|
||||
class XMLRequest(Request):
|
||||
parsed_content_type = 'xml'
|
||||
|
||||
@cached_property
|
||||
def parsed_data(self):
|
||||
if self.parsed_content_type in self.environ.get('CONTENT_TYPE', ''):
|
||||
try:
|
||||
# TODO replace by own loads
|
||||
return client.loads(self.decoded_data, use_builtin_types=True)
|
||||
except Exception:
|
||||
raise BadRequest('Unable to read XMl request')
|
||||
else:
|
||||
raise BadRequest('Not an XML request')
|
||||
|
||||
@property
|
||||
def rpc_method(self):
|
||||
return self.parsed_data[1]
|
||||
|
||||
@property
|
||||
def rpc_params(self):
|
||||
return self.parsed_data[0]
|
||||
|
||||
|
||||
class XMLProtocol:
|
||||
content_type = 'xml'
|
||||
|
||||
@classmethod
|
||||
def request(cls, environ):
|
||||
return XMLRequest(environ)
|
||||
|
||||
@classmethod
|
||||
def response(cls, data, request):
|
||||
if isinstance(request, XMLRequest):
|
||||
if isinstance(data, TrytonException):
|
||||
data = client.Fault(data.code, str(data))
|
||||
elif isinstance(data, Exception):
|
||||
data = client.Fault(255, str(data))
|
||||
else:
|
||||
data = (data,)
|
||||
return Response(client.dumps(
|
||||
data, methodresponse=True, allow_none=True),
|
||||
content_type='text/xml')
|
||||
else:
|
||||
if isinstance(data, UserWarning):
|
||||
return Conflict(data)
|
||||
elif isinstance(data, LoginException):
|
||||
return Forbidden(data)
|
||||
elif isinstance(data, ConcurrencyException):
|
||||
return Locked(data)
|
||||
elif isinstance(data, RateLimitException):
|
||||
return TooManyRequests(data)
|
||||
elif isinstance(data, MissingDependenciesException):
|
||||
return InternalServerError(data)
|
||||
elif isinstance(data, TrytonException):
|
||||
return BadRequest(data)
|
||||
elif isinstance(data, Exception):
|
||||
return InternalServerError(data)
|
||||
return Response(data)
|
||||
Reference in New Issue
Block a user