Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tornado/httpserver.py: 33%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

124 statements  

1# 

2# Copyright 2009 Facebook 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16"""A non-blocking, single-threaded HTTP server. 

17 

18Typical applications have little direct interaction with the `HTTPServer` 

19class except to start a server at the beginning of the process 

20(and even that is often done indirectly via `tornado.web.Application.listen`). 

21 

22.. versionchanged:: 4.0 

23 

24 The ``HTTPRequest`` class that used to live in this module has been moved 

25 to `tornado.httputil.HTTPServerRequest`. The old name remains as an alias. 

26""" 

27 

28import socket 

29import ssl 

30import typing 

31from collections.abc import Awaitable, Callable 

32from typing import Any 

33 

34from tornado import httputil, iostream, netutil 

35from tornado.escape import native_str 

36from tornado.http1connection import HTTP1ConnectionParameters, HTTP1ServerConnection 

37from tornado.tcpserver import TCPServer 

38from tornado.util import Configurable 

39 

40 

41class HTTPServer(TCPServer, Configurable, httputil.HTTPServerConnectionDelegate): 

42 r"""A non-blocking, single-threaded HTTP server. 

43 

44 A server is defined by a subclass of `.HTTPServerConnectionDelegate`, 

45 or, for backwards compatibility, a callback that takes an 

46 `.HTTPServerRequest` as an argument. The delegate is usually a 

47 `tornado.web.Application`. 

48 

49 `HTTPServer` supports keep-alive connections by default 

50 (automatically for HTTP/1.1, or for HTTP/1.0 when the client 

51 requests ``Connection: keep-alive``). 

52 

53 If ``xheaders`` is ``True``, we support the 

54 ``X-Real-Ip``/``X-Forwarded-For`` and 

55 ``X-Scheme``/``X-Forwarded-Proto`` headers, which override the 

56 remote IP and URI scheme/protocol for all requests. These headers 

57 are useful when running Tornado behind a reverse proxy or load 

58 balancer. The ``protocol`` argument can also be set to ``https`` 

59 if Tornado is run behind an SSL-decoding proxy that does not set one of 

60 the supported ``xheaders``. 

61 

62 By default, when parsing the ``X-Forwarded-For`` header, Tornado will 

63 select the last (i.e., the closest) address on the list of hosts as the 

64 remote host IP address. To select the next server in the chain, a list of 

65 trusted downstream hosts may be passed as the ``trusted_downstream`` 

66 argument. These hosts will be skipped when parsing the ``X-Forwarded-For`` 

67 header. 

68 

69 To make this server serve SSL traffic, send the ``ssl_options`` keyword 

70 argument with an `ssl.SSLContext` object. For compatibility with older 

71 versions of Python ``ssl_options`` may also be a dictionary of keyword 

72 arguments for the `ssl.SSLContext.wrap_socket` method.:: 

73 

74 ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 

75 ssl_ctx.load_cert_chain(os.path.join(data_dir, "mydomain.crt"), 

76 os.path.join(data_dir, "mydomain.key")) 

77 HTTPServer(application, ssl_options=ssl_ctx) 

78 

79 `HTTPServer` initialization follows one of three patterns (the 

80 initialization methods are defined on `tornado.tcpserver.TCPServer`): 

81 

82 1. `~tornado.tcpserver.TCPServer.listen`: single-process:: 

83 

84 async def main(): 

85 server = HTTPServer() 

86 server.listen(8888) 

87 await asyncio.Event().wait() 

88 

89 asyncio.run(main()) 

90 

91 In many cases, `tornado.web.Application.listen` can be used to avoid 

92 the need to explicitly create the `HTTPServer`. 

93 

94 While this example does not create multiple processes on its own, when 

95 the ``reuse_port=True`` argument is passed to ``listen()`` you can run 

96 the program multiple times to create a multi-process service. 

97 

98 2. `~tornado.tcpserver.TCPServer.add_sockets`: multi-process:: 

99 

100 sockets = bind_sockets(8888) 

101 tornado.process.fork_processes(0) 

102 async def post_fork_main(): 

103 server = HTTPServer() 

104 server.add_sockets(sockets) 

105 await asyncio.Event().wait() 

106 asyncio.run(post_fork_main()) 

107 

108 The ``add_sockets`` interface is more complicated, but it can be used with 

109 `tornado.process.fork_processes` to run a multi-process service with all 

110 worker processes forked from a single parent. ``add_sockets`` can also be 

111 used in single-process servers if you want to create your listening 

112 sockets in some way other than `~tornado.netutil.bind_sockets`. 

113 

114 Note that when using this pattern, nothing that touches the event loop 

115 can be run before ``fork_processes``. 

116 

117 3. `~tornado.tcpserver.TCPServer.bind`/`~tornado.tcpserver.TCPServer.start`: 

118 simple **deprecated** multi-process:: 

119 

120 server = HTTPServer() 

121 server.bind(8888) 

122 server.start(0) # Forks multiple sub-processes 

123 IOLoop.current().start() 

124 

125 This pattern is deprecated because it requires interfaces in the 

126 `asyncio` module that have been deprecated since Python 3.10. Support for 

127 creating multiple processes in the ``start`` method will be removed in a 

128 future version of Tornado. 

129 

130 .. versionchanged:: 4.0 

131 Added ``decompress_request``, ``chunk_size``, ``max_header_size``, 

132 ``idle_connection_timeout``, ``body_timeout``, ``max_body_size`` 

133 arguments. Added support for `.HTTPServerConnectionDelegate` 

134 instances as ``request_callback``. 

135 

136 .. versionchanged:: 4.1 

137 `.HTTPServerConnectionDelegate.start_request` is now called with 

138 two arguments ``(server_conn, request_conn)`` (in accordance with the 

139 documentation) instead of one ``(request_conn)``. 

140 

141 .. versionchanged:: 4.2 

142 `HTTPServer` is now a subclass of `tornado.util.Configurable`. 

143 

144 .. versionchanged:: 4.5 

145 Added the ``trusted_downstream`` argument. 

146 

147 .. versionchanged:: 5.0 

148 The ``io_loop`` argument has been removed. 

149 """ 

150 

151 def __init__(self, *args: Any, **kwargs: Any) -> None: 

152 # Ignore args to __init__; real initialization belongs in 

153 # initialize since we're Configurable. (there's something 

154 # weird in initialization order between this class, 

155 # Configurable, and TCPServer so we can't leave __init__ out 

156 # completely) 

157 pass 

158 

159 def initialize( 

160 self, 

161 request_callback: ( 

162 httputil.HTTPServerConnectionDelegate 

163 | Callable[[httputil.HTTPServerRequest], None] 

164 ), 

165 no_keep_alive: bool = False, 

166 xheaders: bool = False, 

167 ssl_options: dict[str, Any] | ssl.SSLContext | None = None, 

168 protocol: str | None = None, 

169 decompress_request: bool = False, 

170 chunk_size: int | None = None, 

171 max_header_size: int | None = None, 

172 idle_connection_timeout: float | None = None, 

173 body_timeout: float | None = None, 

174 max_body_size: int | None = None, 

175 max_buffer_size: int | None = None, 

176 trusted_downstream: list[str] | None = None, 

177 ) -> None: 

178 # This method's signature is not extracted with autodoc 

179 # because we want its arguments to appear on the class 

180 # constructor. When changing this signature, also update the 

181 # copy in httpserver.rst. 

182 self.request_callback = request_callback 

183 self.xheaders = xheaders 

184 self.protocol = protocol 

185 self.conn_params = HTTP1ConnectionParameters( 

186 decompress=decompress_request, 

187 chunk_size=chunk_size, 

188 max_header_size=max_header_size, 

189 header_timeout=idle_connection_timeout or 3600, 

190 max_body_size=max_body_size, 

191 body_timeout=body_timeout, 

192 no_keep_alive=no_keep_alive, 

193 ) 

194 TCPServer.__init__( 

195 self, 

196 ssl_options=ssl_options, 

197 max_buffer_size=max_buffer_size, 

198 read_chunk_size=chunk_size, 

199 ) 

200 self._connections: set[HTTP1ServerConnection] = set() 

201 self.trusted_downstream = trusted_downstream 

202 

203 @classmethod 

204 def configurable_base(cls) -> type[Configurable]: 

205 return HTTPServer 

206 

207 @classmethod 

208 def configurable_default(cls) -> type[Configurable]: 

209 return HTTPServer 

210 

211 async def close_all_connections(self) -> None: 

212 """Close all open connections and asynchronously wait for them to finish. 

213 

214 This method is used in combination with `~.TCPServer.stop` to 

215 support clean shutdowns (especially for unittests). Typical 

216 usage would call ``stop()`` first to stop accepting new 

217 connections, then ``await close_all_connections()`` to wait for 

218 existing connections to finish. 

219 

220 This method does not currently close open websocket connections. 

221 

222 Note that this method is a coroutine and must be called with ``await``. 

223 

224 """ 

225 while self._connections: 

226 # Peek at an arbitrary element of the set 

227 conn = next(iter(self._connections)) 

228 await conn.close() 

229 

230 def handle_stream(self, stream: iostream.IOStream, address: tuple) -> None: 

231 context = _HTTPRequestContext( 

232 stream, address, self.protocol, self.trusted_downstream 

233 ) 

234 conn = HTTP1ServerConnection(stream, self.conn_params, context) 

235 self._connections.add(conn) 

236 conn.start_serving(self) 

237 

238 def start_request( 

239 self, server_conn: object, request_conn: httputil.HTTPConnection 

240 ) -> httputil.HTTPMessageDelegate: 

241 if isinstance(self.request_callback, httputil.HTTPServerConnectionDelegate): 

242 delegate = self.request_callback.start_request(server_conn, request_conn) 

243 else: 

244 delegate = _CallableAdapter(self.request_callback, request_conn) 

245 

246 if self.xheaders: 

247 delegate = _ProxyAdapter(delegate, request_conn) 

248 

249 return delegate 

250 

251 def on_close(self, server_conn: object) -> None: 

252 self._connections.remove(typing.cast(HTTP1ServerConnection, server_conn)) 

253 

254 

255class _CallableAdapter(httputil.HTTPMessageDelegate): 

256 def __init__( 

257 self, 

258 request_callback: Callable[[httputil.HTTPServerRequest], None], 

259 request_conn: httputil.HTTPConnection, 

260 ) -> None: 

261 self.connection = request_conn 

262 self.request_callback = request_callback 

263 self.request: httputil.HTTPServerRequest | None = None 

264 self.delegate = None 

265 self._chunks = bytearray() 

266 

267 def headers_received( 

268 self, 

269 start_line: httputil.RequestStartLine | httputil.ResponseStartLine, 

270 headers: httputil.HTTPHeaders, 

271 ) -> Awaitable[None] | None: 

272 self.request = httputil.HTTPServerRequest( 

273 connection=self.connection, 

274 start_line=typing.cast(httputil.RequestStartLine, start_line), 

275 headers=headers, 

276 ) 

277 return None 

278 

279 def data_received(self, chunk: bytes) -> Awaitable[None] | None: 

280 self._chunks += chunk 

281 return None 

282 

283 def finish(self) -> None: 

284 assert self.request is not None 

285 self.request.body = bytes(self._chunks) 

286 self.request._parse_body() 

287 self.request_callback(self.request) 

288 

289 def on_connection_close(self) -> None: 

290 del self._chunks 

291 

292 

293class _HTTPRequestContext: 

294 def __init__( 

295 self, 

296 stream: iostream.IOStream, 

297 address: tuple, 

298 protocol: str | None, 

299 trusted_downstream: list[str] | None = None, 

300 ) -> None: 

301 self.address = address 

302 # Save the socket's address family now so we know how to 

303 # interpret self.address even after the stream is closed 

304 # and its socket attribute replaced with None. 

305 if stream.socket is not None: 

306 self.address_family = stream.socket.family 

307 else: 

308 self.address_family = None 

309 # In HTTPServerRequest we want an IP, not a full socket address. 

310 if ( 

311 self.address_family in (socket.AF_INET, socket.AF_INET6) 

312 and address is not None 

313 ): 

314 self.remote_ip = address[0] 

315 else: 

316 # Unix (or other) socket; fake the remote address. 

317 self.remote_ip = "0.0.0.0" 

318 if protocol: 

319 self.protocol = protocol 

320 elif isinstance(stream, iostream.SSLIOStream): 

321 self.protocol = "https" 

322 else: 

323 self.protocol = "http" 

324 self._orig_remote_ip = self.remote_ip 

325 self._orig_protocol = self.protocol 

326 self.trusted_downstream = set(trusted_downstream or []) 

327 

328 def __str__(self) -> str: 

329 if self.address_family in (socket.AF_INET, socket.AF_INET6): 

330 return self.remote_ip 

331 elif isinstance(self.address, bytes): 

332 # Python 3 with the -bb option warns about str(bytes), 

333 # so convert it explicitly. 

334 # Unix socket addresses are str on mac but bytes on linux. 

335 return native_str(self.address) 

336 else: 

337 return str(self.address) 

338 

339 def _apply_xheaders(self, headers: httputil.HTTPHeaders) -> None: 

340 """Rewrite the ``remote_ip`` and ``protocol`` fields.""" 

341 # Squid uses X-Forwarded-For, others use X-Real-Ip 

342 ip = headers.get("X-Forwarded-For", self.remote_ip) 

343 # Skip trusted downstream hosts in X-Forwarded-For list 

344 for ip in (cand.strip() for cand in reversed(ip.split(","))): 

345 if ip not in self.trusted_downstream: 

346 break 

347 ip = headers.get("X-Real-Ip", ip) 

348 if netutil.is_valid_ip(ip): 

349 self.remote_ip = ip 

350 # AWS uses X-Forwarded-Proto 

351 proto_header = headers.get( 

352 "X-Scheme", headers.get("X-Forwarded-Proto", self.protocol) 

353 ) 

354 if proto_header: 

355 # use only the last proto entry if there is more than one 

356 # TODO: support trusting multiple layers of proxied protocol 

357 proto_header = proto_header.split(",")[-1].strip() 

358 if proto_header in ("http", "https"): 

359 self.protocol = proto_header 

360 

361 def _unapply_xheaders(self) -> None: 

362 """Undo changes from `_apply_xheaders`. 

363 

364 Xheaders are per-request so they should not leak to the next 

365 request on the same connection. 

366 """ 

367 self.remote_ip = self._orig_remote_ip 

368 self.protocol = self._orig_protocol 

369 

370 

371class _ProxyAdapter(httputil.HTTPMessageDelegate): 

372 def __init__( 

373 self, 

374 delegate: httputil.HTTPMessageDelegate, 

375 request_conn: httputil.HTTPConnection, 

376 ) -> None: 

377 self.connection = request_conn 

378 self.delegate = delegate 

379 

380 def headers_received( 

381 self, 

382 start_line: httputil.RequestStartLine | httputil.ResponseStartLine, 

383 headers: httputil.HTTPHeaders, 

384 ) -> Awaitable[None] | None: 

385 # TODO: either make context an official part of the 

386 # HTTPConnection interface or figure out some other way to do this. 

387 self.connection.context._apply_xheaders(headers) # type: ignore 

388 return self.delegate.headers_received(start_line, headers) 

389 

390 def data_received(self, chunk: bytes) -> Awaitable[None] | None: 

391 return self.delegate.data_received(chunk) 

392 

393 def finish(self) -> None: 

394 self.delegate.finish() 

395 self._cleanup() 

396 

397 def on_connection_close(self) -> None: 

398 self.delegate.on_connection_close() 

399 self._cleanup() 

400 

401 def _cleanup(self) -> None: 

402 self.connection.context._unapply_xheaders() # type: ignore 

403 

404 

405HTTPRequest = httputil.HTTPServerRequest