[FIX] service: put autocommit true on EDI import service to store imported data
[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 """ This file contains instance of the http server.
30
31
32 """
33 from websrv_lib import *
34 import openerp.netsvc as netsvc
35 import errno
36 import threading
37 import openerp.tools as tools
38 import posixpath
39 import urllib
40 import os
41 import select
42 import socket
43 import xmlrpclib
44 import logging
45
46 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
47
48 try:
49     import fcntl
50 except ImportError:
51     fcntl = None
52
53 try:
54     from ssl import SSLError
55 except ImportError:
56     class SSLError(Exception): pass
57
58 class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer):
59     """ A threaded httpd server, with all the necessary functionality for us.
60
61         It also inherits the xml-rpc dispatcher, so that some xml-rpc functions
62         will be available to the request handler
63     """
64     encoding = None
65     allow_none = False
66     allow_reuse_address = 1
67     _send_traceback_header = False
68     i = 0
69
70     def __init__(self, addr, requestHandler, proto='http',
71                  logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
72         self.logRequests = logRequests
73
74         SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
75         HTTPServer.__init__(self, addr, requestHandler)
76         
77         self.numThreads = 0
78         self.proto = proto
79         self.__threadno = 0
80
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)
88
89     def handle_error(self, request, client_address):
90         """ Override the error handler
91         """
92         
93         logging.getLogger("init").exception("Server error in request from %s:" % (client_address,))
94
95     def _mark_start(self, thread):
96         self.numThreads += 1
97
98     def _mark_end(self, thread):
99         self.numThreads -= 1
100
101
102     def _get_next_name(self):
103         self.__threadno += 1
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
108     """
109     _logger = None
110     
111     def log_message(self, format, *args):
112         self._logger.debug(format % args) # todo: perhaps other level
113
114     def log_error(self, format, *args):
115         self._logger.error(format % args)
116         
117     def log_exception(self, format, *args):
118         self._logger.exception(format, *args)
119
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))
123     
124 class MultiHandler2(HttpLogHandler, MultiHTTPHandler):
125     _logger = logging.getLogger('http')
126
127
128 class SecureMultiHandler2(HttpLogHandler, SecureMultiHTTPHandler):
129     _logger = logging.getLogger('https')
130
131     def getcert_fnames(self):
132         tc = tools.config
133         fcert = tc.get('secure_cert_file', 'server.cert')
134         fkey = tc.get('secure_pkey_file', 'server.key')
135         return (fcert,fkey)
136
137 class BaseHttpDaemon(threading.Thread, netsvc.Server):
138     _RealProto = '??'
139
140     def __init__(self, interface, port, handler):
141         threading.Thread.__init__(self, name='%sDaemon-%d'%(self._RealProto, port))
142         netsvc.Server.__init__(self)
143         self.__port = port
144         self.__interface = interface
145
146         try:
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,))
154         except Exception, e:
155             logging.getLogger("httpd").exception("Error occured when starting the server daemon.")
156             raise
157
158     @property
159     def socket(self):
160         return self.server.socket
161
162     def attach(self, path, gw):
163         pass
164
165     def stop(self):
166         self.running = False
167         self._close_socket()
168
169     def run(self):
170         self.running = True
171         while self.running:
172             try:
173                 self.server.handle_request()
174             except (socket.error, select.error), e:
175                 if self.running or e.args[0] != errno.EBADF:
176                     raise
177         return True
178
179     def stats(self):
180         res = "%sd: " % self._RealProto + ((self.running and "running") or  "stopped")
181         if self.server:
182             res += ", %d threads" % (self.server.numThreads,)
183         return res
184
185     def append_svc(self, service):
186         if not isinstance(service, HTTPDir):
187             raise Exception("Wrong class for http service")
188         
189         pos = len(self.server.vdirs)
190         lastpos = pos
191         while pos > 0:
192             pos -= 1
193             if self.server.vdirs[pos].matches(service.path):
194                 lastpos = pos
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
197             # we are inserting.
198         self.server.vdirs.insert(lastpos, service)
199
200     def list_services(self):
201         ret = []
202         for svc in self.server.vdirs:
203             ret.append( ( svc.path, str(svc.handler)) )
204         
205         return ret
206     
207
208 class HttpDaemon(BaseHttpDaemon):
209     _RealProto = 'HTTP'
210     def __init__(self, interface, port):
211         super(HttpDaemon, self).__init__(interface, port,
212                                          handler=MultiHandler2)
213
214 class HttpSDaemon(BaseHttpDaemon):
215     _RealProto = 'HTTPS'
216     def __init__(self, interface, port):
217         try:
218             super(HttpSDaemon, self).__init__(interface, port,
219                                               handler=SecureMultiHandler2)
220         except SSLError, e:
221             logging.getLogger('httpsd').exception( \
222                         "Can not load the certificate and/or the private key files")
223             raise
224
225 httpd = None
226 httpsd = None
227
228 def init_servers():
229     global httpd, httpsd
230     if tools.config.get('xmlrpc'):
231         httpd = HttpDaemon(tools.config.get('xmlrpc_interface', ''),
232                            int(tools.config.get('xmlrpc_port', 8069)))
233
234     if tools.config.get('xmlrpcs'):
235         httpsd = HttpSDaemon(tools.config.get('xmlrpcs_interface', ''),
236                              int(tools.config.get('xmlrpcs_port', 8071)))
237
238 def reg_http_service(hts, secure_only = False):
239     """ Register some handler to httpd.
240         hts must be an HTTPDir
241     """
242     global httpd, httpsd
243
244     if httpd and not secure_only:
245         httpd.append_svc(hts)
246
247     if httpsd:
248         httpsd.append_svc(hts)
249
250     if (not httpd) and (not httpsd):
251         logging.getLogger('httpd').warning("No httpd available to register service %s" % hts.path)
252     return
253
254 def list_http_services(protocol=None):
255     global httpd, httpsd
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()
260     else:
261         raise Exception("Incorrect protocol or no http services")
262
263 import SimpleXMLRPCServer
264 class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,HttpLogHandler,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
265     rpc_paths = []
266     protocol_version = 'HTTP/1.1'
267     _logger = logging.getLogger('xmlrpc')
268
269     def _dispatch(self, method, params):
270         try:
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)
275
276     def handle(self):
277         pass
278
279     def finish(self):
280         pass
281
282     def setup(self):
283         self.connection = dummyconn()
284         self.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys())
285
286
287 def init_xmlrpc():
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")
293
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")
299
300 class StaticHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler):
301     _logger = logging.getLogger('httpd')
302     _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD'] }
303
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
309
310     def translate_path(self, path):
311         """Translate a /-separated PATH to the local filename syntax.
312
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.)
316
317         """
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
325         for word in words:
326             if word in (os.curdir, os.pardir): continue
327             path = os.path.join(path, word)
328         return path
329
330 def init_static_http():
331     if not tools.config.get('static_http_enable', False):
332         return
333     
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)"
336     
337     base_path = tools.config.get('static_http_url_prefix', '/')
338     
339     reg_http_service(HTTPDir(base_path,StaticHTTPHandler))
340     
341     logging.getLogger("web-services").info("Registered HTTP dir %s for %s" % \
342                         (document_root, base_path))
343
344 class OerpAuthProxy(AuthProxy):
345     """ Require basic authentication..
346
347         This is a copy of the BasicAuthProxy, which however checks/caches the db
348         as well.
349     """
350     def __init__(self,provider):
351         AuthProxy.__init__(self,provider)
352         self.auth_creds = {}
353         self.auth_tries = 0
354         self.last_auth = None
355
356     def checkRequest(self,handler,path, db=False):        
357         auth_str = handler.headers.get('Authorization',False)
358         try:
359             if not db:
360                 db = handler.get_db_from_path(path)
361         except Exception:
362             if path.startswith('/'):
363                 path = path[1:]
364             psp= path.split('/')
365             if len(psp)>1:
366                 db = psp[0]
367             else:
368                 #FIXME!
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):
372             return True 
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)
378             if acd != False:
379                 self.auth_creds[db] = acd
380                 self.last_auth = db
381                 return True
382         if self.auth_tries > 5:
383             self.provider.log("Failing authorization after 5 requests w/o password")
384             raise AuthRejectedExc("Authorization failed.")
385         self.auth_tries += 1
386         raise AuthRequiredExc(atype='Basic', realm=self.provider.realm)
387
388 import security
389 class OpenERPAuthProvider(AuthProvider):
390     def __init__(self,realm='OpenERP User'):
391         self.realm = realm
392
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]
397
398     def authenticate(self, db, user, passwd, client_address):
399         try:
400             uid = security.login(db,user,passwd)
401             if uid is False:
402                 return False
403             return (user, passwd, db, uid)
404         except Exception,e:
405             logging.getLogger("auth").debug("Fail auth: %s" % e )
406             return False
407
408     def log(self, msg, lvl=logging.INFO):
409         logging.getLogger("auth").log(lvl,msg)
410
411 #eof