Switch to WSGI API

issue4646
review20541003
This commit is contained in:
Cédric Krier
2016-03-15 16:57:26 +01:00
parent 687a51a30c
commit 8a4ba59b2d
31 changed files with 834 additions and 1042 deletions
+1
View File
@@ -1,3 +1,4 @@
* Switch to WSGI API
* Limit the login size in LoginAttempt
* Remove LocalDict from tools
* Add LRUDictTransaction
+38 -68
View File
@@ -1,80 +1,50 @@
#!/usr/bin/env 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.
# 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 sys
import os
import argparse
import glob
from werkzeug.serving import run_simple
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
'..', '..', 'trytond')))
if os.path.isdir(DIR):
sys.path.insert(0, os.path.dirname(DIR))
from trytond import __version__
from trytond import server
import trytond.commandline as commandline
from trytond.config import config, split_netloc
from trytond.application import app
from trytond.pool import Pool
from trytond.modules import get_module_list, get_module_info
def parse_commandline():
options = {}
parser = commandline.get_parser_daemon()
options = parser.parse_args()
commandline.config_log(options)
config.update_etc(options.configfile)
parser = argparse.ArgumentParser(prog='trytond')
parser.add_argument('--version', action='version',
version='%(prog)s ' + __version__)
parser.add_argument("-c", "--config", dest="configfile", metavar='FILE',
default=os.environ.get('TRYTOND_CONFIG'), help="specify config file")
parser.add_argument('--dev', dest='dev', action='store_true',
help='enable development mode')
parser.add_argument("-v", "--verbose", action="store_true",
dest="verbose", help="enable verbose mode")
parser.add_argument("-d", "--database", dest="database_names", nargs='+',
default=[], metavar='DATABASE', help="specify the database name")
parser.add_argument("-u", "--update", dest="update", nargs='+', default=[],
metavar='MODULE', help="update a module")
parser.add_argument("--all", dest="update", action="append_const",
const="ir", help="update all installed modules")
parser.add_argument("--pidfile", dest="pidfile", metavar='FILE',
help="file where the server pid will be stored")
parser.add_argument("--logconf", dest="logconf", metavar='FILE',
help="logging configuration file (ConfigParser format)")
parser.add_argument("--cron", dest="cron", action="store_true",
help="enable cron")
parser.epilog = ('The first time a database is initialized admin '
'password is read from file defined by TRYTONPASSFILE '
'environment variable or interactively ask user.\n'
'The config file can be specified in the TRYTOND_CONFIG '
'environment variable.\n'
'The database URI can be specified in the TRYTOND_DATABASE_URI '
'environment variable.')
options = parser.parse_args()
if not options.database_names and options.update:
parser.error('Missing database option')
return options
if '--profile' in sys.argv:
import profile
import pstats
import tempfile
sys.argv.remove('--profile')
options = parse_commandline()
statfile = tempfile.mkstemp(".stat", "trytond-")[1]
profile.run('server.TrytonServer(options).run()', statfile)
s = pstats.Stats(statfile)
s.sort_stats('cumulative').print_stats()
s.sort_stats('call').print_stats()
s.sort_stats('time').print_stats()
s.sort_stats('time')
s.print_callers()
s.print_callees()
os.remove(statfile)
else:
options = parse_commandline()
server.TrytonServer(options).run()
with commandline.pidfile(options):
for name in options.database_names:
Pool(name).init()
hostname, port = split_netloc(config.get('web', 'listen'))
static_files = {
'/': config.get('web', 'root'),
}
certificate = config.get('ssl', 'certificate')
privatekey = config.get('ssl', 'privatekey')
if certificate or privatekey:
ssl_context = (certificate, privatekey)
else:
ssl_context = None
extra_files = [options.configfile]
for module in get_module_list():
info = get_module_info(module)
path = os.path.join(info['directory'], 'view', '*.xml')
extra_files.extend(glob.glob(path))
run_simple(hostname, port, app,
threaded=True,
extra_files=extra_files,
static_files=static_files,
ssl_context=ssl_context,
use_reloader=options.dev)
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env 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 sys
import os
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
'..', '..', 'trytond')))
if os.path.isdir(DIR):
sys.path.insert(0, os.path.dirname(DIR))
import trytond.commandline as commandline
from trytond.config import config
import trytond.admin as admin
parser = commandline.get_parser_admin()
options = parser.parse_args()
config.update_etc(options.configfile)
commandline.config_log(options)
admin.run(options)
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env 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 sys
import os
import time
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
'..', '..', 'trytond')))
if os.path.isdir(DIR):
sys.path.insert(0, os.path.dirname(DIR))
import trytond.commandline as commandline
from trytond.config import config
import trytond.cron as cron
parser = commandline.get_parser_daemon()
options = parser.parse_args()
config.update_etc(options.configfile)
commandline.config_log(options)
with commandline.pidfile(options):
while True:
cron.run(options)
time.sleep(60)
+1
View File
@@ -27,6 +27,7 @@ First steps
:ref:`Installation <topics-install>` |
:ref:`Configuration <topics-configuration>` |
:ref:`Setup a database <topics-setup-database>`
:ref:`Start the server <topics-start-server>`
The model layer
===============
+8 -24
View File
@@ -29,47 +29,31 @@ possible values.
Some modules could request the usage of other sections for which the guideline
asks them to be named like their module.
jsonrpc
-------
web
---
Defines the behavior of the JSON-RPC_ network interface.
Defines the behavior of the web interface.
listen
~~~~~~
Defines a comma separated list of couples of host (or IP address) and port
number separated by a colon to listen on.
Defines the couple of host (or IP address) and port number separated by a colon
to listen on.
Default `localhost:8000`
hostname
~~~~~~~~
Defines the hostname for this network interface.
Defines the hostname.
data
root
~~~~
Defines the root path to retrieve data for `GET` requests.
Defines the root path served by `GET` requests.
Default: `/var/www/localhost/tryton`
xmlrpc
------
Defines the behavior of the XML-RPC_ network interface.
listen
~~~~~~
Same as for `jsonrpc` except it has no default value.
listen
~~~~~~
Same as for `jsonrpc` except it has no default value.
database
--------
+1
View File
@@ -13,6 +13,7 @@ Introduction to all the key parts of trytond:
configuration
setup_database
logs
start_server
models/index
models/fields_default_value
models/fields_on_change
+2
View File
@@ -8,6 +8,8 @@ Prerequisites
=============
* Python 2.7 or later (http://www.python.org/)
* Werkzeug (http://werkzeug.pocoo.org/)
* wrapt (https://github.com/GrahamDumpleton/wrapt)
* lxml 2.0 or later (http://lxml.de/)
* relatorio 0.2.0 or later (http://code.google.com/p/python-relatorio/)
* Genshi (http://genshi.edgewall.org/)
+2 -2
View File
@@ -20,7 +20,7 @@ Initialize a database
A database can be initialized using this command line::
trytond -c <config file> -d <database name> --all
trytond-admin -c <config file> -d <database name> --all
At the end of the process, `trytond` will ask to set the password for the
At the end of the process, `trytond-admin` will ask to set the password for the
`admin` user.
+41
View File
@@ -0,0 +1,41 @@
.. _topics-start-server:
=======================
How to start the server
=======================
Web service
===========
You can start the default web server bundled in Tryton with this command line::
trytond -c <config file>
The server will wait for client connections on the interface defined in the
`web` section of the :ref:`configuration <topics-configuration>`.
WSGI server
-----------
If you prefer to run Tryton inside your own WSGI server instead of the simple
server of Werkzeug, you can use the application `trytond.application.app` and
set the environment variable `TRYTOND_CONFIG` to point to the
:ref:`configuration <topics-configuration>`.
.. warning:: You must manage to serve the static files from the web root.
Cron service
============
If you want to run some scheduled actions, you must also run the cron server
with this command line::
trytond-cron -c <config file> -d <database>
The server will wake up every minutes and preform the scheduled actions defined
in the `database`.
Services options
================
You will find more options for those services by using `--help` arguments.
+3 -1
View File
@@ -59,7 +59,7 @@ setup(name=name,
'trytond.res': ['tryton.cfg', '*.xml', 'view/*.xml', 'locale/*.po'],
'trytond.tests': ['tryton.cfg', '*.xml'],
},
scripts=['bin/trytond'],
scripts=['bin/trytond', 'bin/trytond-admin', 'bin/trytond-cron'],
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: No Input/Output (Daemon)',
@@ -97,6 +97,8 @@ setup(name=name,
'python-dateutil',
'polib',
'python-sql >= 0.4',
'werkzeug',
'wrapt',
],
extras_require={
'PostgreSQL': pg_require,
+5
View File
@@ -2,13 +2,18 @@
# this repository contains the full copyright notices and license terms.
import os
import time
import logging
from email import charset
__version__ = "3.9.0"
logger = logging.getLogger(__name__)
os.environ['TZ'] = 'UTC'
if hasattr(time, 'tzset'):
time.tzset()
if time.tzname[0] != 'UTC':
logger.error('Timezone must be set to UTC instead of %s', time.tzname[0])
# set email encoding for utf-8 to 'quoted-printable'
charset.add_charset('utf-8', charset.QP, charset.QP)
+83
View File
@@ -0,0 +1,83 @@
# 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 sys
import os
import logging
from getpass import getpass
from sql import Table
from trytond.transaction import Transaction
from trytond import backend
from trytond.pool import Pool
__all__ = ['run']
logger = logging.getLogger(__name__)
def run(options):
Database = backend.get('Database')
init = {}
for db_name in options.database_names:
init[db_name] = False
with Transaction().start(db_name, 0):
database = Database(db_name)
database.connect()
if options.update:
if not database.test():
logger.info("init db")
database.init()
init[db_name] = True
elif not database.test():
raise Exception("'%s' is not a Tryton database!" % db_name)
for db_name in options.database_names:
if options.update:
with Transaction().start(db_name, 0) as transaction,\
transaction.connection.cursor() as cursor:
database = Database(db_name)
database.connect()
if not database.test():
raise Exception("'%s' is not a Tryton database!" % db_name)
lang = Table('ir_lang')
cursor.execute(*lang.select(lang.code,
where=lang.translatable == True))
lang = [x[0] for x in cursor.fetchall()]
else:
lang = None
Pool(db_name).init(update=options.update, lang=lang)
for db_name in options.database_names:
if init[db_name]:
# try to read password from environment variable
# TRYTONPASSFILE, empty TRYTONPASSFILE ignored
passpath = os.getenv('TRYTONPASSFILE')
password = ''
if passpath:
try:
with open(passpath) as passfile:
password = passfile.readline()[:-1]
except Exception, err:
sys.stderr.write('Can not read password '
'from "%s": "%s"\n' % (passpath, err))
if not password:
while True:
password = getpass('Admin Password for %s: ' % db_name)
password2 = getpass('Admin Password Confirmation: ')
if password != password2:
sys.stderr.write('Admin Password Confirmation '
'doesn\'t match Admin Password!\n')
continue
if not password:
sys.stderr.write('Admin Password is required!\n')
continue
break
with Transaction().start(db_name, 0) as transaction:
pool = Pool()
User = pool.get('res.user')
admin, = User.search([('login', '=', 'admin')])
User.write([admin], {
'password': password,
})
+9
View File
@@ -0,0 +1,9 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import Pool
from trytond.wsgi import app
__all__ = ['app']
Pool.start()
import trytond.protocols.dispatcher
+88
View File
@@ -0,0 +1,88 @@
# 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 argparse
import os
import logging
import logging.config
import logging.handlers
from contextlib import contextmanager
from trytond import __version__
logger = logging.getLogger(__name__)
def get_parser():
parser = argparse.ArgumentParser()
parser.add_argument('--version', action='version',
version='%(prog)s ' + __version__)
parser.add_argument("-c", "--config", dest="configfile", metavar='FILE',
default=os.environ.get('TRYTOND_CONFIG'), help="specify config file")
parser.add_argument("-v", "--verbose", action="store_true",
dest="verbose", help="enable verbose mode")
parser.add_argument('--dev', dest='dev', action='store_true',
help='enable development mode')
parser.add_argument("-d", "--database", dest="database_names", nargs='+',
default=[], metavar='DATABASE', help="specify the database name")
parser.add_argument("--logconf", dest="logconf", metavar='FILE',
help="logging configuration file (ConfigParser format)")
return parser
def get_parser_daemon():
parser = get_parser()
parser.add_argument("--pidfile", dest="pidfile", metavar='FILE',
help="file where the server pid will be stored")
return parser
def get_parser_admin():
parser = get_parser()
parser.add_argument("-u", "--update", dest="update", nargs='+', default=[],
metavar='MODULE', help="update a module")
parser.add_argument("--all", dest="update", action="append_const",
const="ir", help="update all installed modules")
parser.epilog = ('The first time a database is initialized admin '
'password is read from file defined by TRYTONPASSFILE '
'environment variable or interactively ask user.\n'
'The config file can be specified in the TRYTOND_CONFIG '
'environment variable.\n'
'The database URI can be specified in the TRYTOND_DATABASE_URI '
'environment variable.')
return parser
def config_log(options):
if options.logconf:
logging.config.fileConfig(options.logconf)
logging.getLogger('server').info('using %s as logging '
'configuration file', options.logconf)
else:
logformat = ('%(process)s %(thread)s [%(asctime)s] '
'%(levelname)s %(name)s %(message)s')
if options.verbose:
if options.dev:
level = logging.DEBUG
else:
level = logging.INFO
else:
level = logging.ERROR
logging.basicConfig(level=level, format=logformat)
logging.captureWarnings(True)
@contextmanager
def pidfile(options):
path = options.pidfile
if not path:
yield
else:
with open(path, 'w') as fd:
fd.write('%d' % os.getpid())
yield
os.unlink(path)
+7 -4
View File
@@ -3,9 +3,11 @@
import os
import ConfigParser
import urlparse
import logging
__all__ = ['config', 'get_hostname', 'get_port', 'split_netloc',
'parse_listen', 'parse_uri']
logger = logging.getLogger(__name__)
def get_hostname(netloc):
@@ -39,10 +41,9 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
def __init__(self):
ConfigParser.RawConfigParser.__init__(self)
self.add_section('jsonrpc')
self.set('jsonrpc', 'listen', 'localhost:8000')
self.set('jsonrpc', 'data', '/var/www/localhost/tryton')
self.add_section('xmlrpc')
self.add_section('web')
self.set('web', 'listen', 'localhost:8000')
self.set('web', 'root', '/var/www/localhost/tryton')
self.add_section('database')
self.set('database', 'uri',
os.environ.get('TRYTOND_DATABASE_URI', 'sqlite://'))
@@ -62,10 +63,12 @@ class TrytonConfigParser(ConfigParser.RawConfigParser):
self.add_section('report')
self.set('report', 'unoconv',
'pipe,name=trytond;urp;StarOffice.ComponentContext')
self.update_etc()
def update_etc(self, configfile=os.environ.get('TRYTOND_CONFIG')):
if not configfile:
return
logger.info('using %s as configuration file', configfile)
self.read(configfile)
def get(self, section, option, *args, **kwargs):
+43
View File
@@ -0,0 +1,43 @@
# 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 threading
import logging
from trytond.pool import Pool
from trytond.transaction import Transaction
__all__ = ['run']
logger = logging.getLogger(__name__)
def run(options):
database_list = Pool.database_list()
for dbname in options.database_names:
thread = _threads.get(dbname)
if thread and thread.is_alive():
logger.info('skip "%s" as previous cron still running', dbname)
continue
pool = Pool(dbname)
if dbname not in database_list:
with Transaction().start(dbname, 0, readonly=True):
pool.init()
if not pool.lock.acquire(0):
logger.warning('can not acquire lock on "%s"', dbname)
continue
try:
try:
Cron = pool.get('ir.cron')
except KeyError:
logger.error(
'missing "ir.cron" on "%s"', dbname, exc_info=True)
continue
finally:
pool.lock.release()
thread = threading.Thread(
target=Cron.run,
args=(dbname,), kwargs={})
logger.info('start thread for "%s"', dbname)
thread.start()
_threads[dbname] = thread
_threads = {}
Pool.start()
+8 -4
View File
@@ -2,7 +2,11 @@
# this repository contains the full copyright notices and license terms.
class UserError(Exception):
class TrytonException(Exception):
pass
class UserError(TrytonException):
def __init__(self, message, description=''):
super(UserError, self).__init__('UserError', (message, description))
@@ -11,7 +15,7 @@ class UserError(Exception):
self.code = 1
class UserWarning(Exception):
class UserWarning(TrytonException):
def __init__(self, name, message, description=''):
super(UserWarning, self).__init__('UserWarning', (name, message,
@@ -22,14 +26,14 @@ class UserWarning(Exception):
self.code = 2
class NotLogged(Exception):
class NotLogged(TrytonException):
def __init__(self):
super(NotLogged, self).__init__('NotLogged')
self.code = 3
class ConcurrencyException(Exception):
class ConcurrencyException(TrytonException):
def __init__(self, message):
super(ConcurrencyException, self).__init__('ConcurrencyException',
-87
View File
@@ -1,87 +0,0 @@
# 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 sys
import os
import subprocess
from threading import Lock
from trytond.modules import get_module_list
_LOCK = Lock()
_TIMES = {}
_MODULES = None
def _modified(path):
_LOCK.acquire()
try:
try:
if not os.path.isfile(path):
return path in _TIMES
mtime = os.stat(path).st_mtime
if path not in _TIMES:
_TIMES[path] = mtime
if mtime != _TIMES[path]:
_TIMES[path] = mtime
return True
except Exception:
return True
finally:
_LOCK.release()
return False
def monitor(files):
'''
Monitor files and module files for change
:return: True if at least one file has changed
'''
global _MODULES
modified = False
for file_ in files:
if _modified(file_):
modified = True
directories = set()
for module in sys.modules.keys():
if not module.startswith('trytond.'):
continue
if not hasattr(sys.modules[module], '__file__'):
continue
path = getattr(sys.modules[module], '__file__')
if not path:
continue
if os.path.splitext(path)[1] in ['.pyc', '.pyo', '.pyd']:
path = path[:-1]
if _modified(path):
if subprocess.call((sys.executable, '-c', 'import %s' % module),
cwd=os.path.dirname(os.path.abspath(os.path.normpath(
os.path.join(__file__, '..'))))):
modified = False
break
modified = True
# Check view XML
directory = os.path.dirname(path)
if directory not in directories:
directories.add(directory)
view_dir = os.path.join(directory, 'view')
if os.path.isdir(view_dir):
for view in os.listdir(view_dir):
view = os.path.join(view_dir, view)
if os.path.splitext(view)[1] == '.xml':
if _modified(view):
modified = True
modules = set(get_module_list())
if _MODULES is None:
_MODULES = modules
for module in modules.difference(_MODULES):
if subprocess.call((sys.executable, '-c',
'import trytond.modules.%s' % module)):
modified = False
break
modified = True
_MODULES = modules
return modified
-53
View File
@@ -1,53 +0,0 @@
# 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 errno
import os
import socket
import threading
from SocketServer import StreamRequestHandler
def endsocket(sock):
if os.name != 'nt':
try:
sock.shutdown(getattr(socket, 'SHUT_RDWR', 2))
except socket.error, e:
if e.errno != errno.ENOTCONN:
raise
sock.close()
class daemon(threading.Thread):
def __init__(self, interface, port, secure, name=None):
threading.Thread.__init__(self, name=name)
self.secure = secure
self.ipv6 = False
for family, _, _, _, _ in socket.getaddrinfo(interface or None, port,
socket.AF_UNSPEC, socket.SOCK_STREAM):
if family == socket.AF_INET6:
self.ipv6 = True
break
def stop(self):
self.server.shutdown()
self.server.socket.shutdown(socket.SHUT_RDWR)
self.server.server_close()
return
def run(self):
self.server.serve_forever()
return True
class RegisterHandlerMixin:
def setup(self):
self.server.handlers.add(self)
StreamRequestHandler.setup(self)
def finish(self):
StreamRequestHandler.finish(self)
try:
self.server.handlers.remove(self)
except KeyError:
pass
+165 -124
View File
@@ -4,7 +4,9 @@
import logging
import time
import pydoc
from functools import wraps
from werkzeug.utils import redirect
from sql import Table
from trytond.pool import Pool
@@ -17,6 +19,7 @@ from trytond.cache import Cache
from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
from trytond.tools import is_instance_method
from trytond.wsgi import app
logger = logging.getLogger(__name__)
@@ -26,131 +29,169 @@ ir_module = Table('ir_module')
res_user = Table('res_user')
def dispatch(host, port, protocol, database_name, user, session, object_type,
object_name, method, *args, **kwargs):
Database = backend.get('Database')
DatabaseOperationalError = backend.get('DatabaseOperationalError')
if object_type == 'common':
if method == 'login':
try:
database = Database(database_name).connect()
cursor = database.get_connection().cursor()
cursor.close()
except Exception:
return False
res = security.login(database_name, user, session)
with Transaction().start(database_name, 0):
Cache.clean(database_name)
Cache.resets(database_name)
msg = res and 'successful login' or 'bad login or password'
logger.info('%s \'%s\' from %s:%d using %s on database \'%s\'',
msg, user, host, port, protocol, database_name)
return res or False
elif method == 'logout':
name = security.logout(database_name, user, session)
logger.info('logout \'%s\' from %s:%d '
'using %s on database \'%s\'',
name, host, port, protocol, database_name)
return True
elif method == 'version':
return __version__
elif method == 'list_lang':
return [
('bg_BG', 'Български'),
('ca_ES', 'Català'),
('cs_CZ', 'Čeština'),
('de_DE', 'Deutsch'),
('en_US', 'English'),
('es_AR', 'Español (Argentina)'),
('es_EC', 'Español (Ecuador)'),
('es_ES', 'Español (España)'),
('es_CO', 'Español (Colombia)'),
('es_MX', 'Español (México)'),
('fr_FR', 'Français'),
('hu_HU', 'Magyar'),
('it_IT', 'Italiano'),
('lt_LT', 'Lietuvių'),
('nl_NL', 'Nederlands'),
('pt_BR', 'Português (Brasil)'),
('ru_RU', 'Russian'),
('sl_SI', 'Slovenščina'),
]
elif method == 'db_exist':
try:
database = Database(*args, **kwargs).connect()
return True
except Exception:
return False
elif method == 'list':
if not config.getboolean('database', 'list'):
raise Exception('AccessDenied')
with Transaction().start(None, 0, close=True) as transaction:
return transaction.database.list()
elif method == 'create':
return create(*args, **kwargs)
elif method == 'restore':
return restore(*args, **kwargs)
elif method == 'drop':
return drop(*args, **kwargs)
elif method == 'dump':
return dump(*args, **kwargs)
return
elif object_type == 'system':
database = Database(database_name).connect()
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:
pool.init()
if method == 'listMethods':
res = []
for type in ('model', 'wizard', 'report'):
for object_name, obj in pool.iterobject(type=type):
for method in obj.__rpc__:
res.append(type + '.' + object_name + '.' + method)
return res
elif method == 'methodSignature':
return 'signatures not supported'
elif method == 'methodHelp':
res = []
args_list = args[0].split('.')
object_type = args_list[0]
object_name = '.'.join(args_list[1:-1])
method = args_list[-1]
obj = pool.get(object_name, type=object_type)
return pydoc.getdoc(getattr(obj, method))
with Transaction().start(
database_name, request.user_id, readonly=True):
pool.init()
return func(request, pool, *args, **kwargs)
return wrapper
for count in range(config.getint('database', 'retry'), -1, -1):
try:
user = security.check(database_name, user, session)
except DatabaseOperationalError:
if count:
continue
raise
break
database_list = Pool.database_list()
pool = Pool(database_name)
if database_name not in database_list:
with Transaction().start(database_name, user,
readonly=True) as transaction:
pool.init()
obj = pool.get(object_name, type=object_type)
@app.route('/<string:database_name>/', methods=['POST'])
def rpc(request, database_name):
methods = {
'common.db.login': login,
'common.db.logout': logout,
'common.db.db_exist': db_exist,
'common.db.create': create,
'common.db.restore': restore,
'common.db.drop': drop,
'common.db.dump': dump,
'system.listMethods': list_method,
'system.methodHelp': help_method,
'system.methodSignature': lambda *a: 'signatures not supported',
}
return methods.get(request.method, _dispatch)(
request, database_name, *request.params)
def login(request, database_name, user, password):
Database = backend.get('Database')
try:
database = Database(database_name).connect()
cursor = database.get_connection().cursor()
cursor.close()
except Exception:
logger.error('fail to connect to %s', database_name, exc_info=True)
return False
session = security.login(database_name, user, password)
with Transaction().start(database_name, 0):
Cache.clean(database_name)
Cache.resets(database_name)
msg = 'successful login' if session else 'bad login or password'
logger.info('%s \'%s\' from %s using %s on database \'%s\'',
msg, user, request.remote_addr, request.scheme, database_name)
return session
@app.auth_required
def logout(request, database_name):
auth = request.authorization
name = security.logout(
database_name, auth.get('userid'), auth.get('session'))
logger.info('logout \'%s\' from %s using %s on database \'%s\'',
name, request.remote_addr, request.scheme, database_name)
return True
@app.route('/', methods=['POST'])
def root(request, *args):
methods = {
'common.server.version': lambda *a: __version__,
'common.db.list_lang': list_lang,
'common.db.list': db_list,
}
return methods[request.method](request, *request.params)
@app.route('/', methods=['GET'])
def home(request):
return redirect('/index.html') # XXX find a better way
def list_lang(*args):
return [
('bg_BG', 'Български'),
('ca_ES', 'Català'),
('cs_CZ', 'Čeština'),
('de_DE', 'Deutsch'),
('en_US', 'English'),
('es_AR', 'Español (Argentina)'),
('es_EC', 'Español (Ecuador)'),
('es_ES', 'Español (España)'),
('es_CO', 'Español (Colombia)'),
('es_MX', 'Español (México)'),
('fr_FR', 'Français'),
('hu_HU', 'Magyar'),
('it_IT', 'Italiano'),
('lt_LT', 'Lietuvių'),
('nl_NL', 'Nederlands'),
('pt_BR', 'Português (Brasil)'),
('ru_RU', 'Russian'),
('sl_SI', 'Slovenščina'),
]
def db_exist(request, database_name):
Database = backend.get('Database')
try:
Database(database_name).connect()
return True
except Exception:
return False
def db_list(*args):
if not config.getboolean('database', 'list'):
raise Exception('AccessDenied')
with Transaction().start(None, 0, close=True) as transaction:
return transaction.database.list()
@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.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):
DatabaseOperationalError = backend.get('DatabaseOperationalError')
obj, method = get_object_method(request, pool)
if method in obj.__rpc__:
rpc = obj.__rpc__[method]
else:
raise UserError('Calling method %s on %s %s is not allowed!'
% (method, object_type, object_name))
log_message = '%s.%s.%s(*%s, **%s) from %s@%s:%d/%s'
log_args = (object_type, object_name, method, args, kwargs,
user, host, port, database_name)
raise UserError('Calling method %s on %s is not allowed'
% (method, obj))
log_message = '%s.%s(*%s, **%s) from %s@%s/%s'
log_args = (obj, method, args, kwargs,
request.authorization.username, request.remote_addr, request.path)
logger.info(log_message, *log_args)
user = request.user_id
for count in range(config.getint('database', 'retry'), -1, -1):
with Transaction().start(database_name, user,
with Transaction().start(pool.database_name, user,
readonly=rpc.readonly) as transaction:
Cache.clean(database_name)
Cache.clean(pool.database_name)
try:
c_args, c_kwargs, transaction.context, transaction.timestamp \
= rpc.convert(obj, *args, **kwargs)
@@ -177,19 +218,19 @@ def dispatch(host, port, protocol, database_name, user, session, object_type,
except Exception:
logger.error(log_message, *log_args, exc_info=True)
raise
Cache.resets(database_name)
try:
with Transaction().start(database_name, 0) as transaction:
pool = Pool(database_name)
Session = pool.get('ir.session')
Session.reset(session)
except DatabaseOperationalError:
logger.debug('Reset session failed', exc_info=True)
Cache.resets(pool.database_name)
if request.authorization.type == 'session':
try:
with Transaction().start(pool.database_name, 0) as transaction:
Session = pool.get('ir.session')
Session.reset(request.authorization.get('session'))
except DatabaseOperationalError:
logger.debug('Reset session failed', exc_info=True)
logger.debug('Result: %s', result)
return result
def create(database_name, password, lang, admin_password):
def create(request, database_name, password, lang, admin_password):
'''
Create a database
@@ -242,7 +283,7 @@ def create(database_name, password, lang, admin_password):
return res
def drop(database_name, password):
def drop(request, database_name, password):
Database = backend.get('Database')
security.check_super(password)
database = Database(database_name)
@@ -264,7 +305,7 @@ def drop(database_name, password):
return True
def dump(database_name, password):
def dump(request, database_name, password):
Database = backend.get('Database')
security.check_super(password)
Database(database_name).close()
@@ -279,7 +320,7 @@ def dump(database_name, password):
return bytes(data)
def restore(database_name, password, data, update=False):
def restore(request, database_name, password, data, update=False):
Database = backend.get('Database')
security.check_super(password)
try:
+43 -275
View File
@@ -1,25 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.config import config
from trytond.protocols.common import daemon, RegisterHandlerMixin
from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
import SimpleXMLRPCServer
import SimpleHTTPServer
import SocketServer
import traceback
import socket
import sys
import os
try:
import fcntl
except ImportError:
fcntl = None
import posixpath
import urllib
import datetime
from decimal import Decimal
try:
@@ -27,8 +9,13 @@ try:
except ImportError:
import json
import base64
import encodings
from io import BytesIO
from werkzeug.wrappers import Response
from werkzeug.utils import cached_property
from werkzeug.exceptions import BadRequest
from trytond.protocols.wrappers import Request
from trytond.exceptions import TrytonException
class JSONDecoder(object):
@@ -127,269 +114,50 @@ JSONEncoder.register(Decimal,
})
class SimpleJSONRPCDispatcher(SimpleXMLRPCServer.SimpleXMLRPCDispatcher):
"""Mix-in class that dispatches JSON-RPC requests.
class JSONRequest(Request):
parsed_content_type = 'json'
This class is used to register JSON-RPC method handlers
and then to dispatch them. There should never be any
reason to instantiate this class directly.
"""
@cached_property
def parsed_data(self):
if self.parsed_content_type in self.environ.get('CONTENT_TYPE', ''):
try:
return json.loads(self.decoded_data, object_hook=JSONDecoder())
except Exception:
raise BadRequest('Unable to read JSON request')
else:
raise BadRequest('Not a JSON request')
def _marshaled_dispatch(self, data, dispatch_method=None, path=None):
"""Dispatches an JSON-RPC method from marshalled (JSON) data.
@cached_property
def method(self):
return self.parsed_data['method']
JSON-RPC methods are dispatched from the marshalled (JSON) data
using the _dispatch method and the result is returned as
marshalled data. For backwards compatibility, a dispatch
function can be provided as an argument (see comment in
SimpleJSONRPCRequestHandler.do_POST) but overriding the
existing method through subclassing is the prefered means
of changing method dispatch behavior.
"""
rawreq = json.loads(data.decode('utf-8'), object_hook=JSONDecoder())
@cached_property
def params(self):
return self.parsed_data['params']
req_id = rawreq.get('id', 0)
method = rawreq['method']
params = rawreq.get('params', [])
response = {'id': req_id}
class JSONProtocol:
content_type = 'json'
try:
# generate response
if dispatch_method is not None:
response['result'] = dispatch_method(method, params)
else:
response['result'] = self._dispatch(method, params)
except (UserError, UserWarning, NotLogged,
ConcurrencyException), exception:
response['error'] = exception.args
except Exception:
@classmethod
def request(cls, environ):
return JSONRequest(environ)
@classmethod
def response(cls, data, request):
if isinstance(request, JSONRequest):
response = {'id': request.parsed_data.get('id', 0)}
else:
response = {}
if isinstance(data, TrytonException):
response['error'] = data.args
elif isinstance(data, Exception):
tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
for path in sys.path:
tb_s = tb_s.replace(path, '')
# report exception back to server
response['error'] = (str(sys.exc_value), tb_s)
return json.dumps(response, cls=JSONEncoder).encode('utf-8')
class GenericJSONRPCRequestHandler:
def _dispatch(self, method, params):
host, port = self.client_address[:2]
database_name = self.path[1:]
if database_name.startswith('sao/'):
database_name = database_name[4:]
method_list = method.split('.')
object_type = method_list[0]
object_name = '.'.join(method_list[1:-1])
method = method_list[-1]
args = (host, port, 'JSON-RPC', database_name, params[0], params[1],
object_type, object_name, method) + tuple(params[2:])
res = dispatch(*args)
return res
class SimpleJSONRPCRequestHandler(RegisterHandlerMixin,
GenericJSONRPCRequestHandler,
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler,
SimpleHTTPServer.SimpleHTTPRequestHandler):
"""Simple JSON-RPC request handler class.
Handles all HTTP POST requests and attempts to decode them as
JSON-RPC requests.
"""
protocol_version = "HTTP/1.1"
rpc_paths = None
encode_threshold = 1400 # common MTU
def send_header(self, keyword, value):
if keyword == 'Content-type' and value == 'text/xml':
value = 'application/json-rpc'
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.send_header(self,
keyword, value)
def do_GET(self):
if self.is_tryton_url(self.path):
self.send_tryton_url(self.path)
return
SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
def do_HEAD(self):
if self.is_tryton_url(self.path):
self.send_tryton_url(self.path)
return
SimpleHTTPServer.SimpleHTTPRequestHandler.do_HEAD(self)
def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax.
Components that mean special things to the local file system
(e.g. drive or directory names) are ignored. (XXX They should
probably be diagnosed.)
"""
# abandon query parameters
path = path.split('?', 1)[0]
path = path.split('#', 1)[0]
path = posixpath.normpath(urllib.unquote(path))
words = path.split('/')
words = filter(None, words)
path = config.get('jsonrpc', 'data')
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
if word in (os.curdir, os.pardir):
continue
path = os.path.join(path, word)
return path
def is_tryton_url(self, path):
words = path.split('/')
try:
return words[2] in ('model', 'wizard', 'report')
except IndexError:
return False
def send_tryton_url(self, path):
self.send_response(300)
hostname = (config.get('jsonrpc', 'hostname')
or unicode(socket.getfqdn(), 'utf8'))
hostname = '.'.join(encodings.idna.ToASCII(part) for part in
hostname.split('.'))
values = {
'hostname': hostname,
'path': path,
}
content = BytesIO()
def write(str_):
content.write(str_.encode('utf-8'))
write('<html')
write('<head>')
write('<meta http-equiv="Refresh" '
'content="0;url=tryton://%(hostname)s%(path)s"/>' % values)
write('<title>Moved</title>')
write('</head>')
write('<body>')
write('<h1>Moved</h1>')
write('<p>This page has moved to '
'<a href="tryton://%(hostname)s%(path)s">'
'tryton://%(hostname)s%(path)s</a>.</p>' % values)
write('</body>')
write('</html>')
length = content.tell()
content.seek(0)
self.send_header('Location', 'tryton://%(hostname)s%(path)s' % values)
self.send_header('Content-type', 'text/html')
self.send_header('Content-Length', str(length))
self.end_headers()
self.copyfile(content, self.wfile)
content.close()
SimpleJSONRPCRequestHandler.extensions_map.update({
'.svg': 'image/svg+xml',
})
class SecureJSONRPCRequestHandler(SimpleJSONRPCRequestHandler):
def setup(self):
self.request = SSLSocket(self.request)
SimpleJSONRPCRequestHandler.setup(self)
class SimpleJSONRPCServer(SocketServer.TCPServer,
SimpleJSONRPCDispatcher):
"""Simple JSON-RPC server.
Simple JSON-RPC server that allows functions and a single instance
to be installed to handle requests. The default implementation
attempts to dispatch JSON-RPC calls to the functions or instance
installed in the server. Override the _dispatch method inhereted
from SimpleJSONRPCDispatcher to change this behavior.
"""
allow_reuse_address = True
# Warning: this is for debugging purposes only! Never set this to True in
# production code, as will be sending out sensitive information (exception
# and stack trace details) when exceptions are raised inside
# SimpleJSONRPCRequestHandler.do_POST
_send_traceback_header = False
def __init__(self, addr, requestHandler=SimpleJSONRPCRequestHandler,
logRequests=True, allow_none=False, encoding=None,
bind_and_activate=True):
self.handlers = set()
self.logRequests = logRequests
SimpleJSONRPCDispatcher.__init__(self, allow_none, encoding)
try:
SocketServer.TCPServer.__init__(self, addr, requestHandler,
bind_and_activate)
except TypeError:
SocketServer.TCPServer.__init__(self, addr, requestHandler)
# [Bug #1222790] If possible, set close-on-exec flag; if a
# method spawns a subprocess, the subprocess shouldn't have
# the listening socket open.
if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
flags |= fcntl.FD_CLOEXEC
fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
def server_close(self):
SocketServer.TCPServer.server_close(self)
for handler in self.handlers.copy():
self.shutdown_request(handler.request)
class SimpleThreadedJSONRPCServer(SocketServer.ThreadingMixIn,
SimpleJSONRPCServer):
timeout = 1
daemon_threads = True
disable_nagle_algorithm = True
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
self.socket.setsockopt(socket.SOL_SOCKET,
socket.SO_KEEPALIVE, 1)
SimpleJSONRPCServer.server_bind(self)
class SimpleThreadedJSONRPCServer6(SimpleThreadedJSONRPCServer):
address_family = socket.AF_INET6
class SecureThreadedJSONRPCServer(SimpleThreadedJSONRPCServer):
def __init__(self, server_address, HandlerClass, logRequests=1):
SimpleThreadedJSONRPCServer.__init__(self, server_address,
HandlerClass, logRequests)
self.socket = socket.socket(self.address_family,
self.socket_type)
self.server_bind()
self.server_activate()
class SecureThreadedJSONRPCServer6(SecureThreadedJSONRPCServer):
address_family = socket.AF_INET6
class JSONRPCDaemon(daemon):
def __init__(self, interface, port, secure=False):
daemon.__init__(self, interface, port, secure, name='JSONRPCDaemon')
if self.secure:
handler_class = SecureJSONRPCRequestHandler
server_class = SecureThreadedJSONRPCServer
if self.ipv6:
server_class = SecureThreadedJSONRPCServer6
response['error'] = (str(data), tb_s)
else:
handler_class = SimpleJSONRPCRequestHandler
server_class = SimpleThreadedJSONRPCServer
if self.ipv6:
server_class = SimpleThreadedJSONRPCServer6
self.server = server_class((interface, port), handler_class, 0)
response['result'] = data
return Response(json.dumps(response, cls=JSONEncoder),
content_type='application/json')
-13
View File
@@ -1,13 +0,0 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.config import config
def SSLSocket(socket):
# Let the import error raise only when used
import ssl
return ssl.wrap_socket(socket,
server_side=True,
certfile=config.get('ssl', 'certificate'),
keyfile=config.get('ssl', 'privatekey'),
ssl_version=ssl.PROTOCOL_SSLv23)
+87
View File
@@ -0,0 +1,87 @@
# 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
from io import BytesIO
from werkzeug.wrappers import Request as _Request
from werkzeug.utils import cached_property
from werkzeug.http import wsgi_to_bytes, bytes_to_wsgi
from werkzeug.datastructures import Authorization
from werkzeug.exceptions import abort
from trytond import security
class Request(_Request):
view_args = None
@property
def decoded_data(self):
if self.content_encoding == 'gzip':
zipfile = gzip.GzipFile(fileobj=BytesIO(self.data), mode='rb')
return zipfile.read()
else:
return self.data
@property
def parsed_data(self):
return self.data
@property
def method(self):
return
@property
def params(self):
return
@cached_property
def authorization(self):
authorization = super(Request, self).authorization
if authorization is None:
header = self.environ.get('HTTP_AUTHORIZATION')
return parse_authorization_header(header)
return authorization
@cached_property
def user_id(self):
database_name = self.view_args['database_name']
auth = self.authorization
if not auth:
abort(401)
if auth.type == 'session':
user_id = security.check(
database_name, auth.get('userid'), auth.get('session'))
if not user_id:
abort(403)
else:
user_id = security.login(
database_name, auth.username, auth.password, cache=False)
if not user_id:
abort(401)
return user_id
def parse_authorization_header(value):
if not value:
return
value = wsgi_to_bytes(value)
try:
auth_type, auth_info = value.split(None, 1)
auth_type = auth_type.lower()
except ValueError:
return
if auth_type == b'session':
try:
username, userid, session = base64.b64decode(auth_info).split(
b':', 3)
userid = int(userid)
except Exception:
return
return Authorization('session', {
'username': bytes_to_wsgi(username),
'userid': userid,
'session': bytes_to_wsgi(session),
})
+45 -138
View File
@@ -1,22 +1,19 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.protocols.sslsocket import SSLSocket
from trytond.protocols.dispatcher import dispatch
from trytond.protocols.common import daemon, RegisterHandlerMixin
from trytond.exceptions import UserError, UserWarning, NotLogged, \
ConcurrencyException
from trytond import security
import SimpleXMLRPCServer
import SocketServer
import xmlrpclib as client
import socket
import base64
import datetime
import logging
# convert decimal to float before marshalling:
from decimal import Decimal
from werkzeug.wrappers import Response
from werkzeug.utils import cached_property
from werkzeug.exceptions import BadRequest
from trytond.protocols.wrappers import Request
from trytond.exceptions import TrytonException
logger = logging.getLogger(__name__)
@@ -138,134 +135,44 @@ if bytes == str:
client.Unmarshaller.dispatch['base64'] = _end_base64
class GenericXMLRPCRequestHandler:
class XMLRequest(Request):
parsed_content_type = 'xml'
def _dispatch(self, method, params):
host, port = self.client_address[:2]
database_name = self.path[1:]
user = self.tryton['user']
session = self.tryton['session']
exception_message = 'Exception calling %s%s' % (method, params)
try:
@cached_property
def parsed_data(self):
if self.parsed_content_type in self.environ.get('CONTENT_TYPE', ''):
try:
method_list = method.split('.')
object_type = method_list[0]
object_name = '.'.join(method_list[1:-1])
method = method_list[-1]
if object_type == 'system' and method == 'getCapabilities':
return {
'introspect': {
'specUrl': ('http://xmlrpc-c.sourceforge.net/'
'xmlrpc-c/introspection.html'),
'specVersion': 1,
},
}
return dispatch(host, port, 'XML-RPC', database_name, user,
session, object_type, object_name, method, *params)
except (NotLogged, ConcurrencyException), exception:
logger.debug(exception_message, exc_info=True)
raise client.Fault(exception.code, str(exception))
except (UserError, UserWarning), exception:
logger.debug(exception_message, exc_info=True)
error, description = exception.args
raise client.Fault(exception.code, str(exception))
except Exception, exception:
logger.error(exception_message, exc_info=True)
raise client.Fault(255, str(exception))
finally:
security.logout(database_name, user, session)
class SimpleXMLRPCRequestHandler(RegisterHandlerMixin,
GenericXMLRPCRequestHandler,
SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
protocol_version = "HTTP/1.1"
rpc_paths = None
encode_threshold = 1400 # common MTU
def parse_request(self):
res = SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.parse_request(self)
if not res:
return res
database_name = self.path[1:]
if not database_name:
self.tryton = {'user': None, 'session': None}
return res
try:
method, up64 = self.headers['Authorization'].split(None, 1)
if method.strip().lower() == 'basic':
user, password = base64.decodestring(up64).split(':', 1)
user_id, session = security.login(database_name, user,
password)
self.tryton = {'user': user_id, 'session': session}
return res
except Exception:
pass
self.send_error(401, 'Unauthorized')
self.send_header("WWW-Authenticate", 'Basic realm="Tryton"')
return False
class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
def setup(self):
self.request = SSLSocket(self.request)
SimpleXMLRPCRequestHandler.setup(self)
class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn,
SimpleXMLRPCServer.SimpleXMLRPCServer):
timeout = 1
daemon_threads = True
def __init__(self, server_address, HandlerClass, logRequests=1):
self.handlers = set()
SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, server_address,
HandlerClass, logRequests)
def server_bind(self):
self.socket.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
self.socket.setsockopt(socket.SOL_SOCKET,
socket.SO_KEEPALIVE, 1)
SimpleXMLRPCServer.SimpleXMLRPCServer.server_bind(self)
def server_close(self):
SimpleXMLRPCServer.SimpleXMLRPCServer.server_close(self)
for handler in self.handlers.copy():
self.shutdown_request(handler.request)
class SimpleThreadedXMLRPCServer6(SimpleThreadedXMLRPCServer):
address_family = socket.AF_INET6
class SecureThreadedXMLRPCServer(SimpleThreadedXMLRPCServer):
def __init__(self, server_address, HandlerClass, logRequests=1):
SimpleThreadedXMLRPCServer.__init__(self, server_address, HandlerClass,
logRequests)
self.socket = socket.socket(self.address_family, self.socket_type)
self.server_bind()
self.server_activate()
class SecureThreadedXMLRPCServer6(SecureThreadedXMLRPCServer):
address_family = socket.AF_INET6
class XMLRPCDaemon(daemon):
def __init__(self, interface, port, secure=False):
daemon.__init__(self, interface, port, secure, name='XMLRPCDaemon')
if self.secure:
handler_class = SecureXMLRPCRequestHandler
server_class = SecureThreadedXMLRPCServer
if self.ipv6:
server_class = SecureThreadedXMLRPCServer6
# TODO replace by own loads
return client.loads(self.decoded_data)
except Exception:
raise BadRequest('Unable to read XMl request')
else:
handler_class = SimpleXMLRPCRequestHandler
server_class = SimpleThreadedXMLRPCServer
if self.ipv6:
server_class = SimpleThreadedXMLRPCServer6
self.server = server_class((interface, port), handler_class, 0)
raise BadRequest('Not an XML request')
@property
def method(self):
return self.parsed_data[1]
@property
def 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(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')
+18 -16
View File
@@ -8,7 +8,7 @@ except ImportError:
from trytond.pool import Pool
from trytond.config import config
from trytond.transaction import Transaction
from trytond.exceptions import NotLogged
from trytond import backend
def _get_pool(dbname):
@@ -31,7 +31,7 @@ def login(dbname, loginname, password, cache=True):
Session = pool.get('ir.session')
session, = Session.create([{}])
return user_id, session.key
return False
return
def logout(dbname, user, session):
@@ -57,17 +57,19 @@ def check_super(passwd):
def check(dbname, user, session):
if user == 0:
raise Exception('AccessDenied')
if not user:
raise NotLogged()
with Transaction().start(dbname, user) as transaction:
pool = _get_pool(dbname)
Session = pool.get('ir.session')
try:
if not Session.check(user, session):
raise NotLogged()
else:
return user
finally:
transaction.commit()
DatabaseOperationalError = backend.get('DatabaseOperationalError')
for count in range(config.getint('database', 'retry'), -1, -1):
with Transaction().start(dbname, user) as transaction:
pool = _get_pool(dbname)
Session = pool.get('ir.session')
try:
if not Session.check(user, session):
return
else:
return user
except DatabaseOperationalError:
if count:
continue
raise
finally:
transaction.commit()
-229
View File
@@ -1,229 +0,0 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
"""
%prog [options]
"""
import logging
import logging.config
import logging.handlers
import sys
import os
import signal
import time
from getpass import getpass
import threading
from sql import Table
from trytond.config import config, parse_listen
from trytond import backend
from trytond.pool import Pool
from trytond.monitor import monitor
from .transaction import Transaction
class TrytonServer(object):
def __init__(self, options):
config.update_etc(options.configfile)
if options.logconf:
logging.config.fileConfig(options.logconf)
logging.getLogger('server').info('using %s as logging '
'configuration file', options.logconf)
else:
logformat = ('%(process)s %(thread)s [%(asctime)s] '
'%(levelname)s %(name)s %(message)s')
if options.verbose:
if options.dev:
level = logging.DEBUG
else:
level = logging.INFO
else:
level = logging.ERROR
logging.basicConfig(level=level, format=logformat)
logging.captureWarnings(True)
self.logger = logging.getLogger(__name__)
if options.configfile:
self.logger.info('using %s as configuration file',
options.configfile)
else:
self.logger.info('using default configuration')
self.logger.info('initialising distributed objects services')
self.xmlrpcd = []
self.jsonrpcd = []
self.options = options
if time.tzname[0] != 'UTC':
self.logger.error('timezone is not set to UTC')
def run(self):
"Run the server and never return"
init = {}
signal.signal(signal.SIGINT, lambda *a: self.stop())
signal.signal(signal.SIGTERM, lambda *a: self.stop())
if hasattr(signal, 'SIGQUIT'):
signal.signal(signal.SIGQUIT, lambda *a: self.stop())
if hasattr(signal, 'SIGUSR1'):
signal.signal(signal.SIGUSR1, lambda *a: self.restart())
if self.options.pidfile:
with open(self.options.pidfile, 'w') as fd_pid:
fd_pid.write("%d" % (os.getpid()))
if not self.options.update:
self.start_servers()
Database = backend.get('Database')
for db_name in self.options.database_names:
init[db_name] = False
try:
with Transaction().start(db_name, 0) as transaction:
database = Database(db_name)
database.connect()
if self.options.update:
if not database.test():
self.logger.info("init db")
database.init()
init[db_name] = True
elif not database.test():
raise Exception("'%s' is not a Tryton database!" %
db_name)
except Exception:
self.stop(False)
raise
for db_name in self.options.database_names:
if self.options.update:
with Transaction().start(db_name, 0) as transaction,\
transaction.connection.cursor() as cursor:
database = Database(db_name)
database.connect()
if not database.test():
raise Exception("'%s' is not a Tryton database!"
% db_name)
lang = Table('ir_lang')
cursor.execute(*lang.select(lang.code,
where=lang.translatable == True))
lang = [x[0] for x in cursor.fetchall()]
else:
lang = None
Pool(db_name).init(update=self.options.update, lang=lang)
for db_name in self.options.database_names:
if init[db_name]:
# try to read password from environment variable
# TRYTONPASSFILE, empty TRYTONPASSFILE ignored
passpath = os.getenv('TRYTONPASSFILE')
password = ''
if passpath:
try:
with open(passpath) as passfile:
password = passfile.readline()[:-1]
except Exception, err:
sys.stderr.write('Can not read password '
'from "%s": "%s"\n' % (passpath, err))
if not password:
while True:
password = getpass('Admin Password for %s: ' % db_name)
password2 = getpass('Admin Password Confirmation: ')
if password != password2:
sys.stderr.write('Admin Password Confirmation '
'doesn\'t match Admin Password!\n')
continue
if not password:
sys.stderr.write('Admin Password is required!\n')
continue
break
with Transaction().start(db_name, 0) as transaction:
pool = Pool()
User = pool.get('res.user')
admin, = User.search([('login', '=', 'admin')])
User.write([admin], {
'password': password,
})
if self.options.update:
self.logger.info('Update/Init succeed!')
logging.shutdown()
sys.exit(0)
threads = {}
while True:
if self.options.cron:
for dbname in Pool.database_list():
thread = threads.get(dbname)
if thread and thread.is_alive():
continue
pool = Pool(dbname)
if not pool.lock.acquire(0):
continue
try:
try:
Cron = pool.get('ir.cron')
except KeyError:
continue
finally:
pool.lock.release()
thread = threading.Thread(
target=Cron.run,
args=(dbname,), kwargs={})
thread.start()
threads[dbname] = thread
if self.options.dev:
for _ in range(60):
if monitor([self.options.configfile]
if self.options.configfile else []):
self.restart()
time.sleep(1)
else:
time.sleep(60)
def start_servers(self):
ssl = config.get('ssl', 'privatekey')
# Launch Server
if config.get('jsonrpc', 'listen'):
from trytond.protocols.jsonrpc import JSONRPCDaemon
for hostname, port in parse_listen(
config.get('jsonrpc', 'listen')):
self.jsonrpcd.append(JSONRPCDaemon(hostname, port, ssl))
self.logger.info("starting JSON-RPC%s protocol on %s:%d",
ssl and ' SSL' or '', hostname or '*', port)
if config.get('xmlrpc', 'listen'):
from trytond.protocols.xmlrpc import XMLRPCDaemon
for hostname, port in parse_listen(
config.get('xmlrpc', 'listen')):
self.xmlrpcd.append(XMLRPCDaemon(hostname, port, ssl))
self.logger.info("starting XML-RPC%s protocol on %s:%d",
ssl and ' SSL' or '', hostname or '*', port)
for servers in (self.xmlrpcd, self.jsonrpcd):
for server in servers:
server.start()
def stop(self, exit=True):
for servers in (self.xmlrpcd, self.jsonrpcd):
for server in servers:
server.stop()
server.join()
if exit:
if self.options.pidfile:
os.unlink(self.options.pidfile)
logging.getLogger('server').info('stopped')
logging.shutdown()
sys.exit(0)
def restart(self):
self.stop(False)
args = ([sys.executable] + ['-W%s' % o for o in sys.warnoptions]
+ sys.argv)
if sys.platform == "win32":
args = ['"%s"' % arg for arg in args]
os.execv(sys.executable, args)
+2 -2
View File
@@ -280,12 +280,12 @@ def db_exist():
def create_db():
if not db_exist():
create(DB_NAME, None, 'en_US', USER_PASSWORD)
create(None, DB_NAME, None, 'en_US', USER_PASSWORD)
def drop_db():
if db_exist():
drop(DB_NAME, None)
drop(None, DB_NAME, None)
def drop_create():
+1 -1
View File
@@ -120,7 +120,7 @@ class Transaction(object):
self.delete = None
self.timestamp = None
current_instance = transactions.pop()
assert current_instance is self
assert current_instance is self, transactions
def set_context(self, context=None, **kwargs):
if context is None:
+1 -1
View File
@@ -10,7 +10,7 @@ from trytond.transaction import Transaction
__all__ = ['URLMixin']
HOSTNAME = (config.get('jsonrpc', 'hostname')
HOSTNAME = (config.get('web', 'hostname')
or socket.getfqdn())
HOSTNAME = '.'.join(encodings.idna.ToASCII(part).decode('ascii')
if part else '' for part in HOSTNAME.split('.'))
+86
View File
@@ -0,0 +1,86 @@
# 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
from werkzeug.wrappers import Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import abort
import wrapt
from trytond.protocols.wrappers import Request
from trytond.protocols.jsonrpc import JSONProtocol
from trytond.protocols.xmlrpc import XMLProtocol
__all__ = ['TrytondWSGI', 'app']
logger = logging.getLogger(__name__)
class TrytondWSGI(object):
def __init__(self):
self.url_map = Map([])
self.protocols = [JSONProtocol, XMLProtocol]
self.error_handlers = []
def route(self, string, methods=None):
def decorator(func):
self.url_map.add(Rule(string, endpoint=func, methods=methods))
return func
return decorator
@wrapt.decorator
def auth_required(self, wrapped, instance, args, kwargs):
request = args[0]
if request.user_id:
return wrapped(*args, **kwargs)
else:
abort(303)
def dispatch_request(self, request):
adapter = self.url_map.bind_to_environ(request.environ)
try:
endpoint, request.view_args = adapter.match()
return endpoint(request, **request.view_args)
except Exception, e:
response = e
for error_handler in self.error_handlers:
rv = error_handler(e)
if isinstance(rv, Response):
response = rv
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)
data = self.dispatch_request(request)
if not isinstance(data, Response):
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 environ.get('CONTENT_TYPE', ''):
response = cls.response(data, request)
break
else:
response = Response(data)
else:
response = data
# TODO custom process response
return response(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
app = TrytondWSGI()