on master help sign in
[enh] Add onions category with Ahmia, Not Evil and Torch

Xpath engine and results template changed to account for the fact that archive.org doesn't cache .onions, though some onion engines migth have their own cache. Disabled by default. Can be enabled by setting the SOCKS proxies to wherever Tor is listening and setting using_tor_proxy as True. Requires Tor and updating packages. To avoid manually adding the timeout on each engine, you can set extra_proxy_timeout to account for Tor's (or whatever proxy used) extra time.

commitc3daa08537668c24224fffecbed4347fee936fcf authora01200356 <a01200356@itesm.mx> dateThu 19 May 2016, 00:38 (10 years ago) parent0a44fa8
11 files changed+399−14
.gitignore+1 searx/engines/__init__.py+13−1 searx/engines/ahmia.py+82 searx/engines/not_evil.py+64 searx/engines/xpath.py+29−7 searx/settings.yml+28−3 searx/templates/legacy/result_templates/default.html+6−1 searx/templates/oscar/macros.html+10−2 searx/webapp.py+1 tests/unit/engines/test_xpath.py+121 tests/unit/test_engines_init.py+44
.gitignore+1
@@ -15,6 +15,7 @@ setup.cfg
1515*.pyc
1616*/*.pyc
1717*~
18+*.swp
1819
1920/node_modules
2021
searx/engines/__init__.py+13−1
@@ -142,6 +142,17 @@ def load_engine(engine_data):
142142 engine.stats['page_load_time'] = 0
143143 engine.stats['page_load_count'] = 0
144144
145+ # tor related settings
146+ if settings['outgoing'].get('using_tor_proxy'):
147+ # use onion url if using tor.
148+ if hasattr(engine, 'onion_url'):
149+ engine.search_url = engine.onion_url + getattr(engine, 'search_path', '')
150+ elif 'onions' in engine.categories:
151+ # exclude onion engines if not using tor.
152+ return None
153+
154+ engine.timeout += settings['outgoing'].get('extra_proxy_timeout', 0)
155+
145156 for category_name in engine.categories:
146157 categories.setdefault(category_name, []).append(engine)
147158
@@ -252,8 +263,9 @@ def get_engines_stats(preferences):
252263
253264
254265def load_engines(engine_list):
255− global engines
266+ global engines, engine_shortcuts
256267 engines.clear()
268+ engine_shortcuts.clear()
257269 for engine_data in engine_list:
258270 engine = load_engine(engine_data)
259271 if engine is not None:
searx/engines/ahmia.pynew file+82
@@ -0,0 +1,82 @@
1+"""
2+ Ahmia (Onions)
3+
4+ @website http://msydqstlz2kzerdg.onion
5+ @provides-api no
6+
7+ @using-api no
8+ @results HTML
9+ @stable no
10+ @parse url, title, content
11+"""
12+
13+from urllib.parse import urlencode, urlparse, parse_qs
14+from lxml.html import fromstring
15+from searx.engines.xpath import extract_url, extract_text
16+
17+# engine config
18+categories = ['onions']
19+paging = True
20+page_size = 10
21+
22+# search url
23+search_url = 'http://msydqstlz2kzerdg.onion/search/?{query}'
24+time_range_support = True
25+time_range_dict = {'day': 1,
26+ 'week': 7,
27+ 'month': 30}
28+
29+# xpaths
30+results_xpath = '//li[@class="result"]'
31+url_xpath = './h4/a/@href'
32+title_xpath = './h4/a[1]'
33+content_xpath = './/p[1]'
34+correction_xpath = '//*[@id="didYouMean"]//a'
35+number_of_results_xpath = '//*[@id="totalResults"]'
36+
37+
38+def request(query, params):
39+ params['url'] = search_url.format(query=urlencode({'q': query}))
40+
41+ if params['time_range'] in time_range_dict:
42+ params['url'] += '&' + urlencode({'d': time_range_dict[params['time_range']]})
43+
44+ return params
45+
46+
47+def response(resp):
48+ results = []
49+ dom = fromstring(resp.text)
50+
51+ # trim results so there's not way too many at once
52+ first_result_index = page_size * (resp.search_params.get('pageno', 1) - 1)
53+ all_results = dom.xpath(results_xpath)
54+ trimmed_results = all_results[first_result_index:first_result_index + page_size]
55+
56+ # get results
57+ for result in trimmed_results:
58+ # remove ahmia url and extract the actual url for the result
59+ raw_url = extract_url(result.xpath(url_xpath), search_url)
60+ cleaned_url = parse_qs(urlparse(raw_url).query).get('redirect_url', [''])[0]
61+
62+ title = extract_text(result.xpath(title_xpath))
63+ content = extract_text(result.xpath(content_xpath))
64+
65+ results.append({'url': cleaned_url,
66+ 'title': title,
67+ 'content': content,
68+ 'is_onion': True})
69+
70+ # get spelling corrections
71+ for correction in dom.xpath(correction_xpath):
72+ results.append({'correction': extract_text(correction)})
73+
74+ # get number of results
75+ number_of_results = dom.xpath(number_of_results_xpath)
76+ if number_of_results:
77+ try:
78+ results.append({'number_of_results': int(extract_text(number_of_results))})
79+ except:
80+ pass
81+
82+ return results
searx/engines/not_evil.pynew file+64
@@ -0,0 +1,64 @@
1+"""
2+ not Evil (Onions)
3+
4+ @website http://hss3uro2hsxfogfq.onion
5+ @provide-api yes (http://hss3uro2hsxfogfq.onion/api.htm)
6+
7+ @using-api no
8+ @results HTML
9+ @stable no
10+ @parse url, title, content
11+"""
12+
13+from urllib.parse import urlencode
14+from lxml import html
15+from searx.engines.xpath import extract_text
16+
17+# engine dependent config
18+categories = ['onions']
19+paging = True
20+page_size = 20
21+
22+# search-url
23+base_url = 'http://hss3uro2hsxfogfq.onion/'
24+search_url = 'index.php?{query}&hostLimit=20&start={pageno}&numRows={page_size}'
25+
26+# specific xpath variables
27+results_xpath = '//*[@id="content"]/div/p'
28+url_xpath = './span[1]'
29+title_xpath = './a[1]'
30+content_xpath = './text()'
31+
32+
33+# do search-request
34+def request(query, params):
35+ offset = (params['pageno'] - 1) * page_size
36+
37+ params['url'] = base_url + search_url.format(pageno=offset,
38+ query=urlencode({'q': query}),
39+ page_size=page_size)
40+
41+ return params
42+
43+
44+# get response from search-request
45+def response(resp):
46+ results = []
47+
48+ # needed because otherwise requests guesses wrong encoding
49+ resp.encoding = 'utf8'
50+ dom = html.fromstring(resp.text)
51+
52+ # parse results
53+ for result in dom.xpath(results_xpath):
54+ url = extract_text(result.xpath(url_xpath)[0])
55+ title = extract_text(result.xpath(title_xpath)[0])
56+ content = extract_text(result.xpath(content_xpath))
57+
58+ # append result
59+ results.append({'url': url,
60+ 'title': title,
61+ 'content': content,
62+ 'is_onion': True})
63+
64+ return results
searx/engines/xpath.py+29−7
@@ -10,6 +10,8 @@ thumbnail_xpath = False
1010paging = False
1111suggestion_xpath = ''
1212results_xpath = ''
13+cached_xpath = ''
14+cached_url = ''
1315
1416# parameters for engines with paging support
1517#
@@ -36,6 +38,8 @@ def request(query, params):
3638def response(resp):
3739 results = []
3840 dom = html.fromstring(resp.text)
41+ is_onion = True if 'onions' in categories else False
42+
3943 if results_xpath:
4044 for result in eval_xpath(dom, results_xpath):
4145 url = extract_url(eval_xpath(result, url_xpath), search_url)
@@ -49,15 +53,33 @@ def response(resp):
4953 if len(thumbnail_xpath_result) > 0:
5054 tmp_result['img_src'] = extract_url(thumbnail_xpath_result, search_url)
5155
56+ # add alternative cached url if available
57+ if cached_xpath:
58+ tmp_result['cached_url'] = cached_url + extract_text(result.xpath(cached_xpath))
59+
60+ if is_onion:
61+ tmp_result['is_onion'] = True
62+
5263 results.append(tmp_result)
5364 else:
54− for url, title, content in zip(
55− (extract_url(x, search_url) for
56− x in eval_xpath(dom, url_xpath)),
57− map(extract_text, eval_xpath(dom, title_xpath)),
58− map(extract_text, eval_xpath(dom, content_xpath))
59− ):
60− results.append({'url': url, 'title': title, 'content': content})
65+ if cached_xpath:
66+ for url, title, content, cached in zip(
67+ (extract_url(x, search_url) for
68+ x in dom.xpath(url_xpath)),
69+ map(extract_text, dom.xpath(title_xpath)),
70+ map(extract_text, dom.xpath(content_xpath)),
71+ map(extract_text, dom.xpath(cached_xpath))
72+ ):
73+ results.append({'url': url, 'title': title, 'content': content,
74+ 'cached_url': cached_url + cached, 'is_onion': is_onion})
75+ else:
76+ for url, title, content in zip(
77+ (extract_url(x, search_url) for
78+ x in dom.xpath(url_xpath)),
79+ map(extract_text, dom.xpath(title_xpath)),
80+ map(extract_text, dom.xpath(content_xpath))
81+ ):
82+ results.append({'url': url, 'title': title, 'content': content, 'is_onion': is_onion})
6183
6284 if not suggestion_xpath:
6385 return results
searx/settings.yml+28−3
@@ -60,8 +60,10 @@ outgoing: # communication with search engines
6060# see http://docs.python-requests.org/en/latest/user/advanced/#proxies
6161# SOCKS proxies are also supported: see http://requests.readthedocs.io/en/master/user/advanced/#socks
6262# proxies :
63−# http : http://127.0.0.1:8080
64−# https: http://127.0.0.1:8080
63+# http : socks5h://127.0.0.1:9050
64+# https: socks5h://127.0.0.1:9050
65+# using_tor_proxy : True
66+# extra_proxy_timeout : 10.0 # Extra seconds to add in order to account for the time taken by the proxy
6567# uncomment below section only if you have more than one network interface
6668# which can be the source of outgoing search requests
6769# source_ips:
@@ -89,6 +91,12 @@ engines:
8991 shortcut: apkm
9092 disabled: True
9193
94+# Requires Tor
95+ - name : ahmia
96+ engine : ahmia
97+ categories : onions
98+ shortcut : ah
99+
92100 - name : arch linux wiki
93101 engine : archlinux
94102 shortcut : al
@@ -185,7 +193,7 @@ engines:
185193 - name : deviantart
186194 engine : deviantart
187195 shortcut : da
188− timeout: 3.0
196+ timeout : 3.0
189197
190198 - name : ddg definitions
191199 engine : duckduckgo_definitions
@@ -514,6 +522,11 @@ engines:
514522 timeout: 5.0
515523 shortcut : npm
516524
525+# Requires Tor
526+ - name : not evil
527+ engine : not_evil
528+ shortcut : ne
529+
517530 - name : nyaa
518531 engine : nyaa
519532 shortcut : nt
@@ -698,6 +711,18 @@ engines:
698711 url: https://torrentz2.eu/
699712 timeout : 3.0
700713
714+# Requires Tor
715+ - name : torch
716+ engine : xpath
717+ paging : True
718+ search_url : http://xmh57jrknzkhv6y3ls3ubitzfqnkrwxhopf5aygthi7d6rplyvk3noyd.onion/cgi-bin/omega/omega?P={query}&DEFAULTOP=and
719+ results_xpath : //table//tr
720+ url_xpath : ./td[2]/a
721+ title_xpath : ./td[2]/b
722+ content_xpath : ./td[2]/small
723+ categories : onions
724+ shortcut : tch
725+
701726 - name : twitter
702727 engine : twitter
703728 shortcut : tw
searx/templates/legacy/result_templates/default.html+6−1
@@ -1,6 +1,11 @@
11<div class="result {{ result.class }}{% for e in result.engines %} {{ e }}{% endfor %}">
22 <h3 class="result_title">{% if "icon_"~result.engine~".ico" in favicons %}<img width="14" height="14" class="favicon" src="{{ url_for('static', filename='img/icons/icon_'+result.engine+'.ico') }}" alt="{{result.engine}}" />{% endif %}<a href="{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ result.title|safe }}</a></h3>
3− <p class="url">{{ result.pretty_url }}&lrm; <a class="cache_link" href="https://web.archive.org/web/{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ _('cached') }}</a>
3+ <p class="url">{{ result.pretty_url }}&lrm;
4+ {% if result.cached_url %}
5+ <a class="cache_link" href="{{ result.cached_url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ _('cached') }}</a>
6+ {% elif not result.is_onion %}
7+ <a class="cache_link" href="https://web.archive.org/web/{{ result.url }}" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %}>{{ _('cached') }}</a>
8+ {% endif %}
49 {% if result.publishedDate %}<span class="published_date">{{ result.publishedDate }}</span>{% endif %}</p>
510 <p class="content">{% if result.img_src %}<img src="{{ image_proxify(result.img_src) }}" class="image" />{% endif %}{% if result.content %}{{ result.content|safe }}<br class="last"/>{% endif %}</p>
611</div>
searx/templates/oscar/macros.html+10−2
@@ -32,7 +32,11 @@
3232 <span class="label label-default">{{ engine }}</span>
3333 {%- endfor -%}
3434 {%- if result.url -%}
35− <small>{{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info", id) }}</small>
35+ {% if result.cached_url %}
36+ <small>{{ result_link(result.cached_url, icon('link') + _('cached'), "text-info", id) }}</small>
37+ {% elif not result.is_onion %}
38+ <small>{{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info", id) }}</small>
39+ {% endif %}
3640 {%- endif -%}
3741 {%- if proxify -%}
3842 <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info", id) }}</small>
@@ -50,7 +54,11 @@
5054 <span class="label label-default">{{ engine }}</span>
5155 {%- endfor %}
5256 {%- if result.url -%}
53− <small>{{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info", id) }}</small>
57+ {% if result.cached_url %}
58+ <small>{{ result_link(result.cached_url, icon('link') + _('cached'), "text-info", id) }}</small>
59+ {% elif not result.is_onion %}
60+ <small>{{ result_link("https://web.archive.org/web/" + result.url, icon('link') + _('cached'), "text-info", id) }}</small>
61+ {% endif %}
5462 {%- endif -%}
5563 {% if proxify -%}
5664 <small>{{ result_link(proxify(result.url), icon('sort') + _('proxied'), "text-info", id) }}</small>
searx/webapp.py+1
@@ -146,6 +146,7 @@ _category_names = (gettext('files'),
146146 gettext('it'),
147147 gettext('news'),
148148 gettext('map'),
149+ gettext('onions'),
149150 gettext('science'))
150151
151152outgoing_proxies = settings['outgoing'].get('proxies') or None
tests/unit/engines/test_xpath.pynew file+121
@@ -0,0 +1,121 @@
1+# -*- coding: utf-8 -*-
2+from collections import defaultdict
3+import mock
4+from searx.engines import xpath
5+from searx.testing import SearxTestCase
6+
7+
8+class TestXpathEngine(SearxTestCase):
9+
10+ def test_request(self):
11+ xpath.search_url = 'https://url.com/{query}'
12+ xpath.categories = []
13+ xpath.paging = False
14+ query = 'test_query'
15+ dicto = defaultdict(dict)
16+ params = xpath.request(query, dicto)
17+ self.assertIn('url', params)
18+ self.assertEquals('https://url.com/test_query', params['url'])
19+
20+ xpath.search_url = 'https://url.com/q={query}&p={pageno}'
21+ xpath.paging = True
22+ query = 'test_query'
23+ dicto = defaultdict(dict)
24+ dicto['pageno'] = 1
25+ params = xpath.request(query, dicto)
26+ self.assertIn('url', params)
27+ self.assertEquals('https://url.com/q=test_query&p=1', params['url'])
28+
29+ def test_response(self):
30+ # without results_xpath
31+ xpath.url_xpath = '//div[@class="search_result"]//a[@class="result"]/@href'
32+ xpath.title_xpath = '//div[@class="search_result"]//a[@class="result"]'
33+ xpath.content_xpath = '//div[@class="search_result"]//p[@class="content"]'
34+
35+ self.assertRaises(AttributeError, xpath.response, None)
36+ self.assertRaises(AttributeError, xpath.response, [])
37+ self.assertRaises(AttributeError, xpath.response, '')
38+ self.assertRaises(AttributeError, xpath.response, '[]')
39+
40+ response = mock.Mock(text='<html></html>')
41+ self.assertEqual(xpath.response(response), [])
42+
43+ html = u"""
44+ <div>
45+ <div class="search_result">
46+ <a class="result" href="https://result1.com">Result 1</a>
47+ <p class="content">Content 1</p>
48+ <a class="cached" href="https://cachedresult1.com">Cache</a>
49+ </div>
50+ <div class="search_result">
51+ <a class="result" href="https://result2.com">Result 2</a>
52+ <p class="content">Content 2</p>
53+ <a class="cached" href="https://cachedresult2.com">Cache</a>
54+ </div>
55+ </div>
56+ """
57+ response = mock.Mock(text=html)
58+ results = xpath.response(response)
59+ self.assertEqual(type(results), list)
60+ self.assertEqual(len(results), 2)
61+ self.assertEqual(results[0]['title'], 'Result 1')
62+ self.assertEqual(results[0]['url'], 'https://result1.com/')
63+ self.assertEqual(results[0]['content'], 'Content 1')
64+ self.assertEqual(results[1]['title'], 'Result 2')
65+ self.assertEqual(results[1]['url'], 'https://result2.com/')
66+ self.assertEqual(results[1]['content'], 'Content 2')
67+
68+ # with cached urls, without results_xpath
69+ xpath.cached_xpath = '//div[@class="search_result"]//a[@class="cached"]/@href'
70+ results = xpath.response(response)
71+ self.assertEqual(type(results), list)
72+ self.assertEqual(len(results), 2)
73+ self.assertEqual(results[0]['cached_url'], 'https://cachedresult1.com')
74+ self.assertEqual(results[1]['cached_url'], 'https://cachedresult2.com')
75+ self.assertFalse(results[0].get('is_onion', False))
76+
77+ # results are onion urls (no results_xpath)
78+ xpath.categories = ['onions']
79+ results = xpath.response(response)
80+ self.assertTrue(results[0]['is_onion'])
81+
82+ # with results_xpath
83+ xpath.results_xpath = '//div[@class="search_result"]'
84+ xpath.url_xpath = './/a[@class="result"]/@href'
85+ xpath.title_xpath = './/a[@class="result"]'
86+ xpath.content_xpath = './/p[@class="content"]'
87+ xpath.cached_xpath = None
88+ xpath.categories = []
89+
90+ self.assertRaises(AttributeError, xpath.response, None)
91+ self.assertRaises(AttributeError, xpath.response, [])
92+ self.assertRaises(AttributeError, xpath.response, '')
93+ self.assertRaises(AttributeError, xpath.response, '[]')
94+
95+ response = mock.Mock(text='<html></html>')
96+ self.assertEqual(xpath.response(response), [])
97+
98+ response = mock.Mock(text=html)
99+ results = xpath.response(response)
100+ self.assertEqual(type(results), list)
101+ self.assertEqual(len(results), 2)
102+ self.assertEqual(results[0]['title'], 'Result 1')
103+ self.assertEqual(results[0]['url'], 'https://result1.com/')
104+ self.assertEqual(results[0]['content'], 'Content 1')
105+ self.assertEqual(results[1]['title'], 'Result 2')
106+ self.assertEqual(results[1]['url'], 'https://result2.com/')
107+ self.assertEqual(results[1]['content'], 'Content 2')
108+
109+ # with cached urls, with results_xpath
110+ xpath.cached_xpath = './/a[@class="cached"]/@href'
111+ results = xpath.response(response)
112+ self.assertEqual(type(results), list)
113+ self.assertEqual(len(results), 2)
114+ self.assertEqual(results[0]['cached_url'], 'https://cachedresult1.com')
115+ self.assertEqual(results[1]['cached_url'], 'https://cachedresult2.com')
116+ self.assertFalse(results[0].get('is_onion', False))
117+
118+ # results are onion urls (with results_xpath)
119+ xpath.categories = ['onions']
120+ results = xpath.response(response)
121+ self.assertTrue(results[0]['is_onion'])
tests/unit/test_engines_init.pynew file+44
@@ -0,0 +1,44 @@
1+from searx.testing import SearxTestCase
2+from searx import settings, engines
3+
4+
5+class TestEnginesInit(SearxTestCase):
6+
7+ @classmethod
8+ def tearDownClass(cls):
9+ settings['outgoing']['using_tor_proxy'] = False
10+ settings['outgoing']['extra_proxy_timeout'] = 0
11+
12+ def test_initialize_engines_default(self):
13+ engine_list = [{'engine': 'dummy', 'name': 'engine1', 'shortcut': 'e1'},
14+ {'engine': 'dummy', 'name': 'engine2', 'shortcut': 'e2'}]
15+
16+ engines.initialize_engines(engine_list)
17+ self.assertEqual(len(engines.engines), 2)
18+ self.assertIn('engine1', engines.engines)
19+ self.assertIn('engine2', engines.engines)
20+
21+ def test_initialize_engines_exclude_onions(self):
22+ settings['outgoing']['using_tor_proxy'] = False
23+ engine_list = [{'engine': 'dummy', 'name': 'engine1', 'shortcut': 'e1', 'categories': 'general'},
24+ {'engine': 'dummy', 'name': 'engine2', 'shortcut': 'e2', 'categories': 'onions'}]
25+
26+ engines.initialize_engines(engine_list)
27+ self.assertEqual(len(engines.engines), 1)
28+ self.assertIn('engine1', engines.engines)
29+ self.assertNotIn('onions', engines.categories)
30+
31+ def test_initialize_engines_include_onions(self):
32+ settings['outgoing']['using_tor_proxy'] = True
33+ settings['outgoing']['extra_proxy_timeout'] = 100.0
34+ engine_list = [{'engine': 'dummy', 'name': 'engine1', 'shortcut': 'e1', 'categories': 'general',
35+ 'timeout': 20.0, 'onion_url': 'http://engine1.onion'},
36+ {'engine': 'dummy', 'name': 'engine2', 'shortcut': 'e2', 'categories': 'onions'}]
37+
38+ engines.initialize_engines(engine_list)
39+ self.assertEqual(len(engines.engines), 2)
40+ self.assertIn('engine1', engines.engines)
41+ self.assertIn('engine2', engines.engines)
42+ self.assertIn('onions', engines.categories)
43+ self.assertIn('http://engine1.onion', engines.engines['engine1'].search_url)
44+ self.assertEqual(engines.engines['engine1'].timeout, 120.0)