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