test_networking_utils.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. #!/usr/bin/env python3
  2. # Allow direct execution
  3. import os
  4. import sys
  5. import pytest
  6. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  7. import contextlib
  8. import io
  9. import platform
  10. import random
  11. import ssl
  12. import urllib.error
  13. import warnings
  14. from yt_dlp.cookies import YoutubeDLCookieJar
  15. from yt_dlp.dependencies import certifi
  16. from yt_dlp.networking import Response
  17. from yt_dlp.networking._helper import (
  18. InstanceStoreMixin,
  19. add_accept_encoding_header,
  20. get_redirect_method,
  21. make_socks_proxy_opts,
  22. select_proxy,
  23. ssl_load_certs,
  24. )
  25. from yt_dlp.networking.exceptions import (
  26. HTTPError,
  27. IncompleteRead,
  28. _CompatHTTPError,
  29. )
  30. from yt_dlp.socks import ProxyType
  31. from yt_dlp.utils.networking import HTTPHeaderDict
  32. TEST_DIR = os.path.dirname(os.path.abspath(__file__))
  33. class TestNetworkingUtils:
  34. def test_select_proxy(self):
  35. proxies = {
  36. 'all': 'socks5://example.com',
  37. 'http': 'http://example.com:1080',
  38. 'no': 'bypass.example.com,yt-dl.org'
  39. }
  40. assert select_proxy('https://example.com', proxies) == proxies['all']
  41. assert select_proxy('http://example.com', proxies) == proxies['http']
  42. assert select_proxy('http://bypass.example.com', proxies) is None
  43. assert select_proxy('https://yt-dl.org', proxies) is None
  44. @pytest.mark.parametrize('socks_proxy,expected', [
  45. ('socks5h://example.com', {
  46. 'proxytype': ProxyType.SOCKS5,
  47. 'addr': 'example.com',
  48. 'port': 1080,
  49. 'rdns': True,
  50. 'username': None,
  51. 'password': None
  52. }),
  53. ('socks5://user:@example.com:5555', {
  54. 'proxytype': ProxyType.SOCKS5,
  55. 'addr': 'example.com',
  56. 'port': 5555,
  57. 'rdns': False,
  58. 'username': 'user',
  59. 'password': ''
  60. }),
  61. ('socks4://u%40ser:pa%20ss@127.0.0.1:1080', {
  62. 'proxytype': ProxyType.SOCKS4,
  63. 'addr': '127.0.0.1',
  64. 'port': 1080,
  65. 'rdns': False,
  66. 'username': 'u@ser',
  67. 'password': 'pa ss'
  68. }),
  69. ('socks4a://:pa%20ss@127.0.0.1', {
  70. 'proxytype': ProxyType.SOCKS4A,
  71. 'addr': '127.0.0.1',
  72. 'port': 1080,
  73. 'rdns': True,
  74. 'username': '',
  75. 'password': 'pa ss'
  76. })
  77. ])
  78. def test_make_socks_proxy_opts(self, socks_proxy, expected):
  79. assert make_socks_proxy_opts(socks_proxy) == expected
  80. def test_make_socks_proxy_unknown(self):
  81. with pytest.raises(ValueError, match='Unknown SOCKS proxy version: socks'):
  82. make_socks_proxy_opts('socks://127.0.0.1')
  83. @pytest.mark.skipif(not certifi, reason='certifi is not installed')
  84. def test_load_certifi(self):
  85. context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  86. context2 = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  87. ssl_load_certs(context, use_certifi=True)
  88. context2.load_verify_locations(cafile=certifi.where())
  89. assert context.get_ca_certs() == context2.get_ca_certs()
  90. # Test load normal certs
  91. # XXX: could there be a case where system certs are the same as certifi?
  92. context3 = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
  93. ssl_load_certs(context3, use_certifi=False)
  94. assert context3.get_ca_certs() != context.get_ca_certs()
  95. @pytest.mark.parametrize('method,status,expected', [
  96. ('GET', 303, 'GET'),
  97. ('HEAD', 303, 'HEAD'),
  98. ('PUT', 303, 'GET'),
  99. ('POST', 301, 'GET'),
  100. ('HEAD', 301, 'HEAD'),
  101. ('POST', 302, 'GET'),
  102. ('HEAD', 302, 'HEAD'),
  103. ('PUT', 302, 'PUT'),
  104. ('POST', 308, 'POST'),
  105. ('POST', 307, 'POST'),
  106. ('HEAD', 308, 'HEAD'),
  107. ('HEAD', 307, 'HEAD'),
  108. ])
  109. def test_get_redirect_method(self, method, status, expected):
  110. assert get_redirect_method(method, status) == expected
  111. @pytest.mark.parametrize('headers,supported_encodings,expected', [
  112. ({'Accept-Encoding': 'br'}, ['gzip', 'br'], {'Accept-Encoding': 'br'}),
  113. ({}, ['gzip', 'br'], {'Accept-Encoding': 'gzip, br'}),
  114. ({'Content-type': 'application/json'}, [], {'Content-type': 'application/json', 'Accept-Encoding': 'identity'}),
  115. ])
  116. def test_add_accept_encoding_header(self, headers, supported_encodings, expected):
  117. headers = HTTPHeaderDict(headers)
  118. add_accept_encoding_header(headers, supported_encodings)
  119. assert headers == HTTPHeaderDict(expected)
  120. class TestInstanceStoreMixin:
  121. class FakeInstanceStoreMixin(InstanceStoreMixin):
  122. def _create_instance(self, **kwargs):
  123. return random.randint(0, 1000000)
  124. def _close_instance(self, instance):
  125. pass
  126. def test_mixin(self):
  127. mixin = self.FakeInstanceStoreMixin()
  128. assert mixin._get_instance(d={'a': 1, 'b': 2, 'c': {'d', 4}}) == mixin._get_instance(d={'a': 1, 'b': 2, 'c': {'d', 4}})
  129. assert mixin._get_instance(d={'a': 1, 'b': 2, 'c': {'e', 4}}) != mixin._get_instance(d={'a': 1, 'b': 2, 'c': {'d', 4}})
  130. assert mixin._get_instance(d={'a': 1, 'b': 2, 'c': {'d', 4}} != mixin._get_instance(d={'a': 1, 'b': 2, 'g': {'d', 4}}))
  131. assert mixin._get_instance(d={'a': 1}, e=[1, 2, 3]) == mixin._get_instance(d={'a': 1}, e=[1, 2, 3])
  132. assert mixin._get_instance(d={'a': 1}, e=[1, 2, 3]) != mixin._get_instance(d={'a': 1}, e=[1, 2, 3, 4])
  133. cookiejar = YoutubeDLCookieJar()
  134. assert mixin._get_instance(b=[1, 2], c=cookiejar) == mixin._get_instance(b=[1, 2], c=cookiejar)
  135. assert mixin._get_instance(b=[1, 2], c=cookiejar) != mixin._get_instance(b=[1, 2], c=YoutubeDLCookieJar())
  136. # Different order
  137. assert mixin._get_instance(c=cookiejar, b=[1, 2]) == mixin._get_instance(b=[1, 2], c=cookiejar)
  138. m = mixin._get_instance(t=1234)
  139. assert mixin._get_instance(t=1234) == m
  140. mixin._clear_instances()
  141. assert mixin._get_instance(t=1234) != m
  142. class TestNetworkingExceptions:
  143. @staticmethod
  144. def create_response(status):
  145. return Response(fp=io.BytesIO(b'test'), url='http://example.com', headers={'tesT': 'test'}, status=status)
  146. @pytest.mark.parametrize('http_error_class', [HTTPError, lambda r: _CompatHTTPError(HTTPError(r))])
  147. def test_http_error(self, http_error_class):
  148. response = self.create_response(403)
  149. error = http_error_class(response)
  150. assert error.status == 403
  151. assert str(error) == error.msg == 'HTTP Error 403: Forbidden'
  152. assert error.reason == response.reason
  153. assert error.response is response
  154. data = error.response.read()
  155. assert data == b'test'
  156. assert repr(error) == '<HTTPError 403: Forbidden>'
  157. @pytest.mark.parametrize('http_error_class', [HTTPError, lambda *args, **kwargs: _CompatHTTPError(HTTPError(*args, **kwargs))])
  158. def test_redirect_http_error(self, http_error_class):
  159. response = self.create_response(301)
  160. error = http_error_class(response, redirect_loop=True)
  161. assert str(error) == error.msg == 'HTTP Error 301: Moved Permanently (redirect loop detected)'
  162. assert error.reason == 'Moved Permanently'
  163. def test_compat_http_error(self):
  164. response = self.create_response(403)
  165. error = _CompatHTTPError(HTTPError(response))
  166. assert isinstance(error, HTTPError)
  167. assert isinstance(error, urllib.error.HTTPError)
  168. @contextlib.contextmanager
  169. def raises_deprecation_warning():
  170. with warnings.catch_warnings(record=True) as w:
  171. warnings.simplefilter('always')
  172. yield
  173. if len(w) == 0:
  174. pytest.fail('Did not raise DeprecationWarning')
  175. if len(w) > 1:
  176. pytest.fail(f'Raised multiple warnings: {w}')
  177. if not issubclass(w[-1].category, DeprecationWarning):
  178. pytest.fail(f'Expected DeprecationWarning, got {w[-1].category}')
  179. w.clear()
  180. with raises_deprecation_warning():
  181. assert error.code == 403
  182. with raises_deprecation_warning():
  183. assert error.getcode() == 403
  184. with raises_deprecation_warning():
  185. assert error.hdrs is error.response.headers
  186. with raises_deprecation_warning():
  187. assert error.info() is error.response.headers
  188. with raises_deprecation_warning():
  189. assert error.headers is error.response.headers
  190. with raises_deprecation_warning():
  191. assert error.filename == error.response.url
  192. with raises_deprecation_warning():
  193. assert error.url == error.response.url
  194. with raises_deprecation_warning():
  195. assert error.geturl() == error.response.url
  196. # Passthrough file operations
  197. with raises_deprecation_warning():
  198. assert error.read() == b'test'
  199. with raises_deprecation_warning():
  200. assert not error.closed
  201. with raises_deprecation_warning():
  202. # Technically Response operations are also passed through, which should not be used.
  203. assert error.get_header('test') == 'test'
  204. # Should not raise a warning
  205. error.close()
  206. @pytest.mark.skipif(
  207. platform.python_implementation() == 'PyPy', reason='garbage collector works differently in pypy')
  208. def test_compat_http_error_autoclose(self):
  209. # Compat HTTPError should not autoclose response
  210. response = self.create_response(403)
  211. _CompatHTTPError(HTTPError(response))
  212. assert not response.closed
  213. def test_incomplete_read_error(self):
  214. error = IncompleteRead(b'test', 3, cause='test')
  215. assert isinstance(error, IncompleteRead)
  216. assert repr(error) == '<IncompleteRead: 4 bytes read, 3 more expected>'
  217. assert str(error) == error.msg == '4 bytes read, 3 more expected'
  218. assert error.partial == b'test'
  219. assert error.expected == 3
  220. assert error.cause == 'test'
  221. error = IncompleteRead(b'aaa')
  222. assert repr(error) == '<IncompleteRead: 3 bytes read>'
  223. assert str(error) == '3 bytes read'