Compare commits

...

6 Commits

Author SHA1 Message Date
coletdjnz
d47f6f3c60
Merge d3ae5688b4 into b83ca24eb7 2024-11-10 09:28:03 +05:30
coletdjnz
d3ae5688b4
change to --proxy-no-check-certificates 2024-09-08 17:31:24 +12:00
coletdjnz
fc8b4a3be9
Expose CLI args 2024-09-08 17:27:33 +12:00
coletdjnz
a3cf32ad5b
Add proxy nocheckcertificate and client_certificate options to YoutubeDL 2024-09-08 17:23:20 +12:00
coletdjnz
40ab38b660
add legacy_ssl_support tests 2024-09-08 17:00:12 +12:00
coletdjnz
4accb0befe
[networking] Add proxy_client_cert, proxy_verify and legacy_proxy_ssl_support options 2024-09-08 15:55:55 +12:00
10 changed files with 294 additions and 19 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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 [])

View File

@ -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,

View File

@ -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

View File

@ -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),
) )

View File

@ -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}
@param proxy_client_cert: SSL client certificate configuration for proxy connections.
dict with {client_certificate, client_certificate_key, client_certificate_password} 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)

View File

@ -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',