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