diff options
Diffstat (limited to 'searx/network/network.py')
| -rw-r--r-- | searx/network/network.py | 143 |
1 files changed, 70 insertions, 73 deletions
diff --git a/searx/network/network.py b/searx/network/network.py index eb53afb7f..42828ce41 100644 --- a/searx/network/network.py +++ b/searx/network/network.py @@ -13,11 +13,16 @@ import asyncio import ipaddress from itertools import cycle -import httpx +from curl_cffi import CurlHttpVersion +from curl_cffi.requests.exceptions import ( + ConnectionError as CurlConnectionError, + ProxyError, + RequestException, +) from searx import logger, sxng_debug from searx.extended_types import SXNG_Response -from .client import new_client, get_loop, AsyncHTTPTransportNoHttp +from .client import DEFAULT_IMPERSONATE, AsyncClient, new_client, get_loop from .raise_for_httperror import raise_for_httperror @@ -48,9 +53,8 @@ class Network: 'enable_http', 'verify', 'enable_http2', + 'enable_http3', 'max_connections', - 'max_keepalive_connections', - 'keepalive_expiry', 'local_addresses', 'proxies', 'using_tor_proxy', @@ -64,6 +68,7 @@ class Network: ) _TOR_CHECK_RESULT = {} + _CLIENT_KWARGS = ('verify', 'max_redirects', 'impersonate', 'curl_options', 'enable_http3') def __init__( # pylint: disable=too-many-arguments @@ -71,9 +76,8 @@ class Network: enable_http: bool = True, verify: bool = True, enable_http2: bool = False, + enable_http3: bool = False, max_connections: int = None, # pyright: ignore[reportArgumentType] - max_keepalive_connections: int = None, # pyright: ignore[reportArgumentType] - keepalive_expiry: float = None, # pyright: ignore[reportArgumentType] proxies: str | dict[str, str] | None = None, using_tor_proxy: bool = False, local_addresses: str | list[str] | None = None, @@ -86,9 +90,8 @@ class Network: self.enable_http = enable_http self.verify = verify self.enable_http2 = enable_http2 + self.enable_http3 = enable_http3 self.max_connections = max_connections - self.max_keepalive_connections = max_keepalive_connections - self.keepalive_expiry = keepalive_expiry self.proxies = proxies self.using_tor_proxy = using_tor_proxy self.local_addresses = local_addresses @@ -137,7 +140,6 @@ class Network: def iter_proxies(self) -> Generator[tuple[str, list[str]]]: if not self.proxies: return - # https://www.python-httpx.org/compatibility/#proxy-keys if isinstance(self.proxies, str): yield 'all://', [self.proxies] else: @@ -155,62 +157,73 @@ class Network: # pylint: disable=stop-iteration-return yield tuple((pattern, next(proxy_url_cycle)) for pattern, proxy_url_cycle in proxy_settings.items()) - async def log_response(self, response: httpx.Response): + _HTTP_VERSION = { + int(CurlHttpVersion.V1_0): "HTTP/1.0", + int(CurlHttpVersion.V1_1): "HTTP/1.1", + int(CurlHttpVersion.V2_0): "HTTP/2", + int(CurlHttpVersion.V2TLS): "HTTP/2", + int(CurlHttpVersion.V2_PRIOR_KNOWLEDGE): "HTTP/2", + int(CurlHttpVersion.V3): "HTTP/3", + int(CurlHttpVersion.V3ONLY): "HTTP/3", + } + + async def log_response(self, response: SXNG_Response): request = response.request - status = f"{response.status_code} {response.reason_phrase}" - response_line = f"{response.http_version} {status}" + http_version = self._HTTP_VERSION.get(response.http_version, str(response.http_version)) + status = f"{response.status_code} {response.reason}" + response_line = f"{http_version} {status}" content_type = response.headers.get("Content-Type") content_type = f' ({content_type})' if content_type else '' - self._logger.debug(f'HTTP Request: {request.method} {request.url} "{response_line}"{content_type}') + method = request.method if request else "?" + url = request.url if request else response.url + self._logger.debug(f'HTTP Request: {method} {url} "{response_line}"{content_type}') @staticmethod - async def check_tor_proxy(client: httpx.AsyncClient, proxies) -> bool: + async def check_tor_proxy(client: AsyncClient, proxies) -> bool: if proxies in Network._TOR_CHECK_RESULT: return Network._TOR_CHECK_RESULT[proxies] - result = True - # ignore client._transport because it is not used with all:// - for transport in client._mounts.values(): # pylint: disable=protected-access - if isinstance(transport, AsyncHTTPTransportNoHttp): - continue - if getattr(transport, "_pool") and getattr( - # pylint: disable=protected-access - transport._pool, # type: ignore - "_rdns", - False, - ): - continue + if not proxies or not all(url.startswith('socks5h://') for _, url in proxies): + Network._TOR_CHECK_RESULT[proxies] = False return False + response = await client.get("https://check.torproject.org/api/ip", timeout=60) - if not response.json()["IsTor"]: - result = False + result = bool(response.json()["IsTor"]) Network._TOR_CHECK_RESULT[proxies] = result return result - async def get_client(self, verify: bool | None = None, max_redirects: int | None = None) -> httpx.AsyncClient: + async def get_client( + self, + verify: bool | None = None, + max_redirects: int | None = None, + impersonate: str | None = None, + curl_options: dict[int, t.Any] | None = None, + enable_http3: bool | None = None, + ) -> AsyncClient: verify = self.verify if verify is None else verify max_redirects = self.max_redirects if max_redirects is None else max_redirects + impersonate = impersonate or DEFAULT_IMPERSONATE + enable_http3 = self.enable_http3 if enable_http3 is None else enable_http3 local_address = next(self._local_addresses_cycle) proxies = next(self._proxies_cycle) # is a tuple so it can be part of the key - key = (verify, max_redirects, local_address, proxies) - hook_log_response = self.log_response if sxng_debug else None + curl_key = tuple(sorted((int(k), v) for k, v in (curl_options or {}).items())) + key = (verify, max_redirects, local_address, proxies, impersonate, curl_key, enable_http3) if key not in self._clients or self._clients[key].is_closed: client = new_client( self.enable_http, verify, self.enable_http2, + enable_http3, self.max_connections, - self.max_keepalive_connections, - self.keepalive_expiry, dict(proxies), local_address, - 0, max_redirects, - hook_log_response, + impersonate=impersonate, + curl_options=curl_options, ) if self.using_tor_proxy and not await self.check_tor_proxy(client, proxies): await client.aclose() - raise httpx.ProxyError('Network configuration problem: not using Tor') + raise ProxyError('Network configuration problem: not using Tor') self._clients[key] = client return self._clients[key] @@ -218,22 +231,14 @@ class Network: async def close_client(client): try: await client.aclose() - except httpx.HTTPError: + except RequestException: pass await asyncio.gather(*[close_client(client) for client in self._clients.values()], return_exceptions=False) @staticmethod def extract_kwargs_clients(kwargs: dict[str, t.Any]) -> dict[str, t.Any]: - kwargs_clients: dict[str, t.Any] = {} - if 'verify' in kwargs: - kwargs_clients['verify'] = kwargs.pop('verify') - if 'max_redirects' in kwargs: - kwargs_clients['max_redirects'] = kwargs.pop('max_redirects') - if 'allow_redirects' in kwargs: - # see https://github.com/encode/httpx/pull/1808 - kwargs['follow_redirects'] = kwargs.pop('allow_redirects') - return kwargs_clients + return {key: kwargs.pop(key) for key in Network._CLIENT_KWARGS if key in kwargs} @staticmethod def extract_do_raise_for_httperror(kwargs: dict[str, t.Any]): @@ -243,23 +248,18 @@ class Network: del kwargs['raise_for_httperror'] return do_raise_for_httperror - def patch_response(self, response: httpx.Response, do_raise_for_httperror: bool) -> SXNG_Response: - if isinstance(response, httpx.Response): - response = t.cast(SXNG_Response, response) - # requests compatibility (response is not streamed) - # see also https://www.python-httpx.org/compatibility/#checking-for-4xx5xx-responses - response.ok = not response.is_error - - # raise an exception - if do_raise_for_httperror: - try: - raise_for_httperror(response) - except: - self._logger.warning(f"HTTP Request failed: {response.request.method} {response.request.url}") - raise + def patch_response(self, response: SXNG_Response, do_raise_for_httperror: bool) -> SXNG_Response: + if do_raise_for_httperror: + try: + raise_for_httperror(response) + except: + method = response.request.method if response.request else "?" + url = response.request.url if response.request else response.url + self._logger.warning(f"HTTP Request failed: {method} {url}") + raise return response - def is_valid_response(self, response: httpx.Response): + def is_valid_response(self, response: SXNG_Response): # pylint: disable=too-many-boolean-expressions if ( (self.retry_on_http_error is True and 400 <= response.status_code <= 599) @@ -276,26 +276,29 @@ class Network: kwargs_clients = Network.extract_kwargs_clients(kwargs) while retries >= 0: # pragma: no cover client = await self.get_client(**kwargs_clients) - cookies = kwargs.pop("cookies", None) - client.cookies = httpx.Cookies(cookies) try: + method = method.upper() + client.check_url(url) if stream: return client.stream(method, url, **kwargs) response = await client.request(method, url, **kwargs) + if sxng_debug: + await self.log_response(response) if self.is_valid_response(response) or retries <= 0: return self.patch_response(response, do_raise_for_httperror) - except httpx.RemoteProtocolError as e: + await client.aclose() + except CurlConnectionError as e: if not was_disconnected: # the server has closed the connection: # try again without decreasing the retries variable & with a new HTTP client was_disconnected = True await client.aclose() - self._logger.warning('httpx.RemoteProtocolError: the server has disconnected, retrying') + self._logger.warning('ConnectionError: the server has disconnected, retrying') continue if retries <= 0: raise e - except (httpx.RequestError, httpx.HTTPStatusError) as e: + except RequestException as e: if retries <= 0: raise e retries -= 1 @@ -346,15 +349,12 @@ def initialize( settings_engines = settings_engines or settings['engines'] settings_outgoing = settings_outgoing or settings['outgoing'] - # default parameters for AsyncHTTPTransport - # see https://github.com/encode/httpx/blob/e05a5372eb6172287458b37447c30f650047e1b8/httpx/_transports/default.py#L108-L121 # pylint: disable=line-too-long default_params: dict[str, t.Any] = { 'enable_http': False, 'verify': settings_outgoing['verify'], 'enable_http2': settings_outgoing['enable_http2'], + 'enable_http3': False, 'max_connections': settings_outgoing['pool_connections'], - 'max_keepalive_connections': settings_outgoing['pool_maxsize'], - 'keepalive_expiry': settings_outgoing['keepalive_expiry'], 'local_addresses': settings_outgoing['source_ips'], 'using_tor_proxy': settings_outgoing['using_tor_proxy'], 'proxies': settings_outgoing['proxies'], @@ -424,9 +424,6 @@ def initialize( def done(): """Close all HTTP client - Avoid a warning at exit - See https://github.com/encode/httpx/pull/2026 - Note: since Network.aclose has to be async, it is not possible to call this method on Network.__del__ So Network.aclose is called here using atexit.register """ |
