Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tornado/netutil.py: 25%
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"""Miscellaneous network utility code."""
18import asyncio
19import concurrent.futures
20import errno
21import functools
22import os
23import socket
24import ssl
25import stat
26import sys
27from collections.abc import Awaitable, Callable
28from typing import Any
30from tornado.concurrent import dummy_executor, run_on_executor
31from tornado.ioloop import IOLoop
32from tornado.util import Configurable, errno_from_exception
35# Note that the naming of ssl.Purpose is confusing; the purpose
36# of a context is to authenticate the opposite side of the connection.
37#
38# These are created lazily (rather than at import time) because
39# ssl.create_default_context() loads the platform's default certificate
40# store, which on some platforms (e.g. macOS) may start background
41# threads. Creating these contexts eagerly at import time could leave
42# such threads running at a call to os.fork() (e.g. in
43# tornado.process.fork_processes), which is unsafe.
44@functools.lru_cache(maxsize=1)
45def _client_ssl_defaults() -> ssl.SSLContext:
46 ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
47 if hasattr(ssl, "OP_NO_COMPRESSION"):
48 # See netutil.ssl_options_to_context
49 ctx.options |= ssl.OP_NO_COMPRESSION
50 return ctx
53@functools.lru_cache(maxsize=1)
54def _server_ssl_defaults() -> ssl.SSLContext:
55 ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
56 if hasattr(ssl, "OP_NO_COMPRESSION"):
57 ctx.options |= ssl.OP_NO_COMPRESSION
58 return ctx
61# ThreadedResolver runs getaddrinfo on a thread. If the hostname is unicode,
62# getaddrinfo attempts to import encodings.idna. If this is done at
63# module-import time, the import lock is already held by the main thread,
64# leading to deadlock. Avoid it by caching the idna encoder on the main
65# thread now.
66"foo".encode("idna")
68# For undiagnosed reasons, 'latin1' codec may also need to be preloaded.
69"foo".encode("latin1")
71# Default backlog used when calling sock.listen()
72_DEFAULT_BACKLOG = 128
75def bind_sockets(
76 port: int,
77 address: str | None = None,
78 family: socket.AddressFamily = socket.AF_UNSPEC,
79 backlog: int = _DEFAULT_BACKLOG,
80 flags: int | None = None,
81 reuse_port: bool = False,
82) -> list[socket.socket]:
83 """Creates listening sockets bound to the given port and address.
85 Returns a list of socket objects (multiple sockets are returned if
86 the given address maps to multiple IP addresses, which is most common
87 for mixed IPv4 and IPv6 use).
89 Address may be either an IP address or hostname. If it's a hostname,
90 the server will listen on all IP addresses associated with the
91 name. Address may be an empty string or None to listen on all
92 available interfaces. Family may be set to either `socket.AF_INET`
93 or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise
94 both will be used if available.
96 The ``backlog`` argument has the same meaning as for
97 `socket.listen() <socket.socket.listen>`.
99 ``flags`` is a bitmask of AI_* flags to `~socket.getaddrinfo`, like
100 ``socket.AI_PASSIVE | socket.AI_NUMERICHOST``.
102 ``reuse_port`` option sets ``SO_REUSEPORT`` option for every socket
103 in the list. If your platform doesn't support this option ValueError will
104 be raised.
105 """
106 if reuse_port and not hasattr(socket, "SO_REUSEPORT"):
107 raise ValueError("the platform doesn't support SO_REUSEPORT")
109 sockets = []
110 if address == "":
111 address = None
112 if not socket.has_ipv6 and family == socket.AF_UNSPEC:
113 # Python can be compiled with --disable-ipv6, which causes
114 # operations on AF_INET6 sockets to fail, but does not
115 # automatically exclude those results from getaddrinfo
116 # results.
117 # http://bugs.python.org/issue16208
118 family = socket.AF_INET
119 if flags is None:
120 flags = socket.AI_PASSIVE
121 bound_port = None
122 unique_addresses: set = set()
123 for res in sorted(
124 socket.getaddrinfo(address, port, family, socket.SOCK_STREAM, 0, flags),
125 key=lambda x: x[0],
126 ):
127 if res in unique_addresses:
128 continue
130 unique_addresses.add(res)
132 af, socktype, proto, canonname, sockaddr = res
133 if (
134 sys.platform == "darwin"
135 and address == "localhost"
136 and af == socket.AF_INET6
137 and sockaddr[3] != 0 # type: ignore
138 ):
139 # Mac OS X includes a link-local address fe80::1%lo0 in the
140 # getaddrinfo results for 'localhost'. However, the firewall
141 # doesn't understand that this is a local address and will
142 # prompt for access (often repeatedly, due to an apparent
143 # bug in its ability to remember granting access to an
144 # application). Skip these addresses.
145 continue
146 try:
147 sock = socket.socket(af, socktype, proto)
148 except OSError as e:
149 if errno_from_exception(e) == errno.EAFNOSUPPORT:
150 continue
151 raise
152 if os.name != "nt":
153 try:
154 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
155 except OSError as e:
156 if errno_from_exception(e) != errno.ENOPROTOOPT:
157 # Hurd doesn't support SO_REUSEADDR.
158 raise
159 if reuse_port:
160 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
161 if af == socket.AF_INET6:
162 # On linux, ipv6 sockets accept ipv4 too by default,
163 # but this makes it impossible to bind to both
164 # 0.0.0.0 in ipv4 and :: in ipv6. On other systems,
165 # separate sockets *must* be used to listen for both ipv4
166 # and ipv6. For consistency, always disable ipv4 on our
167 # ipv6 sockets and use a separate ipv4 socket when needed.
168 #
169 # Python 2.x on windows doesn't have IPPROTO_IPV6.
170 if hasattr(socket, "IPPROTO_IPV6"):
171 sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
173 # automatic port allocation with port=None
174 # should bind on the same port on IPv4 and IPv6
175 host, requested_port = sockaddr[:2]
176 if requested_port == 0 and bound_port is not None:
177 sockaddr = tuple([host, bound_port] + list(sockaddr[2:]))
179 sock.setblocking(False)
180 try:
181 sock.bind(sockaddr)
182 except OSError as e:
183 if (
184 errno_from_exception(e) == errno.EADDRNOTAVAIL
185 and address == "localhost"
186 and sockaddr[0] == "::1"
187 ):
188 # On some systems (most notably docker with default
189 # configurations), ipv6 is partially disabled:
190 # socket.has_ipv6 is true, we can create AF_INET6
191 # sockets, and getaddrinfo("localhost", ...,
192 # AF_PASSIVE) resolves to ::1, but we get an error
193 # when binding.
194 #
195 # Swallow the error, but only for this specific case.
196 # If EADDRNOTAVAIL occurs in other situations, it
197 # might be a real problem like a typo in a
198 # configuration.
199 sock.close()
200 continue
201 else:
202 raise
203 bound_port = sock.getsockname()[1]
204 sock.listen(backlog)
205 sockets.append(sock)
206 return sockets
209if hasattr(socket, "AF_UNIX"):
211 def bind_unix_socket(
212 file: str, mode: int = 0o600, backlog: int = _DEFAULT_BACKLOG
213 ) -> socket.socket:
214 """Creates a listening unix socket.
216 If a socket with the given name already exists, it will be deleted.
217 If any other file with that name exists, an exception will be
218 raised.
220 Returns a socket object (not a list of socket objects like
221 `bind_sockets`)
222 """
223 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
224 try:
225 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
226 except OSError as e:
227 if errno_from_exception(e) != errno.ENOPROTOOPT:
228 # Hurd doesn't support SO_REUSEADDR
229 raise
230 sock.setblocking(False)
231 # File names comprising of an initial null-byte denote an abstract
232 # namespace, on Linux, and therefore are not subject to file system
233 # orientated processing.
234 if not file.startswith("\0"):
235 try:
236 st = os.stat(file)
237 except FileNotFoundError:
238 pass
239 else:
240 if stat.S_ISSOCK(st.st_mode):
241 os.remove(file)
242 else:
243 raise ValueError("File %s exists and is not a socket" % file)
244 sock.bind(file)
245 os.chmod(file, mode)
246 else:
247 sock.bind(file)
248 sock.listen(backlog)
249 return sock
252def add_accept_handler(
253 sock: socket.socket, callback: Callable[[socket.socket, Any], None]
254) -> Callable[[], None]:
255 """Adds an `.IOLoop` event handler to accept new connections on ``sock``.
257 When a connection is accepted, ``callback(connection, address)`` will
258 be run (``connection`` is a socket object, and ``address`` is the
259 address of the other end of the connection). Note that this signature
260 is different from the ``callback(fd, events)`` signature used for
261 `.IOLoop` handlers.
263 A callable is returned which, when called, will remove the `.IOLoop`
264 event handler and stop processing further incoming connections.
266 .. versionchanged:: 5.0
267 The ``io_loop`` argument (deprecated since version 4.1) has been removed.
269 .. versionchanged:: 5.0
270 A callable is returned (``None`` was returned before).
271 """
272 io_loop = IOLoop.current()
273 removed = [False]
275 def accept_handler(fd: socket.socket, events: int) -> None:
276 # More connections may come in while we're handling callbacks;
277 # to prevent starvation of other tasks we must limit the number
278 # of connections we accept at a time. Ideally we would accept
279 # up to the number of connections that were waiting when we
280 # entered this method, but this information is not available
281 # (and rearranging this method to call accept() as many times
282 # as possible before running any callbacks would have adverse
283 # effects on load balancing in multiprocess configurations).
284 # Instead, we use the (default) listen backlog as a rough
285 # heuristic for the number of connections we can reasonably
286 # accept at once.
287 for i in range(_DEFAULT_BACKLOG):
288 if removed[0]:
289 # The socket was probably closed
290 return
291 try:
292 connection, address = sock.accept()
293 except BlockingIOError:
294 # EWOULDBLOCK indicates we have accepted every
295 # connection that is available.
296 return
297 except ConnectionAbortedError:
298 # ECONNABORTED indicates that there was a connection
299 # but it was closed while still in the accept queue.
300 # (observed on FreeBSD).
301 continue
302 callback(connection, address)
304 def remove_handler() -> None:
305 io_loop.remove_handler(sock)
306 removed[0] = True
308 io_loop.add_handler(sock, accept_handler, IOLoop.READ)
309 return remove_handler
312def is_valid_ip(ip: str) -> bool:
313 """Returns ``True`` if the given string is a well-formed IP address.
315 Supports IPv4 and IPv6.
316 """
317 if not ip or "\x00" in ip:
318 # getaddrinfo resolves empty strings to localhost, and truncates
319 # on zero bytes.
320 return False
321 try:
322 res = socket.getaddrinfo(
323 ip, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_NUMERICHOST
324 )
325 return bool(res)
326 except socket.gaierror as e:
327 if e.args[0] == socket.EAI_NONAME:
328 return False
329 raise
330 except UnicodeError:
331 # `socket.getaddrinfo` will raise a UnicodeError from the
332 # `idna` decoder if the input is longer than 63 characters,
333 # even for socket.AI_NUMERICHOST. See
334 # https://bugs.python.org/issue32958 for discussion
335 return False
336 return True
339class Resolver(Configurable):
340 """Configurable asynchronous DNS resolver interface.
342 By default, a blocking implementation is used (which simply calls
343 `socket.getaddrinfo`). An alternative implementation can be
344 chosen with the `Resolver.configure <.Configurable.configure>`
345 class method::
347 Resolver.configure('tornado.netutil.ThreadedResolver')
349 The implementations of this interface included with Tornado are
351 * `tornado.netutil.DefaultLoopResolver`
352 * `tornado.netutil.DefaultExecutorResolver` (deprecated)
353 * `tornado.netutil.BlockingResolver` (deprecated)
354 * `tornado.netutil.ThreadedResolver` (deprecated)
355 * `tornado.netutil.OverrideResolver`
356 * `tornado.platform.caresresolver.CaresResolver` (deprecated)
358 .. versionchanged:: 5.0
359 The default implementation has changed from `BlockingResolver` to
360 `DefaultExecutorResolver`.
362 .. versionchanged:: 6.2
363 The default implementation has changed from `DefaultExecutorResolver` to
364 `DefaultLoopResolver`.
365 """
367 @classmethod
368 def configurable_base(cls) -> type["Resolver"]:
369 return Resolver
371 @classmethod
372 def configurable_default(cls) -> type["Resolver"]:
373 return DefaultLoopResolver
375 def resolve(
376 self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC
377 ) -> Awaitable[list[tuple[int, Any]]]:
378 """Resolves an address.
380 The ``host`` argument is a string which may be a hostname or a
381 literal IP address.
383 Returns a `.Future` whose result is a list of (family,
384 address) pairs, where address is a tuple suitable to pass to
385 `socket.connect <socket.socket.connect>` (i.e. a ``(host,
386 port)`` pair for IPv4; additional fields may be present for
387 IPv6). If a ``callback`` is passed, it will be run with the
388 result as an argument when it is complete.
390 :raises IOError: if the address cannot be resolved.
392 .. versionchanged:: 4.4
393 Standardized all implementations to raise `IOError`.
395 .. versionchanged:: 6.0 The ``callback`` argument was removed.
396 Use the returned awaitable object instead.
398 """
399 raise NotImplementedError()
401 def close(self) -> None:
402 """Closes the `Resolver`, freeing any resources used.
404 .. versionadded:: 3.1
406 """
407 pass
410def _resolve_addr(
411 host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC
412) -> list[tuple[int, Any]]:
413 # On Solaris, getaddrinfo fails if the given port is not found
414 # in /etc/services and no socket type is given, so we must pass
415 # one here. The socket type used here doesn't seem to actually
416 # matter (we discard the one we get back in the results),
417 # so the addresses we return should still be usable with SOCK_DGRAM.
418 addrinfo = socket.getaddrinfo(host, port, family, socket.SOCK_STREAM)
419 results = []
420 for fam, socktype, proto, canonname, address in addrinfo:
421 results.append((fam, address))
422 return results # type: ignore
425class DefaultExecutorResolver(Resolver):
426 """Resolver implementation using `.IOLoop.run_in_executor`.
428 .. versionadded:: 5.0
430 .. deprecated:: 6.2
432 Use `DefaultLoopResolver` instead.
433 """
435 async def resolve(
436 self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC
437 ) -> list[tuple[int, Any]]:
438 result = await IOLoop.current().run_in_executor(
439 None, _resolve_addr, host, port, family
440 )
441 return result
444class DefaultLoopResolver(Resolver):
445 """Resolver implementation using `asyncio.loop.getaddrinfo`."""
447 async def resolve(
448 self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC
449 ) -> list[tuple[int, Any]]:
450 # On Solaris, getaddrinfo fails if the given port is not found
451 # in /etc/services and no socket type is given, so we must pass
452 # one here. The socket type used here doesn't seem to actually
453 # matter (we discard the one we get back in the results),
454 # so the addresses we return should still be usable with SOCK_DGRAM.
455 return [
456 (fam, address)
457 for fam, _, _, _, address in await asyncio.get_running_loop().getaddrinfo(
458 host, port, family=family, type=socket.SOCK_STREAM
459 )
460 ]
463class ExecutorResolver(Resolver):
464 """Resolver implementation using a `concurrent.futures.Executor`.
466 Use this instead of `ThreadedResolver` when you require additional
467 control over the executor being used.
469 The executor will be shut down when the resolver is closed unless
470 ``close_resolver=False``; use this if you want to reuse the same
471 executor elsewhere.
473 .. versionchanged:: 5.0
474 The ``io_loop`` argument (deprecated since version 4.1) has been removed.
476 .. deprecated:: 5.0
477 The default `Resolver` now uses `asyncio.loop.getaddrinfo`;
478 use that instead of this class.
479 """
481 def initialize(
482 self,
483 executor: concurrent.futures.Executor | None = None,
484 close_executor: bool = True,
485 ) -> None:
486 if executor is not None:
487 self.executor = executor
488 self.close_executor = close_executor
489 else:
490 self.executor = dummy_executor
491 self.close_executor = False
493 def close(self) -> None:
494 if self.close_executor:
495 self.executor.shutdown()
496 self.executor = None # type: ignore
498 @run_on_executor
499 def resolve(
500 self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC
501 ) -> list[tuple[int, Any]]:
502 return _resolve_addr(host, port, family)
505class BlockingResolver(ExecutorResolver):
506 """Default `Resolver` implementation, using `socket.getaddrinfo`.
508 The `.IOLoop` will be blocked during the resolution, although the
509 callback will not be run until the next `.IOLoop` iteration.
511 .. deprecated:: 5.0
512 The default `Resolver` now uses `.IOLoop.run_in_executor`; use that instead
513 of this class.
514 """
516 def initialize(self) -> None: # type: ignore
517 super().initialize()
520class ThreadedResolver(ExecutorResolver):
521 """Multithreaded non-blocking `Resolver` implementation.
523 The thread pool size can be configured with::
525 Resolver.configure('tornado.netutil.ThreadedResolver',
526 num_threads=10)
528 .. versionchanged:: 3.1
529 All ``ThreadedResolvers`` share a single thread pool, whose
530 size is set by the first one to be created.
532 .. deprecated:: 5.0
533 The default `Resolver` now uses `.IOLoop.run_in_executor`; use that instead
534 of this class.
535 """
537 _threadpool: concurrent.futures.ThreadPoolExecutor | None = None
538 _threadpool_pid: int | None = None
540 def initialize(self, num_threads: int = 10) -> None: # type: ignore
541 threadpool = ThreadedResolver._create_threadpool(num_threads)
542 super().initialize(executor=threadpool, close_executor=False)
544 @classmethod
545 def _create_threadpool(
546 cls, num_threads: int
547 ) -> concurrent.futures.ThreadPoolExecutor:
548 pid = os.getpid()
549 if cls._threadpool_pid != pid:
550 # Threads cannot survive after a fork, so if our pid isn't what it
551 # was when we created the pool then delete it.
552 cls._threadpool = None
553 if cls._threadpool is None:
554 cls._threadpool = concurrent.futures.ThreadPoolExecutor(num_threads)
555 cls._threadpool_pid = pid
556 return cls._threadpool
559class OverrideResolver(Resolver):
560 """Wraps a resolver with a mapping of overrides.
562 This can be used to make local DNS changes (e.g. for testing)
563 without modifying system-wide settings.
565 The mapping can be in three formats::
567 {
568 # Hostname to host or ip
569 "example.com": "127.0.1.1",
571 # Host+port to host+port
572 ("login.example.com", 443): ("localhost", 1443),
574 # Host+port+address family to host+port
575 ("login.example.com", 443, socket.AF_INET6): ("::1", 1443),
576 }
578 .. versionchanged:: 5.0
579 Added support for host-port-family triplets.
580 """
582 def initialize(self, resolver: Resolver, mapping: dict) -> None:
583 self.resolver = resolver
584 self.mapping = mapping
586 def close(self) -> None:
587 self.resolver.close()
589 def resolve(
590 self, host: str, port: int, family: socket.AddressFamily = socket.AF_UNSPEC
591 ) -> Awaitable[list[tuple[int, Any]]]:
592 if (host, port, family) in self.mapping:
593 host, port = self.mapping[(host, port, family)]
594 elif (host, port) in self.mapping:
595 host, port = self.mapping[(host, port)]
596 elif host in self.mapping:
597 host = self.mapping[host]
598 return self.resolver.resolve(host, port, family)
601# These are the keyword arguments to ssl.wrap_socket that must be translated
602# to their SSLContext equivalents (the other arguments are still passed
603# to SSLContext.wrap_socket).
604_SSL_CONTEXT_KEYWORDS = frozenset(
605 ["ssl_version", "certfile", "keyfile", "cert_reqs", "ca_certs", "ciphers"]
606)
609def ssl_options_to_context(
610 ssl_options: dict[str, Any] | ssl.SSLContext,
611 server_side: bool | None = None,
612) -> ssl.SSLContext:
613 """Try to convert an ``ssl_options`` dictionary to an
614 `~ssl.SSLContext` object.
616 The ``ssl_options`` argument may be either an `ssl.SSLContext` object or a dictionary containing
617 keywords to be passed to ``ssl.SSLContext.wrap_socket``. This function converts the dict form
618 to its `~ssl.SSLContext` equivalent, and may be used when a component which accepts both forms
619 needs to upgrade to the `~ssl.SSLContext` version to use features like SNI or ALPN.
621 .. versionchanged:: 6.2
623 Added server_side argument. Omitting this argument will result in a DeprecationWarning on
624 Python 3.10.
626 """
627 if isinstance(ssl_options, ssl.SSLContext):
628 return ssl_options
629 assert isinstance(ssl_options, dict)
630 assert all(k in _SSL_CONTEXT_KEYWORDS for k in ssl_options), ssl_options
631 # TODO: Now that we have the server_side argument, can we switch to
632 # create_default_context or would that change behavior?
633 default_version = ssl.PROTOCOL_TLS
634 if server_side:
635 default_version = ssl.PROTOCOL_TLS_SERVER
636 elif server_side is not None:
637 default_version = ssl.PROTOCOL_TLS_CLIENT
638 context = ssl.SSLContext(ssl_options.get("ssl_version", default_version))
639 if "certfile" in ssl_options:
640 context.load_cert_chain(
641 ssl_options["certfile"], ssl_options.get("keyfile", None)
642 )
643 if "cert_reqs" in ssl_options:
644 if ssl_options["cert_reqs"] == ssl.CERT_NONE:
645 # This may have been set automatically by PROTOCOL_TLS_CLIENT but is
646 # incompatible with CERT_NONE so we must manually clear it.
647 context.check_hostname = False
648 context.verify_mode = ssl_options["cert_reqs"]
649 if "ca_certs" in ssl_options:
650 context.load_verify_locations(ssl_options["ca_certs"])
651 if "ciphers" in ssl_options:
652 context.set_ciphers(ssl_options["ciphers"])
653 if hasattr(ssl, "OP_NO_COMPRESSION"):
654 # Disable TLS compression to avoid CRIME and related attacks.
655 # This constant depends on openssl version 1.0.
656 # TODO: Do we need to do this ourselves or can we trust
657 # the defaults?
658 context.options |= ssl.OP_NO_COMPRESSION
659 return context
662def ssl_wrap_socket(
663 socket: socket.socket,
664 ssl_options: dict[str, Any] | ssl.SSLContext,
665 server_hostname: str | None = None,
666 server_side: bool | None = None,
667 **kwargs: Any,
668) -> ssl.SSLSocket:
669 """Returns an ``ssl.SSLSocket`` wrapping the given socket.
671 ``ssl_options`` may be either an `ssl.SSLContext` object or a
672 dictionary (as accepted by `ssl_options_to_context`). Additional
673 keyword arguments are passed to `ssl.SSLContext.wrap_socket`.
675 .. versionchanged:: 6.2
677 Added server_side argument. Omitting this argument will
678 result in a DeprecationWarning on Python 3.10.
679 """
680 context = ssl_options_to_context(ssl_options, server_side=server_side)
681 if server_side is None:
682 server_side = False
683 assert ssl.HAS_SNI
684 # TODO: add a unittest for hostname validation (python added server-side SNI support in 3.4)
685 # In the meantime it can be manually tested with
686 # python3 -m tornado.httpclient https://sni.velox.ch
687 return context.wrap_socket(
688 socket, server_hostname=server_hostname, server_side=server_side, **kwargs
689 )