-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
gh-135056: Add a --cors CLI argument to http.server #135057
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -117,13 +117,25 @@ 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 | ||||||||||||||||||||||
super().__init__(*args, **kwargs) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def server_bind(self): | ||||||||||||||||||||||
"""Override server_bind to store the server name.""" | ||||||||||||||||||||||
socketserver.TCPServer.server_bind(self) | ||||||||||||||||||||||
host, port = self.server_address[:2] | ||||||||||||||||||||||
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 = {} | ||||||||||||||||||||||
response_headers = getattr(self, 'response_headers', None) | ||||||||||||||||||||||
if response_headers: | ||||||||||||||||||||||
kwargs['response_headers'] = self.response_headers | ||||||||||||||||||||||
self.RequestHandlerClass(*args, **kwargs) | ||||||||||||||||||||||
Comment on lines
+133
to
+138
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
|
||||||||||||||||||||||
class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): | ||||||||||||||||||||||
daemon_threads = True | ||||||||||||||||||||||
|
@@ -132,7 +144,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): | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
And pass |
||||||||||||||||||||||
try: | ||||||||||||||||||||||
import ssl | ||||||||||||||||||||||
except ImportError: | ||||||||||||||||||||||
|
@@ -150,7 +162,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 +705,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 {} | ||||||||||||||||||||||
Comment on lines
+708
to
+712
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
You're already checking for |
||||||||||||||||||||||
super().__init__(*args, **kwargs) | ||||||||||||||||||||||
|
||||||||||||||||||||||
def do_GET(self): | ||||||||||||||||||||||
|
@@ -736,6 +750,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) | ||||||||||||||||||||||
Comment on lines
+754
to
+756
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's make it a private method, say |
||||||||||||||||||||||
self.end_headers() | ||||||||||||||||||||||
return None | ||||||||||||||||||||||
for index in self.index_pages: | ||||||||||||||||||||||
|
@@ -795,6 +813,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 +991,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): | ||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Let's group the parameters per purpose |
||||||||||||||||||||||
"""Test the HTTP request handler class. | ||||||||||||||||||||||
|
||||||||||||||||||||||
This runs an HTTP server on port 8000 (or the port argument). | ||||||||||||||||||||||
|
@@ -981,9 +1002,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 +1046,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,15 +1075,19 @@ 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) | ||||||||||||||||||||||
Comment on lines
+1078
to
+1082
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||
|
||||||||||||||||||||||
class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): | ||||||||||||||||||||||
pass | ||||||||||||||||||||||
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 +1098,7 @@ class HTTPSDualStackServer(DualStackServerMixin, ThreadingHTTPSServer): | |||||||||||||||||||||
tls_cert=args.tls_cert, | ||||||||||||||||||||||
tls_key=args.tls_key, | ||||||||||||||||||||||
tls_password=tls_key_password, | ||||||||||||||||||||||
response_headers=response_headers | ||||||||||||||||||||||
) | ||||||||||||||||||||||
|
||||||||||||||||||||||
|
||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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, | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You must also modify |
||||||||||||||
**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': '*'} | ||||||||||||||
) | ||||||||||||||
Comment on lines
+832
to
+834
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
def test_cors(self): | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
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): | ||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Add a ``--cors`` cli option to :program:`python -m http.server`. Contributed by | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's also update What's New/3.15.rst |
||
Anton I. Sipos. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As Hugo said, since we're anyway exposing
response-headers
, I think we should also expose it from the CLI. It could be useful for users in general (e.g.,--add-header NAME VALUE
with the-H
alias).