From 0d02fbe1a3566985f2d17934988a9fe7b62135f4 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 26 May 2025 21:15:56 -0700 Subject: [PATCH 1/3] gh-135056: Add a --cors CLI argument to http.server Add a --cors command line argument to the stdlib http.server module, which will add an `Access-Control-Allow-Origin: *` header to all responses. As part of this implementation, add a `response_headers` argument to SimpleHTTPRequestHandler and HttpServer, which allows callers to add addition headers to the response. --- Doc/library/http.server.rst | 20 ++++++++- Lib/http/server.py | 42 +++++++++++++++---- Lib/test/test_httpservers.py | 32 ++++++++++++-- ...-06-02-22-23-38.gh-issue-135056.yz3dSs.rst | 2 + 4 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 063344e0284258..0f40214b771d2f 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,10 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.9 The *directory* parameter accepts a :term:`path-like object`. + .. versionchanged:: 3.15 + The *response_headers* parameter accepts an optional dictionary of + additional HTTP headers to add to each response. + 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 +432,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 headers specified in the dictionary instance argument + ``response_headers`` are each individually sent in the response. + Then follows a blank line signifying the end of the headers, and then the contents of the file are output. @@ -437,6 +444,9 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.7 Support of the ``'If-Modified-Since'`` header. + .. versionchanged:: 3.15 + 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,6 +553,14 @@ The following options are accepted: .. versionadded:: 3.14 +.. option:: --cors + + Adds an additional CORS (Cross-Origin Resource sharing) header to each response:: + + Access-Control-Allow-Origin: * + + .. versionadded:: 3.15 + .. _http.server-security: diff --git a/Lib/http/server.py b/Lib/http/server.py index ef10d185932633..152a4275d32ab5 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -117,6 +117,10 @@ class HTTPServer(socketserver.TCPServer): allow_reuse_address = True # Seems to make sense in testing environment allow_reuse_port = True + def __init__(self, *args, response_headers=None, **kwargs): + self.response_headers = response_headers if response_headers is not None else {} + super().__init__(*args, **kwargs) + def server_bind(self): """Override server_bind to store the server name.""" socketserver.TCPServer.server_bind(self) @@ -124,6 +128,11 @@ def server_bind(self): self.server_name = socket.getfqdn(host) self.server_port = port + def finish_request(self, request, client_address): + """Finish one request by instantiating RequestHandlerClass.""" + args = (request, client_address, self) + kwargs = dict(response_headers=self.response_headers) if self.response_headers else dict() + self.RequestHandlerClass(*args, **kwargs) class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): daemon_threads = True @@ -132,7 +141,7 @@ class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): class HTTPSServer(HTTPServer): def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, *, certfile, keyfile=None, - password=None, alpn_protocols=None): + password=None, alpn_protocols=None, response_headers=None): try: import ssl except ImportError: @@ -150,7 +159,8 @@ def __init__(self, server_address, RequestHandlerClass, super().__init__(server_address, RequestHandlerClass, - bind_and_activate) + bind_and_activate, + response_headers=response_headers) def server_activate(self): """Wrap the socket in SSLSocket.""" @@ -692,10 +702,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 or {} super().__init__(*args, **kwargs) def do_GET(self): @@ -736,6 +747,10 @@ def send_head(self): new_url = urllib.parse.urlunsplit(new_parts) self.send_header("Location", new_url) self.send_header("Content-Length", "0") + # User specified response_headers + if self.response_headers is not None: + for header, value in self.response_headers.items(): + self.send_header(header, value) self.end_headers() return None for index in self.index_pages: @@ -795,6 +810,9 @@ def send_head(self): self.send_header("Content-Length", str(fs[6])) self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + if self.response_headers is not None: + for header, value in self.response_headers.items(): + self.send_header(header, value) self.end_headers() return f except: @@ -970,7 +988,7 @@ def _get_best_family(*address): def test(HandlerClass=BaseHTTPRequestHandler, ServerClass=ThreadingHTTPServer, protocol="HTTP/1.0", port=8000, bind=None, - tls_cert=None, tls_key=None, tls_password=None): + tls_cert=None, tls_key=None, tls_password=None, response_headers=None): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). @@ -981,9 +999,10 @@ def test(HandlerClass=BaseHTTPRequestHandler, if tls_cert: server = ServerClass(addr, HandlerClass, certfile=tls_cert, - keyfile=tls_key, password=tls_password) + keyfile=tls_key, password=tls_password, + response_headers=response_headers) else: - server = ServerClass(addr, HandlerClass) + server = ServerClass(addr, HandlerClass, response_headers=response_headers) with server as httpd: host, port = httpd.socket.getsockname()[:2] @@ -1024,6 +1043,8 @@ def _main(args=None): parser.add_argument('port', default=8000, type=int, nargs='?', help='bind to this port ' '(default: %(default)s)') + parser.add_argument('--cors', action='store_true', + help='Enable Access-Control-Allow-Origin: * header') args = parser.parse_args(args) if not args.tls_cert and args.tls_key: @@ -1051,8 +1072,11 @@ def server_bind(self): return super().server_bind() def finish_request(self, request, client_address): - self.RequestHandlerClass(request, client_address, self, - directory=args.directory) + handler_args = (request, client_address, self) + handler_kwargs = dict(directory=args.directory) + if self.response_headers: + handler_kwargs['response_headers'] = self.response_headers + self.RequestHandlerClass(*handler_args, **handler_kwargs) class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): pass @@ -1060,6 +1084,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): pass ServerClass = HTTPSDualStackServer if args.tls_cert else HTTPDualStackServer + response_headers = {'Access-Control-Allow-Origin': '*'} if args.cors else None test( HandlerClass=SimpleHTTPRequestHandler, @@ -1070,6 +1095,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): tls_cert=args.tls_cert, tls_key=args.tls_key, tls_password=tls_key_password, + response_headers=response_headers ) diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 2548a7c5f292f0..b1b711b0387db0 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -81,11 +81,12 @@ def test_https_server_raises_runtime_error(self): class TestServerThread(threading.Thread): - def __init__(self, test_object, request_handler, tls=None): + def __init__(self, test_object, request_handler, tls=None, server_kwargs=None): threading.Thread.__init__(self) self.request_handler = request_handler self.test_object = test_object self.tls = tls + self.server_kwargs = server_kwargs or {} def run(self): if self.tls: @@ -95,7 +96,8 @@ def run(self): request_handler=self.request_handler, ) else: - self.server = HTTPServer(('localhost', 0), self.request_handler) + self.server = HTTPServer(('localhost', 0), self.request_handler, + **self.server_kwargs) self.test_object.HOST, self.test_object.PORT = self.server.socket.getsockname() self.test_object.server_started.set() self.test_object = None @@ -113,12 +115,14 @@ class BaseTestCase(unittest.TestCase): # Optional tuple (certfile, keyfile, password) to use for HTTPS servers. tls = None + server_kwargs = None def setUp(self): self._threads = threading_helper.threading_setup() os.environ = os_helper.EnvironmentVarGuard() self.server_started = threading.Event() - self.thread = TestServerThread(self, self.request_handler, self.tls) + self.thread = TestServerThread(self, self.request_handler, self.tls, + self.server_kwargs) self.thread.start() self.server_started.wait() @@ -824,6 +828,16 @@ def test_path_without_leading_slash(self): self.tempdir_name + "/?hi=1") +class CorsHTTPServerTestCase(SimpleHTTPServerTestCase): + server_kwargs = dict( + response_headers = {'Access-Control-Allow-Origin': '*'} + ) + def test_cors(self): + response = self.request(self.base_url + '/test') + self.check_status_and_reason(response, HTTPStatus.OK) + self.assertEqual(response.getheader('Access-Control-Allow-Origin'), '*') + + class SocketlessRequestHandler(SimpleHTTPRequestHandler): def __init__(self, directory=None): request = mock.Mock() @@ -1306,6 +1320,7 @@ class CommandLineTestCase(unittest.TestCase): 'tls_cert': None, 'tls_key': None, 'tls_password': None, + 'response_headers': None, } def setUp(self): @@ -1371,6 +1386,17 @@ 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_cors_flag(self, mock_func): + self.invoke_httpd('--cors') + call_args = self.args | dict( + response_headers={ + 'Access-Control-Allow-Origin': '*' + } + ) + 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): 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..d6fa033573e25b --- /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 ``--cors`` cli option to ``python -m http.server``. Contributed by +Anton I. Sipos From 1838da7f9bf7ab91f7699120a9fc0d24c8501edd Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 2 Jun 2025 22:43:23 -0700 Subject: [PATCH 2/3] gh-issue-135056: Fix doc versionchanged and NEWS entries. --- Doc/library/http.server.rst | 6 +++--- .../Library/2025-06-02-22-23-38.gh-issue-135056.yz3dSs.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/http.server.rst b/Doc/library/http.server.rst index 0f40214b771d2f..ab53f71c030c96 100644 --- a/Doc/library/http.server.rst +++ b/Doc/library/http.server.rst @@ -374,7 +374,7 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.9 The *directory* parameter accepts a :term:`path-like object`. - .. versionchanged:: 3.15 + .. versionchanged:: next The *response_headers* parameter accepts an optional dictionary of additional HTTP headers to add to each response. @@ -444,7 +444,7 @@ instantiation, of which this module provides three different variants: .. versionchanged:: 3.7 Support of the ``'If-Modified-Since'`` header. - .. versionchanged:: 3.15 + .. versionchanged:: next Support ``response_headers`` as an instance argument. The :class:`SimpleHTTPRequestHandler` class can be used in the following @@ -559,7 +559,7 @@ The following options are accepted: Access-Control-Allow-Origin: * - .. versionadded:: 3.15 + .. versionadded:: next .. _http.server-security: 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 index d6fa033573e25b..929a4d08d19834 100644 --- 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 @@ -1,2 +1,2 @@ -Add a ``--cors`` cli option to ``python -m http.server``. Contributed by -Anton I. Sipos +Add a ``--cors`` cli option to :program:`python -m http.server`. Contributed by +Anton I. Sipos. From a3256fd21ba1d1c420e77a0007bea186a2f1f6a2 Mon Sep 17 00:00:00 2001 From: "Anton I. Sipos" Date: Mon, 2 Jun 2025 23:09:49 -0700 Subject: [PATCH 3/3] gh-13056: Allow unspecified response_headers in HTTPServer. This fixes the breakage to HttpServer as used by wsgiref. --- Lib/http/server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/http/server.py b/Lib/http/server.py index 152a4275d32ab5..1dfe735e688276 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -118,7 +118,7 @@ class HTTPServer(socketserver.TCPServer): allow_reuse_port = True def __init__(self, *args, response_headers=None, **kwargs): - self.response_headers = response_headers if response_headers is not None else {} + self.response_headers = response_headers super().__init__(*args, **kwargs) def server_bind(self): @@ -131,7 +131,10 @@ def server_bind(self): def finish_request(self, request, client_address): """Finish one request by instantiating RequestHandlerClass.""" args = (request, client_address, self) - kwargs = dict(response_headers=self.response_headers) if self.response_headers else dict() + kwargs = {} + response_headers = getattr(self, 'response_headers', None) + if response_headers: + kwargs['response_headers'] = self.response_headers self.RequestHandlerClass(*args, **kwargs) class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

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:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy