.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
client.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2# pylint: disable=missing-module-docstring, global-statement
3
4import asyncio
5import logging
6import random
7from ssl import SSLContext
8import threading
9from typing import Any, Dict
10
11import httpx
12from httpx_socks import AsyncProxyTransport
13from python_socks import parse_proxy_url, ProxyConnectionError, ProxyTimeoutError, ProxyError
14import uvloop
15
16from searx import logger
17
18
19uvloop.install()
20
21
22logger = logger.getChild('searx.network.client')
23LOOP = None
24SSLCONTEXTS: Dict[Any, SSLContext] = {}
25
26
27def shuffle_ciphers(ssl_context):
28 """Shuffle httpx's default ciphers of a SSL context randomly.
29
30 From `What Is TLS Fingerprint and How to Bypass It`_
31
32 > When implementing TLS fingerprinting, servers can't operate based on a
33 > locked-in whitelist database of fingerprints. New fingerprints appear
34 > when web clients or TLS libraries release new versions. So, they have to
35 > live off a blocklist database instead.
36 > ...
37 > It's safe to leave the first three as is but shuffle the remaining ciphers
38 > and you can bypass the TLS fingerprint check.
39
40 .. _What Is TLS Fingerprint and How to Bypass It:
41 https://www.zenrows.com/blog/what-is-tls-fingerprint#how-to-bypass-tls-fingerprinting
42
43 """
44 c_list = httpx._config.DEFAULT_CIPHERS.split(':') # pylint: disable=protected-access
45 sc_list, c_list = c_list[:3], c_list[3:]
46 random.shuffle(c_list)
47 ssl_context.set_ciphers(":".join(sc_list + c_list))
48
49
50def get_sslcontexts(proxy_url=None, cert=None, verify=True, trust_env=True, http2=False):
51 key = (proxy_url, cert, verify, trust_env, http2)
52 if key not in SSLCONTEXTS:
53 SSLCONTEXTS[key] = httpx.create_ssl_context(cert, verify, trust_env, http2)
54 shuffle_ciphers(SSLCONTEXTS[key])
55 return SSLCONTEXTS[key]
56
57
58class AsyncHTTPTransportNoHttp(httpx.AsyncHTTPTransport):
59 """Block HTTP request
60
61 The constructor is blank because httpx.AsyncHTTPTransport.__init__ creates an SSLContext unconditionally:
62 https://github.com/encode/httpx/blob/0f61aa58d66680c239ce43c8cdd453e7dc532bfc/httpx/_transports/default.py#L271
63
64 Each SSLContext consumes more than 500kb of memory, since there is about one network per engine.
65
66 In consequence, this class overrides all public methods
67
68 For reference: https://github.com/encode/httpx/issues/2298
69 """
70
71 def __init__(self, *args, **kwargs):
72 # pylint: disable=super-init-not-called
73 # this on purpose if the base class is not called
74 pass
75
76 async def handle_async_request(self, request):
77 raise httpx.UnsupportedProtocol('HTTP protocol is disabled')
78
79 async def aclose(self) -> None:
80 pass
81
82 async def __aenter__(self):
83 return self
84
85 async def __aexit__(
86 self,
87 exc_type=None,
88 exc_value=None,
89 traceback=None,
90 ) -> None:
91 pass
92
93
94class AsyncProxyTransportFixed(AsyncProxyTransport):
95 """Fix httpx_socks.AsyncProxyTransport
96
97 Map python_socks exceptions to httpx.ProxyError exceptions
98 """
99
100 async def handle_async_request(self, request):
101 try:
102 return await super().handle_async_request(request)
103 except ProxyConnectionError as e:
104 raise httpx.ProxyError("ProxyConnectionError: " + e.strerror, request=request) from e
105 except ProxyTimeoutError as e:
106 raise httpx.ProxyError("ProxyTimeoutError: " + e.args[0], request=request) from e
107 except ProxyError as e:
108 raise httpx.ProxyError("ProxyError: " + e.args[0], request=request) from e
109
110
111def get_transport_for_socks_proxy(verify, http2, local_address, proxy_url, limit, retries):
112 # support socks5h (requests compatibility):
113 # https://requests.readthedocs.io/en/master/user/advanced/#socks
114 # socks5:// hostname is resolved on client side
115 # socks5h:// hostname is resolved on proxy side
116 rdns = False
117 socks5h = 'socks5h://'
118 if proxy_url.startswith(socks5h):
119 proxy_url = 'socks5://' + proxy_url[len(socks5h) :]
120 rdns = True
121
122 proxy_type, proxy_host, proxy_port, proxy_username, proxy_password = parse_proxy_url(proxy_url)
123 verify = get_sslcontexts(proxy_url, None, verify, True, http2) if verify is True else verify
125 proxy_type=proxy_type,
126 proxy_host=proxy_host,
127 proxy_port=proxy_port,
128 username=proxy_username,
129 password=proxy_password,
130 rdns=rdns,
131 loop=get_loop(),
132 verify=verify,
133 http2=http2,
134 local_address=local_address,
135 limits=limit,
136 retries=retries,
137 )
138
139
140def get_transport(verify, http2, local_address, proxy_url, limit, retries):
141 verify = get_sslcontexts(None, None, verify, True, http2) if verify is True else verify
142 return httpx.AsyncHTTPTransport(
143 # pylint: disable=protected-access
144 verify=verify,
145 http2=http2,
146 limits=limit,
147 proxy=httpx._config.Proxy(proxy_url) if proxy_url else None,
148 local_address=local_address,
149 retries=retries,
150 )
151
152
154 # pylint: disable=too-many-arguments
155 enable_http,
156 verify,
157 enable_http2,
158 max_connections,
159 max_keepalive_connections,
160 keepalive_expiry,
161 proxies,
162 local_address,
163 retries,
164 max_redirects,
165 hook_log_response,
166):
167 limit = httpx.Limits(
168 max_connections=max_connections,
169 max_keepalive_connections=max_keepalive_connections,
170 keepalive_expiry=keepalive_expiry,
171 )
172 # See https://www.python-httpx.org/advanced/#routing
173 mounts = {}
174 for pattern, proxy_url in proxies.items():
175 if not enable_http and pattern.startswith('http://'):
176 continue
177 if proxy_url.startswith('socks4://') or proxy_url.startswith('socks5://') or proxy_url.startswith('socks5h://'):
178 mounts[pattern] = get_transport_for_socks_proxy(
179 verify, enable_http2, local_address, proxy_url, limit, retries
180 )
181 else:
182 mounts[pattern] = get_transport(verify, enable_http2, local_address, proxy_url, limit, retries)
183
184 if not enable_http:
185 mounts['http://'] = AsyncHTTPTransportNoHttp()
186
187 transport = get_transport(verify, enable_http2, local_address, None, limit, retries)
188
189 event_hooks = None
190 if hook_log_response:
191 event_hooks = {'response': [hook_log_response]}
192
193 return httpx.AsyncClient(
194 transport=transport,
195 mounts=mounts,
196 max_redirects=max_redirects,
197 event_hooks=event_hooks,
198 )
199
200
202 return LOOP
203
204
205def init():
206 # log
207 for logger_name in (
208 'httpx',
209 'httpcore.proxy',
210 'httpcore.connection',
211 'httpcore.http11',
212 'httpcore.http2',
213 'hpack.hpack',
214 'hpack.table',
215 ):
216 logging.getLogger(logger_name).setLevel(logging.WARNING)
217
218 # loop
219 def loop_thread():
220 global LOOP
221 LOOP = asyncio.new_event_loop()
222 LOOP.run_forever()
223
224 thread = threading.Thread(
225 target=loop_thread,
226 name='asyncio_loop',
227 daemon=True,
228 )
229 thread.start()
230
231
232init()
None __aexit__(self, exc_type=None, exc_value=None, traceback=None)
Definition client.py:90
new_client(enable_http, verify, enable_http2, max_connections, max_keepalive_connections, keepalive_expiry, proxies, local_address, retries, max_redirects, hook_log_response)
Definition client.py:166
get_transport_for_socks_proxy(verify, http2, local_address, proxy_url, limit, retries)
Definition client.py:111
get_sslcontexts(proxy_url=None, cert=None, verify=True, trust_env=True, http2=False)
Definition client.py:50
shuffle_ciphers(ssl_context)
Definition client.py:27
get_transport(verify, http2, local_address, proxy_url, limit, retries)
Definition client.py:140