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 """ This file contains instance of the http server.
33 from websrv_lib import *
34 import openerp.netsvc as netsvc
37 import openerp.tools as tools
46 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
54 from ssl import SSLError
56 class SSLError(Exception): pass
58 class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer):
59 """ A threaded httpd server, with all the necessary functionality for us.
61 It also inherits the xml-rpc dispatcher, so that some xml-rpc functions
62 will be available to the request handler
66 allow_reuse_address = 1
67 _send_traceback_header = False
70 def __init__(self, addr, requestHandler, proto='http',
71 logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
72 self.logRequests = logRequests
74 SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
75 HTTPServer.__init__(self, addr, requestHandler)
81 # [Bug #1222790] If possible, set close-on-exec flag; if a
82 # method spawns a subprocess, the subprocess shouldn't have
83 # the listening socket open.
84 if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
85 flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
86 flags |= fcntl.FD_CLOEXEC
87 fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
89 def handle_error(self, request, client_address):
90 """ Override the error handler
93 logging.getLogger("init").exception("Server error in request from %s:" % (client_address,))
95 def _mark_start(self, thread):
98 def _mark_end(self, thread):
102 def _get_next_name(self):
104 return 'http-client-%d' % self.__threadno
105 class HttpLogHandler:
106 """ helper class for uniform log handling
107 Please define self._logger at each class that is derived from this
111 def log_message(self, format, *args):
112 self._logger.debug(format % args) # todo: perhaps other level
114 def log_error(self, format, *args):
115 self._logger.error(format % args)
117 def log_exception(self, format, *args):
118 self._logger.exception(format, *args)
120 def log_request(self, code='-', size='-'):
121 self._logger.log(netsvc.logging.DEBUG_RPC, '"%s" %s %s',
122 self.requestline, str(code), str(size))
124 class MultiHandler2(HttpLogHandler, MultiHTTPHandler):
125 _logger = logging.getLogger('http')
128 class SecureMultiHandler2(HttpLogHandler, SecureMultiHTTPHandler):
129 _logger = logging.getLogger('https')
131 def getcert_fnames(self):
133 fcert = tc.get('secure_cert_file', 'server.cert')
134 fkey = tc.get('secure_pkey_file', 'server.key')
137 class BaseHttpDaemon(threading.Thread, netsvc.Server):
140 def __init__(self, interface, port, handler):
141 threading.Thread.__init__(self, name='%sDaemon-%d'%(self._RealProto, port))
142 netsvc.Server.__init__(self)
144 self.__interface = interface
147 self.server = ThreadedHTTPServer((interface, port), handler, proto=self._RealProto)
148 self.server.vdirs = []
149 self.server.logRequests = True
150 self.server.timeout = self._busywait_timeout
151 logging.getLogger("web-services").info(
152 "starting %s service at %s port %d" %
153 (self._RealProto, interface or '0.0.0.0', port,))
155 logging.getLogger("httpd").exception("Error occured when starting the server daemon.")
160 return self.server.socket
162 def attach(self, path, gw):
173 self.server.handle_request()
174 except (socket.error, select.error), e:
175 if self.running or e.args[0] != errno.EBADF:
180 res = "%sd: " % self._RealProto + ((self.running and "running") or "stopped")
182 res += ", %d threads" % (self.server.numThreads,)
185 def append_svc(self, service):
186 if not isinstance(service, HTTPDir):
187 raise Exception("Wrong class for http service")
189 pos = len(self.server.vdirs)
193 if self.server.vdirs[pos].matches(service.path):
195 # we won't break here, but search all way to the top, to
196 # ensure there is no lesser entry that will shadow the one
198 self.server.vdirs.insert(lastpos, service)
200 def list_services(self):
202 for svc in self.server.vdirs:
203 ret.append( ( svc.path, str(svc.handler)) )
208 class HttpDaemon(BaseHttpDaemon):
210 def __init__(self, interface, port):
211 super(HttpDaemon, self).__init__(interface, port,
212 handler=MultiHandler2)
214 class HttpSDaemon(BaseHttpDaemon):
216 def __init__(self, interface, port):
218 super(HttpSDaemon, self).__init__(interface, port,
219 handler=SecureMultiHandler2)
221 logging.getLogger('httpsd').exception( \
222 "Can not load the certificate and/or the private key files")
230 if tools.config.get('xmlrpc'):
231 httpd = HttpDaemon(tools.config.get('xmlrpc_interface', ''),
232 int(tools.config.get('xmlrpc_port', 8069)))
234 if tools.config.get('xmlrpcs'):
235 httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''),
236 int(tools.config.get('xmlrpcs_port', 8071)))
238 def reg_http_service(hts, secure_only = False):
239 """ Register some handler to httpd.
240 hts must be an HTTPDir
244 if httpd and not secure_only:
245 httpd.append_svc(hts)
248 httpsd.append_svc(hts)
250 if (not httpd) and (not httpsd):
251 logging.getLogger('httpd').warning("No httpd available to register service %s" % hts.path)
254 def list_http_services(protocol=None):
256 if httpd and (protocol == 'http' or protocol == None):
257 return httpd.list_services()
258 elif httpsd and (protocol == 'https' or protocol == None):
259 return httpsd.list_services()
261 raise Exception("Incorrect protocol or no http services")
263 import SimpleXMLRPCServer
264 class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
266 protocol_version = 'HTTP/1.1'
267 _logger = logging.getLogger('xmlrpc')
269 def _dispatch(self, method, params):
271 service_name = self.path.split("/")[-1]
272 return self.dispatch(service_name, method, params)
273 except netsvc.OpenERPDispatcherException, e:
274 raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback)
283 self.connection = dummyconn()
284 self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys())
288 if tools.config.get('xmlrpc', False):
289 # Example of http file serving:
290 # reg_http_service(HTTPDir('/test/',HTTPHandler))
291 reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler))
292 logging.getLogger("web-services").info("Registered XML-RPC over HTTP")
294 if tools.config.get('xmlrpcs', False) \
295 and not tools.config.get('xmlrpc', False):
296 # only register at the secure server
297 reg_http_service(HTTPDir('/xmlrpc/', XMLRPCRequestHandler), True)
298 logging.getLogger("web-services").info("Registered XML-RPC over HTTPS only")
300 class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
301 _logger = logging.getLogger('httpd')
302 _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] }
304 def __init__(self,request, client_address, server):
305 HTTPHandler.__init__(self,request,client_address,server)
306 document_root = tools.config.get('static_http_document_root', False)
307 assert document_root, "Please specify static_http_document_root in configuration, or disable static-httpd!"
308 self.__basepath = document_root
310 def translate_path(self, path):
311 """Translate a /-separated PATH to the local filename syntax.
313 Components that mean special things to the local file system
314 (e.g. drive or directory names) are ignored. (XXX They should
315 probably be diagnosed.)
318 # abandon query parameters
319 path = path.split('?',1)[0]
320 path = path.split('#',1)[0]
321 path = posixpath.normpath(urllib.unquote(path))
322 words = path.split('/')
323 words = filter(None, words)
324 path = self.__basepath
326 if word in (os.curdir, os.pardir): continue
327 path = os.path.join(path, word)
330 def init_static_http():
331 if not tools.config.get('static_http_enable', False):
334 document_root = tools.config.get('static_http_document_root', False)
335 assert document_root, "Document root must be specified explicitly to enable static HTTP service (option --static-http-document-root)"
337 base_path = tools.config.get('static_http_url_prefix', '/')
339 reg_http_service(HTTPDir(base_path,StaticHTTPHandler))
341 logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \
342 (document_root, base_path))
344 class OerpAuthProxy(AuthProxy):
345 """ Require basic authentication..
347 This is a copy of the BasicAuthProxy, which however checks/caches the db
350 def __init__(self,provider):
351 AuthProxy.__init__(self,provider)
354 self.last_auth = None
356 def checkRequest(self,handler,path, db=False):
357 auth_str = handler.headers.get('Authorization',False)
360 db = handler.get_db_from_path(path)
362 if path.startswith('/'):
369 self.provider.log("Wrong path: %s, failing auth" %path)
370 raise AuthRejectedExc("Authorization failed. Wrong sub-path.")
371 if self.auth_creds.get(db):
373 if auth_str and auth_str.startswith('Basic '):
374 auth_str=auth_str[len('Basic '):]
375 (user,passwd) = base64.decodestring(auth_str).split(':')
376 self.provider.log("Found user=\"%s\", passwd=\"***\" for db=\"%s\"" %(user,db))
377 acd = self.provider.authenticate(db,user,passwd,handler.client_address)
379 self.auth_creds[db] = acd
382 if self.auth_tries > 5:
383 self.provider.log("Failing authorization after 5 requests w/o password")
384 raise AuthRejectedExc("Authorization failed.")
386 raise AuthRequiredExc(atype='Basic', realm=self.provider.realm)
389 class OpenERPAuthProvider(AuthProvider):
390 def __init__(self,realm='OpenERP User'):
393 def setupAuth(self, multi, handler):
394 if not multi.sec_realms.has_key(self.realm):
395 multi.sec_realms[self.realm] = OerpAuthProxy(self)
396 handler.auth_proxy = multi.sec_realms[self.realm]
398 def authenticate(self, db, user, passwd, client_address):
400 uid = security.login(db,user,passwd)
403 return (user, passwd, db, uid)
405 logging.getLogger("auth").debug("Fail auth: %s" % e )
408 def log(self, msg, lvl=logging.INFO):
409 logging.getLogger("auth").log(lvl,msg)