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
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
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.
16"""A non-blocking, single-threaded TCP server."""
18import errno
19import os
20import socket
21import ssl
22from collections.abc import Awaitable, Callable, Iterable
23from typing import Any
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
38class TCPServer:
39 r"""A non-blocking, single-threaded TCP server.
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::
44 from tornado.tcpserver import TCPServer
45 from tornado.iostream import StreamClosedError
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
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.::
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)
66 `TCPServer` initialization follows one of three patterns:
68 1. `listen`: single-process::
70 async def main():
71 server = TCPServer()
72 server.listen(8888)
73 await asyncio.Event().wait()
75 asyncio.run(main())
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.
81 2. `add_sockets`: multi-process::
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())
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`.
97 Note that when using this pattern, nothing that touches the event loop
98 can be run before ``fork_processes``.
100 3. `bind`/`start`: simple **deprecated** multi-process::
102 server = TCPServer()
103 server.bind(8888)
104 server.start(0) # Forks multiple sub-processes
105 IOLoop.current().start()
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.
112 .. versionadded:: 3.1
113 The ``max_buffer_size`` argument.
115 .. versionchanged:: 5.0
116 The ``io_loop`` argument has been removed.
117 """
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
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')
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 )
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.
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.
170 All arguments have the same meaning as in
171 `tornado.netutil.bind_sockets`.
173 .. versionchanged:: 6.2
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)
188 def add_sockets(self, sockets: Iterable[socket.socket]) -> None:
189 """Makes this server start accepting connections on the given sockets.
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 )
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])
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.
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.
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.
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`.
233 This method may be called multiple times prior to `start` to listen on
234 multiple ports or interfaces.
236 .. versionchanged:: 4.4
237 Added the ``reuse_port`` argument.
239 .. versionchanged:: 6.2
240 Added the ``flags`` argument to match `.bind_sockets`.
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)
259 def start(
260 self, num_processes: int | None = 1, max_restarts: int | None = None
261 ) -> None:
262 """Starts this server in the `.IOLoop`.
264 By default, we run the server in this process and do not fork any
265 additional child process.
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.
272 Since we use processes and not threads, there is no shared memory
273 between any server code.
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)``.
281 Values of ``num_processes`` other than 1 are not supported on Windows.
283 The ``max_restarts`` argument is passed to `.fork_processes`.
285 .. versionchanged:: 6.0
287 Added ``max_restarts`` argument.
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)
301 def stop(self) -> None:
302 """Stops listening for new connections.
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()
316 def handle_stream(self, stream: IOStream, address: tuple) -> Awaitable[None] | None:
317 """Override to handle a new `.IOStream` from an incoming connection.
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.
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.
328 .. versionchanged:: 4.2
329 Added the option for this method to be a coroutine.
330 """
331 raise NotImplementedError()
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 )
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)