[MAJOR IMP] Rewrite the http/RPC engine and let HTTP/1.1 features.

This patch attempts a major change in the structure of the XML-RPC
framework. There is one http server, capable of multiple services
(except XML-RPC). That server could handle authentication, and is also
HTTP/1.1 capable, which means it supports **persistent connections** !

At this commit, the old behaviour of the XML-RPC protocol is merely
working. The netsvc.Service is split, so expect wizard/report breakages.
External modules (koo) also break with this API.

The net-svc code is crippled and gone FTM.

bzr revid: p_christ@hol.gr-20090829152346-7i1iiqs8skdddamq
This commit is contained in:
P. Christeas 2009-08-29 18:23:46 +03:00
parent 8558374c2b
commit 4abaf2763e
9 changed files with 831 additions and 362 deletions

View File

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

View File

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

View File

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

View File

@ -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'],

211
bin/service/http_server.py Normal file
View File

@ -0,0 +1,211 @@
# -*- encoding: utf-8 -*-
#
# Copyright P. Christeas <p_christ@hol.gr> 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

View File

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

357
bin/service/websrv_lib.py Normal file
View File

@ -0,0 +1,357 @@
# -*- encoding: utf-8 -*-
#
# Copyright P. Christeas <p_christ@hol.gr> 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

View File

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

View File

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