@@ -7,7 +7,7 @@ from itertools import cycle
77
88import httpx
99
10−from .client import new_client, get_loop
10+from .client import new_client, get_loop, AsyncHTTPTransportNoHttp
1111
1212
1313DEFAULT_NAME = '__DEFAULT__'
@@ -36,9 +36,11 @@ class Network:
3636
3737 __slots__ = ('enable_http', 'verify', 'enable_http2',
3838 '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',
4040 '_local_addresses_cycle', '_proxies_cycle', '_clients')
4141
42+ _TOR_CHECK_RESULT = {}
43+
4244 def __init__(self,
4345 enable_http=True,
4446 verify=True,
@@ -47,6 +49,7 @@ class Network:
4749 max_keepalive_connections=None,
4850 keepalive_expiry=None,
4951 proxies=None,
52+ using_tor_proxy=False,
5053 local_addresses=None,
5154 retries=0,
5255 retry_on_http_error=None,
@@ -58,6 +61,7 @@ class Network:
5861 self.max_keepalive_connections = max_keepalive_connections
5962 self.keepalive_expiry = keepalive_expiry
6063 self.proxies = proxies
64+ self.using_tor_proxy = using_tor_proxy
6165 self.local_addresses = local_addresses
6266 self.retries = retries
6367 self.retry_on_http_error = retry_on_http_error
@@ -121,23 +125,49 @@ class Network:
121125 while True:
122126 yield tuple((pattern, next(proxy_url_cycle)) for pattern, proxy_url_cycle in proxy_settings.items())
123127
124− def get_client(self, verify=None, max_redirects=None):
128+ @staticmethod
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]
132+
133+ result = True
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):
137+ continue
138+ if not getattr(transport, '_rdns', False):
139+ result = False
140+ break
141+ else:
142+ response = await client.get('https://check.torproject.org/api/ip')
143+ if not response.json()['IsTor']:
144+ result = False
145+ Network._TOR_CHECK_RESULT[proxies] = result
146+ return result
147+
148+ async def get_client(self, verify=None, max_redirects=None):
125149 verify = self.verify if verify is None else verify
126150 max_redirects = self.max_redirects if max_redirects is None else max_redirects
127151 local_address = next(self._local_addresses_cycle)
128152 proxies = next(self._proxies_cycle) # is a tuple so it can be part of the key
129153 key = (verify, max_redirects, local_address, proxies)
130154 if key not in self._clients or self._clients[key].is_closed:
131− self._clients[key] = new_client(self.enable_http,
132− verify,
133− self.enable_http2,
134− self.max_connections,
135− self.max_keepalive_connections,
136− self.keepalive_expiry,
137− dict(proxies),
138− local_address,
139− 0,
140− max_redirects)
155+ client = new_client(
156+ self.enable_http,
157+ verify,
158+ self.enable_http2,
159+ self.max_connections,
160+ self.max_keepalive_connections,
161+ self.keepalive_expiry,
162+ dict(proxies),
163+ local_address,
164+ 0,
165+ max_redirects
166+ )
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
141171 return self._clients[key]
142172
143173 async def aclose(self):
@@ -168,7 +198,7 @@ class Network:
168198 retries = self.retries
169199 while retries >= 0: # pragma: no cover
170200 kwargs_clients = Network.get_kwargs_clients(kwargs)
171− client = self.get_client(**kwargs_clients)
201+ client = await self.get_client(**kwargs_clients)
172202 try:
173203 response = await client.request(method, url, **kwargs)
174204 if self.is_valid_respones(response) or retries <= 0:
@@ -178,11 +208,11 @@ class Network:
178208 raise e
179209 retries -= 1
180210
181− def stream(self, method, url, **kwargs):
211+ async def stream(self, method, url, **kwargs):
182212 retries = self.retries
183213 while retries >= 0: # pragma: no cover
184214 kwargs_clients = Network.get_kwargs_clients(kwargs)
185− client = self.get_client(**kwargs_clients)
215+ client = await self.get_client(**kwargs_clients)
186216 try:
187217 response = client.stream(method, url, **kwargs)
188218 if self.is_valid_respones(response) or retries <= 0:
@@ -202,6 +232,22 @@ def get_network(name=None):
202232 return NETWORKS[name or DEFAULT_NAME]
203233
204234
235+def check_network_configuration():
236+ async def check():
237+ exception_count = 0
238+ for network in NETWORKS.values():
239+ if network.using_tor_proxy:
240+ try:
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")
249+
250+
205251def initialize(settings_engines=None, settings_outgoing=None):
206252 from searx.engines import engines
207253 from searx import settings
@@ -225,6 +271,7 @@ def initialize(settings_engines=None, settings_outgoing=None):
225271 'keepalive_expiry': settings_outgoing.get('keepalive_expiry', 5.0),
226272 'local_addresses': settings_outgoing.get('source_ips'),
227273 'proxies': settings_outgoing.get('proxies'),
274+ 'using_tor_proxy': settings_outgoing.get('using_tor_proxy'),
228275 # default maximum redirect
229276 # from https://github.com/psf/requests/blob/8c211a96cdbe9fe320d63d9e1ae15c5c07e179f8/requests/models.py#L55
230277 'max_redirects': settings_outgoing.get('max_redirects', 30),