1 # -*- encoding: utf-8 -*-
4 # Copyright P. Christeas <p_christ@hol.gr> 2008,2009
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
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.
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.
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 ###############################################################################
29 """ Framework for generic http servers
36 from BaseHTTPServer import *
37 from SimpleHTTPServer import SimpleHTTPRequestHandler
39 class AuthRequiredExc(Exception):
40 def __init__(self,atype,realm):
41 Exception.__init__(self)
45 class AuthRejectedExc(Exception):
49 def __init__(self,realm):
52 def setupAuth(self, multi,handler):
53 """ Attach an AuthProxy object to handler
57 def authenticate(self, user, passwd, client_address):
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)
70 """ This class will hold authentication information for a handler,
73 def __init__(self, provider):
74 self.provider = provider
76 def checkRequest(self,handler,path = '/'):
77 """ Check if we are allowed to process that request
81 class BasicAuthProxy(AuthProxy):
82 """ Require basic authentication..
84 def __init__(self,provider):
85 AuthProxy.__init__(self,provider)
86 self.auth_creds = None
89 def checkRequest(self,handler,path = '/'):
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)
100 if self.auth_tries > 5:
101 self.provider.log("Failing authorization after 5 requests w/o password")
102 raise AuthRejectedExc("Authorization failed.")
104 raise AuthRequiredExc(atype = 'Basic', realm=self.provider.realm)
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()
115 """ Classes here should NOT handle inside their constructor
126 """ A dispatcher class, like a virtual folder in httpd
128 def __init__(self,path,handler, auth_provider = None):
130 self.handler = handler
131 self.auth_provider = auth_provider
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):
141 """ a class to use instead of the real connection
143 def makefile(self, mode, bufsize):
147 def shutdown(self, tru):
150 def _quote_html(html):
151 return html.replace("&", "&").replace("<", "<").replace(">", ">")
154 #error_message_format = """ """
155 def send_error(self, code, message=None):
156 #overriden from BaseHTTPRequestHandler, we also send the content-length
158 short, long = self.responses[code]
160 short, 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)
173 if self.command != 'HEAD' and code >= 200 and code not in (204, 304):
174 self.wfile.write(content)
176 class MultiHTTPHandler(FixSendError,BaseHTTPRequestHandler):
177 """ this is a multiple handler, that will dispatch each request
178 to a nested handler, iff it matches
180 The handler will also have *one* dict of authentication proxies,
181 groupped by their realm.
184 protocol_version = "HTTP/1.1"
185 default_request_version = "HTTP/0.9" # compatibility with py2.5
187 auth_required_msg = """ <html><head><title>Authorization required</title></head>
188 <body>You must authenticate to use this service</body><html>\r\r"""
190 def __init__(self, request, client_address, server):
191 self.in_handlers = {}
193 SocketServer.StreamRequestHandler.__init__(self,request,client_address,server)
194 self.log_message("MultiHttpHandler init for %s" %(str(client_address)))
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
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
205 self.request_version = fore.request_version
206 if auth_provider and auth_provider.realm:
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)
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))
221 self.wfile.write(self.auth_required_msg)
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
228 mname = 'do_' + fore.command
229 if not hasattr(fore, mname):
230 fore.send_error(501, "Unsupported method (%r)" % fore.command)
232 fore.close_connection = 0
233 method = getattr(fore, mname)
235 if fore.close_connection:
236 # print "Closing connection because of handler"
237 self.close_connection = fore.close_connection
239 def parse_rawline(self):
240 """Parse a request (internal).
242 The request should be stored in self.raw_requestline; the results
243 are in self.command, self.path, self.request_version and
246 Return True for success, False for failure; on failure, an
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()
261 [command, path, version] = words
262 if version[:5] != 'HTTP/':
263 self.send_error(400, "Bad request version (%r)" % version)
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
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:
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)
280 if version_number >= (1, 1):
281 self.close_connection = 0
282 if version_number >= (2, 0):
284 "Invalid HTTP Version (%s)" % base_version_number)
286 elif len(words) == 2:
287 [command, path] = words
288 self.close_connection = 1
291 "Bad HTTP/0.9 request type (%r)" % command)
296 self.send_error(400, "Bad request syntax (%r)" % requestline)
298 self.request_version = version
299 self.command, self.path, self.version = command, path, version
302 def handle_one_request(self):
303 """Handle a single HTTP request.
304 Dispatch to the correct handler.
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?")
312 if not self.parse_rawline():
313 self.log_message("Could not parse rawline.")
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)
320 npath = self.path[len(p):]
321 if not npath.startswith('/'):
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
336 self.send_error(404, "Path not found: %s" % self.path)
339 def _get_ignore_body(self,fore):
340 if not fore.headers.has_key("content-length"):
342 max_chunk_size = 10*1024*1024
343 size_remaining = int(fore.headers["content-length"])
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)
351 class SecureMultiHTTPHandler(MultiHTTPHandler):
352 def getcert_fnames(self):
353 """ Return a pair with the filenames of ssl cert,key
355 Override this to direct to other filenames
357 return ('server.cert','server.key')
361 certfile, keyfile = self.getcert_fnames()
362 self.connection = ssl.wrap_socket(self.request,
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)
372 # With ssl connections, closing the filehandlers alone may not
373 # work because of ref counting. We explicitly tell the socket
375 MultiHTTPHandler.finish(self)
376 self.connection.shutdown(socket.SHUT_RDWR)
379 class ConnThreadingMixIn:
380 """Mix-in class to handle each _connection_ in a new thread.
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.
387 # Decides how threads will act upon termination of the
389 daemon_threads = False
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:
398 def _handle_request2(self):
399 """Handle one request, without blocking.
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().
406 request, client_address = self.get_request()
409 if self.verify_request(request, client_address):
411 self.process_request(request, client_address)
413 self.handle_error(request, client_address)
414 self.close_request(request)