5from types
import TracebackType
10from ssl
import SSLContext
14from httpx_socks
import AsyncProxyTransport
15from python_socks
import parse_proxy_url, ProxyConnectionError, ProxyTimeoutError, ProxyError
18from searx
import logger
23CertTypes = str | tuple[str, str] | tuple[str, str, str]
24SslContextKeyType = tuple[str |
None, CertTypes |
None, bool, bool]
26logger = logger.getChild(
'searx.network.client')
27LOOP: asyncio.AbstractEventLoop =
None
29SSLCONTEXTS: dict[SslContextKeyType, SSLContext] = {}
33 """Shuffle httpx's default ciphers of a SSL context randomly.
35 From `What Is TLS Fingerprint and How to Bypass It`_
37 > When implementing TLS fingerprinting, servers can't operate based on a
38 > locked-in whitelist database of fingerprints. New fingerprints appear
39 > when web clients or TLS libraries release new versions. So, they have to
40 > live off a blocklist database instead.
42 > It's safe to leave the first three as is but shuffle the remaining ciphers
43 > and you can bypass the TLS fingerprint check.
45 .. _What Is TLS Fingerprint and How to Bypass It:
46 https://www.zenrows.com/blog/what-is-tls-fingerprint#how-to-bypass-tls-fingerprinting
49 c_list = [cipher[
"name"]
for cipher
in ssl_context.get_ciphers()]
50 sc_list, c_list = c_list[:3], c_list[3:]
51 random.shuffle(c_list)
52 ssl_context.set_ciphers(
":".join(sc_list + c_list))
56 proxy_url: str |
None =
None, cert: CertTypes |
None =
None, verify: bool =
True, trust_env: bool =
True
58 key: SslContextKeyType = (proxy_url, cert, verify, trust_env)
59 if key
not in SSLCONTEXTS:
60 SSLCONTEXTS[key] = httpx.create_ssl_context(verify, cert, trust_env)
62 return SSLCONTEXTS[key]
68 The constructor is blank because httpx.AsyncHTTPTransport.__init__ creates an SSLContext unconditionally:
69 https://github.com/encode/httpx/blob/0f61aa58d66680c239ce43c8cdd453e7dc532bfc/httpx/_transports/default.py#L271
71 Each SSLContext consumes more than 500kb of memory, since there is about one network per engine.
73 In consequence, this class overrides all public methods
75 For reference: https://github.com/encode/httpx/issues/2298
84 raise httpx.UnsupportedProtocol(
'HTTP protocol is disabled')
94 exc_type: type[BaseException] |
None =
None,
95 exc_value: BaseException |
None =
None,
96 traceback: TracebackType |
None =
None,
102 """Fix httpx_socks.AsyncProxyTransport
104 Map python_socks exceptions to httpx.ProxyError exceptions
110 except ProxyConnectionError
as e:
111 raise httpx.ProxyError(
"ProxyConnectionError: " + str(e.strerror), request=request)
from e
112 except ProxyTimeoutError
as e:
113 raise httpx.ProxyError(
"ProxyTimeoutError: " + e.args[0], request=request)
from e
114 except ProxyError
as e:
115 raise httpx.ProxyError(
"ProxyError: " + e.args[0], request=request)
from e
119 verify: bool, http2: bool, local_address: str, proxy_url: str, limit: httpx.Limits, retries: int
126 socks5h =
'socks5h://'
127 if proxy_url.startswith(socks5h):
128 proxy_url =
'socks5://' + proxy_url[len(socks5h) :]
131 proxy_type, proxy_host, proxy_port, proxy_username, proxy_password = parse_proxy_url(proxy_url)
132 _verify =
get_sslcontexts(proxy_url,
None, verify,
True)
if verify
is True else verify
134 proxy_type=proxy_type,
135 proxy_host=proxy_host,
136 proxy_port=proxy_port,
137 username=proxy_username,
138 password=proxy_password,
143 local_address=local_address,
150 verify: bool, http2: bool, local_address: str, proxy_url: str |
None, limit: httpx.Limits, retries: int
152 _verify =
get_sslcontexts(
None,
None, verify,
True)
if verify
is True else verify
153 return httpx.AsyncHTTPTransport(
158 proxy=httpx._config.Proxy(proxy_url)
if proxy_url
else None,
159 local_address=local_address,
169 max_connections: int,
170 max_keepalive_connections: int,
171 keepalive_expiry: float,
172 proxies: dict[str, str],
176 hook_log_response: t.Callable[..., t.Any] |
None,
177) -> httpx.AsyncClient:
178 limit = httpx.Limits(
179 max_connections=max_connections,
180 max_keepalive_connections=max_keepalive_connections,
181 keepalive_expiry=keepalive_expiry,
185 mounts:
None | (dict[str, t.Any |
None]) = {}
186 for pattern, proxy_url
in proxies.items():
187 if not enable_http
and pattern.startswith(
'http://'):
189 if proxy_url.startswith(
'socks4://')
or proxy_url.startswith(
'socks5://')
or proxy_url.startswith(
'socks5h://'):
191 verify, enable_http2, local_address, proxy_url, limit, retries
194 mounts[pattern] =
get_transport(verify, enable_http2, local_address, proxy_url, limit, retries)
199 transport =
get_transport(verify, enable_http2, local_address,
None, limit, retries)
202 if hook_log_response:
203 event_hooks = {
'response': [hook_log_response]}
205 return httpx.AsyncClient(
208 max_redirects=max_redirects,
209 event_hooks=event_hooks,
222 'httpcore.connection',
228 logging.getLogger(logger_name).setLevel(logging.WARNING)
233 LOOP = asyncio.new_event_loop()
236 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)