mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-11-26 09:11:25 +01:00
Compare commits
6 Commits
a64c0c5018
...
d47f6f3c60
Author | SHA1 | Date | |
---|---|---|---|
|
d47f6f3c60 | ||
|
d3ae5688b4 | ||
|
fc8b4a3be9 | ||
|
a3cf32ad5b | ||
|
40ab38b660 | ||
|
4accb0befe |
13
README.md
13
README.md
|
@ -772,6 +772,7 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
that do not support RFC 5746 secure
|
that do not support RFC 5746 secure
|
||||||
renegotiation
|
renegotiation
|
||||||
--no-check-certificates Suppress HTTPS certificate validation
|
--no-check-certificates Suppress HTTPS certificate validation
|
||||||
|
--proxy-no-check-certificates Suppress HTTPS Proxy certificate validation
|
||||||
--prefer-insecure Use an unencrypted connection to retrieve
|
--prefer-insecure Use an unencrypted connection to retrieve
|
||||||
information about the video (Currently
|
information about the video (Currently
|
||||||
supported only for YouTube)
|
supported only for YouTube)
|
||||||
|
@ -879,6 +880,18 @@ If you fork the project on GitHub, you can run your fork's [build workflow](.git
|
||||||
Password for client certificate private key,
|
Password for client certificate private key,
|
||||||
if encrypted. If not provided, and the key
|
if encrypted. If not provided, and the key
|
||||||
is encrypted, yt-dlp will ask interactively
|
is encrypted, yt-dlp will ask interactively
|
||||||
|
--proxy-client-certificate CERTFILE
|
||||||
|
Path to client certificate file in PEM
|
||||||
|
format for HTTPS proxy. May include the
|
||||||
|
private key
|
||||||
|
--proxy-client-certificate-key KEYFILE
|
||||||
|
Path to private key file for client
|
||||||
|
certificate for HTTPS proxy
|
||||||
|
--proxy-client-certificate-password PASSWORD
|
||||||
|
Password for client certificate private key,
|
||||||
|
if encrypted, for HTTPS proxy. If not
|
||||||
|
provided, and the key is encrypted, yt-dlp
|
||||||
|
will ask interactively
|
||||||
|
|
||||||
## Post-Processing Options:
|
## Post-Processing Options:
|
||||||
-x, --extract-audio Convert video files to audio-only files
|
-x, --extract-audio Convert video files to audio-only files
|
||||||
|
|
|
@ -19,6 +19,8 @@ from yt_dlp.dependencies import urllib3
|
||||||
from yt_dlp.networking import Request
|
from yt_dlp.networking import Request
|
||||||
from yt_dlp.networking.exceptions import HTTPError, ProxyError, SSLError
|
from yt_dlp.networking.exceptions import HTTPError, ProxyError, SSLError
|
||||||
|
|
||||||
|
MTLS_CERT_DIR = os.path.join(TEST_DIR, 'testdata', 'certificate')
|
||||||
|
|
||||||
|
|
||||||
class HTTPProxyAuthMixin:
|
class HTTPProxyAuthMixin:
|
||||||
|
|
||||||
|
@ -135,6 +137,35 @@ class HTTPSProxyHandler(HTTPProxyHandler):
|
||||||
super().__init__(request, *args, **kwargs)
|
super().__init__(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class MTLSHTTPSProxyHandler(HTTPProxyHandler):
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
||||||
|
cacertfn = os.path.join(MTLS_CERT_DIR, 'ca.crt')
|
||||||
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
sslctx.verify_mode = ssl.CERT_REQUIRED
|
||||||
|
sslctx.load_verify_locations(cafile=cacertfn)
|
||||||
|
sslctx.load_cert_chain(certfn, None)
|
||||||
|
if isinstance(request, ssl.SSLSocket):
|
||||||
|
request = SSLTransport(request, ssl_context=sslctx, server_side=True)
|
||||||
|
else:
|
||||||
|
request = sslctx.wrap_socket(request, server_side=True)
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyHTTPSProxyHandler(HTTPProxyHandler):
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
||||||
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
sslctx.maximum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
sslctx.set_ciphers('SHA1:AESCCM:aDSS:eNULL:aNULL')
|
||||||
|
sslctx.load_cert_chain(certfn, None)
|
||||||
|
if isinstance(request, ssl.SSLSocket):
|
||||||
|
request = SSLTransport(request, ssl_context=sslctx, server_side=True)
|
||||||
|
else:
|
||||||
|
request = sslctx.wrap_socket(request, server_side=True)
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class HTTPConnectProxyHandler(BaseHTTPRequestHandler, HTTPProxyAuthMixin):
|
class HTTPConnectProxyHandler(BaseHTTPRequestHandler, HTTPProxyAuthMixin):
|
||||||
protocol_version = 'HTTP/1.1'
|
protocol_version = 'HTTP/1.1'
|
||||||
default_request_version = 'HTTP/1.1'
|
default_request_version = 'HTTP/1.1'
|
||||||
|
@ -178,6 +209,39 @@ class HTTPSConnectProxyHandler(HTTPConnectProxyHandler):
|
||||||
self.server.close_request(self._original_request)
|
self.server.close_request(self._original_request)
|
||||||
|
|
||||||
|
|
||||||
|
class MTLSHTTPSConnectProxyHandler(HTTPConnectProxyHandler):
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
||||||
|
cacertfn = os.path.join(MTLS_CERT_DIR, 'ca.crt')
|
||||||
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
sslctx.verify_mode = ssl.CERT_REQUIRED
|
||||||
|
sslctx.load_verify_locations(cafile=cacertfn)
|
||||||
|
sslctx.load_cert_chain(certfn, None)
|
||||||
|
request = sslctx.wrap_socket(request, server_side=True)
|
||||||
|
self._original_request = request
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def do_CONNECT(self):
|
||||||
|
super().do_CONNECT()
|
||||||
|
self.server.close_request(self._original_request)
|
||||||
|
|
||||||
|
|
||||||
|
class LegacyHTTPSConnectProxyHandler(HTTPConnectProxyHandler):
|
||||||
|
def __init__(self, request, *args, **kwargs):
|
||||||
|
certfn = os.path.join(TEST_DIR, 'testcert.pem')
|
||||||
|
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
|
sslctx.maximum_version = ssl.TLSVersion.TLSv1_2
|
||||||
|
sslctx.set_ciphers('SHA1:AESCCM:aDSS:eNULL:aNULL')
|
||||||
|
sslctx.load_cert_chain(certfn, None)
|
||||||
|
request = sslctx.wrap_socket(request, server_side=True)
|
||||||
|
self._original_request = request
|
||||||
|
super().__init__(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def do_CONNECT(self):
|
||||||
|
super().do_CONNECT()
|
||||||
|
self.server.close_request(self._original_request)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def proxy_server(proxy_server_class, request_handler, bind_ip=None, **proxy_server_kwargs):
|
def proxy_server(proxy_server_class, request_handler, bind_ip=None, **proxy_server_kwargs):
|
||||||
server = server_thread = None
|
server = server_thread = None
|
||||||
|
@ -285,7 +349,7 @@ class TestHTTPProxy:
|
||||||
@pytest.mark.skip_handler('Urllib', 'urllib does not support https proxies')
|
@pytest.mark.skip_handler('Urllib', 'urllib does not support https proxies')
|
||||||
def test_https(self, handler, ctx):
|
def test_https(self, handler, ctx):
|
||||||
with ctx.http_server(HTTPSProxyHandler) as server_address:
|
with ctx.http_server(HTTPSProxyHandler) as server_address:
|
||||||
with handler(verify=False, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
with handler(proxy_verify=False, verify=False, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
||||||
proxy_info = ctx.proxy_info_request(rh)
|
proxy_info = ctx.proxy_info_request(rh)
|
||||||
assert proxy_info['proxy'] == server_address
|
assert proxy_info['proxy'] == server_address
|
||||||
assert proxy_info['connect'] is False
|
assert proxy_info['connect'] is False
|
||||||
|
@ -294,10 +358,64 @@ class TestHTTPProxy:
|
||||||
@pytest.mark.skip_handler('Urllib', 'urllib does not support https proxies')
|
@pytest.mark.skip_handler('Urllib', 'urllib does not support https proxies')
|
||||||
def test_https_verify_failed(self, handler, ctx):
|
def test_https_verify_failed(self, handler, ctx):
|
||||||
with ctx.http_server(HTTPSProxyHandler) as server_address:
|
with ctx.http_server(HTTPSProxyHandler) as server_address:
|
||||||
with handler(verify=True, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
with handler(proxy_verify=True, verify=False, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
||||||
# Accept SSLError as may not be feasible to tell if it is proxy or request error.
|
# Accept both ProxyError and SSLError as may not be feasible to tell if it is proxy or request error.
|
||||||
# note: if request proto also does ssl verification, this may also be the error of the request.
|
with pytest.raises((ProxyError, SSLError)):
|
||||||
# Until we can support passing custom cacerts to handlers, we cannot properly test this for all cases.
|
ctx.proxy_info_request(rh)
|
||||||
|
|
||||||
|
@pytest.mark.skip_handler('Urllib', 'urllib does not support https proxies')
|
||||||
|
@pytest.mark.skip_handler('CurlCFFI', 'legacy_ssl ignored by CurlCFFI')
|
||||||
|
def test_https_legacy_ssl_support(self, handler, ctx):
|
||||||
|
with ctx.http_server(LegacyHTTPSProxyHandler) as server_address:
|
||||||
|
with handler(proxy_verify=False, verify=False, proxy_legacy_ssl_support=True, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
||||||
|
proxy_info = ctx.proxy_info_request(rh)
|
||||||
|
assert proxy_info['proxy'] == server_address
|
||||||
|
assert proxy_info['connect'] is False
|
||||||
|
assert 'Proxy-Authorization' not in proxy_info['headers']
|
||||||
|
|
||||||
|
with handler(proxy_verify=False, verify=False, proxy_legacy_ssl_support=False, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
||||||
|
with pytest.raises((ProxyError, SSLError)):
|
||||||
|
ctx.proxy_info_request(rh)
|
||||||
|
|
||||||
|
@pytest.mark.skip_handler('Urllib', 'urllib does not support https proxies')
|
||||||
|
@pytest.mark.parametrize('proxy_client_cert', [
|
||||||
|
{'client_certificate': os.path.join(MTLS_CERT_DIR, 'clientwithkey.crt')},
|
||||||
|
{
|
||||||
|
'client_certificate': os.path.join(MTLS_CERT_DIR, 'client.crt'),
|
||||||
|
'client_certificate_key': os.path.join(MTLS_CERT_DIR, 'client.key'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'client_certificate': os.path.join(MTLS_CERT_DIR, 'clientwithencryptedkey.crt'),
|
||||||
|
'client_certificate_password': 'foobar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'client_certificate': os.path.join(MTLS_CERT_DIR, 'client.crt'),
|
||||||
|
'client_certificate_key': os.path.join(MTLS_CERT_DIR, 'clientencrypted.key'),
|
||||||
|
'client_certificate_password': 'foobar',
|
||||||
|
},
|
||||||
|
], ids=['combined_nopass', 'nocombined_nopass', 'combined_pass', 'nocombined_pass'])
|
||||||
|
def test_https_mtls(self, handler, ctx, proxy_client_cert):
|
||||||
|
with ctx.http_server(MTLSHTTPSProxyHandler) as server_address:
|
||||||
|
with handler(
|
||||||
|
proxy_verify=False,
|
||||||
|
verify=False,
|
||||||
|
proxy_client_cert=proxy_client_cert,
|
||||||
|
proxies={ctx.REQUEST_PROTO: f'https://{server_address}'},
|
||||||
|
) as rh:
|
||||||
|
proxy_info = ctx.proxy_info_request(rh)
|
||||||
|
assert proxy_info['proxy'] == server_address
|
||||||
|
assert proxy_info['connect'] is False
|
||||||
|
assert 'Proxy-Authorization' not in proxy_info['headers']
|
||||||
|
|
||||||
|
@pytest.mark.skip_handler('Urllib', 'urllib does not support https proxies')
|
||||||
|
def test_https_mtls_error(self, handler, ctx):
|
||||||
|
with ctx.http_server(MTLSHTTPSProxyHandler) as server_address:
|
||||||
|
with handler(
|
||||||
|
proxy_verify=False,
|
||||||
|
verify=False,
|
||||||
|
proxy_client_cert=None,
|
||||||
|
proxies={ctx.REQUEST_PROTO: f'https://{server_address}'},
|
||||||
|
) as rh:
|
||||||
with pytest.raises((ProxyError, SSLError)):
|
with pytest.raises((ProxyError, SSLError)):
|
||||||
ctx.proxy_info_request(rh)
|
ctx.proxy_info_request(rh)
|
||||||
|
|
||||||
|
@ -331,10 +449,6 @@ class TestHTTPConnectProxy:
|
||||||
assert proxy_info['proxy'] == server_address
|
assert proxy_info['proxy'] == server_address
|
||||||
assert 'Proxy-Authorization' in proxy_info['headers']
|
assert 'Proxy-Authorization' in proxy_info['headers']
|
||||||
|
|
||||||
@pytest.mark.skip_handler(
|
|
||||||
'Requests',
|
|
||||||
'bug in urllib3 causes unclosed socket: https://github.com/urllib3/urllib3/issues/3374',
|
|
||||||
)
|
|
||||||
def test_http_connect_bad_auth(self, handler, ctx):
|
def test_http_connect_bad_auth(self, handler, ctx):
|
||||||
with ctx.http_server(HTTPConnectProxyHandler, username='test', password='test') as server_address:
|
with ctx.http_server(HTTPConnectProxyHandler, username='test', password='test') as server_address:
|
||||||
with handler(verify=False, proxies={ctx.REQUEST_PROTO: f'http://test:bad@{server_address}'}) as rh:
|
with handler(verify=False, proxies={ctx.REQUEST_PROTO: f'http://test:bad@{server_address}'}) as rh:
|
||||||
|
@ -355,7 +469,7 @@ class TestHTTPConnectProxy:
|
||||||
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
||||||
def test_https_connect_proxy(self, handler, ctx):
|
def test_https_connect_proxy(self, handler, ctx):
|
||||||
with ctx.http_server(HTTPSConnectProxyHandler) as server_address:
|
with ctx.http_server(HTTPSConnectProxyHandler) as server_address:
|
||||||
with handler(verify=False, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
with handler(proxy_verify=False, verify=False, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
||||||
proxy_info = ctx.proxy_info_request(rh)
|
proxy_info = ctx.proxy_info_request(rh)
|
||||||
assert proxy_info['proxy'] == server_address
|
assert proxy_info['proxy'] == server_address
|
||||||
assert proxy_info['connect'] is True
|
assert proxy_info['connect'] is True
|
||||||
|
@ -364,17 +478,71 @@ class TestHTTPConnectProxy:
|
||||||
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
||||||
def test_https_connect_verify_failed(self, handler, ctx):
|
def test_https_connect_verify_failed(self, handler, ctx):
|
||||||
with ctx.http_server(HTTPSConnectProxyHandler) as server_address:
|
with ctx.http_server(HTTPSConnectProxyHandler) as server_address:
|
||||||
with handler(verify=True, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
with handler(proxy_verify=True, verify=False, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
||||||
# Accept SSLError as may not be feasible to tell if it is proxy or request error.
|
# Accept both ProxyError and SSLError as may not be feasible to tell if it is proxy or request error.
|
||||||
# note: if request proto also does ssl verification, this may also be the error of the request.
|
|
||||||
# Until we can support passing custom cacerts to handlers, we cannot properly test this for all cases.
|
|
||||||
with pytest.raises((ProxyError, SSLError)):
|
with pytest.raises((ProxyError, SSLError)):
|
||||||
ctx.proxy_info_request(rh)
|
ctx.proxy_info_request(rh)
|
||||||
|
|
||||||
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
||||||
def test_https_connect_proxy_auth(self, handler, ctx):
|
def test_https_connect_proxy_auth(self, handler, ctx):
|
||||||
with ctx.http_server(HTTPSConnectProxyHandler, username='test', password='test') as server_address:
|
with ctx.http_server(HTTPSConnectProxyHandler, username='test', password='test') as server_address:
|
||||||
with handler(verify=False, proxies={ctx.REQUEST_PROTO: f'https://test:test@{server_address}'}) as rh:
|
with handler(proxy_verify=False, verify=False, proxies={ctx.REQUEST_PROTO: f'https://test:test@{server_address}'}) as rh:
|
||||||
proxy_info = ctx.proxy_info_request(rh)
|
proxy_info = ctx.proxy_info_request(rh)
|
||||||
assert proxy_info['proxy'] == server_address
|
assert proxy_info['proxy'] == server_address
|
||||||
assert 'Proxy-Authorization' in proxy_info['headers']
|
assert 'Proxy-Authorization' in proxy_info['headers']
|
||||||
|
|
||||||
|
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
||||||
|
@pytest.mark.parametrize('proxy_client_cert', [
|
||||||
|
{'client_certificate': os.path.join(MTLS_CERT_DIR, 'clientwithkey.crt')},
|
||||||
|
{
|
||||||
|
'client_certificate': os.path.join(MTLS_CERT_DIR, 'client.crt'),
|
||||||
|
'client_certificate_key': os.path.join(MTLS_CERT_DIR, 'client.key'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'client_certificate': os.path.join(MTLS_CERT_DIR, 'clientwithencryptedkey.crt'),
|
||||||
|
'client_certificate_password': 'foobar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'client_certificate': os.path.join(MTLS_CERT_DIR, 'client.crt'),
|
||||||
|
'client_certificate_key': os.path.join(MTLS_CERT_DIR, 'clientencrypted.key'),
|
||||||
|
'client_certificate_password': 'foobar',
|
||||||
|
},
|
||||||
|
], ids=['combined_nopass', 'nocombined_nopass', 'combined_pass', 'nocombined_pass'])
|
||||||
|
def test_https_connect_mtls(self, handler, ctx, proxy_client_cert):
|
||||||
|
with ctx.http_server(MTLSHTTPSConnectProxyHandler) as server_address:
|
||||||
|
with handler(
|
||||||
|
proxy_verify=False,
|
||||||
|
verify=False,
|
||||||
|
proxy_client_cert=proxy_client_cert,
|
||||||
|
proxies={ctx.REQUEST_PROTO: f'https://{server_address}'},
|
||||||
|
) as rh:
|
||||||
|
proxy_info = ctx.proxy_info_request(rh)
|
||||||
|
assert proxy_info['proxy'] == server_address
|
||||||
|
assert proxy_info['connect'] is True
|
||||||
|
assert 'Proxy-Authorization' not in proxy_info['headers']
|
||||||
|
|
||||||
|
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
||||||
|
def test_https_connect_mtls_error(self, handler, ctx):
|
||||||
|
with ctx.http_server(MTLSHTTPSConnectProxyHandler) as server_address:
|
||||||
|
with handler(
|
||||||
|
proxy_verify=False,
|
||||||
|
verify=False,
|
||||||
|
proxy_client_cert=None,
|
||||||
|
proxies={ctx.REQUEST_PROTO: f'https://{server_address}'},
|
||||||
|
) as rh:
|
||||||
|
with pytest.raises((ProxyError, SSLError)):
|
||||||
|
ctx.proxy_info_request(rh)
|
||||||
|
|
||||||
|
@pytest.mark.skipif(urllib3 is None, reason='requires urllib3 to test')
|
||||||
|
@pytest.mark.skip_handler('CurlCFFI', 'legacy_ssl ignored by CurlCFFI')
|
||||||
|
def test_https_connect_legacy_ssl_support(self, handler, ctx):
|
||||||
|
with ctx.http_server(LegacyHTTPSConnectProxyHandler) as server_address:
|
||||||
|
with handler(proxy_verify=False, verify=False, proxy_legacy_ssl_support=True, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
||||||
|
proxy_info = ctx.proxy_info_request(rh)
|
||||||
|
assert proxy_info['proxy'] == server_address
|
||||||
|
assert proxy_info['connect'] is True
|
||||||
|
assert 'Proxy-Authorization' not in proxy_info['headers']
|
||||||
|
|
||||||
|
with handler(proxy_verify=False, verify=False, proxy_legacy_ssl_support=False, proxies={ctx.REQUEST_PROTO: f'https://{server_address}'}) as rh:
|
||||||
|
with pytest.raises((ProxyError, SSLError)):
|
||||||
|
ctx.proxy_info_request(rh)
|
||||||
|
|
|
@ -61,7 +61,7 @@ from yt_dlp.networking.impersonate import (
|
||||||
ImpersonateRequestHandler,
|
ImpersonateRequestHandler,
|
||||||
ImpersonateTarget,
|
ImpersonateTarget,
|
||||||
)
|
)
|
||||||
from yt_dlp.utils import YoutubeDLError
|
from yt_dlp.utils import YoutubeDLError, traverse_obj
|
||||||
from yt_dlp.utils._utils import _YDLLogger as FakeLogger
|
from yt_dlp.utils._utils import _YDLLogger as FakeLogger
|
||||||
from yt_dlp.utils.networking import HTTPHeaderDict, std_headers
|
from yt_dlp.utils.networking import HTTPHeaderDict, std_headers
|
||||||
|
|
||||||
|
@ -772,6 +772,10 @@ class TestClientCertificate:
|
||||||
'client_certificate_password': 'foobar',
|
'client_certificate_password': 'foobar',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def test_mtls_required(self, handler):
|
||||||
|
with pytest.raises(SSLError):
|
||||||
|
self._run_test(handler)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('handler', ['CurlCFFI'], indirect=True)
|
@pytest.mark.parametrize('handler', ['CurlCFFI'], indirect=True)
|
||||||
class TestHTTPImpersonateRequestHandler(TestRequestHandlerBase):
|
class TestHTTPImpersonateRequestHandler(TestRequestHandlerBase):
|
||||||
|
@ -1776,6 +1780,7 @@ class TestYoutubeDLNetworking:
|
||||||
'compat_opts': ['no-certifi'],
|
'compat_opts': ['no-certifi'],
|
||||||
'nocheckcertificate': True,
|
'nocheckcertificate': True,
|
||||||
'legacyserverconnect': True,
|
'legacyserverconnect': True,
|
||||||
|
'proxy_nocheckcertificate': True,
|
||||||
}) as ydl:
|
}) as ydl:
|
||||||
rh = self.build_handler(ydl)
|
rh = self.build_handler(ydl)
|
||||||
assert rh.headers.get('test') == 'testtest'
|
assert rh.headers.get('test') == 'testtest'
|
||||||
|
@ -1787,6 +1792,7 @@ class TestYoutubeDLNetworking:
|
||||||
assert rh.prefer_system_certs is True
|
assert rh.prefer_system_certs is True
|
||||||
assert rh.verify is False
|
assert rh.verify is False
|
||||||
assert rh.legacy_ssl_support is True
|
assert rh.legacy_ssl_support is True
|
||||||
|
assert rh.proxy_verify is False
|
||||||
|
|
||||||
@pytest.mark.parametrize('ydl_params', [
|
@pytest.mark.parametrize('ydl_params', [
|
||||||
{'client_certificate': 'fakecert.crt'},
|
{'client_certificate': 'fakecert.crt'},
|
||||||
|
@ -1799,6 +1805,22 @@ class TestYoutubeDLNetworking:
|
||||||
rh = self.build_handler(ydl)
|
rh = self.build_handler(ydl)
|
||||||
assert rh._client_cert == ydl_params # XXX: Too bound to implementation
|
assert rh._client_cert == ydl_params # XXX: Too bound to implementation
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('client_cert', [
|
||||||
|
{'client_certificate': 'fakecert.crt'},
|
||||||
|
{'client_certificate': 'fakecert.crt', 'client_certificate_key': 'fakekey.key'},
|
||||||
|
{'client_certificate': 'fakecert.crt', 'client_certificate_key': 'fakekey.key', 'client_certificate_password': 'foobar'},
|
||||||
|
{'client_certificate_key': 'fakekey.key', 'client_certificate_password': 'foobar'},
|
||||||
|
])
|
||||||
|
def test_proxy_client_certificate(self, client_cert):
|
||||||
|
ydl_params = traverse_obj(client_cert, {
|
||||||
|
'proxy_client_certificate': 'client_certificate',
|
||||||
|
'proxy_client_certificate_key': 'client_certificate_key',
|
||||||
|
'proxy_client_certificate_password': 'client_certificate_password',
|
||||||
|
})
|
||||||
|
with FakeYDL(ydl_params) as ydl:
|
||||||
|
rh = self.build_handler(ydl)
|
||||||
|
assert rh._proxy_client_cert == client_cert
|
||||||
|
|
||||||
def test_urllib_file_urls(self):
|
def test_urllib_file_urls(self):
|
||||||
with FakeYDL({'enable_file_urls': False}) as ydl:
|
with FakeYDL({'enable_file_urls': False}) as ydl:
|
||||||
rh = self.build_handler(ydl, UrllibRH)
|
rh = self.build_handler(ydl, UrllibRH)
|
||||||
|
|
|
@ -356,7 +356,7 @@ class TestWebsSocketRequestHandlerConformance:
|
||||||
'client_certificate_key': os.path.join(MTLS_CERT_DIR, 'clientencrypted.key'),
|
'client_certificate_key': os.path.join(MTLS_CERT_DIR, 'clientencrypted.key'),
|
||||||
'client_certificate_password': 'foobar',
|
'client_certificate_password': 'foobar',
|
||||||
},
|
},
|
||||||
))
|
), ids=['combined_nopass', 'nocombined_nopass', 'combined_pass', 'nocombined_pass'])
|
||||||
def test_mtls(self, handler, client_cert):
|
def test_mtls(self, handler, client_cert):
|
||||||
with handler(
|
with handler(
|
||||||
# Disable client-side validation of unacceptable self-signed testcert.pem
|
# Disable client-side validation of unacceptable self-signed testcert.pem
|
||||||
|
@ -366,6 +366,15 @@ class TestWebsSocketRequestHandlerConformance:
|
||||||
) as rh:
|
) as rh:
|
||||||
ws_validate_and_send(rh, Request(self.mtls_wss_base_url)).close()
|
ws_validate_and_send(rh, Request(self.mtls_wss_base_url)).close()
|
||||||
|
|
||||||
|
def test_mtls_required(self, handler):
|
||||||
|
with handler(
|
||||||
|
# Disable client-side validation of unacceptable self-signed testcert.pem
|
||||||
|
# The test is of a check on the server side, so unaffected
|
||||||
|
verify=False,
|
||||||
|
) as rh:
|
||||||
|
with pytest.raises(SSLError):
|
||||||
|
ws_validate_and_send(rh, Request(self.mtls_wss_base_url))
|
||||||
|
|
||||||
def test_request_disable_proxy(self, handler):
|
def test_request_disable_proxy(self, handler):
|
||||||
for proxy_proto in handler._SUPPORTED_PROXY_SCHEMES or ['ws']:
|
for proxy_proto in handler._SUPPORTED_PROXY_SCHEMES or ['ws']:
|
||||||
# Given handler is configured with a proxy
|
# Given handler is configured with a proxy
|
||||||
|
|
|
@ -339,10 +339,15 @@ class YoutubeDL:
|
||||||
legacyserverconnect: Explicitly allow HTTPS connection to servers that do not
|
legacyserverconnect: Explicitly allow HTTPS connection to servers that do not
|
||||||
support RFC 5746 secure renegotiation
|
support RFC 5746 secure renegotiation
|
||||||
nocheckcertificate: Do not verify SSL certificates
|
nocheckcertificate: Do not verify SSL certificates
|
||||||
|
proxy_nocheckcertificate: Do not verify SSL certificates for HTTPS proxy
|
||||||
client_certificate: Path to client certificate file in PEM format. May include the private key
|
client_certificate: Path to client certificate file in PEM format. May include the private key
|
||||||
client_certificate_key: Path to private key file for client certificate
|
client_certificate_key: Path to private key file for client certificate
|
||||||
client_certificate_password: Password for client certificate private key, if encrypted.
|
client_certificate_password: Password for client certificate private key, if encrypted.
|
||||||
If not provided and the key is encrypted, yt-dlp will ask interactively
|
If not provided and the key is encrypted, yt-dlp will ask interactively
|
||||||
|
proxy_client_certificate: Path to client certificate file in PEM format for HTTPS proxy. May include the private key
|
||||||
|
proxy_client_certificate_key: Path to private key file for client certificate for HTTPS proxy.
|
||||||
|
proxy_client_certificate_password: Password for client certificate private key, if encrypted, for HTTPS proxy.
|
||||||
|
If not provided and the key is encrypted, yt-dlp will ask interactively
|
||||||
prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
|
prefer_insecure: Use HTTP instead of HTTPS to retrieve information.
|
||||||
(Only supported by some extractors)
|
(Only supported by some extractors)
|
||||||
enable_file_urls: Enable file:// URLs. This is disabled by default for security reasons.
|
enable_file_urls: Enable file:// URLs. This is disabled by default for security reasons.
|
||||||
|
@ -4218,6 +4223,7 @@ class YoutubeDL:
|
||||||
proxies=proxies,
|
proxies=proxies,
|
||||||
prefer_system_certs='no-certifi' in self.params['compat_opts'],
|
prefer_system_certs='no-certifi' in self.params['compat_opts'],
|
||||||
verify=not self.params.get('nocheckcertificate'),
|
verify=not self.params.get('nocheckcertificate'),
|
||||||
|
proxy_verify=not self.params.get('proxy_nocheckcertificate'),
|
||||||
**traverse_obj(self.params, {
|
**traverse_obj(self.params, {
|
||||||
'verbose': 'debug_printtraffic',
|
'verbose': 'debug_printtraffic',
|
||||||
'source_address': 'source_address',
|
'source_address': 'source_address',
|
||||||
|
@ -4230,6 +4236,11 @@ class YoutubeDL:
|
||||||
'client_certificate_key': 'client_certificate_key',
|
'client_certificate_key': 'client_certificate_key',
|
||||||
'client_certificate_password': 'client_certificate_password',
|
'client_certificate_password': 'client_certificate_password',
|
||||||
},
|
},
|
||||||
|
'proxy_client_cert': {
|
||||||
|
'client_certificate': 'proxy_client_certificate',
|
||||||
|
'client_certificate_key': 'proxy_client_certificate_key',
|
||||||
|
'client_certificate_password': 'proxy_client_certificate_password',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
director.preferences.update(preferences or [])
|
director.preferences.update(preferences or [])
|
||||||
|
|
|
@ -799,6 +799,9 @@ def parse_options(argv=None):
|
||||||
'client_certificate': opts.client_certificate,
|
'client_certificate': opts.client_certificate,
|
||||||
'client_certificate_key': opts.client_certificate_key,
|
'client_certificate_key': opts.client_certificate_key,
|
||||||
'client_certificate_password': opts.client_certificate_password,
|
'client_certificate_password': opts.client_certificate_password,
|
||||||
|
'proxy_client_certificate': opts.proxy_client_certificate,
|
||||||
|
'proxy_client_certificate_key': opts.proxy_client_certificate_key,
|
||||||
|
'proxy_client_certificate_password': opts.proxy_client_certificate_password,
|
||||||
'quiet': opts.quiet,
|
'quiet': opts.quiet,
|
||||||
'no_warnings': opts.no_warnings,
|
'no_warnings': opts.no_warnings,
|
||||||
'forceurl': opts.geturl,
|
'forceurl': opts.geturl,
|
||||||
|
@ -911,6 +914,7 @@ def parse_options(argv=None):
|
||||||
'cookiesfrombrowser': opts.cookiesfrombrowser,
|
'cookiesfrombrowser': opts.cookiesfrombrowser,
|
||||||
'legacyserverconnect': opts.legacy_server_connect,
|
'legacyserverconnect': opts.legacy_server_connect,
|
||||||
'nocheckcertificate': opts.no_check_certificate,
|
'nocheckcertificate': opts.no_check_certificate,
|
||||||
|
'proxy_nocheckcertificate': opts.proxy_no_check_certificate,
|
||||||
'prefer_insecure': opts.prefer_insecure,
|
'prefer_insecure': opts.prefer_insecure,
|
||||||
'enable_file_urls': opts.enable_file_urls,
|
'enable_file_urls': opts.enable_file_urls,
|
||||||
'http_headers': opts.headers,
|
'http_headers': opts.headers,
|
||||||
|
|
|
@ -187,7 +187,7 @@ class CurlCFFIRH(ImpersonateRequestHandler, InstanceStoreMixin):
|
||||||
# curl_cffi does not currently set these for proxies
|
# curl_cffi does not currently set these for proxies
|
||||||
session.curl.setopt(CurlOpt.PROXY_CAINFO, certifi.where())
|
session.curl.setopt(CurlOpt.PROXY_CAINFO, certifi.where())
|
||||||
|
|
||||||
if not self.verify:
|
if not self.proxy_verify:
|
||||||
session.curl.setopt(CurlOpt.PROXY_SSL_VERIFYPEER, 0)
|
session.curl.setopt(CurlOpt.PROXY_SSL_VERIFYPEER, 0)
|
||||||
session.curl.setopt(CurlOpt.PROXY_SSL_VERIFYHOST, 0)
|
session.curl.setopt(CurlOpt.PROXY_SSL_VERIFYHOST, 0)
|
||||||
|
|
||||||
|
@ -202,6 +202,15 @@ class CurlCFFIRH(ImpersonateRequestHandler, InstanceStoreMixin):
|
||||||
if client_certificate_password:
|
if client_certificate_password:
|
||||||
session.curl.setopt(CurlOpt.KEYPASSWD, client_certificate_password)
|
session.curl.setopt(CurlOpt.KEYPASSWD, client_certificate_password)
|
||||||
|
|
||||||
|
if self._proxy_client_cert:
|
||||||
|
session.curl.setopt(CurlOpt.PROXY_SSLCERT, self._proxy_client_cert['client_certificate'])
|
||||||
|
proxy_client_certificate_key = self._proxy_client_cert.get('client_certificate_key')
|
||||||
|
proxy_client_certificate_password = self._proxy_client_cert.get('client_certificate_password')
|
||||||
|
if proxy_client_certificate_key:
|
||||||
|
session.curl.setopt(CurlOpt.PROXY_SSLKEY, proxy_client_certificate_key)
|
||||||
|
if proxy_client_certificate_password:
|
||||||
|
session.curl.setopt(CurlOpt.PROXY_KEYPASSWD, proxy_client_certificate_password)
|
||||||
|
|
||||||
timeout = self._calculate_timeout(request)
|
timeout = self._calculate_timeout(request)
|
||||||
|
|
||||||
# set CURLOPT_LOW_SPEED_LIMIT and CURLOPT_LOW_SPEED_TIME to act as a read timeout. [1]
|
# set CURLOPT_LOW_SPEED_LIMIT and CURLOPT_LOW_SPEED_TIME to act as a read timeout. [1]
|
||||||
|
@ -243,6 +252,8 @@ class CurlCFFIRH(ImpersonateRequestHandler, InstanceStoreMixin):
|
||||||
or (e.code == CurlECode.RECV_ERROR and 'CONNECT' in str(e))
|
or (e.code == CurlECode.RECV_ERROR and 'CONNECT' in str(e))
|
||||||
):
|
):
|
||||||
raise ProxyError(cause=e) from e
|
raise ProxyError(cause=e) from e
|
||||||
|
elif e.code == CurlECode.RECV_ERROR and 'SSL' in str(e):
|
||||||
|
raise SSLError(cause=e) from e
|
||||||
else:
|
else:
|
||||||
raise TransportError(cause=e) from e
|
raise TransportError(cause=e) from e
|
||||||
|
|
||||||
|
|
|
@ -301,6 +301,7 @@ class RequestsRH(RequestHandler, InstanceStoreMixin):
|
||||||
session = RequestsSession()
|
session = RequestsSession()
|
||||||
http_adapter = RequestsHTTPAdapter(
|
http_adapter = RequestsHTTPAdapter(
|
||||||
ssl_context=self._make_sslcontext(legacy_ssl_support=legacy_ssl_support),
|
ssl_context=self._make_sslcontext(legacy_ssl_support=legacy_ssl_support),
|
||||||
|
proxy_ssl_context=self._make_proxy_sslcontext(),
|
||||||
source_address=self.source_address,
|
source_address=self.source_address,
|
||||||
max_retries=urllib3.util.retry.Retry(False),
|
max_retries=urllib3.util.retry.Retry(False),
|
||||||
)
|
)
|
||||||
|
|
|
@ -187,10 +187,14 @@ class RequestHandler(abc.ABC):
|
||||||
@param source_address: Client-side IP address to bind to for requests.
|
@param source_address: Client-side IP address to bind to for requests.
|
||||||
@param verbose: Print debug request and traffic information to stdout.
|
@param verbose: Print debug request and traffic information to stdout.
|
||||||
@param prefer_system_certs: Whether to prefer system certificates over other means (e.g. certifi).
|
@param prefer_system_certs: Whether to prefer system certificates over other means (e.g. certifi).
|
||||||
@param client_cert: SSL client certificate configuration.
|
@param client_cert: SSL client certificate configuration.z
|
||||||
dict with {client_certificate, client_certificate_key, client_certificate_password}
|
dict with {client_certificate, client_certificate_key, client_certificate_password}
|
||||||
|
@param proxy_client_cert: SSL client certificate configuration for proxy connections.
|
||||||
|
dict with {client_certificate, client_certificate_key, client_certificate_password}
|
||||||
@param verify: Verify SSL certificates
|
@param verify: Verify SSL certificates
|
||||||
|
@param proxy_verify: Verify SSL certificates of proxy connections
|
||||||
@param legacy_ssl_support: Enable legacy SSL options such as legacy server connect and older cipher support.
|
@param legacy_ssl_support: Enable legacy SSL options such as legacy server connect and older cipher support.
|
||||||
|
@param proxy_legacy_ssl_support: Enable legacy SSL options such as legacy server connect and older cipher support for proxy connections.
|
||||||
|
|
||||||
Some configuration options may be available for individual Requests too. In this case,
|
Some configuration options may be available for individual Requests too. In this case,
|
||||||
either the Request configuration option takes precedence or they are merged.
|
either the Request configuration option takes precedence or they are merged.
|
||||||
|
@ -230,8 +234,11 @@ class RequestHandler(abc.ABC):
|
||||||
verbose: bool = False,
|
verbose: bool = False,
|
||||||
prefer_system_certs: bool = False,
|
prefer_system_certs: bool = False,
|
||||||
client_cert: dict[str, str | None] | None = None,
|
client_cert: dict[str, str | None] | None = None,
|
||||||
|
proxy_client_cert: dict[str, str | None] | None = None,
|
||||||
verify: bool = True,
|
verify: bool = True,
|
||||||
|
proxy_verify: bool = True,
|
||||||
legacy_ssl_support: bool = False,
|
legacy_ssl_support: bool = False,
|
||||||
|
proxy_legacy_ssl_support: bool = False,
|
||||||
**_,
|
**_,
|
||||||
):
|
):
|
||||||
|
|
||||||
|
@ -244,8 +251,11 @@ class RequestHandler(abc.ABC):
|
||||||
self.verbose = verbose
|
self.verbose = verbose
|
||||||
self.prefer_system_certs = prefer_system_certs
|
self.prefer_system_certs = prefer_system_certs
|
||||||
self._client_cert = client_cert or {}
|
self._client_cert = client_cert or {}
|
||||||
|
self._proxy_client_cert = proxy_client_cert or {}
|
||||||
self.verify = verify
|
self.verify = verify
|
||||||
|
self.proxy_verify = proxy_verify
|
||||||
self.legacy_ssl_support = legacy_ssl_support
|
self.legacy_ssl_support = legacy_ssl_support
|
||||||
|
self.proxy_legacy_ssl_support = proxy_legacy_ssl_support
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def _make_sslcontext(self, legacy_ssl_support=None):
|
def _make_sslcontext(self, legacy_ssl_support=None):
|
||||||
|
@ -256,6 +266,14 @@ class RequestHandler(abc.ABC):
|
||||||
**self._client_cert,
|
**self._client_cert,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _make_proxy_sslcontext(self, legacy_ssl_support=None):
|
||||||
|
return make_ssl_context(
|
||||||
|
verify=self.proxy_verify,
|
||||||
|
legacy_support=legacy_ssl_support if legacy_ssl_support is not None else self.proxy_legacy_ssl_support,
|
||||||
|
use_certifi=not self.prefer_system_certs,
|
||||||
|
**self._proxy_client_cert,
|
||||||
|
)
|
||||||
|
|
||||||
def _merge_headers(self, request_headers):
|
def _merge_headers(self, request_headers):
|
||||||
return HTTPHeaderDict(self.headers, request_headers)
|
return HTTPHeaderDict(self.headers, request_headers)
|
||||||
|
|
||||||
|
|
|
@ -790,6 +790,20 @@ def create_parser():
|
||||||
help='Password for client certificate private key, if encrypted. '
|
help='Password for client certificate private key, if encrypted. '
|
||||||
'If not provided, and the key is encrypted, yt-dlp will ask interactively')
|
'If not provided, and the key is encrypted, yt-dlp will ask interactively')
|
||||||
|
|
||||||
|
authentication.add_option(
|
||||||
|
'--proxy-client-certificate',
|
||||||
|
dest='proxy_client_certificate', metavar='CERTFILE',
|
||||||
|
help='Path to client certificate file in PEM format for HTTPS proxy. May include the private key')
|
||||||
|
authentication.add_option(
|
||||||
|
'--proxy-client-certificate-key',
|
||||||
|
dest='proxy_client_certificate_key', metavar='KEYFILE',
|
||||||
|
help='Path to private key file for client certificate for HTTPS proxy')
|
||||||
|
authentication.add_option(
|
||||||
|
'--proxy-client-certificate-password',
|
||||||
|
dest='proxy_client_certificate_password', metavar='PASSWORD',
|
||||||
|
help='Password for client certificate private key, if encrypted, for HTTPS proxy. '
|
||||||
|
'If not provided, and the key is encrypted, yt-dlp will ask interactively')
|
||||||
|
|
||||||
video_format = optparse.OptionGroup(parser, 'Video Format Options')
|
video_format = optparse.OptionGroup(parser, 'Video Format Options')
|
||||||
video_format.add_option(
|
video_format.add_option(
|
||||||
'-f', '--format',
|
'-f', '--format',
|
||||||
|
@ -1094,6 +1108,10 @@ def create_parser():
|
||||||
'--no-check-certificates',
|
'--no-check-certificates',
|
||||||
action='store_true', dest='no_check_certificate', default=False,
|
action='store_true', dest='no_check_certificate', default=False,
|
||||||
help='Suppress HTTPS certificate validation')
|
help='Suppress HTTPS certificate validation')
|
||||||
|
workarounds.add_option(
|
||||||
|
'--proxy-no-check-certificates',
|
||||||
|
action='store_true', dest='proxy_no_check_certificate', default=False,
|
||||||
|
help='Suppress HTTPS Proxy certificate validation')
|
||||||
workarounds.add_option(
|
workarounds.add_option(
|
||||||
'--prefer-insecure', '--prefer-unsecure',
|
'--prefer-insecure', '--prefer-unsecure',
|
||||||
action='store_true', dest='prefer_insecure',
|
action='store_true', dest='prefer_insecure',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user