3"""This is the implementation of the Mullvad-Leta meta-search engine.
5This engine **REQUIRES** that searxng operate within a Mullvad VPN
7If using docker, consider using gluetun for easily connecting to the Mullvad
9- https://github.com/qdm12/gluetun
11Otherwise, follow instructions provided by Mullvad for enabling the VPN on Linux
13- https://mullvad.net/en/help/install-mullvad-app-linux
17 The :py:obj:`EngineTraits` is empty by default. Maintainers have to run
18 ``make data.traits`` (in the Mullvad VPN / :py:obj:`fetch_traits`) and rebase
19 the modified JSON file ``searx/data/engine_traits.json`` on every single
23from __future__
import annotations
25from typing
import TYPE_CHECKING
26from httpx
import Response
30from searx.utils import eval_xpath, extract_text, eval_xpath_list
36 logger = logging.getLogger()
42leta_engine: str =
'google'
44search_url =
"https://leta.mullvad.net"
48 "website": search_url,
49 "wikidata_id":
'Q47008412',
50 "official_api_documentation":
'https://leta.mullvad.net/faq',
51 "use_official_api":
False,
52 "require_api_key":
False,
57categories = [
'general',
'web']
60time_range_support =
True
68available_leta_engines = [
75 """Returns true if the VPN is connected, False otherwise"""
76 connected_text = extract_text(eval_xpath(dom,
'//main/div/p[1]'))
77 return connected_text !=
'You are not connected to Mullvad VPN.'
81 """Assigns the headers to make a request to Mullvad Leta"""
82 headers[
'Accept'] =
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
83 headers[
'Content-Type'] =
"application/x-www-form-urlencoded"
84 headers[
'Host'] =
"leta.mullvad.net"
85 headers[
'Origin'] =
"https://leta.mullvad.net"
90 country = traits.get_region(params.get(
'searxng_locale',
'all'), traits.all_locale)
92 result_engine = leta_engine
93 if leta_engine
not in available_leta_engines:
94 result_engine = available_leta_engines[0]
96 'Configured engine "%s" not one of the available engines %s, defaulting to "%s"',
98 available_leta_engines,
102 params[
'url'] = search_url
103 params[
'method'] =
'POST'
106 "gl": country
if country
is str
else '',
107 'engine': result_engine,
111 params[
'data'][
'oc'] =
"on"
114 if params[
'time_range']
in time_range_dict:
115 params[
'dateRestrict'] = time_range_dict[params[
'time_range']]
117 params[
'dateRestrict'] =
''
119 if params[
'pageno'] > 1:
121 params[
'data'][
'start'] =
''.join([str(params[
'pageno'] - 1),
"1"])
123 if params[
'headers']
is None:
124 params[
'headers'] = {}
132 if len(dom_result) == 3:
133 [a_elem, h3_elem, p_elem] = dom_result
134 elif len(dom_result) == 4:
135 [_, a_elem, h3_elem, p_elem] = dom_result
140 'url': extract_text(a_elem.text),
141 'title': extract_text(h3_elem),
142 'content': extract_text(p_elem),
147 for search_result
in search_results:
148 dom_result = eval_xpath_list(search_result,
'div/div/*')
150 if result
is not None:
155 """Checks if connected to Mullvad VPN, then extracts the search results from
156 the DOM resp: requests response object"""
158 dom = html.fromstring(resp.text)
161 search_results = eval_xpath(dom.body,
'//main/div[2]/div')
166 """Fetch languages and regions from Mullvad-Leta
170 Fetching the engine traits also requires a Mullvad VPN connection. If
171 not connected, then an error message will print and no traits will be
180 if not isinstance(resp, Response):
181 print(
"ERROR: failed to get response from mullvad-leta. Are you connected to the VPN?")
184 print(
"ERROR: response from mullvad-leta is not OK. Are you connected to the VPN?")
186 dom = html.fromstring(resp.text)
188 print(
'ERROR: Not connected to Mullvad VPN')
191 options = eval_xpath_list(dom.body,
'//main/div/form/div[2]/div/select[1]/option')
192 if options
is None or len(options) <= 0:
193 print(
'ERROR: could not find any results. Are you connected to the VPN?')
195 eng_country = x.get(
"value")
197 sxng_locales = get_official_locales(eng_country, engine_traits.languages.keys(), regional=
True)
201 "ERROR: can't map from Mullvad-Leta country %s (%s) to a babel region."
202 % (x.get(
'data-name'), eng_country)
206 for sxng_locale
in sxng_locales:
207 engine_traits.regions[region_tag(sxng_locale)] = eng_country
bool is_vpn_connected(html.HtmlElement dom)
extract_results(html.HtmlElement search_results)
request(str query, dict params)
extract_result(list[html.HtmlElement] dom_result)
dict assign_headers(dict headers)
fetch_traits(EngineTraits engine_traits)