2023-11-20 09:04:04 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-03-22 23:27:10 +01:00
|
|
|
import contextlib
|
2024-07-02 00:51:27 +02:00
|
|
|
import functools
|
2023-11-20 09:04:04 +01:00
|
|
|
import io
|
|
|
|
import logging
|
|
|
|
import ssl
|
|
|
|
import sys
|
|
|
|
|
2023-12-30 22:27:36 +01:00
|
|
|
from ._helper import (
|
|
|
|
create_connection,
|
|
|
|
create_socks_proxy_socket,
|
|
|
|
make_socks_proxy_opts,
|
|
|
|
select_proxy,
|
|
|
|
)
|
|
|
|
from .common import Features, Response, register_rh
|
2023-11-20 09:04:04 +01:00
|
|
|
from .exceptions import (
|
|
|
|
CertificateVerifyError,
|
|
|
|
HTTPError,
|
2023-12-30 22:27:36 +01:00
|
|
|
ProxyError,
|
2023-11-20 09:04:04 +01:00
|
|
|
RequestError,
|
|
|
|
SSLError,
|
2023-12-30 22:27:36 +01:00
|
|
|
TransportError,
|
2023-11-20 09:04:04 +01:00
|
|
|
)
|
|
|
|
from .websocket import WebSocketRequestHandler, WebSocketResponse
|
|
|
|
from ..dependencies import websockets
|
|
|
|
from ..socks import ProxyError as SocksProxyError
|
2023-12-30 22:27:36 +01:00
|
|
|
from ..utils import int_or_none
|
2023-11-20 09:04:04 +01:00
|
|
|
|
|
|
|
if not websockets:
|
|
|
|
raise ImportError('websockets is not installed')
|
|
|
|
|
|
|
|
import websockets.version
|
|
|
|
|
|
|
|
websockets_version = tuple(map(int_or_none, websockets.version.version.split('.')))
|
2024-09-28 00:46:22 +02:00
|
|
|
if websockets_version < (13, 0):
|
|
|
|
raise ImportError('Only websockets>=13.0 is supported')
|
2023-11-20 09:04:04 +01:00
|
|
|
|
|
|
|
import websockets.sync.client
|
|
|
|
from websockets.uri import parse_uri
|
|
|
|
|
2024-03-22 23:27:10 +01:00
|
|
|
# In websockets Connection, recv_exc and recv_events_exc are defined
|
|
|
|
# after the recv events handler thread is started [1].
|
|
|
|
# On our CI using PyPy, in some cases a race condition may occur
|
|
|
|
# where the recv events handler thread tries to use these attributes before they are defined [2].
|
|
|
|
# 1: https://github.com/python-websockets/websockets/blame/de768cf65e7e2b1a3b67854fb9e08816a5ff7050/src/websockets/sync/connection.py#L93
|
|
|
|
# 2: "AttributeError: 'ClientConnection' object has no attribute 'recv_events_exc'. Did you mean: 'recv_events'?"
|
|
|
|
import websockets.sync.connection # isort: split
|
|
|
|
with contextlib.suppress(Exception):
|
|
|
|
websockets.sync.connection.Connection.recv_exc = None
|
|
|
|
|
2023-11-20 09:04:04 +01:00
|
|
|
|
|
|
|
class WebsocketsResponseAdapter(WebSocketResponse):
|
|
|
|
|
2024-03-22 23:27:10 +01:00
|
|
|
def __init__(self, ws: websockets.sync.client.ClientConnection, url):
|
2023-11-20 09:04:04 +01:00
|
|
|
super().__init__(
|
2024-03-22 23:27:10 +01:00
|
|
|
fp=io.BytesIO(ws.response.body or b''),
|
2023-11-20 09:04:04 +01:00
|
|
|
url=url,
|
2024-03-22 23:27:10 +01:00
|
|
|
headers=ws.response.headers,
|
|
|
|
status=ws.response.status_code,
|
|
|
|
reason=ws.response.reason_phrase,
|
2023-11-20 09:04:04 +01:00
|
|
|
)
|
2024-03-22 23:27:10 +01:00
|
|
|
self._ws = ws
|
2023-11-20 09:04:04 +01:00
|
|
|
|
|
|
|
def close(self):
|
2024-03-22 23:27:10 +01:00
|
|
|
self._ws.close()
|
2023-11-20 09:04:04 +01:00
|
|
|
super().close()
|
|
|
|
|
|
|
|
def send(self, message):
|
|
|
|
# https://websockets.readthedocs.io/en/stable/reference/sync/client.html#websockets.sync.client.ClientConnection.send
|
|
|
|
try:
|
2024-03-22 23:27:10 +01:00
|
|
|
return self._ws.send(message)
|
2023-11-20 09:04:04 +01:00
|
|
|
except (websockets.exceptions.WebSocketException, RuntimeError, TimeoutError) as e:
|
|
|
|
raise TransportError(cause=e) from e
|
|
|
|
except SocksProxyError as e:
|
|
|
|
raise ProxyError(cause=e) from e
|
|
|
|
except TypeError as e:
|
|
|
|
raise RequestError(cause=e) from e
|
|
|
|
|
|
|
|
def recv(self):
|
|
|
|
# https://websockets.readthedocs.io/en/stable/reference/sync/client.html#websockets.sync.client.ClientConnection.recv
|
|
|
|
try:
|
2024-03-22 23:27:10 +01:00
|
|
|
return self._ws.recv()
|
2023-11-20 09:04:04 +01:00
|
|
|
except SocksProxyError as e:
|
|
|
|
raise ProxyError(cause=e) from e
|
|
|
|
except (websockets.exceptions.WebSocketException, RuntimeError, TimeoutError) as e:
|
|
|
|
raise TransportError(cause=e) from e
|
|
|
|
|
|
|
|
|
|
|
|
@register_rh
|
|
|
|
class WebsocketsRH(WebSocketRequestHandler):
|
|
|
|
"""
|
|
|
|
Websockets request handler
|
|
|
|
https://websockets.readthedocs.io
|
|
|
|
https://github.com/python-websockets/websockets
|
|
|
|
"""
|
|
|
|
_SUPPORTED_URL_SCHEMES = ('wss', 'ws')
|
|
|
|
_SUPPORTED_PROXY_SCHEMES = ('socks4', 'socks4a', 'socks5', 'socks5h')
|
|
|
|
_SUPPORTED_FEATURES = (Features.ALL_PROXY, Features.NO_PROXY)
|
|
|
|
RH_NAME = 'websockets'
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2024-02-17 23:32:34 +01:00
|
|
|
self.__logging_handlers = {}
|
2023-11-20 09:04:04 +01:00
|
|
|
for name in ('websockets.client', 'websockets.server'):
|
|
|
|
logger = logging.getLogger(name)
|
|
|
|
handler = logging.StreamHandler(stream=sys.stdout)
|
|
|
|
handler.setFormatter(logging.Formatter(f'{self.RH_NAME}: %(message)s'))
|
2024-02-17 23:32:34 +01:00
|
|
|
self.__logging_handlers[name] = handler
|
2023-11-20 09:04:04 +01:00
|
|
|
logger.addHandler(handler)
|
|
|
|
if self.verbose:
|
|
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
|
|
|
|
def _check_extensions(self, extensions):
|
|
|
|
super()._check_extensions(extensions)
|
|
|
|
extensions.pop('timeout', None)
|
|
|
|
extensions.pop('cookiejar', None)
|
2024-07-14 01:22:43 +02:00
|
|
|
extensions.pop('legacy_ssl', None)
|
2023-11-20 09:04:04 +01:00
|
|
|
|
2024-02-17 23:32:34 +01:00
|
|
|
def close(self):
|
|
|
|
# Remove the logging handler that contains a reference to our logger
|
|
|
|
# See: https://github.com/yt-dlp/yt-dlp/issues/8922
|
|
|
|
for name, handler in self.__logging_handlers.items():
|
|
|
|
logging.getLogger(name).removeHandler(handler)
|
|
|
|
|
2023-11-20 09:04:04 +01:00
|
|
|
def _send(self, request):
|
2024-03-17 04:52:38 +01:00
|
|
|
timeout = self._calculate_timeout(request)
|
2023-11-20 09:04:04 +01:00
|
|
|
headers = self._merge_headers(request.headers)
|
|
|
|
if 'cookie' not in headers:
|
2024-03-17 04:52:38 +01:00
|
|
|
cookiejar = self._get_cookiejar(request)
|
2023-11-20 09:04:04 +01:00
|
|
|
cookie_header = cookiejar.get_cookie_header(request.url)
|
|
|
|
if cookie_header:
|
|
|
|
headers['cookie'] = cookie_header
|
|
|
|
|
|
|
|
wsuri = parse_uri(request.url)
|
|
|
|
create_conn_kwargs = {
|
|
|
|
'source_address': (self.source_address, 0) if self.source_address else None,
|
2024-06-12 01:09:58 +02:00
|
|
|
'timeout': timeout,
|
2023-11-20 09:04:04 +01:00
|
|
|
}
|
2024-03-17 04:52:38 +01:00
|
|
|
proxy = select_proxy(request.url, self._get_proxies(request))
|
2023-11-20 09:04:04 +01:00
|
|
|
try:
|
|
|
|
if proxy:
|
|
|
|
socks_proxy_options = make_socks_proxy_opts(proxy)
|
|
|
|
sock = create_connection(
|
|
|
|
address=(socks_proxy_options['addr'], socks_proxy_options['port']),
|
|
|
|
_create_socket_func=functools.partial(
|
|
|
|
create_socks_proxy_socket, (wsuri.host, wsuri.port), socks_proxy_options),
|
2024-06-12 01:09:58 +02:00
|
|
|
**create_conn_kwargs,
|
2023-11-20 09:04:04 +01:00
|
|
|
)
|
|
|
|
else:
|
|
|
|
sock = create_connection(
|
|
|
|
address=(wsuri.host, wsuri.port),
|
2024-06-12 01:09:58 +02:00
|
|
|
**create_conn_kwargs,
|
2023-11-20 09:04:04 +01:00
|
|
|
)
|
2024-07-14 01:22:43 +02:00
|
|
|
ssl_ctx = self._make_sslcontext(legacy_ssl_support=request.extensions.get('legacy_ssl'))
|
2023-11-20 09:04:04 +01:00
|
|
|
conn = websockets.sync.client.connect(
|
|
|
|
sock=sock,
|
|
|
|
uri=request.url,
|
|
|
|
additional_headers=headers,
|
|
|
|
open_timeout=timeout,
|
|
|
|
user_agent_header=None,
|
2024-08-21 09:17:26 +02:00
|
|
|
ssl=ssl_ctx if wsuri.secure else None,
|
2023-11-20 09:04:04 +01:00
|
|
|
close_timeout=0, # not ideal, but prevents yt-dlp hanging
|
|
|
|
)
|
|
|
|
return WebsocketsResponseAdapter(conn, url=request.url)
|
|
|
|
|
|
|
|
# Exceptions as per https://websockets.readthedocs.io/en/stable/reference/sync/client.html
|
|
|
|
except SocksProxyError as e:
|
|
|
|
raise ProxyError(cause=e) from e
|
|
|
|
except websockets.exceptions.InvalidURI as e:
|
|
|
|
raise RequestError(cause=e) from e
|
|
|
|
except ssl.SSLCertVerificationError as e:
|
|
|
|
raise CertificateVerifyError(cause=e) from e
|
|
|
|
except ssl.SSLError as e:
|
|
|
|
raise SSLError(cause=e) from e
|
|
|
|
except websockets.exceptions.InvalidStatus as e:
|
|
|
|
raise HTTPError(
|
|
|
|
Response(
|
|
|
|
fp=io.BytesIO(e.response.body),
|
|
|
|
url=request.url,
|
|
|
|
headers=e.response.headers,
|
|
|
|
status=e.response.status_code,
|
|
|
|
reason=e.response.reason_phrase),
|
|
|
|
) from e
|
|
|
|
except (OSError, TimeoutError, websockets.exceptions.WebSocketException) as e:
|
|
|
|
raise TransportError(cause=e) from e
|