diff --git a/bin/netsvc.py b/bin/netsvc.py index c4194e3024e..e9c9fc21728 100644 --- a/bin/netsvc.py +++ b/bin/netsvc.py @@ -37,17 +37,23 @@ import time import xmlrpclib import release -SERVICES = {} -GROUPS = {} - class Service(object): + """ Base class for *Local* services + + Functionality here is trusted, no authentication. + """ + _services = {} def __init__(self, name, audience=''): - SERVICES[name] = self + Service._services[name] = self self.__name = name self._methods = {} def joinGroup(self, name): - GROUPS.setdefault(name, {})[self.__name] = self + raise Exception("No group for local services") + #GROUPS.setdefault(name, {})[self.__name] = self + + def service_exist(self,name): + return Service._services.has_key(name) def exportMethod(self, method): if callable(method): @@ -59,11 +65,16 @@ class Service(object): else: raise -class LocalService(Service): +class LocalService(object): + """ Proxy for local services. + + Any instance of this class will behave like the single instance + of Service(name) + """ def __init__(self, name): self.__name = name try: - self._service = SERVICES[name] + self._service = Service._services[name] for method_name, method_definition in self._service._methods.items(): setattr(self, method_name, method_definition) except KeyError, keyError: @@ -72,8 +83,42 @@ class LocalService(Service): def __call__(self, method, *params): return getattr(self, method)(*params) -def service_exist(name): - return SERVICES.get(name, False) +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) + """ + + _services = {} + _groups = {} + + def __init__(self, name, audience=''): + ExportService._services[name] = self + self.__name = name + + def joinGroup(self, name): + ExportService._groups.setdefault(name, {})[self.__name] = self + + @classmethod + def getService(cls,name): + return cls._services[name] + + def dispatch(self, method, auth, params): + pass + + def new_dispatch(self,method,auth,params): + pass + + def abortResponse(self, error, description, origin, details): + if not tools.config['debug_mode']: + raise Exception("%s -- %s\n\n%s"%(origin, description, details)) + else: + raise LOG_NOTSET = 'notset' LOG_DEBUG_RPC = 'debug_rpc' @@ -244,10 +289,46 @@ class Agent(object): import traceback -class xmlrpc(object): - class RpcGateway(object): - def __init__(self, name): - self.name = name +class Server: + """ Generic interface for all servers with an event loop etc. + Override this to impement http, net-rpc etc. servers. + + Servers here must have threaded behaviour. start() must not block, + there is no run(). + """ + __is_started = False + __servers = [] + + def __init__(self): + if Server.__is_started: + raise Exception('All instances of servers must be inited before the startAll()') + Server.__servers.append(self) + + def start(self): + print "called stub Server.start" + pass + + def stop(self): + print "called stub Server.stop" + pass + + @classmethod + def startAll(cls): + if cls.__is_started: + return + print "Starting %d services" % len(cls.__servers) + for srv in cls.__servers: + srv.start() + cls.__is_started = True + + @classmethod + def quitAll(cls): + if not cls.__is_started: + return + for srv in cls.__servers: + srv.stop() + cls.__is_started = False + class OpenERPDispatcherException(Exception): def __init__(self, exception, traceback): @@ -264,7 +345,11 @@ class OpenERPDispatcher: self.log('service', service_name) self.log('method', method) self.log('params', params) - result = LocalService(service_name)(method, *params) + if hasattr(self,'auth_provider'): + auth = self.auth_provider + else: + auth = None + result = ExportService.getService(service_name).dispatch(method, auth, params) self.log('result', result) return result except Exception, e: @@ -279,193 +364,4 @@ class OpenERPDispatcher: pdb.post_mortem(tb[2]) raise OpenERPDispatcherException(e, tb_s) -class GenericXMLRPCRequestHandler(OpenERPDispatcher): - def _dispatch(self, method, params): - try: - service_name = self.path.split("/")[-1] - return self.dispatch(service_name, method, params) - except OpenERPDispatcherException, e: - raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback) - -class SSLSocket(object): - def __init__(self, socket): - if not hasattr(socket, 'sock_shutdown'): - from OpenSSL import SSL - ctx = SSL.Context(SSL.SSLv23_METHOD) - ctx.use_privatekey_file(tools.config['secure_pkey_file']) - ctx.use_certificate_file(tools.config['secure_cert_file']) - - self.socket = SSL.Connection(ctx, socket) - else: - self.socket = socket - - def shutdown(self, how): - return self.socket.sock_shutdown(how) - - def __getattr__(self, name): - return getattr(self.socket, name) - -class SimpleXMLRPCRequestHandler(GenericXMLRPCRequestHandler, SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): - rpc_paths = map(lambda s: '/xmlrpc/%s' % s, SERVICES.keys()) - -class SecureXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): - def setup(self): - self.connection = SSLSocket(self.request) - self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) - self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) - -class SimpleThreadedXMLRPCServer(SocketServer.ThreadingMixIn, SimpleXMLRPCServer.SimpleXMLRPCServer): - encoding = None - allow_none = False - - def server_bind(self): - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - SimpleXMLRPCServer.SimpleXMLRPCServer.server_bind(self) - - def handle_error(self, request, client_address): - """ Override the error handler - """ - import traceback - Logger().notifyChannel("init", LOG_ERROR,"Server error in request from %s:\n%s" % - (client_address,traceback.format_exc())) - -class SecureThreadedXMLRPCServer(SimpleThreadedXMLRPCServer): - def __init__(self, server_address, HandlerClass, logRequests=1): - SimpleThreadedXMLRPCServer.__init__(self, server_address, HandlerClass, logRequests) - self.socket = SSLSocket(socket.socket(self.address_family, self.socket_type)) - self.server_bind() - self.server_activate() - - def handle_error(self, request, client_address): - """ Override the error handler - """ - import traceback - e_type, e_value, e_traceback = sys.exc_info() - Logger().notifyChannel("init", LOG_ERROR,"SSL Request handler error in request from %s: %s\n%s" % - (client_address,str(e_type),traceback.format_exc())) - -class HttpDaemon(threading.Thread): - def __init__(self, interface, port, secure=False): - threading.Thread.__init__(self) - self.__port = port - self.__interface = interface - self.secure = bool(secure) - handler_class = (SimpleXMLRPCRequestHandler, SecureXMLRPCRequestHandler)[self.secure] - server_class = (SimpleThreadedXMLRPCServer, SecureThreadedXMLRPCServer)[self.secure] - - if self.secure: - from OpenSSL.SSL import Error as SSLError - else: - class SSLError(Exception): pass - try: - self.server = server_class((interface, port), handler_class, 0) - except SSLError, e: - Logger().notifyChannel('xml-rpc-ssl', LOG_CRITICAL, "Can not load the certificate and/or the private key files") - sys.exit(1) - except Exception, e: - Logger().notifyChannel('xml-rpc', LOG_CRITICAL, "Error occur when starting the server daemon: %s" % (e,)) - sys.exit(1) - - - def attach(self, path, gw): - pass - - def stop(self): - self.running = False - if os.name != 'nt': - self.server.socket.shutdown( hasattr(socket, 'SHUT_RDWR') and socket.SHUT_RDWR or 2 ) - self.server.socket.close() - - def run(self): - self.server.register_introspection_functions() - - self.running = True - while self.running: - self.server.handle_request() - return True - - # If the server need to be run recursively - # - #signal.signal(signal.SIGALRM, self.my_handler) - #signal.alarm(6) - #while True: - # self.server.handle_request() - #signal.alarm(0) # Disable the alarm - -import tiny_socket -class TinySocketClientThread(threading.Thread, OpenERPDispatcher): - def __init__(self, sock, threads): - threading.Thread.__init__(self) - self.sock = sock - self.threads = threads - - def run(self): - import select - self.running = True - try: - ts = tiny_socket.mysocket(self.sock) - except: - self.sock.close() - self.threads.remove(self) - return False - while self.running: - try: - msg = ts.myreceive() - except: - self.sock.close() - self.threads.remove(self) - return False - try: - result = self.dispatch(msg[0], msg[1], msg[2:]) - ts.mysend(result) - except OpenERPDispatcherException, e: - new_e = Exception(tools.exception_to_unicode(e.exception)) # avoid problems of pickeling - ts.mysend(new_e, exception=True, traceback=e.traceback) - - self.sock.close() - self.threads.remove(self) - return True - - def stop(self): - self.running = False - - -class TinySocketServerThread(threading.Thread): - def __init__(self, interface, port, secure=False): - threading.Thread.__init__(self) - self.__port = port - self.__interface = interface - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.socket.bind((self.__interface, self.__port)) - self.socket.listen(5) - self.threads = [] - - def run(self): - import select - try: - self.running = True - while self.running: - (clientsocket, address) = self.socket.accept() - ct = TinySocketClientThread(clientsocket, self.threads) - self.threads.append(ct) - ct.start() - self.socket.close() - except Exception, e: - self.socket.close() - return False - - def stop(self): - self.running = False - for t in self.threads: - t.stop() - try: - if hasattr(socket, 'SHUT_RDWR'): - self.socket.shutdown(socket.SHUT_RDWR) - else: - self.socket.shutdown(2) - self.socket.close() - except: - return False - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/bin/openerp-server.py b/bin/openerp-server.py index e095f4ed629..f783ba4f666 100755 --- a/bin/openerp-server.py +++ b/bin/openerp-server.py @@ -152,35 +152,22 @@ if tools.config["stop_after_init"]: #---------------------------------------------------------- -# Launch Server +# Launch Servers #---------------------------------------------------------- -if tools.config['xmlrpc']: - port = int(tools.config['port']) - interface = tools.config["interface"] - secure = tools.config["secure"] +import service.http_server - httpd = netsvc.HttpDaemon(interface, port, secure) +service.http_server.init_servers() +service.http_server.init_xmlrpc() - xml_gw = netsvc.xmlrpc.RpcGateway('web-services') - httpd.attach("/xmlrpc", xml_gw) - logger.notifyChannel("web-services", netsvc.LOG_INFO, - "starting XML-RPC%s services, port %s" % - ((tools.config['secure'] and ' Secure' or ''), port)) -# -#if tools.config["soap"]: -# soap_gw = netsvc.xmlrpc.RpcGateway('web-services') -# httpd.attach("/soap", soap_gw ) -# logger.notifyChannel("web-services", netsvc.LOG_INFO, 'starting SOAP services, port '+str(port)) -# - -if tools.config['netrpc']: - netport = int(tools.config['netport']) - netinterface = tools.config["netinterface"] - tinySocket = netsvc.TinySocketServerThread(netinterface, netport, False) - logger.notifyChannel("web-services", netsvc.LOG_INFO, - "starting NET-RPC service, port %d" % (netport,)) +# *-* TODO +#if tools.config['netrpc']: + #netport = int(tools.config['netport']) + #netinterface = tools.config["netinterface"] + #tinySocket = netsvc.TinySocketServerThread(netinterface, netport, False) + #logger.notifyChannel("web-services", netsvc.LOG_INFO, + #"starting NET-RPC service, port %d" % (netport,)) LST_SIGNALS = ['SIGINT', 'SIGTERM'] if os.name == 'posix': @@ -196,11 +183,8 @@ def handler(signum, _): :param signum: the signal number :param _: """ - if tools.config['netrpc']: - tinySocket.stop() - if tools.config['xmlrpc']: - httpd.stop() netsvc.Agent.quit() + netsvc.Server.quitAll() if tools.config['pidfile']: os.unlink(tools.config['pidfile']) logger.notifyChannel('shutdown', netsvc.LOG_INFO, @@ -217,16 +201,14 @@ if tools.config['pidfile']: fd.write(pidtext) fd.close() + +netsvc.Server.startAll() + logger.notifyChannel("web-services", netsvc.LOG_INFO, 'the server is running, waiting for connections...') -if tools.config['netrpc']: - tinySocket.start() -if tools.config['xmlrpc']: - httpd.start() - while True: - time.sleep(1) + time.sleep(60) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/bin/osv/osv.py b/bin/osv/osv.py index f6a364b7891..807ecad566d 100644 --- a/bin/osv/osv.py +++ b/bin/osv/osv.py @@ -86,7 +86,6 @@ class osv_pool(netsvc.Service): self._init = True self._init_parent = {} netsvc.Service.__init__(self, 'object_proxy', audience='') - self.joinGroup('web-services') self.exportMethod(self.obj_list) self.exportMethod(self.exec_workflow) self.exportMethod(self.execute) diff --git a/bin/report/interface.py b/bin/report/interface.py index bcbcd0f53fb..17fd771ad49 100644 --- a/bin/report/interface.py +++ b/bin/report/interface.py @@ -49,7 +49,7 @@ def toxml(val): class report_int(netsvc.Service): def __init__(self, name, audience='*'): - assert not netsvc.service_exist(name), 'The report "%s" already exist!' % name + assert not self.service_exist(name), 'The report "%s" already exist!' % name super(report_int, self).__init__(name, audience) if name[0:7]<>'report.': raise Exception, 'ConceptionError, bad report name, should start with "report."' @@ -57,7 +57,7 @@ class report_int(netsvc.Service): self.id = 0 self.name2 = '.'.join(name.split('.')[1:]) self.title = None - self.joinGroup('report') + #self.joinGroup('report') self.exportMethod(self.create) def create(self, cr, uid, ids, datas, context=None): @@ -239,8 +239,9 @@ def register_all(db): cr.execute("SELECT * FROM ir_act_report_xml WHERE auto=%s ORDER BY id", (True,)) result = cr.dictfetchall() cr.close() + svcs = netsvc.Service._services for r in result: - if netsvc.service_exist('report.'+r['report_name']): + if svcs.has_key('report.'+r['report_name']): continue if r['report_rml'] or r['report_rml_content_data']: report_sxw('report.'+r['report_name'], r['model'], diff --git a/bin/service/http_server.py b/bin/service/http_server.py new file mode 100644 index 00000000000..76cf5b2fc91 --- /dev/null +++ b/bin/service/http_server.py @@ -0,0 +1,211 @@ +# -*- encoding: utf-8 -*- + +# +# Copyright P. Christeas 2008,2009 +# +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################### + +""" This file contains instance of the http server. + + +""" +from websrv_lib import * +import netsvc +import threading +import tools +import os +import socket +import xmlrpclib + +from SimpleXMLRPCServer import SimpleXMLRPCDispatcher + +try: + import fcntl +except ImportError: + fcntl = None + +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, + 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, bind_and_activate) + + # [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 + """ + import traceback + netsvc.Logger().notifyChannel("init", netsvc.LOG_ERROR,"Server error in request from %s:\n%s" % + (client_address,traceback.format_exc())) + +class MultiHandler2(MultiHTTPHandler): + def log_message(self, format, *args): + netsvc.Logger().notifyChannel('http',netsvc.LOG_DEBUG,format % args) + + +class SecureMultiHandler2(SecureMultiHTTPHandler): + def log_message(self, format, *args): + netsvc.Logger().notifyChannel('https',netsvc.LOG_DEBUG,format % args) + +class HttpDaemon(threading.Thread, netsvc.Server): + def __init__(self, interface, port): + threading.Thread.__init__(self) + netsvc.Server.__init__(self) + self.__port = port + self.__interface = interface + + try: + self.server = ThreadedHTTPServer((interface, port), MultiHandler2) + self.server.vdirs = [] + self.server.logRequests = True + except Exception, e: + netsvc.Logger().notifyChannel('httpd', netsvc.LOG_CRITICAL, "Error occur when starting the server daemon: %s" % (e,)) + raise + + + def attach(self, path, gw): + pass + + def stop(self): + self.running = False + if os.name != 'nt': + self.server.socket.shutdown( hasattr(socket, 'SHUT_RDWR') and socket.SHUT_RDWR or 2 ) + self.server.socket.close() + + def run(self): + #self.server.register_introspection_functions() + + self.running = True + while self.running: + self.server.handle_request() + return True + +class HttpSDaemon(threading.Thread, netsvc.Server): + def __init__(self, interface, port): + threading.Thread.__init__(self) + netsvc.Server.__init__(self) + self.__port = port + self.__interface = interface + + from ssl import SSLError + + try: + self.server = ThreadedHTTPServer((interface, port), SecureMultiHandler2) + self.server.vdirs = [] + self.server.logRequests = True + except SSLError, e: + netsvc.Logger().notifyChannel('httpd-ssl', netsvc.LOG_CRITICAL, "Can not load the certificate and/or the private key files") + raise + except Exception, e: + netsvc.Logger().notifyChannel('httpd-ssl', netsvc.LOG_CRITICAL, "Error occur when starting the server daemon: %s" % (e,)) + raise + +httpd = None +httpsd = None + +def init_servers(): + global httpd, httpsd + if tools.config.get_misc('httpd','enable', True): + print "creating a httpd" + httpd = HttpDaemon(tools.config.get_misc('httpd','interface', ''), \ + tools.config.get_misc('httpd','port', 8069)) + + if tools.config.get_misc('httpsd','enable', False): + print "creating a httpsd" + httpsd = HttpSDaemon(tools.config.get_misc('httpsd','interface', ''), \ + tools.config.get_misc('httpsd','port', 8071)) + +def reg_http_service(hts, secure_only = False): + """ Register some handler to httpd. + hts must be an HTTPDir + """ + global httpd, httpsd + if not isinstance(hts, HTTPDir): + raise Exception("Wrong class for http service") + + if httpd and not secure_only: + httpd.server.vdirs.append(hts) + + if httpsd: + httpsd.server.vdirs.append(hts) + + if (not httpd) and (not httpsd): + netsvc.Logger().notifyChannel('httpd',netsvc.LOG_WARNING,"No httpd available to register service %s",hts.path) + return + +import SimpleXMLRPCServer +class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): + rpc_paths = [] + protocol_version = 'HTTP/1.1' + def _dispatch(self, method, params): + try: + service_name = self.path.split("/")[-1] + return self.dispatch(service_name, method, params) + except netsvc.OpenERPDispatcherException, e: + raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback) + + def log_message(self, format, *args): + netsvc.Logger().notifyChannel('xmlrpc',netsvc.LOG_DEBUG_RPC,format % args) + + def handle(self): + pass + + def finish(self): + pass + + def setup(self): + self.connection = dummyconn() + if not len(XMLRPCRequestHandler.rpc_paths): + XMLRPCRequestHandler.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys()) + pass + + +def init_xmlrpc(): + if not tools.config.get_misc('xmlrpc','enable', True): + return + reg_http_service(HTTPDir('/xmlrpc/',XMLRPCRequestHandler)) + # Example of http file serving: + # reg_http_service(HTTPDir('/test/',HTTPHandler)) + print "Started XML-RPC" +#eof diff --git a/bin/service/web_services.py b/bin/service/web_services.py index 5710b7ffe09..c2d68cadbe2 100644 --- a/bin/service/web_services.py +++ b/bin/service/web_services.py @@ -41,28 +41,36 @@ import tools import locale logging.basicConfig() -class db(netsvc.Service): +class db(netsvc.ExportService): def __init__(self, name="db"): - netsvc.Service.__init__(self, name) + netsvc.ExportService.__init__(self, name) self.joinGroup("web-services") - self.exportMethod(self.create) - self.exportMethod(self.get_progress) - self.exportMethod(self.drop) - self.exportMethod(self.dump) - self.exportMethod(self.restore) - self.exportMethod(self.rename) - self.exportMethod(self.list) - self.exportMethod(self.list_lang) - self.exportMethod(self.change_admin_password) - self.exportMethod(self.server_version) - self.exportMethod(self.migrate_databases) self.actions = {} self.id = 0 self.id_protect = threading.Semaphore() self._pg_psw_env_var_is_set = False # on win32, pg_dump need the PGPASSWORD env var - def create(self, password, db_name, demo, lang, user_password='admin'): + def dispatch(self, method, auth, params): + if method in [ 'create', 'get_progress', 'drop', 'dump', + 'restore', 'rename', + 'change_admin_password', 'migrate_databases' ]: + passwd = params[0] + params = params[1:] + security.check_super(password) + elif method in [ 'db_exist', 'list', 'list_lang', 'server_version' ]: + # params = params + # No security check for these methods + pass + else: + raise KeyError("Method not found: %s" % method) + fn = getattr(self, 'exp_'+method) + return fn(*params) + + def new_dispatch(self,method,auth,params): + pass + + def exp_create(self, db_name, demo, lang, user_password='admin'): security.check_super(password) self.id_protect.acquire() self.id += 1 @@ -131,8 +139,7 @@ class db(netsvc.Service): self.actions[id]['thread'] = create_thread return id - def get_progress(self, password, id): - security.check_super(password) + def exp_get_progress(self, id): if self.actions[id]['thread'].isAlive(): # return addons.init_progress[db_name] return (min(self.actions[id].get('progress', 0),0.95), []) @@ -147,8 +154,7 @@ class db(netsvc.Service): del self.actions[id] raise Exception, e - def drop(self, password, db_name): - security.check_super(password) + def exp_drop(self, db_name): sql_db.close_db(db_name) logger = netsvc.Logger() @@ -180,8 +186,7 @@ class db(netsvc.Service): if os.name == 'nt' and self._pg_psw_env_var_is_set: os.environ['PGPASSWORD'] = '' - def dump(self, password, db_name): - security.check_super(password) + def exp_dump(self, db_name): logger = netsvc.Logger() self._set_pg_psw_env_var() @@ -210,7 +215,7 @@ class db(netsvc.Service): return base64.encodestring(data) - def restore(self, password, db_name, data): + def exp_restore(self, db_name, data): security.check_super(password) logger = netsvc.Logger() @@ -261,8 +266,7 @@ class db(netsvc.Service): return True - def rename(self, password, old_name, new_name): - security.check_super(password) + def exp_rename(self, old_name, new_name): sql_db.close_db(old_name) logger = netsvc.Logger() @@ -288,14 +292,14 @@ class db(netsvc.Service): sql_db.close_db('template1') return True - def db_exist(self, db_name): + def exp_db_exist(self, db_name): try: db = sql_db.db_connect(db_name) return True except: return False - def list(self): + def exp_list(self): db = sql_db.db_connect('template1') cr = db.cursor() try: @@ -321,27 +325,25 @@ class db(netsvc.Service): res.sort() return res - def change_admin_password(self, old_password, new_password): - security.check_super(old_password) + def exp_change_admin_password(self, new_password): tools.config['admin_passwd'] = new_password tools.config.save() return True - def list_lang(self): + def exp_list_lang(self): return tools.scan_languages() - def server_version(self): + def exp_server_version(self): """ Return the version of the server Used by the client to verify the compatibility with its own version """ return release.version - def migrate_databases(self, password, databases): + def exp_migrate_databases(self,databases): from osv.orm import except_orm from osv.osv import except_osv - security.check_super(password) l = netsvc.Logger() for db in databases: try: @@ -360,64 +362,74 @@ class db(netsvc.Service): return True db() -class common(netsvc.Service): +class _ObjectService(netsvc.ExportService): + "A common base class for those who have fn(db, uid, password,...) " + + def common_dispatch(self, method, auth, params): + (db, uid, passwd ) = params[0:3] + params = params[3:] + security.check(db,uid,passwd) + cr = pooler.get_db(db).cursor() + fn = getattr(self, 'exp_'+method) + res = fn(cr, uid, *params) + cr.commit() + cr.close() + return res + +class common(_ObjectService): def __init__(self,name="common"): - netsvc.Service.__init__(self,name) + _ObjectService.__init__(self,name) self.joinGroup("web-services") - self.exportMethod(self.ir_get) - self.exportMethod(self.ir_set) - self.exportMethod(self.ir_del) - self.exportMethod(self.about) - self.exportMethod(self.login) - self.exportMethod(self.logout) - self.exportMethod(self.timezone_get) - self.exportMethod(self.get_available_updates) - self.exportMethod(self.get_migration_scripts) - self.exportMethod(self.get_server_environment) - self.exportMethod(self.login_message) - self.exportMethod(self.set_loglevel) - def ir_set(self, db, uid, password, keys, args, name, value, replace=True, isobject=False): - security.check(db, uid, password) - cr = pooler.get_db(db).cursor() + def dispatch(self, method, auth, params): + logger = netsvc.Logger() + if method in [ 'ir_set','ir_del', 'ir_get' ]: + return self.common_dispatch(method,auth,params) + 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]) + 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']: + pass + elif method in ['get_available_updates', 'get_migration_scripts', 'set_loglevel']: + passwd = params[0] + params = params[1:] + security.check_super(passwd) + else: + raise Exception("Method not found: %s" % method) + + fn = getattr(self, 'exp_'+method) + return fn(*params) + + + def new_dispatch(self,method,auth,params): + pass + + def exp_ir_set(self, cr, uid, keys, args, name, value, replace=True, isobject=False): res = ir.ir_set(cr,uid, keys, args, name, value, replace, isobject) - cr.commit() - cr.close() return res - def ir_del(self, db, uid, password, id): - security.check(db, uid, password) - cr = pooler.get_db(db).cursor() + def exp_ir_del(self, cr, uid, id): res = ir.ir_del(cr,uid, id) - cr.commit() - cr.close() return res - def ir_get(self, db, uid, password, keys, args=None, meta=None, context=None): + def exp_ir_get(self, cr, uid, keys, args=None, meta=None, context=None): if not args: args=[] if not context: context={} - security.check(db, uid, password) - cr = pooler.get_db(db).cursor() res = ir.ir_get(cr,uid, keys, args, meta, context) - cr.commit() - cr.close() return res - def login(self, db, login, password): - res = security.login(db, login, password) - logger = netsvc.Logger() - msg = res and 'successful login' or 'bad login or password' - logger.notifyChannel("web-service", netsvc.LOG_INFO, "%s from '%s' using database '%s'" % (msg, login, db.lower())) - return res or False - - def logout(self, db, login, password): - logger = netsvc.Logger() - logger.notifyChannel("web-service", netsvc.LOG_INFO,'Logout %s from database %s'%(login,db)) - return True - - def about(self, extended=False): + def exp_about(self, extended=False): """Return information about the OpenERP Server. @param extended: if True then return version info @@ -437,12 +449,11 @@ GNU Public Licence. return info, release.version return info - def timezone_get(self, db, login, password): + def exp_timezone_get(self, db, login, password): return time.tzname[0] - def get_available_updates(self, password, contract_id, contract_password): - security.check_super(password) + def exp_get_available_updates(self, contract_id, contract_password): import tools.maintenance as tm try: rc = tm.remote_contract(contract_id, contract_password) @@ -455,8 +466,7 @@ GNU Public Licence. self.abortResponse(1, 'Migration Error', 'warning', str(e)) - def get_migration_scripts(self, password, contract_id, contract_password): - security.check_super(password) + def exp_get_migration_scripts(self, contract_id, contract_password): l = netsvc.Logger() import tools.maintenance as tm try: @@ -528,7 +538,7 @@ GNU Public Licence. l.notifyChannel('migration', netsvc.LOG_ERROR, tb_s) raise - def get_server_environment(self): + def exp_get_server_environment(self): os_lang = '.'.join( [x for x in locale.getdefaultlocale() if x] ) if not os_lang: os_lang = 'NOT SET' @@ -553,42 +563,36 @@ GNU Public Licence. return environment - def login_message(self): + def exp_login_message(self): return tools.config.get('login_message', False) - def set_loglevel(self, password, loglevel): - security.check_super(password) + def exp_set_loglevel(self,loglevel): l = netsvc.Logger() l.set_loglevel(int(loglevel)) return True common() -class objects_proxy(netsvc.Service): +class objects_proxy(netsvc.ExportService): def __init__(self, name="object"): - netsvc.Service.__init__(self,name) + netsvc.ExportService.__init__(self,name) self.joinGroup('web-services') - self.exportMethod(self.execute) - self.exportMethod(self.exec_workflow) - self.exportMethod(self.obj_list) - def exec_workflow(self, db, uid, passwd, object, method, id): - security.check(db, uid, passwd) - service = netsvc.LocalService("object_proxy") - res = service.exec_workflow(db, uid, object, method, id) - return res + def dispatch(self, method, auth, params): + (db, uid, passwd ) = params[0:3] + params = params[3:] + if method not in ['execute','exec_workflow','obj_list']: + raise KeyError("Method not supported %s" % method) + security.check(db,uid,passwd) + ls = netsvc.LocalService('object_proxy') + fn = getattr(ls, method) + res = fn(db, uid, *params) + return res - def execute(self, db, uid, passwd, object, method, *args): - security.check(db, uid, passwd) - service = netsvc.LocalService("object_proxy") - res = service.execute(db, uid, object, method, *args) - return res + + def new_dispatch(self,method,auth,params): + pass - def obj_list(self, db, uid, passwd): - security.check(db, uid, passwd) - service = netsvc.LocalService("object_proxy") - res = service.obj_list() - return res objects_proxy() @@ -603,26 +607,36 @@ objects_proxy() # Wizard datas: {} # TODO: change local request to OSE request/reply pattern # -class wizard(netsvc.Service): +class wizard(netsvc.ExportService): def __init__(self, name='wizard'): - netsvc.Service.__init__(self,name) + netsvc.ExportService.__init__(self,name) self.joinGroup('web-services') - self.exportMethod(self.execute) - self.exportMethod(self.create) self.id = 0 self.wiz_datas = {} self.wiz_name = {} self.wiz_uid = {} + def dispatch(self, method, auth, params): + (db, uid, passwd ) = params[0:3] + params = params[3:] + if method not in ['execute','create']: + raise KeyError("Method not supported %s" % method) + security.check(db,uid,passwd) + fn = getattr(self, 'exp_'+method) + res = fn(ls, db, uid, *params) + return res + + def new_dispatch(self,method,auth,params): + pass + def _execute(self, db, uid, wiz_id, datas, action, context): self.wiz_datas[wiz_id].update(datas) wiz = netsvc.LocalService('wizard.'+self.wiz_name[wiz_id]) return wiz.execute(db, uid, self.wiz_datas[wiz_id], action, context) - def create(self, db, uid, passwd, wiz_name, datas=None): + def exp_create(self, db, uid, wiz_name, datas=None): if not datas: datas={} - security.check(db, uid, passwd) #FIXME: this is not thread-safe self.id += 1 self.wiz_datas[self.id] = {} @@ -630,10 +644,9 @@ class wizard(netsvc.Service): self.wiz_uid[self.id] = uid return self.id - def execute(self, db, uid, passwd, wiz_id, datas, action='init', context=None): + def exp_execute(self, db, uid, wiz_id, datas, action='init', context=None): if not context: context={} - security.check(db, uid, passwd) if wiz_id in self.wiz_uid: if self.wiz_uid[wiz_id] == uid: @@ -657,22 +670,33 @@ class ExceptionWithTraceback(Exception): self.traceback = tb self.args = (msg, tb) -class report_spool(netsvc.Service): +class report_spool(netsvc.ExportService): def __init__(self, name='report'): - netsvc.Service.__init__(self, name) + netsvc.ExportService.__init__(self, name) self.joinGroup('web-services') - self.exportMethod(self.report) - self.exportMethod(self.report_get) self._reports = {} self.id = 0 self.id_protect = threading.Semaphore() - def report(self, db, uid, passwd, object, ids, datas=None, context=None): + def dispatch(self, method, auth, params): + (db, uid, passwd ) = params[0:3] + params = params[3:] + if method not in ['report','report_get']: + raise KeyError("Method not supported %s" % method) + security.check(db,uid,passwd) + fn = getattr(self, 'exp_' + method) + res = fn(db, uid, *params) + return res + + + def new_dispatch(self,method,auth,params): + pass + + def exp_report(self, db, uid, object, ids, datas=None, context=None): if not datas: datas={} if not context: context={} - security.check(db, uid, passwd) self.id_protect.acquire() self.id += 1 @@ -728,8 +752,7 @@ class report_spool(netsvc.Service): del self._reports[report_id] return res - def report_get(self, db, uid, passwd, report_id): - security.check(db, uid, passwd) + def exp_report_get(self, db, uid, report_id): if report_id in self._reports: if self._reports[report_id]['uid'] == uid: diff --git a/bin/service/websrv_lib.py b/bin/service/websrv_lib.py new file mode 100644 index 00000000000..d5c39473e54 --- /dev/null +++ b/bin/service/websrv_lib.py @@ -0,0 +1,357 @@ +# -*- encoding: utf-8 -*- + +# +# Copyright P. Christeas 2008,2009 +# +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################### + +""" Framework for generic http servers + +""" + + +class AuthRequiredExc(Exception): + def __init__(self,atype,realm): + Exception.__init__(self) + self.atype = atype + self.realm = realm + +class AuthRejectedExc(Exception): + pass + +class AuthProvider: + def __init__(self,realm): + self.realm = realm + + def setupAuth(self, multi,handler): + """ Attach an AuthProxy object to handler + """ + pass + + def authenticate(self, user, passwd, client_address): + #if user == 'user' and passwd == 'password': + # return (user, passwd) + #else: + return False + +class BasicAuthProvider(AuthProvider): + def setupAuth(self, multi, handler): + if not multi.sec_realms.has_key(self.realm): + multi.sec_realms[self.realm] = BasicAuthProxy(self) + + +class AuthProxy: + """ This class will hold authentication information for a handler, + i.e. a connection + """ + def __init__(self, provider): + self.provider = provider + + def checkRequest(self,handler,path = '/'): + """ Check if we are allowed to process that request + """ + pass + +import base64 +class BasicAuthProxy(AuthProxy): + """ Require basic authentication.. + """ + def __init__(self,provider): + AuthProxy.__init__(self,provider) + self.auth_creds = None + self.auth_tries = 0 + + def checkRequest(self,handler,path = '/'): + if self.auth_creds: + return True + auth_str = handler.headers.get('Authorization',False) + if auth_str and auth_str.startswith('Basic '): + auth_str=auth_str[len('Basic '):] + (user,passwd) = base64.decodestring(auth_str).split(':') + print "Found user=\"%s\", passwd=\"%s\"" %(user,passwd) + self.auth_creds = self.provider.authenticate(user,passwd,handler.client_address) + if self.auth_creds: + return True + if self.auth_tries > 5: + raise AuthRejectedExc("Authorization failed.") + self.auth_tries += 1 + raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm) + + +from BaseHTTPServer import * + +from SimpleHTTPServer import SimpleHTTPRequestHandler +class HTTPHandler(SimpleHTTPRequestHandler): + def __init__(self,request, client_address, server): + SimpleHTTPRequestHandler.__init__(self,request,client_address,server) + # print "Handler for %s inited" % str(client_address) + self.protocol_version = 'HTTP/1.1' + self.connection = dummyconn() + + def handle(self): + """ Classes here should NOT handle inside their constructor + """ + pass + + def finish(self): + pass + + def setup(self): + pass + +class HTTPDir: + """ A dispatcher class, like a virtual folder in httpd + """ + def __init__(self,path,handler, auth_provider = None): + self.path = path + self.handler = handler + self.auth_provider = auth_provider + + def matches(self, request): + """ Test if some request matches us. If so, return + the matched path. """ + if request.startswith(self.path): + return self.path + return False + +class noconnection: + """ a class to use instead of the real connection + """ + def makefile(self, mode, bufsize): + return None + +class dummyconn: + def shutdown(self, tru): + pass + +import SocketServer +class MultiHTTPHandler(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" + + def __init__(self, request, client_address, server): + self.in_handlers = {} + self.sec_realms = {} + 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, auth_provider): + """ 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 + self.request_version = fore.request_version + if auth_provider and auth_provider.realm: + try: + self.sec_realms[auth_provider.realm].checkRequest(fore,path) + except AuthRequiredExc,ae: + if self.request_version != 'HTTP/1.1': + self.log_error("Cannot require auth at %s",self.request_version) + self.send_error(401) + return + self.send_response(401,'Authorization required') + self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm)) + self.send_header('Content-Type','text/html') + self.send_header('Content-Length','0') + self.end_headers() + #self.wfile.write("\r\n") + return + except AuthRejectedExc,e: + self.send_error(401,e.args[0]) + self.close_connection = 1 + return + mname = 'do_' + fore.command + if not hasattr(fore, mname): + fore.send_error(501, "Unsupported method (%r)" % fore.command) + return + fore.close_connection = 0 + method = getattr(fore, mname) + method() + if fore.close_connection: + # print "Closing connection because of handler" + self.close_connection = fore.close_connection + + 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.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 + for vdir in self.server.vdirs: + p = vdir.matches(self.path) + if p == False: + continue + npath = self.path[len(p):] + if not npath.startswith('/'): + npath = '/' + npath + + if not self.in_handlers.has_key(p): + self.in_handlers[p] = vdir.handler(noconnection(),self.client_address,self.server) + if vdir.auth_provider: + vdir.auth_provider.setupAuth(self, self.in_handlers[p]) + hnd = self.in_handlers[p] + hnd.rfile = self.rfile + hnd.wfile = self.wfile + self.rlpath = self.raw_requestline + self._handle_one_foreign(hnd,npath, vdir.auth_provider) + # print "Handled, closing = ", self.close_connection + return + # if no match: + self.send_error(404, "Path not found: %s" % self.path) + return + + +class SecureMultiHTTPHandler(MultiHTTPHandler): + def setup(self): + import ssl + self.connection = ssl.wrap_socket(self.request, + server_side=True, + certfile="server.cert", + keyfile="server.key", + 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) + +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 _handle_request_noblock(self): + """Start a new thread to process the request.""" + t = threading.Thread(target = self._handle_request2) + print "request came, handling in new thread",t + if self.daemon_threads: + t.setDaemon (1) + t.start() + + 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: + request, client_address = self.get_request() + except socket.error: + return + if self.verify_request(request, client_address): + try: + self.process_request(request, client_address) + except: + self.handle_error(request, client_address) + self.close_request(request) + +#eof diff --git a/bin/tools/translate.py b/bin/tools/translate.py index 01789857a94..378c5994555 100644 --- a/bin/tools/translate.py +++ b/bin/tools/translate.py @@ -473,8 +473,8 @@ def trans_generate(lang, modules, dbname=None): push_translation(module, 'view', encode(obj.model), 0, t) elif model=='ir.actions.wizard': service_name = 'wizard.'+encode(obj.wiz_name) - if netsvc.SERVICES.get(service_name): - obj2 = netsvc.SERVICES[service_name] + if netsvc.Service._services.get(service_name): + obj2 = netsvc.Service._services[service_name] for state_name, state_def in obj2.states.iteritems(): if 'result' in state_def: result = state_def['result'] diff --git a/bin/wizard/__init__.py b/bin/wizard/__init__.py index 1621559c628..88a2c2470bf 100644 --- a/bin/wizard/__init__.py +++ b/bin/wizard/__init__.py @@ -44,7 +44,7 @@ class interface(netsvc.Service): states = {} def __init__(self, name): - assert not netsvc.service_exist('wizard.'+name), 'The wizard "%s" already exists!'%name + assert not self.service_exist('wizard.'+name), 'The wizard "%s" already exists!'%name super(interface, self).__init__('wizard.'+name) self.exportMethod(self.execute) self.wiz_name = name