1 # -*- coding: utf-8 -*-
3 # Copyright P. Christeas <p_christ@hol.gr> 2008-2010
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
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.
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.
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 ###############################################################################
27 #.apidoc title: HTTP Layer library (websrv_lib)
29 """ Framework for generic http servers
31 This library contains *no* OpenERP-specific functionality. It should be
32 usable in other projects, too.
39 from BaseHTTPServer import *
40 from SimpleHTTPServer import SimpleHTTPRequestHandler
42 class AuthRequiredExc(Exception):
43 def __init__(self,atype,realm):
44 Exception.__init__(self)
48 class AuthRejectedExc(Exception):
52 def __init__(self,realm):
55 def setupAuth(self, multi,handler):
56 """ Attach an AuthProxy object to handler
60 def authenticate(self, user, passwd, client_address):
66 class BasicAuthProvider(AuthProvider):
67 def setupAuth(self, multi, handler):
68 if not multi.sec_realms.has_key(self.realm):
69 multi.sec_realms[self.realm] = BasicAuthProxy(self)
73 """ This class will hold authentication information for a handler,
76 def __init__(self, provider):
77 self.provider = provider
79 def checkRequest(self,handler,path = '/'):
80 """ Check if we are allowed to process that request
84 class BasicAuthProxy(AuthProxy):
85 """ Require basic authentication..
87 def __init__(self,provider):
88 AuthProxy.__init__(self,provider)
89 self.auth_creds = None
92 def checkRequest(self,handler,path = '/'):
95 auth_str = handler.headers.get('Authorization',False)
96 if auth_str and auth_str.startswith('Basic '):
97 auth_str=auth_str[len('Basic '):]
98 (user,passwd) = base64.decodestring(auth_str).split(':')
99 self.provider.log("Found user=\"%s\", passwd=\"%s\"" %(user,passwd))
100 self.auth_creds = self.provider.authenticate(user,passwd,handler.client_address)
103 if self.auth_tries > 5:
104 self.provider.log("Failing authorization after 5 requests w/o password")
105 raise AuthRejectedExc("Authorization failed.")
107 raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm)
110 class HTTPHandler(SimpleHTTPRequestHandler):
111 def __init__(self,request, client_address, server):
112 SimpleHTTPRequestHandler.__init__(self,request,client_address,server)
113 # print "Handler for %s inited" % str(client_address)
114 self.protocol_version = 'HTTP/1.1'
115 self.connection = dummyconn()
118 """ Classes here should NOT handle inside their constructor
129 """ A dispatcher class, like a virtual folder in httpd
131 def __init__(self,path,handler, auth_provider = None):
133 self.handler = handler
134 self.auth_provider = auth_provider
136 def matches(self, request):
137 """ Test if some request matches us. If so, return
138 the matched path. """
139 if request.startswith(self.path):
143 class noconnection(object):
144 """ a class to use instead of the real connection
146 def __init__(self, realsocket=None):
147 self.__hidden_socket = realsocket
149 def makefile(self, mode, bufsize):
155 def getsockname(self):
156 """ We need to return info about the real socket that is used for the request
158 if not self.__hidden_socket:
159 raise AttributeError("No-connection class cannot tell real socket")
160 return self.__hidden_socket.getsockname()
163 def shutdown(self, tru):
166 def _quote_html(html):
167 return html.replace("&", "&").replace("<", "<").replace(">", ">")
170 #error_message_format = """ """
171 def send_error(self, code, message=None):
172 #overriden from BaseHTTPRequestHandler, we also send the content-length
174 short, long = self.responses[code]
176 short, long = '???', '???'
180 self.log_error("code %d, message %s", code, message)
181 # using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
182 content = (self.error_message_format %
183 {'code': code, 'message': _quote_html(message), 'explain': explain})
184 self.send_response(code, message)
185 self.send_header("Content-Type", self.error_content_type)
186 self.send_header('Connection', 'close')
187 self.send_header('Content-Length', len(content) or 0)
189 if hasattr(self, '_flush'):
192 if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
193 self.wfile.write(content)
196 _HTTP_OPTIONS = {'Allow': ['OPTIONS' ] }
198 def do_OPTIONS(self):
199 """return the list of capabilities """
201 opts = self._HTTP_OPTIONS
202 nopts = self._prep_OPTIONS(opts)
206 self.send_response(200)
207 self.send_header("Content-Length", 0)
208 if 'Microsoft' in self.headers.get('User-Agent', ''):
209 self.send_header('MS-Author-Via', 'DAV')
210 # Microsoft's webdav lib ass-umes that the server would
211 # be a FrontPage(tm) one, unless we send a non-standard
212 # header that we are not an elephant.
213 # http://www.ibm.com/developerworks/rational/library/2089.html
215 for key, value in opts.items():
216 if isinstance(value, basestring):
217 self.send_header(key, value)
218 elif isinstance(value, (tuple, list)):
219 self.send_header(key, ', '.join(value))
222 def _prep_OPTIONS(self, opts):
223 """Prepare the OPTIONS response, if needed
225 Sometimes, like in special DAV folders, the OPTIONS may contain
226 extra keywords, perhaps also dependant on the request url.
227 @param the options already. MUST be copied before being altered
228 @return the updated options.
233 class MultiHTTPHandler(FixSendError, HttpOptions, BaseHTTPRequestHandler):
234 """ this is a multiple handler, that will dispatch each request
235 to a nested handler, iff it matches
237 The handler will also have *one* dict of authentication proxies,
238 groupped by their realm.
241 protocol_version = "HTTP/1.1"
242 default_request_version = "HTTP/0.9" # compatibility with py2.5
244 auth_required_msg = """ <html><head><title>Authorization required</title></head>
245 <body>You must authenticate to use this service</body><html>\r\r"""
247 def __init__(self, request, client_address, server):
248 self.in_handlers = {}
250 SocketServer.StreamRequestHandler.__init__(self,request,client_address,server)
251 self.log_message("MultiHttpHandler init for %s" %(str(client_address)))
253 def _handle_one_foreign(self,fore, path, auth_provider):
254 """ This method overrides the handle_one_request for *children*
255 handlers. It is required, since the first line should not be
259 fore.raw_requestline = "%s %s %s\n" % (self.command, path, self.version)
260 if not fore.parse_request(): # An error code has been sent, just exit
262 if fore.headers.status:
263 self.log_error("Parse error at headers: %s", fore.headers.status)
264 self.close_connection = 1
265 self.send_error(400,"Parse error at HTTP headers")
268 self.request_version = fore.request_version
269 if auth_provider and auth_provider.realm:
271 self.sec_realms[auth_provider.realm].checkRequest(fore,path)
272 except AuthRequiredExc,ae:
273 # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the
274 # authorisation features of HTTP/1.1
275 if self.request_version != 'HTTP/1.1' and ('Darwin/9.' not in fore.headers.get('User-Agent', '')):
276 self.log_error("Cannot require auth at %s", self.request_version)
279 self._get_ignore_body(fore) # consume any body that came, not loose sync with input
280 self.send_response(401,'Authorization required')
281 self.send_header('WWW-Authenticate','%s realm="%s"' % (ae.atype,ae.realm))
282 self.send_header('Connection', 'keep-alive')
283 self.send_header('Content-Type','text/html')
284 self.send_header('Content-Length',len(self.auth_required_msg))
286 self.wfile.write(self.auth_required_msg)
288 except AuthRejectedExc,e:
289 self.log_error("Rejected auth: %s" % e.args[0])
290 self.send_error(403,e.args[0])
291 self.close_connection = 1
293 mname = 'do_' + fore.command
294 if not hasattr(fore, mname):
295 if fore.command == 'OPTIONS':
298 self.send_error(501, "Unsupported method (%r)" % fore.command)
300 fore.close_connection = 0
301 method = getattr(fore, mname)
304 except (AuthRejectedExc, AuthRequiredExc):
307 if hasattr(self, 'log_exception'):
308 self.log_exception("Could not run %s", mname)
310 self.log_error("Could not run %s: %s", mname, e)
311 self.send_error(500, "Internal error")
312 # may not work if method has already sent data
313 fore.close_connection = 1
314 self.close_connection = 1
315 if hasattr(fore, '_flush'):
319 if fore.close_connection:
320 # print "Closing connection because of handler"
321 self.close_connection = fore.close_connection
322 if hasattr(fore, '_flush'):
326 def parse_rawline(self):
327 """Parse a request (internal).
329 The request should be stored in self.raw_requestline; the results
330 are in self.command, self.path, self.request_version and
333 Return True for success, False for failure; on failure, an
337 self.command = None # set in case of error on the first line
338 self.request_version = version = self.default_request_version
339 self.close_connection = 1
340 requestline = self.raw_requestline
341 if requestline[-2:] == '\r\n':
342 requestline = requestline[:-2]
343 elif requestline[-1:] == '\n':
344 requestline = requestline[:-1]
345 self.requestline = requestline
346 words = requestline.split()
348 [command, path, version] = words
349 if version[:5] != 'HTTP/':
350 self.send_error(400, "Bad request version (%r)" % version)
353 base_version_number = version.split('/', 1)[1]
354 version_number = base_version_number.split(".")
355 # RFC 2145 section 3.1 says there can be only one "." and
356 # - major and minor numbers MUST be treated as
358 # - HTTP/2.4 is a lower version than HTTP/2.13, which in
359 # turn is lower than HTTP/12.3;
360 # - Leading zeros MUST be ignored by recipients.
361 if len(version_number) != 2:
363 version_number = int(version_number[0]), int(version_number[1])
364 except (ValueError, IndexError):
365 self.send_error(400, "Bad request version (%r)" % version)
367 if version_number >= (1, 1):
368 self.close_connection = 0
369 if version_number >= (2, 0):
371 "Invalid HTTP Version (%s)" % base_version_number)
373 elif len(words) == 2:
374 [command, path] = words
375 self.close_connection = 1
377 self.log_error("Junk http request: %s", self.raw_requestline)
379 "Bad HTTP/0.9 request type (%r)" % command)
384 #self.send_error(400, "Bad request syntax (%r)" % requestline)
386 self.request_version = version
387 self.command, self.path, self.version = command, path, version
390 def handle_one_request(self):
391 """Handle a single HTTP request.
392 Dispatch to the correct handler.
394 self.request.setblocking(True)
395 self.raw_requestline = self.rfile.readline()
396 if not self.raw_requestline:
397 self.close_connection = 1
398 # self.log_message("no requestline, connection closed?")
400 if not self.parse_rawline():
401 self.log_message("Could not parse rawline.")
403 # self.parse_request(): # Do NOT parse here. the first line should be the only
405 if self.path == '*' and self.command == 'OPTIONS':
406 # special handling of path='*', must not use any vdir at all.
407 if not self.parse_request():
412 for vdir in self.server.vdirs:
413 p = vdir.matches(self.path)
416 npath = self.path[len(p):]
417 if not npath.startswith('/'):
420 if not self.in_handlers.has_key(p):
421 self.in_handlers[p] = vdir.handler(noconnection(self.request),self.client_address,self.server)
422 if vdir.auth_provider:
423 vdir.auth_provider.setupAuth(self, self.in_handlers[p])
424 hnd = self.in_handlers[p]
425 hnd.rfile = self.rfile
426 hnd.wfile = self.wfile
427 self.rlpath = self.raw_requestline
429 self._handle_one_foreign(hnd,npath, vdir.auth_provider)
431 if e.errno == errno.EPIPE:
432 self.log_message("Could not complete request %s," \
433 "client closed connection", self.rlpath.rstrip())
438 self.send_error(404, "Path not found: %s" % self.path)
441 def _get_ignore_body(self,fore):
442 if not fore.headers.has_key("content-length"):
444 max_chunk_size = 10*1024*1024
445 size_remaining = int(fore.headers["content-length"])
447 while size_remaining:
448 chunk_size = min(size_remaining, max_chunk_size)
449 got = fore.rfile.read(chunk_size)
450 size_remaining -= len(got)
453 class SecureMultiHTTPHandler(MultiHTTPHandler):
454 def getcert_fnames(self):
455 """ Return a pair with the filenames of ssl cert,key
457 Override this to direct to other filenames
459 return ('server.cert','server.key')
463 certfile, keyfile = self.getcert_fnames()
465 self.connection = ssl.wrap_socket(self.request,
469 ssl_version=ssl.PROTOCOL_SSLv23)
470 self.rfile = self.connection.makefile('rb', self.rbufsize)
471 self.wfile = self.connection.makefile('wb', self.wbufsize)
472 self.log_message("Secure %s connection from %s",self.connection.cipher(),self.client_address)
474 self.request.shutdown(socket.SHUT_RDWR)
478 # With ssl connections, closing the filehandlers alone may not
479 # work because of ref counting. We explicitly tell the socket
481 MultiHTTPHandler.finish(self)
483 self.connection.shutdown(socket.SHUT_RDWR)
488 class ConnThreadingMixIn:
489 """Mix-in class to handle each _connection_ in a new thread.
491 This is necessary for persistent connections, where multiple
492 requests should be handled synchronously at each connection, but
493 multiple connections can run in parallel.
496 # Decides how threads will act upon termination of the
498 daemon_threads = False
500 def _get_next_name(self):
503 def _handle_request_noblock(self):
504 """Start a new thread to process the request."""
505 if not threading: # happens while quitting python
507 t = threading.Thread(name=self._get_next_name(), target=self._handle_request2)
508 if self.daemon_threads:
512 def _mark_start(self, thread):
513 """ Mark the start of a request thread """
516 def _mark_end(self, thread):
517 """ Mark the end of a request thread """
520 def _handle_request2(self):
521 """Handle one request, without blocking.
523 I assume that select.select has returned that the socket is
524 readable before this function was called, so there should be
525 no risk of blocking in get_request().
528 self._mark_start(threading.currentThread())
529 request, client_address = self.get_request()
530 if self.verify_request(request, client_address):
532 self.process_request(request, client_address)
534 self.handle_error(request, client_address)
535 self.close_request(request)
539 self._mark_end(threading.currentThread())