Http server: support the OPTIONS at the multi-handler.
[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:
140     """ a class to use instead of the real connection
141     """
142     def makefile(self, mode, bufsize):
143         return None
144
145 class dummyconn:
146     def shutdown(self, tru):
147         pass
148
149 def _quote_html(html):
150     return html.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
151
152 class FixSendError:
153     #error_message_format = """ """
154     def send_error(self, code, message=None):
155         #overriden from BaseHTTPRequestHandler, we also send the content-length
156         try:
157             short, long = self.responses[code]
158         except KeyError:
159             short, long = '???', '???'
160         if message is None:
161             message = short
162         explain = long
163         self.log_error("code %d, message %s", code, message)
164         # using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
165         content = (self.error_message_format %
166                    {'code': code, 'message': _quote_html(message), 'explain': explain})
167         self.send_response(code, message)
168         self.send_header("Content-Type", self.error_content_type)
169         self.send_header('Connection', 'close')
170         self.send_header('Content-Length', len(content) or 0)
171         self.end_headers()
172         if hasattr(self, '_flush'):
173             self._flush()
174         
175         if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
176             self.wfile.write(content)
177
178 class HttpOptions:
179     _HTTP_OPTIONS = 'OPTIONS'
180
181     def do_OPTIONS(self):
182         """return the list of capabilities """
183
184         self.send_response(200)
185         self.send_header("Content-Length", 0)
186
187         self.send_header('Allow', self._HTTP_OPTIONS)
188         self.end_headers()
189
190 class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler):
191     """ this is a multiple handler, that will dispatch each request
192         to a nested handler, iff it matches
193
194         The handler will also have *one* dict of authentication proxies,
195         groupped by their realm.
196     """
197
198     protocol_version = "HTTP/1.1"
199     default_request_version = "HTTP/0.9"    # compatibility with py2.5
200
201     auth_required_msg = """ <html><head><title>Authorization required</title></head>
202     <body>You must authenticate to use this service</body><html>\r\r"""
203
204     def __init__(self, request, client_address, server):
205         self.in_handlers = {}
206         self.sec_realms = {}
207         SocketServer.StreamRequestHandler.__init__(self,request,client_address,server)
208         self.log_message("MultiHttpHandler init for %s" %(str(client_address)))
209
210     def _handle_one_foreign(self,fore, path, auth_provider):
211         """ This method overrides the handle_one_request for *children*
212             handlers. It is required, since the first line should not be
213             read again..
214
215         """
216         fore.raw_requestline = "%s %s %s\n" % (self.command, path, self.version)
217         if not fore.parse_request(): # An error code has been sent, just exit
218             return
219         self.request_version = fore.request_version
220         if auth_provider and auth_provider.realm:
221             try:
222                 self.sec_realms[auth_provider.realm].checkRequest(fore,path)
223             except AuthRequiredExc,ae:
224                 if self.request_version != 'HTTP/1.1':
225                     self.log_error("Cannot require auth at %s",self.request_version)
226                     self.send_error(403)
227                     return
228                 self._get_ignore_body(fore) # consume any body that came, not loose sync with input
229                 self.send_response(401,'Authorization required')
230                 self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm))
231                 self.send_header('Connection', 'keep-alive')
232                 self.send_header('Content-Type','text/html')
233                 self.send_header('Content-Length',len(self.auth_required_msg))
234                 self.end_headers()
235                 self.wfile.write(self.auth_required_msg)
236                 return
237             except AuthRejectedExc,e:
238                 self.log_error("Rejected auth: %s" % e.args[0])
239                 self.send_error(403,e.args[0])
240                 self.close_connection = 1
241                 return
242         mname = 'do_' + fore.command
243         if not hasattr(fore, mname):
244             if fore.command == 'OPTIONS':
245                 self.do_OPTIONS()
246                 return
247             self.send_error(501, "Unsupported method (%r)" % fore.command)
248             return
249         fore.close_connection = 0
250         method = getattr(fore, mname)
251         try:
252             method()
253         except (AuthRejectedExc, AuthRequiredExc):
254             raise
255         except Exception, e:
256             self.log_error("Could not run %s: %s" % (mname, e))
257             self._get_ignore_body(fore)
258             self.send_error(500, "Internal error")
259             # may not work if method has already sent data
260             fore.close_connection = 1
261             self.close_connection = 1
262             if hasattr(fore, '_flush'):
263                 fore._flush()
264             return
265         
266         if fore.close_connection:
267             # print "Closing connection because of handler"
268             self.close_connection = fore.close_connection
269         if hasattr(fore, '_flush'):
270             fore._flush()
271
272
273     def parse_rawline(self):
274         """Parse a request (internal).
275
276         The request should be stored in self.raw_requestline; the results
277         are in self.command, self.path, self.request_version and
278         self.headers.
279
280         Return True for success, False for failure; on failure, an
281         error is sent back.
282
283         """
284         self.command = None  # set in case of error on the first line
285         self.request_version = version = self.default_request_version
286         self.close_connection = 1
287         requestline = self.raw_requestline
288         if requestline[-2:] == '\r\n':
289             requestline = requestline[:-2]
290         elif requestline[-1:] == '\n':
291             requestline = requestline[:-1]
292         self.requestline = requestline
293         words = requestline.split()
294         if len(words) == 3:
295             [command, path, version] = words
296             if version[:5] != 'HTTP/':
297                 self.send_error(400, "Bad request version (%r)" % version)
298                 return False
299             try:
300                 base_version_number = version.split('/', 1)[1]
301                 version_number = base_version_number.split(".")
302                 # RFC 2145 section 3.1 says there can be only one "." and
303                 #   - major and minor numbers MUST be treated as
304                 #      separate integers;
305                 #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
306                 #      turn is lower than HTTP/12.3;
307                 #   - Leading zeros MUST be ignored by recipients.
308                 if len(version_number) != 2:
309                     raise ValueError
310                 version_number = int(version_number[0]), int(version_number[1])
311             except (ValueError, IndexError):
312                 self.send_error(400, "Bad request version (%r)" % version)
313                 return False
314             if version_number >= (1, 1):
315                 self.close_connection = 0
316             if version_number >= (2, 0):
317                 self.send_error(505,
318                           "Invalid HTTP Version (%s)" % base_version_number)
319                 return False
320         elif len(words) == 2:
321             [command, path] = words
322             self.close_connection = 1
323             if command != 'GET':
324                 self.send_error(400,
325                                 "Bad HTTP/0.9 request type (%r)" % command)
326                 return False
327         elif not words:
328             return False
329         else:
330             #self.send_error(400, "Bad request syntax (%r)" % requestline)
331             return False
332         self.request_version = version
333         self.command, self.path, self.version = command, path, version
334         return True
335
336     def handle_one_request(self):
337         """Handle a single HTTP request.
338            Dispatch to the correct handler.
339         """
340         self.request.setblocking(True)
341         self.raw_requestline = self.rfile.readline()
342         if not self.raw_requestline:
343             self.close_connection = 1
344             # self.log_message("no requestline, connection closed?")
345             return
346         if not self.parse_rawline():
347             self.log_message("Could not parse rawline.")
348             return
349         # self.parse_request(): # Do NOT parse here. the first line should be the only
350         
351         if self.path == '*' and self.command == 'OPTIONS':
352             # special handling of path='*', must not use any vdir at all.
353             if not self.parse_request():
354                 return
355             self.do_OPTIONS()
356             return
357             
358         for vdir in self.server.vdirs:
359             p = vdir.matches(self.path)
360             if p == False:
361                 continue
362             npath = self.path[len(p):]
363             if not npath.startswith('/'):
364                 npath = '/' + npath
365
366             if not self.in_handlers.has_key(p):
367                 self.in_handlers[p] = vdir.handler(noconnection(),self.client_address,self.server)
368                 if vdir.auth_provider:
369                     vdir.auth_provider.setupAuth(self, self.in_handlers[p])
370             hnd = self.in_handlers[p]
371             hnd.rfile = self.rfile
372             hnd.wfile = self.wfile
373             self.rlpath = self.raw_requestline
374             self._handle_one_foreign(hnd,npath, vdir.auth_provider)
375             # print "Handled, closing = ", self.close_connection
376             return
377         # if no match:
378         self.send_error(404, "Path not found: %s" % self.path)
379         return
380
381     def _get_ignore_body(self,fore):
382         if not fore.headers.has_key("content-length"):
383             return
384         max_chunk_size = 10*1024*1024
385         size_remaining = int(fore.headers["content-length"])
386         got = ''
387         while size_remaining:
388             chunk_size = min(size_remaining, max_chunk_size)
389             got = fore.rfile.read(chunk_size)
390             size_remaining -= len(got)
391
392
393 class SecureMultiHTTPHandler(MultiHTTPHandler):
394     def getcert_fnames(self):
395         """ Return a pair with the filenames of ssl cert,key
396
397             Override this to direct to other filenames
398         """
399         return ('server.cert','server.key')
400
401     def setup(self):
402         import ssl
403         certfile, keyfile = self.getcert_fnames()
404         try:
405             self.connection = ssl.wrap_socket(self.request,
406                                 server_side=True,
407                                 certfile=certfile,
408                                 keyfile=keyfile,
409                                 ssl_version=ssl.PROTOCOL_SSLv23)
410             self.rfile = self.connection.makefile('rb', self.rbufsize)
411             self.wfile = self.connection.makefile('wb', self.wbufsize)
412             self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
413         except Exception:
414             self.request.shutdown(socket.SHUT_RDWR)
415             raise
416
417     def finish(self):
418         # With ssl connections, closing the filehandlers alone may not
419         # work because of ref counting. We explicitly tell the socket
420         # to shutdown.
421         MultiHTTPHandler.finish(self)
422         try:
423             self.connection.shutdown(socket.SHUT_RDWR)
424         except Exception:
425             pass
426
427 import threading
428 class ConnThreadingMixIn:
429     """Mix-in class to handle each _connection_ in a new thread.
430
431        This is necessary for persistent connections, where multiple
432        requests should be handled synchronously at each connection, but
433        multiple connections can run in parallel.
434     """
435
436     # Decides how threads will act upon termination of the
437     # main process
438     daemon_threads = False
439
440     def _handle_request_noblock(self):
441         """Start a new thread to process the request."""
442         if not threading: # happens while quitting python
443             return
444         t = threading.Thread(target = self._handle_request2)
445         if self.daemon_threads:
446             t.setDaemon (1)
447         t.start()
448     
449     def _mark_start(self, thread):
450         """ Mark the start of a request thread """
451         pass
452
453     def _mark_end(self, thread):
454         """ Mark the end of a request thread """
455         pass
456
457     def _handle_request2(self):
458         """Handle one request, without blocking.
459
460         I assume that select.select has returned that the socket is
461         readable before this function was called, so there should be
462         no risk of blocking in get_request().
463         """
464         try:
465             self._mark_start(threading.currentThread())
466             request, client_address = self.get_request()
467         except socket.error:
468             self._mark_end(threading.currentThread())
469             return
470         if self.verify_request(request, client_address):
471             try:
472                 self.process_request(request, client_address)
473             except Exception:
474                 self.handle_error(request, client_address)
475                 self.close_request(request)
476         self._mark_end(threading.currentThread())
477
478 #eof