diff -r 9ba21fde093a Doc/library/http.server.rst --- a/Doc/library/http.server.rst Mon Jan 19 14:56:24 2015 +0530 +++ b/Doc/library/http.server.rst Tue Jan 20 14:39:05 2015 +0530 @@ -314,6 +314,10 @@ ``application/octet-stream``. The mapping is used case-insensitively, and so should contain only lower-cased keys. + .. attribute:: base_files + + Lists the files we want the server to check and render to client. We can add/remove file names as required. Order of files denotes priority. Defaults to ['index.htm', 'index.html'] + The :class:`SimpleHTTPRequestHandler` class defines the following methods: .. method:: do_HEAD() @@ -350,7 +354,24 @@ For example usage, see the implementation of the :func:`test` function invocation in the :mod:`http.server` module. + + :class:`SimpleHTTPRequestHandler` class defines the following helper methods as well: + .. method:: redirect_browser(path, parts) + + Ensures that path ends with a trailing backslash making it compliant with Apache. + + .. method:: get_path_or_dir(path) + + For a given path, return file's full path or list of directories via io.BytesIO object. + + .. method:: get_file(path) + + If the requested path points to a file, returns file object otherwise converts Response to 404. + + .. method:: apply_headers(f, path) + + Updates Response's header to 200 OK, respective Content-type, Content-Length, Last-Modified and end_headers as defined in BaseHTTPRequestHandler. The :class:`SimpleHTTPRequestHandler` class can be used in the following manner in order to create a very basic webserver serving files relative to @@ -368,6 +389,37 @@ print("serving at port", PORT) httpd.serve_forever() +:class:`SimpleHTTPRequestHandler` class can also be subclassed to provide custom or extra behaviour:: + + # Make server serve `myfile.html` instead of `index.html`. + from http.server import SimpleHTTPRequestHandler + import socketserver + + PORT = 8000 + + class CustomSimpleHttpRequestHandler(SimpleHTTPRequestHandler): + base_files = ['myfile.html', 'myfile.htm', 'index.htm'] + + Handler = CustomSimpleHttpRequestHandler + + httpd = socketserver.TCPServer(("", PORT), Handler) + + print("serving at port", PORT) + httpd.serve_forever() + + + # Make server handle special cases for URL + # ... + + class CustomSimpleHttpRequestHandler(SimpleHTTPRequestHandler): + def do_GET(self): + if self.path.endswith('.custom'): + self.do_custom_handling() + else: + super().do_GET() + + # ... + .. _http-server-cli: :mod:`http.server` can also be invoked directly using the :option:`-m` diff -r 9ba21fde093a Lib/http/server.py --- a/Lib/http/server.py Mon Jan 19 14:56:24 2015 +0530 +++ b/Lib/http/server.py Tue Jan 20 14:39:05 2015 +0530 @@ -672,12 +672,14 @@ server_version = "SimpleHTTP/" + __version__ + base_files = ['index.html', 'index.htm'] + def do_GET(self): """Serve a GET request.""" f = self.send_head() if f: try: - self.copyfile(f, self.wfile) + self.copy_file(f, self.wfile) finally: f.close() @@ -702,40 +704,70 @@ f = None if os.path.isdir(path): parts = urllib.parse.urlsplit(self.path) - if not parts.path.endswith('/'): - # redirect browser - doing basically what apache does - self.send_response(301) - new_parts = (parts[0], parts[1], parts[2] + '/', - parts[3], parts[4]) - new_url = urllib.parse.urlunsplit(new_parts) - self.send_header("Location", new_url) - self.end_headers() + + if self.redirect_browser(path, parts): return None - for index in "index.html", "index.htm": - index = os.path.join(path, index) - if os.path.exists(index): - path = index - break - else: - return self.list_directory(path) - ctype = self.guess_type(path) + + path = self.get_path_or_dir(path) + if isinstance(path, io.BytesIO): + return path + + f = self.get_file(path) + if not f: + return None + try: - f = open(path, 'rb') - except OSError: - self.send_error(404, "File not found") - return None - try: - self.send_response(200) - self.send_header("Content-type", ctype) - fs = os.fstat(f.fileno()) - self.send_header("Content-Length", str(fs[6])) - self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) - self.end_headers() + self.apply_headers(f, path) return f except: f.close() raise + def redirect_browser(self, path, parts): + """If the path does not have a trailing backslash, + redirect browser to path with trailing backslash. + Similar to Apache. + """ + if not parts.path.endswith('/'): + self.send_response(301) + new_parts = (parts[0], parts[1], parts[2] + "/", + parts[3], parts[4]) + new_url = urllib.parse.urlunsplit(new_parts) + self.send_header("Location", new_url) + self.end_headers() + return True + return False + + def get_path_or_dir(self, path): + """For the given `path`, depending on if it is file or directory + return full path for a file and io.BytesIO object for a directory. + """ + for base_file in self.base_files: + index = os.path.join(path, base_file) + if os.path.exists(index): + return index + else: + return self.list_directory(path) + + def get_file(self, path): + """Open path else send 404""" + try: + return open(path, 'rb') + except OSError: + self.send_error(404, "File not found") + return None + + def apply_headers(self, f, path): + """Apply remaining headers before sending back the file object""" + ctype = self.guess_type(path) + + self.send_response(200) + self.send_header("Content-type", ctype) + fs = os.fstat(f.fileno()) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self.end_headers() + def list_directory(self, path): """Helper to produce a directory listing (absent index.html). @@ -822,7 +854,7 @@ path += '/' return path - def copyfile(self, source, outputfile): + def copy_file(self, source, outputfile): """Copy all data between two file objects. The SOURCE argument is a file object open for reading diff -r 9ba21fde093a Lib/test/test_httpservers.py --- a/Lib/test/test_httpservers.py Mon Jan 19 14:56:24 2015 +0530 +++ b/Lib/test/test_httpservers.py Tue Jan 20 14:39:05 2015 +0530 @@ -346,6 +346,57 @@ self.check_status_and_reason(response, 501) +class SimpleHTTPHelperTestCase(BaseTestCase): + class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler): + pass + + def setUp(self): + BaseTestCase.setUp(self) + + self.cwd = os.getcwd() + basetempdir = tempfile.gettempdir() + os.chdir(basetempdir) + self.tempdir = tempfile.mkdtemp(dir=basetempdir) + self.tempdir_name = os.path.basename(self.tempdir) + + self.con = http.client.HTTPConnection(self.HOST, self.PORT) + self.con.connect() + + def get_response(self, tail=False): + path = self.tempdir_name + if tail: + path = '/'.join([path, tail]) + + self.con.request('GET', path) + return self.con.getresponse() + + def test_redirect_browser_true(self): + res = self.get_response() + self.assertEqual(res.status, 301) + + def test_redirect_browser_false(self): + res = self.get_response('') + self.assertEqual(res.status, 200) + + def test_file_not_found(self): + res = self.get_response("404file") + self.assertEqual(res.status, 404) + + def test_returns_directory(self): + res = self.get_response('/') + html = res.fp.read() + is_dir = 'Directory listing for' in html + self.assertTrue(is_dir) + + def test_returns_file(self): + data = b"Test File for data" + temp = open(os.path.join(self.tempdir, 'test'), 'wb') + temp.write(data) + temp.close() + res = self.get_response('test') + self.assertEqual(data, res.fp.read()) + + cgi_file1 = """\ #!%s @@ -738,6 +789,7 @@ self.assertEqual(result[0], b'HTTP/1.1 400 Line too long\r\n') self.assertFalse(self.handler.get_called) + class SimpleHTTPRequestHandlerTestCase(unittest.TestCase): """ Test url parsing """ def setUp(self):