fe1db8f7bc9ffaff7838a99b5a0dd74281e0f6dc
[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                 #if user == 'user' and passwd == 'password':
59                 #       return (user, passwd)
60                 #else:
61                         return False
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                         print "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                         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     def send_error(self, code, message=None):
154         #overriden from BaseHTTPRequestHandler, we also send the content-length
155         try:
156             short, long = self.responses[code]
157         except KeyError:
158             short, long = '???', '???'
159         if message is None:
160             message = short
161         explain = long
162         self.log_error("code %d, message %s", code, message)
163         # using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
164         content = (self.error_message_format %
165                    {'code': code, 'message': _quote_html(message), 'explain': explain})
166         self.send_response(code, message)
167         self.send_header("Content-Type", self.error_content_type)
168         self.send_header('Connection', 'close')
169         self.send_header('Content-Length', len(content) or 0)
170         self.end_headers()
171         if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
172             self.wfile.write(content)
173
174 class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
175     """ this is a multiple handler, that will dispatch each request
176         to a nested handler, iff it matches
177         
178         The handler will also have *one* dict of authentication proxies,
179         groupped by their realm.
180     """
181
182     protocol_version = "HTTP/1.1"
183     
184     def __init__(self, request, client_address, server):
185         self.in_handlers = {}
186         self.sec_realms = {}
187         SocketServer.StreamRequestHandler.__init__(self,request,client_address,server)
188         self.log_message("MultiHttpHandler init for %s" %(str(client_address)))
189
190     def _handle_one_foreign(self,fore, path, auth_provider):
191         """ This method overrides the handle_one_request for *children*
192             handlers. It is required, since the first line should not be
193             read again..
194
195         """
196         fore.raw_requestline = "%s %s %s\n" % (self.command, path, self.version)
197         if not fore.parse_request(): # An error code has been sent, just exit
198             return
199         self.request_version = fore.request_version
200         if auth_provider and auth_provider.realm:
201                 try:
202                         self.sec_realms[auth_provider.realm].checkRequest(fore,path)
203                 except AuthRequiredExc,ae:
204                         if self.request_version != 'HTTP/1.1':
205                                 self.log_error("Cannot require auth at %s",self.request_version)
206                                 self.send_error(401)
207                                 return
208                         self.send_response(401,'Authorization required')
209                         self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm))
210                         self.send_header('Content-Type','text/html')
211                         self.send_header('Content-Length','0')
212                         self.end_headers()
213                         #self.wfile.write("\r\n")
214                         return
215                 except AuthRejectedExc,e:
216                         self.send_error(401,e.args[0])
217                         self.close_connection = 1
218                         return
219         mname = 'do_' + fore.command
220         if not hasattr(fore, mname):
221             fore.send_error(501, "Unsupported method (%r)" % fore.command)
222             return
223         fore.close_connection = 0
224         method = getattr(fore, mname)
225         method()
226         if fore.close_connection:
227                 # print "Closing connection because of handler"
228                 self.close_connection = fore.close_connection
229
230     def parse_rawline(self):
231         """Parse a request (internal).
232
233         The request should be stored in self.raw_requestline; the results
234         are in self.command, self.path, self.request_version and
235         self.headers.
236
237         Return True for success, False for failure; on failure, an
238         error is sent back.
239
240         """
241         self.command = None  # set in case of error on the first line
242         self.request_version = version = self.default_request_version
243         self.close_connection = 1
244         requestline = self.raw_requestline
245         if requestline[-2:] == '\r\n':
246             requestline = requestline[:-2]
247         elif requestline[-1:] == '\n':
248             requestline = requestline[:-1]
249         self.requestline = requestline
250         words = requestline.split()
251         if len(words) == 3:
252             [command, path, version] = words
253             if version[:5] != 'HTTP/':
254                 self.send_error(400, "Bad request version (%r)" % version)
255                 return False
256             try:
257                 base_version_number = version.split('/', 1)[1]
258                 version_number = base_version_number.split(".")
259                 # RFC 2145 section 3.1 says there can be only one "." and
260                 #   - major and minor numbers MUST be treated as
261                 #      separate integers;
262                 #   - HTTP/2.4 is a lower version than HTTP/2.13, which in
263                 #      turn is lower than HTTP/12.3;
264                 #   - Leading zeros MUST be ignored by recipients.
265                 if len(version_number) != 2:
266                     raise ValueError
267                 version_number = int(version_number[0]), int(version_number[1])
268             except (ValueError, IndexError):
269                 self.send_error(400, "Bad request version (%r)" % version)
270                 return False
271             if version_number >= (1, 1):
272                 self.close_connection = 0
273             if version_number >= (2, 0):
274                 self.send_error(505,
275                           "Invalid HTTP Version (%s)" % base_version_number)
276                 return False
277         elif len(words) == 2:
278             [command, path] = words
279             self.close_connection = 1
280             if command != 'GET':
281                 self.send_error(400,
282                                 "Bad HTTP/0.9 request type (%r)" % command)
283                 return False
284         elif not words:
285             return False
286         else:
287             self.send_error(400, "Bad request syntax (%r)" % requestline)
288             return False
289         self.request_version = version
290         self.command, self.path, self.version = command, path, version
291         return True
292
293     def handle_one_request(self):
294         """Handle a single HTTP request.
295            Dispatch to the correct handler.
296         """
297         self.request.setblocking(True)
298         self.raw_requestline = self.rfile.readline()
299         if not self.raw_requestline:
300                 self.close_connection = 1
301                 # self.log_message("no requestline, connection closed?")
302                 return
303         if not self.parse_rawline():
304                 self.log_message("Could not parse rawline.")
305                 return
306         # self.parse_request(): # Do NOT parse here. the first line should be the only 
307         for vdir in self.server.vdirs:
308                 p = vdir.matches(self.path)
309                 if p == False:
310                         continue
311                 npath = self.path[len(p):]
312                 if not npath.startswith('/'):
313                         npath = '/' + npath
314
315                 if not self.in_handlers.has_key(p):
316                         self.in_handlers[p] = vdir.handler(noconnection(),self.client_address,self.server)
317                         if vdir.auth_provider:
318                                 vdir.auth_provider.setupAuth(self, self.in_handlers[p])
319                 hnd = self.in_handlers[p]
320                 hnd.rfile = self.rfile
321                 hnd.wfile = self.wfile
322                 self.rlpath = self.raw_requestline
323                 self._handle_one_foreign(hnd,npath, vdir.auth_provider)
324                 # print "Handled, closing = ", self.close_connection
325                 return
326         # if no match:
327         self.send_error(404, "Path not found: %s" % self.path)
328         return
329
330
331 class SecureMultiHTTPHandler(MultiHTTPHandler):
332     def getcert_fnames(self):
333         """ Return a pair with the filenames of ssl cert,key
334         
335             Override this to direct to other filenames
336         """
337         return ('server.cert','server.key')
338         
339     def setup(self):
340         import ssl
341         certfile, keyfile = self.getcert_fnames()
342         self.connection = ssl.wrap_socket(self.request,
343                                 server_side=True,
344                                 certfile=certfile,
345                                 keyfile=keyfile,
346                                 ssl_version=ssl.PROTOCOL_SSLv23)
347         self.rfile = self.connection.makefile('rb', self.rbufsize)
348         self.wfile = self.connection.makefile('wb', self.wbufsize)
349         self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
350
351     def finish(self):
352         # With ssl connections, closing the filehandlers alone may not
353         # work because of ref counting. We explicitly tell the socket
354         # to shutdown.
355         MultiHTTPHandler.finish(self)
356         self.connection.shutdown(socket.SHUT_RDWR)
357
358 import threading
359 class ConnThreadingMixIn:
360     """Mix-in class to handle each _connection_ in a new thread.
361     
362         This is necessary for persistent connections, where multiple
363         requests should be handled synchronously at each connection, but
364         multiple connections can run in parallel.
365         """
366
367     # Decides how threads will act upon termination of the
368     # main process
369     daemon_threads = False
370
371     def _handle_request_noblock(self):
372         """Start a new thread to process the request."""
373         t = threading.Thread(target = self._handle_request2)
374         print "request came, handling in new thread",t
375         if self.daemon_threads:
376             t.setDaemon (1)
377         t.start()
378         
379     def _handle_request2(self):
380         """Handle one request, without blocking.
381
382         I assume that select.select has returned that the socket is
383         readable before this function was called, so there should be
384         no risk of blocking in get_request().
385         """
386         try:
387             request, client_address = self.get_request()
388         except socket.error:
389             return
390         if self.verify_request(request, client_address):
391             try:
392                 self.process_request(request, client_address)
393             except:
394                 self.handle_error(request, client_address)
395                 self.close_request(request)
396
397 #eof