websrv_lib: allow "noconnection" object to carry the socket name
[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         self.request_version = fore.request_version
259         if auth_provider and auth_provider.realm:
260             try:
261                 self.sec_realms[auth_provider.realm].checkRequest(fore,path)
262             except AuthRequiredExc,ae:
263                 if self.request_version != 'HTTP/1.1':
264                     self.log_error("Cannot require auth at %s",self.request_version)
265                     self.send_error(403)
266                     return
267                 self._get_ignore_body(fore) # consume any body that came, not loose sync with input
268                 self.send_response(401,'Authorization required')
269                 self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm))
270                 self.send_header('Connection', 'keep-alive')
271                 self.send_header('Content-Type','text/html')
272                 self.send_header('Content-Length',len(self.auth_required_msg))
273                 self.end_headers()
274                 self.wfile.write(self.auth_required_msg)
275                 return
276             except AuthRejectedExc,e:
277                 self.log_error("Rejected auth: %s" % e.args[0])
278                 self.send_error(403,e.args[0])
279                 self.close_connection = 1
280                 return
281         mname = 'do_' + fore.command
282         if not hasattr(fore, mname):
283             if fore.command == 'OPTIONS':
284                 self.do_OPTIONS()
285                 return
286             self.send_error(501, "Unsupported method (%r)" % fore.command)
287             return
288         fore.close_connection = 0
289         method = getattr(fore, mname)
290         try:
291             method()
292         except (AuthRejectedExc, AuthRequiredExc):
293             raise
294         except Exception, e:
295             if hasattr(self, 'log_exception'):
296                 self.log_exception("Could not run %s", mname)
297             else:
298                 self.log_error("Could not run %s: %s", mname, e)
299             self.send_error(500, "Internal error")
300             # may not work if method has already sent data
301             fore.close_connection = 1
302             self.close_connection = 1
303             if hasattr(fore, '_flush'):
304                 fore._flush()
305             return
306         
307         if fore.close_connection:
308             # print "Closing connection because of handler"
309             self.close_connection = fore.close_connection
310         if hasattr(fore, '_flush'):
311             fore._flush()
312
313
314     def parse_rawline(self):
315         """Parse a request (internal).
316
317         The request should be stored in self.raw_requestline; the results
318         are in self.command, self.path, self.request_version and
319         self.headers.
320
321         Return True for success, False for failure; on failure, an
322         error is sent back.
323
324         """
325         self.command = None  # set in case of error on the first line
326         self.request_version = version = self.default_request_version
327         self.close_connection = 1
328         requestline = self.raw_requestline
329         if requestline[-2:] == '\r\n':
330             requestline = requestline[:-2]
331         elif requestline[-1:] == '\n':
332             requestline = requestline[:-1]
333         self.requestline = requestline
334         words = requestline.split()
335         if len(words) == 3:
336             [command, path, version] = words
337             if version[:5] != 'HTTP/':
338                 self.send_error(400, "Bad request version (%r)" % version)
339                 return False
340             try:
341                 base_version_number = version.split('/', 1)[1]
342                 version_number = base_version_number.split(".")
343                 # RFC 2145 section 3.1 says there can be only one "." and
344                 #   - major and minor numbers MUST be treated as
345                 #      separate integers;
346                 #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
347                 #      turn is lower than HTTP/12.3;
348                 #   - Leading zeros MUST be ignored by recipients.
349                 if len(version_number) != 2:
350                     raise ValueError
351                 version_number = int(version_number[0]), int(version_number[1])
352             except (ValueError, IndexError):
353                 self.send_error(400, "Bad request version (%r)" % version)
354                 return False
355             if version_number >= (1, 1):
356                 self.close_connection = 0
357             if version_number >= (2, 0):
358                 self.send_error(505,
359                           "Invalid HTTP Version (%s)" % base_version_number)
360                 return False
361         elif len(words) == 2:
362             [command, path] = words
363             self.close_connection = 1
364             if command != 'GET':
365                 self.log_error("Junk http request: %s", self.raw_requestline)
366                 self.send_error(400,
367                                 "Bad HTTP/0.9 request type (%r)" % command)
368                 return False
369         elif not words:
370             return False
371         else:
372             #self.send_error(400, "Bad request syntax (%r)" % requestline)
373             return False
374         self.request_version = version
375         self.command, self.path, self.version = command, path, version
376         return True
377
378     def handle_one_request(self):
379         """Handle a single HTTP request.
380            Dispatch to the correct handler.
381         """
382         self.request.setblocking(True)
383         self.raw_requestline = self.rfile.readline()
384         if not self.raw_requestline:
385             self.close_connection = 1
386             # self.log_message("no requestline, connection closed?")
387             return
388         if not self.parse_rawline():
389             self.log_message("Could not parse rawline.")
390             return
391         # self.parse_request(): # Do NOT parse here. the first line should be the only
392         
393         if self.path == '*' and self.command == 'OPTIONS':
394             # special handling of path='*', must not use any vdir at all.
395             if not self.parse_request():
396                 return
397             self.do_OPTIONS()
398             return
399             
400         for vdir in self.server.vdirs:
401             p = vdir.matches(self.path)
402             if p == False:
403                 continue
404             npath = self.path[len(p):]
405             if not npath.startswith('/'):
406                 npath = '/' + npath
407
408             if not self.in_handlers.has_key(p):
409                 self.in_handlers[p] = vdir.handler(noconnection(self.request),self.client_address,self.server)
410                 if vdir.auth_provider:
411                     vdir.auth_provider.setupAuth(self, self.in_handlers[p])
412             hnd = self.in_handlers[p]
413             hnd.rfile = self.rfile
414             hnd.wfile = self.wfile
415             self.rlpath = self.raw_requestline
416             self._handle_one_foreign(hnd,npath, vdir.auth_provider)
417             # print "Handled, closing = ", self.close_connection
418             return
419         # if no match:
420         self.send_error(404, "Path not found: %s" % self.path)
421         return
422
423     def _get_ignore_body(self,fore):
424         if not fore.headers.has_key("content-length"):
425             return
426         max_chunk_size = 10*1024*1024
427         size_remaining = int(fore.headers["content-length"])
428         got = ''
429         while size_remaining:
430             chunk_size = min(size_remaining, max_chunk_size)
431             got = fore.rfile.read(chunk_size)
432             size_remaining -= len(got)
433
434
435 class SecureMultiHTTPHandler(MultiHTTPHandler):
436     def getcert_fnames(self):
437         """ Return a pair with the filenames of ssl cert,key
438
439             Override this to direct to other filenames
440         """
441         return ('server.cert','server.key')
442
443     def setup(self):
444         import ssl
445         certfile, keyfile = self.getcert_fnames()
446         try:
447             self.connection = ssl.wrap_socket(self.request,
448                                 server_side=True,
449                                 certfile=certfile,
450                                 keyfile=keyfile,
451                                 ssl_version=ssl.PROTOCOL_SSLv23)
452             self.rfile = self.connection.makefile('rb', self.rbufsize)
453             self.wfile = self.connection.makefile('wb', self.wbufsize)
454             self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
455         except Exception:
456             self.request.shutdown(socket.SHUT_RDWR)
457             raise
458
459     def finish(self):
460         # With ssl connections, closing the filehandlers alone may not
461         # work because of ref counting. We explicitly tell the socket
462         # to shutdown.
463         MultiHTTPHandler.finish(self)
464         try:
465             self.connection.shutdown(socket.SHUT_RDWR)
466         except Exception:
467             pass
468
469 import threading
470 class ConnThreadingMixIn:
471     """Mix-in class to handle each _connection_ in a new thread.
472
473        This is necessary for persistent connections, where multiple
474        requests should be handled synchronously at each connection, but
475        multiple connections can run in parallel.
476     """
477
478     # Decides how threads will act upon termination of the
479     # main process
480     daemon_threads = False
481
482     def _get_next_name(self):
483         return None
484
485     def _handle_request_noblock(self):
486         """Start a new thread to process the request."""
487         if not threading: # happens while quitting python
488             return
489         t = threading.Thread(name=self._get_next_name(), target=self._handle_request2)
490         if self.daemon_threads:
491             t.setDaemon (1)
492         t.start()
493
494     def _mark_start(self, thread):
495         """ Mark the start of a request thread """
496         pass
497
498     def _mark_end(self, thread):
499         """ Mark the end of a request thread """
500         pass
501
502     def _handle_request2(self):
503         """Handle one request, without blocking.
504
505         I assume that select.select has returned that the socket is
506         readable before this function was called, so there should be
507         no risk of blocking in get_request().
508         """
509         try:
510             self._mark_start(threading.currentThread())
511             request, client_address = self.get_request()
512             if self.verify_request(request, client_address):
513                 try:
514                     self.process_request(request, client_address)
515                 except Exception:
516                     self.handle_error(request, client_address)
517                     self.close_request(request)
518         except socket.error:
519             return
520         finally:
521             self._mark_end(threading.currentThread())
522
523 #eof