diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index bb1e8ec26bb..f552e8166cd 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -35,6 +35,7 @@ from osv import fields,osv from osv.orm import browse_record from service import security from tools.translate import _ +import openerp.exceptions class groups(osv.osv): _name = "res.groups" @@ -437,14 +438,14 @@ class users(osv.osv): if passwd == tools.config['admin_passwd']: return True else: - raise security.ExceptionNoTb('AccessDenied') + raise openerp.exceptions.AccessDenied() def check(self, db, uid, passwd): """Verifies that the given (uid, password) pair is authorized for the database ``db`` and raise an exception if it is not.""" if not passwd: # empty passwords disallowed for obvious security reasons - raise security.ExceptionNoTb('AccessDenied') + raise openerp.exceptions.AccessDenied() if self._uid_cache.get(db, {}).get(uid) == passwd: return cr = pooler.get_db(db).cursor() @@ -453,7 +454,7 @@ class users(osv.osv): (int(uid), passwd, True)) res = cr.fetchone()[0] if not res: - raise security.ExceptionNoTb('AccessDenied') + raise openerp.exceptions.AccessDenied() if self._uid_cache.has_key(db): ulist = self._uid_cache[db] ulist[uid] = passwd @@ -470,7 +471,7 @@ class users(osv.osv): cr.execute('SELECT id FROM res_users WHERE id=%s AND password=%s', (uid, passwd)) res = cr.fetchone() if not res: - raise security.ExceptionNoTb('Bad username or password') + raise openerp.exceptions.AccessDenied() return res[0] finally: cr.close() @@ -481,7 +482,7 @@ class users(osv.osv): password is not used to authenticate requests. :return: True - :raise: security.ExceptionNoTb when old password is wrong + :raise: openerp.exceptions.AccessDenied when old password is wrong :raise: except_osv when new password is not set or empty """ self.check(cr.dbname, uid, old_passwd) diff --git a/openerp/exceptions.py b/openerp/exceptions.py new file mode 100644 index 00000000000..d12910c3448 --- /dev/null +++ b/openerp/exceptions.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2011 OpenERP s.a. (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +""" OpenERP core exceptions. + +This module defines a few exception types. Those types are understood by the +RPC layer. Any other exception type bubbling until the RPC layer will be +treated as a 'Server error'. + +""" + +class Warning(Exception): + pass + +class AccessDenied(Exception): + """ Login/password error. No message, no traceback. """ + def __init__(self): + super(AccessDenied, self).__init__('AccessDenied.') + self.traceback = ('', '', '') + +class AccessError(Exception): + """ Access rights error. """ + +class DeferredException(Exception): + """ Exception object holding a traceback for asynchronous reporting. + + Some RPC calls (database creation and report generation) happen with + an initial request followed by multiple, polling requests. This class + is used to store the possible exception occuring in the thread serving + the first request, and is then sent to a polling request. + + ('Traceback' is misleading, this is really a exc_info() triple.) + """ + def __init__(self, msg, tb): + self.message = msg + self.traceback = tb + self.args = (msg, tb) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 8d11891640e..7e62687e335 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -37,6 +37,7 @@ from pprint import pformat # TODO modules that import netsvc only for things from loglevels must be changed to use loglevels. from loglevels import * import tools +import openerp.exceptions def close_socket(sock): """ Closes a socket instance cleanly @@ -60,11 +61,12 @@ def close_socket(sock): #.apidoc title: Common Services: netsvc #.apidoc module-mods: member-order: bysource -def abort_response(error, description, origin, details): - if not tools.config['debug_mode']: - raise Exception("%s -- %s\n\n%s"%(origin, description, details)) +def abort_response(dummy_1, description, dummy_2, details): + # TODO Replace except_{osv,orm} with these directly. + if description == 'AccessError': + raise openerp.exceptions.AccessError(details) else: - raise + raise openerp.exceptions.Warning(details) class Service(object): """ Base class for *Local* services @@ -96,12 +98,9 @@ def LocalService(name): class ExportService(object): """ Proxy for exported services. - All methods here should take an AuthProxy as their first parameter. It - will be appended by the calling framework. - Note that this class has no direct proxy, capable of calling eservice.method(). Rather, the proxy should call - dispatch(method,auth,params) + dispatch(method, params) """ _services = {} @@ -118,7 +117,7 @@ class ExportService(object): # Dispatch a RPC call w.r.t. the method name. The dispatching # w.r.t. the service (this class) is done by OpenERPDispatcher. - def dispatch(self, method, auth, params): + def dispatch(self, method, params): raise Exception("stub dispatch at %s" % self.__name) BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, _NOTHING, DEFAULT = range(10) @@ -371,11 +370,6 @@ class Server: def _close_socket(self): close_socket(self.socket) -class OpenERPDispatcherException(Exception): - def __init__(self, exception, traceback): - self.exception = exception - self.traceback = traceback - def replace_request_password(args): # password is always 3rd argument in a request, we replace it in RPC logs # so it's easier to forward logs for diagnostics/debugging purposes... @@ -393,7 +387,7 @@ def log(title, msg, channel=logging.DEBUG_RPC, depth=None, fn=""): logger.log(channel, indent+line) indent=indent_after -def dispatch_rpc(service_name, method, params, auth): +def dispatch_rpc(service_name, method, params): """ Handle a RPC call. This is pure Python code, the actual marshalling (from/to XML-RPC or @@ -408,7 +402,7 @@ def dispatch_rpc(service_name, method, params, auth): _log('service', tuple(replace_request_password(params)), depth=None, fn='%s.%s'%(service_name,method)) if logger.isEnabledFor(logging.DEBUG_RPC): start_time = time.time() - result = ExportService.getService(service_name).dispatch(method, auth, params) + result = ExportService.getService(service_name).dispatch(method, params) if logger.isEnabledFor(logging.DEBUG_RPC): end_time = time.time() if not logger.isEnabledFor(logging.DEBUG_RPC_ANSWER): @@ -416,13 +410,24 @@ def dispatch_rpc(service_name, method, params, auth): _log('execution time', '%.3fs' % (end_time - start_time), channel=logging.DEBUG_RPC_ANSWER) _log('result', result, channel=logging.DEBUG_RPC_ANSWER) return result + except openerp.exceptions.AccessError: + raise + except openerp.exceptions.AccessDenied: + raise + except openerp.exceptions.Warning: + raise + except openerp.exceptions.DeferredException, e: + _log('exception', tools.exception_to_unicode(e)) + post_mortem(e.traceback) + raise except Exception, e: _log('exception', tools.exception_to_unicode(e)) - tb = getattr(e, 'traceback', sys.exc_info()) - tb_s = "".join(traceback.format_exception(*tb)) - if tools.config['debug_mode'] and isinstance(tb[2], types.TracebackType): - import pdb - pdb.post_mortem(tb[2]) - raise OpenERPDispatcherException(e, tb_s) + post_mortem(sys.exc_info()) + raise + +def post_mortem(info): + if tools.config['debug_mode'] and isinstance(info[2], types.TracebackType): + import pdb + pdb.post_mortem(info[2]) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index 00672dd94bd..7f5894e5146 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -32,13 +32,10 @@ import openerp.sql_db as sql_db from openerp.tools.func import wraps from openerp.tools.translate import translate from openerp.osv.orm import MetaModel, Model, TransientModel, AbstractModel +import openerp.exceptions -class except_osv(Exception): - def __init__(self, name, value, exc_type='warning'): - self.name = name - self.exc_type = exc_type - self.value = value - self.args = (exc_type, name) +# For backward compatibility +except_osv = openerp.exceptions.Warning service = None @@ -122,7 +119,7 @@ class object_proxy(): self.logger.debug("AccessError", exc_info=True) netsvc.abort_response(1, inst.name, 'warning', inst.value) except except_osv, inst: - netsvc.abort_response(1, inst.name, inst.exc_type, inst.value) + netsvc.abort_response(1, inst.name, 'warning', inst.value) except IntegrityError, inst: osv_pool = pooler.get_pool(dbname) for key in osv_pool._sql_error.keys(): diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index 62e1585dade..5dbb764847a 100644 --- a/openerp/service/__init__.py +++ b/openerp/service/__init__.py @@ -57,7 +57,6 @@ def start_services(): # Initialize the HTTP stack. #http_server.init_servers() - #http_server.init_xmlrpc() #http_server.init_static_http() netrpc_server.init_servers() @@ -67,7 +66,6 @@ def start_services(): # Start the top-level servers threads (normally HTTP, HTTPS, and NETRPC). openerp.netsvc.Server.startAll() - # Start the WSGI server. openerp.wsgi.start_server() diff --git a/openerp/service/http_server.py b/openerp/service/http_server.py index 9bfb9397d19..63bd991ee35 100644 --- a/openerp/service/http_server.py +++ b/openerp/service/http_server.py @@ -64,53 +64,6 @@ try: except ImportError: class SSLError(Exception): pass -class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer): - """ A threaded httpd server, with all the necessary functionality for us. - - It also inherits the xml-rpc dispatcher, so that some xml-rpc functions - will be available to the request handler - """ - encoding = None - allow_none = False - allow_reuse_address = 1 - _send_traceback_header = False - i = 0 - - def __init__(self, addr, requestHandler, proto='http', - logRequests=True, allow_none=False, encoding=None, bind_and_activate=True): - self.logRequests = logRequests - - SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding) - HTTPServer.__init__(self, addr, requestHandler) - - self.numThreads = 0 - self.proto = proto - self.__threadno = 0 - - # [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 handle_error(self, request, client_address): - """ Override the error handler - """ - - logging.getLogger("init").exception("Server error in request from %s:" % (client_address,)) - - def _mark_start(self, thread): - self.numThreads += 1 - - def _mark_end(self, thread): - self.numThreads -= 1 - - - def _get_next_name(self): - self.__threadno += 1 - return 'http-client-%d' % self.__threadno class HttpLogHandler: """ helper class for uniform log handling Please define self._logger at each class that is derived from this @@ -129,136 +82,6 @@ class HttpLogHandler: def log_request(self, code='-', size='-'): self._logger.log(netsvc.logging.DEBUG_RPC, '"%s" %s %s', self.requestline, str(code), str(size)) - -class MultiHandler2(HttpLogHandler, MultiHTTPHandler): - _logger = logging.getLogger('http') - - -class SecureMultiHandler2(HttpLogHandler, SecureMultiHTTPHandler): - _logger = logging.getLogger('https') - - def getcert_fnames(self): - tc = tools.config - fcert = tc.get('secure_cert_file', 'server.cert') - fkey = tc.get('secure_pkey_file', 'server.key') - return (fcert,fkey) - -class BaseHttpDaemon(threading.Thread, netsvc.Server): - _RealProto = '??' - - def __init__(self, interface, port, handler): - threading.Thread.__init__(self, name='%sDaemon-%d'%(self._RealProto, port)) - netsvc.Server.__init__(self) - self.__port = port - self.__interface = interface - - try: - self.server = ThreadedHTTPServer((interface, port), handler, proto=self._RealProto) - self.server.logRequests = True - self.server.timeout = self._busywait_timeout - logging.getLogger("web-services").info( - "starting %s service at %s port %d" % - (self._RealProto, interface or '0.0.0.0', port,)) - except Exception, e: - logging.getLogger("httpd").exception("Error occured when starting the server daemon.") - raise - - @property - def socket(self): - return self.server.socket - - def attach(self, path, gw): - pass - - def stop(self): - self.running = False - self._close_socket() - - def run(self): - self.running = True - while self.running: - try: - self.server.handle_request() - except (socket.error, select.error), e: - if self.running or e.args[0] != errno.EBADF: - raise - return True - - def stats(self): - res = "%sd: " % self._RealProto + ((self.running and "running") or "stopped") - if self.server: - res += ", %d threads" % (self.server.numThreads,) - return res - -# No need for these two classes: init_server() below can initialize correctly -# directly the BaseHttpDaemon class. -class HttpDaemon(BaseHttpDaemon): - _RealProto = 'HTTP' - def __init__(self, interface, port): - super(HttpDaemon, self).__init__(interface, port, - handler=MultiHandler2) - -class HttpSDaemon(BaseHttpDaemon): - _RealProto = 'HTTPS' - def __init__(self, interface, port): - try: - super(HttpSDaemon, self).__init__(interface, port, - handler=SecureMultiHandler2) - except SSLError, e: - logging.getLogger('httpsd').exception( \ - "Can not load the certificate and/or the private key files") - raise - -httpd = None -httpsd = None - -def init_servers(): - global httpd, httpsd - if tools.config.get('xmlrpc'): - httpd = HttpDaemon(tools.config.get('xmlrpc_interface', ''), - int(tools.config.get('xmlrpc_port', 8069))) - - if tools.config.get('xmlrpcs'): - httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''), - int(tools.config.get('xmlrpcs_port', 8071))) - -import SimpleXMLRPCServer -class XMLRPCRequestHandler(FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): - rpc_paths = [] - protocol_version = 'HTTP/1.1' - _logger = logging.getLogger('xmlrpc') - - def _dispatch(self, method, params): - try: - service_name = self.path.split("/")[-1] - auth = getattr(self, 'auth_provider', None) - return netsvc.dispatch_rpc(service_name, method, params, auth) - except netsvc.OpenERPDispatcherException, e: - raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback) - - def handle(self): - pass - - def finish(self): - pass - - def setup(self): - self.connection = dummyconn() - self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys()) - - -def init_xmlrpc(): - if tools.config.get('xmlrpc', False): - # Example of http file serving: - # reg_http_service('/test/', HTTPHandler) - reg_http_service('/xmlrpc/', XMLRPCRequestHandler) - logging.getLogger("web-services").info("Registered XML-RPC over HTTP") - - if tools.config.get('xmlrpcs', False) \ - and not tools.config.get('xmlrpc', False): - # only register at the secure server - reg_http_service('/xmlrpc/', XMLRPCRequestHandler, secure_only=True) - logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only") class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler): _logger = logging.getLogger('httpd') diff --git a/openerp/service/netrpc_server.py b/openerp/service/netrpc_server.py index 3667b38c0b8..adebca63c7f 100644 --- a/openerp/service/netrpc_server.py +++ b/openerp/service/netrpc_server.py @@ -65,21 +65,13 @@ class TinySocketClientThread(threading.Thread): except socket.timeout: #terminate this channel because other endpoint is gone break - except netsvc.OpenERPDispatcherException, e: - try: - new_e = Exception(tools.exception_to_unicode(e.exception)) # avoid problems of pickeling - logging.getLogger('web-services').debug("netrpc: rpc-dispatching exception", exc_info=True) - ts.mysend(new_e, exception=True, traceback=e.traceback) - except Exception: - #terminate this channel if we can't properly send back the error - logging.getLogger('web-services').exception("netrpc: cannot deliver exception message to client") - break except Exception, e: try: + new_e = Exception(tools.exception_to_unicode(e)) # avoid problems of pickeling tb = getattr(e, 'traceback', sys.exc_info()) tb_s = "".join(traceback.format_exception(*tb)) logging.getLogger('web-services').debug("netrpc: communication-level exception", exc_info=True) - ts.mysend(e, exception=True, traceback=tb_s) + ts.mysend(new_e, exception=True, traceback=tb_s) break except Exception, ex: #terminate this channel if we can't properly send back the error diff --git a/openerp/service/security.py b/openerp/service/security.py index 86c381b683f..ff140ad261b 100644 --- a/openerp/service/security.py +++ b/openerp/service/security.py @@ -19,18 +19,12 @@ # ############################################################################## +import openerp.exceptions import openerp.pooler as pooler import openerp.tools as tools #.apidoc title: Authentication helpers -class ExceptionNoTb(Exception): - """ When rejecting a password, hide the traceback - """ - def __init__(self, msg): - super(ExceptionNoTb, self).__init__(msg) - self.traceback = ('','','') - def login(db, login, password): pool = pooler.get_pool(db) user_obj = pool.get('res.users') @@ -40,7 +34,7 @@ def check_super(passwd): if passwd == tools.config['admin_passwd']: return True else: - raise ExceptionNoTb('AccessDenied: Invalid super administrator password.') + raise openerp.exceptions.AccessDenied() def check(db, uid, passwd): pool = pooler.get_pool(db) diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index c941d3a809f..a6be9827c52 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -38,6 +38,7 @@ import openerp.release as release import openerp.sql_db as sql_db import openerp.tools as tools import openerp.modules +import openerp.exceptions #.apidoc title: Exported Service methods #.apidoc module-mods: member-order: bysource @@ -93,7 +94,7 @@ class db(netsvc.ExportService): self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var - def dispatch(self, method, auth, params): + def dispatch(self, method, params): if method in [ 'create', 'get_progress', 'drop', 'dump', 'restore', 'rename', 'change_admin_password', 'migrate_databases', @@ -161,7 +162,7 @@ class db(netsvc.ExportService): self.actions.pop(id) return (1.0, users) else: - e = self.actions[id]['exception'] + e = self.actions[id]['exception'] # TODO this seems wrong: actions[id]['traceback'] is set, but not 'exception'. self.actions.pop(id) raise Exception, e @@ -302,7 +303,7 @@ class db(netsvc.ExportService): def exp_list(self, document=False): if not tools.config['list_db'] and not document: - raise Exception('AccessDenied') + raise openerp.exceptions.AccessDenied() db = sql_db.db_connect('template1') cr = db.cursor() @@ -356,7 +357,7 @@ class db(netsvc.ExportService): except except_orm, inst: netsvc.abort_response(1, inst.name, 'warning', inst.value) except except_osv, inst: - netsvc.abort_response(1, inst.name, inst.exc_type, inst.value) + netsvc.abort_response(1, inst.name, 'warning', inst.value) except Exception: import traceback tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback)) @@ -368,20 +369,14 @@ class common(netsvc.ExportService): def __init__(self,name="common"): netsvc.ExportService.__init__(self,name) - def dispatch(self, method, auth, params): + def dispatch(self, method, params): logger = netsvc.Logger() if method == 'login': - # At this old dispatcher, we do NOT update the auth proxy res = security.login(params[0], params[1], params[2]) msg = res and 'successful login' or 'bad login or password' # TODO log the client ip address.. logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, params[1], params[0].lower())) return res or False - elif method == 'logout': - if auth: - auth.logout(params[1]) # TODO I didn't see any AuthProxy implementing this method. - logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db)) - return True elif method in ['about', 'timezone_get', 'get_server_environment', 'login_message','get_stats', 'check_connectivity', 'list_http_services']: @@ -562,7 +557,7 @@ class objects_proxy(netsvc.ExportService): def __init__(self, name="object"): netsvc.ExportService.__init__(self,name) - def dispatch(self, method, auth, params): + def dispatch(self, method, params): (db, uid, passwd ) = params[0:3] params = params[3:] if method == 'obj_list': @@ -595,7 +590,7 @@ class wizard(netsvc.ExportService): self.wiz_name = {} self.wiz_uid = {} - def dispatch(self, method, auth, params): + def dispatch(self, method, params): (db, uid, passwd ) = params[0:3] params = params[3:] if method not in ['execute','create']: @@ -628,9 +623,9 @@ class wizard(netsvc.ExportService): if self.wiz_uid[wiz_id] == uid: return self._execute(db, uid, wiz_id, datas, action, context) else: - raise Exception, 'AccessDenied' + raise openerp.exceptions.AccessDenied() else: - raise Exception, 'WizardNotFound' + raise openerp.exceptions.Warning('Wizard not found.') # # TODO: set a maximum report number per user to avoid DOS attacks @@ -639,12 +634,6 @@ class wizard(netsvc.ExportService): # False -> True # -class ExceptionWithTraceback(Exception): - def __init__(self, msg, tb): - self.message = msg - self.traceback = tb - self.args = (msg, tb) - class report_spool(netsvc.ExportService): def __init__(self, name='report'): netsvc.ExportService.__init__(self, name) @@ -652,7 +641,7 @@ class report_spool(netsvc.ExportService): self.id = 0 self.id_protect = threading.Semaphore() - def dispatch(self, method, auth, params): + def dispatch(self, method, params): (db, uid, passwd ) = params[0:3] params = params[3:] if method not in ['report', 'report_get', 'render_report']: @@ -683,7 +672,7 @@ class report_spool(netsvc.ExportService): (result, format) = obj.create(cr, uid, ids, datas, context) if not result: tb = sys.exc_info() - self._reports[id]['exception'] = ExceptionWithTraceback('RML is not available at specified location or not enough data to print!', tb) + self._reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb) self._reports[id]['result'] = result self._reports[id]['format'] = format self._reports[id]['state'] = True @@ -695,9 +684,9 @@ class report_spool(netsvc.ExportService): logger.notifyChannel('web-services', netsvc.LOG_ERROR, 'Exception: %s\n%s' % (str(exception), tb_s)) if hasattr(exception, 'name') and hasattr(exception, 'value'): - self._reports[id]['exception'] = ExceptionWithTraceback(tools.ustr(exception.name), tools.ustr(exception.value)) + self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value)) else: - self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb) + self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb) self._reports[id]['state'] = True cr.commit() cr.close() @@ -726,7 +715,7 @@ class report_spool(netsvc.ExportService): (result, format) = obj.create(cr, uid, ids, datas, context) if not result: tb = sys.exc_info() - self._reports[id]['exception'] = ExceptionWithTraceback('RML is not available at specified location or not enough data to print!', tb) + self._reports[id]['exception'] = openerp.exceptions.DeferredException('RML is not available at specified location or not enough data to print!', tb) self._reports[id]['result'] = result self._reports[id]['format'] = format self._reports[id]['state'] = True @@ -738,9 +727,9 @@ class report_spool(netsvc.ExportService): logger.notifyChannel('web-services', netsvc.LOG_ERROR, 'Exception: %s\n%s' % (str(exception), tb_s)) if hasattr(exception, 'name') and hasattr(exception, 'value'): - self._reports[id]['exception'] = ExceptionWithTraceback(tools.ustr(exception.name), tools.ustr(exception.value)) + self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.ustr(exception.name), tools.ustr(exception.value)) else: - self._reports[id]['exception'] = ExceptionWithTraceback(tools.exception_to_unicode(exception), tb) + self._reports[id]['exception'] = openerp.exceptions.DeferredException(tools.exception_to_unicode(exception), tb) self._reports[id]['state'] = True cr.commit() cr.close() diff --git a/openerp/service/websrv_lib.py b/openerp/service/websrv_lib.py index 1efab672eb3..3df441458fd 100644 --- a/openerp/service/websrv_lib.py +++ b/openerp/service/websrv_lib.py @@ -232,305 +232,3 @@ class HttpOptions: """ return opts -class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler): - """ this is a multiple handler, that will dispatch each request - to a nested handler, iff it matches - - The handler will also have *one* dict of authentication proxies, - groupped by their realm. - """ - - protocol_version = "HTTP/1.1" - default_request_version = "HTTP/0.9" # compatibility with py2.5 - - auth_required_msg = """ Authorization required - You must authenticate to use this service\r\r""" - - def __init__(self, request, client_address, server): - self.in_handlers = {} - SocketServer.StreamRequestHandler.__init__(self,request,client_address,server) - self.log_message("MultiHttpHandler init for %s" %(str(client_address))) - - def _handle_one_foreign(self, fore, path): - """ This method overrides the handle_one_request for *children* - handlers. It is required, since the first line should not be - read again.. - - """ - fore.raw_requestline = "%s %s %s\n" % (self.command, path, self.version) - if not fore.parse_request(): # An error code has been sent, just exit - return - if fore.headers.status: - self.log_error("Parse error at headers: %s", fore.headers.status) - self.close_connection = 1 - self.send_error(400,"Parse error at HTTP headers") - return - - self.request_version = fore.request_version - if hasattr(fore, 'auth_provider'): - try: - fore.auth_provider.checkRequest(fore,path) - except AuthRequiredExc,ae: - # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the - # authorisation features of HTTP/1.1 - if self.request_version != 'HTTP/1.1' and ('Darwin/9.' not in fore.headers.get('User-Agent', '')): - self.log_error("Cannot require auth at %s", self.request_version) - self.send_error(403) - return - self._get_ignore_body(fore) # consume any body that came, not loose sync with input - self.send_response(401,'Authorization required') - self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm)) - self.send_header('Connection', 'keep-alive') - self.send_header('Content-Type','text/html') - self.send_header('Content-Length',len(self.auth_required_msg)) - self.end_headers() - self.wfile.write(self.auth_required_msg) - return - except AuthRejectedExc,e: - self.log_error("Rejected auth: %s" % e.args[0]) - self.send_error(403,e.args[0]) - self.close_connection = 1 - return - mname = 'do_' + fore.command - if not hasattr(fore, mname): - if fore.command == 'OPTIONS': - self.do_OPTIONS() - return - self.send_error(501, "Unsupported method (%r)" % fore.command) - return - fore.close_connection = 0 - method = getattr(fore, mname) - try: - method() - except (AuthRejectedExc, AuthRequiredExc): - raise - except Exception, e: - if hasattr(self, 'log_exception'): - self.log_exception("Could not run %s", mname) - else: - self.log_error("Could not run %s: %s", mname, e) - self.send_error(500, "Internal error") - # may not work if method has already sent data - fore.close_connection = 1 - self.close_connection = 1 - if hasattr(fore, '_flush'): - fore._flush() - return - - if fore.close_connection: - # print "Closing connection because of handler" - self.close_connection = fore.close_connection - if hasattr(fore, '_flush'): - fore._flush() - - - def parse_rawline(self): - """Parse a request (internal). - - The request should be stored in self.raw_requestline; the results - are in self.command, self.path, self.request_version and - self.headers. - - Return True for success, False for failure; on failure, an - error is sent back. - - """ - self.command = None # set in case of error on the first line - self.request_version = version = self.default_request_version - self.close_connection = 1 - requestline = self.raw_requestline - if requestline[-2:] == '\r\n': - requestline = requestline[:-2] - elif requestline[-1:] == '\n': - requestline = requestline[:-1] - self.requestline = requestline - words = requestline.split() - if len(words) == 3: - [command, path, version] = words - if version[:5] != 'HTTP/': - self.send_error(400, "Bad request version (%r)" % version) - return False - try: - base_version_number = version.split('/', 1)[1] - version_number = base_version_number.split(".") - # RFC 2145 section 3.1 says there can be only one "." and - # - major and minor numbers MUST be treated as - # separate integers; - # - HTTP/2.4 is a lower version than HTTP/2.13, which in - # turn is lower than HTTP/12.3; - # - Leading zeros MUST be ignored by recipients. - if len(version_number) != 2: - raise ValueError - version_number = int(version_number[0]), int(version_number[1]) - except (ValueError, IndexError): - self.send_error(400, "Bad request version (%r)" % version) - return False - if version_number >= (1, 1): - self.close_connection = 0 - if version_number >= (2, 0): - self.send_error(505, - "Invalid HTTP Version (%s)" % base_version_number) - return False - elif len(words) == 2: - [command, path] = words - self.close_connection = 1 - if command != 'GET': - self.log_error("Junk http request: %s", self.raw_requestline) - self.send_error(400, - "Bad HTTP/0.9 request type (%r)" % command) - return False - elif not words: - return False - else: - #self.send_error(400, "Bad request syntax (%r)" % requestline) - return False - self.request_version = version - self.command, self.path, self.version = command, path, version - return True - - def handle_one_request(self): - """Handle a single HTTP request. - Dispatch to the correct handler. - """ - self.request.setblocking(True) - self.raw_requestline = self.rfile.readline() - if not self.raw_requestline: - self.close_connection = 1 - # self.log_message("no requestline, connection closed?") - return - if not self.parse_rawline(): - self.log_message("Could not parse rawline.") - return - # self.parse_request(): # Do NOT parse here. the first line should be the only - - if self.path == '*' and self.command == 'OPTIONS': - # special handling of path='*', must not use any vdir at all. - if not self.parse_request(): - return - self.do_OPTIONS() - return - vdir = find_http_service(self.path, self.server.proto == 'HTTPS') - if vdir: - p = vdir.path - npath = self.path[len(p):] - if not npath.startswith('/'): - npath = '/' + npath - - if not self.in_handlers.has_key(p): - self.in_handlers[p] = vdir.instanciate_handler(noconnection(self.request),self.client_address,self.server) - hnd = self.in_handlers[p] - hnd.rfile = self.rfile - hnd.wfile = self.wfile - self.rlpath = self.raw_requestline - try: - self._handle_one_foreign(hnd, npath) - except IOError, e: - if e.errno == errno.EPIPE: - self.log_message("Could not complete request %s," \ - "client closed connection", self.rlpath.rstrip()) - else: - raise - else: # no match: - self.send_error(404, "Path not found: %s" % self.path) - - def _get_ignore_body(self,fore): - if not fore.headers.has_key("content-length"): - return - max_chunk_size = 10*1024*1024 - size_remaining = int(fore.headers["content-length"]) - got = '' - while size_remaining: - chunk_size = min(size_remaining, max_chunk_size) - got = fore.rfile.read(chunk_size) - size_remaining -= len(got) - - -class SecureMultiHTTPHandler(MultiHTTPHandler): - def getcert_fnames(self): - """ Return a pair with the filenames of ssl cert,key - - Override this to direct to other filenames - """ - return ('server.cert','server.key') - - def setup(self): - import ssl - certfile, keyfile = self.getcert_fnames() - try: - self.connection = ssl.wrap_socket(self.request, - server_side=True, - certfile=certfile, - keyfile=keyfile, - ssl_version=ssl.PROTOCOL_SSLv23) - self.rfile = self.connection.makefile('rb', self.rbufsize) - self.wfile = self.connection.makefile('wb', self.wbufsize) - self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address) - except Exception: - self.request.shutdown(socket.SHUT_RDWR) - raise - - def finish(self): - # With ssl connections, closing the filehandlers alone may not - # work because of ref counting. We explicitly tell the socket - # to shutdown. - MultiHTTPHandler.finish(self) - try: - self.connection.shutdown(socket.SHUT_RDWR) - except Exception: - pass - -import threading -class ConnThreadingMixIn: - """Mix-in class to handle each _connection_ in a new thread. - - This is necessary for persistent connections, where multiple - requests should be handled synchronously at each connection, but - multiple connections can run in parallel. - """ - - # Decides how threads will act upon termination of the - # main process - daemon_threads = False - - def _get_next_name(self): - return None - - def _handle_request_noblock(self): - """Start a new thread to process the request.""" - if not threading: # happens while quitting python - return - t = threading.Thread(name=self._get_next_name(), target=self._handle_request2) - if self.daemon_threads: - t.setDaemon (1) - t.start() - - def _mark_start(self, thread): - """ Mark the start of a request thread """ - pass - - def _mark_end(self, thread): - """ Mark the end of a request thread """ - pass - - def _handle_request2(self): - """Handle one request, without blocking. - - I assume that select.select has returned that the socket is - readable before this function was called, so there should be - no risk of blocking in get_request(). - """ - try: - self._mark_start(threading.currentThread()) - request, client_address = self.get_request() - if self.verify_request(request, client_address): - try: - self.process_request(request, client_address) - except Exception: - self.handle_error(request, client_address) - self.close_request(request) - except socket.error: - return - finally: - self._mark_end(threading.currentThread()) - -#eof diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 5a8f19f8d83..e5aa378bf04 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -36,62 +36,98 @@ import signal import sys import threading import time +import traceback import openerp +import openerp.modules import openerp.tools.config as config import service.websrv_lib as websrv_lib +# XML-RPC fault codes. Some care must be taken when changing these: the +# constants are also defined client-side and must remain in sync. +# User code must use the exceptions defined in ``openerp.exceptions`` (not +# create directly ``xmlrpclib.Fault`` objects). +XML_RPC_FAULT_CODE_APPLICATION_ERROR = 1 +XML_RPC_FAULT_CODE_DEFERRED_APPLICATION_ERROR = 2 +XML_RPC_FAULT_CODE_ACCESS_DENIED = 3 +XML_RPC_FAULT_CODE_ACCESS_ERROR = 4 +XML_RPC_FAULT_CODE_WARNING = 5 + def xmlrpc_return(start_response, service, method, params): - """ Helper to call a service's method with some params, using a - wsgi-supplied ``start_response`` callback.""" - # This mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for exception - # handling. + """ + Helper to call a service's method with some params, using a wsgi-supplied + ``start_response`` callback. + + This is the place to look at to see the mapping between core exceptions + and XML-RPC fault codes. + """ + # Map OpenERP core exceptions to XML-RPC fault codes. Specific exceptions + # defined in ``openerp.exceptions`` are mapped to specific fault codes; + # all the other exceptions are mapped to the generic + # XML_RPC_FAULT_CODE_APPLICATION_ERROR value. + # This also mimics SimpleXMLRPCDispatcher._marshaled_dispatch() for + # exception handling. try: - result = openerp.netsvc.dispatch_rpc(service, method, params, None) # TODO auth + result = openerp.netsvc.dispatch_rpc(service, method, params) response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) - except openerp.netsvc.OpenERPDispatcherException, e: - fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback) + except openerp.exceptions.Warning, e: + fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_WARNING, str(e)) response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) - except: - exc_type, exc_value, exc_tb = sys.exc_info() - fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)) + except openerp.exceptions.AccessError, e: + fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_ACCESS_ERROR, str(e)) + response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) + except openerp.exceptions.AccessDenied, e: + fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_ACCESS_DENIED, str(e)) + response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) + except openerp.exceptions.DeferredException, e: + info = e.traceback + # Which one is the best ? + formatted_info = "".join(traceback.format_exception(*info)) + #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info + fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_DEFERRED_APPLICATION_ERROR, formatted_info) + response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) + except Exception, e: + info = sys.exc_info() + # Which one is the best ? + formatted_info = "".join(traceback.format_exception(*info)) + #formatted_info = openerp.tools.exception_to_unicode(e) + '\n' + info + fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_APPLICATION_ERROR, formatted_info) response = xmlrpclib.dumps(fault, allow_none=None, encoding=None) start_response("200 OK", [('Content-Type','text/xml'), ('Content-Length', str(len(response)))]) return [response] def wsgi_xmlrpc(environ, start_response): """ The main OpenERP WSGI handler.""" - if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/xmlrpc'): + if environ['REQUEST_METHOD'] == 'POST' and environ['PATH_INFO'].startswith('/openerp/6.1/xmlrpc'): length = int(environ['CONTENT_LENGTH']) data = environ['wsgi.input'].read(length) params, method = xmlrpclib.loads(data) - path = environ['PATH_INFO'][len('/openerp/xmlrpc'):] + path = environ['PATH_INFO'][len('/openerp/6.1/xmlrpc'):] if path.startswith('/'): path = path[1:] if path.endswith('/'): p = path[:-1] path = path.split('/') - # All routes are hard-coded. Need a way to register addons-supplied handlers. + # All routes are hard-coded. # No need for a db segment. if len(path) == 1: service = path[0] if service == 'common': - if method in ('create_database', 'list', 'server_version'): - return xmlrpc_return(start_response, 'db', method, params) - else: - return xmlrpc_return(start_response, 'common', method, params) + if method in ('server_version',): + service = 'db' + return xmlrpc_return(start_response, service, method, params) + # A db segment must be given. elif len(path) == 2: service, db_name = path params = (db_name,) + params if service == 'model': - return xmlrpc_return(start_response, 'object', method, params) - elif service == 'report': - return xmlrpc_return(start_response, 'report', method, params) + service = 'object' + return xmlrpc_return(start_response, service, method, params) # TODO the body has been read, need to raise an exception (not return None). diff --git a/tests/test_xmlrpc.py b/tests/test_xmlrpc.py index 30af863f12d..f31efd24803 100644 --- a/tests/test_xmlrpc.py +++ b/tests/test_xmlrpc.py @@ -27,6 +27,10 @@ common_proxy_60 = None db_proxy_60 = None object_proxy_60 = None +common_proxy_61 = None +db_proxy_61 = None +model_proxy_61 = None + def setUpModule(): """ Start the OpenERP server similary to the openerp-server script and @@ -40,19 +44,32 @@ def setUpModule(): global db_proxy_60 global object_proxy_60 + global common_proxy_61 + global db_proxy_61 + global model_proxy_61 + # Use the old (pre 6.1) API. url = 'http://%s:%d/xmlrpc/' % (HOST, PORT) common_proxy_60 = xmlrpclib.ServerProxy(url + 'common') db_proxy_60 = xmlrpclib.ServerProxy(url + 'db') object_proxy_60 = xmlrpclib.ServerProxy(url + 'object') + # Use the new (6.1) API. + url = 'http://%s:%d/openerp/6.1/xmlrpc/' % (HOST, PORT) + common_proxy_61 = xmlrpclib.ServerProxy(url + 'common') + db_proxy_61 = xmlrpclib.ServerProxy(url + 'db') + model_proxy_61 = xmlrpclib.ServerProxy(url + 'model/' + DB) + + # Mmm need to make sure the server is listening for XML-RPC requests. + time.sleep(10) + def tearDownModule(): """ Shutdown the OpenERP server similarly to a single ctrl-c. """ openerp.service.stop_services() class test_xmlrpc(unittest2.TestCase): - def test_xmlrpc_create_database_polling(self): + def test_00_xmlrpc_create_database_polling(self): """ Simulate a OpenERP client requesting the creation of a database and polling the server until the creation is complete. @@ -80,6 +97,13 @@ class test_xmlrpc(unittest2.TestCase): 'ir.model', 'search', [], {}) assert ids + def test_xmlrpc_61_ir_model_search(self): + """ Try a search on the object service. """ + ids = model_proxy_61.execute(ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', []) + assert ids + ids = model_proxy_61.execute(ADMIN_USER_ID, ADMIN_PASSWORD, 'ir.model', 'search', [], {}) + assert ids + if __name__ == '__main__': unittest2.main()