[IMP] db creation: cleanup logging and exception traceback reification
[odoo/odoo.git] / openerp / service / websrv_lib.py
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright P. Christeas <p_christ@hol.gr> 2008-2010
4 #
5 # WARNING: This program as such is intended to be used by professional
6 # programmers who take the whole responsability of assessing all potential
7 # consequences resulting from its eventual inadequacies and bugs
8 # End users who are looking for a ready-to-use solution with commercial
9 # garantees and support are strongly adviced to contract a Free Software
10 # Service Company
11 #
12 # This program is Free Software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; either version 2
15 # of the License, or (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
25 ###############################################################################
26
27 #.apidoc title: HTTP Layer library (websrv_lib)
28
29 """ Framework for generic http servers
30
31     This library contains *no* OpenERP-specific functionality. It should be
32     usable in other projects, too.
33 """
34
35 import socket
36 import base64
37 import errno
38 import SocketServer
39 from BaseHTTPServer import *
40 from SimpleHTTPServer import SimpleHTTPRequestHandler
41
42 class AuthRequiredExc(Exception):
43     def __init__(self,atype,realm):
44         Exception.__init__(self)
45         self.atype = atype
46         self.realm = realm
47
48 class AuthRejectedExc(Exception):
49     pass
50
51 class AuthProvider:
52     def __init__(self,realm):
53         self.realm = realm
54
55     def setupAuth(self, multi,handler):
56         """ Attach an AuthProxy object to handler
57         """
58         pass
59
60     def authenticate(self, user, passwd, client_address):
61         return False
62
63     def log(self, msg):
64         print msg
65
66 class BasicAuthProvider(AuthProvider):
67     def setupAuth(self, multi, handler):
68         if not multi.sec_realms.has_key(self.realm):
69             multi.sec_realms[self.realm] = BasicAuthProxy(self)
70
71
72 class AuthProxy:
73     """ This class will hold authentication information for a handler,
74         i.e. a connection
75     """
76     def __init__(self, provider):
77         self.provider = provider
78
79     def checkRequest(self,handler,path = '/'):
80         """ Check if we are allowed to process that request
81         """
82         pass
83
84 class BasicAuthProxy(AuthProxy):
85     """ Require basic authentication..
86     """
87     def __init__(self,provider):
88         AuthProxy.__init__(self,provider)
89         self.auth_creds = None
90         self.auth_tries = 0
91
92     def checkRequest(self,handler,path = '/'):
93         if self.auth_creds:
94             return True
95         auth_str = handler.headers.get('Authorization',False)
96         if auth_str and auth_str.startswith('Basic '):
97             auth_str=auth_str[len('Basic '):]
98             (user,passwd) = base64.decodestring(auth_str).split(':')
99             self.provider.log("Found user=\"%s\", passwd=\"%s\"" %(user,passwd))
100             self.auth_creds = self.provider.authenticate(user,passwd,handler.client_address)
101             if self.auth_creds:
102                 return True
103         if self.auth_tries > 5:
104             self.provider.log("Failing authorization after 5 requests w/o password")
105             raise AuthRejectedExc("Authorization failed.")
106         self.auth_tries += 1
107         raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm)
108
109
110 class HTTPHandler(SimpleHTTPRequestHandler):
111     def __init__(self,request, client_address, server):
112         SimpleHTTPRequestHandler.__init__(self,request,client_address,server)
113         # print "Handler for %s inited" % str(client_address)
114         self.protocol_version = 'HTTP/1.1'
115         self.connection = dummyconn()
116
117     def handle(self):
118         """ Classes here should NOT handle inside their constructor
119         """
120         pass
121
122     def finish(self):
123         pass
124
125     def setup(self):
126         pass
127
128 class HTTPDir:
129     """ A dispatcher class, like a virtual folder in httpd
130     """
131     def __init__(self,path,handler, auth_provider = None):
132         self.path = path
133         self.handler = handler
134         self.auth_provider = auth_provider
135
136     def matches(self, request):
137         """ Test if some request matches us. If so, return
138             the matched path. """
139         if request.startswith(self.path):
140             return self.path
141         return False
142
143 class noconnection(object):
144     """ a class to use instead of the real connection
145     """
146     def __init__(self, realsocket=None):
147         self.__hidden_socket = realsocket
148
149     def makefile(self, mode, bufsize):
150         return None
151
152     def close(self):
153         pass
154
155     def getsockname(self):
156         """ We need to return info about the real socket that is used for the request
157         """
158         if not self.__hidden_socket:
159             raise AttributeError("No-connection class cannot tell real socket")
160         return self.__hidden_socket.getsockname()
161
162 class dummyconn:
163     def shutdown(self, tru):
164         pass
165
166 def _quote_html(html):
167     return html.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
168
169 class FixSendError:
170     #error_message_format = """ """
171     def send_error(self, code, message=None):
172         #overriden from BaseHTTPRequestHandler, we also send the content-length
173         try:
174             short, long = self.responses[code]
175         except KeyError:
176             short, long = '???', '???'
177         if message is None:
178             message = short
179         explain = long
180         self.log_error("code %d, message %s", code, message)
181         # using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
182         content = (self.error_message_format %
183                    {'code': code, 'message': _quote_html(message), 'explain': explain})
184         self.send_response(code, message)
185         self.send_header("Content-Type", self.error_content_type)
186         self.send_header('Connection', 'close')
187         self.send_header('Content-Length', len(content) or 0)
188         self.end_headers()
189         if hasattr(self, '_flush'):
190             self._flush()
191         
192         if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
193             self.wfile.write(content)
194
195 class HttpOptions:
196     _HTTP_OPTIONS = {'Allow': ['OPTIONS' ] }
197
198     def do_OPTIONS(self):
199         """return the list of capabilities """
200
201         opts = self._HTTP_OPTIONS
202         nopts = self._prep_OPTIONS(opts)
203         if nopts:
204             opts = nopts
205
206         self.send_response(200)
207         self.send_header("Content-Length", 0)
208         if 'Microsoft' in self.headers.get('User-Agent', ''):
209             self.send_header('MS-Author-Via', 'DAV') 
210             # Microsoft's webdav lib ass-umes that the server would
211             # be a FrontPage(tm) one, unless we send a non-standard
212             # header that we are not an elephant.
213             # http://www.ibm.com/developerworks/rational/library/2089.html
214
215         for key, value in opts.items():
216             if isinstance(value, basestring):
217                 self.send_header(key, value)
218             elif isinstance(value, (tuple, list)):
219                 self.send_header(key, ', '.join(value))
220         self.end_headers()
221
222     def _prep_OPTIONS(self, opts):
223         """Prepare the OPTIONS response, if needed
224         
225         Sometimes, like in special DAV folders, the OPTIONS may contain
226         extra keywords, perhaps also dependant on the request url. 
227         @param the options already. MUST be copied before being altered
228         @return the updated options.
229         
230         """
231         return opts
232
233 class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler):
234     """ this is a multiple handler, that will dispatch each request
235         to a nested handler, iff it matches
236
237         The handler will also have *one* dict of authentication proxies,
238         groupped by their realm.
239     """
240
241     protocol_version = "HTTP/1.1"
242     default_request_version = "HTTP/0.9"    # compatibility with py2.5
243
244     auth_required_msg = """ <html><head><title>Authorization required</title></head>
245     <body>You must authenticate to use this service</body><html>\r\r"""
246
247     def __init__(self, request, client_address, server):
248         self.in_handlers = {}
249         self.sec_realms = {}
250         SocketServer.StreamRequestHandler.__init__(self,request,client_address,server)
251         self.log_message("MultiHttpHandler init for %s" %(str(client_address)))
252
253     def _handle_one_foreign(self,fore, path, auth_provider):
254         """ This method overrides the handle_one_request for *children*
255             handlers. It is required, since the first line should not be
256             read again..
257
258         """
259         fore.raw_requestline = "%s %s %s\n" % (self.command, path, self.version)
260         if not fore.parse_request(): # An error code has been sent, just exit
261             return
262         if fore.headers.status:
263             self.log_error("Parse error at headers: %s", fore.headers.status)
264             self.close_connection = 1
265             self.send_error(400,"Parse error at HTTP headers")
266             return
267
268         self.request_version = fore.request_version
269         if auth_provider and auth_provider.realm:
270             try:
271                 self.sec_realms[auth_provider.realm].checkRequest(fore,path)
272             except AuthRequiredExc,ae:
273                 # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
274                 # authorisation features of HTTP/1.1 
275                 if self.request_version != 'HTTP/1.1' and ('Darwin/9.' not in fore.headers.get('User-Agent', '')):
276                     self.log_error("Cannot require auth at %s", self.request_version)
277                     self.send_error(403)
278                     return
279                 self._get_ignore_body(fore) # consume any body that came, not loose sync with input
280                 self.send_response(401,'Authorization required')
281                 self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm))
282                 self.send_header('Connection', 'keep-alive')
283                 self.send_header('Content-Type','text/html')
284                 self.send_header('Content-Length',len(self.auth_required_msg))
285                 self.end_headers()
286                 self.wfile.write(self.auth_required_msg)
287                 return
288             except AuthRejectedExc,e:
289                 self.log_error("Rejected auth: %s" % e.args[0])
290                 self.send_error(403,e.args[0])
291                 self.close_connection = 1
292                 return
293         mname = 'do_' + fore.command
294         if not hasattr(fore, mname):
295             if fore.command == 'OPTIONS':
296                 self.do_OPTIONS()
297                 return
298             self.send_error(501, "Unsupported method (%r)" % fore.command)
299             return
300         fore.close_connection = 0
301         method = getattr(fore, mname)
302         try:
303             method()
304         except (AuthRejectedExc, AuthRequiredExc):
305             raise
306         except Exception, e:
307             if hasattr(self, 'log_exception'):
308                 self.log_exception("Could not run %s", mname)
309             else:
310                 self.log_error("Could not run %s: %s", mname, e)
311             self.send_error(500, "Internal error")
312             # may not work if method has already sent data
313             fore.close_connection = 1
314             self.close_connection = 1
315             if hasattr(fore, '_flush'):
316                 fore._flush()
317             return
318         
319         if fore.close_connection:
320             # print "Closing connection because of handler"
321             self.close_connection = fore.close_connection
322         if hasattr(fore, '_flush'):
323             fore._flush()
324
325
326     def parse_rawline(self):
327         """Parse a request (internal).
328
329         The request should be stored in self.raw_requestline; the results
330         are in self.command, self.path, self.request_version and
331         self.headers.
332
333         Return True for success, False for failure; on failure, an
334         error is sent back.
335
336         """
337         self.command = None  # set in case of error on the first line
338         self.request_version = version = self.default_request_version
339         self.close_connection = 1
340         requestline = self.raw_requestline
341         if requestline[-2:] == '\r\n':
342             requestline = requestline[:-2]
343         elif requestline[-1:] == '\n':
344             requestline = requestline[:-1]
345         self.requestline = requestline
346         words = requestline.split()
347         if len(words) == 3:
348             [command, path, version] = words
349             if version[:5] != 'HTTP/':
350                 self.send_error(400, "Bad request version (%r)" % version)
351                 return False
352             try:
353                 base_version_number = version.split('/', 1)[1]
354                 version_number = base_version_number.split(".")
355                 # RFC 2145 section 3.1 says there can be only one "." and
356                 #   - major and minor numbers MUST be treated as
357                 #      separate integers;
358                 #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
359                 #      turn is lower than HTTP/12.3;
360                 #   - Leading zeros MUST be ignored by recipients.
361                 if len(version_number) != 2:
362                     raise ValueError
363                 version_number = int(version_number[0]), int(version_number[1])
364             except (ValueError, IndexError):
365                 self.send_error(400, "Bad request version (%r)" % version)
366                 return False
367             if version_number >= (1, 1):
368                 self.close_connection = 0
369             if version_number >= (2, 0):
370                 self.send_error(505,
371                           "Invalid HTTP Version (%s)" % base_version_number)
372                 return False
373         elif len(words) == 2:
374             [command, path] = words
375             self.close_connection = 1
376             if command != 'GET':
377                 self.log_error("Junk http request: %s", self.raw_requestline)
378                 self.send_error(400,
379                                 "Bad HTTP/0.9 request type (%r)" % command)
380                 return False
381         elif not words:
382             return False
383         else:
384             #self.send_error(400, "Bad request syntax (%r)" % requestline)
385             return False
386         self.request_version = version
387         self.command, self.path, self.version = command, path, version
388         return True
389
390     def handle_one_request(self):
391         """Handle a single HTTP request.
392            Dispatch to the correct handler.
393         """
394         self.request.setblocking(True)
395         self.raw_requestline = self.rfile.readline()
396         if not self.raw_requestline:
397             self.close_connection = 1
398             # self.log_message("no requestline, connection closed?")
399             return
400         if not self.parse_rawline():
401             self.log_message("Could not parse rawline.")
402             return
403         # self.parse_request(): # Do NOT parse here. the first line should be the only
404         
405         if self.path == '*' and self.command == 'OPTIONS':
406             # special handling of path='*', must not use any vdir at all.
407             if not self.parse_request():
408                 return
409             self.do_OPTIONS()
410             return
411             
412         for vdir in self.server.vdirs:
413             p = vdir.matches(self.path)
414             if p == False:
415                 continue
416             npath = self.path[len(p):]
417             if not npath.startswith('/'):
418                 npath = '/' + npath
419
420             if not self.in_handlers.has_key(p):
421                 self.in_handlers[p] = vdir.handler(noconnection(self.request),self.client_address,self.server)
422                 if vdir.auth_provider:
423                     vdir.auth_provider.setupAuth(self, self.in_handlers[p])
424             hnd = self.in_handlers[p]
425             hnd.rfile = self.rfile
426             hnd.wfile = self.wfile
427             self.rlpath = self.raw_requestline
428             try:
429                 self._handle_one_foreign(hnd,npath, vdir.auth_provider)
430             except IOError, e:
431                 if e.errno == errno.EPIPE:
432                     self.log_message("Could not complete request %s," \
433                             "client closed connection", self.rlpath.rstrip())
434                 else:
435                     raise
436             return
437         # if no match:
438         self.send_error(404, "Path not found: %s" % self.path)
439         return
440
441     def _get_ignore_body(self,fore):
442         if not fore.headers.has_key("content-length"):
443             return
444         max_chunk_size = 10*1024*1024
445         size_remaining = int(fore.headers["content-length"])
446         got = ''
447         while size_remaining:
448             chunk_size = min(size_remaining, max_chunk_size)
449             got = fore.rfile.read(chunk_size)
450             size_remaining -= len(got)
451
452
453 class SecureMultiHTTPHandler(MultiHTTPHandler):
454     def getcert_fnames(self):
455         """ Return a pair with the filenames of ssl cert,key
456
457             Override this to direct to other filenames
458         """
459         return ('server.cert','server.key')
460
461     def setup(self):
462         import ssl
463         certfile, keyfile = self.getcert_fnames()
464         try:
465             self.connection = ssl.wrap_socket(self.request,
466                                 server_side=True,
467                                 certfile=certfile,
468                                 keyfile=keyfile,
469                                 ssl_version=ssl.PROTOCOL_SSLv23)
470             self.rfile = self.connection.makefile('rb', self.rbufsize)
471             self.wfile = self.connection.makefile('wb', self.wbufsize)
472             self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
473         except Exception:
474             self.request.shutdown(socket.SHUT_RDWR)
475             raise
476
477     def finish(self):
478         # With ssl connections, closing the filehandlers alone may not
479         # work because of ref counting. We explicitly tell the socket
480         # to shutdown.
481         MultiHTTPHandler.finish(self)
482         try:
483             self.connection.shutdown(socket.SHUT_RDWR)
484         except Exception:
485             pass
486
487 import threading
488 class ConnThreadingMixIn:
489     """Mix-in class to handle each _connection_ in a new thread.
490
491        This is necessary for persistent connections, where multiple
492        requests should be handled synchronously at each connection, but
493        multiple connections can run in parallel.
494     """
495
496     # Decides how threads will act upon termination of the
497     # main process
498     daemon_threads = False
499
500     def _get_next_name(self):
501         return None
502
503     def _handle_request_noblock(self):
504         """Start a new thread to process the request."""
505         if not threading: # happens while quitting python
506             return
507         t = threading.Thread(name=self._get_next_name(), target=self._handle_request2)
508         if self.daemon_threads:
509             t.setDaemon (1)
510         t.start()
511
512     def _mark_start(self, thread):
513         """ Mark the start of a request thread """
514         pass
515
516     def _mark_end(self, thread):
517         """ Mark the end of a request thread """
518         pass
519
520     def _handle_request2(self):
521         """Handle one request, without blocking.
522
523         I assume that select.select has returned that the socket is
524         readable before this function was called, so there should be
525         no risk of blocking in get_request().
526         """
527         try:
528             self._mark_start(threading.currentThread())
529             request, client_address = self.get_request()
530             if self.verify_request(request, client_address):
531                 try:
532                     self.process_request(request, client_address)
533                 except Exception:
534                     self.handle_error(request, client_address)
535                     self.close_request(request)
536         except socket.error:
537             return
538         finally:
539             self._mark_end(threading.currentThread())
540
541 #eof