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