[REF] base: Changed code to avoid overriding of context values.
[odoo/odoo.git] / openerp / service / http_server.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright P. Christeas <p_christ@hol.gr> 2008-2010
4 # Copyright 2010 OpenERP SA. (http://www.openerp.com)
5 #
6 #
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
12 # Service Company
13 #
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.
18 #
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.
23 #
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 ###############################################################################
28
29 #.apidoc title: HTTP and XML-RPC Server
30
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.
34
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.
40 """
41
42 from websrv_lib import *
43 import openerp.netsvc as netsvc
44 import errno
45 import threading
46 import openerp.tools as tools
47 import posixpath
48 import urllib
49 import os
50 import select
51 import socket
52 import xmlrpclib
53 import logging
54
55 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
56
57 try:
58     import fcntl
59 except ImportError:
60     fcntl = None
61
62 try:
63     from ssl import SSLError
64 except ImportError:
65     class SSLError(Exception): pass
66
67 class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer):
68     """ A threaded httpd server, with all the necessary functionality for us.
69
70         It also inherits the xml-rpc dispatcher, so that some xml-rpc functions
71         will be available to the request handler
72     """
73     encoding = None
74     allow_none = False
75     allow_reuse_address = 1
76     _send_traceback_header = False
77     i = 0
78
79     def __init__(self, addr, requestHandler, proto='http',
80                  logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
81         self.logRequests = logRequests
82
83         SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
84         HTTPServer.__init__(self, addr, requestHandler)
85         
86         self.numThreads = 0
87         self.proto = proto
88         self.__threadno = 0
89
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)
97
98     def handle_error(self, request, client_address):
99         """ Override the error handler
100         """
101         
102         logging.getLogger("init").exception("Server error in request from %s:" % (client_address,))
103
104     def _mark_start(self, thread):
105         self.numThreads += 1
106
107     def _mark_end(self, thread):
108         self.numThreads -= 1
109
110
111     def _get_next_name(self):
112         self.__threadno += 1
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
117     """
118     _logger = None
119     
120     def log_message(self, format, *args):
121         self._logger.debug(format % args) # todo: perhaps other level
122
123     def log_error(self, format, *args):
124         self._logger.error(format % args)
125         
126     def log_exception(self, format, *args):
127         self._logger.exception(format, *args)
128
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))
132     
133 class MultiHandler2(HttpLogHandler, MultiHTTPHandler):
134     _logger = logging.getLogger('http')
135
136
137 class SecureMultiHandler2(HttpLogHandler, SecureMultiHTTPHandler):
138     _logger = logging.getLogger('https')
139
140     def getcert_fnames(self):
141         tc = tools.config
142         fcert = tc.get('secure_cert_file', 'server.cert')
143         fkey = tc.get('secure_pkey_file', 'server.key')
144         return (fcert,fkey)
145
146 class BaseHttpDaemon(threading.Thread, netsvc.Server):
147     _RealProto = '??'
148
149     def __init__(self, interface, port, handler):
150         threading.Thread.__init__(self, name='%sDaemon-%d'%(self._RealProto, port))
151         netsvc.Server.__init__(self)
152         self.__port = port
153         self.__interface = interface
154
155         try:
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,))
163         except Exception, e:
164             logging.getLogger("httpd").exception("Error occured when starting the server daemon.")
165             raise
166
167     @property
168     def socket(self):
169         return self.server.socket
170
171     def attach(self, path, gw):
172         pass
173
174     def stop(self):
175         self.running = False
176         self._close_socket()
177
178     def run(self):
179         self.running = True
180         while self.running:
181             try:
182                 self.server.handle_request()
183             except (socket.error, select.error), e:
184                 if self.running or e.args[0] != errno.EBADF:
185                     raise
186         return True
187
188     def stats(self):
189         res = "%sd: " % self._RealProto + ((self.running and "running") or  "stopped")
190         if self.server:
191             res += ", %d threads" % (self.server.numThreads,)
192         return res
193
194     def append_svc(self, service):
195         if not isinstance(service, HTTPDir):
196             raise Exception("Wrong class for http service")
197         
198         pos = len(self.server.vdirs)
199         lastpos = pos
200         while pos > 0:
201             pos -= 1
202             if self.server.vdirs[pos].matches(service.path):
203                 lastpos = pos
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
206             # we are inserting.
207         self.server.vdirs.insert(lastpos, service)
208
209     def list_services(self):
210         ret = []
211         for svc in self.server.vdirs:
212             ret.append( ( svc.path, str(svc.handler)) )
213         
214         return ret
215     
216
217 class HttpDaemon(BaseHttpDaemon):
218     _RealProto = 'HTTP'
219     def __init__(self, interface, port):
220         super(HttpDaemon, self).__init__(interface, port,
221                                          handler=MultiHandler2)
222
223 class HttpSDaemon(BaseHttpDaemon):
224     _RealProto = 'HTTPS'
225     def __init__(self, interface, port):
226         try:
227             super(HttpSDaemon, self).__init__(interface, port,
228                                               handler=SecureMultiHandler2)
229         except SSLError, e:
230             logging.getLogger('httpsd').exception( \
231                         "Can not load the certificate and/or the private key files")
232             raise
233
234 httpd = None
235 httpsd = None
236
237 def init_servers():
238     global httpd, httpsd
239     if tools.config.get('xmlrpc'):
240         httpd = HttpDaemon(tools.config.get('xmlrpc_interface', ''),
241                            int(tools.config.get('xmlrpc_port', 8069)))
242
243     if tools.config.get('xmlrpcs'):
244         httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''),
245                              int(tools.config.get('xmlrpcs_port', 8071)))
246
247 def reg_http_service(hts, secure_only = False):
248     """ Register some handler to httpd.
249         hts must be an HTTPDir
250     """
251     global httpd, httpsd
252
253     if httpd and not secure_only:
254         httpd.append_svc(hts)
255
256     if httpsd:
257         httpsd.append_svc(hts)
258
259     if (not httpd) and (not httpsd):
260         logging.getLogger('httpd').warning("No httpd available to register service %s" % hts.path)
261     return
262
263 def list_http_services(protocol=None):
264     global httpd, httpsd
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()
269     else:
270         raise Exception("Incorrect protocol or no http services")
271
272 import SimpleXMLRPCServer
273 class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
274     rpc_paths = []
275     protocol_version = 'HTTP/1.1'
276     _logger = logging.getLogger('xmlrpc')
277
278     def _dispatch(self, method, params):
279         try:
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)
284
285     def handle(self):
286         pass
287
288     def finish(self):
289         pass
290
291     def setup(self):
292         self.connection = dummyconn()
293         self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys())
294
295
296 def init_xmlrpc():
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")
302
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")
308
309 class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
310     _logger = logging.getLogger('httpd')
311     _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] }
312
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
318
319     def translate_path(self, path):
320         """Translate a /-separated PATH to the local filename syntax.
321
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.)
325
326         """
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
334         for word in words:
335             if word in (os.curdir, os.pardir): continue
336             path = os.path.join(path, word)
337         return path
338
339 def init_static_http():
340     if not tools.config.get('static_http_enable', False):
341         return
342     
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)"
345     
346     base_path = tools.config.get('static_http_url_prefix', '/')
347     
348     reg_http_service(HTTPDir(base_path,StaticHTTPHandler))
349     
350     logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \
351                         (document_root, base_path))
352
353 class OerpAuthProxy(AuthProxy):
354     """ Require basic authentication..
355
356         This is a copy of the BasicAuthProxy, which however checks/caches the db
357         as well.
358     """
359     def __init__(self,provider):
360         AuthProxy.__init__(self,provider)
361         self.auth_creds = {}
362         self.auth_tries = 0
363         self.last_auth = None
364
365     def checkRequest(self,handler,path, db=False):        
366         auth_str = handler.headers.get('Authorization',False)
367         try:
368             if not db:
369                 db = handler.get_db_from_path(path)
370         except Exception:
371             if path.startswith('/'):
372                 path = path[1:]
373             psp= path.split('/')
374             if len(psp)>1:
375                 db = psp[0]
376             else:
377                 #FIXME!
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):
381             return True 
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)
387             if acd != False:
388                 self.auth_creds[db] = acd
389                 self.last_auth = db
390                 return True
391         if self.auth_tries > 5:
392             self.provider.log("Failing authorization after 5 requests w/o password")
393             raise AuthRejectedExc("Authorization failed.")
394         self.auth_tries += 1
395         raise AuthRequiredExc(atype='Basic', realm=self.provider.realm)
396
397 import security
398 class OpenERPAuthProvider(AuthProvider):
399     def __init__(self,realm='OpenERP User'):
400         self.realm = realm
401
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]
406
407     def authenticate(self, db, user, passwd, client_address):
408         try:
409             uid = security.login(db,user,passwd)
410             if uid is False:
411                 return False
412             return (user, passwd, db, uid)
413         except Exception,e:
414             logging.getLogger("auth").debug("Fail auth: %s" % e )
415             return False
416
417     def log(self, msg, lvl=logging.INFO):
418         logging.getLogger("auth").log(lvl,msg)
419
420 #eof