disable_internet.py 6.1 KB

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