1 # -*- coding: utf-8 -*-
3 # Copyright P. Christeas <p_christ@hol.gr> 2008-2010
4 # Copyright 2010 OpenERP SA. (http://www.openerp.com)
7 # WARNING: This program as such is intended to be used by professional
8 # programmers who take the whole responsability of assessing all potential
9 # consequences resulting from its eventual inadequacies and bugs
10 # End users who are looking for a ready-to-use solution with commercial
11 # garantees and support are strongly adviced to contract a Free Software
14 # This program is Free Software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 2
17 # of the License, or (at your option) any later version.
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
27 ###############################################################################
29 #.apidoc title: HTTP and XML-RPC Server
31 """ This module offers the family of HTTP-based servers. These are not a single
32 class/functionality, but a set of network stack layers, implementing
33 extendable HTTP protocols.
35 The OpenERP server defines a single instance of a HTTP server, listening at
36 the standard 8069, 8071 ports (well, it is 2 servers, and ports are
37 configurable, of course). This "single" server then uses a `MultiHTTPHandler`
38 to dispatch requests to the appropriate channel protocol, like the XML-RPC,
39 static HTTP, DAV or other.
42 from websrv_lib import *
43 import openerp.netsvc as netsvc
46 import openerp.tools as tools
55 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
63 from ssl import SSLError
65 class SSLError(Exception): pass
67 class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer):
68 """ A threaded httpd server, with all the necessary functionality for us.
70 It also inherits the xml-rpc dispatcher, so that some xml-rpc functions
71 will be available to the request handler
75 allow_reuse_address = 1
76 _send_traceback_header = False
79 def __init__(self, addr, requestHandler, proto='http',
80 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
81 self.logRequests = logRequests
83 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
84 HTTPServer.__init__(self, addr, requestHandler)
90 # [Bug #1222790] If possible, set close-on-exec flag; if a
91 # method spawns a subprocess, the subprocess shouldn't have
92 # the listening socket open.
93 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
94 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
95 flags |= fcntl.FD_CLOEXEC
96 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
98 def handle_error(self, request, client_address):
99 """ Override the error handler
102 logging.getLogger("init").exception("Server error in request from %s:" % (client_address,))
104 def _mark_start(self, thread):
107 def _mark_end(self, thread):
111 def _get_next_name(self):
113 return 'http-client-%d' % self.__threadno
114 class HttpLogHandler:
115 """ helper class for uniform log handling
116 Please define self._logger at each class that is derived from this
120 def log_message(self, format, *args):
121 self._logger.debug(format % args) # todo: perhaps other level
123 def log_error(self, format, *args):
124 self._logger.error(format % args)
126 def log_exception(self, format, *args):
127 self._logger.exception(format, *args)
129 def log_request(self, code='-', size='-'):
130 self._logger.log(netsvc.logging.DEBUG_RPC, '"%s" %s %s',
131 self.requestline, str(code), str(size))
133 class MultiHandler2(HttpLogHandler, MultiHTTPHandler):
134 _logger = logging.getLogger('http')
137 class SecureMultiHandler2(HttpLogHandler, SecureMultiHTTPHandler):
138 _logger = logging.getLogger('https')
140 def getcert_fnames(self):
142 fcert = tc.get('secure_cert_file', 'server.cert')
143 fkey = tc.get('secure_pkey_file', 'server.key')
146 class BaseHttpDaemon(threading.Thread, netsvc.Server):
149 def __init__(self, interface, port, handler):
150 threading.Thread.__init__(self, name='%sDaemon-%d'%(self._RealProto, port))
151 netsvc.Server.__init__(self)
153 self.__interface = interface
156 self.server = ThreadedHTTPServer((interface, port), handler, proto=self._RealProto)
157 self.server.vdirs = []
158 self.server.logRequests = True
159 self.server.timeout = self._busywait_timeout
160 logging.getLogger("web-services").info(
161 "starting %s service at %s port %d" %
162 (self._RealProto, interface or '0.0.0.0', port,))
164 logging.getLogger("httpd").exception("Error occured when starting the server daemon.")
169 return self.server.socket
171 def attach(self, path, gw):
182 self.server.handle_request()
183 except (socket.error, select.error), e:
184 if self.running or e.args[0] != errno.EBADF:
189 res = "%sd: " % self._RealProto + ((self.running and "running") or "stopped")
191 res += ", %d threads" % (self.server.numThreads,)
194 def append_svc(self, service):
195 if not isinstance(service, HTTPDir):
196 raise Exception("Wrong class for http service")
198 pos = len(self.server.vdirs)
202 if self.server.vdirs[pos].matches(service.path):
204 # we won't break here, but search all way to the top, to
205 # ensure there is no lesser entry that will shadow the one
207 self.server.vdirs.insert(lastpos, service)
209 def list_services(self):
211 for svc in self.server.vdirs:
212 ret.append( ( svc.path, str(svc.handler)) )
217 class HttpDaemon(BaseHttpDaemon):
219 def __init__(self, interface, port):
220 super(HttpDaemon, self).__init__(interface, port,
221 handler=MultiHandler2)
223 class HttpSDaemon(BaseHttpDaemon):
225 def __init__(self, interface, port):
227 super(HttpSDaemon, self).__init__(interface, port,
228 handler=SecureMultiHandler2)
230 logging.getLogger('httpsd').exception( \
231 "Can not load the certificate and/or the private key files")
239 if tools.config.get('xmlrpc'):
240 httpd = HttpDaemon(tools.config.get('xmlrpc_interface', ''),
241 int(tools.config.get('xmlrpc_port', 8069)))
243 if tools.config.get('xmlrpcs'):
244 httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''),
245 int(tools.config.get('xmlrpcs_port', 8071)))
247 def reg_http_service(hts, secure_only = False):
248 """ Register some handler to httpd.
249 hts must be an HTTPDir
253 if httpd and not secure_only:
254 httpd.append_svc(hts)
257 httpsd.append_svc(hts)
259 if (not httpd) and (not httpsd):
260 logging.getLogger('httpd').warning("No httpd available to register service %s" % hts.path)
263 def list_http_services(protocol=None):
265 if httpd and (protocol == 'http' or protocol == None):
266 return httpd.list_services()
267 elif httpsd and (protocol == 'https' or protocol == None):
268 return httpsd.list_services()
270 raise Exception("Incorrect protocol or no http services")
272 import SimpleXMLRPCServer
273 class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
275 protocol_version = 'HTTP/1.1'
276 _logger = logging.getLogger('xmlrpc')
278 def _dispatch(self, method, params):
280 service_name = self.path.split("/")[-1]
281 return self.dispatch(service_name, method, params)
282 except netsvc.OpenERPDispatcherException, e:
283 raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback)
292 self.connection = dummyconn()
293 self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys())
297 if tools.config.get('xmlrpc', False):
298 # Example of http file serving:
299 # reg_http_service(HTTPDir('/test/',HTTPHandler))
300 reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler))
301 logging.getLogger("web-services").info("Registered XML-RPC over HTTP")
303 if tools.config.get('xmlrpcs', False) \
304 and not tools.config.get('xmlrpc', False):
305 # only register at the secure server
306 reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler), True)
307 logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only")
309 class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
310 _logger = logging.getLogger('httpd')
311 _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] }
313 def __init__(self,request, client_address, server):
314 HTTPHandler.__init__(self,request,client_address,server)
315 document_root = tools.config.get('static_http_document_root', False)
316 assert document_root, "Please specify static_http_document_root in configuration, or disable static-httpd!"
317 self.__basepath = document_root
319 def translate_path(self, path):
320 """Translate a /-separated PATH to the local filename syntax.
322 Components that mean special things to the local file system
323 (e.g. drive or directory names) are ignored. (XXX They should
324 probably be diagnosed.)
327 # abandon query parameters
328 path = path.split('?',1)[0]
329 path = path.split('#',1)[0]
330 path = posixpath.normpath(urllib.unquote(path))
331 words = path.split('/')
332 words = filter(None, words)
333 path = self.__basepath
335 if word in (os.curdir, os.pardir): continue
336 path = os.path.join(path, word)
339 def init_static_http():
340 if not tools.config.get('static_http_enable', False):
343 document_root = tools.config.get('static_http_document_root', False)
344 assert document_root, "Document root must be specified explicitly to enable static HTTP service (option --static-http-document-root)"
346 base_path = tools.config.get('static_http_url_prefix', '/')
348 reg_http_service(HTTPDir(base_path,StaticHTTPHandler))
350 logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \
351 (document_root, base_path))
353 class OerpAuthProxy(AuthProxy):
354 """ Require basic authentication..
356 This is a copy of the BasicAuthProxy, which however checks/caches the db
359 def __init__(self,provider):
360 AuthProxy.__init__(self,provider)
363 self.last_auth = None
365 def checkRequest(self,handler,path, db=False):
366 auth_str = handler.headers.get('Authorization',False)
369 db = handler.get_db_from_path(path)
371 if path.startswith('/'):
378 self.provider.log("Wrong path: %s, failing auth" %path)
379 raise AuthRejectedExc("Authorization failed. Wrong sub-path.")
380 if self.auth_creds.get(db):
382 if auth_str and auth_str.startswith('Basic '):
383 auth_str=auth_str[len('Basic '):]
384 (user,passwd) = base64.decodestring(auth_str).split(':')
385 self.provider.log("Found user=\"%s\", passwd=\"***\" for db=\"%s\"" %(user,db))
386 acd = self.provider.authenticate(db,user,passwd,handler.client_address)
388 self.auth_creds[db] = acd
391 if self.auth_tries > 5:
392 self.provider.log("Failing authorization after 5 requests w/o password")
393 raise AuthRejectedExc("Authorization failed.")
395 raise AuthRequiredExc(atype='Basic', realm=self.provider.realm)
398 class OpenERPAuthProvider(AuthProvider):
399 def __init__(self,realm='OpenERP User'):
402 def setupAuth(self, multi, handler):
403 if not multi.sec_realms.has_key(self.realm):
404 multi.sec_realms[self.realm] = OerpAuthProxy(self)
405 handler.auth_proxy = multi.sec_realms[self.realm]
407 def authenticate(self, db, user, passwd, client_address):
409 uid = security.login(db,user,passwd)
412 return (user, passwd, db, uid)
414 logging.getLogger("auth").debug("Fail auth: %s" % e )
417 def log(self, msg, lvl=logging.INFO):
418 logging.getLogger("auth").log(lvl,msg)