_helper.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. from __future__ import annotations
  2. import contextlib
  3. import functools
  4. import ssl
  5. import sys
  6. import typing
  7. import urllib.parse
  8. import urllib.request
  9. from .exceptions import RequestError, UnsupportedRequest
  10. from ..dependencies import certifi
  11. from ..socks import ProxyType
  12. from ..utils import format_field, traverse_obj
  13. if typing.TYPE_CHECKING:
  14. from collections.abc import Iterable
  15. from ..utils.networking import HTTPHeaderDict
  16. def ssl_load_certs(context: ssl.SSLContext, use_certifi=True):
  17. if certifi and use_certifi:
  18. context.load_verify_locations(cafile=certifi.where())
  19. else:
  20. try:
  21. context.load_default_certs()
  22. # Work around the issue in load_default_certs when there are bad certificates. See:
  23. # https://github.com/hypervideo/hypervideo/issues/1060,
  24. # https://bugs.python.org/issue35665, https://bugs.python.org/issue45312
  25. except ssl.SSLError:
  26. # enum_certificates is not present in mingw python. See https://github.com/hypervideo/hypervideo/issues/1151
  27. if sys.platform == 'win32' and hasattr(ssl, 'enum_certificates'):
  28. for storename in ('CA', 'ROOT'):
  29. ssl_load_windows_store_certs(context, storename)
  30. context.set_default_verify_paths()
  31. def ssl_load_windows_store_certs(ssl_context, storename):
  32. # Code adapted from _load_windows_store_certs in https://github.com/python/cpython/blob/main/Lib/ssl.py
  33. try:
  34. certs = [cert for cert, encoding, trust in ssl.enum_certificates(storename)
  35. if encoding == 'x509_asn' and (
  36. trust is True or ssl.Purpose.SERVER_AUTH.oid in trust)]
  37. except PermissionError:
  38. return
  39. for cert in certs:
  40. with contextlib.suppress(ssl.SSLError):
  41. ssl_context.load_verify_locations(cadata=cert)
  42. def make_socks_proxy_opts(socks_proxy):
  43. url_components = urllib.parse.urlparse(socks_proxy)
  44. if url_components.scheme.lower() == 'socks5':
  45. socks_type = ProxyType.SOCKS5
  46. rdns = False
  47. elif url_components.scheme.lower() == 'socks5h':
  48. socks_type = ProxyType.SOCKS5
  49. rdns = True
  50. elif url_components.scheme.lower() == 'socks4':
  51. socks_type = ProxyType.SOCKS4
  52. rdns = False
  53. elif url_components.scheme.lower() == 'socks4a':
  54. socks_type = ProxyType.SOCKS4A
  55. rdns = True
  56. else:
  57. raise ValueError(f'Unknown SOCKS proxy version: {url_components.scheme.lower()}')
  58. def unquote_if_non_empty(s):
  59. if not s:
  60. return s
  61. return urllib.parse.unquote_plus(s)
  62. return {
  63. 'proxytype': socks_type,
  64. 'addr': url_components.hostname,
  65. 'port': url_components.port or 1080,
  66. 'rdns': rdns,
  67. 'username': unquote_if_non_empty(url_components.username),
  68. 'password': unquote_if_non_empty(url_components.password),
  69. }
  70. def select_proxy(url, proxies):
  71. """Unified proxy selector for all backends"""
  72. url_components = urllib.parse.urlparse(url)
  73. if 'no' in proxies:
  74. hostport = url_components.hostname + format_field(url_components.port, None, ':%s')
  75. if urllib.request.proxy_bypass_environment(hostport, {'no': proxies['no']}):
  76. return
  77. elif urllib.request.proxy_bypass(hostport): # check system settings
  78. return
  79. return traverse_obj(proxies, url_components.scheme or 'http', 'all')
  80. def get_redirect_method(method, status):
  81. """Unified redirect method handling"""
  82. # A 303 must either use GET or HEAD for subsequent request
  83. # https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.4
  84. if status == 303 and method != 'HEAD':
  85. method = 'GET'
  86. # 301 and 302 redirects are commonly turned into a GET from a POST
  87. # for subsequent requests by browsers, so we'll do the same.
  88. # https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.2
  89. # https://datatracker.ietf.org/doc/html/rfc7231#section-6.4.3
  90. if status in (301, 302) and method == 'POST':
  91. method = 'GET'
  92. return method
  93. def make_ssl_context(
  94. verify=True,
  95. client_certificate=None,
  96. client_certificate_key=None,
  97. client_certificate_password=None,
  98. legacy_support=False,
  99. use_certifi=True,
  100. ):
  101. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  102. context.check_hostname = verify
  103. context.verify_mode = ssl.CERT_REQUIRED if verify else ssl.CERT_NONE
  104. # Some servers may reject requests if ALPN extension is not sent. See:
  105. # https://github.com/python/cpython/issues/85140
  106. # https://github.com/hypervideo/hypervideo/issues/3878
  107. with contextlib.suppress(NotImplementedError):
  108. context.set_alpn_protocols(['http/1.1'])
  109. if verify:
  110. ssl_load_certs(context, use_certifi)
  111. if legacy_support:
  112. context.options |= 4 # SSL_OP_LEGACY_SERVER_CONNECT
  113. context.set_ciphers('DEFAULT') # compat
  114. elif ssl.OPENSSL_VERSION_INFO >= (1, 1, 1) and not ssl.OPENSSL_VERSION.startswith('LibreSSL'):
  115. # Use the default SSL ciphers and minimum TLS version settings from Python 3.10 [1].
  116. # This is to ensure consistent behavior across Python versions and libraries, and help avoid fingerprinting
  117. # in some situations [2][3].
  118. # Python 3.10 only supports OpenSSL 1.1.1+ [4]. Because this change is likely
  119. # untested on older versions, we only apply this to OpenSSL 1.1.1+ to be safe.
  120. # LibreSSL is excluded until further investigation due to cipher support issues [5][6].
  121. # 1. https://github.com/python/cpython/commit/e983252b516edb15d4338b0a47631b59ef1e2536
  122. # 2. https://github.com/hypervideo/hypervideo/issues/4627
  123. # 3. https://github.com/hypervideo/hypervideo/pull/5294
  124. # 4. https://peps.python.org/pep-0644/
  125. # 5. https://peps.python.org/pep-0644/#libressl-support
  126. # 6. https://github.com/hypervideo/hypervideo/commit/5b9f253fa0aee996cf1ed30185d4b502e00609c4#commitcomment-89054368
  127. context.set_ciphers(
  128. '@SECLEVEL=2:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES:DHE+AES:!aNULL:!eNULL:!aDSS:!SHA1:!AESCCM')
  129. context.minimum_version = ssl.TLSVersion.TLSv1_2
  130. if client_certificate:
  131. try:
  132. context.load_cert_chain(
  133. client_certificate, keyfile=client_certificate_key,
  134. password=client_certificate_password)
  135. except ssl.SSLError:
  136. raise RequestError('Unable to load client certificate')
  137. if getattr(context, 'post_handshake_auth', None) is not None:
  138. context.post_handshake_auth = True
  139. return context
  140. class InstanceStoreMixin:
  141. def __init__(self, **kwargs):
  142. self.__instances = []
  143. super().__init__(**kwargs) # So that both MRO works
  144. @staticmethod
  145. def _create_instance(**kwargs):
  146. raise NotImplementedError
  147. def _get_instance(self, **kwargs):
  148. for key, instance in self.__instances:
  149. if key == kwargs:
  150. return instance
  151. instance = self._create_instance(**kwargs)
  152. self.__instances.append((kwargs, instance))
  153. return instance
  154. def _close_instance(self, instance):
  155. if callable(getattr(instance, 'close', None)):
  156. instance.close()
  157. def _clear_instances(self):
  158. for _, instance in self.__instances:
  159. self._close_instance(instance)
  160. self.__instances.clear()
  161. def add_accept_encoding_header(headers: HTTPHeaderDict, supported_encodings: Iterable[str]):
  162. if 'Accept-Encoding' not in headers:
  163. headers['Accept-Encoding'] = ', '.join(supported_encodings) or 'identity'
  164. def wrap_request_errors(func):
  165. @functools.wraps(func)
  166. def wrapper(self, *args, **kwargs):
  167. try:
  168. return func(self, *args, **kwargs)
  169. except UnsupportedRequest as e:
  170. if e.handler is None:
  171. e.handler = self
  172. raise
  173. return wrapper