Two hacks to make it python2.5 compatible..
[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         self.connection = ssl.wrap_socket(self.request,
363                                 server_side=True,
364                                 certfile=certfile,
365                                 keyfile=keyfile,
366                                 ssl_version=ssl.PROTOCOL_SSLv23)
367         self.rfile = self.connection.makefile('rb', self.rbufsize)
368         self.wfile = self.connection.makefile('wb', self.wbufsize)
369         self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
370
371     def finish(self):
372         # With ssl connections, closing the filehandlers alone may not
373         # work because of ref counting. We explicitly tell the socket
374         # to shutdown.
375         MultiHTTPHandler.finish(self)
376         self.connection.shutdown(socket.SHUT_RDWR)
377
378 import threading
379 class ConnThreadingMixIn:
380     """Mix-in class to handle each _connection_ in a new thread.
381     
382         This is necessary for persistent connections, where multiple
383         requests should be handled synchronously at each connection, but
384         multiple connections can run in parallel.
385         """
386
387     # Decides how threads will act upon termination of the
388     # main process
389     daemon_threads = False
390
391     def _handle_request_noblock(self):
392         """Start a new thread to process the request."""
393         t = threading.Thread(target = self._handle_request2)
394         if self.daemon_threads:
395             t.setDaemon (1)
396         t.start()
397         
398     def _handle_request2(self):
399         """Handle one request, without blocking.
400
401         I assume that select.select has returned that the socket is
402         readable before this function was called, so there should be
403         no risk of blocking in get_request().
404         """
405         try:
406             request, client_address = self.get_request()
407         except socket.error:
408             return
409         if self.verify_request(request, client_address):
410             try:
411                 self.process_request(request, client_address)
412             except:
413                 self.handle_error(request, client_address)
414                 self.close_request(request)
415
416 #eof