diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index ae75e6dc5fdcf3..b45ee7e0d997de 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -376,13 +376,16 @@ provides three different variants: :func:`os.listdir` to scan the directory, and returns a ``404`` error response if the :func:`~os.listdir` fails. - If the request was mapped to a file, it is opened. Any :exc:`OSError` - exception in opening the requested file is mapped to a ``404``, - ``'File not found'`` error. If there was a ``'If-Modified-Since'`` - header in the request, and the file was not modified after this time, - a ``304``, ``'Not Modified'`` response is sent. Otherwise, the content - type is guessed by calling the :meth:`guess_type` method, which in turn - uses the *extensions_map* variable, and the file contents are returned. + If the request did not map to a directory, a file is assumed. If the + file does not exist, and the file name has no extension, then default + extensions, if any, are tried. After an appropriate file name is + selected, it is opened. Any :exc:`OSError` exception in opening the + requested file is mapped to a ``404``, ``'File not found'`` error. If + there was a ``'If-Modified-Since'`` header in the request, and the file + was not modified after this time, a ``304``, ``'Not Modified'`` response + is sent. Otherwise, the content type is guessed by calling the + :meth:`guess_type` method, which in turn uses the *extensions_map* + variable, and the file contents are returned. A ``'Content-type:'`` header with the guessed content type is output, followed by a ``'Content-Length:'`` header with the file's size and a @@ -398,6 +401,9 @@ provides three different variants: .. versionchanged:: 3.7 Support of the ``'If-Modified-Since'`` header. + .. versionchanged:: 3.12 + Support for default extensions added. + The :class:`SimpleHTTPRequestHandler` class can be used in the following manner in order to create a very basic webserver serving files relative to the current directory:: @@ -416,7 +422,8 @@ the current directory:: :class:`SimpleHTTPRequestHandler` can also be subclassed to enhance behavior, such as using different index file names by overriding the class attribute -:attr:`index_pages`. +:attr:`index_pages`, or locating files with default extensions by overriding +the class attribute :attr:`default_extensions`. .. _http-server-cli: @@ -462,6 +469,17 @@ following command runs an HTTP/1.1 conformant server:: .. versionadded:: 3.11 ``--protocol`` argument was introduced. +By default, the server uses the exact url to locate file names. The option +``-e/--extension`` enables searching for ``.html``/``.htm`` files, or allows +specifying other extensions:: + + python -m http.server --extension # enables .html/.htm + pithon -m http.server --extension .pl .asp # enables .pl/.asp + +.. versionadded:: 3.12 + ``--ext`` argument was introduced. + + .. class:: CGIHTTPRequestHandler(request, client_address, server) This class is used to serve either files or output of CGI scripts from the diff --git a/Lib/http/server.py b/Lib/http/server.py index 971f08046d50b5..a67f5511bfe614 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -654,6 +654,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): server_version = "SimpleHTTP/" + __version__ index_pages = ("index.html", "index.htm") + default_extensions = () extensions_map = _encodings_map_default = { '.gz': 'application/gzip', '.Z': 'application/octet-stream', @@ -723,6 +724,12 @@ def send_head(self): if path.endswith("/"): self.send_error(HTTPStatus.NOT_FOUND, "File not found") return None + # Special case for URLs with no extension. + if os.path.splitext(path)[1] == "" and not os.path.exists(path): + for extension in self.default_extensions: + if os.path.exists(path + extension): + path += extension + break try: f = open(path, 'rb') except OSError: @@ -1246,7 +1253,7 @@ def _get_best_family(*address): def test(HandlerClass=BaseHTTPRequestHandler, ServerClass=ThreadingHTTPServer, - protocol="HTTP/1.0", port=8000, bind=None): + protocol="HTTP/1.0", port=8000, bind=None, extensions=None): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). @@ -1254,6 +1261,7 @@ def test(HandlerClass=BaseHTTPRequestHandler, """ ServerClass.address_family, addr = _get_best_family(bind, port) HandlerClass.protocol_version = protocol + HandlerClass.default_extensions = extensions with ServerClass(addr, HandlerClass) as httpd: host, port = httpd.socket.getsockname()[:2] url_host = f'[{host}]' if ':' in host else host @@ -1284,10 +1292,19 @@ def test(HandlerClass=BaseHTTPRequestHandler, default='HTTP/1.0', help='conform to this HTTP version ' '(default: %(default)s)') + parser.add_argument('-e', '--extension', default=None, nargs='*', + help='extensions to try if none specified in url ' + '(default: none; default with -e alone: .html .htm)') parser.add_argument('port', default=8000, type=int, nargs='?', help='bind to this port ' '(default: %(default)s)') args = parser.parse_args() + if args.extension is None: + ext = () + elif args.extension == []: + ext = ('.html', '.htm') + else: + ext = tuple(args.extension) if args.cgi: handler_class = CGIHTTPRequestHandler else: @@ -1313,4 +1330,5 @@ def finish_request(self, request, client_address): port=args.port, bind=args.bind, protocol=args.protocol, + extensions=ext, ) diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index cbcf94136ac4eb..d5a3a4e02a113a 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -590,6 +590,21 @@ def test_path_without_leading_slash(self): self.assertEqual(response.getheader("Location"), self.tempdir_name + "/?hi=1") + def test_default_extension(self): + # Checks an extensionless path finds a HMTL file, and finds an + # extensionless file with priority. + self.thread.request_handler.default_extensions = ('.html', ) + data1 = b"SPAM SPAM SPAM!\r\n" + with open(os.path.join(self.tempdir, 'spam.html'), 'wb') as f: + f.write(data1) + response = self.request(self.base_url + '/spam') + self.check_status_and_reason(response, HTTPStatus.OK, data1) + data2 = b"The one true spam.\r\n" + with open(os.path.join(self.tempdir, 'spam'), 'wb') as f: + f.write(data2) + response = self.request(self.base_url + '/spam') + self.check_status_and_reason(response, HTTPStatus.OK, data2) + def test_html_escape_filename(self): filename = '.txt' fullpath = os.path.join(self.tempdir, filename) diff --git a/Misc/NEWS.d/next/Library/2023-01-04-20-33-14.gh-issue-100463.ofnOye.rst b/Misc/NEWS.d/next/Library/2023-01-04-20-33-14.gh-issue-100463.ofnOye.rst new file mode 100644 index 00000000000000..219f4f34730423 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-01-04-20-33-14.gh-issue-100463.ofnOye.rst @@ -0,0 +1,2 @@ +Add support for default extensions in +``http.server.SimpleHTTPRequestHandler``.