|
| 1 | +# Originally from astropy project (http://astropy.org), under BSD |
| 2 | +# 3-clause license. |
| 3 | + |
| 4 | +from __future__ import (absolute_import, division, print_function, |
| 5 | + unicode_literals) |
| 6 | + |
| 7 | +import contextlib |
| 8 | +import socket |
| 9 | + |
| 10 | +from matplotlib.externals.six.moves import urllib |
| 11 | + |
| 12 | +# save original socket method for restoration |
| 13 | +# These are global so that re-calling the turn_off_internet function doesn't |
| 14 | +# overwrite them again |
| 15 | +socket_original = socket.socket |
| 16 | +socket_create_connection = socket.create_connection |
| 17 | +socket_bind = socket.socket.bind |
| 18 | +socket_connect = socket.socket.connect |
| 19 | + |
| 20 | + |
| 21 | +INTERNET_OFF = False |
| 22 | + |
| 23 | +# urllib2 uses a global variable to cache its default "opener" for opening |
| 24 | +# connections for various protocols; we store it off here so we can restore to |
| 25 | +# the default after re-enabling internet use |
| 26 | +_orig_opener = None |
| 27 | + |
| 28 | + |
| 29 | +# ::1 is apparently another valid name for localhost? |
| 30 | +# it is returned by getaddrinfo when that function is given localhost |
| 31 | + |
| 32 | +def check_internet_off(original_function): |
| 33 | + """ |
| 34 | + Wraps ``original_function``, which in most cases is assumed |
| 35 | + to be a `socket.socket` method, to raise an `IOError` for any operations |
| 36 | + on non-local AF_INET sockets. |
| 37 | + """ |
| 38 | + |
| 39 | + def new_function(*args, **kwargs): |
| 40 | + if isinstance(args[0], socket.socket): |
| 41 | + if not args[0].family in (socket.AF_INET, socket.AF_INET6): |
| 42 | + # Should be fine in all but some very obscure cases |
| 43 | + # More to the point, we don't want to affect AF_UNIX |
| 44 | + # sockets. |
| 45 | + return original_function(*args, **kwargs) |
| 46 | + host = args[1][0] |
| 47 | + addr_arg = 1 |
| 48 | + valid_hosts = ('localhost', '127.0.0.1', '::1') |
| 49 | + else: |
| 50 | + # The only other function this is used to wrap currently is |
| 51 | + # socket.create_connection, which should be passed a 2-tuple, but |
| 52 | + # we'll check just in case |
| 53 | + if not (isinstance(args[0], tuple) and len(args[0]) == 2): |
| 54 | + return original_function(*args, **kwargs) |
| 55 | + |
| 56 | + host = args[0][0] |
| 57 | + addr_arg = 0 |
| 58 | + valid_hosts = ('localhost', '127.0.0.1') |
| 59 | + |
| 60 | + hostname = socket.gethostname() |
| 61 | + fqdn = socket.getfqdn() |
| 62 | + |
| 63 | + if host in (hostname, fqdn): |
| 64 | + host = 'localhost' |
| 65 | + new_addr = (host, args[addr_arg][1]) |
| 66 | + args = args[:addr_arg] + (new_addr,) + args[addr_arg + 1:] |
| 67 | + |
| 68 | + if any([h in host for h in valid_hosts]): |
| 69 | + return original_function(*args, **kwargs) |
| 70 | + else: |
| 71 | + raise IOError("An attempt was made to connect to the internet " |
| 72 | + "by a test that was not marked `remote_data`.") |
| 73 | + return new_function |
| 74 | + |
| 75 | + |
| 76 | +def turn_off_internet(verbose=False): |
| 77 | + """ |
| 78 | + Disable internet access via python by preventing connections from being |
| 79 | + created using the socket module. Presumably this could be worked around by |
| 80 | + using some other means of accessing the internet, but all default python |
| 81 | + modules (urllib, requests, etc.) use socket [citation needed]. |
| 82 | + """ |
| 83 | + |
| 84 | + global INTERNET_OFF |
| 85 | + global _orig_opener |
| 86 | + |
| 87 | + if INTERNET_OFF: |
| 88 | + return |
| 89 | + |
| 90 | + INTERNET_OFF = True |
| 91 | + |
| 92 | + __tracebackhide__ = True |
| 93 | + if verbose: |
| 94 | + print("Internet access disabled") |
| 95 | + |
| 96 | + # Update urllib2 to force it not to use any proxies |
| 97 | + # Must use {} here (the default of None will kick off an automatic search |
| 98 | + # for proxies) |
| 99 | + _orig_opener = urllib.request.build_opener() |
| 100 | + no_proxy_handler = urllib.request.ProxyHandler({}) |
| 101 | + opener = urllib.request.build_opener(no_proxy_handler) |
| 102 | + urllib.request.install_opener(opener) |
| 103 | + |
| 104 | + socket.create_connection = check_internet_off(socket_create_connection) |
| 105 | + socket.socket.bind = check_internet_off(socket_bind) |
| 106 | + socket.socket.connect = check_internet_off(socket_connect) |
| 107 | + |
| 108 | + return socket |
| 109 | + |
| 110 | + |
| 111 | +def turn_on_internet(verbose=False): |
| 112 | + """ |
| 113 | + Restore internet access. Not used, but kept in case it is needed. |
| 114 | + """ |
| 115 | + |
| 116 | + global INTERNET_OFF |
| 117 | + global _orig_opener |
| 118 | + |
| 119 | + if not INTERNET_OFF: |
| 120 | + return |
| 121 | + |
| 122 | + INTERNET_OFF = False |
| 123 | + |
| 124 | + if verbose: |
| 125 | + print("Internet access enabled") |
| 126 | + |
| 127 | + urllib.request.install_opener(_orig_opener) |
| 128 | + |
| 129 | + socket.create_connection = socket_create_connection |
| 130 | + socket.socket.bind = socket_bind |
| 131 | + socket.socket.connect = socket_connect |
| 132 | + return socket |
| 133 | + |
| 134 | + |
| 135 | +@contextlib.contextmanager |
| 136 | +def no_internet(verbose=False): |
| 137 | + """Context manager to temporarily disable internet access (if not already |
| 138 | + disabled). If it was already disabled before entering the context manager |
| 139 | + (i.e. `turn_off_internet` was called previously) then this is a no-op and |
| 140 | + leaves internet access disabled until a manual call to `turn_on_internet`. |
| 141 | + """ |
| 142 | + |
| 143 | + already_disabled = INTERNET_OFF |
| 144 | + |
| 145 | + turn_off_internet(verbose=verbose) |
| 146 | + try: |
| 147 | + yield |
| 148 | + finally: |
| 149 | + if not already_disabled: |
| 150 | + turn_on_internet(verbose=verbose) |
0 commit comments