diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
index 02957c51a2..d0a74f2c7b 100644
a
|
b
|
This is a simple server for use in testing or debugging Django apps. It hasn't
|
7 | 7 | been reviewed for security issues. DON'T USE IT FOR PRODUCTION USE! |
8 | 8 | """ |
9 | 9 | |
| 10 | from http import HTTPStatus |
10 | 11 | import logging |
11 | 12 | import socket |
12 | 13 | import socketserver |
… |
… |
class ServerHandler(simple_server.ServerHandler):
|
94 | 95 | content_length = 0 |
95 | 96 | super().__init__(LimitedStream(stdin, content_length), stdout, stderr, environ, **kwargs) |
96 | 97 | |
| 98 | def _write(self, data): |
| 99 | if self.headers_sent and self.headers.get('Transfer-Encoding', False) == "chunked": |
| 100 | data = format(len(data), "X").encode() + b"\r\n" + data + b"\r\n" |
| 101 | |
| 102 | super()._write(data) |
| 103 | |
| 104 | def send_headers(self): |
| 105 | """Transmit headers to the client, via self._write()""" |
| 106 | self.cleanup_headers() |
| 107 | if not self.origin_server or self.client_is_modern(): |
| 108 | self.send_preamble() |
| 109 | self._write(bytes(self.headers)) |
| 110 | # Only mark headers as sent after they're actually written |
| 111 | self.headers_sent = True |
| 112 | |
| 113 | def finish_content(self): |
| 114 | super().finish_content() |
| 115 | |
| 116 | if self.headers_sent and self.headers.get('Transfer-Encoding', False) == "chunked": |
| 117 | self._write(b'') # End message for chunked encoding |
| 118 | |
| 119 | def set_content_length(self): |
| 120 | """Compute Content-Length or switch to chunked encoding if possible""" |
| 121 | try: |
| 122 | blocks = len(self.result) |
| 123 | except (TypeError, AttributeError, NotImplementedError): |
| 124 | pass |
| 125 | else: |
| 126 | if blocks == 1: |
| 127 | self.headers['Content-Length'] = str(self.bytes_sent) |
| 128 | return |
| 129 | |
| 130 | # Switch to chunked encoding |
| 131 | self.headers['Transfer-Encoding'] = 'chunked' |
| 132 | |
97 | 133 | def cleanup_headers(self): |
98 | 134 | super().cleanup_headers() |
99 | | # HTTP/1.1 requires support for persistent connections. Send 'close' if |
100 | | # the content length is unknown to prevent clients from reusing the |
101 | | # connection. |
102 | | if 'Content-Length' not in self.headers: |
103 | | self.headers['Connection'] = 'close' |
104 | 135 | # Persistent connections require threading server. |
105 | | elif not isinstance(self.request_handler.server, socketserver.ThreadingMixIn): |
| 136 | if not isinstance(self.request_handler.server, socketserver.ThreadingMixIn): |
106 | 137 | self.headers['Connection'] = 'close' |
107 | 138 | # Mark the connection for closing if it's set as such above or if the |
108 | 139 | # application sent the header. |
… |
… |
class WSGIRequestHandler(simple_server.WSGIRequestHandler):
|
172 | 203 | self.handle_one_request() |
173 | 204 | while not self.close_connection: |
174 | 205 | self.handle_one_request() |
| 206 | |
| 207 | # Wait for the connection to be closed by the client to ensure |
| 208 | # that all data was received. Shutting down the connection seems |
| 209 | # to flush any data in the send buffer and immediately ends the |
| 210 | # connection, which risks having large responses cut off. |
| 211 | if getattr(self, 'request_version', None) == 'HTTP/1.1': |
| 212 | self.rfile.peek() |
| 213 | |
175 | 214 | try: |
176 | 215 | self.connection.shutdown(socket.SHUT_WR) |
177 | 216 | except (AttributeError, OSError): |
178 | 217 | pass |
179 | 218 | |
180 | 219 | def handle_one_request(self): |
181 | | """Copy of WSGIRequestHandler.handle() but with different ServerHandler""" |
| 220 | """ |
| 221 | Copy of WSGIRequestHandler.handle() but with different ServerHandler, |
| 222 | and re-aligned with BaseHTTPRequestHandler.handle() |
| 223 | """ |
182 | 224 | self.raw_requestline = self.rfile.readline(65537) |
183 | 225 | if len(self.raw_requestline) > 65536: |
184 | 226 | self.requestline = '' |
185 | 227 | self.request_version = '' |
186 | 228 | self.command = '' |
187 | | self.send_error(414) |
| 229 | self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG) |
| 230 | return |
| 231 | |
| 232 | if not self.raw_requestline: |
| 233 | self.close_connection = True |
188 | 234 | return |
189 | 235 | |
190 | 236 | if not self.parse_request(): # An error code has been sent, just exit |
… |
… |
class WSGIRequestHandler(simple_server.WSGIRequestHandler):
|
196 | 242 | handler.request_handler = self # backpointer for logging & connection closing |
197 | 243 | handler.run(self.server.get_app()) |
198 | 244 | |
| 245 | self.wfile.flush() #actually send the response if not already done. |
| 246 | |
199 | 247 | |
200 | 248 | def run(addr, port, wsgi_handler, ipv6=False, threading=False, server_cls=WSGIServer): |
201 | 249 | server_address = (addr, port) |
diff --git a/tests/servers/test_basehttp.py b/tests/servers/test_basehttp.py
index 32fdbf3c0e..4bedc390dc 100644
a
|
b
|
|
1 | | from io import BytesIO |
| 1 | from io import BytesIO, BufferedReader |
2 | 2 | |
3 | 3 | from django.core.handlers.wsgi import WSGIRequest |
4 | 4 | from django.core.servers.basehttp import WSGIRequestHandler |
… |
… |
class WSGIRequestHandlerTestCase(SimpleTestCase):
|
19 | 19 | |
20 | 20 | def test_log_message(self): |
21 | 21 | request = WSGIRequest(self.request_factory.get('/').environ) |
22 | | request.makefile = lambda *args, **kwargs: BytesIO() |
| 22 | request.makefile = lambda *args, **kwargs: BufferedReader(BytesIO()) |
23 | 23 | handler = WSGIRequestHandler(request, '192.168.0.2', None) |
24 | 24 | level_status_codes = { |
25 | 25 | 'info': [200, 301, 304], |
… |
… |
class WSGIRequestHandlerTestCase(SimpleTestCase):
|
41 | 41 | |
42 | 42 | def test_https(self): |
43 | 43 | request = WSGIRequest(self.request_factory.get('/').environ) |
44 | | request.makefile = lambda *args, **kwargs: BytesIO() |
| 44 | request.makefile = lambda *args, **kwargs: BufferedReader(BytesIO()) |
45 | 45 | |
46 | 46 | handler = WSGIRequestHandler(request, '192.168.0.2', None) |
47 | 47 | |
… |
… |
class WSGIRequestHandlerTestCase(SimpleTestCase):
|
75 | 75 | rfile.write(b"Some_Header: bad\r\n") |
76 | 76 | rfile.write(b"Other_Header: bad\r\n") |
77 | 77 | rfile.seek(0) |
| 78 | rfile = BufferedReader(rfile) |
78 | 79 | |
79 | 80 | # WSGIRequestHandler closes the output file; we need to make this a |
80 | 81 | # no-op so we can still read its contents. |
diff --git a/tests/servers/tests.py b/tests/servers/tests.py
index 33d0605443..d239d34a49 100644
a
|
b
|
class LiveServerViews(LiveServerBase):
|
69 | 69 | |
70 | 70 | def test_closes_connection_without_content_length(self): |
71 | 71 | """ |
72 | | A HTTP 1.1 server is supposed to support keep-alive. Since our |
73 | | development server is rather simple we support it only in cases where |
74 | | we can detect a content length from the response. This should be doable |
75 | | for all simple views and streaming responses where an iterable with |
76 | | length of one is passed. The latter follows as result of `set_content_length` |
77 | | from https://github.com/python/cpython/blob/master/Lib/wsgiref/handlers.py. |
78 | | |
79 | | If we cannot detect a content length we explicitly set the `Connection` |
80 | | header to `close` to notify the client that we do not actually support |
81 | | it. |
| 72 | TODO - Updated description |
82 | 73 | """ |
83 | 74 | conn = HTTPConnection(LiveServerViews.server_thread.host, LiveServerViews.server_thread.port, timeout=1) |
84 | 75 | try: |
85 | 76 | conn.request('GET', '/streaming_example_view/', headers={'Connection': 'keep-alive'}) |
86 | 77 | response = conn.getresponse() |
87 | | self.assertTrue(response.will_close) |
| 78 | self.assertFalse(response.will_close) |
88 | 79 | self.assertEqual(response.read(), b'Iamastream') |
89 | 80 | self.assertEqual(response.status, 200) |
90 | | self.assertEqual(response.getheader('Connection'), 'close') |
| 81 | self.assertIsNone(response.getheader('Connection')) |
91 | 82 | |
92 | 83 | conn.request('GET', '/streaming_example_view/', headers={'Connection': 'close'}) |
93 | 84 | response = conn.getresponse() |
94 | | self.assertTrue(response.will_close) |
| 85 | self.assertFalse(response.will_close) |
95 | 86 | self.assertEqual(response.read(), b'Iamastream') |
96 | 87 | self.assertEqual(response.status, 200) |
97 | | self.assertEqual(response.getheader('Connection'), 'close') |
| 88 | self.assertIsNone(response.getheader('Connection')) |
98 | 89 | finally: |
99 | 90 | conn.close() |
100 | 91 | |