diff -r 469ff344f8fd Doc/library/http.server.rst --- a/Doc/library/http.server.rst Sat Jan 31 12:20:40 2015 -0800 +++ b/Doc/library/http.server.rst Sun Feb 01 18:51:14 2015 +0530 @@ -314,6 +314,10 @@ ``application/octet-stream``. The mapping is used case-insensitively, and so should contain only lower-cased keys. + .. attribute:: index_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() @@ -351,6 +355,28 @@ 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(url, status) + + Redirects to new url + + .. method:: get_file_or_dir(path) + + For a given path, return file object 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:: get_index_file(path) + + Check if any of the filenames from `index_files` exists in `path`. If it does exist, return file object to succesful filename. + + .. method:: apply_headers(f, status) + + Applies headers as per `status`. For extensible functionality, can be overwritten to provide required functionality. 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 +394,38 @@ 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 469ff344f8fd Lib/http/server.py --- a/Lib/http/server.py Sat Jan 31 12:20:40 2015 -0800 +++ b/Lib/http/server.py Sun Feb 01 18:51:14 2015 +0530 @@ -619,6 +619,8 @@ server_version = "SimpleHTTP/" + __version__ + index_files = ['index.html', 'index.htm'] + def do_GET(self): """Serve a GET request.""" f = self.send_head() @@ -646,42 +648,124 @@ """ path = self.translate_path(self.path) - f = None + redirect_path = self._redirect_path(path) + if redirect_path: + self.redirect(redirect_path) + return None + + file_object = self.get_file_or_dir(path) + return file_object + + def _redirect_path(self, path): + """If path is a directory, returns new_path + with trailing backslash appended if path does not + have trailing backslash. Otherwise returns None. + """ + if not os.path.isdir(path): + return None + + parts = urllib.parse.urlsplit(self.path) + if not parts.path.endswith('/'): + new_parts = (parts[0], parts[1], parts[2] + "/", + parts[3], parts[4]) + new_path = urllib.parse.urlunsplit(new_parts) + return new_path + return None + + def redirect(self, url, status=HTTPStatus.MOVED_PERMANENTLY): + """Is called upon only when we require an HTTP redirect, + as required in case of Apache like redirect for url + without trailing backslash. + """ + if status not in (301,): + raise Exception('Unknown Status Code: %s' % status) + + self.send_response(status) + self.send_header("Location", url) + self.end_headers() + + def get_index_file(self, path): + """In given directory path, return file object + if file exists with file name in index_files + """ + for index_file in self.index_files: + index = os.path.join(path, index_file) + if os.path.exists(index): + return self.get_file(index) + else: + return None + + def get_file_or_dir(self, path): + """For the given `path`, depending on if it is file or directory + return file object for file path or if file name from index_files exists in directory, + otherwise return io.BytesIO object for the directory. + """ 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(HTTPStatus.MOVED_PERMANENTLY) - 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 None - for index in "index.html", "index.htm": - index = os.path.join(path, index) - if os.path.exists(index): - path = index - break + # `path` is a directory + index_file = self.get_index_file(path) + if index_file: + # With `index_file` + self.apply_headers(index_file) + return index_file else: + # Is just a directory return self.list_directory(path) - ctype = self.guess_type(path) + elif os.path.isfile(path): + # `path` is file + _file = self.get_file(path) + self.apply_headers(_file) + return _file + else: + # Nothing at `path` exists + self.apply_headers(None, HTTPStatus.NOT_FOUND) + + def get_file(self, path): + """Open path else send 404""" try: - f = open(path, 'rb') + return open(path, 'rb') except OSError: self.send_error(HTTPStatus.NOT_FOUND, "File not found") return None - try: - self.send_response(HTTPStatus.OK) - 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() - return f - except: - f.close() - raise + + def _get_status_type(self, status): + """Returns a string denoting whether given status + is VALID, CLIENT_ERROR or SERVER_ERROR. Can be overwritten + in conjunction to self.apply_headers to handle specific usecases. + """ + if 200 <= status < 400: + return 'VALID' + elif 400 <= status < 500: + return 'CLIENT_ERROR' + elif 500 <= status: + return 'SERVER_ERROR' + else: + raise Exception('Unknown Status Code: %s' % status) + + def _apply_success_headers(self, f, status): + """For Success response for the request, + if f is a list of directories, then return None; Otherwise, + apply remaining headers before sending back the file object. + """ + if not f or isinstance(f, io.BytesIO): + return + + self.send_response(status) + ctype = self.guess_type(f.name) + 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 apply_headers(self, f=None, status=http.HTTPStatus.OK): + """Applies headers as per return string. + Can be overwritten to handle different cases. + """ + status_type = self._get_status_type(status) + if status_type is 'VALID': + self._apply_success_headers(f, status) + else: + self.send_error(status.value, status.description) def list_directory(self, path): """Helper to produce a directory listing (absent index.html). diff -r 469ff344f8fd Lib/test/test_httpservers.py --- a/Lib/test/test_httpservers.py Sat Jan 31 12:20:40 2015 -0800 +++ b/Lib/test/test_httpservers.py Sun Feb 01 18:51:14 2015 +0530 @@ -760,6 +760,54 @@ self.assertEqual(path, self.translated) +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=None): + path = self.tempdir_name + if tail is not None: + 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() + self.assertIn(b'Directory listing for', html) + + def test_returns_file(self): + data = b"Test File for data" + with open(os.path.join(self.tempdir, 'test'), 'wb') as temp: + temp.write(data) + res = self.get_response('test') + self.assertEqual(data, res.fp.read()) + + def test_main(verbose=None): cwd = os.getcwd() try: