.oO SearXNG Developer Documentation Oo.
Loading...
Searching...
No Matches
mullvad_leta.py
Go to the documentation of this file.
1# SPDX-License-Identifier: AGPL-3.0-or-later
2
3"""This is the implementation of the Mullvad-Leta meta-search engine.
4
5This engine **REQUIRES** that searxng operate within a Mullvad VPN
6
7If using docker, consider using gluetun for easily connecting to the Mullvad
8
9- https://github.com/qdm12/gluetun
10
11Otherwise, follow instructions provided by Mullvad for enabling the VPN on Linux
12
13- https://mullvad.net/en/help/install-mullvad-app-linux
14
15.. hint::
16
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
20 update of SearXNG!
21"""
22
23from typing import TYPE_CHECKING
24from httpx import Response
25from lxml import html
26from searx.enginelib.traits import EngineTraits
27from searx.locales import region_tag, get_official_locales
28from searx.utils import eval_xpath, extract_text, eval_xpath_list
29from searx.exceptions import SearxEngineResponseException
30
31if TYPE_CHECKING:
32 import logging
33
34 logger = logging.getLogger()
35
36traits: EngineTraits
37
38use_cache: bool = True # non-cache use only has 100 searches per day!
39
40search_url = "https://leta.mullvad.net"
41
42# about
43about = {
44 "website": search_url,
45 "wikidata_id": 'Q47008412', # the Mullvad id - not leta, but related
46 "official_api_documentation": 'https://leta.mullvad.net/faq',
47 "use_official_api": False,
48 "require_api_key": False,
49 "results": 'HTML',
50}
51
52# engine dependent config
53categories = ['general', 'web']
54paging = True
55max_page = 50
56time_range_support = True
57time_range_dict = {
58 "day": "d1",
59 "week": "w1",
60 "month": "m1",
61 "year": "y1",
62}
63
64
65def is_vpn_connected(dom: html.HtmlElement) -> bool:
66 """Returns true if the VPN is connected, False otherwise"""
67 connected_text = extract_text(eval_xpath(dom, '//main/div/p[1]'))
68 return connected_text != 'You are not connected to Mullvad VPN.'
69
70
71def assign_headers(headers: dict) -> dict:
72 """Assigns the headers to make a request to Mullvad Leta"""
73 headers['Accept'] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
74 headers['Content-Type'] = "application/x-www-form-urlencoded"
75 headers['Host'] = "leta.mullvad.net"
76 headers['Origin'] = "https://leta.mullvad.net"
77 return headers
78
79
80def request(query: str, params: dict):
81 country = traits.get_region(params.get('searxng_locale', 'all'), traits.all_locale) # type: ignore
82
83 params['url'] = search_url
84 params['method'] = 'POST'
85 params['data'] = {
86 "q": query,
87 "gl": country if country is str else '',
88 }
89 # pylint: disable=undefined-variable
90 if use_cache:
91 params['data']['oc'] = "on"
92 # pylint: enable=undefined-variable
93
94 if params['time_range'] in time_range_dict:
95 params['dateRestrict'] = time_range_dict[params['time_range']]
96 else:
97 params['dateRestrict'] = ''
98
99 if params['pageno'] > 1:
100 # Page 1 is n/a, Page 2 is 11, page 3 is 21, ...
101 params['data']['start'] = ''.join([str(params['pageno'] - 1), "1"])
102
103 if params['headers'] is None:
104 params['headers'] = {}
105
106 assign_headers(params['headers'])
107 return params
108
109
110def extract_result(dom_result: html.HtmlElement):
111 [a_elem, h3_elem, p_elem] = eval_xpath_list(dom_result, 'div/div/*')
112 return {
113 'url': extract_text(a_elem.text),
114 'title': extract_text(h3_elem),
115 'content': extract_text(p_elem),
116 }
117
118
119def response(resp: Response):
120 """Checks if connected to Mullvad VPN, then extracts the search results from
121 the DOM resp: requests response object"""
122
123 dom = html.fromstring(resp.text)
124 if not is_vpn_connected(dom):
125 raise SearxEngineResponseException('Not connected to Mullvad VPN')
126 search_results = eval_xpath(dom.body, '//main/div[2]/div')
127 return [extract_result(sr) for sr in search_results]
128
129
130def fetch_traits(engine_traits: EngineTraits):
131 """Fetch languages and regions from Mullvad-Leta
132
133 .. warning::
134
135 Fetching the engine traits also requires a Mullvad VPN connection. If
136 not connected, then an error message will print and no traits will be
137 updated.
138 """
139 # pylint: disable=import-outside-toplevel
140 # see https://github.com/searxng/searxng/issues/762
141 from searx.network import post as http_post
142
143 # pylint: enable=import-outside-toplevel
144 resp = http_post(search_url, headers=assign_headers({}))
145 if not isinstance(resp, Response):
146 print("ERROR: failed to get response from mullvad-leta. Are you connected to the VPN?")
147 return
148 if not resp.ok:
149 print("ERROR: response from mullvad-leta is not OK. Are you connected to the VPN?")
150 return
151 dom = html.fromstring(resp.text)
152 if not is_vpn_connected(dom):
153 print('ERROR: Not connected to Mullvad VPN')
154 return
155 # supported region codes
156 options = eval_xpath_list(dom.body, '//main/div/form/div[2]/div/select[1]/option')
157 if options is None or len(options) <= 0:
158 print('ERROR: could not find any results. Are you connected to the VPN?')
159 for x in options:
160 eng_country = x.get("value")
161
162 sxng_locales = get_official_locales(eng_country, engine_traits.languages.keys(), regional=True)
163
164 if not sxng_locales:
165 print(
166 "ERROR: can't map from Mullvad-Leta country %s (%s) to a babel region."
167 % (x.get('data-name'), eng_country)
168 )
169 continue
170
171 for sxng_locale in sxng_locales:
172 engine_traits.regions[region_tag(sxng_locale)] = eng_country
extract_result(html.HtmlElement dom_result)
bool is_vpn_connected(html.HtmlElement dom)
request(str query, dict params)
dict assign_headers(dict headers)
fetch_traits(EngineTraits engine_traits)