-
-
Notifications
You must be signed in to change notification settings - Fork 34.5k
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 47 commits
0d02fbe
1838da7
a3256fd
77b5fff
5f89c97
a3243fe
b1026d2
6f88c13
7a793f2
9450b86
5a30d91
d317cc2
5f1fb94
c376a71
89a89f0
9653710
f3ae904
44efbed
d47c5a7
8d1286a
db9de68
e149708
c16f4c9
777b5b6
eac5c6a
c9c8083
c2d6bb3
3377cf7
06a9977
fae21f9
be78515
53965ff
c280ed8
64122df
f0d1bac
e99780e
2e829bb
8baa875
7856d27
303ab5b
ed0b0b3
79c577b
526e499
46c1c91
c1fee3b
3a4fed6
9f0ed01
1036a91
49ffc92
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -366,7 +366,8 @@ 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, extra_response_headers=None) | ||||||
|
|
||||||
| This class serves files from the directory *directory* and below, | ||||||
| or the current directory if *directory* is not provided, directly | ||||||
|
|
@@ -378,6 +379,9 @@ instantiation, of which this module provides three different variants: | |||||
| .. versionchanged:: 3.9 | ||||||
| The *directory* parameter accepts a :term:`path-like object`. | ||||||
|
|
||||||
| .. versionchanged:: next | ||||||
| Added *extra_response_headers* parameter. | ||||||
|
|
||||||
| 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. | ||||||
|
|
@@ -400,6 +404,15 @@ instantiation, of which this module provides three different variants: | |||||
| This dictionary is no longer filled with the default system mappings, | ||||||
| but only contains overrides. | ||||||
|
|
||||||
| .. attribute:: extra_response_headers | ||||||
|
|
||||||
| A sequence of ``(name, value)`` pairs containing user-defined extra HTTP | ||||||
| response headers to add to each successful HTTP status 200 response. These | ||||||
| headers are not included in other status code responses. | ||||||
|
|
||||||
| Headers that the server sends automatically (for instance Content-Type) | ||||||
| will not be overwritten by extra_response_headers. | ||||||
|
|
||||||
| The :class:`SimpleHTTPRequestHandler` class defines the following methods: | ||||||
|
|
||||||
| .. method:: do_HEAD() | ||||||
|
|
@@ -432,6 +445,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 ``extra_response_headers`` is a sequence of | ||||||
|
Member
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
Author
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. Fixed in 1036a91 |
||||||
| ``(name, value)`` pairs containing user-defined extra response headers. | ||||||
|
|
||||||
| Then follows a blank line signifying the end of the headers, and then the | ||||||
| contents of the file are output. | ||||||
|
|
||||||
|
|
@@ -547,6 +563,15 @@ The following options are accepted: | |||||
|
|
||||||
| .. versionadded:: 3.14 | ||||||
|
|
||||||
| .. option:: -H, --header <header> <value> | ||||||
|
picnixz marked this conversation as resolved.
|
||||||
|
|
||||||
| Specify an additional extra HTTP Response Header to send on successful HTTP | ||||||
| 200 responses. Can be used multiple times to send additional custom response | ||||||
| headers. Headers that are sent automatically by the server (for instance | ||||||
| Content-Type) will not be overwritten by the server. | ||||||
|
|
||||||
| .. versionadded:: next | ||||||
|
|
||||||
|
|
||||||
|
picnixz marked this conversation as resolved.
|
||||||
| .. _http.server-security: | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -466,6 +466,7 @@ def handle_one_request(self): | |||||||||||||||||||||||||||||||||||||
| def handle(self): | ||||||||||||||||||||||||||||||||||||||
| """Handle multiple requests if necessary.""" | ||||||||||||||||||||||||||||||||||||||
| self.close_connection = True | ||||||||||||||||||||||||||||||||||||||
| self.default_response_headers = [] | ||||||||||||||||||||||||||||||||||||||
|
Member
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'm always worried about creating new public attributes because we had a breakage of that with
Author
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 a good point. Looking at this Github code search , at least at this simple of a search I only see some projects that are defining a new function |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self.handle_one_request() | ||||||||||||||||||||||||||||||||||||||
| while not self.close_connection: | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -551,13 +552,15 @@ def send_response_only(self, code, message=None): | |||||||||||||||||||||||||||||||||||||
| (self.protocol_version, code, message)).encode( | ||||||||||||||||||||||||||||||||||||||
| 'latin-1', 'strict')) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def send_header(self, keyword, value): | ||||||||||||||||||||||||||||||||||||||
| def send_header(self, keyword, value, is_extra=False): | ||||||||||||||||||||||||||||||||||||||
| """Send a MIME header to the headers buffer.""" | ||||||||||||||||||||||||||||||||||||||
| if self.request_version != 'HTTP/0.9': | ||||||||||||||||||||||||||||||||||||||
| if not hasattr(self, '_headers_buffer'): | ||||||||||||||||||||||||||||||||||||||
| self._headers_buffer = [] | ||||||||||||||||||||||||||||||||||||||
| self._headers_buffer.append( | ||||||||||||||||||||||||||||||||||||||
| ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) | ||||||||||||||||||||||||||||||||||||||
| if not is_extra and hasattr(self, 'default_response_headers'): | ||||||||||||||||||||||||||||||||||||||
| self.default_response_headers.append((keyword, value)) | ||||||||||||||||||||||||||||||||||||||
|
Member
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
Author
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. Fixed in 49ffc92 |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if keyword.lower() == 'connection': | ||||||||||||||||||||||||||||||||||||||
| if value.lower() == 'close': | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -735,10 +738,11 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): | |||||||||||||||||||||||||||||||||||||
| '.xz': 'application/x-xz', | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def __init__(self, *args, directory=None, **kwargs): | ||||||||||||||||||||||||||||||||||||||
| def __init__(self, *args, directory=None, extra_response_headers=None, **kwargs): | ||||||||||||||||||||||||||||||||||||||
|
picnixz marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||
| if directory is None: | ||||||||||||||||||||||||||||||||||||||
| directory = os.getcwd() | ||||||||||||||||||||||||||||||||||||||
| self.directory = os.fspath(directory) | ||||||||||||||||||||||||||||||||||||||
| self.extra_response_headers = extra_response_headers | ||||||||||||||||||||||||||||||||||||||
| super().__init__(*args, **kwargs) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def do_GET(self): | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -756,6 +760,14 @@ def do_HEAD(self): | |||||||||||||||||||||||||||||||||||||
| if f: | ||||||||||||||||||||||||||||||||||||||
| f.close() | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _send_extra_response_headers(self): | ||||||||||||||||||||||||||||||||||||||
| """Send the headers stored in self.extra_response_headers""" | ||||||||||||||||||||||||||||||||||||||
|
Member
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
|
||||||||||||||||||||||||||||||||||||||
| if self.extra_response_headers is not None: | ||||||||||||||||||||||||||||||||||||||
| for header, value in self.extra_response_headers: | ||||||||||||||||||||||||||||||||||||||
| # Don't send the header if it's already sent as part of the default response headers | ||||||||||||||||||||||||||||||||||||||
| if header.lower() not in (h.lower() for h, _ in self.default_response_headers): | ||||||||||||||||||||||||||||||||||||||
|
Member
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
Author
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. Fixed in 49ffc92 |
||||||||||||||||||||||||||||||||||||||
| self.send_header(header, value, is_extra=True) | ||||||||||||||||||||||||||||||||||||||
|
Member
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
Author
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. Fixed in 49ffc92 |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def send_head(self): | ||||||||||||||||||||||||||||||||||||||
| """Common code for GET and HEAD commands. | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -838,6 +850,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_extra_response_headers() | ||||||||||||||||||||||||||||||||||||||
| self.end_headers() | ||||||||||||||||||||||||||||||||||||||
| return f | ||||||||||||||||||||||||||||||||||||||
| except: | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -902,6 +915,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_extra_response_headers() | ||||||||||||||||||||||||||||||||||||||
| self.end_headers() | ||||||||||||||||||||||||||||||||||||||
| return f | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -1010,25 +1024,33 @@ def _get_best_family(*address): | |||||||||||||||||||||||||||||||||||||
| return family, sockaddr | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def _make_server(HandlerClass=BaseHTTPRequestHandler, | ||||||||||||||||||||||||||||||||||||||
|
picnixz marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||
| ServerClass=ThreadingHTTPServer, | ||||||||||||||||||||||||||||||||||||||
| protocol="HTTP/1.0", port=8000, bind=None, | ||||||||||||||||||||||||||||||||||||||
| tls_cert=None, tls_key=None, tls_password=None): | ||||||||||||||||||||||||||||||||||||||
| ServerClass.address_family, addr = _get_best_family(bind, port) | ||||||||||||||||||||||||||||||||||||||
| HandlerClass.protocol_version = protocol | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if tls_cert: | ||||||||||||||||||||||||||||||||||||||
| return ServerClass(addr, HandlerClass, certfile=tls_cert, | ||||||||||||||||||||||||||||||||||||||
| keyfile=tls_key, password=tls_password) | ||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||
| return ServerClass(addr, HandlerClass) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| 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). | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||
| ServerClass.address_family, addr = _get_best_family(bind, port) | ||||||||||||||||||||||||||||||||||||||
| HandlerClass.protocol_version = protocol | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if tls_cert: | ||||||||||||||||||||||||||||||||||||||
| server = ServerClass(addr, HandlerClass, certfile=tls_cert, | ||||||||||||||||||||||||||||||||||||||
| keyfile=tls_key, password=tls_password) | ||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||
| server = ServerClass(addr, HandlerClass) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| with server as httpd: | ||||||||||||||||||||||||||||||||||||||
| 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' | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1069,6 +1091,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 specified multiple times)') | ||||||||||||||||||||||||||||||||||||||
| args = parser.parse_args(args) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if not args.tls_cert and args.tls_key: | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1097,7 +1123,8 @@ def server_bind(self): | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| def finish_request(self, request, client_address): | ||||||||||||||||||||||||||||||||||||||
| self.RequestHandlerClass(request, client_address, self, | ||||||||||||||||||||||||||||||||||||||
| directory=args.directory) | ||||||||||||||||||||||||||||||||||||||
| directory=args.directory, | ||||||||||||||||||||||||||||||||||||||
| extra_response_headers=args.header) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class HTTPDualStackServer(DualStackServerMixin, ThreadingHTTPServer): | ||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -540,8 +540,16 @@ def test_err(self): | |||||||||||||||||||||||
| self.assertIn(f"{t.status_client_error}404", lines[1]) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class CustomHeaderSimpleHTTPRequestHandler(SimpleHTTPRequestHandler): | ||||||||||||||||||||||||
| extra_response_headers = None | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def __init__(self, *args, **kwargs): | ||||||||||||||||||||||||
| kwargs.setdefault('extra_response_headers', self.extra_response_headers) | ||||||||||||||||||||||||
| super().__init__(*args, **kwargs) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
Comment on lines
+543
to
+549
Member
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
Author
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. Fixed in 777b5b6 |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class SimpleHTTPServerTestCase(BaseTestCase): | ||||||||||||||||||||||||
| class request_handler(NoLogRequestHandler, SimpleHTTPRequestHandler): | ||||||||||||||||||||||||
| class request_handler(NoLogRequestHandler, CustomHeaderSimpleHTTPRequestHandler): | ||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def setUp(self): | ||||||||||||||||||||||||
|
|
@@ -898,6 +906,55 @@ def test_path_without_leading_slash(self): | |||||||||||||||||||||||
| self.assertEqual(response.getheader("Location"), | ||||||||||||||||||||||||
| self.tempdir_name + "/?hi=1") | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_extra_response_headers_list_dir(self): | ||||||||||||||||||||||||
| with mock.patch.object(self.request_handler, 'extra_response_headers', [ | ||||||||||||||||||||||||
| ('X-Test1', 'test1'), | ||||||||||||||||||||||||
| ('X-Test2', 'test2'), | ||||||||||||||||||||||||
| ]): | ||||||||||||||||||||||||
| response = self.request(self.base_url + '/') | ||||||||||||||||||||||||
| self.assertEqual(response.status, 200) | ||||||||||||||||||||||||
| self.assertEqual(response.getheader("X-Test1"), 'test1') | ||||||||||||||||||||||||
| self.assertEqual(response.getheader("X-Test2"), 'test2') | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_extra_response_headers_get_file(self): | ||||||||||||||||||||||||
| with mock.patch.object(self.request_handler, 'extra_response_headers', [ | ||||||||||||||||||||||||
| ('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.status, 200) | ||||||||||||||||||||||||
| self.assertEqual(response.getheader("Set-Cookie"), | ||||||||||||||||||||||||
| 'test1=value1, test2=value2') | ||||||||||||||||||||||||
| self.assertEqual(response.getheader("X-Test1"), 'value3') | ||||||||||||||||||||||||
|
hugovk marked this conversation as resolved.
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_extra_response_headers_missing_on_404(self): | ||||||||||||||||||||||||
| with mock.patch.object(self.request_handler, 'extra_response_headers', [ | ||||||||||||||||||||||||
| ('X-Test1', 'value'), | ||||||||||||||||||||||||
| ]): | ||||||||||||||||||||||||
| response = self.request(self.base_url + '/missing.html') | ||||||||||||||||||||||||
| self.assertEqual(response.status, 404) | ||||||||||||||||||||||||
| self.assertEqual(response.getheader("X-Test1"), None) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_extra_response_headers_dont_overwrite_default_headers(self): | ||||||||||||||||||||||||
| with mock.patch.object(self.request_handler, 'extra_response_headers', [ | ||||||||||||||||||||||||
| ('Content-Type', 'test/not_allowed'), | ||||||||||||||||||||||||
| ('Server', 'not_allowed'), | ||||||||||||||||||||||||
| ('Set-Cookie', 'test=allowed'), | ||||||||||||||||||||||||
| ]): | ||||||||||||||||||||||||
| # The Content-Type header should not be overwritten by the extra_response_headers | ||||||||||||||||||||||||
| # But cookies in the extra_allowed_duplicate_headers are allowed, | ||||||||||||||||||||||||
| # including Set-Cookie | ||||||||||||||||||||||||
| response = self.request(self.base_url + '/') | ||||||||||||||||||||||||
| self.assertEqual(response.status, 200) | ||||||||||||||||||||||||
| self.assertNotEqual(response.getheader("Content-Type"), 'test/not_allowed') | ||||||||||||||||||||||||
| self.assertNotEqual(response.getheader("Server"), 'not_allowed') | ||||||||||||||||||||||||
| self.assertEqual(response.getheader("Set-Cookie"), 'test=allowed') | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class SocketlessRequestHandler(SimpleHTTPRequestHandler): | ||||||||||||||||||||||||
| def __init__(self, directory=None): | ||||||||||||||||||||||||
|
|
@@ -1447,6 +1504,21 @@ 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) | ||||||||||||||||||||||||
|
Member
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
Author
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 some new tests to verify exceptions with incorrect header CLI usage in c2d6bb3. I agree that |
||||||||||||||||||||||||
| mock_func.reset_mock() | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_extra_header_flag_too_few_args(self): | ||||||||||||||||||||||||
| with self.assertRaises(SystemExit): | ||||||||||||||||||||||||
| self.invoke_httpd('--header', 'h1') | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def test_extra_header_flag_too_many_args(self): | ||||||||||||||||||||||||
| with self.assertRaises(SystemExit): | ||||||||||||||||||||||||
| self.invoke_httpd('--header', 'h1', 'v1', 'h2') | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @unittest.skipIf(ssl is None, "requires ssl") | ||||||||||||||||||||||||
| @mock.patch('http.server.test') | ||||||||||||||||||||||||
| def test_tls_cert_and_key_flags(self, mock_func): | ||||||||||||||||||||||||
|
|
@@ -1530,6 +1602,36 @@ 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_extra_response_headers_arg(self, _, mock_make_server): | ||||||||||||||||||||||||
| server._main( | ||||||||||||||||||||||||
| ['-H', 'Set-Cookie', 'k=v', '-H', 'Set-Cookie', 'k2=v2:v3 v4', '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) | ||||||||||||||||||||||||
| self.addCleanup(httpd.server_close) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # 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 extra_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, | ||||||||||||||||||||||||
| extra_response_headers=[ | ||||||||||||||||||||||||
| ['Set-Cookie', 'k=v'], ['Set-Cookie', 'k2=v2:v3 v4'] | ||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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 ``-H`` or ``--header`` CLI option to :program:`python -m http.server`. Contributed by | ||
| 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.
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.
Fixed in 1036a91