Merged with stable
[odoo/odoo.git] / bin / service / http_server.py
1 # -*- encoding: utf-8 -*-
2
3 #
4 # Copyright P. Christeas <p_christ@hol.gr> 2008,2009
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 netsvc
35 import threading
36 import tools
37 import os
38 import socket
39 import xmlrpclib
40
41 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
42
43 try:
44     import fcntl
45 except ImportError:
46     fcntl = None
47     
48 try:
49     from ssl import SSLError
50 except ImportError:
51     class SSLError(Exception): pass
52
53 class ThreadedHTTPServer(ConnThreadingMixIn, SimpleXMLRPCDispatcher, HTTPServer):
54     """ A threaded httpd server, with all the necessary functionality for us.
55     
56         It also inherits the xml-rpc dispatcher, so that some xml-rpc functions
57         will be available to the request handler
58     """
59     encoding = None
60     allow_none = False
61     allow_reuse_address = 1
62     _send_traceback_header = False
63     i = 0
64
65     def __init__(self, addr, requestHandler,
66                  logRequests=True, allow_none=False, encoding=None, bind_and_activate=True):
67         self.logRequests = logRequests
68
69         SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding)
70         HTTPServer.__init__(self, addr, requestHandler)
71
72         # [Bug #1222790] If possible, set close-on-exec flag; if a
73         # method spawns a subprocess, the subprocess shouldn't have
74         # the listening socket open.
75         if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'):
76             flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD)
77             flags |= fcntl.FD_CLOEXEC
78             fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags)
79
80     def handle_error(self, request, client_address):
81         """ Override the error handler
82         """
83         import traceback
84         netsvc.Logger().notifyChannel("init", netsvc.LOG_ERROR,"Server error in request from %s:\n%s" %
85             (client_address,traceback.format_exc()))
86
87 class MultiHandler2(MultiHTTPHandler):
88     def log_message(self, format, *args):
89         netsvc.Logger().notifyChannel('http',netsvc.LOG_DEBUG,format % args)
90
91     def log_error(self, format, *args):
92         netsvc.Logger().notifyChannel('http',netsvc.LOG_ERROR,format % args)
93
94
95 class SecureMultiHandler2(SecureMultiHTTPHandler):
96     def log_message(self, format, *args):
97         netsvc.Logger().notifyChannel('https',netsvc.LOG_DEBUG,format % args)
98
99     def getcert_fnames(self):
100         tc = tools.config
101         fcert = tc.get_misc('httpsd','sslcert', 'ssl/server.cert')
102         fkey = tc.get_misc('httpsd','sslkey', 'ssl/server.key')
103         return (fcert,fkey)
104
105     def log_message(self, format, *args):
106         netsvc.Logger().notifyChannel('http',netsvc.LOG_DEBUG,format % args)
107
108     def log_error(self, format, *args):
109         netsvc.Logger().notifyChannel('http',netsvc.LOG_ERROR,format % args)
110
111 class BaseHttpDaemon(threading.Thread, netsvc.Server):
112     def __init__(self, interface, port, handler):
113         threading.Thread.__init__(self)
114         netsvc.Server.__init__(self)
115         self.__port = port
116         self.__interface = interface
117
118         try:
119             self.server = ThreadedHTTPServer((interface, port), handler)
120             self.server.vdirs = []
121             self.server.logRequests = True
122             netsvc.Logger().notifyChannel(
123                 "web-services", netsvc.LOG_INFO,
124                 "starting HTTPS service at %s port %d" %
125                 (interface or '0.0.0.0', port,))
126         except Exception, e:
127             netsvc.Logger().notifyChannel(
128                 'httpd', netsvc.LOG_CRITICAL,
129                 "Error occur when starting the server daemon: %s" % (e,))
130             raise
131
132     def attach(self, path, gw):
133         pass
134
135     def stop(self):
136         self.running = False
137         if os.name != 'nt':
138             try:
139                 self.server.socket.shutdown(
140                     getattr(socket, 'SHUT_RDWR', 2))
141             except socket.error, e:
142                 if e.errno != 57: raise
143                 # OSX, socket shutdowns both sides if any side closes it
144                 # causing an error 57 'Socket is not connected' on shutdown
145                 # of the other side (or something), see
146                 # http://bugs.python.org/issue4397
147                 netsvc.Logger().notifyChannel(
148                     'server', netsvc.LOG_DEBUG,
149                     '"%s" when shutting down server socket, '
150                     'this is normal under OS X'%e)
151         self.server.socket.close()
152
153     def run(self):
154         self.running = True
155         while self.running:
156             self.server.handle_request()
157         return True
158
159 class HttpDaemon(BaseHttpDaemon):
160     def __init__(self, interface, port):
161         super(HttpDaemon, self).__init__(interface, port,
162                                          handler=MultiHandler2)
163
164 class HttpSDaemon(BaseHttpDaemon):
165     def __init__(self, interface, port):
166         try:
167             super(HttpSDaemon, self).__init__(interface, port,
168                                               handler=SecureMultiHandler2)
169         except SSLError, e:
170             netsvc.Logger().notifyChannel(
171                 'httpd-ssl', netsvc.LOG_CRITICAL,
172                 "Can not load the certificate and/or the private key files")
173             raise
174
175 httpd = None
176 httpsd = None
177
178 def init_servers():
179     global httpd, httpsd
180     if tools.config.get_misc('httpd','enable', True):
181         httpd = HttpDaemon(tools.config.get_misc('httpd','interface', ''), \
182             int(tools.config.get_misc('httpd','port', tools.config.get('port',8069))))
183
184     if tools.config.get_misc('httpsd','enable', False):
185         httpsd = HttpSDaemon(tools.config.get_misc('httpsd','interface', ''), \
186             int(tools.config.get_misc('httpsd','port', 8071)))
187
188 def reg_http_service(hts, secure_only = False):
189     """ Register some handler to httpd.
190         hts must be an HTTPDir
191     """
192     global httpd, httpsd
193     if not isinstance(hts, HTTPDir):
194         raise Exception("Wrong class for http service")
195     
196     if httpd and not secure_only:
197         httpd.server.vdirs.append(hts)
198     
199     if httpsd:
200         httpsd.server.vdirs.append(hts)
201     
202     if (not httpd) and (not httpsd):
203         netsvc.Logger().notifyChannel('httpd',netsvc.LOG_WARNING,"No httpd available to register service %s" % hts.path)
204     return
205
206 import SimpleXMLRPCServer
207 class XMLRPCRequestHandler(netsvc.OpenERPDispatcher,FixSendError,SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
208     rpc_paths = [] 
209     protocol_version = 'HTTP/1.1'
210     def _dispatch(self, method, params):
211         try:
212             service_name = self.path.split("/")[-1]
213             return self.dispatch(service_name, method, params)
214         except netsvc.OpenERPDispatcherException, e:
215             raise xmlrpclib.Fault(tools.exception_to_unicode(e.exception), e.traceback)
216
217     def log_message(self, format, *args):
218         netsvc.Logger().notifyChannel('xmlrpc',netsvc.LOG_DEBUG_RPC,format % args)
219
220     def handle(self):
221         pass
222
223     def finish(self):
224         pass
225
226     def setup(self):
227         self.connection = dummyconn()
228         if not len(XMLRPCRequestHandler.rpc_paths):
229             XMLRPCRequestHandler.rpc_paths = map(lambda s: '/%s' % s, netsvc.ExportService._services.keys())
230         pass
231
232
233 def init_xmlrpc():
234     if not tools.config.get_misc('xmlrpc','enable', True):
235         return
236     reg_http_service(HTTPDir('/xmlrpc/',XMLRPCRequestHandler))
237     # Example of http file serving:
238     # reg_http_service(HTTPDir('/test/',HTTPHandler))
239     netsvc.Logger().notifyChannel("web-services", netsvc.LOG_INFO, 
240             "Registered XML-RPC over HTTP")
241             
242
243 class OerpAuthProxy(AuthProxy):
244     """ Require basic authentication..
245     
246         This is a copy of the BasicAuthProxy, which however checks/caches the db
247         as well.
248     """
249     def __init__(self,provider):
250         AuthProxy.__init__(self,provider)
251         self.auth_creds = {}
252         self.auth_tries = 0
253         self.last_auth = None
254
255     def checkRequest(self,handler,path = '/'):
256         if self.auth_creds:
257             return True
258         auth_str = handler.headers.get('Authorization',False)
259         try:
260             db = handler.get_db_from_path(path)
261             print "Got db:",db
262         except:
263             if path.startswith('/'):
264                 path = path[1:]
265             psp= path.split('/')
266             if len(psp)>1:
267                 db = psp[0]
268             else:
269                 #FIXME!
270                 self.provider.log("Wrong path: %s, failing auth" %path)
271                 raise AuthRejectedExc("Authorization failed. Wrong sub-path.")
272
273         if auth_str and auth_str.startswith('Basic '):
274             auth_str=auth_str[len('Basic '):]
275             (user,passwd) = base64.decodestring(auth_str).split(':')
276             self.provider.log("Found user=\"%s\", passwd=\"***\" for db=\"%s\"" %(user,db))
277             acd = self.provider.authenticate(db,user,passwd,handler.client_address)
278             if acd != False:
279                 self.auth_creds[db] = acd
280                 self.last_auth=db
281                 return True
282         if self.auth_tries > 5:
283             self.provider.log("Failing authorization after 5 requests w/o password")
284             raise AuthRejectedExc("Authorization failed.")
285         self.auth_tries += 1
286         raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm)
287
288 import security
289 class OpenERPAuthProvider(AuthProvider):
290     def __init__(self,realm = 'OpenERP User'):
291         self.realm = realm
292
293     def setupAuth(self, multi, handler):
294         if not multi.sec_realms.has_key(self.realm):
295             multi.sec_realms[self.realm] = OerpAuthProxy(self)
296         handler.auth_proxy = multi.sec_realms[self.realm]
297
298     def authenticate(self, db, user, passwd, client_address):
299         try:
300             uid = security.login(db,user,passwd)
301             if uid is False:
302                 return False
303             return (user, passwd, db, uid)
304         except Exception,e:
305             netsvc.Logger().notifyChannel("auth",netsvc.LOG_DEBUG,"Fail auth:"+ str(e))
306             return False
307         
308     def log(self, msg):
309         netsvc.Logger().notifyChannel("auth",netsvc.LOG_INFO,msg)
310
311 #eof