5from types
import TracebackType
10from ssl
import SSLContext
14from httpx_socks
import AsyncProxyTransport
15from python_socks
import parse_proxy_url, ProxyConnectionError, ProxyTimeoutError, ProxyError
17from searx
import logger
19CertTypes = str | tuple[str, str] | tuple[str, str, str]
20SslContextKeyType = tuple[str |
None, CertTypes |
None, bool, bool]
22logger = logger.getChild(
'searx.network.client')
23LOOP: asyncio.AbstractEventLoop =
None
25SSLCONTEXTS: dict[SslContextKeyType, SSLContext] = {}
29 """Shuffle httpx's default ciphers of a SSL context randomly.
31 From `What Is TLS Fingerprint and How to Bypass It`_
33 > When implementing TLS fingerprinting, servers can't operate based on a
34 > locked-in whitelist database of fingerprints. New fingerprints appear
35 > when web clients or TLS libraries release new versions. So, they have to
36 > live off a blocklist database instead.
38 > It's safe to leave the first three as is but shuffle the remaining ciphers
39 > and you can bypass the TLS fingerprint check.
41 .. _What Is TLS Fingerprint and How to Bypass It:
42 https://www.zenrows.com/blog/what-is-tls-fingerprint#how-to-bypass-tls-fingerprinting
45 c_list = [cipher[
"name"]
for cipher
in ssl_context.get_ciphers()]
46 sc_list, c_list = c_list[:3], c_list[3:]
47 random.shuffle(c_list)
48 ssl_context.set_ciphers(
":".join(sc_list + c_list))
52 proxy_url: str |
None =
None, cert: CertTypes |
None =
None, verify: bool =
True, trust_env: bool =
True
54 key: SslContextKeyType = (proxy_url, cert, verify, trust_env)
55 if key
not in SSLCONTEXTS:
56 SSLCONTEXTS[key] = httpx.create_ssl_context(verify, cert, trust_env)
58 return SSLCONTEXTS[key]
64 The constructor is blank because httpx.AsyncHTTPTransport.__init__ creates an SSLContext unconditionally:
65 https://github.com/encode/httpx/blob/0f61aa58d66680c239ce43c8cdd453e7dc532bfc/httpx/_transports/default.py#L271
67 Each SSLContext consumes more than 500kb of memory, since there is about one network per engine.
69 In consequence, this class overrides all public methods
71 For reference: https://github.com/encode/httpx/issues/2298
80 raise httpx.UnsupportedProtocol(
'HTTP protocol is disabled')
90 exc_type: type[BaseException] |
None =
None,
91 exc_value: BaseException |
None =
None,
92 traceback: TracebackType |
None =
None,
98 """Fix httpx_socks.AsyncProxyTransport
100 Map python_socks exceptions to httpx.ProxyError exceptions
106 except ProxyConnectionError
as e:
107 raise httpx.ProxyError(
"ProxyConnectionError: " + str(e.strerror), request=request)
from e
108 except ProxyTimeoutError
as e:
109 raise httpx.ProxyError(
"ProxyTimeoutError: " + e.args[0], request=request)
from e
110 except ProxyError
as e:
111 raise httpx.ProxyError(
"ProxyError: " + e.args[0], request=request)
from e
115 verify: bool, http2: bool, local_address: str, proxy_url: str, limit: httpx.Limits, retries: int
122 socks5h =
'socks5h://'
123 if proxy_url.startswith(socks5h):
124 proxy_url =
'socks5://' + proxy_url[len(socks5h) :]
127 proxy_type, proxy_host, proxy_port, proxy_username, proxy_password = parse_proxy_url(proxy_url)
128 _verify =
get_sslcontexts(proxy_url,
None, verify,
True)
if verify
is True else verify
130 proxy_type=proxy_type,
131 proxy_host=proxy_host,
132 proxy_port=proxy_port,
133 username=proxy_username,
134 password=proxy_password,
139 local_address=local_address,
146 verify: bool, http2: bool, local_address: str, proxy_url: str |
None, limit: httpx.Limits, retries: int
148 _verify =
get_sslcontexts(
None,
None, verify,
True)
if verify
is True else verify
149 return httpx.AsyncHTTPTransport(
154 proxy=httpx._config.Proxy(proxy_url)
if proxy_url
else None,
155 local_address=local_address,
165 max_connections: int,
166 max_keepalive_connections: int,
167 keepalive_expiry: float,
168 proxies: dict[str, str],
172 hook_log_response: t.Callable[..., t.Any] |
None,
173) -> httpx.AsyncClient:
174 limit = httpx.Limits(
175 max_connections=max_connections,
176 max_keepalive_connections=max_keepalive_connections,
177 keepalive_expiry=keepalive_expiry,
181 mounts:
None | (dict[str, t.Any |
None]) = {}
182 for pattern, proxy_url
in proxies.items():
183 if not enable_http
and pattern.startswith(
'http://'):
185 if proxy_url.startswith(
'socks4://')
or proxy_url.startswith(
'socks5://')
or proxy_url.startswith(
'socks5h://'):
187 verify, enable_http2, local_address, proxy_url, limit, retries
190 mounts[pattern] =
get_transport(verify, enable_http2, local_address, proxy_url, limit, retries)
195 transport =
get_transport(verify, enable_http2, local_address,
None, limit, retries)
198 if hook_log_response:
199 event_hooks = {
'response': [hook_log_response]}
201 return httpx.AsyncClient(
204 max_redirects=max_redirects,
205 event_hooks=event_hooks,
218 'httpcore.connection',
224 logging.getLogger(logger_name).setLevel(logging.WARNING)
229 LOOP = asyncio.new_event_loop()
232 thread = threading.Thread(
__init__(self, *args, **kwargs)
handle_async_request(self, httpx.Request request)
None __aexit__(self, type[BaseException]|None exc_type=None, BaseException|None exc_value=None, TracebackType|None traceback=None)
handle_async_request(self, httpx.Request request)
SSLContext get_sslcontexts(str|None proxy_url=None, CertTypes|None cert=None, bool verify=True, bool trust_env=True)
shuffle_ciphers(SSLContext ssl_context)
get_transport_for_socks_proxy(bool verify, bool http2, str local_address, str proxy_url, httpx.Limits limit, int retries)
asyncio.AbstractEventLoop get_loop()
get_transport(bool verify, bool http2, str local_address, str|None proxy_url, httpx.Limits limit, int retries)
httpx.AsyncClient new_client(bool enable_http, bool verify, bool enable_http2, int max_connections, int max_keepalive_connections, float keepalive_expiry, dict[str, str] proxies, str local_address, int retries, int max_redirects, t.Callable[..., t.Any]|None hook_log_response)