diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 063344e0284258..62df260e8f5eb2 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -362,7 +362,7 @@ instantiation, of which this module provides three different variants: delays, it now always returns the IP address. -.. class:: SimpleHTTPRequestHandler(request, client_address, server, directory=None) +.. class:: SimpleHTTPRequestHandler(request, client_address, server, directory=None, response_headers=None) This class serves files from the directory *directory* and below, or the current directory if *directory* is not provided, directly @@ -374,6 +374,11 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.9 The *directory* parameter accepts a :term:`path-like object`. + .. versionchanged:: next + Added *response_headers*, which accepts an optional iterable of + name/value pairs of HTTP headers to add to each successful HTTP status 200 + response. All other status code responses will not include these headers. + A lot of the work, such as parsing the request, is done by the base class :class:`BaseHTTPRequestHandler`. This class implements the :func:`do_GET` and :func:`do_HEAD` functions. @@ -428,6 +433,9 @@ instantiation, of which this module provides three different variants: followed by a ``'Content-Length:'`` header with the file's size and a ``'Last-Modified:'`` header with the file's modification time. + The instance attribute ``response_headers`` is used as an iterable of + name/value pairs to set user specified custom response headers. + Then follows a blank line signifying the end of the headers, and then the contents of the file are output. @@ -437,6 +445,9 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.7 Support of the ``'If-Modified-Since'`` header. + .. versionchanged:: next + Support ``response_headers`` as an instance argument. + 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:: @@ -543,7 +554,6 @@ The following options are accepted: .. versionadded:: 3.14 - .. _http.server-security: Security considerations diff --git a/Lib/http/server.py b/Lib/http/server.py index ef10d185932633..ac22c6a76cdba3 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -692,10 +692,11 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): '.xz': 'application/x-xz', } - def __init__(self, *args, directory=None, **kwargs): + def __init__(self, *args, directory=None, response_headers=None, **kwargs): if directory is None: directory = os.getcwd() self.directory = os.fspath(directory) + self.response_headers = response_headers super().__init__(*args, **kwargs) def do_GET(self): @@ -713,6 +714,13 @@ def do_HEAD(self): if f: f.close() + def send_custom_response_headers(self): + """Send the headers stored in self.response_headers""" + # User specified response_headers + if self.response_headers is not None: + for header, value in self.response_headers: + self.send_header(header, value) + def send_head(self): """Common code for GET and HEAD commands. @@ -795,6 +803,7 @@ def send_head(self): self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self.send_custom_response_headers() self.end_headers() return f except: @@ -859,6 +868,7 @@ def list_directory(self, path): self.send_response(HTTPStatus.OK) self.send_header("Content-type", "text/html; charset=%s" % enc) self.send_header("Content-Length", str(len(encoded))) + self.send_custom_response_headers() self.end_headers() return f @@ -998,6 +1008,7 @@ def test(HandlerClass=BaseHTTPRequestHandler, except KeyboardInterrupt: print("\nKeyboard interrupt received, exiting.") sys.exit(0) + return server def _main(args=None): @@ -1024,6 +1035,10 @@ def _main(args=None): parser.add_argument('port', default=8000, type=int, nargs='?', help='bind to this port ' '(default: %(default)s)') + parser.add_argument('-H', '--header', nargs=2, action='append', + metavar=('HEADER', 'VALUE'), + help='Add a custom response header ' + '(can be used multiple times)') args = parser.parse_args(args) if not args.tls_cert and args.tls_key: @@ -1052,7 +1067,8 @@ def server_bind(self): def finish_request(self, request, client_address): self.RequestHandlerClass(request, client_address, self, - directory=args.directory) + directory=args.directory, + response_headers=args.header) class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): pass @@ -1061,7 +1077,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer - test( + return test( HandlerClass=SimpleHTTPRequestHandler, ServerClass=ServerClass, port=args.port, diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 2548a7c5f292f0..77b9ef1aa4d870 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -465,8 +465,14 @@ def test_err(self): self.assertEndsWith(lines[1], '"ERROR / HTTP/1.1" 404 -') +class CustomHeaderSimpleHTTPRequestHandler(SimpleHTTPRequestHandler): + custom_headers = None + def __init__(self, *args, directory=None, response_headers=None, **kwargs): + super().__init__(*args, response_headers=self.custom_headers, **kwargs) + + class SimpleHTTPServerTestCase(BaseTestCase): - class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler): + class request_handler(NoLogRequestHandler, CustomHeaderSimpleHTTPRequestHandler): pass def setUp(self): @@ -823,6 +829,28 @@ def test_path_without_leading_slash(self): self.assertEqual(response.getheader("Location"), self.tempdir_name + "/?hi=1") + def test_custom_headers_list_dir(self): + with mock.patch.object(self.request_handler, 'custom_headers', new=[ + ('X-Test1', 'test1'), + ('X-Test2', 'test2'), + ]): + response = self.request(self.base_url + '/') + self.assertEqual(response.getheader("X-Test1"), 'test1') + self.assertEqual(response.getheader("X-Test2"), 'test2') + + def test_custom_headers_get_file(self): + with mock.patch.object(self.request_handler, 'custom_headers', new=[ + ('Set-Cookie', 'test1=value1'), + ('Set-Cookie', 'test2=value2'), + ('X-Test1', 'value3'), + ]): + data = b"Dummy index file\r\n" + with open(os.path.join(self.tempdir_name, 'index.html'), 'wb') as f: + f.write(data) + response = self.request(self.base_url + '/') + self.assertEqual(response.getheader("Set-Cookie"), + 'test1=value1, test2=value2') + self.assertEqual(response.getheader("X-Test1"), 'value3') class SocketlessRequestHandler(SimpleHTTPRequestHandler): def __init__(self, directory=None): @@ -1371,6 +1399,13 @@ def test_protocol_flag(self, mock_func): mock_func.assert_called_once_with(**call_args) mock_func.reset_mock() + @mock.patch('http.server.test') + def test_header_flag(self, mock_func): + call_args = self.args + self.invoke_httpd('--header', 'h1', 'v1', '-H', 'h2', 'v2') + mock_func.assert_called_once_with(**call_args) + mock_func.reset_mock() + @unittest.skipIf(ssl is None, "requires ssl") @mock.patch('http.server.test') def test_tls_cert_and_key_flags(self, mock_func): @@ -1454,6 +1489,26 @@ def test_unknown_flag(self, _): self.assertEqual(stdout.getvalue(), '') self.assertIn('error', stderr.getvalue()) + def test_response_headers_arg(self): + with mock.patch.object(HTTPServer, 'serve_forever'): + httpd = server._main( + ['-H', 'Set-Cookie', 'k=v', '-H', 'Set-Cookie', 'k2=v2', '8080'] + ) + request_handler_class = httpd.RequestHandlerClass + with mock.patch.object( + request_handler_class, '__init__' + ) as mock_handler_init: + mock_handler_init.return_value = None + # finish_request instantiates a request handler class, + # ensure response_headers are passed to it + httpd.finish_request(mock.Mock(), '127.0.0.1') + mock_handler_init.assert_called_once_with( + mock.ANY, mock.ANY, mock.ANY, directory=mock.ANY, + response_headers=[ + ['Set-Cookie', 'k=v'], ['Set-Cookie', 'k2=v2'] + ] + ) + class CommandLineRunTimeTestCase(unittest.TestCase): served_data = os.urandom(32) diff --git a/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst new file mode 100644 index 00000000000000..0565260dc443ec --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst @@ -0,0 +1,2 @@ +Add a ``--header`` CLI option to :program:`python -m http.server`. Contributed by +Anton I. Sipos.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: