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