|
| 1 | +import contextlib |
| 2 | +import errno |
| 3 | +import socket |
| 4 | +import unittest |
| 5 | +import sys |
| 6 | + |
| 7 | +from .. import support |
| 8 | + |
| 9 | + |
| 10 | +HOST = "localhost" |
| 11 | +HOSTv4 = "127.0.0.1" |
| 12 | +HOSTv6 = "::1" |
| 13 | + |
| 14 | + |
| 15 | +def find_unused_port(family=socket.AF_INET, socktype=socket.SOCK_STREAM): |
| 16 | + """Returns an unused port that should be suitable for binding. This is |
| 17 | + achieved by creating a temporary socket with the same family and type as |
| 18 | + the 'sock' parameter (default is AF_INET, SOCK_STREAM), and binding it to |
| 19 | + the specified host address (defaults to 0.0.0.0) with the port set to 0, |
| 20 | + eliciting an unused ephemeral port from the OS. The temporary socket is |
| 21 | + then closed and deleted, and the ephemeral port is returned. |
| 22 | +
|
| 23 | + Either this method or bind_port() should be used for any tests where a |
| 24 | + server socket needs to be bound to a particular port for the duration of |
| 25 | + the test. Which one to use depends on whether the calling code is creating |
| 26 | + a python socket, or if an unused port needs to be provided in a constructor |
| 27 | + or passed to an external program (i.e. the -accept argument to openssl's |
| 28 | + s_server mode). Always prefer bind_port() over find_unused_port() where |
| 29 | + possible. Hard coded ports should *NEVER* be used. As soon as a server |
| 30 | + socket is bound to a hard coded port, the ability to run multiple instances |
| 31 | + of the test simultaneously on the same host is compromised, which makes the |
| 32 | + test a ticking time bomb in a buildbot environment. On Unix buildbots, this |
| 33 | + may simply manifest as a failed test, which can be recovered from without |
| 34 | + intervention in most cases, but on Windows, the entire python process can |
| 35 | + completely and utterly wedge, requiring someone to log in to the buildbot |
| 36 | + and manually kill the affected process. |
| 37 | +
|
| 38 | + (This is easy to reproduce on Windows, unfortunately, and can be traced to |
| 39 | + the SO_REUSEADDR socket option having different semantics on Windows versus |
| 40 | + Unix/Linux. On Unix, you can't have two AF_INET SOCK_STREAM sockets bind, |
| 41 | + listen and then accept connections on identical host/ports. An EADDRINUSE |
| 42 | + OSError will be raised at some point (depending on the platform and |
| 43 | + the order bind and listen were called on each socket). |
| 44 | +
|
| 45 | + However, on Windows, if SO_REUSEADDR is set on the sockets, no EADDRINUSE |
| 46 | + will ever be raised when attempting to bind two identical host/ports. When |
| 47 | + accept() is called on each socket, the second caller's process will steal |
| 48 | + the port from the first caller, leaving them both in an awkwardly wedged |
| 49 | + state where they'll no longer respond to any signals or graceful kills, and |
| 50 | + must be forcibly killed via OpenProcess()/TerminateProcess(). |
| 51 | +
|
| 52 | + The solution on Windows is to use the SO_EXCLUSIVEADDRUSE socket option |
| 53 | + instead of SO_REUSEADDR, which effectively affords the same semantics as |
| 54 | + SO_REUSEADDR on Unix. Given the propensity of Unix developers in the Open |
| 55 | + Source world compared to Windows ones, this is a common mistake. A quick |
| 56 | + look over OpenSSL's 0.9.8g source shows that they use SO_REUSEADDR when |
| 57 | + openssl.exe is called with the 's_server' option, for example. See |
| 58 | + http://bugs.python.org/issue2550 for more info. The following site also |
| 59 | + has a very thorough description about the implications of both REUSEADDR |
| 60 | + and EXCLUSIVEADDRUSE on Windows: |
| 61 | + http://msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx) |
| 62 | +
|
| 63 | + XXX: although this approach is a vast improvement on previous attempts to |
| 64 | + elicit unused ports, it rests heavily on the assumption that the ephemeral |
| 65 | + port returned to us by the OS won't immediately be dished back out to some |
| 66 | + other process when we close and delete our temporary socket but before our |
| 67 | + calling code has a chance to bind the returned port. We can deal with this |
| 68 | + issue if/when we come across it. |
| 69 | + """ |
| 70 | + |
| 71 | + with socket.socket(family, socktype) as tempsock: |
| 72 | + port = bind_port(tempsock) |
| 73 | + del tempsock |
| 74 | + return port |
| 75 | + |
| 76 | +def bind_port(sock, host=HOST): |
| 77 | + """Bind the socket to a free port and return the port number. Relies on |
| 78 | + ephemeral ports in order to ensure we are using an unbound port. This is |
| 79 | + important as many tests may be running simultaneously, especially in a |
| 80 | + buildbot environment. This method raises an exception if the sock.family |
| 81 | + is AF_INET and sock.type is SOCK_STREAM, *and* the socket has SO_REUSEADDR |
| 82 | + or SO_REUSEPORT set on it. Tests should *never* set these socket options |
| 83 | + for TCP/IP sockets. The only case for setting these options is testing |
| 84 | + multicasting via multiple UDP sockets. |
| 85 | +
|
| 86 | + Additionally, if the SO_EXCLUSIVEADDRUSE socket option is available (i.e. |
| 87 | + on Windows), it will be set on the socket. This will prevent anyone else |
| 88 | + from bind()'ing to our host/port for the duration of the test. |
| 89 | + """ |
| 90 | + |
| 91 | + if sock.family == socket.AF_INET and sock.type == socket.SOCK_STREAM: |
| 92 | + if hasattr(socket, 'SO_REUSEADDR'): |
| 93 | + if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) == 1: |
| 94 | + raise support.TestFailed("tests should never set the " |
| 95 | + "SO_REUSEADDR socket option on " |
| 96 | + "TCP/IP sockets!") |
| 97 | + if hasattr(socket, 'SO_REUSEPORT'): |
| 98 | + try: |
| 99 | + if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) == 1: |
| 100 | + raise support.TestFailed("tests should never set the " |
| 101 | + "SO_REUSEPORT socket option on " |
| 102 | + "TCP/IP sockets!") |
| 103 | + except OSError: |
| 104 | + # Python's socket module was compiled using modern headers |
| 105 | + # thus defining SO_REUSEPORT but this process is running |
| 106 | + # under an older kernel that does not support SO_REUSEPORT. |
| 107 | + pass |
| 108 | + if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'): |
| 109 | + sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) |
| 110 | + |
| 111 | + sock.bind((host, 0)) |
| 112 | + port = sock.getsockname()[1] |
| 113 | + return port |
| 114 | + |
| 115 | +def bind_unix_socket(sock, addr): |
| 116 | + """Bind a unix socket, raising SkipTest if PermissionError is raised.""" |
| 117 | + assert sock.family == socket.AF_UNIX |
| 118 | + try: |
| 119 | + sock.bind(addr) |
| 120 | + except PermissionError: |
| 121 | + sock.close() |
| 122 | + raise unittest.SkipTest('cannot bind AF_UNIX sockets') |
| 123 | + |
| 124 | +def _is_ipv6_enabled(): |
| 125 | + """Check whether IPv6 is enabled on this host.""" |
| 126 | + if socket.has_ipv6: |
| 127 | + sock = None |
| 128 | + try: |
| 129 | + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) |
| 130 | + sock.bind((HOSTv6, 0)) |
| 131 | + return True |
| 132 | + except OSError: |
| 133 | + pass |
| 134 | + finally: |
| 135 | + if sock: |
| 136 | + sock.close() |
| 137 | + return False |
| 138 | + |
| 139 | +IPV6_ENABLED = _is_ipv6_enabled() |
| 140 | + |
| 141 | + |
| 142 | +_bind_nix_socket_error = None |
| 143 | +def skip_unless_bind_unix_socket(test): |
| 144 | + """Decorator for tests requiring a functional bind() for unix sockets.""" |
| 145 | + if not hasattr(socket, 'AF_UNIX'): |
| 146 | + return unittest.skip('No UNIX Sockets')(test) |
| 147 | + global _bind_nix_socket_error |
| 148 | + if _bind_nix_socket_error is None: |
| 149 | + from .os_helper import TESTFN, unlink |
| 150 | + path = TESTFN + "can_bind_unix_socket" |
| 151 | + with socket.socket(socket.AF_UNIX) as sock: |
| 152 | + try: |
| 153 | + sock.bind(path) |
| 154 | + _bind_nix_socket_error = False |
| 155 | + except OSError as e: |
| 156 | + _bind_nix_socket_error = e |
| 157 | + finally: |
| 158 | + unlink(path) |
| 159 | + if _bind_nix_socket_error: |
| 160 | + msg = 'Requires a functional unix bind(): %s' % _bind_nix_socket_error |
| 161 | + return unittest.skip(msg)(test) |
| 162 | + else: |
| 163 | + return test |
| 164 | + |
| 165 | + |
| 166 | +def get_socket_conn_refused_errs(): |
| 167 | + """ |
| 168 | + Get the different socket error numbers ('errno') which can be received |
| 169 | + when a connection is refused. |
| 170 | + """ |
| 171 | + errors = [errno.ECONNREFUSED] |
| 172 | + if hasattr(errno, 'ENETUNREACH'): |
| 173 | + # On Solaris, ENETUNREACH is returned sometimes instead of ECONNREFUSED |
| 174 | + errors.append(errno.ENETUNREACH) |
| 175 | + if hasattr(errno, 'EADDRNOTAVAIL'): |
| 176 | + # bpo-31910: socket.create_connection() fails randomly |
| 177 | + # with EADDRNOTAVAIL on Travis CI |
| 178 | + errors.append(errno.EADDRNOTAVAIL) |
| 179 | + if hasattr(errno, 'EHOSTUNREACH'): |
| 180 | + # bpo-37583: The destination host cannot be reached |
| 181 | + errors.append(errno.EHOSTUNREACH) |
| 182 | + if not IPV6_ENABLED: |
| 183 | + errors.append(errno.EAFNOSUPPORT) |
| 184 | + return errors |
| 185 | + |
| 186 | + |
| 187 | +_NOT_SET = object() |
| 188 | + |
| 189 | +@contextlib.contextmanager |
| 190 | +def transient_internet(resource_name, *, timeout=_NOT_SET, errnos=()): |
| 191 | + """Return a context manager that raises ResourceDenied when various issues |
| 192 | + with the internet connection manifest themselves as exceptions.""" |
| 193 | + import nntplib |
| 194 | + import urllib.error |
| 195 | + if timeout is _NOT_SET: |
| 196 | + timeout = support.INTERNET_TIMEOUT |
| 197 | + |
| 198 | + default_errnos = [ |
| 199 | + ('ECONNREFUSED', 111), |
| 200 | + ('ECONNRESET', 104), |
| 201 | + ('EHOSTUNREACH', 113), |
| 202 | + ('ENETUNREACH', 101), |
| 203 | + ('ETIMEDOUT', 110), |
| 204 | + # socket.create_connection() fails randomly with |
| 205 | + # EADDRNOTAVAIL on Travis CI. |
| 206 | + ('EADDRNOTAVAIL', 99), |
| 207 | + ] |
| 208 | + default_gai_errnos = [ |
| 209 | + ('EAI_AGAIN', -3), |
| 210 | + ('EAI_FAIL', -4), |
| 211 | + ('EAI_NONAME', -2), |
| 212 | + ('EAI_NODATA', -5), |
| 213 | + # Encountered when trying to resolve IPv6-only hostnames |
| 214 | + ('WSANO_DATA', 11004), |
| 215 | + ] |
| 216 | + |
| 217 | + denied = support.ResourceDenied("Resource %r is not available" % resource_name) |
| 218 | + captured_errnos = errnos |
| 219 | + gai_errnos = [] |
| 220 | + if not captured_errnos: |
| 221 | + captured_errnos = [getattr(errno, name, num) |
| 222 | + for (name, num) in default_errnos] |
| 223 | + gai_errnos = [getattr(socket, name, num) |
| 224 | + for (name, num) in default_gai_errnos] |
| 225 | + |
| 226 | + def filter_error(err): |
| 227 | + n = getattr(err, 'errno', None) |
| 228 | + if (isinstance(err, TimeoutError) or |
| 229 | + (isinstance(err, socket.gaierror) and n in gai_errnos) or |
| 230 | + (isinstance(err, urllib.error.HTTPError) and |
| 231 | + 500 <= err.code <= 599) or |
| 232 | + (isinstance(err, urllib.error.URLError) and |
| 233 | + (("ConnectionRefusedError" in err.reason) or |
| 234 | + ("TimeoutError" in err.reason) or |
| 235 | + ("EOFError" in err.reason))) or |
| 236 | + n in captured_errnos): |
| 237 | + if not support.verbose: |
| 238 | + sys.stderr.write(denied.args[0] + "\n") |
| 239 | + raise denied from err |
| 240 | + |
| 241 | + old_timeout = socket.getdefaulttimeout() |
| 242 | + try: |
| 243 | + if timeout is not None: |
| 244 | + socket.setdefaulttimeout(timeout) |
| 245 | + yield |
| 246 | + except nntplib.NNTPTemporaryError as err: |
| 247 | + if support.verbose: |
| 248 | + sys.stderr.write(denied.args[0] + "\n") |
| 249 | + raise denied from err |
| 250 | + except OSError as err: |
| 251 | + # urllib can wrap original socket errors multiple times (!), we must |
| 252 | + # unwrap to get at the original error. |
| 253 | + while True: |
| 254 | + a = err.args |
| 255 | + if len(a) >= 1 and isinstance(a[0], OSError): |
| 256 | + err = a[0] |
| 257 | + # The error can also be wrapped as args[1]: |
| 258 | + # except socket.error as msg: |
| 259 | + # raise OSError('socket error', msg).with_traceback(sys.exc_info()[2]) |
| 260 | + elif len(a) >= 2 and isinstance(a[1], OSError): |
| 261 | + err = a[1] |
| 262 | + else: |
| 263 | + break |
| 264 | + filter_error(err) |
| 265 | + raise |
| 266 | + # XXX should we catch generic exceptions and look for their |
| 267 | + # __cause__ or __context__? |
| 268 | + finally: |
| 269 | + socket.setdefaulttimeout(old_timeout) |
0 commit comments