@@ -7,7 +7,7 @@ from itertools import cycle
8import httpx
8import httpx
10−from .client import new_client, get_loop
10+from .client import new_client, get_loop, AsyncHTTPTransportNoHttp
13DEFAULT_NAME = '__DEFAULT__'
13DEFAULT_NAME = '__DEFAULT__'
@@ -36,9 +36,11 @@ class Network:
37 __slots__ = ('enable_http', 'verify', 'enable_http2',
37 __slots__ = ('enable_http', 'verify', 'enable_http2',
38 'max_connections', 'max_keepalive_connections', 'keepalive_expiry',
38 'max_connections', 'max_keepalive_connections', 'keepalive_expiry',
39− 'local_addresses', 'proxies', 'max_redirects', 'retries', 'retry_on_http_error',
39+ 'local_addresses', 'proxies', 'using_tor_proxy', 'max_redirects', 'retries', 'retry_on_http_error',
40 '_local_addresses_cycle', '_proxies_cycle', '_clients')
40 '_local_addresses_cycle', '_proxies_cycle', '_clients')
42+ _TOR_CHECK_RESULT = {}
42 def __init__(self,
44 def __init__(self,
43 enable_http=True,
45 enable_http=True,
44 verify=True,
46 verify=True,
@@ -47,6 +49,7 @@ class Network:
47 max_keepalive_connections=None,
49 max_keepalive_connections=None,
48 keepalive_expiry=None,
50 keepalive_expiry=None,
49 proxies=None,
51 proxies=None,
52+ using_tor_proxy=False,
50 local_addresses=None,
53 local_addresses=None,
51 retries=0,
54 retries=0,
52 retry_on_http_error=None,
55 retry_on_http_error=None,
@@ -58,6 +61,7 @@ class Network:
58 self.max_keepalive_connections = max_keepalive_connections
61 self.max_keepalive_connections = max_keepalive_connections
59 self.keepalive_expiry = keepalive_expiry
62 self.keepalive_expiry = keepalive_expiry
60 self.proxies = proxies
63 self.proxies = proxies
64+ self.using_tor_proxy = using_tor_proxy
61 self.local_addresses = local_addresses
65 self.local_addresses = local_addresses
62 self.retries = retries
66 self.retries = retries
63 self.retry_on_http_error = retry_on_http_error
67 self.retry_on_http_error = retry_on_http_error
@@ -121,23 +125,49 @@ class Network:
121 while True:
125 while True:
122 yield tuple((pattern, next(proxy_url_cycle)) for pattern, proxy_url_cycle in proxy_settings.items())
126 yield tuple((pattern, next(proxy_url_cycle)) for pattern, proxy_url_cycle in proxy_settings.items())
129+ async def check_tor_proxy(client: httpx.AsyncClient, proxies) -> bool:
130+ if proxies in Network._TOR_CHECK_RESULT:
131+ return Network._TOR_CHECK_RESULT[proxies]
134+ # ignore client._transport because it is not used with all://
135+ for transport in client._mounts.values(): # pylint: disable=protected-access
136+ if isinstance(transport, AsyncHTTPTransportNoHttp):
138+ if not getattr(transport, '_rdns', False):
142+ response = await client.get('https://check.torproject.org/api/ip')
143+ if not response.json()['IsTor']:
145+ Network._TOR_CHECK_RESULT[proxies] = result
124− def get_client(self, verify=None, max_redirects=None):
148+ async def get_client(self, verify=None, max_redirects=None):
125 verify = self.verify if verify is None else verify
149 verify = self.verify if verify is None else verify
126 max_redirects = self.max_redirects if max_redirects is None else max_redirects
150 max_redirects = self.max_redirects if max_redirects is None else max_redirects
127 local_address = next(self._local_addresses_cycle)
151 local_address = next(self._local_addresses_cycle)
128 proxies = next(self._proxies_cycle) # is a tuple so it can be part of the key
152 proxies = next(self._proxies_cycle) # is a tuple so it can be part of the key
129 key = (verify, max_redirects, local_address, proxies)
153 key = (verify, max_redirects, local_address, proxies)
130 if key not in self._clients or self._clients[key].is_closed:
154 if key not in self._clients or self._clients[key].is_closed:
155+ client = new_client(
131− self._clients[key] = new_client(self.enable_http,
156+ self.enable_http,
133− self.enable_http2,
158+ self.enable_http2,
134− self.max_connections,
159+ self.max_connections,
135− self.max_keepalive_connections,
160+ self.max_keepalive_connections,
136− self.keepalive_expiry,
161+ self.keepalive_expiry,
137− dict(proxies),
162+ dict(proxies),
138− local_address,
163+ local_address,
140− max_redirects)
165+ max_redirects
167+ if self.using_tor_proxy and not await self.check_tor_proxy(client, proxies):
168+ await client.aclose()
169+ raise httpx.ProxyError('Network configuration problem: not using Tor')
170+ self._clients[key] = client
141 return self._clients[key]
171 return self._clients[key]
143 async def aclose(self):
173 async def aclose(self):
@@ -168,7 +198,7 @@ class Network:
168 retries = self.retries
198 retries = self.retries
169 while retries >= 0: # pragma: no cover
199 while retries >= 0: # pragma: no cover
170 kwargs_clients = Network.get_kwargs_clients(kwargs)
200 kwargs_clients = Network.get_kwargs_clients(kwargs)
171− client = self.get_client(**kwargs_clients)
201+ client = await self.get_client(**kwargs_clients)
173 response = await client.request(method, url, **kwargs)
203 response = await client.request(method, url, **kwargs)
174 if self.is_valid_respones(response) or retries <= 0:
204 if self.is_valid_respones(response) or retries <= 0:
@@ -178,11 +208,11 @@ class Network:
179 retries -= 1
209 retries -= 1
181− def stream(self, method, url, **kwargs):
211+ async def stream(self, method, url, **kwargs):
182 retries = self.retries
212 retries = self.retries
183 while retries >= 0: # pragma: no cover
213 while retries >= 0: # pragma: no cover
184 kwargs_clients = Network.get_kwargs_clients(kwargs)
214 kwargs_clients = Network.get_kwargs_clients(kwargs)
185− client = self.get_client(**kwargs_clients)
215+ client = await self.get_client(**kwargs_clients)
187 response = client.stream(method, url, **kwargs)
217 response = client.stream(method, url, **kwargs)
188 if self.is_valid_respones(response) or retries <= 0:
218 if self.is_valid_respones(response) or retries <= 0:
@@ -202,6 +232,22 @@ def get_network(name=None):
202 return NETWORKS[name or DEFAULT_NAME]
232 return NETWORKS[name or DEFAULT_NAME]
235+def check_network_configuration():
238+ for network in NETWORKS.values():
239+ if network.using_tor_proxy:
241+ await network.get_client()
242+ except Exception: # pylint: disable=broad-except
243+ exception_count += 1
244+ return exception_count
245+ future = asyncio.run_coroutine_threadsafe(check(), get_loop())
246+ exception_count = future.result()
247+ if exception_count > 0:
248+ raise RuntimeError("Invalid network configuration")
205def initialize(settings_engines=None, settings_outgoing=None):
251def initialize(settings_engines=None, settings_outgoing=None):
206 from searx.engines import engines
252 from searx.engines import engines
207 from searx import settings
253 from searx import settings
@@ -225,6 +271,7 @@ def initialize(settings_engines=None, settings_outgoing=None):
225 'keepalive_expiry': settings_outgoing.get('keepalive_expiry', 5.0),
271 'keepalive_expiry': settings_outgoing.get('keepalive_expiry', 5.0),
226 'local_addresses': settings_outgoing.get('source_ips'),
272 'local_addresses': settings_outgoing.get('source_ips'),
227 'proxies': settings_outgoing.get('proxies'),
273 'proxies': settings_outgoing.get('proxies'),
274+ 'using_tor_proxy': settings_outgoing.get('using_tor_proxy'),
228 # default maximum redirect
275 # default maximum redirect
229 # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55
276 # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55
230 'max_redirects': settings_outgoing.get('max_redirects', 30),
277 'max_redirects': settings_outgoing.get('max_redirects', 30),