on master help sign in
[enh] verify that Tor proxy works every time searx starts

based on @MarcAbonce commit on searx

commitbc60d834c58e7e2cf3706cca18fc1ffb68c640c6 authorAlexandre Flament <alex@al-f.net> dateThu 23 Sep 2021, 11:31 (5 years ago) parent3bcca43
5 files changed+80−30
searx/network/__init__.py+2−2 searx/network/network.py+63−16 searx/search/__init__.py+4−1 searx/webapp.py+1−1 tests/unit/network/test_network.py+10−10
searx/network/__init__.py+2−2
@@ -8,7 +8,7 @@ from time import time
88import httpx
99import h2.exceptions
1010
11−from .network import get_network, initialize
11+from .network import get_network, initialize, check_network_configuration
1212from .client import get_loop
1313from .raise_for_httperror import raise_for_httperror
1414
@@ -154,7 +154,7 @@ def delete(url, **kwargs):
154154
155155async def stream_chunk_to_queue(network, q, method, url, **kwargs):
156156 try:
157− async with network.stream(method, url, **kwargs) as response:
157+ async with await network.stream(method, url, **kwargs) as response:
158158 q.put(response)
159159 async for chunk in response.aiter_bytes(65536):
160160 if len(chunk) > 0:
searx/network/network.py+63−16
@@ -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),
searx/search/__init__.py+4−1
@@ -30,6 +30,7 @@ from searx import logger
3030from searx.plugins import plugins
3131from searx.search.models import EngineRef, SearchQuery
3232from searx.search.processors import processors, initialize as initialize_processors
33+from searx.network import check_network_configuration
3334from searx.search.checker import initialize as initialize_checker
3435
3536
@@ -47,9 +48,11 @@ else:
4748 sys.exit(1)
4849
4950
50−def initialize(settings_engines=None, enable_checker=False):
51+def initialize(settings_engines=None, enable_checker=False, check_network=False):
5152 settings_engines = settings_engines or settings['engines']
5253 initialize_processors(settings_engines)
54+ if check_network:
55+ check_network_configuration()
5356 if enable_checker:
5457 initialize_checker()
5558
searx/webapp.py+1−1
@@ -153,7 +153,7 @@ werkzeug_reloader = flask_run_development or (searx_debug and __name__ == "__mai
153153# initialize the engines except on the first run of the werkzeug server.
154154if not werkzeug_reloader\
155155 or (werkzeug_reloader and os.environ.get("WERKZEUG_RUN_MAIN") == "true"):
156− search_initialize(enable_checker=True)
156+ search_initialize(enable_checker=True, check_network=True)
157157
158158babel = Babel(app)
159159
tests/unit/network/test_network.py+10−10
@@ -90,12 +90,12 @@ class TestNetwork(SearxTestCase):
9090
9191 async def test_get_client(self):
9292 network = Network(verify=True)
93− client1 = network.get_client()
94− client2 = network.get_client(verify=True)
95− client3 = network.get_client(max_redirects=10)
96− client4 = network.get_client(verify=True)
97− client5 = network.get_client(verify=False)
98− client6 = network.get_client(max_redirects=10)
93+ client1 = await network.get_client()
94+ client2 = await network.get_client(verify=True)
95+ client3 = await network.get_client(max_redirects=10)
96+ client4 = await network.get_client(verify=True)
97+ client5 = await network.get_client(verify=False)
98+ client6 = await network.get_client(max_redirects=10)
9999
100100 self.assertEqual(client1, client2)
101101 self.assertEqual(client1, client4)
@@ -107,7 +107,7 @@ class TestNetwork(SearxTestCase):
107107
108108 async def test_aclose(self):
109109 network = Network(verify=True)
110− network.get_client()
110+ await network.get_client()
111111 await network.aclose()
112112
113113 async def test_request(self):
@@ -211,7 +211,7 @@ class TestNetworkStreamRetries(SearxTestCase):
211211 async def test_retries_ok(self):
212212 with patch.object(httpx.AsyncClient, 'stream', new=TestNetworkStreamRetries.get_response_exception_then_200()):
213213 network = Network(enable_http=True, retries=1, retry_on_http_error=403)
214− response = network.stream('GET', 'https://example.com/')
214+ response = await network.stream('GET', 'https://example.com/')
215215 self.assertEqual(response.text, TestNetworkStreamRetries.TEXT)
216216 await network.aclose()
217217
@@ -219,7 +219,7 @@ class TestNetworkStreamRetries(SearxTestCase):
219219 with patch.object(httpx.AsyncClient, 'stream', new=TestNetworkStreamRetries.get_response_exception_then_200()):
220220 network = Network(enable_http=True, retries=0, retry_on_http_error=403)
221221 with self.assertRaises(httpx.RequestError):
222− network.stream('GET', 'https://example.com/')
222+ await network.stream('GET', 'https://example.com/')
223223 await network.aclose()
224224
225225 async def test_retries_exception(self):
@@ -234,6 +234,6 @@ class TestNetworkStreamRetries(SearxTestCase):
234234
235235 with patch.object(httpx.AsyncClient, 'stream', new=stream):
236236 network = Network(enable_http=True, retries=0, retry_on_http_error=403)
237− response = network.stream('GET', 'https://example.com/')
237+ response = await network.stream('GET', 'https://example.com/')
238238 self.assertEqual(response.status_code, 403)
239239 await network.aclose()