-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
gh-135056: Add a --header 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
0d02fbe
1838da7
a3256fd
77b5fff
5f89c97
a3243fe
b1026d2
6f88c13
7a793f2
9450b86
5a30d91
d317cc2
5f1fb94
c376a71
89a89f0
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 | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -362,17 +362,23 @@ 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 | ||||||||||
mapping the directory structure to HTTP requests. | ||||||||||
|
||||||||||
.. versionchanged:: 3.7 | ||||||||||
Added the *directory* parameter. | ||||||||||
Added the *directory* keyword argument. | ||||||||||
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. This wasn't what I had in mind. It's still correct to use parameter (so revert c376a71). What I wanted is for you to change the signature: .. class:: SimpleHTTPRequestHandler(request, client_address, server, directory=None, response_headers=None) to .. class:: SimpleHTTPRequestHandler(request, client_address, server,\
*, directory=None, response_headers=None) |
||||||||||
|
||||||||||
.. versionchanged:: 3.9 | ||||||||||
The *directory* parameter accepts a :term:`path-like object`. | ||||||||||
The *directory* keyword argument accepts a :term:`path-like object`. | ||||||||||
|
||||||||||
.. versionchanged:: next | ||||||||||
Added a *response_headers* keyword argument, 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` | ||||||||||
|
@@ -428,6 +434,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. | ||||||||||
Comment on lines
+437
to
+438
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
|
||||||||||
|
||||||||||
Then follows a blank line signifying the end of the headers, and then the | ||||||||||
contents of the file are output. | ||||||||||
|
||||||||||
|
@@ -437,6 +446,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. | ||||||||||
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. Isn’t this redundant with the entry already under the constructor heading? 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. Perhaps - it seems the constructor documentation is used to make a brief mention of each argument and when it was added, with more detail being filled in later. My latest commits make several changes requested elsewhere for other reasons, but if the current version is still too redundant in multiple places I can make some more edits. 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. I don't think we should add this information. There is no notion of an "instance argument": it should rather be an instance attribute, and this should be documented through a |
||||||||||
|
||||||||||
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 +555,6 @@ The following options are accepted: | |||||||||
|
||||||||||
.. versionadded:: 3.14 | ||||||||||
|
||||||||||
|
||||||||||
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. This is an unrelated change, please revert. |
||||||||||
.. _http.server-security: | ||||||||||
|
||||||||||
Security considerations | ||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -109,6 +109,18 @@ difflib | |||||
(Contributed by Jiahao Li in :gh:`134580`.) | ||||||
|
||||||
|
||||||
http.server | ||||||
----------- | ||||||
|
||||||
* Added a new ``response_headers=`` keyword argument to | ||||||
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:`SimpleHTTPRequestHandler` to support custom headers in HTTP responses. | ||||||
(Contributed by Anton I. Sipos in :gh:`135057`.) | ||||||
|
||||||
* Added a ``--header`` flag to the :program:`python -m http.server` | ||||||
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. Please document |
||||||
command-line interface to support custom headers in HTTP responses. | ||||||
(Contributed by Anton I. Sipos in :gh:`135057`.) | ||||||
|
||||||
|
||||||
math | ||||||
---- | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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 | ||||||
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. Clarify as an internal private attribute:
Suggested change
Or document SimpleHTTPRequestHandler.response_headers as a public attribute. 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. So far I have intended |
||||||
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): | ||||||
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. Should we simply have "send_response_headers"? or is there a need to add "custom_response_headers"? is there a plan for a future distinction between these two? 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. I added this function to DRY the usages in both I suppose it would be possible to combine the functionality of the other headers into a single
I do like the distinct naming of 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. I think we should make it a private method, rename it I would first opt for a private method and a private attribute. Alternatively, I would name the attribute |
||||||
"""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 | ||||||
|
||||||
|
@@ -967,25 +977,34 @@ def _get_best_family(*address): | |||||
return family, sockaddr | ||||||
|
||||||
|
||||||
def test(HandlerClass=BaseHTTPRequestHandler, | ||||||
def _make_server(HandlerClass=BaseHTTPRequestHandler, | ||||||
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. Pleae align the argument list vertically as it is done currently |
||||||
ServerClass=ThreadingHTTPServer, | ||||||
protocol="HTTP/1.0", port=8000, bind=None, | ||||||
tls_cert=None, tls_key=None, tls_password=None): | ||||||
"""Test the HTTP request handler class. | ||||||
|
||||||
This runs an HTTP server on port 8000 (or the port argument). | ||||||
|
||||||
""" | ||||||
ServerClass.address_family, addr = _get_best_family(bind, port) | ||||||
HandlerClass.protocol_version = protocol | ||||||
|
||||||
if tls_cert: | ||||||
server = ServerClass(addr, HandlerClass, certfile=tls_cert, | ||||||
return ServerClass(addr, HandlerClass, certfile=tls_cert, | ||||||
keyfile=tls_key, password=tls_password) | ||||||
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. Please align the call. |
||||||
else: | ||||||
server = ServerClass(addr, HandlerClass) | ||||||
return ServerClass(addr, HandlerClass) | ||||||
|
||||||
with server as httpd: | ||||||
|
||||||
def test(HandlerClass=BaseHTTPRequestHandler, | ||||||
ServerClass=ThreadingHTTPServer, | ||||||
protocol="HTTP/1.0", port=8000, bind=None, | ||||||
tls_cert=None, tls_key=None, tls_password=None): | ||||||
"""Test the HTTP request handler class. | ||||||
|
||||||
This runs an HTTP server on port 8000 (or the port argument). | ||||||
|
||||||
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
|
||||||
""" | ||||||
with _make_server( | ||||||
HandlerClass=HandlerClass, ServerClass=ServerClass, | ||||||
protocol=protocol, port=port, bind=bind, tls_cert=tls_cert, | ||||||
tls_key=tls_key, tls_password=tls_password | ||||||
) as httpd: | ||||||
host, port = httpd.socket.getsockname()[:2] | ||||||
url_host = f'[{host}]' if ':' in host else host | ||||||
protocol = 'HTTPS' if tls_cert else 'HTTP' | ||||||
|
@@ -1024,6 +1043,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)') | ||||||
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. The 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. I agree in the code the multiple hint is obvious, but I added this for users running
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. Oh right. I think this should be improved in argparse then. |
||||||
args = parser.parse_args(args) | ||||||
|
||||||
if not args.tls_cert and args.tls_key: | ||||||
|
@@ -1052,7 +1075,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 | ||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
Comment on lines
+468
to
+472
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 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) | ||||||||||||||||||||||||
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. I don't think we need to check this as You can however check bad usages of |
||||||||||||||||||||||||
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,34 @@ def test_unknown_flag(self, _): | |||||||||||||||||||||||
self.assertEqual(stdout.getvalue(), '') | ||||||||||||||||||||||||
self.assertIn('error', stderr.getvalue()) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
@mock.patch('http.server._make_server', wraps=server._make_server) | ||||||||||||||||||||||||
@mock.patch.object(HTTPServer, 'serve_forever') | ||||||||||||||||||||||||
def test_response_headers_arg(self, _, mock_make_server): | ||||||||||||||||||||||||
server._main( | ||||||||||||||||||||||||
['-H', 'Set-Cookie', 'k=v', '-H', 'Set-Cookie', 'k2=v2', '8080'] | ||||||||||||||||||||||||
) | ||||||||||||||||||||||||
# Get an instance of the server / RequestHandler by using | ||||||||||||||||||||||||
# the spied call args, then calling _make_server with them. | ||||||||||||||||||||||||
args, kwargs = mock_make_server.call_args | ||||||||||||||||||||||||
httpd = server._make_server(*args, **kwargs) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
# Ensure the RequestHandler class is passed the correct response | ||||||||||||||||||||||||
# headers | ||||||||||||||||||||||||
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'] | ||||||||||||||||||||||||
] | ||||||||||||||||||||||||
Comment on lines
+1514
to
+1517
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 CommandLineRunTimeTestCase(unittest.TestCase): | ||||||||||||||||||||||||
served_data = os.urandom(32) | ||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Add a ``--header`` 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. It's more than just the |
||
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.
Document that
directory
andresponse_headers
are keyword arguments actually.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.
Done in c376a71