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

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

85 statements  

1# 

2# Copyright 2011 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 TCP server.""" 

17 

18import errno 

19import os 

20import socket 

21import ssl 

22from collections.abc import Awaitable, Callable, Iterable 

23from typing import Any 

24 

25from tornado import gen, process 

26from tornado.ioloop import IOLoop 

27from tornado.iostream import IOStream, SSLIOStream 

28from tornado.log import app_log 

29from tornado.netutil import ( 

30 _DEFAULT_BACKLOG, 

31 add_accept_handler, 

32 bind_sockets, 

33 ssl_wrap_socket, 

34) 

35from tornado.util import errno_from_exception 

36 

37 

38class TCPServer: 

39 r"""A non-blocking, single-threaded TCP server. 

40 

41 To use `TCPServer`, define a subclass which overrides the `handle_stream` 

42 method. For example, a simple echo server could be defined like this:: 

43 

44 from tornado.tcpserver import TCPServer 

45 from tornado.iostream import StreamClosedError 

46 

47 class EchoServer(TCPServer): 

48 async def handle_stream(self, stream, address): 

49 while True: 

50 try: 

51 data = await stream.read_until(b"\n") await 

52 stream.write(data) 

53 except StreamClosedError: 

54 break 

55 

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

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

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

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

60 

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

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

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

64 TCPServer(ssl_options=ssl_ctx) 

65 

66 `TCPServer` initialization follows one of three patterns: 

67 

68 1. `listen`: single-process:: 

69 

70 async def main(): 

71 server = TCPServer() 

72 server.listen(8888) 

73 await asyncio.Event().wait() 

74 

75 asyncio.run(main()) 

76 

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

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

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

80 

81 2. `add_sockets`: multi-process:: 

82 

83 sockets = bind_sockets(8888) 

84 tornado.process.fork_processes(0) 

85 async def post_fork_main(): 

86 server = TCPServer() 

87 server.add_sockets(sockets) 

88 await asyncio.Event().wait() 

89 asyncio.run(post_fork_main()) 

90 

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

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

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

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

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

96 

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

98 can be run before ``fork_processes``. 

99 

100 3. `bind`/`start`: simple **deprecated** multi-process:: 

101 

102 server = TCPServer() 

103 server.bind(8888) 

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

105 IOLoop.current().start() 

106 

107 This pattern is deprecated because it requires interfaces in the 

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

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

110 future version of Tornado. 

111 

112 .. versionadded:: 3.1 

113 The ``max_buffer_size`` argument. 

114 

115 .. versionchanged:: 5.0 

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

117 """ 

118 

119 def __init__( 

120 self, 

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

122 max_buffer_size: int | None = None, 

123 read_chunk_size: int | None = None, 

124 ) -> None: 

125 self.ssl_options = ssl_options 

126 self._sockets: dict[int, socket.socket] = {} 

127 self._handlers: dict[int, Callable[[], None]] = {} 

128 self._pending_sockets: list[socket.socket] = [] 

129 self._started = False 

130 self._stopped = False 

131 self.max_buffer_size = max_buffer_size 

132 self.read_chunk_size = read_chunk_size 

133 

134 # Verify the SSL options. Otherwise we don't get errors until clients 

135 # connect. This doesn't verify that the keys are legitimate, but 

136 # the SSL module doesn't do that until there is a connected socket 

137 # which seems like too much work 

138 if self.ssl_options is not None and isinstance(self.ssl_options, dict): 

139 # Only certfile is required: it can contain both keys 

140 if "certfile" not in self.ssl_options: 

141 raise KeyError('missing key "certfile" in ssl_options') 

142 

143 if not os.path.exists(self.ssl_options["certfile"]): 

144 raise ValueError( 

145 'certfile "%s" does not exist' % self.ssl_options["certfile"] 

146 ) 

147 if "keyfile" in self.ssl_options and not os.path.exists( 

148 self.ssl_options["keyfile"] 

149 ): 

150 raise ValueError( 

151 'keyfile "%s" does not exist' % self.ssl_options["keyfile"] 

152 ) 

153 

154 def listen( 

155 self, 

156 port: int, 

157 address: str | None = None, 

158 family: socket.AddressFamily = socket.AF_UNSPEC, 

159 backlog: int = _DEFAULT_BACKLOG, 

160 flags: int | None = None, 

161 reuse_port: bool = False, 

162 ) -> None: 

163 """Starts accepting connections on the given port. 

164 

165 This method may be called more than once to listen on multiple ports. 

166 `listen` takes effect immediately; it is not necessary to call 

167 `TCPServer.start` afterwards. It is, however, necessary to start the 

168 event loop if it is not already running. 

169 

170 All arguments have the same meaning as in 

171 `tornado.netutil.bind_sockets`. 

172 

173 .. versionchanged:: 6.2 

174 

175 Added ``family``, ``backlog``, ``flags``, and ``reuse_port`` 

176 arguments to match `tornado.netutil.bind_sockets`. 

177 """ 

178 sockets = bind_sockets( 

179 port, 

180 address=address, 

181 family=family, 

182 backlog=backlog, 

183 flags=flags, 

184 reuse_port=reuse_port, 

185 ) 

186 self.add_sockets(sockets) 

187 

188 def add_sockets(self, sockets: Iterable[socket.socket]) -> None: 

189 """Makes this server start accepting connections on the given sockets. 

190 

191 The ``sockets`` parameter is a list of socket objects such as 

192 those returned by `~tornado.netutil.bind_sockets`. 

193 `add_sockets` is typically used in combination with that 

194 method and `tornado.process.fork_processes` to provide greater 

195 control over the initialization of a multi-process server. 

196 """ 

197 for sock in sockets: 

198 self._sockets[sock.fileno()] = sock 

199 self._handlers[sock.fileno()] = add_accept_handler( 

200 sock, self._handle_connection 

201 ) 

202 

203 def add_socket(self, socket: socket.socket) -> None: 

204 """Singular version of `add_sockets`. Takes a single socket object.""" 

205 self.add_sockets([socket]) 

206 

207 def bind( 

208 self, 

209 port: int, 

210 address: str | None = None, 

211 family: socket.AddressFamily = socket.AF_UNSPEC, 

212 backlog: int = _DEFAULT_BACKLOG, 

213 flags: int | None = None, 

214 reuse_port: bool = False, 

215 ) -> None: 

216 """Binds this server to the given port on the given address. 

217 

218 To start the server, call `start`. If you want to run this server in a 

219 single process, you can call `listen` as a shortcut to the sequence of 

220 `bind` and `start` calls. 

221 

222 Address may be either an IP address or hostname. If it's a hostname, 

223 the server will listen on all IP addresses associated with the name. 

224 Address may be an empty string or None to listen on all available 

225 interfaces. Family may be set to either `socket.AF_INET` or 

226 `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise both 

227 will be used if available. 

228 

229 The ``backlog`` argument has the same meaning as for `socket.listen 

230 <socket.socket.listen>`. The ``reuse_port`` argument has the same 

231 meaning as for `.bind_sockets`. 

232 

233 This method may be called multiple times prior to `start` to listen on 

234 multiple ports or interfaces. 

235 

236 .. versionchanged:: 4.4 

237 Added the ``reuse_port`` argument. 

238 

239 .. versionchanged:: 6.2 

240 Added the ``flags`` argument to match `.bind_sockets`. 

241 

242 .. deprecated:: 6.2 

243 Use either ``listen()`` or ``add_sockets()`` instead of ``bind()`` 

244 and ``start()``. 

245 """ 

246 sockets = bind_sockets( 

247 port, 

248 address=address, 

249 family=family, 

250 backlog=backlog, 

251 flags=flags, 

252 reuse_port=reuse_port, 

253 ) 

254 if self._started: 

255 self.add_sockets(sockets) 

256 else: 

257 self._pending_sockets.extend(sockets) 

258 

259 def start( 

260 self, num_processes: int | None = 1, max_restarts: int | None = None 

261 ) -> None: 

262 """Starts this server in the `.IOLoop`. 

263 

264 By default, we run the server in this process and do not fork any 

265 additional child process. 

266 

267 If num_processes is ``None`` or <= 0, we detect the number of cores 

268 available on this machine and fork that number of child 

269 processes. If num_processes is given and > 1, we fork that 

270 specific number of sub-processes. 

271 

272 Since we use processes and not threads, there is no shared memory 

273 between any server code. 

274 

275 Note that multiple processes are not compatible with the autoreload 

276 module (or the ``autoreload=True`` option to `tornado.web.Application` 

277 which defaults to True when ``debug=True``). 

278 When using multiple processes, no IOLoops can be created or 

279 referenced until after the call to ``TCPServer.start(n)``. 

280 

281 Values of ``num_processes`` other than 1 are not supported on Windows. 

282 

283 The ``max_restarts`` argument is passed to `.fork_processes`. 

284 

285 .. versionchanged:: 6.0 

286 

287 Added ``max_restarts`` argument. 

288 

289 .. deprecated:: 6.2 

290 Use either ``listen()`` or ``add_sockets()`` instead of ``bind()`` 

291 and ``start()``. 

292 """ 

293 assert not self._started 

294 self._started = True 

295 if num_processes != 1: 

296 process.fork_processes(num_processes, max_restarts) 

297 sockets = self._pending_sockets 

298 self._pending_sockets = [] 

299 self.add_sockets(sockets) 

300 

301 def stop(self) -> None: 

302 """Stops listening for new connections. 

303 

304 Requests currently in progress may still continue after the 

305 server is stopped. 

306 """ 

307 if self._stopped: 

308 return 

309 self._stopped = True 

310 for fd, sock in self._sockets.items(): 

311 assert sock.fileno() == fd 

312 # Unregister socket from IOLoop 

313 self._handlers.pop(fd)() 

314 sock.close() 

315 

316 def handle_stream(self, stream: IOStream, address: tuple) -> Awaitable[None] | None: 

317 """Override to handle a new `.IOStream` from an incoming connection. 

318 

319 This method may be a coroutine; if so any exceptions it raises 

320 asynchronously will be logged. Accepting of incoming connections 

321 will not be blocked by this coroutine. 

322 

323 If this `TCPServer` is configured for SSL, ``handle_stream`` 

324 may be called before the SSL handshake has completed. Use 

325 `.SSLIOStream.wait_for_handshake` if you need to verify the client's 

326 certificate or use NPN/ALPN. 

327 

328 .. versionchanged:: 4.2 

329 Added the option for this method to be a coroutine. 

330 """ 

331 raise NotImplementedError() 

332 

333 def _handle_connection(self, connection: socket.socket, address: Any) -> None: 

334 if self.ssl_options is not None: 

335 assert ssl, "OpenSSL required for SSL" 

336 try: 

337 connection = ssl_wrap_socket( 

338 connection, 

339 self.ssl_options, 

340 server_side=True, 

341 do_handshake_on_connect=False, 

342 ) 

343 except ssl.SSLError as err: 

344 if err.args[0] == ssl.SSL_ERROR_EOF: 

345 return connection.close() 

346 else: 

347 raise 

348 except OSError as err: 

349 # If the connection is closed immediately after it is created 

350 # (as in a port scan), we can get one of several errors. 

351 # wrap_socket makes an internal call to getpeername, 

352 # which may return either EINVAL (Mac OS X) or ENOTCONN 

353 # (Linux). If it returns ENOTCONN, this error is 

354 # silently swallowed by the ssl module, so we need to 

355 # catch another error later on (AttributeError in 

356 # SSLIOStream._do_ssl_handshake). 

357 # To test this behavior, try nmap with the -sT flag. 

358 # https://github.com/tornadoweb/tornado/pull/750 

359 if errno_from_exception(err) in (errno.ECONNABORTED, errno.EINVAL): 

360 return connection.close() 

361 else: 

362 raise 

363 try: 

364 if self.ssl_options is not None: 

365 stream: IOStream = SSLIOStream( 

366 connection, 

367 max_buffer_size=self.max_buffer_size, 

368 read_chunk_size=self.read_chunk_size, 

369 ) 

370 else: 

371 stream = IOStream( 

372 connection, 

373 max_buffer_size=self.max_buffer_size, 

374 read_chunk_size=self.read_chunk_size, 

375 ) 

376 

377 future = self.handle_stream(stream, address) 

378 if future is not None: 

379 IOLoop.current().add_future( 

380 gen.convert_yielded(future), lambda f: f.result() 

381 ) 

382 except Exception: 

383 app_log.error("Error in connection callback", exc_info=True)