[MERGE] /openerp/6.1 xmlrpc compliant

bzr revid: al@openerp.com-20110928220539-5q8eujk1l0q43d4x
This commit is contained in:
Antony Lesuisse 2011-09-29 00:05:39 +02:00
commit c71979e6ae
12 changed files with 196 additions and 582 deletions

View File

@ -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)

57
openerp/exceptions.py Normal file
View File

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2011 OpenERP s.a. (<http://openerp.com>).
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
""" 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:

View File

@ -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:

View File

@ -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():

View File

@ -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()

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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 = """ <html><head><title>Authorization required</title></head>
<body>You must authenticate to use this service</body><html>\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

View File

@ -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).

View File

@ -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()