[IMP] http move db dispatching on the orm level

Split low level dispatching and high level dispatching.
Low level dispatching is used when the db is unknown it's only used by a few
controller in base and web.
High level dispatching is used when the db is known, it is used by most
controllers and it handles authentication and errors. Because it's a regular
osv object all it is fully overridable by openerp modules.

bzr revid: al@openerp.com-20131110014609-io03vspj2q1wtqa0
This commit is contained in:
Antony Lesuisse 2013-11-10 02:46:09 +01:00
parent c9a7e69a75
commit d50577b69d
3 changed files with 241 additions and 209 deletions

View File

@ -37,6 +37,7 @@ import ir_config_parameter
import osv_memory_autovacuum import osv_memory_autovacuum
import ir_mail_server import ir_mail_server
import ir_fields import ir_fields
import ir_http
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,95 @@
#----------------------------------------------------------
# ir_http modular http routing
#----------------------------------------------------------
import logging
import werkzeug.exceptions
import werkzeug.routing
import openerp
from openerp import http
from openerp.http import request
from openerp.osv import osv
_logger = logging.getLogger(__name__)
class ir_http(osv.osv):
_name = 'ir.http'
_description = "HTTP routing"
def __init__(self, registry, cr):
osv.osv.__init__(self, registry, cr)
def _find_handler(self):
# TODO move to __init__(self, registry, cr)
if not hasattr(self, 'routing_map'):
_logger.info("Generating routing map")
cr = request.cr
m = request.registry.get('ir.module.module')
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')])
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name']))
mods = ['', "web"] + sorted(installed)
self.routing_map = http.routing_map(mods, False)
# fallback to non-db handlers
path = request.httprequest.path
urls = self.routing_map.bind_to_environ(request.httprequest.environ)
return urls.match(path)
def _auth_method_user(self):
request.uid = request.session.uid
if not request.uid:
raise SessionExpiredException("Session expired")
def _auth_method_admin(self):
if not request.db:
raise SessionExpiredException("No valid database for request %s" % request.httprequest)
request.uid = openerp.SUPERUSER_ID
def _auth_method_none(self):
request.disable_db = True
request.uid = None
def _authenticate(self, func, arguments):
auth_method = getattr(func, "auth", "user")
if request.session.uid:
try:
request.session.check_security()
except SessionExpiredException, e:
request.session.logout()
raise SessionExpiredException("Session expired for request %s" % request.httprequest)
getattr(self, "_auth_method_%s" % auth_method)()
return auth_method
def _handle_404(self, exception):
raise exception
def _handle_403(self, exception):
raise exception
def _handle_500(self, exception):
raise exception
def _dispatch(self):
# locate the controller method
try:
func, arguments = self._find_handler()
except werkzeug.exceptions.NotFound, e:
return self._handle_404(e)
# check authentication level
try:
auth_method = self._authenticate(func, arguments)
except werkzeug.exceptions.NotFound, e:
return self._handle_403(e)
# set and execute handler
try:
request.set_handler(func, arguments, auth_method)
result = request.dispatch()
except Exception, e:
return self._handle_500(e)
return result
# vim:et:

View File

@ -27,9 +27,10 @@ import simplejson
import werkzeug.contrib.sessions import werkzeug.contrib.sessions
import werkzeug.datastructures import werkzeug.datastructures
import werkzeug.exceptions import werkzeug.exceptions
import werkzeug.local
import werkzeug.routing
import werkzeug.wrappers import werkzeug.wrappers
import werkzeug.wsgi import werkzeug.wsgi
import werkzeug.routing as routing
import openerp import openerp
from openerp.service import security, model as service_model from openerp.service import security, model as service_model
@ -111,14 +112,6 @@ class WebRequest(object):
self.context = dict(self.session.context) self.context = dict(self.session.context)
self.lang = self.context["lang"] self.lang = self.context["lang"]
def _authenticate(self):
if self.session.uid:
try:
self.session.check_security()
except SessionExpiredException, e:
self.session.logout()
raise SessionExpiredException("Session expired for request %s" % self.httprequest)
auth_methods[self.auth_method]()
@property @property
def registry(self): def registry(self):
""" """
@ -147,8 +140,16 @@ class WebRequest(object):
self._cr = self._cr_cm.__enter__() self._cr = self._cr_cm.__enter__()
return self._cr return self._cr
def set_handler(self, func, arguments, auth):
# is this needed ?
arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")])
self.func = func
self.func_request_type = func.exposed
self.func_arguments = arguments
self.auth_method = auth
def _call_function(self, *args, **kwargs): def _call_function(self, *args, **kwargs):
self._authenticate()
try: try:
# ugly syntax only to get the __exit__ arguments to pass to self._cr # ugly syntax only to get the __exit__ arguments to pass to self._cr
request = self request = self
@ -165,6 +166,13 @@ class WebRequest(object):
if self.func_request_type != self._request_type: if self.func_request_type != self._request_type:
raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \ raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \
% (self.func, self.httprequest.path, self.func_request_type, self._request_type)) % (self.func, self.httprequest.path, self.func_request_type, self._request_type))
# Backward for 7.0
if getattr(self.func, '_first_arg_is_req', False):
args = (request,) + args
# TODO by chs
#@service_model.check
#def checked_call(dbname, *a, **kw):
# return func(*a, **kw)
return self.func(*args, **kwargs) return self.func(*args, **kwargs)
finally: finally:
# just to be sure no one tries to re-use the request # just to be sure no one tries to re-use the request
@ -180,26 +188,6 @@ class WebRequest(object):
warnings.warn('please use request.registry and request.cr directly', DeprecationWarning) warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
yield (self.registry, self.cr) yield (self.registry, self.cr)
def auth_method_user():
request.uid = request.session.uid
if not request.uid:
raise SessionExpiredException("Session expired")
def auth_method_admin():
if not request.db:
raise SessionExpiredException("No valid database for request %s" % request.httprequest)
request.uid = openerp.SUPERUSER_ID
def auth_method_none():
request.disable_db = True
request.uid = None
auth_methods = {
"user": auth_method_user,
"admin": auth_method_admin,
"none": auth_method_none,
}
def route(route, type="http", auth="user"): def route(route, type="http", auth="user"):
""" """
Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
@ -219,7 +207,6 @@ def route(route, type="http", auth="user"):
configuration indicating the current database nor the current user. configuration indicating the current database nor the current user.
""" """
assert type in ["http", "json"] assert type in ["http", "json"]
assert auth in auth_methods.keys()
def decorator(f): def decorator(f):
if isinstance(route, list): if isinstance(route, list):
f.routes = route f.routes = route
@ -231,12 +218,6 @@ def route(route, type="http", auth="user"):
return f return f
return decorator return decorator
def reject_nonliteral(dct):
if '__ref' in dct:
raise ValueError(
"Non literal contexts can not be sent to the server anymore (%r)" % (dct,))
return dct
class JsonRequest(WebRequest): class JsonRequest(WebRequest):
""" JSON-RPC2 over HTTP. """ JSON-RPC2 over HTTP.
@ -302,7 +283,7 @@ class JsonRequest(WebRequest):
request = self.httprequest.stream.read() request = self.httprequest.stream.read()
# Read POST content or POST Form Data named "request" # Read POST content or POST Form Data named "request"
self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral) self.jsonrequest = simplejson.loads(request)
self.params = dict(self.jsonrequest.get("params", {})) self.params = dict(self.jsonrequest.get("params", {}))
self.context = self.params.pop('context', self.session.context) self.context = self.params.pop('context', self.session.context)
@ -464,9 +445,7 @@ def httprequest(f):
#---------------------------------------------------------- #----------------------------------------------------------
# Thread local global request object # Thread local global request object
#---------------------------------------------------------- #----------------------------------------------------------
from werkzeug.local import LocalStack _request_stack = werkzeug.local.LocalStack()
_request_stack = LocalStack()
request = _request_stack() request = _request_stack()
""" """
@ -482,7 +461,7 @@ def set_request(req):
_request_stack.pop() _request_stack.pop()
#---------------------------------------------------------- #----------------------------------------------------------
# Controller metaclass registration # Controller and route registration
#---------------------------------------------------------- #----------------------------------------------------------
addons_module = {} addons_module = {}
addons_manifest = {} addons_manifest = {}
@ -516,6 +495,32 @@ class ControllerType(type):
class Controller(object): class Controller(object):
__metaclass__ = ControllerType __metaclass__ = ControllerType
def routing_map(modules, nodb_only):
routing_map = werkzeug.routing.Map(strict_slashes=False)
for module in modules:
if module not in controllers_per_module:
continue
for v in controllers_per_module[module]:
cls = v[1]
subclasses = cls.__subclasses__()
subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and c.__module__.split(".")[2] in modules]
if subclasses:
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
cls = type(name, tuple(reversed(subclasses)), {})
o = cls()
members = inspect.getmembers(o)
for mk, mv in members:
if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and (not nodb_only or nodb_only == (mv.auth == "none")):
for url in mv.routes:
if getattr(mv, "combine", False):
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
if url.endswith("/") and len(url) > 1:
url = url[: -1]
routing_map.add(werkzeug.routing.Rule(url, endpoint=mv))
return routing_map
#---------------------------------------------------------- #----------------------------------------------------------
# HTTP Sessions # HTTP Sessions
#---------------------------------------------------------- #----------------------------------------------------------
@ -679,6 +684,8 @@ class OpenERPSession(werkzeug.contrib.sessions.Session):
context['lang'] = lang or 'en_US' context['lang'] = lang or 'en_US'
# Deprecated to be removed in 9
""" """
Damn properties for retro-compatibility. All of that is deprecated, all Damn properties for retro-compatibility. All of that is deprecated, all
of that. of that.
@ -794,7 +801,7 @@ def session_gc(session_store):
pass pass
#---------------------------------------------------------- #----------------------------------------------------------
# WSGI Application # WSGI Layer
#---------------------------------------------------------- #----------------------------------------------------------
# Add potentially missing (older ubuntu) font mime types # Add potentially missing (older ubuntu) font mime types
mimetypes.add_type('application/font-woff', '.woff') mimetypes.add_type('application/font-woff', '.woff')
@ -848,106 +855,27 @@ class Root(object):
"""Root WSGI application for the OpenERP Web Client. """Root WSGI application for the OpenERP Web Client.
""" """
def __init__(self): def __init__(self):
self.addons = {}
self.statics = {}
self.no_db_router = None
self.load_addons()
# Setup http sessions # Setup http sessions
path = session_path() path = session_path()
self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
_logger.debug('HTTP sessions stored in: %s', path) _logger.debug('HTTP sessions stored in: %s', path)
self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession)
# TODO should we move this to ir.http so that only configured modules are served ?
_logger.info("HTTP Configuring static files")
self.load_addons()
_logger.info("Generating nondb routing")
self.routing_map = routing_map(['', "web"], True)
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
""" Handle a WSGI request """ Handle a WSGI request
""" """
return self.dispatch(environ, start_response) return self.dispatch(environ, start_response)
def dispatch(self, environ, start_response):
"""
Performs the actual WSGI dispatching for the application.
"""
try:
httprequest = werkzeug.wrappers.Request(environ)
httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
httprequest.app = self
session_gc(self.session_store)
sid = httprequest.args.get('session_id')
explicit_session = True
if not sid:
sid = httprequest.headers.get("X-Openerp-Session-Id")
if not sid:
sid = httprequest.cookies.get('session_id')
explicit_session = False
if sid is None:
httprequest.session = self.session_store.new()
else:
httprequest.session = self.session_store.get(sid)
self._find_db(httprequest)
if not "lang" in httprequest.session.context:
lang = httprequest.accept_languages.best or "en_US"
lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
httprequest.session.context["lang"] = lang
request = self._build_request(httprequest)
db = request.db
if db:
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
with set_request(request):
self.find_handler()
result = request.dispatch()
if db:
openerp.modules.registry.RegistryManager.signal_caches_change(db)
if isinstance(result, basestring):
headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
response = werkzeug.wrappers.Response(result, headers=headers)
else:
response = result
if httprequest.session.should_save:
self.session_store.save(httprequest.session)
# We must not set the cookie if the session id was specified using a http header or a GET parameter.
# There are two reasons to this:
# - When using one of those two means we consider that we are overriding the cookie, which means creating a new
# session on top of an already existing session and we don't want to create a mess with the 'normal' session
# (the one using the cookie). That is a special feature of the Session Javascript class.
# - It could allow session fixation attacks.
if not explicit_session and hasattr(response, 'set_cookie'):
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
return response(environ, start_response)
except werkzeug.exceptions.HTTPException, e:
return e(environ, start_response)
def _find_db(self, httprequest):
db = db_monodb(httprequest)
if db != httprequest.session.db:
httprequest.session.logout()
httprequest.session.db = db
def _build_request(self, httprequest):
if httprequest.args.get('jsonp'):
return JsonRequest(httprequest)
if httprequest.mimetype == "application/json":
return JsonRequest(httprequest)
else:
return HttpRequest(httprequest)
def load_addons(self): def load_addons(self):
""" Load all addons from addons patch containg static files and """ Load all addons from addons patch containg static files and
controllers and configure them. """ controllers and configure them. """
statics = {}
for addons_path in openerp.modules.module.ad_paths: for addons_path in openerp.modules.module.ad_paths:
for module in sorted(os.listdir(str(addons_path))): for module in sorted(os.listdir(str(addons_path))):
@ -960,99 +888,104 @@ class Root(object):
_logger.debug("Loading %s", module) _logger.debug("Loading %s", module)
if 'openerp.addons' in sys.modules: if 'openerp.addons' in sys.modules:
m = __import__('openerp.addons.' + module) m = __import__('openerp.addons.' + module)
else:
m = __import__(module)
addons_module[module] = m addons_module[module] = m
addons_manifest[module] = manifest addons_manifest[module] = manifest
self.statics['/%s/static' % module] = path_static statics['/%s/static' % module] = path_static
app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics) app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, statics)
self.dispatch = DisableCacheMiddleware(app) self.dispatch = DisableCacheMiddleware(app)
def _build_router(self, db): def setup_session(self, httprequest):
_logger.info("Generating routing configuration for database %s" % db) # recover or create session
routing_map = routing.Map(strict_slashes=False) session_gc(self.session_store)
def gen(modules, nodb_only): sid = httprequest.args.get('session_id')
for module in modules: explicit_session = True
for v in controllers_per_module[module]: if not sid:
cls = v[1] sid = httprequest.headers.get("X-Openerp-Session-Id")
if not sid:
subclasses = cls.__subclasses__() sid = httprequest.cookies.get('session_id')
subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and explicit_session = False
c.__module__.split(".")[2] in modules] if sid is None:
if subclasses: httprequest.session = self.session_store.new()
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
cls = type(name, tuple(reversed(subclasses)), {})
o = cls()
members = inspect.getmembers(o)
for mk, mv in members:
if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \
nodb_only == (getattr(mv, "auth", "none") == "none"):
for url in mv.routes:
if getattr(mv, "combine", False):
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/')
if url.endswith("/") and len(url) > 1:
url = url[: -1]
routing_map.add(routing.Rule(url, endpoint=mv))
modules_set = set(controllers_per_module.keys()) - set(['', 'web'])
# building all none methods
gen(['', "web"] + sorted(modules_set), True)
if not db:
return routing_map
registry = openerp.modules.registry.RegistryManager.get(db)
with registry.cursor() as cr:
m = registry.get('ir.module.module')
ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')])
installed = set(x['name'] for x in m.read(cr, 1, ids, ['name']))
modules_set = modules_set & installed
# building all other methods
gen(['', "web"] + sorted(modules_set), False)
return routing_map
def get_db_router(self, db):
if db is None:
router = self.no_db_router
else: else:
router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None) httprequest.session = self.session_store.get(sid)
if not router: return explicit_session
router = self._build_router(db)
if db is None:
self.no_db_router = router
else:
openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router
return router
def find_handler(self): def setup_db(self, httprequest):
# if no db is found on the session try to deduce it from the domain
db = db_monodb(httprequest)
if db != httprequest.session.db:
httprequest.session.logout()
httprequest.session.db = db
def setup_lang(self, httprequest):
if not "lang" in httprequest.session.context:
lang = httprequest.accept_languages.best or "en_US"
lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_')
httprequest.session.context["lang"] = lang
def get_request(self, httprequest):
# deduce type of request
if httprequest.args.get('jsonp'):
return JsonRequest(httprequest)
if httprequest.mimetype == "application/json":
return JsonRequest(httprequest)
else:
return HttpRequest(httprequest)
def get_response(self, httprequest, result, explicit_session):
if isinstance(result, basestring):
headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))]
response = werkzeug.wrappers.Response(result, headers=headers)
else:
response = result
if httprequest.session.should_save:
self.session_store.save(httprequest.session)
# We must not set the cookie if the session id was specified using a http header or a GET parameter.
# There are two reasons to this:
# - When using one of those two means we consider that we are overriding the cookie, which means creating a new
# session on top of an already existing session and we don't want to create a mess with the 'normal' session
# (the one using the cookie). That is a special feature of the Session Javascript class.
# - It could allow session fixation attacks.
if not explicit_session and hasattr(response, 'set_cookie'):
response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60)
return response
def dispatch(self, environ, start_response):
""" """
Tries to discover the controller handling the request for the path specified in the request. Performs the actual WSGI dispatching for the application.
""" """
path = request.httprequest.path try:
urls = self.get_db_router(request.db).bind_to_environ(request.httprequest.environ) httprequest = werkzeug.wrappers.Request(environ)
func, arguments = urls.match(path) httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict
arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")]) httprequest.app = self
@service_model.check explicit_session = self.setup_session(httprequest)
def checked_call(dbname, *a, **kw): self.setup_db(httprequest)
return func(*a, **kw) self.setup_lang(httprequest)
def nfunc(*args, **kwargs): request = self.get_request(httprequest)
kwargs.update(arguments)
if getattr(func, '_first_arg_is_req', False):
args = (request,) + args
if request.db: with set_request(request):
return checked_call(request.db, *args, **kwargs) db = request.db
return func(*args, **kwargs) if db:
openerp.modules.registry.RegistryManager.check_registry_signaling(db)
result = request.registry['ir.http']._dispatch()
openerp.modules.registry.RegistryManager.signal_caches_change(db)
else:
# fallback to non-db handlers
urls = self.routing_map.bind_to_environ(request.httprequest.environ)
func, arguments = urls.match(request.httprequest.path)
request.set_handler(func, arguments, "none")
result = request.dispatch()
response = self.get_response(httprequest, result, explicit_session)
return response(environ, start_response)
request.func = nfunc except werkzeug.exceptions.HTTPException, e:
request.auth_method = getattr(func, "auth", "user") return e(environ, start_response)
request.func_request_type = func.exposed
def db_list(force=False, httprequest=None): def db_list(force=False, httprequest=None):
httprequest = httprequest or request.httprequest httprequest = httprequest or request.httprequest
@ -1091,6 +1024,9 @@ def db_monodb(httprequest=None):
return dbs[0] return dbs[0]
return None return None
#----------------------------------------------------------
# RPC controlller
#----------------------------------------------------------
class CommonController(Controller): class CommonController(Controller):
@route('/jsonrpc', type='json', auth="none") @route('/jsonrpc', type='json', auth="none")