From a254a13dc36a4ce4f0fc7c9aa50cb1afb6757543 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Sun, 25 Sep 2011 17:09:23 +0200 Subject: [PATCH 1/9] [IMP] xmlrpc: versioned API. bzr revid: vmt@openerp.com-20110925150923-au0u0nply8lriise --- openerp/wsgi.py | 6 +++--- tests/test_xmlrpc.py | 26 +++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 5a8f19f8d83..658fdbea712 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -61,18 +61,18 @@ def xmlrpc_return(start_response, service, method, params): 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: 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() From a7a826b2df2532315c25e572cded68b39ed0965d Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Sun, 25 Sep 2011 17:23:18 +0200 Subject: [PATCH 2/9] [IMP] netsvc: removed unused auth param. The auth param is an auth proxy/provider. The idea is that an auth proxy is associated to some HTTP request handler. The handler can then use it when necessary, depending on the accessed resource. As of now, it is only used by the webdav module. But really, the caldav module accesses the proxy directly on the handler. Currently in web_services, authentication is simply done via security.check(). bzr revid: vmt@openerp.com-20110925152318-i3jvimumm51e2lu4 --- openerp/netsvc.py | 11 ++++------- openerp/service/web_services.py | 16 +++++----------- openerp/wsgi.py | 2 +- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 8d11891640e..4f9fd3b89ca 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -96,12 +96,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 +115,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) @@ -393,7 +390,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 +405,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): diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index c941d3a809f..f0e38d9743f 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -93,7 +93,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', @@ -368,20 +368,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 +556,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 +589,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']: @@ -652,7 +646,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']: diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 658fdbea712..ad9b57e95b6 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -47,7 +47,7 @@ def xmlrpc_return(start_response, service, method, params): # This 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) From b7a0c1061ef3347f35389128355e96a84843858c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Sun, 25 Sep 2011 18:29:25 +0200 Subject: [PATCH 3/9] [IMP] xmlrpc: correctly use int fault code instead of string. bzr revid: vmt@openerp.com-20110925162925-7d0p807u6h087whk --- openerp/wsgi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index ad9b57e95b6..b407800db9a 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -50,7 +50,7 @@ def xmlrpc_return(start_response, service, method, params): 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) + fault = xmlrpclib.Fault(2, openerp.tools.exception_to_unicode(e.exception) + '\n' + e.traceback) # TODO map OpenERP-specific exception to some fault code response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) except: exc_type, exc_value, exc_tb = sys.exc_info() From f16e2ef10a893f856434f68561c56a1aaaafc874 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Sun, 25 Sep 2011 19:08:27 +0200 Subject: [PATCH 4/9] [IMP] websrv_lib: removed top-level http handlers. We have to keep for now the sub-handler code because webdav uses it. bzr revid: vmt@openerp.com-20110925170827-x2xvdz5pzupnu28k --- openerp/service/__init__.py | 1 - openerp/service/http_server.py | 177 ------------------- openerp/service/websrv_lib.py | 302 --------------------------------- 3 files changed, 480 deletions(-) diff --git a/openerp/service/__init__.py b/openerp/service/__init__.py index 62e1585dade..9a7dbbee225 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() 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/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 From bbd10d96c0f6acc3d3d2fe5269f6af6a3e295f40 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 26 Sep 2011 14:53:58 +0200 Subject: [PATCH 5/9] [IMP] exceptions: replace ExceptionNoTb with AccessDenied. bzr revid: vmt@openerp.com-20110926125358-8yy4tvnemfna72u7 --- openerp/addons/base/res/res_users.py | 11 ++++++----- openerp/osv/osv.py | 10 +++------- openerp/service/security.py | 10 ++-------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index 51be4adc747..000ea6e35fc 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/osv/osv.py b/openerp/osv/osv.py index 02c6fbe3c5d..f2f8bb6be23 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -30,14 +30,10 @@ from psycopg2 import IntegrityError, errorcodes from openerp.tools.func import wraps from openerp.tools.translate import translate from openerp.osv.orm import MetaModel +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 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) From 157580b8e8ac9735887cbc12d1e3efcf368cb70a Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 26 Sep 2011 17:14:03 +0200 Subject: [PATCH 6/9] [IMP] xmlrpc: use proper exception type to map to xmlrpc error codes. bzr revid: vmt@openerp.com-20110926151403-fhx4ph22tua2s3st --- openerp/exceptions.py | 36 +++++++++++++++++++++++++++++++++ openerp/netsvc.py | 16 +++++++++++---- openerp/osv/osv.py | 2 +- openerp/service/web_services.py | 5 +++-- openerp/wsgi.py | 13 +++++++++++- 5 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 openerp/exceptions.py diff --git a/openerp/exceptions.py b/openerp/exceptions.py new file mode 100644 index 00000000000..944e34fb3ef --- /dev/null +++ b/openerp/exceptions.py @@ -0,0 +1,36 @@ +# -*- 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 . +# +############################################################################## + +class Warning(Exception): + pass + +class AccessDenied(Exception): + """ Login/password error. No message, no traceback. """ + def __init__(self): + import random + super(AccessDenied, self).__init__('Try again. %s out of %s characters are correct.' % (random.randint(0, 30), 30)) + self.traceback = ('', '', '') + +class AccessError(Exception): + """ Access rights error. """ + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 4f9fd3b89ca..7f81228de8c 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 @@ -413,6 +415,12 @@ def dispatch_rpc(service_name, method, params): _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 Exception, e: _log('exception', tools.exception_to_unicode(e)) tb = getattr(e, 'traceback', sys.exc_info()) diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index f2f8bb6be23..3c63b5c96d2 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -117,7 +117,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/web_services.py b/openerp/service/web_services.py index f0e38d9743f..181c7a25b78 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 @@ -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)) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index b407800db9a..f1886a4cb37 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -38,6 +38,7 @@ import threading import time import openerp +import openerp.modules import openerp.tools.config as config import service.websrv_lib as websrv_lib @@ -49,8 +50,18 @@ def xmlrpc_return(start_response, service, method, params): try: result = openerp.netsvc.dispatch_rpc(service, method, params) response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) + except openerp.exceptions.AccessError, e: + fault = xmlrpclib.Fault(5, str(e)) + response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) + except openerp.exceptions.Warning, e: + fault = xmlrpclib.Fault(4, str(e)) + response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) + except openerp.exceptions.AccessDenied, e: + fault = xmlrpclib.Fault(3, str(e)) + response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) except openerp.netsvc.OpenERPDispatcherException, e: - fault = xmlrpclib.Fault(2, openerp.tools.exception_to_unicode(e.exception) + '\n' + e.traceback) # TODO map OpenERP-specific exception to some fault code + # TODO collapse this case with the next one. + fault = xmlrpclib.Fault(2, openerp.tools.exception_to_unicode(e.exception) + '\n' + e.traceback) response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) except: exc_type, exc_value, exc_tb = sys.exc_info() From e8dc0edbaa173a7367dc2abcd528a797f5af857b Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 27 Sep 2011 10:19:02 +0200 Subject: [PATCH 7/9] [IMP] xmlrpc: use some commented constant symobls instead of raw numbers. bzr revid: vmt@openerp.com-20110927081902-4zu2mri9zx8j27tb --- openerp/service/web_services.py | 4 ++-- openerp/wsgi.py | 32 +++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 181c7a25b78..a3bdb48a953 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -623,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 diff --git a/openerp/wsgi.py b/openerp/wsgi.py index f1886a4cb37..984b049d902 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -42,22 +42,40 @@ 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 = 2 +XML_RPC_FAULT_CODE_ACCESS_DENIED = 3 +XML_RPC_FAULT_CODE_WARNING = 4 +XML_RPC_FAULT_CODE_ACCESS_ERROR = 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) response = xmlrpclib.dumps((result,), methodresponse=1, allow_none=False, encoding=None) except openerp.exceptions.AccessError, e: - fault = xmlrpclib.Fault(5, str(e)) + fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_ACCESS_ERROR, str(e)) response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) except openerp.exceptions.Warning, e: - fault = xmlrpclib.Fault(4, str(e)) + fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_WARNING, str(e)) response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) except openerp.exceptions.AccessDenied, e: - fault = xmlrpclib.Fault(3, str(e)) + fault = xmlrpclib.Fault(XML_RPC_FAULT_CODE_ACCESS_DENIED, str(e)) response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) except openerp.netsvc.OpenERPDispatcherException, e: # TODO collapse this case with the next one. From 08cfadaff074703cf480051a1351f851fcaf477f Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 27 Sep 2011 12:22:46 +0200 Subject: [PATCH 8/9] [IMP] xmlrpc: somewhat streamlined the exception types/ exception handling code. OpenERPDispatcherException which was simply wrapping an exception together with an exc_info is removed. bzr revid: vmt@openerp.com-20110927102246-87ibftawukc2d8lc --- openerp/exceptions.py | 22 ++++++++++++++++++++++ openerp/netsvc.py | 22 +++++++++++----------- openerp/service/netrpc_server.py | 12 ++---------- openerp/service/web_services.py | 20 +++++++------------- openerp/wsgi.py | 22 +++++++++++++++------- 5 files changed, 57 insertions(+), 41 deletions(-) diff --git a/openerp/exceptions.py b/openerp/exceptions.py index 944e34fb3ef..50571306d29 100644 --- a/openerp/exceptions.py +++ b/openerp/exceptions.py @@ -19,6 +19,14 @@ # ############################################################################## +""" 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 @@ -32,5 +40,19 @@ class AccessDenied(Exception): 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 7f81228de8c..7e62687e335 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -370,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... @@ -421,13 +416,18 @@ def dispatch_rpc(service_name, method, params): 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/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/web_services.py b/openerp/service/web_services.py index a3bdb48a953..a6be9827c52 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -162,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 @@ -634,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) @@ -678,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 @@ -690,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() @@ -721,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 @@ -733,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/wsgi.py b/openerp/wsgi.py index 984b049d902..8db39a780c7 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -36,6 +36,7 @@ import signal import sys import threading import time +import traceback import openerp import openerp.modules @@ -46,7 +47,8 @@ import service.websrv_lib as websrv_lib # 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 = 2 +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_WARNING = 4 XML_RPC_FAULT_CODE_ACCESS_ERROR = 5 @@ -77,13 +79,19 @@ def xmlrpc_return(start_response, service, method, params): 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.netsvc.OpenERPDispatcherException, e: - # TODO collapse this case with the next one. - fault = xmlrpclib.Fault(2, openerp.tools.exception_to_unicode(e.exception) + '\n' + e.traceback) + 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: - exc_type, exc_value, exc_tb = sys.exc_info() - fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value)) + 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] From 610eef07cd96c0fc26adf9887ae4566db20c066c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 27 Sep 2011 15:35:22 +0200 Subject: [PATCH 9/9] [IMP] xmlrpc: reverted the common/db URI fusion, we prefer to keep the separate URI. bzr revid: vmt@openerp.com-20110927133522-5qcngogolyw88vo7 --- openerp/wsgi.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openerp/wsgi.py b/openerp/wsgi.py index 8db39a780c7..d88c2d28ad3 100644 --- a/openerp/wsgi.py +++ b/openerp/wsgi.py @@ -116,19 +116,18 @@ def wsgi_xmlrpc(environ, start_response): 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).