3 # Allow direct execution
9 from yt_dlp
.networking
.common
import Features
, DEFAULT_TIMEOUT
11 sys
.path
.insert(0, os
.path
.dirname(os
.path
.dirname(os
.path
.abspath(__file__
))))
29 from email
.message
import Message
30 from http
.cookiejar
import CookieJar
32 from test
.helper
import (
36 verify_address_availability
,
38 from yt_dlp
.cookies
import YoutubeDLCookieJar
39 from yt_dlp
.dependencies
import brotli
, curl_cffi
, requests
, urllib3
40 from yt_dlp
.networking
import (
48 from yt_dlp
.networking
._urllib
import UrllibRH
49 from yt_dlp
.networking
.exceptions
import (
50 CertificateVerifyError
,
60 from yt_dlp
.networking
.impersonate
import (
61 ImpersonateRequestHandler
,
64 from yt_dlp
.utils
import YoutubeDLError
65 from yt_dlp
.utils
._utils
import _YDLLogger
as FakeLogger
66 from yt_dlp
.utils
.networking
import HTTPHeaderDict
, std_headers
68 TEST_DIR
= os
.path
.dirname(os
.path
.abspath(__file__
))
71 class HTTPTestRequestHandler(http
.server
.BaseHTTPRequestHandler
):
72 protocol_version
= 'HTTP/1.1'
73 default_request_version
= 'HTTP/1.1'
75 def log_message(self
, format
, *args
):
79 payload
= str(self
.headers
).encode()
80 self
.send_response(200)
81 self
.send_header('Content-Type', 'application/json')
82 self
.send_header('Content-Length', str(len(payload
)))
84 self
.wfile
.write(payload
)
87 self
.send_response(int(self
.path
[len('/redirect_'):]))
88 self
.send_header('Location', '/method')
89 self
.send_header('Content-Length', '0')
92 def _method(self
, method
, payload
=None):
93 self
.send_response(200)
94 self
.send_header('Content-Length', str(len(payload
or '')))
95 self
.send_header('Method', method
)
98 self
.wfile
.write(payload
)
100 def _status(self
, status
):
101 payload
= f
'<html>{status} NOT FOUND</html>'.encode()
102 self
.send_response(int(status
))
103 self
.send_header('Content-Type', 'text/html; charset=utf-8')
104 self
.send_header('Content-Length', str(len(payload
)))
106 self
.wfile
.write(payload
)
108 def _read_data(self
):
109 if 'Content-Length' in self
.headers
:
110 return self
.rfile
.read(int(self
.headers
['Content-Length']))
115 data
= self
._read
_data
() + str(self
.headers
).encode()
116 if self
.path
.startswith('/redirect_'):
118 elif self
.path
.startswith('/method'):
119 self
._method
('POST', data
)
120 elif self
.path
.startswith('/headers'):
126 if self
.path
.startswith('/redirect_'):
128 elif self
.path
.startswith('/method'):
134 data
= self
._read
_data
() + str(self
.headers
).encode()
135 if self
.path
.startswith('/redirect_'):
137 elif self
.path
.startswith('/method'):
138 self
._method
('PUT', data
)
143 if self
.path
== '/video.html':
144 payload
= b
'<html><video src="/vid.mp4" /></html>'
145 self
.send_response(200)
146 self
.send_header('Content-Type', 'text/html; charset=utf-8')
147 self
.send_header('Content-Length', str(len(payload
)))
149 self
.wfile
.write(payload
)
150 elif self
.path
== '/vid.mp4':
151 payload
= b
'\x00\x00\x00\x00\x20\x66\x74[video]'
152 self
.send_response(200)
153 self
.send_header('Content-Type', 'video/mp4')
154 self
.send_header('Content-Length', str(len(payload
)))
156 self
.wfile
.write(payload
)
157 elif self
.path
== '/%E4%B8%AD%E6%96%87.html':
158 payload
= b
'<html><video src="/vid.mp4" /></html>'
159 self
.send_response(200)
160 self
.send_header('Content-Type', 'text/html; charset=utf-8')
161 self
.send_header('Content-Length', str(len(payload
)))
163 self
.wfile
.write(payload
)
164 elif self
.path
== '/%c7%9f':
165 payload
= b
'<html><video src="/vid.mp4" /></html>'
166 self
.send_response(200)
167 self
.send_header('Content-Type', 'text/html; charset=utf-8')
168 self
.send_header('Content-Length', str(len(payload
)))
170 self
.wfile
.write(payload
)
171 elif self
.path
.startswith('/redirect_loop'):
172 self
.send_response(301)
173 self
.send_header('Location', self
.path
)
174 self
.send_header('Content-Length', '0')
176 elif self
.path
== '/redirect_dotsegments':
177 self
.send_response(301)
178 # redirect to /headers but with dot segments before
179 self
.send_header('Location', '/a/b/./../../headers')
180 self
.send_header('Content-Length', '0')
182 elif self
.path
== '/redirect_dotsegments_absolute':
183 self
.send_response(301)
184 # redirect to /headers but with dot segments before - absolute url
185 self
.send_header('Location', f
'http://127.0.0.1:{http_server_port(self.server)}/a/b/./../../headers')
186 self
.send_header('Content-Length', '0')
188 elif self
.path
.startswith('/redirect_'):
190 elif self
.path
.startswith('/method'):
191 self
._method
('GET', str(self
.headers
).encode())
192 elif self
.path
.startswith('/headers'):
194 elif self
.path
.startswith('/308-to-headers'):
195 self
.send_response(308)
196 # redirect to "localhost" for testing cookie redirection handling
197 self
.send_header('Location', f
'http://localhost:{self.connection.getsockname()[1]}/headers')
198 self
.send_header('Content-Length', '0')
200 elif self
.path
== '/trailing_garbage':
201 payload
= b
'<html><video src="/vid.mp4" /></html>'
202 self
.send_response(200)
203 self
.send_header('Content-Type', 'text/html; charset=utf-8')
204 self
.send_header('Content-Encoding', 'gzip')
206 with gzip
.GzipFile(fileobj
=buf
, mode
='wb') as f
:
208 compressed
= buf
.getvalue() + b
'trailing garbage'
209 self
.send_header('Content-Length', str(len(compressed
)))
211 self
.wfile
.write(compressed
)
212 elif self
.path
== '/302-non-ascii-redirect':
213 new_url
= f
'http://127.0.0.1:{http_server_port(self.server)}/中文.html'
214 self
.send_response(301)
215 self
.send_header('Location', new_url
)
216 self
.send_header('Content-Length', '0')
218 elif self
.path
== '/content-encoding':
219 encodings
= self
.headers
.get('ytdl-encoding', '')
220 payload
= b
'<html><video src="/vid.mp4" /></html>'
221 for encoding
in filter(None, (e
.strip() for e
in encodings
.split(','))):
222 if encoding
== 'br' and brotli
:
223 payload
= brotli
.compress(payload
)
224 elif encoding
== 'gzip':
226 with gzip
.GzipFile(fileobj
=buf
, mode
='wb') as f
:
228 payload
= buf
.getvalue()
229 elif encoding
== 'deflate':
230 payload
= zlib
.compress(payload
)
231 elif encoding
== 'unsupported':
237 self
.send_response(200)
238 self
.send_header('Content-Encoding', encodings
)
239 self
.send_header('Content-Length', str(len(payload
)))
241 self
.wfile
.write(payload
)
242 elif self
.path
.startswith('/gen_'):
243 payload
= b
'<html></html>'
244 self
.send_response(int(self
.path
[len('/gen_'):]))
245 self
.send_header('Content-Type', 'text/html; charset=utf-8')
246 self
.send_header('Content-Length', str(len(payload
)))
248 self
.wfile
.write(payload
)
249 elif self
.path
.startswith('/incompleteread'):
250 payload
= b
'<html></html>'
251 self
.send_response(200)
252 self
.send_header('Content-Type', 'text/html; charset=utf-8')
253 self
.send_header('Content-Length', '234234')
255 self
.wfile
.write(payload
)
257 elif self
.path
.startswith('/timeout_'):
258 time
.sleep(int(self
.path
[len('/timeout_'):]))
260 elif self
.path
== '/source_address':
261 payload
= str(self
.client_address
[0]).encode()
262 self
.send_response(200)
263 self
.send_header('Content-Type', 'text/html; charset=utf-8')
264 self
.send_header('Content-Length', str(len(payload
)))
266 self
.wfile
.write(payload
)
268 elif self
.path
== '/get_cookie':
269 self
.send_response(200)
270 self
.send_header('Set-Cookie', 'test=ytdlp; path=/')
276 def send_header(self
, keyword
, value
):
278 Forcibly allow HTTP server to send non percent-encoded non-ASCII characters in headers.
279 This is against what is defined in RFC 3986, however we need to test we support this
280 since some sites incorrectly do this.
282 if keyword
.lower() == 'connection':
283 return super().send_header(keyword
, value
)
285 if not hasattr(self
, '_headers_buffer'):
286 self
._headers
_buffer
= []
288 self
._headers
_buffer
.append(f
'{keyword}: {value}\r\n'.encode())
291 class TestRequestHandlerBase
:
293 def setup_class(cls
):
294 cls
.http_httpd
= http
.server
.ThreadingHTTPServer(
295 ('127.0.0.1', 0), HTTPTestRequestHandler
)
296 cls
.http_port
= http_server_port(cls
.http_httpd
)
297 cls
.http_server_thread
= threading
.Thread(target
=cls
.http_httpd
.serve_forever
)
298 # FIXME: we should probably stop the http server thread after each test
299 # See: https://github.com/yt-dlp/yt-dlp/pull/7094#discussion_r1199746041
300 cls
.http_server_thread
.daemon
= True
301 cls
.http_server_thread
.start()
304 certfn
= os
.path
.join(TEST_DIR
, 'testcert.pem')
305 cls
.https_httpd
= http
.server
.ThreadingHTTPServer(
306 ('127.0.0.1', 0), HTTPTestRequestHandler
)
307 sslctx
= ssl
.SSLContext(ssl
.PROTOCOL_TLS_SERVER
)
308 sslctx
.load_cert_chain(certfn
, None)
309 cls
.https_httpd
.socket
= sslctx
.wrap_socket(cls
.https_httpd
.socket
, server_side
=True)
310 cls
.https_port
= http_server_port(cls
.https_httpd
)
311 cls
.https_server_thread
= threading
.Thread(target
=cls
.https_httpd
.serve_forever
)
312 cls
.https_server_thread
.daemon
= True
313 cls
.https_server_thread
.start()
316 @pytest.mark
.parametrize('handler', ['Urllib', 'Requests', 'CurlCFFI'], indirect
=True)
317 class TestHTTPRequestHandler(TestRequestHandlerBase
):
319 def test_verify_cert(self
, handler
):
320 with
handler() as rh
:
321 with pytest
.raises(CertificateVerifyError
):
322 validate_and_send(rh
, Request(f
'https://127.0.0.1:{self.https_port}/headers'))
324 with
handler(verify
=False) as rh
:
325 r
= validate_and_send(rh
, Request(f
'https://127.0.0.1:{self.https_port}/headers'))
326 assert r
.status
== 200
329 def test_ssl_error(self
, handler
):
330 # HTTPS server with too old TLS version
331 # XXX: is there a better way to test this than to create a new server?
332 https_httpd
= http
.server
.ThreadingHTTPServer(
333 ('127.0.0.1', 0), HTTPTestRequestHandler
)
334 sslctx
= ssl
.SSLContext(ssl
.PROTOCOL_TLS_SERVER
)
335 https_httpd
.socket
= sslctx
.wrap_socket(https_httpd
.socket
, server_side
=True)
336 https_port
= http_server_port(https_httpd
)
337 https_server_thread
= threading
.Thread(target
=https_httpd
.serve_forever
)
338 https_server_thread
.daemon
= True
339 https_server_thread
.start()
341 with
handler(verify
=False) as rh
:
342 with pytest
.raises(SSLError
, match
=r
'(?i)ssl(?:v3|/tls).alert.handshake.failure') as exc_info
:
343 validate_and_send(rh
, Request(f
'https://127.0.0.1:{https_port}/headers'))
344 assert not issubclass(exc_info
.type, CertificateVerifyError
)
346 @pytest.mark
.skip_handler('CurlCFFI', 'legacy_ssl ignored by CurlCFFI')
347 def test_legacy_ssl_extension(self
, handler
):
348 # HTTPS server with old ciphers
349 # XXX: is there a better way to test this than to create a new server?
350 https_httpd
= http
.server
.ThreadingHTTPServer(
351 ('127.0.0.1', 0), HTTPTestRequestHandler
)
352 sslctx
= ssl
.SSLContext(ssl
.PROTOCOL_TLS_SERVER
)
353 sslctx
.maximum_version
= ssl
.TLSVersion
.TLSv1_2
354 sslctx
.set_ciphers('SHA1:AESCCM:aDSS:eNULL:aNULL')
355 sslctx
.load_cert_chain(os
.path
.join(TEST_DIR
, 'testcert.pem'), None)
356 https_httpd
.socket
= sslctx
.wrap_socket(https_httpd
.socket
, server_side
=True)
357 https_port
= http_server_port(https_httpd
)
358 https_server_thread
= threading
.Thread(target
=https_httpd
.serve_forever
)
359 https_server_thread
.daemon
= True
360 https_server_thread
.start()
362 with
handler(verify
=False) as rh
:
363 res
= validate_and_send(rh
, Request(f
'https://127.0.0.1:{https_port}/headers', extensions
={'legacy_ssl': True}))
364 assert res
.status
== 200
367 # Ensure only applies to request extension
368 with pytest
.raises(SSLError
):
369 validate_and_send(rh
, Request(f
'https://127.0.0.1:{https_port}/headers'))
371 @pytest.mark
.skip_handler('CurlCFFI', 'legacy_ssl ignored by CurlCFFI')
372 def test_legacy_ssl_support(self
, handler
):
373 # HTTPS server with old ciphers
374 # XXX: is there a better way to test this than to create a new server?
375 https_httpd
= http
.server
.ThreadingHTTPServer(
376 ('127.0.0.1', 0), HTTPTestRequestHandler
)
377 sslctx
= ssl
.SSLContext(ssl
.PROTOCOL_TLS_SERVER
)
378 sslctx
.maximum_version
= ssl
.TLSVersion
.TLSv1_2
379 sslctx
.set_ciphers('SHA1:AESCCM:aDSS:eNULL:aNULL')
380 sslctx
.load_cert_chain(os
.path
.join(TEST_DIR
, 'testcert.pem'), None)
381 https_httpd
.socket
= sslctx
.wrap_socket(https_httpd
.socket
, server_side
=True)
382 https_port
= http_server_port(https_httpd
)
383 https_server_thread
= threading
.Thread(target
=https_httpd
.serve_forever
)
384 https_server_thread
.daemon
= True
385 https_server_thread
.start()
387 with
handler(verify
=False, legacy_ssl_support
=True) as rh
:
388 res
= validate_and_send(rh
, Request(f
'https://127.0.0.1:{https_port}/headers'))
389 assert res
.status
== 200
392 def test_percent_encode(self
, handler
):
393 with
handler() as rh
:
394 # Unicode characters should be encoded with uppercase percent-encoding
395 res
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/中文.html'))
396 assert res
.status
== 200
398 # don't normalize existing percent encodings
399 res
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/%c7%9f'))
400 assert res
.status
== 200
403 @pytest.mark
.parametrize('path', [
404 '/a/b/./../../headers',
405 '/redirect_dotsegments',
406 # https://github.com/yt-dlp/yt-dlp/issues/9020
407 '/redirect_dotsegments_absolute',
409 def test_remove_dot_segments(self
, handler
, path
):
410 with
handler(verbose
=True) as rh
:
411 # This isn't a comprehensive test,
412 # but it should be enough to check whether the handler is removing dot segments in required scenarios
413 res
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}{path}'))
414 assert res
.status
== 200
415 assert res
.url
== f
'http://127.0.0.1:{self.http_port}/headers'
418 @pytest.mark
.skip_handler('CurlCFFI', 'not supported by curl-cffi (non-standard)')
419 def test_unicode_path_redirection(self
, handler
):
420 with
handler() as rh
:
421 r
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/302-non-ascii-redirect'))
422 assert r
.url
== f
'http://127.0.0.1:{self.http_port}/%E4%B8%AD%E6%96%87.html'
425 def test_raise_http_error(self
, handler
):
426 with
handler() as rh
:
427 for bad_status
in (400, 500, 599, 302):
428 with pytest
.raises(HTTPError
):
429 validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/gen_{bad_status}'))
431 # Should not raise an error
432 validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/gen_200')).close()
434 def test_response_url(self
, handler
):
435 with
handler() as rh
:
436 # Response url should be that of the last url in redirect chain
437 res
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/redirect_301'))
438 assert res
.url
== f
'http://127.0.0.1:{self.http_port}/method'
440 res2
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/gen_200'))
441 assert res2
.url
== f
'http://127.0.0.1:{self.http_port}/gen_200'
444 # Covers some basic cases we expect some level of consistency between request handlers for
445 @pytest.mark
.parametrize('redirect_status,method,expected', [
446 # A 303 must either use GET or HEAD for subsequent request
447 (303, 'POST', ('', 'GET', False)),
448 (303, 'HEAD', ('', 'HEAD', False)),
450 # 301 and 302 turn POST only into a GET
451 (301, 'POST', ('', 'GET', False)),
452 (301, 'HEAD', ('', 'HEAD', False)),
453 (302, 'POST', ('', 'GET', False)),
454 (302, 'HEAD', ('', 'HEAD', False)),
456 # 307 and 308 should not change method
457 (307, 'POST', ('testdata', 'POST', True)),
458 (308, 'POST', ('testdata', 'POST', True)),
459 (307, 'HEAD', ('', 'HEAD', False)),
460 (308, 'HEAD', ('', 'HEAD', False)),
462 def test_redirect(self
, handler
, redirect_status
, method
, expected
):
463 with
handler() as rh
:
464 data
= b
'testdata' if method
== 'POST' else None
467 headers
['Content-Type'] = 'application/test'
468 res
= validate_and_send(
469 rh
, Request(f
'http://127.0.0.1:{self.http_port}/redirect_{redirect_status}', method
=method
, data
=data
,
475 data_recv
+= res
.read(len(data
))
476 if data_recv
!= data
:
480 headers
+= res
.read()
482 assert expected
[0] == data_recv
.decode()
483 assert expected
[1] == res
.headers
.get('method')
484 assert expected
[2] == ('content-length' in headers
.decode().lower())
486 def test_request_cookie_header(self
, handler
):
487 # We should accept a Cookie header being passed as in normal headers and handle it appropriately.
488 with
handler() as rh
:
489 # Specified Cookie header should be used
490 res
= validate_and_send(
492 f
'http://127.0.0.1:{self.http_port}/headers',
493 headers
={'Cookie': 'test=test'})).read().decode()
494 assert 'cookie: test=test' in res
.lower()
496 # Specified Cookie header should be removed on any redirect
497 res
= validate_and_send(
499 f
'http://127.0.0.1:{self.http_port}/308-to-headers',
500 headers
={'Cookie': 'test=test2'})).read().decode()
501 assert 'cookie: test=test2' not in res
.lower()
503 # Specified Cookie header should override global cookiejar for that request
504 # Whether cookies from the cookiejar is applied on the redirect is considered undefined for now
505 cookiejar
= YoutubeDLCookieJar()
506 cookiejar
.set_cookie(http
.cookiejar
.Cookie(
507 version
=0, name
='test', value
='ytdlp', port
=None, port_specified
=False,
508 domain
='127.0.0.1', domain_specified
=True, domain_initial_dot
=False, path
='/',
509 path_specified
=True, secure
=False, expires
=None, discard
=False, comment
=None,
510 comment_url
=None, rest
={}))
512 with
handler(cookiejar
=cookiejar
) as rh
:
513 data
= validate_and_send(
514 rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers', headers
={'cookie': 'test=test3'})).read()
515 assert b
'cookie: test=ytdlp' not in data
.lower()
516 assert b
'cookie: test=test3' in data
.lower()
518 def test_redirect_loop(self
, handler
):
519 with
handler() as rh
:
520 with pytest
.raises(HTTPError
, match
='redirect loop'):
521 validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/redirect_loop'))
523 def test_incompleteread(self
, handler
):
524 with
handler(timeout
=2) as rh
:
525 with pytest
.raises(IncompleteRead
, match
='13 bytes read, 234221 more expected'):
526 validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/incompleteread')).read()
528 def test_cookies(self
, handler
):
529 cookiejar
= YoutubeDLCookieJar()
530 cookiejar
.set_cookie(http
.cookiejar
.Cookie(
531 0, 'test', 'ytdlp', None, False, '127.0.0.1', True,
532 False, '/headers', True, False, None, False, None, None, {}))
534 with
handler(cookiejar
=cookiejar
) as rh
:
535 data
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers')).read()
536 assert b
'cookie: test=ytdlp' in data
.lower()
539 with
handler() as rh
:
540 data
= validate_and_send(
541 rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers', extensions
={'cookiejar': cookiejar
})).read()
542 assert b
'cookie: test=ytdlp' in data
.lower()
544 def test_cookie_sync_only_cookiejar(self
, handler
):
545 # Ensure that cookies are ONLY being handled by the cookiejar
546 with
handler() as rh
:
547 validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/get_cookie', extensions
={'cookiejar': YoutubeDLCookieJar()}))
548 data
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers', extensions
={'cookiejar': YoutubeDLCookieJar()})).read()
549 assert b
'cookie: test=ytdlp' not in data
.lower()
551 def test_cookie_sync_delete_cookie(self
, handler
):
552 # Ensure that cookies are ONLY being handled by the cookiejar
553 cookiejar
= YoutubeDLCookieJar()
554 with
handler(cookiejar
=cookiejar
) as rh
:
555 validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/get_cookie'))
556 data
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers')).read()
557 assert b
'cookie: test=ytdlp' in data
.lower()
558 cookiejar
.clear_session_cookies()
559 data
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers')).read()
560 assert b
'cookie: test=ytdlp' not in data
.lower()
562 def test_headers(self
, handler
):
564 with
handler(headers
=HTTPHeaderDict({'test1': 'test', 'test2': 'test2'})) as rh
:
566 data
= validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers')).read().lower()
567 assert b
'test1: test' in data
569 # Per request headers, merged with global
570 data
= validate_and_send(rh
, Request(
571 f
'http://127.0.0.1:{self.http_port}/headers', headers
={'test2': 'changed', 'test3': 'test3'})).read().lower()
572 assert b
'test1: test' in data
573 assert b
'test2: changed' in data
574 assert b
'test2: test2' not in data
575 assert b
'test3: test3' in data
577 def test_read_timeout(self
, handler
):
578 with
handler() as rh
:
579 # Default timeout is 20 seconds, so this should go through
581 rh
, Request(f
'http://127.0.0.1:{self.http_port}/timeout_1'))
583 with
handler(timeout
=0.1) as rh
:
584 with pytest
.raises(TransportError
):
586 rh
, Request(f
'http://127.0.0.1:{self.http_port}/timeout_5'))
588 # Per request timeout, should override handler timeout
590 rh
, Request(f
'http://127.0.0.1:{self.http_port}/timeout_1', extensions
={'timeout': 4}))
592 def test_connect_timeout(self
, handler
):
593 # nothing should be listening on this port
594 connect_timeout_url
= 'http://10.255.255.255'
595 with
handler(timeout
=0.01) as rh
, pytest
.raises(TransportError
):
597 validate_and_send(rh
, Request(connect_timeout_url
))
598 assert time
.time() - now
< DEFAULT_TIMEOUT
600 # Per request timeout, should override handler timeout
601 request
= Request(connect_timeout_url
, extensions
={'timeout': 0.01})
602 with
handler() as rh
, pytest
.raises(TransportError
):
604 validate_and_send(rh
, request
)
605 assert time
.time() - now
< DEFAULT_TIMEOUT
607 def test_source_address(self
, handler
):
608 source_address
= f
'127.0.0.{random.randint(5, 255)}'
609 # on some systems these loopback addresses we need for testing may not be available
610 # see: https://github.com/yt-dlp/yt-dlp/issues/8890
611 verify_address_availability(source_address
)
612 with
handler(source_address
=source_address
) as rh
:
613 data
= validate_and_send(
614 rh
, Request(f
'http://127.0.0.1:{self.http_port}/source_address')).read().decode()
615 assert source_address
== data
617 # Not supported by CurlCFFI
618 @pytest.mark
.skip_handler('CurlCFFI', 'not supported by curl-cffi')
619 def test_gzip_trailing_garbage(self
, handler
):
620 with
handler() as rh
:
621 data
= validate_and_send(rh
, Request(f
'http://localhost:{self.http_port}/trailing_garbage')).read().decode()
622 assert data
== '<html><video src="/vid.mp4" /></html>'
624 @pytest.mark
.skip_handler('CurlCFFI', 'not applicable to curl-cffi')
625 @pytest.mark
.skipif(not brotli
, reason
='brotli support is not installed')
626 def test_brotli(self
, handler
):
627 with
handler() as rh
:
628 res
= validate_and_send(
630 f
'http://127.0.0.1:{self.http_port}/content-encoding',
631 headers
={'ytdl-encoding': 'br'}))
632 assert res
.headers
.get('Content-Encoding') == 'br'
633 assert res
.read() == b
'<html><video src="/vid.mp4" /></html>'
635 def test_deflate(self
, handler
):
636 with
handler() as rh
:
637 res
= validate_and_send(
639 f
'http://127.0.0.1:{self.http_port}/content-encoding',
640 headers
={'ytdl-encoding': 'deflate'}))
641 assert res
.headers
.get('Content-Encoding') == 'deflate'
642 assert res
.read() == b
'<html><video src="/vid.mp4" /></html>'
644 def test_gzip(self
, handler
):
645 with
handler() as rh
:
646 res
= validate_and_send(
648 f
'http://127.0.0.1:{self.http_port}/content-encoding',
649 headers
={'ytdl-encoding': 'gzip'}))
650 assert res
.headers
.get('Content-Encoding') == 'gzip'
651 assert res
.read() == b
'<html><video src="/vid.mp4" /></html>'
653 def test_multiple_encodings(self
, handler
):
654 with
handler() as rh
:
655 for pair
in ('gzip,deflate', 'deflate, gzip', 'gzip, gzip', 'deflate, deflate'):
656 res
= validate_and_send(
658 f
'http://127.0.0.1:{self.http_port}/content-encoding',
659 headers
={'ytdl-encoding': pair
}))
660 assert res
.headers
.get('Content-Encoding') == pair
661 assert res
.read() == b
'<html><video src="/vid.mp4" /></html>'
663 @pytest.mark
.skip_handler('CurlCFFI', 'not supported by curl-cffi')
664 def test_unsupported_encoding(self
, handler
):
665 with
handler() as rh
:
666 res
= validate_and_send(
668 f
'http://127.0.0.1:{self.http_port}/content-encoding',
669 headers
={'ytdl-encoding': 'unsupported', 'Accept-Encoding': '*'}))
670 assert res
.headers
.get('Content-Encoding') == 'unsupported'
671 assert res
.read() == b
'raw'
673 def test_read(self
, handler
):
674 with
handler() as rh
:
675 res
= validate_and_send(
676 rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers'))
677 assert res
.readable()
678 assert res
.read(1) == b
'H'
679 assert res
.read(3) == b
'ost'
680 assert res
.read().decode().endswith('\n\n')
681 assert res
.read() == b
''
683 def test_request_disable_proxy(self
, handler
):
684 for proxy_proto
in handler
._SUPPORTED
_PROXY
_SCHEMES
or ['http']:
685 # Given the handler is configured with a proxy
686 with
handler(proxies
={'http': f
'{proxy_proto}://10.255.255.255'}, timeout
=5) as rh
:
687 # When a proxy is explicitly set to None for the request
688 res
= validate_and_send(
689 rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers', proxies
={'http': None}))
690 # Then no proxy should be used
692 assert res
.status
== 200
694 @pytest.mark
.skip_handlers_if(
695 lambda _
, handler
: Features
.NO_PROXY
not in handler
._SUPPORTED
_FEATURES
, 'handler does not support NO_PROXY')
696 def test_noproxy(self
, handler
):
697 for proxy_proto
in handler
._SUPPORTED
_PROXY
_SCHEMES
or ['http']:
698 # Given the handler is configured with a proxy
699 with
handler(proxies
={'http': f
'{proxy_proto}://10.255.255.255'}, timeout
=5) as rh
:
700 for no_proxy
in (f
'127.0.0.1:{self.http_port}', '127.0.0.1', 'localhost'):
701 # When request no proxy includes the request url host
702 nop_response
= validate_and_send(
703 rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers', proxies
={'no': no_proxy
}))
704 # Then the proxy should not be used
705 assert nop_response
.status
== 200
708 @pytest.mark
.skip_handlers_if(
709 lambda _
, handler
: Features
.ALL_PROXY
not in handler
._SUPPORTED
_FEATURES
, 'handler does not support ALL_PROXY')
710 def test_allproxy(self
, handler
):
711 # This is a bit of a hacky test, but it should be enough to check whether the handler is using the proxy.
712 # 0.1s might not be enough of a timeout if proxy is not used in all cases, but should still get failures.
713 with
handler(proxies
={'all': 'http://10.255.255.255'}, timeout
=0.1) as rh
:
714 with pytest
.raises(TransportError
):
715 validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers')).close()
717 with
handler(timeout
=0.1) as rh
:
718 with pytest
.raises(TransportError
):
721 f
'http://127.0.0.1:{self.http_port}/headers', proxies
={'all': 'http://10.255.255.255'})).close()
724 @pytest.mark
.parametrize('handler', ['Urllib', 'Requests', 'CurlCFFI'], indirect
=True)
725 class TestClientCertificate
:
727 def setup_class(cls
):
728 certfn
= os
.path
.join(TEST_DIR
, 'testcert.pem')
729 cls
.certdir
= os
.path
.join(TEST_DIR
, 'testdata', 'certificate')
730 cacertfn
= os
.path
.join(cls
.certdir
, 'ca.crt')
731 cls
.httpd
= http
.server
.ThreadingHTTPServer(('127.0.0.1', 0), HTTPTestRequestHandler
)
732 sslctx
= ssl
.SSLContext(ssl
.PROTOCOL_TLS_SERVER
)
733 sslctx
.verify_mode
= ssl
.CERT_REQUIRED
734 sslctx
.load_verify_locations(cafile
=cacertfn
)
735 sslctx
.load_cert_chain(certfn
, None)
736 cls
.httpd
.socket
= sslctx
.wrap_socket(cls
.httpd
.socket
, server_side
=True)
737 cls
.port
= http_server_port(cls
.httpd
)
738 cls
.server_thread
= threading
.Thread(target
=cls
.httpd
.serve_forever
)
739 cls
.server_thread
.daemon
= True
740 cls
.server_thread
.start()
742 def _run_test(self
, handler
, **handler_kwargs
):
744 # Disable client-side validation of unacceptable self-signed testcert.pem
745 # The test is of a check on the server side, so unaffected
749 validate_and_send(rh
, Request(f
'https://127.0.0.1:{self.port}/video.html')).read().decode()
751 def test_certificate_combined_nopass(self
, handler
):
752 self
._run
_test
(handler
, client_cert
={
753 'client_certificate': os
.path
.join(self
.certdir
, 'clientwithkey.crt'),
756 def test_certificate_nocombined_nopass(self
, handler
):
757 self
._run
_test
(handler
, client_cert
={
758 'client_certificate': os
.path
.join(self
.certdir
, 'client.crt'),
759 'client_certificate_key': os
.path
.join(self
.certdir
, 'client.key'),
762 def test_certificate_combined_pass(self
, handler
):
763 self
._run
_test
(handler
, client_cert
={
764 'client_certificate': os
.path
.join(self
.certdir
, 'clientwithencryptedkey.crt'),
765 'client_certificate_password': 'foobar',
768 def test_certificate_nocombined_pass(self
, handler
):
769 self
._run
_test
(handler
, client_cert
={
770 'client_certificate': os
.path
.join(self
.certdir
, 'client.crt'),
771 'client_certificate_key': os
.path
.join(self
.certdir
, 'clientencrypted.key'),
772 'client_certificate_password': 'foobar',
776 @pytest.mark
.parametrize('handler', ['CurlCFFI'], indirect
=True)
777 class TestHTTPImpersonateRequestHandler(TestRequestHandlerBase
):
778 def test_supported_impersonate_targets(self
, handler
):
779 with
handler(headers
=std_headers
) as rh
:
780 # note: this assumes the impersonate request handler supports the impersonate extension
781 for target
in rh
.supported_targets
:
782 res
= validate_and_send(rh
, Request(
783 f
'http://127.0.0.1:{self.http_port}/headers', extensions
={'impersonate': target
}))
784 assert res
.status
== 200
785 assert std_headers
['user-agent'].lower() not in res
.read().decode().lower()
787 def test_response_extensions(self
, handler
):
788 with
handler() as rh
:
789 for target
in rh
.supported_targets
:
791 f
'http://127.0.0.1:{self.http_port}/gen_200', extensions
={'impersonate': target
})
792 res
= validate_and_send(rh
, request
)
793 assert res
.extensions
['impersonate'] == rh
._get
_request
_target
(request
)
795 def test_http_error_response_extensions(self
, handler
):
796 with
handler() as rh
:
797 for target
in rh
.supported_targets
:
799 f
'http://127.0.0.1:{self.http_port}/gen_404', extensions
={'impersonate': target
})
801 validate_and_send(rh
, request
)
802 except HTTPError
as e
:
804 assert res
.extensions
['impersonate'] == rh
._get
_request
_target
(request
)
807 class TestRequestHandlerMisc
:
808 """Misc generic tests for request handlers, not related to request or validation testing"""
809 @pytest.mark
.parametrize('handler,logger_name', [
810 ('Requests', 'urllib3'),
811 ('Websockets', 'websockets.client'),
812 ('Websockets', 'websockets.server'),
813 ], indirect
=['handler'])
814 def test_remove_logging_handler(self
, handler
, logger_name
):
815 # Ensure any logging handlers, which may contain a YoutubeDL instance,
816 # are removed when we close the request handler
817 # See: https://github.com/yt-dlp/yt-dlp/issues/8922
818 logging_handlers
= logging
.getLogger(logger_name
).handlers
819 before_count
= len(logging_handlers
)
821 assert len(logging_handlers
) == before_count
+ 1
823 assert len(logging_handlers
) == before_count
826 @pytest.mark
.parametrize('handler', ['Urllib'], indirect
=True)
827 class TestUrllibRequestHandler(TestRequestHandlerBase
):
828 def test_file_urls(self
, handler
):
829 # See https://github.com/ytdl-org/youtube-dl/issues/8227
830 tf
= tempfile
.NamedTemporaryFile(delete
=False)
833 req
= Request(pathlib
.Path(tf
.name
).as_uri())
834 with
handler() as rh
:
835 with pytest
.raises(UnsupportedRequest
):
838 # Test that urllib never loaded FileHandler
839 with pytest
.raises(TransportError
):
842 with
handler(enable_file_urls
=True) as rh
:
843 res
= validate_and_send(rh
, req
)
844 assert res
.read() == b
'foobar'
849 def test_http_error_returns_content(self
, handler
):
850 # urllib HTTPError will try close the underlying response if reference to the HTTPError object is lost
852 with
handler() as rh
:
855 validate_and_send(rh
, Request(f
'http://127.0.0.1:{self.http_port}/gen_404'))
856 except HTTPError
as e
:
859 assert get_response().read() == b
'<html></html>'
861 def test_verify_cert_error_text(self
, handler
):
862 # Check the output of the error message
863 with
handler() as rh
:
865 CertificateVerifyError
,
866 match
=r
'\[SSL: CERTIFICATE_VERIFY_FAILED\] certificate verify failed: self.signed certificate',
868 validate_and_send(rh
, Request(f
'https://127.0.0.1:{self.https_port}/headers'))
870 @pytest.mark
.parametrize('req,match,version_check', [
871 # https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1256
872 # bpo-39603: Check implemented in 3.7.9+, 3.8.5+
874 Request('http://127.0.0.1', method
='GET\n'),
875 'method can\'t contain control characters',
876 lambda v
: v
< (3, 7, 9) or (3, 8, 0) <= v
< (3, 8, 5),
878 # https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1265
879 # bpo-38576: Check implemented in 3.7.8+, 3.8.3+
881 Request('http://127.0.0. 1', method
='GET'),
882 'URL can\'t contain control characters',
883 lambda v
: v
< (3, 7, 8) or (3, 8, 0) <= v
< (3, 8, 3),
885 # https://github.com/python/cpython/blob/987b712b4aeeece336eed24fcc87a950a756c3e2/Lib/http/client.py#L1288C31-L1288C50
886 (Request('http://127.0.0.1', headers
={'foo\n': 'bar'}), 'Invalid header name', None),
888 def test_httplib_validation_errors(self
, handler
, req
, match
, version_check
):
889 if version_check
and version_check(sys
.version_info
):
890 pytest
.skip(f
'Python {sys.version} version does not have the required validation for this test.')
892 with
handler() as rh
:
893 with pytest
.raises(RequestError
, match
=match
) as exc_info
:
894 validate_and_send(rh
, req
)
895 assert not isinstance(exc_info
.value
, TransportError
)
898 @pytest.mark
.parametrize('handler', ['Requests'], indirect
=True)
899 class TestRequestsRequestHandler(TestRequestHandlerBase
):
900 @pytest.mark
.parametrize('raised,expected', [
901 (lambda: requests
.exceptions
.ConnectTimeout(), TransportError
),
902 (lambda: requests
.exceptions
.ReadTimeout(), TransportError
),
903 (lambda: requests
.exceptions
.Timeout(), TransportError
),
904 (lambda: requests
.exceptions
.ConnectionError(), TransportError
),
905 (lambda: requests
.exceptions
.ProxyError(), ProxyError
),
906 (lambda: requests
.exceptions
.SSLError('12[CERTIFICATE_VERIFY_FAILED]34'), CertificateVerifyError
),
907 (lambda: requests
.exceptions
.SSLError(), SSLError
),
908 (lambda: requests
.exceptions
.InvalidURL(), RequestError
),
909 (lambda: requests
.exceptions
.InvalidHeader(), RequestError
),
910 # catch-all: https://github.com/psf/requests/blob/main/src/requests/adapters.py#L535
911 (lambda: urllib3
.exceptions
.HTTPError(), TransportError
),
912 (lambda: requests
.exceptions
.RequestException(), RequestError
),
913 # (lambda: requests.exceptions.TooManyRedirects(), HTTPError) - Needs a response object
915 def test_request_error_mapping(self
, handler
, monkeypatch
, raised
, expected
):
916 with
handler() as rh
:
917 def mock_get_instance(*args
, **kwargs
):
919 def request(self
, *args
, **kwargs
):
923 monkeypatch
.setattr(rh
, '_get_instance', mock_get_instance
)
925 with pytest
.raises(expected
) as exc_info
:
926 rh
.send(Request('http://fake'))
928 assert exc_info
.type is expected
930 @pytest.mark
.parametrize('raised,expected,match', [
931 (lambda: urllib3
.exceptions
.SSLError(), SSLError
, None),
932 (lambda: urllib3
.exceptions
.TimeoutError(), TransportError
, None),
933 (lambda: urllib3
.exceptions
.ReadTimeoutError(None, None, None), TransportError
, None),
934 (lambda: urllib3
.exceptions
.ProtocolError(), TransportError
, None),
935 (lambda: urllib3
.exceptions
.DecodeError(), TransportError
, None),
936 (lambda: urllib3
.exceptions
.HTTPError(), TransportError
, None), # catch-all
938 lambda: urllib3
.exceptions
.ProtocolError('error', http
.client
.IncompleteRead(partial
=b
'abc', expected
=4)),
940 '3 bytes read, 4 more expected',
943 lambda: urllib3
.exceptions
.ProtocolError('error', urllib3
.exceptions
.IncompleteRead(partial
=3, expected
=5)),
945 '3 bytes read, 5 more expected',
948 def test_response_error_mapping(self
, handler
, monkeypatch
, raised
, expected
, match
):
949 from requests
.models
import Response
as RequestsResponse
950 from urllib3
.response
import HTTPResponse
as Urllib3Response
952 from yt_dlp
.networking
._requests
import RequestsResponseAdapter
953 requests_res
= RequestsResponse()
954 requests_res
.raw
= Urllib3Response(body
=b
'', status
=200)
955 res
= RequestsResponseAdapter(requests_res
)
957 def mock_read(*args
, **kwargs
):
959 monkeypatch
.setattr(res
.fp
, 'read', mock_read
)
961 with pytest
.raises(expected
, match
=match
) as exc_info
:
964 assert exc_info
.type is expected
966 def test_close(self
, handler
, monkeypatch
):
968 session
= rh
._get
_instance
(cookiejar
=rh
.cookiejar
)
970 original_close
= session
.close
972 def mock_close(*args
, **kwargs
):
975 return original_close(*args
, **kwargs
)
977 monkeypatch
.setattr(session
, 'close', mock_close
)
982 @pytest.mark
.parametrize('handler', ['CurlCFFI'], indirect
=True)
983 class TestCurlCFFIRequestHandler(TestRequestHandlerBase
):
985 @pytest.mark
.parametrize('params,extensions', [
986 ({'impersonate': ImpersonateTarget('chrome', '110')}, {}),
987 ({'impersonate': ImpersonateTarget('chrome', '99')}, {'impersonate': ImpersonateTarget('chrome', '110')}),
989 def test_impersonate(self
, handler
, params
, extensions
):
990 with
handler(headers
=std_headers
, **params
) as rh
:
991 res
= validate_and_send(
992 rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers', extensions
=extensions
)).read().decode()
993 assert 'sec-ch-ua: "Chromium";v="110"' in res
994 # Check that user agent is added over ours
995 assert 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' in res
997 def test_headers(self
, handler
):
998 with
handler(headers
=std_headers
) as rh
:
999 # Ensure curl-impersonate overrides our standard headers (usually added
1000 res
= validate_and_send(
1001 rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers', extensions
={
1002 'impersonate': ImpersonateTarget('safari')}, headers
={'x-custom': 'test', 'sec-fetch-mode': 'custom'})).read().decode().lower()
1004 assert std_headers
['user-agent'].lower() not in res
1005 assert std_headers
['accept-language'].lower() not in res
1006 assert std_headers
['sec-fetch-mode'].lower() not in res
1007 # other than UA, custom headers that differ from std_headers should be kept
1008 assert 'sec-fetch-mode: custom' in res
1009 assert 'x-custom: test' in res
1010 # but when not impersonating don't remove std_headers
1011 res
= validate_and_send(
1012 rh
, Request(f
'http://127.0.0.1:{self.http_port}/headers', headers
={'x-custom': 'test'})).read().decode().lower()
1013 # std_headers should be present
1014 for k
, v
in std_headers
.items():
1015 assert f
'{k}: {v}'.lower() in res
1017 @pytest.mark
.parametrize('raised,expected,match', [
1018 (lambda: curl_cffi
.requests
.errors
.RequestsError(
1019 '', code
=curl_cffi
.const
.CurlECode
.PARTIAL_FILE
), IncompleteRead
, None),
1020 (lambda: curl_cffi
.requests
.errors
.RequestsError(
1021 '', code
=curl_cffi
.const
.CurlECode
.OPERATION_TIMEDOUT
), TransportError
, None),
1022 (lambda: curl_cffi
.requests
.errors
.RequestsError(
1023 '', code
=curl_cffi
.const
.CurlECode
.RECV_ERROR
), TransportError
, None),
1025 def test_response_error_mapping(self
, handler
, monkeypatch
, raised
, expected
, match
):
1026 import curl_cffi
.requests
1028 from yt_dlp
.networking
._curlcffi
import CurlCFFIResponseAdapter
1029 curl_res
= curl_cffi
.requests
.Response()
1030 res
= CurlCFFIResponseAdapter(curl_res
)
1032 def mock_read(*args
, **kwargs
):
1035 except Exception as e
:
1036 e
.response
= curl_res
1038 monkeypatch
.setattr(res
.fp
, 'read', mock_read
)
1040 with pytest
.raises(expected
, match
=match
) as exc_info
:
1043 assert exc_info
.type is expected
1045 @pytest.mark
.parametrize('raised,expected,match', [
1046 (lambda: curl_cffi
.requests
.errors
.RequestsError(
1047 '', code
=curl_cffi
.const
.CurlECode
.OPERATION_TIMEDOUT
), TransportError
, None),
1048 (lambda: curl_cffi
.requests
.errors
.RequestsError(
1049 '', code
=curl_cffi
.const
.CurlECode
.PEER_FAILED_VERIFICATION
), CertificateVerifyError
, None),
1050 (lambda: curl_cffi
.requests
.errors
.RequestsError(
1051 '', code
=curl_cffi
.const
.CurlECode
.SSL_CONNECT_ERROR
), SSLError
, None),
1052 (lambda: curl_cffi
.requests
.errors
.RequestsError(
1053 '', code
=curl_cffi
.const
.CurlECode
.TOO_MANY_REDIRECTS
), HTTPError
, None),
1054 (lambda: curl_cffi
.requests
.errors
.RequestsError(
1055 '', code
=curl_cffi
.const
.CurlECode
.PROXY
), ProxyError
, None),
1057 def test_request_error_mapping(self
, handler
, monkeypatch
, raised
, expected
, match
):
1058 import curl_cffi
.requests
1059 curl_res
= curl_cffi
.requests
.Response()
1060 curl_res
.status_code
= 301
1062 with
handler() as rh
:
1063 original_get_instance
= rh
._get
_instance
1065 def mock_get_instance(*args
, **kwargs
):
1066 instance
= original_get_instance(*args
, **kwargs
)
1068 def request(*_
, **__
):
1071 except Exception as e
:
1072 e
.response
= curl_res
1074 monkeypatch
.setattr(instance
, 'request', request
)
1077 monkeypatch
.setattr(rh
, '_get_instance', mock_get_instance
)
1079 with pytest
.raises(expected
) as exc_info
:
1080 rh
.send(Request('http://fake'))
1082 assert exc_info
.type is expected
1084 def test_response_reader(self
, handler
):
1086 def __init__(self
, raise_error
=False):
1087 self
.raise_error
= raise_error
1090 def iter_content(self
):
1094 if self
.raise_error
:
1095 raise Exception('test')
1100 from yt_dlp
.networking
._curlcffi
import CurlCFFIResponseReader
1102 res
= CurlCFFIResponseReader(FakeResponse())
1104 assert res
.bytes_read
== 0
1105 assert res
.read(1) == b
'f'
1106 assert res
.bytes_read
== 3
1107 assert res
._buffer
== b
'oo'
1109 assert res
.read(2) == b
'oo'
1110 assert res
.bytes_read
== 3
1111 assert res
._buffer
== b
''
1113 assert res
.read(2) == b
'ba'
1114 assert res
.bytes_read
== 6
1115 assert res
._buffer
== b
'r'
1117 assert res
.read(3) == b
'rz'
1118 assert res
.bytes_read
== 7
1119 assert res
._buffer
== b
''
1121 assert res
._response
.closed
1123 # should handle no size param
1124 res2
= CurlCFFIResponseReader(FakeResponse())
1125 assert res2
.read() == b
'foobarz'
1126 assert res2
.bytes_read
== 7
1127 assert res2
._buffer
== b
''
1130 # should close on an exception
1131 res3
= CurlCFFIResponseReader(FakeResponse(raise_error
=True))
1132 with pytest
.raises(Exception, match
='test'):
1134 assert res3
._buffer
== b
''
1135 assert res3
.bytes_read
== 7
1138 # buffer should be cleared on close
1139 res4
= CurlCFFIResponseReader(FakeResponse())
1141 assert res4
._buffer
== b
'o'
1144 assert res4
._buffer
== b
''
1147 def run_validation(handler
, error
, req
, **handler_kwargs
):
1148 with
handler(**handler_kwargs
) as rh
:
1150 with pytest
.raises(error
):
1156 class TestRequestHandlerValidation
:
1158 class ValidationRH(RequestHandler
):
1159 def _send(self
, request
):
1160 raise RequestError('test')
1162 class NoCheckRH(ValidationRH
):
1163 _SUPPORTED_FEATURES
= None
1164 _SUPPORTED_PROXY_SCHEMES
= None
1165 _SUPPORTED_URL_SCHEMES
= None
1167 def _check_extensions(self
, extensions
):
1170 class HTTPSupportedRH(ValidationRH
):
1171 _SUPPORTED_URL_SCHEMES
= ('http',)
1173 URL_SCHEME_TESTS
= [
1174 # scheme, expected to fail, handler kwargs
1176 ('http', False, {}),
1177 ('https', False, {}),
1178 ('data', False, {}),
1180 ('file', UnsupportedRequest
, {}),
1181 ('file', False, {'enable_file_urls': True}),
1184 ('http', False, {}),
1185 ('https', False, {}),
1192 ('http', False, {}),
1193 ('https', False, {}),
1195 (NoCheckRH
, [('http', False, {})]),
1196 (ValidationRH
, [('http', UnsupportedRequest
, {})]),
1199 PROXY_SCHEME_TESTS
= [
1200 # proxy scheme, expected to fail
1201 ('Urllib', 'http', [
1203 ('https', UnsupportedRequest
),
1208 ('socks', UnsupportedRequest
),
1210 ('Requests', 'http', [
1218 ('CurlCFFI', 'http', [
1226 ('Websockets', 'ws', [
1227 ('http', UnsupportedRequest
),
1228 ('https', UnsupportedRequest
),
1234 (NoCheckRH
, 'http', [('http', False)]),
1235 (HTTPSupportedRH
, 'http', [('http', UnsupportedRequest
)]),
1236 (NoCheckRH
, 'http', [('http', False)]),
1237 (HTTPSupportedRH
, 'http', [('http', UnsupportedRequest
)]),
1241 # proxy key, proxy scheme, expected to fail
1242 ('Urllib', 'http', [
1243 ('all', 'http', False),
1244 ('unrelated', 'http', False),
1246 ('Requests', 'http', [
1247 ('all', 'http', False),
1248 ('unrelated', 'http', False),
1250 ('CurlCFFI', 'http', [
1251 ('all', 'http', False),
1252 ('unrelated', 'http', False),
1254 ('Websockets', 'ws', [
1255 ('all', 'socks5', False),
1256 ('unrelated', 'socks5', False),
1258 (NoCheckRH
, 'http', [('all', 'http', False)]),
1259 (HTTPSupportedRH
, 'http', [('all', 'http', UnsupportedRequest
)]),
1260 (HTTPSupportedRH
, 'http', [('no', 'http', UnsupportedRequest
)]),
1264 ('Urllib', 'http', [
1265 ({'cookiejar': 'notacookiejar'}, AssertionError),
1266 ({'cookiejar': YoutubeDLCookieJar()}, False),
1267 ({'cookiejar': CookieJar()}, AssertionError),
1268 ({'timeout': 1}, False),
1269 ({'timeout': 'notatimeout'}, AssertionError),
1270 ({'unsupported': 'value'}, UnsupportedRequest
),
1271 ({'legacy_ssl': False}, False),
1272 ({'legacy_ssl': True}, False),
1273 ({'legacy_ssl': 'notabool'}, AssertionError),
1275 ('Requests', 'http', [
1276 ({'cookiejar': 'notacookiejar'}, AssertionError),
1277 ({'cookiejar': YoutubeDLCookieJar()}, False),
1278 ({'timeout': 1}, False),
1279 ({'timeout': 'notatimeout'}, AssertionError),
1280 ({'unsupported': 'value'}, UnsupportedRequest
),
1281 ({'legacy_ssl': False}, False),
1282 ({'legacy_ssl': True}, False),
1283 ({'legacy_ssl': 'notabool'}, AssertionError),
1285 ('CurlCFFI', 'http', [
1286 ({'cookiejar': 'notacookiejar'}, AssertionError),
1287 ({'cookiejar': YoutubeDLCookieJar()}, False),
1288 ({'timeout': 1}, False),
1289 ({'timeout': 'notatimeout'}, AssertionError),
1290 ({'unsupported': 'value'}, UnsupportedRequest
),
1291 ({'impersonate': ImpersonateTarget('badtarget', None, None, None)}, UnsupportedRequest
),
1292 ({'impersonate': 123}, AssertionError),
1293 ({'impersonate': ImpersonateTarget('chrome', None, None, None)}, False),
1294 ({'impersonate': ImpersonateTarget(None, None, None, None)}, False),
1295 ({'impersonate': ImpersonateTarget()}, False),
1296 ({'impersonate': 'chrome'}, AssertionError),
1297 ({'legacy_ssl': False}, False),
1298 ({'legacy_ssl': True}, False),
1299 ({'legacy_ssl': 'notabool'}, AssertionError),
1301 (NoCheckRH
, 'http', [
1302 ({'cookiejar': 'notacookiejar'}, False),
1303 ({'somerandom': 'test'}, False), # but any extension is allowed through
1305 ('Websockets', 'ws', [
1306 ({'cookiejar': YoutubeDLCookieJar()}, False),
1307 ({'timeout': 2}, False),
1308 ({'legacy_ssl': False}, False),
1309 ({'legacy_ssl': True}, False),
1310 ({'legacy_ssl': 'notabool'}, AssertionError),
1314 @pytest.mark
.parametrize('handler,fail,scheme', [
1315 ('Urllib', False, 'http'),
1316 ('Requests', False, 'http'),
1317 ('CurlCFFI', False, 'http'),
1318 ('Websockets', False, 'ws'),
1319 ], indirect
=['handler'])
1320 def test_no_proxy(self
, handler
, fail
, scheme
):
1321 run_validation(handler
, fail
, Request(f
'{scheme}://', proxies
={'no': '127.0.0.1,github.com'}))
1322 run_validation(handler
, fail
, Request(f
'{scheme}://'), proxies
={'no': '127.0.0.1,github.com'})
1324 @pytest.mark
.parametrize('handler,scheme', [
1326 (HTTPSupportedRH
, 'http'),
1327 ('Requests', 'http'),
1328 ('CurlCFFI', 'http'),
1329 ('Websockets', 'ws'),
1330 ], indirect
=['handler'])
1331 def test_empty_proxy(self
, handler
, scheme
):
1332 run_validation(handler
, False, Request(f
'{scheme}://', proxies
={scheme
: None}))
1333 run_validation(handler
, False, Request(f
'{scheme}://'), proxies
={scheme
: None})
1335 @pytest.mark
.parametrize('proxy_url', ['//example.com', 'example.com', '127.0.0.1', '/a/b/c'])
1336 @pytest.mark
.parametrize('handler,scheme', [
1338 (HTTPSupportedRH
, 'http'),
1339 ('Requests', 'http'),
1340 ('CurlCFFI', 'http'),
1341 ('Websockets', 'ws'),
1342 ], indirect
=['handler'])
1343 def test_invalid_proxy_url(self
, handler
, scheme
, proxy_url
):
1344 run_validation(handler
, UnsupportedRequest
, Request(f
'{scheme}://', proxies
={scheme
: proxy_url
}))
1346 @pytest.mark
.parametrize('handler,scheme,fail,handler_kwargs', [
1347 (handler_tests
[0], scheme
, fail
, handler_kwargs
)
1348 for handler_tests
in URL_SCHEME_TESTS
1349 for scheme
, fail
, handler_kwargs
in handler_tests
[1]
1350 ], indirect
=['handler'])
1351 def test_url_scheme(self
, handler
, scheme
, fail
, handler_kwargs
):
1352 run_validation(handler
, fail
, Request(f
'{scheme}://'), **(handler_kwargs
or {}))
1354 @pytest.mark
.parametrize('handler,scheme,proxy_key,proxy_scheme,fail', [
1355 (handler_tests
[0], handler_tests
[1], proxy_key
, proxy_scheme
, fail
)
1356 for handler_tests
in PROXY_KEY_TESTS
1357 for proxy_key
, proxy_scheme
, fail
in handler_tests
[2]
1358 ], indirect
=['handler'])
1359 def test_proxy_key(self
, handler
, scheme
, proxy_key
, proxy_scheme
, fail
):
1360 run_validation(handler
, fail
, Request(f
'{scheme}://', proxies
={proxy_key
: f
'{proxy_scheme}://example.com'}))
1361 run_validation(handler
, fail
, Request(f
'{scheme}://'), proxies
={proxy_key
: f
'{proxy_scheme}://example.com'})
1363 @pytest.mark
.parametrize('handler,req_scheme,scheme,fail', [
1364 (handler_tests
[0], handler_tests
[1], scheme
, fail
)
1365 for handler_tests
in PROXY_SCHEME_TESTS
1366 for scheme
, fail
in handler_tests
[2]
1367 ], indirect
=['handler'])
1368 def test_proxy_scheme(self
, handler
, req_scheme
, scheme
, fail
):
1369 run_validation(handler
, fail
, Request(f
'{req_scheme}://', proxies
={req_scheme
: f
'{scheme}://example.com'}))
1370 run_validation(handler
, fail
, Request(f
'{req_scheme}://'), proxies
={req_scheme
: f
'{scheme}://example.com'})
1372 @pytest.mark
.parametrize('handler,scheme,extensions,fail', [
1373 (handler_tests
[0], handler_tests
[1], extensions
, fail
)
1374 for handler_tests
in EXTENSION_TESTS
1375 for extensions
, fail
in handler_tests
[2]
1376 ], indirect
=['handler'])
1377 def test_extension(self
, handler
, scheme
, extensions
, fail
):
1379 handler
, fail
, Request(f
'{scheme}://', extensions
=extensions
))
1381 def test_invalid_request_type(self
):
1382 rh
= self
.ValidationRH(logger
=FakeLogger())
1383 for method
in (rh
.validate
, rh
.send
):
1384 with pytest
.raises(TypeError, match
='Expected an instance of Request'):
1385 method('not a request')
1388 class FakeResponse(Response
):
1389 def __init__(self
, request
):
1390 # XXX: we could make request part of standard response interface
1391 self
.request
= request
1392 super().__init
__(fp
=io
.BytesIO(b
''), headers
={}, url
=request
.url
)
1395 class FakeRH(RequestHandler
):
1397 def __init__(self
, *args
, **params
):
1398 self
.params
= params
1399 super().__init
__(*args
, **params
)
1401 def _validate(self
, request
):
1404 def _send(self
, request
: Request
):
1405 if request
.url
.startswith('ssl://'):
1406 raise SSLError(request
.url
[len('ssl://'):])
1407 return FakeResponse(request
)
1410 class FakeRHYDL(FakeYDL
):
1411 def __init__(self
, *args
, **kwargs
):
1412 super().__init
__(*args
, **kwargs
)
1413 self
._request
_director
= self
.build_request_director([FakeRH
])
1416 class AllUnsupportedRHYDL(FakeYDL
):
1418 def __init__(self
, *args
, **kwargs
):
1420 class UnsupportedRH(RequestHandler
):
1421 def _send(self
, request
: Request
):
1424 _SUPPORTED_FEATURES
= ()
1425 _SUPPORTED_PROXY_SCHEMES
= ()
1426 _SUPPORTED_URL_SCHEMES
= ()
1428 super().__init
__(*args
, **kwargs
)
1429 self
._request
_director
= self
.build_request_director([UnsupportedRH
])
1432 class TestRequestDirector
:
1434 def test_handler_operations(self
):
1435 director
= RequestDirector(logger
=FakeLogger())
1436 handler
= FakeRH(logger
=FakeLogger())
1437 director
.add_handler(handler
)
1438 assert director
.handlers
.get(FakeRH
.RH_KEY
) is handler
1440 # Handler should overwrite
1441 handler2
= FakeRH(logger
=FakeLogger())
1442 director
.add_handler(handler2
)
1443 assert director
.handlers
.get(FakeRH
.RH_KEY
) is not handler
1444 assert director
.handlers
.get(FakeRH
.RH_KEY
) is handler2
1445 assert len(director
.handlers
) == 1
1447 class AnotherFakeRH(FakeRH
):
1449 director
.add_handler(AnotherFakeRH(logger
=FakeLogger()))
1450 assert len(director
.handlers
) == 2
1451 assert director
.handlers
.get(AnotherFakeRH
.RH_KEY
).RH_KEY
== AnotherFakeRH
.RH_KEY
1453 director
.handlers
.pop(FakeRH
.RH_KEY
, None)
1454 assert director
.handlers
.get(FakeRH
.RH_KEY
) is None
1455 assert len(director
.handlers
) == 1
1457 # RequestErrors should passthrough
1458 with pytest
.raises(SSLError
):
1459 director
.send(Request('ssl://something'))
1461 def test_send(self
):
1462 director
= RequestDirector(logger
=FakeLogger())
1463 with pytest
.raises(RequestError
):
1464 director
.send(Request('any://'))
1465 director
.add_handler(FakeRH(logger
=FakeLogger()))
1466 assert isinstance(director
.send(Request('http://')), FakeResponse
)
1468 def test_unsupported_handlers(self
):
1469 class SupportedRH(RequestHandler
):
1470 _SUPPORTED_URL_SCHEMES
= ['http']
1472 def _send(self
, request
: Request
):
1473 return Response(fp
=io
.BytesIO(b
'supported'), headers
={}, url
=request
.url
)
1475 director
= RequestDirector(logger
=FakeLogger())
1476 director
.add_handler(SupportedRH(logger
=FakeLogger()))
1477 director
.add_handler(FakeRH(logger
=FakeLogger()))
1479 # First should take preference
1480 assert director
.send(Request('http://')).read() == b
'supported'
1481 assert director
.send(Request('any://')).read() == b
''
1483 director
.handlers
.pop(FakeRH
.RH_KEY
)
1484 with pytest
.raises(NoSupportingHandlers
):
1485 director
.send(Request('any://'))
1487 def test_unexpected_error(self
):
1488 director
= RequestDirector(logger
=FakeLogger())
1490 class UnexpectedRH(FakeRH
):
1491 def _send(self
, request
: Request
):
1492 raise TypeError('something')
1494 director
.add_handler(UnexpectedRH(logger
=FakeLogger
))
1495 with pytest
.raises(NoSupportingHandlers
, match
=r
'1 unexpected error'):
1496 director
.send(Request('any://'))
1498 director
.handlers
.clear()
1499 assert len(director
.handlers
) == 0
1501 # Should not be fatal
1502 director
.add_handler(FakeRH(logger
=FakeLogger()))
1503 director
.add_handler(UnexpectedRH(logger
=FakeLogger
))
1504 assert director
.send(Request('any://'))
1506 def test_preference(self
):
1507 director
= RequestDirector(logger
=FakeLogger())
1508 director
.add_handler(FakeRH(logger
=FakeLogger()))
1510 class SomeRH(RequestHandler
):
1511 _SUPPORTED_URL_SCHEMES
= ['http']
1513 def _send(self
, request
: Request
):
1514 return Response(fp
=io
.BytesIO(b
'supported'), headers
={}, url
=request
.url
)
1516 def some_preference(rh
, request
):
1517 return (0 if not isinstance(rh
, SomeRH
)
1518 else 100 if 'prefer' in request
.headers
1521 director
.add_handler(SomeRH(logger
=FakeLogger()))
1522 director
.preferences
.add(some_preference
)
1524 assert director
.send(Request('http://')).read() == b
''
1525 assert director
.send(Request('http://', headers
={'prefer': '1'})).read() == b
'supported'
1527 def test_close(self
, monkeypatch
):
1528 director
= RequestDirector(logger
=FakeLogger())
1529 director
.add_handler(FakeRH(logger
=FakeLogger()))
1532 def mock_close(*args
, **kwargs
):
1536 monkeypatch
.setattr(director
.handlers
[FakeRH
.RH_KEY
], 'close', mock_close
)
1541 # XXX: do we want to move this to test_YoutubeDL.py?
1542 class TestYoutubeDLNetworking
:
1545 def build_handler(ydl
, handler
: RequestHandler
= FakeRH
):
1546 return ydl
.build_request_director([handler
]).handlers
.get(handler
.RH_KEY
)
1548 def test_compat_opener(self
):
1549 with
FakeYDL() as ydl
:
1550 with warnings
.catch_warnings():
1551 warnings
.simplefilter('ignore', category
=DeprecationWarning)
1552 assert isinstance(ydl
._opener
, urllib
.request
.OpenerDirector
)
1554 @pytest.mark
.parametrize('proxy,expected', [
1555 ('http://127.0.0.1:8080', {'all': 'http://127.0.0.1:8080'}),
1556 ('', {'all': '__noproxy__'}),
1557 (None, {'http': 'http://127.0.0.1:8081', 'https': 'http://127.0.0.1:8081'}), # env, set https
1559 def test_proxy(self
, proxy
, expected
, monkeypatch
):
1560 monkeypatch
.setenv('HTTP_PROXY', 'http://127.0.0.1:8081')
1561 with
FakeYDL({'proxy': proxy
}) as ydl
:
1562 assert ydl
.proxies
== expected
1564 def test_compat_request(self
):
1565 with
FakeRHYDL() as ydl
:
1566 assert ydl
.urlopen('test://')
1567 urllib_req
= urllib
.request
.Request('http://foo.bar', data
=b
'test', method
='PUT', headers
={'X-Test': '1'})
1568 urllib_req
.add_unredirected_header('Cookie', 'bob=bob')
1569 urllib_req
.timeout
= 2
1570 with warnings
.catch_warnings():
1571 warnings
.simplefilter('ignore', category
=DeprecationWarning)
1572 req
= ydl
.urlopen(urllib_req
).request
1573 assert req
.url
== urllib_req
.get_full_url()
1574 assert req
.data
== urllib_req
.data
1575 assert req
.method
== urllib_req
.get_method()
1576 assert 'X-Test' in req
.headers
1577 assert 'Cookie' in req
.headers
1578 assert req
.extensions
.get('timeout') == 2
1580 with pytest
.raises(AssertionError):
1583 def test_extract_basic_auth(self
):
1584 with
FakeRHYDL() as ydl
:
1585 res
= ydl
.urlopen(Request('http://user:pass@foo.bar'))
1586 assert res
.request
.headers
['Authorization'] == 'Basic dXNlcjpwYXNz'
1588 def test_sanitize_url(self
):
1589 with
FakeRHYDL() as ydl
:
1590 res
= ydl
.urlopen(Request('httpss://foo.bar'))
1591 assert res
.request
.url
== 'https://foo.bar'
1593 def test_file_urls_error(self
):
1594 # use urllib handler
1595 with
FakeYDL() as ydl
:
1596 with pytest
.raises(RequestError
, match
=r
'file:// URLs are disabled by default'):
1597 ydl
.urlopen('file://')
1599 @pytest.mark
.parametrize('scheme', (['ws', 'wss']))
1600 def test_websocket_unavailable_error(self
, scheme
):
1601 with
AllUnsupportedRHYDL() as ydl
:
1602 with pytest
.raises(RequestError
, match
=r
'This request requires WebSocket support'):
1603 ydl
.urlopen(f
'{scheme}://')
1605 def test_legacy_server_connect_error(self
):
1606 with
FakeRHYDL() as ydl
:
1607 for error
in ('UNSAFE_LEGACY_RENEGOTIATION_DISABLED', 'SSLV3_ALERT_HANDSHAKE_FAILURE'):
1608 with pytest
.raises(RequestError
, match
=r
'Try using --legacy-server-connect'):
1609 ydl
.urlopen(f
'ssl://{error}')
1611 with pytest
.raises(SSLError
, match
='testerror'):
1612 ydl
.urlopen('ssl://testerror')
1614 def test_unsupported_impersonate_target(self
):
1615 class FakeImpersonationRHYDL(FakeYDL
):
1616 def __init__(self
, *args
, **kwargs
):
1617 class HTTPRH(RequestHandler
):
1618 def _send(self
, request
: Request
):
1620 _SUPPORTED_URL_SCHEMES
= ('http',)
1621 _SUPPORTED_PROXY_SCHEMES
= None
1623 super().__init
__(*args
, **kwargs
)
1624 self
._request
_director
= self
.build_request_director([HTTPRH
])
1626 with
FakeImpersonationRHYDL() as ydl
:
1629 match
=r
'Impersonate target "test" is not available',
1631 ydl
.urlopen(Request('http://', extensions
={'impersonate': ImpersonateTarget('test', None, None, None)}))
1633 def test_unsupported_impersonate_extension(self
):
1634 class FakeHTTPRHYDL(FakeYDL
):
1635 def __init__(self
, *args
, **kwargs
):
1636 class IRH(ImpersonateRequestHandler
):
1637 def _send(self
, request
: Request
):
1640 _SUPPORTED_URL_SCHEMES
= ('http',)
1641 _SUPPORTED_IMPERSONATE_TARGET_MAP
= {ImpersonateTarget('abc'): 'test'}
1642 _SUPPORTED_PROXY_SCHEMES
= None
1644 super().__init
__(*args
, **kwargs
)
1645 self
._request
_director
= self
.build_request_director([IRH
])
1647 with
FakeHTTPRHYDL() as ydl
:
1650 match
=r
'Impersonate target "test" is not available',
1652 ydl
.urlopen(Request('http://', extensions
={'impersonate': ImpersonateTarget('test', None, None, None)}))
1654 def test_raise_impersonate_error(self
):
1657 match
=r
'Impersonate target "test" is not available',
1659 FakeYDL({'impersonate': ImpersonateTarget('test', None, None, None)})
1661 def test_pass_impersonate_param(self
, monkeypatch
):
1663 class IRH(ImpersonateRequestHandler
):
1664 def _send(self
, request
: Request
):
1667 _SUPPORTED_URL_SCHEMES
= ('http',)
1668 _SUPPORTED_IMPERSONATE_TARGET_MAP
= {ImpersonateTarget('abc'): 'test'}
1670 # Bypass the check on initialize
1671 brh
= FakeYDL
.build_request_director
1672 monkeypatch
.setattr(FakeYDL
, 'build_request_director', lambda cls
, handlers
, preferences
=None: brh(cls
, handlers
=[IRH
]))
1675 'impersonate': ImpersonateTarget('abc', None, None, None),
1677 rh
= self
.build_handler(ydl
, IRH
)
1678 assert rh
.impersonate
== ImpersonateTarget('abc', None, None, None)
1680 def test_get_impersonate_targets(self
):
1682 for target_client
in ('abc', 'xyz', 'asd'):
1683 class TestRH(ImpersonateRequestHandler
):
1684 def _send(self
, request
: Request
):
1686 _SUPPORTED_URL_SCHEMES
= ('http',)
1687 _SUPPORTED_IMPERSONATE_TARGET_MAP
= {ImpersonateTarget(target_client
): 'test'}
1688 RH_KEY
= target_client
1689 RH_NAME
= target_client
1690 handlers
.append(TestRH
)
1692 with
FakeYDL() as ydl
:
1693 ydl
._request
_director
= ydl
.build_request_director(handlers
)
1694 assert set(ydl
._get
_available
_impersonate
_targets
()) == {
1695 (ImpersonateTarget('xyz'), 'xyz'),
1696 (ImpersonateTarget('abc'), 'abc'),
1697 (ImpersonateTarget('asd'), 'asd'),
1699 assert ydl
._impersonate
_target
_available
(ImpersonateTarget('abc'))
1700 assert ydl
._impersonate
_target
_available
(ImpersonateTarget())
1701 assert not ydl
._impersonate
_target
_available
(ImpersonateTarget('zxy'))
1703 @pytest.mark
.parametrize('proxy_key,proxy_url,expected', [
1704 ('http', '__noproxy__', None),
1705 ('no', '127.0.0.1,foo.bar', '127.0.0.1,foo.bar'),
1706 ('https', 'example.com', 'http://example.com'),
1707 ('https', '//example.com', 'http://example.com'),
1708 ('https', 'socks5://example.com', 'socks5h://example.com'),
1709 ('http', 'socks://example.com', 'socks4://example.com'),
1710 ('http', 'socks4://example.com', 'socks4://example.com'),
1711 ('unrelated', '/bad/proxy', '/bad/proxy'), # clean_proxies should ignore bad proxies
1713 def test_clean_proxy(self
, proxy_key
, proxy_url
, expected
, monkeypatch
):
1714 # proxies should be cleaned in urlopen()
1715 with
FakeRHYDL() as ydl
:
1716 req
= ydl
.urlopen(Request('test://', proxies
={proxy_key
: proxy_url
})).request
1717 assert req
.proxies
[proxy_key
] == expected
1719 # and should also be cleaned when building the handler
1720 monkeypatch
.setenv(f
'{proxy_key.upper()}_PROXY', proxy_url
)
1721 with
FakeYDL() as ydl
:
1722 rh
= self
.build_handler(ydl
)
1723 assert rh
.proxies
[proxy_key
] == expected
1725 def test_clean_proxy_header(self
):
1726 with
FakeRHYDL() as ydl
:
1727 req
= ydl
.urlopen(Request('test://', headers
={'ytdl-request-proxy': '//foo.bar'})).request
1728 assert 'ytdl-request-proxy' not in req
.headers
1729 assert req
.proxies
== {'all': 'http://foo.bar'}
1731 with
FakeYDL({'http_headers': {'ytdl-request-proxy': '//foo.bar'}}) as ydl
:
1732 rh
= self
.build_handler(ydl
)
1733 assert 'ytdl-request-proxy' not in rh
.headers
1734 assert rh
.proxies
== {'all': 'http://foo.bar'}
1736 def test_clean_header(self
):
1737 with
FakeRHYDL() as ydl
:
1738 res
= ydl
.urlopen(Request('test://', headers
={'Youtubedl-no-compression': True}))
1739 assert 'Youtubedl-no-compression' not in res
.request
.headers
1740 assert res
.request
.headers
.get('Accept-Encoding') == 'identity'
1742 with
FakeYDL({'http_headers': {'Youtubedl-no-compression': True}}) as ydl
:
1743 rh
= self
.build_handler(ydl
)
1744 assert 'Youtubedl-no-compression' not in rh
.headers
1745 assert rh
.headers
.get('Accept-Encoding') == 'identity'
1747 with
FakeYDL({'http_headers': {'Ytdl-socks-proxy': 'socks://localhost:1080'}}) as ydl
:
1748 rh
= self
.build_handler(ydl
)
1749 assert 'Ytdl-socks-proxy' not in rh
.headers
1751 def test_build_handler_params(self
):
1753 'http_headers': {'test': 'testtest'},
1754 'socket_timeout': 2,
1755 'proxy': 'http://127.0.0.1:8080',
1756 'source_address': '127.0.0.45',
1757 'debug_printtraffic': True,
1758 'compat_opts': ['no-certifi'],
1759 'nocheckcertificate': True,
1760 'legacyserverconnect': True,
1762 rh
= self
.build_handler(ydl
)
1763 assert rh
.headers
.get('test') == 'testtest'
1764 assert 'Accept' in rh
.headers
# ensure std_headers are still there
1765 assert rh
.timeout
== 2
1766 assert rh
.proxies
.get('all') == 'http://127.0.0.1:8080'
1767 assert rh
.source_address
== '127.0.0.45'
1768 assert rh
.verbose
is True
1769 assert rh
.prefer_system_certs
is True
1770 assert rh
.verify
is False
1771 assert rh
.legacy_ssl_support
is True
1773 @pytest.mark
.parametrize('ydl_params', [
1774 {'client_certificate': 'fakecert.crt'},
1775 {'client_certificate': 'fakecert.crt', 'client_certificate_key': 'fakekey.key'},
1776 {'client_certificate': 'fakecert.crt', 'client_certificate_key': 'fakekey.key', 'client_certificate_password': 'foobar'},
1777 {'client_certificate_key': 'fakekey.key', 'client_certificate_password': 'foobar'},
1779 def test_client_certificate(self
, ydl_params
):
1780 with
FakeYDL(ydl_params
) as ydl
:
1781 rh
= self
.build_handler(ydl
)
1782 assert rh
._client
_cert
== ydl_params
# XXX: Too bound to implementation
1784 def test_urllib_file_urls(self
):
1785 with
FakeYDL({'enable_file_urls': False}) as ydl
:
1786 rh
= self
.build_handler(ydl
, UrllibRH
)
1787 assert rh
.enable_file_urls
is False
1789 with
FakeYDL({'enable_file_urls': True}) as ydl
:
1790 rh
= self
.build_handler(ydl
, UrllibRH
)
1791 assert rh
.enable_file_urls
is True
1793 def test_compat_opt_prefer_urllib(self
):
1794 # This assumes urllib only has a preference when this compat opt is given
1795 with
FakeYDL({'compat_opts': ['prefer-legacy-http-handler']}) as ydl
:
1796 director
= ydl
.build_request_director([UrllibRH
])
1797 assert len(director
.preferences
) == 1
1798 assert director
.preferences
.pop()(UrllibRH
, None)
1803 def test_query(self
):
1804 req
= Request('http://example.com?q=something', query
={'v': 'xyz'})
1805 assert req
.url
== 'http://example.com?q=something&v=xyz'
1807 req
.update(query
={'v': '123'})
1808 assert req
.url
== 'http://example.com?q=something&v=123'
1809 req
.update(url
='http://example.com', query
={'v': 'xyz'})
1810 assert req
.url
== 'http://example.com?v=xyz'
1812 def test_method(self
):
1813 req
= Request('http://example.com')
1814 assert req
.method
== 'GET'
1816 assert req
.method
== 'POST'
1818 assert req
.method
== 'GET'
1821 assert req
.method
== 'PUT'
1823 assert req
.method
== 'PUT'
1824 with pytest
.raises(TypeError):
1827 def test_request_helpers(self
):
1828 assert HEADRequest('http://example.com').method
== 'HEAD'
1829 assert PUTRequest('http://example.com').method
== 'PUT'
1831 def test_headers(self
):
1832 req
= Request('http://example.com', headers
={'tesT': 'test'})
1833 assert req
.headers
== HTTPHeaderDict({'test': 'test'})
1834 req
.update(headers
={'teSt2': 'test2'})
1835 assert req
.headers
== HTTPHeaderDict({'test': 'test', 'test2': 'test2'})
1837 req
.headers
= new_headers
= HTTPHeaderDict({'test': 'test'})
1838 assert req
.headers
== HTTPHeaderDict({'test': 'test'})
1839 assert req
.headers
is new_headers
1841 # test converts dict to case insensitive dict
1842 req
.headers
= new_headers
= {'test2': 'test2'}
1843 assert isinstance(req
.headers
, HTTPHeaderDict
)
1844 assert req
.headers
is not new_headers
1846 with pytest
.raises(TypeError):
1849 def test_data_type(self
):
1850 req
= Request('http://example.com')
1851 assert req
.data
is None
1852 # test bytes is allowed
1854 assert req
.data
== b
'test'
1855 # test iterable of bytes is allowed
1856 i
= [b
'test', b
'test2']
1858 assert req
.data
== i
1860 # test file-like object is allowed
1861 f
= io
.BytesIO(b
'test')
1863 assert req
.data
== f
1865 # common mistake: test str not allowed
1866 with pytest
.raises(TypeError):
1868 assert req
.data
!= 'test'
1870 # common mistake: test dict is not allowed
1871 with pytest
.raises(TypeError):
1872 req
.data
= {'test': 'test'}
1873 assert req
.data
!= {'test': 'test'}
1875 def test_content_length_header(self
):
1876 req
= Request('http://example.com', headers
={'Content-Length': '0'}, data
=b
'')
1877 assert req
.headers
.get('Content-Length') == '0'
1880 assert 'Content-Length' not in req
.headers
1882 req
= Request('http://example.com', headers
={'Content-Length': '10'})
1883 assert 'Content-Length' not in req
.headers
1885 def test_content_type_header(self
):
1886 req
= Request('http://example.com', headers
={'Content-Type': 'test'}, data
=b
'test')
1887 assert req
.headers
.get('Content-Type') == 'test'
1889 assert req
.headers
.get('Content-Type') == 'test'
1891 assert 'Content-Type' not in req
.headers
1893 assert req
.headers
.get('Content-Type') == 'application/x-www-form-urlencoded'
1895 def test_update_req(self
):
1896 req
= Request('http://example.com')
1897 assert req
.data
is None
1898 assert req
.method
== 'GET'
1899 assert 'Content-Type' not in req
.headers
1900 # Test that zero-byte payloads will be sent
1901 req
.update(data
=b
'')
1902 assert req
.data
== b
''
1903 assert req
.method
== 'POST'
1904 assert req
.headers
.get('Content-Type') == 'application/x-www-form-urlencoded'
1906 def test_proxies(self
):
1907 req
= Request(url
='http://example.com', proxies
={'http': 'http://127.0.0.1:8080'})
1908 assert req
.proxies
== {'http': 'http://127.0.0.1:8080'}
1910 def test_extensions(self
):
1911 req
= Request(url
='http://example.com', extensions
={'timeout': 2})
1912 assert req
.extensions
== {'timeout': 2}
1914 def test_copy(self
):
1916 url
='http://example.com',
1917 extensions
={'cookiejar': CookieJar()},
1918 headers
={'Accept-Encoding': 'br'},
1919 proxies
={'http': 'http://127.0.0.1'},
1922 req_copy
= req
.copy()
1923 assert req_copy
is not req
1924 assert req_copy
.url
== req
.url
1925 assert req_copy
.headers
== req
.headers
1926 assert req_copy
.headers
is not req
.headers
1927 assert req_copy
.proxies
== req
.proxies
1928 assert req_copy
.proxies
is not req
.proxies
1930 # Data is not able to be copied
1931 assert req_copy
.data
== req
.data
1932 assert req_copy
.data
is req
.data
1934 # Shallow copy extensions
1935 assert req_copy
.extensions
is not req
.extensions
1936 assert req_copy
.extensions
['cookiejar'] == req
.extensions
['cookiejar']
1938 # Subclasses are copied by default
1939 class AnotherRequest(Request
):
1942 req
= AnotherRequest(url
='http://127.0.0.1')
1943 assert isinstance(req
.copy(), AnotherRequest
)
1946 req
= Request(url
='https://фtest.example.com/ some spaceв?ä=c')
1947 assert req
.url
== 'https://xn--test-z6d.example.com/%20some%20space%D0%B2?%C3%A4=c'
1949 assert Request(url
='//example.com').url
== 'http://example.com'
1951 with pytest
.raises(TypeError):
1952 Request(url
='https://').url
= None
1957 @pytest.mark
.parametrize('reason,status,expected', [
1958 ('custom', 200, 'custom'),
1959 (None, 404, 'Not Found'), # fallback status
1960 ('', 403, 'Forbidden'),
1963 def test_reason(self
, reason
, status
, expected
):
1964 res
= Response(io
.BytesIO(b
''), url
='test://', headers
={}, status
=status
, reason
=reason
)
1965 assert res
.reason
== expected
1967 def test_headers(self
):
1969 headers
.add_header('Test', 'test')
1970 headers
.add_header('Test', 'test2')
1971 headers
.add_header('content-encoding', 'br')
1972 res
= Response(io
.BytesIO(b
''), headers
=headers
, url
='test://')
1973 assert res
.headers
.get_all('test') == ['test', 'test2']
1974 assert 'Content-Encoding' in res
.headers
1976 def test_get_header(self
):
1978 headers
.add_header('Set-Cookie', 'cookie1')
1979 headers
.add_header('Set-cookie', 'cookie2')
1980 headers
.add_header('Test', 'test')
1981 headers
.add_header('Test', 'test2')
1982 res
= Response(io
.BytesIO(b
''), headers
=headers
, url
='test://')
1983 assert res
.get_header('test') == 'test, test2'
1984 assert res
.get_header('set-Cookie') == 'cookie1'
1985 assert res
.get_header('notexist', 'default') == 'default'
1987 def test_compat(self
):
1988 res
= Response(io
.BytesIO(b
''), url
='test://', status
=404, headers
={'test': 'test'})
1989 with warnings
.catch_warnings():
1990 warnings
.simplefilter('ignore', category
=DeprecationWarning)
1991 assert res
.code
== res
.getcode() == res
.status
1992 assert res
.geturl() == res
.url
1993 assert res
.info() is res
.headers
1994 assert res
.getheader('test') == res
.get_header('test')
1997 class TestImpersonateTarget
:
1998 @pytest.mark
.parametrize('target_str,expected', [
1999 ('abc', ImpersonateTarget('abc', None, None, None)),
2000 ('abc-120_esr', ImpersonateTarget('abc', '120_esr', None, None)),
2001 ('abc-120:xyz', ImpersonateTarget('abc', '120', 'xyz', None)),
2002 ('abc-120:xyz-5.6', ImpersonateTarget('abc', '120', 'xyz', '5.6')),
2003 ('abc:xyz', ImpersonateTarget('abc', None, 'xyz', None)),
2004 ('abc:', ImpersonateTarget('abc', None, None, None)),
2005 ('abc-120:', ImpersonateTarget('abc', '120', None, None)),
2006 (':xyz', ImpersonateTarget(None, None, 'xyz', None)),
2007 (':xyz-6.5', ImpersonateTarget(None, None, 'xyz', '6.5')),
2008 (':', ImpersonateTarget(None, None, None, None)),
2009 ('', ImpersonateTarget(None, None, None, None)),
2011 def test_target_from_str(self
, target_str
, expected
):
2012 assert ImpersonateTarget
.from_str(target_str
) == expected
2014 @pytest.mark
.parametrize('target_str', [
2015 '-120', ':-12.0', '-12:-12', '-:-',
2016 '::', 'a-c-d:', 'a-c-d:e-f-g', 'a:b:',
2018 def test_target_from_invalid_str(self
, target_str
):
2019 with pytest
.raises(ValueError):
2020 ImpersonateTarget
.from_str(target_str
)
2022 @pytest.mark
.parametrize('target,expected', [
2023 (ImpersonateTarget('abc', None, None, None), 'abc'),
2024 (ImpersonateTarget('abc', '120', None, None), 'abc-120'),
2025 (ImpersonateTarget('abc', '120', 'xyz', None), 'abc-120:xyz'),
2026 (ImpersonateTarget('abc', '120', 'xyz', '5'), 'abc-120:xyz-5'),
2027 (ImpersonateTarget('abc', None, 'xyz', None), 'abc:xyz'),
2028 (ImpersonateTarget('abc', '120', None, None), 'abc-120'),
2029 (ImpersonateTarget('abc', '120', 'xyz', None), 'abc-120:xyz'),
2030 (ImpersonateTarget('abc', None, 'xyz'), 'abc:xyz'),
2031 (ImpersonateTarget(None, None, 'xyz', '6.5'), ':xyz-6.5'),
2032 (ImpersonateTarget('abc'), 'abc'),
2033 (ImpersonateTarget(None, None, None, None), ''),
2035 def test_str(self
, target
, expected
):
2036 assert str(target
) == expected
2038 @pytest.mark
.parametrize('args', [
2039 ('abc', None, None, '5'),
2040 ('abc', '120', None, '5'),
2041 (None, '120', None, None),
2042 (None, '120', None, '5'),
2043 (None, None, None, '5'),
2044 (None, '120', 'xyz', '5'),
2046 def test_invalid_impersonate_target(self
, args
):
2047 with pytest
.raises(ValueError):
2048 ImpersonateTarget(*args
)
2050 @pytest.mark
.parametrize('target1,target2,is_in,is_eq', [
2051 (ImpersonateTarget('abc', None, None, None), ImpersonateTarget('abc', None, None, None), True, True),
2052 (ImpersonateTarget('abc', None, None, None), ImpersonateTarget('abc', '120', None, None), True, False),
2053 (ImpersonateTarget('abc', None, 'xyz', 'test'), ImpersonateTarget('abc', '120', 'xyz', None), True, False),
2054 (ImpersonateTarget('abc', '121', 'xyz', 'test'), ImpersonateTarget('abc', '120', 'xyz', 'test'), False, False),
2055 (ImpersonateTarget('abc'), ImpersonateTarget('abc', '120', 'xyz', 'test'), True, False),
2056 (ImpersonateTarget('abc', '120', 'xyz', 'test'), ImpersonateTarget('abc'), True, False),
2057 (ImpersonateTarget(), ImpersonateTarget('abc', '120', 'xyz'), True, False),
2058 (ImpersonateTarget(), ImpersonateTarget(), True, True),
2060 def test_impersonate_target_in(self
, target1
, target2
, is_in
, is_eq
):
2061 assert (target1
in target2
) is is_in
2062 assert (target1
== target2
) is is_eq