webapp.py 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. #!/usr/bin/env python
  2. '''
  3. searx is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as published by
  5. the Free Software Foundation, either version 3 of the License, or
  6. (at your option) any later version.
  7. searx is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with searx. If not, see < http://www.gnu.org/licenses/ >.
  13. (C) 2013- by Adam Tauber, <asciimoo@gmail.com>
  14. '''
  15. if __name__ == '__main__':
  16. from sys import path
  17. from os.path import realpath, dirname
  18. path.append(realpath(dirname(realpath(__file__)) + '/../'))
  19. import cStringIO
  20. import hashlib
  21. import hmac
  22. import json
  23. import os
  24. import requests
  25. from searx import logger
  26. logger = logger.getChild('webapp')
  27. try:
  28. from pygments import highlight
  29. from pygments.lexers import get_lexer_by_name
  30. from pygments.formatters import HtmlFormatter
  31. except:
  32. logger.critical("cannot import dependency: pygments")
  33. from sys import exit
  34. exit(1)
  35. from cgi import escape
  36. from datetime import datetime, timedelta
  37. from urllib import urlencode
  38. from urlparse import urlparse, urljoin
  39. from werkzeug.contrib.fixers import ProxyFix
  40. from flask import (
  41. Flask, request, render_template, url_for, Response, make_response,
  42. redirect, send_from_directory
  43. )
  44. from flask_babel import Babel, gettext, format_date, format_decimal
  45. from flask.json import jsonify
  46. from searx import settings, searx_dir, searx_debug
  47. from searx.exceptions import SearxException, SearxParameterException
  48. from searx.engines import (
  49. categories, engines, engine_shortcuts, get_engines_stats, initialize_engines
  50. )
  51. from searx.utils import (
  52. UnicodeWriter, highlight_content, html_to_text, get_themes,
  53. get_static_files, get_result_templates, gen_useragent, dict_subset,
  54. prettify_url
  55. )
  56. from searx.version import VERSION_STRING
  57. from searx.languages import language_codes
  58. from searx.search import SearchWithPlugins, get_search_query_from_webapp
  59. from searx.query import RawTextQuery
  60. from searx.autocomplete import searx_bang, backends as autocomplete_backends
  61. from searx.plugins import plugins
  62. from searx.preferences import Preferences, ValidationException
  63. from searx.answerers import answerers
  64. # check if the pyopenssl, ndg-httpsclient, pyasn1 packages are installed.
  65. # They are needed for SSL connection without trouble, see #298
  66. try:
  67. import OpenSSL.SSL # NOQA
  68. import ndg.httpsclient # NOQA
  69. import pyasn1 # NOQA
  70. except ImportError:
  71. logger.critical("The pyopenssl, ndg-httpsclient, pyasn1 packages have to be installed.\n"
  72. "Some HTTPS connections will fail")
  73. # serve pages with HTTP/1.1
  74. from werkzeug.serving import WSGIRequestHandler
  75. WSGIRequestHandler.protocol_version = "HTTP/{}".format(settings['server'].get('http_protocol_version', '1.0'))
  76. static_path, templates_path, themes =\
  77. get_themes(settings['ui']['themes_path']
  78. if settings['ui']['themes_path']
  79. else searx_dir)
  80. default_theme = settings['ui']['default_theme']
  81. static_files = get_static_files(searx_dir)
  82. result_templates = get_result_templates(searx_dir)
  83. app = Flask(
  84. __name__,
  85. static_folder=static_path,
  86. template_folder=templates_path
  87. )
  88. app.jinja_env.trim_blocks = True
  89. app.jinja_env.lstrip_blocks = True
  90. app.secret_key = settings['server']['secret_key']
  91. if not searx_debug or os.environ.get("WERKZEUG_RUN_MAIN") == "true":
  92. initialize_engines(settings['engines'])
  93. babel = Babel(app)
  94. rtl_locales = ['ar', 'arc', 'bcc', 'bqi', 'ckb', 'dv', 'fa', 'glk', 'he',
  95. 'ku', 'mzn', 'pnb'', ''ps', 'sd', 'ug', 'ur', 'yi']
  96. global_favicons = []
  97. for indice, theme in enumerate(themes):
  98. global_favicons.append([])
  99. theme_img_path = searx_dir + "/static/themes/" + theme + "/img/icons/"
  100. for (dirpath, dirnames, filenames) in os.walk(theme_img_path):
  101. global_favicons[indice].extend(filenames)
  102. # used when translating category names
  103. _category_names = (gettext('files'),
  104. gettext('general'),
  105. gettext('music'),
  106. gettext('social media'),
  107. gettext('images'),
  108. gettext('videos'),
  109. gettext('it'),
  110. gettext('news'),
  111. gettext('map'),
  112. gettext('science'))
  113. outgoing_proxies = settings['outgoing'].get('proxies', None)
  114. @babel.localeselector
  115. def get_locale():
  116. locale = request.accept_languages.best_match(settings['locales'].keys())
  117. if request.preferences.get_value('locale') != '':
  118. locale = request.preferences.get_value('locale')
  119. if 'locale' in request.args\
  120. and request.args['locale'] in settings['locales']:
  121. locale = request.args['locale']
  122. if 'locale' in request.form\
  123. and request.form['locale'] in settings['locales']:
  124. locale = request.form['locale']
  125. return locale
  126. # code-highlighter
  127. @app.template_filter('code_highlighter')
  128. def code_highlighter(codelines, language=None):
  129. if not language:
  130. language = 'text'
  131. try:
  132. # find lexer by programing language
  133. lexer = get_lexer_by_name(language, stripall=True)
  134. except:
  135. # if lexer is not found, using default one
  136. logger.debug('highlighter cannot find lexer for {0}'.format(language))
  137. lexer = get_lexer_by_name('text', stripall=True)
  138. html_code = ''
  139. tmp_code = ''
  140. last_line = None
  141. # parse lines
  142. for line, code in codelines:
  143. if not last_line:
  144. line_code_start = line
  145. # new codeblock is detected
  146. if last_line is not None and\
  147. last_line + 1 != line:
  148. # highlight last codepart
  149. formatter = HtmlFormatter(linenos='inline',
  150. linenostart=line_code_start)
  151. html_code = html_code + highlight(tmp_code, lexer, formatter)
  152. # reset conditions for next codepart
  153. tmp_code = ''
  154. line_code_start = line
  155. # add codepart
  156. tmp_code += code + '\n'
  157. # update line
  158. last_line = line
  159. # highlight last codepart
  160. formatter = HtmlFormatter(linenos='inline', linenostart=line_code_start)
  161. html_code = html_code + highlight(tmp_code, lexer, formatter)
  162. return html_code
  163. # Extract domain from url
  164. @app.template_filter('extract_domain')
  165. def extract_domain(url):
  166. return urlparse(url)[1]
  167. def get_base_url():
  168. if settings['server']['base_url']:
  169. hostname = settings['server']['base_url']
  170. else:
  171. scheme = 'http'
  172. if request.is_secure:
  173. scheme = 'https'
  174. hostname = url_for('index', _external=True, _scheme=scheme)
  175. return hostname
  176. def get_current_theme_name(override=None):
  177. """Returns theme name.
  178. Checks in this order:
  179. 1. override
  180. 2. cookies
  181. 3. settings"""
  182. if override and (override in themes or override == '__common__'):
  183. return override
  184. theme_name = request.args.get('theme', request.preferences.get_value('theme'))
  185. if theme_name not in themes:
  186. theme_name = default_theme
  187. return theme_name
  188. def get_result_template(theme, template_name):
  189. themed_path = theme + '/result_templates/' + template_name
  190. if themed_path in result_templates:
  191. return themed_path
  192. return 'result_templates/' + template_name
  193. def url_for_theme(endpoint, override_theme=None, **values):
  194. if endpoint == 'static' and values.get('filename'):
  195. theme_name = get_current_theme_name(override=override_theme)
  196. filename_with_theme = "themes/{}/{}".format(theme_name, values['filename'])
  197. if filename_with_theme in static_files:
  198. values['filename'] = filename_with_theme
  199. return url_for(endpoint, **values)
  200. def proxify(url):
  201. if url.startswith('//'):
  202. url = 'https:' + url
  203. if not settings.get('result_proxy'):
  204. return url
  205. url_params = dict(mortyurl=url.encode('utf-8'))
  206. if settings['result_proxy'].get('key'):
  207. url_params['mortyhash'] = hmac.new(settings['result_proxy']['key'],
  208. url.encode('utf-8'),
  209. hashlib.sha256).hexdigest()
  210. return '{0}?{1}'.format(settings['result_proxy']['url'],
  211. urlencode(url_params))
  212. def image_proxify(url):
  213. if url.startswith('//'):
  214. url = 'https:' + url
  215. if not request.preferences.get_value('image_proxy'):
  216. return url
  217. if settings.get('result_proxy'):
  218. return proxify(url)
  219. h = hmac.new(settings['server']['secret_key'], url.encode('utf-8'), hashlib.sha256).hexdigest()
  220. return '{0}?{1}'.format(url_for('image_proxy'),
  221. urlencode(dict(url=url.encode('utf-8'), h=h)))
  222. def render(template_name, override_theme=None, **kwargs):
  223. disabled_engines = request.preferences.engines.get_disabled()
  224. enabled_categories = set(category for engine_name in engines
  225. for category in engines[engine_name].categories
  226. if (engine_name, category) not in disabled_engines)
  227. if 'categories' not in kwargs:
  228. kwargs['categories'] = ['general']
  229. kwargs['categories'].extend(x for x in
  230. sorted(categories.keys())
  231. if x != 'general'
  232. and x in enabled_categories)
  233. if 'all_categories' not in kwargs:
  234. kwargs['all_categories'] = ['general']
  235. kwargs['all_categories'].extend(x for x in
  236. sorted(categories.keys())
  237. if x != 'general')
  238. if 'selected_categories' not in kwargs:
  239. kwargs['selected_categories'] = []
  240. for arg in request.args:
  241. if arg.startswith('category_'):
  242. c = arg.split('_', 1)[1]
  243. if c in categories:
  244. kwargs['selected_categories'].append(c)
  245. if not kwargs['selected_categories']:
  246. cookie_categories = request.preferences.get_value('categories')
  247. for ccateg in cookie_categories:
  248. kwargs['selected_categories'].append(ccateg)
  249. if not kwargs['selected_categories']:
  250. kwargs['selected_categories'] = ['general']
  251. if 'autocomplete' not in kwargs:
  252. kwargs['autocomplete'] = request.preferences.get_value('autocomplete')
  253. if get_locale() in rtl_locales and 'rtl' not in kwargs:
  254. kwargs['rtl'] = True
  255. kwargs['searx_version'] = VERSION_STRING
  256. kwargs['method'] = request.preferences.get_value('method')
  257. kwargs['safesearch'] = str(request.preferences.get_value('safesearch'))
  258. kwargs['language_codes'] = language_codes
  259. if 'current_language' not in kwargs:
  260. kwargs['current_language'] = request.preferences.get_value('language')
  261. # override url_for function in templates
  262. kwargs['url_for'] = url_for_theme
  263. kwargs['image_proxify'] = image_proxify
  264. kwargs['proxify'] = proxify if settings.get('result_proxy') else None
  265. kwargs['get_result_template'] = get_result_template
  266. kwargs['theme'] = get_current_theme_name(override=override_theme)
  267. kwargs['template_name'] = template_name
  268. kwargs['cookies'] = request.cookies
  269. kwargs['errors'] = request.errors
  270. kwargs['instance_name'] = settings['general']['instance_name']
  271. kwargs['results_on_new_tab'] = request.preferences.get_value('results_on_new_tab')
  272. kwargs['scripts'] = set()
  273. for plugin in request.user_plugins:
  274. for script in plugin.js_dependencies:
  275. kwargs['scripts'].add(script)
  276. kwargs['styles'] = set()
  277. for plugin in request.user_plugins:
  278. for css in plugin.css_dependencies:
  279. kwargs['styles'].add(css)
  280. return render_template(
  281. '{}/{}'.format(kwargs['theme'], template_name), **kwargs)
  282. @app.before_request
  283. def pre_request():
  284. request.errors = []
  285. preferences = Preferences(themes, categories.keys(), engines, plugins)
  286. request.preferences = preferences
  287. try:
  288. preferences.parse_cookies(request.cookies)
  289. except:
  290. request.errors.append(gettext('Invalid settings, please edit your preferences'))
  291. # merge GET, POST vars
  292. # request.form
  293. request.form = dict(request.form.items())
  294. for k, v in request.args.items():
  295. if k not in request.form:
  296. request.form[k] = v
  297. # request.user_plugins
  298. request.user_plugins = []
  299. allowed_plugins = preferences.plugins.get_enabled()
  300. disabled_plugins = preferences.plugins.get_disabled()
  301. for plugin in plugins:
  302. if ((plugin.default_on and plugin.id not in disabled_plugins)
  303. or plugin.id in allowed_plugins):
  304. request.user_plugins.append(plugin)
  305. def index_error(output_format, error_message):
  306. if output_format == 'json':
  307. return Response(json.dumps({'error': error_message}),
  308. mimetype='application/json')
  309. elif output_format == 'csv':
  310. response = Response('', mimetype='application/csv')
  311. cont_disp = 'attachment;Filename=searx.csv'
  312. response.headers.add('Content-Disposition', cont_disp)
  313. return response
  314. elif output_format == 'rss':
  315. response_rss = render(
  316. 'opensearch_response_rss.xml',
  317. results=[],
  318. q=request.form['q'] if 'q' in request.form else '',
  319. number_of_results=0,
  320. base_url=get_base_url(),
  321. error_message=error_message
  322. )
  323. return Response(response_rss, mimetype='text/xml')
  324. else:
  325. # html
  326. request.errors.append(gettext('search error'))
  327. return render(
  328. 'index.html',
  329. )
  330. @app.route('/search', methods=['GET', 'POST'])
  331. @app.route('/', methods=['GET', 'POST'])
  332. def index():
  333. """Render index page.
  334. Supported outputs: html, json, csv, rss.
  335. """
  336. # output_format
  337. output_format = request.form.get('format', 'html')
  338. if output_format not in ['html', 'csv', 'json', 'rss']:
  339. output_format = 'html'
  340. # check if there is query
  341. if request.form.get('q') is None:
  342. if output_format == 'html':
  343. return render(
  344. 'index.html',
  345. )
  346. else:
  347. return index_error(output_format, 'No query'), 400
  348. # search
  349. search_query = None
  350. result_container = None
  351. try:
  352. search_query = get_search_query_from_webapp(request.preferences, request.form)
  353. # search = Search(search_query) # without plugins
  354. search = SearchWithPlugins(search_query, request)
  355. result_container = search.search()
  356. except Exception as e:
  357. # log exception
  358. logger.exception('search error')
  359. # is it an invalid input parameter or something else ?
  360. if (issubclass(e.__class__, SearxParameterException)):
  361. return index_error(output_format, e.message), 400
  362. else:
  363. return index_error(output_format, gettext('search error')), 500
  364. # results
  365. results = result_container.get_ordered_results()
  366. number_of_results = result_container.results_number()
  367. if number_of_results < result_container.results_length():
  368. number_of_results = 0
  369. # UI
  370. advanced_search = request.form.get('advanced_search', None)
  371. # output
  372. for result in results:
  373. if output_format == 'html':
  374. if 'content' in result and result['content']:
  375. result['content'] = highlight_content(escape(result['content'][:1024]),
  376. search_query.query.encode('utf-8'))
  377. result['title'] = highlight_content(escape(result['title'] or u''),
  378. search_query.query.encode('utf-8'))
  379. else:
  380. if result.get('content'):
  381. result['content'] = html_to_text(result['content']).strip()
  382. # removing html content and whitespace duplications
  383. result['title'] = ' '.join(html_to_text(result['title']).strip().split())
  384. result['pretty_url'] = prettify_url(result['url'])
  385. # TODO, check if timezone is calculated right
  386. if 'publishedDate' in result:
  387. try: # test if publishedDate >= 1900 (datetime module bug)
  388. result['pubdate'] = result['publishedDate'].strftime('%Y-%m-%d %H:%M:%S%z')
  389. except ValueError:
  390. result['publishedDate'] = None
  391. else:
  392. if result['publishedDate'].replace(tzinfo=None) >= datetime.now() - timedelta(days=1):
  393. timedifference = datetime.now() - result['publishedDate'].replace(tzinfo=None)
  394. minutes = int((timedifference.seconds / 60) % 60)
  395. hours = int(timedifference.seconds / 60 / 60)
  396. if hours == 0:
  397. result['publishedDate'] = gettext(u'{minutes} minute(s) ago').format(minutes=minutes)
  398. else:
  399. result['publishedDate'] = gettext(u'{hours} hour(s), {minutes} minute(s) ago').format(hours=hours, minutes=minutes) # noqa
  400. else:
  401. result['publishedDate'] = format_date(result['publishedDate'])
  402. if output_format == 'json':
  403. return Response(json.dumps({'query': search_query.query,
  404. 'number_of_results': number_of_results,
  405. 'results': results,
  406. 'answers': list(result_container.answers),
  407. 'corrections': list(result_container.corrections),
  408. 'infoboxes': result_container.infoboxes,
  409. 'suggestions': list(result_container.suggestions)}),
  410. mimetype='application/json')
  411. elif output_format == 'csv':
  412. csv = UnicodeWriter(cStringIO.StringIO())
  413. keys = ('title', 'url', 'content', 'host', 'engine', 'score')
  414. csv.writerow(keys)
  415. for row in results:
  416. row['host'] = row['parsed_url'].netloc
  417. csv.writerow([row.get(key, '') for key in keys])
  418. csv.stream.seek(0)
  419. response = Response(csv.stream.read(), mimetype='application/csv')
  420. cont_disp = 'attachment;Filename=searx_-_{0}.csv'.format(search_query.query.encode('utf-8'))
  421. response.headers.add('Content-Disposition', cont_disp)
  422. return response
  423. elif output_format == 'rss':
  424. response_rss = render(
  425. 'opensearch_response_rss.xml',
  426. results=results,
  427. q=request.form['q'],
  428. number_of_results=number_of_results,
  429. base_url=get_base_url(),
  430. override_theme='__common__',
  431. )
  432. return Response(response_rss, mimetype='text/xml')
  433. return render(
  434. 'results.html',
  435. results=results,
  436. q=request.form['q'],
  437. selected_categories=search_query.categories,
  438. pageno=search_query.pageno,
  439. time_range=search_query.time_range,
  440. number_of_results=format_decimal(number_of_results),
  441. advanced_search=advanced_search,
  442. suggestions=result_container.suggestions,
  443. answers=result_container.answers,
  444. corrections=result_container.corrections,
  445. infoboxes=result_container.infoboxes,
  446. paging=result_container.paging,
  447. current_language=search_query.lang,
  448. base_url=get_base_url(),
  449. theme=get_current_theme_name(),
  450. favicons=global_favicons[themes.index(get_current_theme_name())]
  451. )
  452. @app.route('/about', methods=['GET'])
  453. def about():
  454. """Render about page"""
  455. return render(
  456. 'about.html',
  457. )
  458. @app.route('/autocompleter', methods=['GET', 'POST'])
  459. def autocompleter():
  460. """Return autocompleter results"""
  461. # set blocked engines
  462. disabled_engines = request.preferences.engines.get_disabled()
  463. # parse query
  464. raw_text_query = RawTextQuery(request.form.get('q', '').encode('utf-8'), disabled_engines)
  465. raw_text_query.parse_query()
  466. # check if search query is set
  467. if not raw_text_query.getSearchQuery():
  468. return '', 400
  469. # run autocompleter
  470. completer = autocomplete_backends.get(request.preferences.get_value('autocomplete'))
  471. # parse searx specific autocompleter results like !bang
  472. raw_results = searx_bang(raw_text_query)
  473. # normal autocompletion results only appear if max 3 inner results returned
  474. if len(raw_results) <= 3 and completer:
  475. # get language from cookie
  476. language = request.preferences.get_value('language')
  477. if not language or language == 'all':
  478. language = 'en'
  479. else:
  480. language = language.split('-')[0]
  481. # run autocompletion
  482. raw_results.extend(completer(raw_text_query.getSearchQuery(), language))
  483. # parse results (write :language and !engine back to result string)
  484. results = []
  485. for result in raw_results:
  486. raw_text_query.changeSearchQuery(result)
  487. # add parsed result
  488. results.append(raw_text_query.getFullQuery())
  489. # return autocompleter results
  490. if request.form.get('format') == 'x-suggestions':
  491. return Response(json.dumps([raw_text_query.query, results]),
  492. mimetype='application/json')
  493. return Response(json.dumps(results),
  494. mimetype='application/json')
  495. @app.route('/preferences', methods=['GET', 'POST'])
  496. def preferences():
  497. """Render preferences page && save user preferences"""
  498. # save preferences
  499. if request.method == 'POST':
  500. resp = make_response(redirect(urljoin(settings['server']['base_url'], url_for('index'))))
  501. try:
  502. request.preferences.parse_form(request.form)
  503. except ValidationException:
  504. request.errors.append(gettext('Invalid settings, please edit your preferences'))
  505. return resp
  506. return request.preferences.save(resp)
  507. # render preferences
  508. image_proxy = request.preferences.get_value('image_proxy')
  509. lang = request.preferences.get_value('language')
  510. disabled_engines = request.preferences.engines.get_disabled()
  511. allowed_plugins = request.preferences.plugins.get_enabled()
  512. # stats for preferences page
  513. stats = {}
  514. for c in categories:
  515. for e in categories[c]:
  516. stats[e.name] = {'time': None,
  517. 'warn_timeout': False,
  518. 'warn_time': False}
  519. if e.timeout > settings['outgoing']['request_timeout']:
  520. stats[e.name]['warn_timeout'] = True
  521. # get first element [0], the engine time,
  522. # and then the second element [1] : the time (the first one is the label)
  523. for engine_stat in get_engines_stats()[0][1]:
  524. stats[engine_stat.get('name')]['time'] = round(engine_stat.get('avg'), 3)
  525. if engine_stat.get('avg') > settings['outgoing']['request_timeout']:
  526. stats[engine_stat.get('name')]['warn_time'] = True
  527. # end of stats
  528. return render('preferences.html',
  529. locales=settings['locales'],
  530. current_locale=get_locale(),
  531. image_proxy=image_proxy,
  532. engines_by_category=categories,
  533. stats=stats,
  534. answerers=[{'info': a.self_info(), 'keywords': a.keywords} for a in answerers],
  535. disabled_engines=disabled_engines,
  536. autocomplete_backends=autocomplete_backends,
  537. shortcuts={y: x for x, y in engine_shortcuts.items()},
  538. themes=themes,
  539. plugins=plugins,
  540. allowed_plugins=allowed_plugins,
  541. theme=get_current_theme_name(),
  542. preferences=True)
  543. @app.route('/image_proxy', methods=['GET'])
  544. def image_proxy():
  545. url = request.args.get('url').encode('utf-8')
  546. if not url:
  547. return '', 400
  548. h = hmac.new(settings['server']['secret_key'], url, hashlib.sha256).hexdigest()
  549. if h != request.args.get('h'):
  550. return '', 400
  551. headers = dict_subset(request.headers, {'If-Modified-Since', 'If-None-Match'})
  552. headers['User-Agent'] = gen_useragent()
  553. resp = requests.get(url,
  554. stream=True,
  555. timeout=settings['outgoing']['request_timeout'],
  556. headers=headers,
  557. proxies=outgoing_proxies)
  558. if resp.status_code == 304:
  559. return '', resp.status_code
  560. if resp.status_code != 200:
  561. logger.debug('image-proxy: wrong response code: {0}'.format(resp.status_code))
  562. if resp.status_code >= 400:
  563. return '', resp.status_code
  564. return '', 400
  565. if not resp.headers.get('content-type', '').startswith('image/'):
  566. logger.debug('image-proxy: wrong content-type: {0}'.format(resp.headers.get('content-type')))
  567. return '', 400
  568. img = ''
  569. chunk_counter = 0
  570. for chunk in resp.iter_content(1024 * 1024):
  571. chunk_counter += 1
  572. if chunk_counter > 5:
  573. return '', 502 # Bad gateway - file is too big (>5M)
  574. img += chunk
  575. headers = dict_subset(resp.headers, {'Content-Length', 'Length', 'Date', 'Last-Modified', 'Expires', 'Etag'})
  576. return Response(img, mimetype=resp.headers['content-type'], headers=headers)
  577. @app.route('/stats', methods=['GET'])
  578. def stats():
  579. """Render engine statistics page."""
  580. stats = get_engines_stats()
  581. return render(
  582. 'stats.html',
  583. stats=stats,
  584. )
  585. @app.route('/robots.txt', methods=['GET'])
  586. def robots():
  587. return Response("""User-agent: *
  588. Allow: /
  589. Allow: /about
  590. Disallow: /stats
  591. Disallow: /preferences
  592. Disallow: /*?*q=*
  593. """, mimetype='text/plain')
  594. @app.route('/opensearch.xml', methods=['GET'])
  595. def opensearch():
  596. method = 'post'
  597. if request.preferences.get_value('method') == 'GET':
  598. method = 'get'
  599. # chrome/chromium only supports HTTP GET....
  600. if request.headers.get('User-Agent', '').lower().find('webkit') >= 0:
  601. method = 'get'
  602. ret = render('opensearch.xml',
  603. opensearch_method=method,
  604. host=get_base_url(),
  605. urljoin=urljoin,
  606. override_theme='__common__')
  607. resp = Response(response=ret,
  608. status=200,
  609. mimetype="text/xml")
  610. return resp
  611. @app.route('/favicon.ico')
  612. def favicon():
  613. return send_from_directory(os.path.join(app.root_path,
  614. 'static/themes',
  615. get_current_theme_name(),
  616. 'img'),
  617. 'favicon.png',
  618. mimetype='image/vnd.microsoft.icon')
  619. @app.route('/clear_cookies')
  620. def clear_cookies():
  621. resp = make_response(redirect(urljoin(settings['server']['base_url'], url_for('index'))))
  622. for cookie_name in request.cookies:
  623. resp.delete_cookie(cookie_name)
  624. return resp
  625. @app.route('/config')
  626. def config():
  627. return jsonify({'categories': categories.keys(),
  628. 'engines': [{'name': engine_name,
  629. 'categories': engine.categories,
  630. 'shortcut': engine.shortcut,
  631. 'enabled': not engine.disabled,
  632. 'paging': engine.paging,
  633. 'language_support': engine.language_support,
  634. 'supported_languages':
  635. engine.supported_languages.keys()
  636. if isinstance(engine.supported_languages, dict)
  637. else engine.supported_languages,
  638. 'safesearch': engine.safesearch,
  639. 'time_range_support': engine.time_range_support,
  640. 'timeout': engine.timeout}
  641. for engine_name, engine in engines.items()],
  642. 'plugins': [{'name': plugin.name,
  643. 'enabled': plugin.default_on}
  644. for plugin in plugins],
  645. 'instance_name': settings['general']['instance_name'],
  646. 'locales': settings['locales'],
  647. 'default_locale': settings['ui']['default_locale'],
  648. 'autocomplete': settings['search']['autocomplete'],
  649. 'safe_search': settings['search']['safe_search'],
  650. 'default_theme': settings['ui']['default_theme'],
  651. 'version': VERSION_STRING})
  652. @app.errorhandler(404)
  653. def page_not_found(e):
  654. return render('404.html'), 404
  655. def run():
  656. app.run(
  657. debug=searx_debug,
  658. use_debugger=searx_debug,
  659. port=settings['server']['port'],
  660. host=settings['server']['bind_address'],
  661. threaded=True
  662. )
  663. class ReverseProxyPathFix(object):
  664. '''Wrap the application in this middleware and configure the
  665. front-end server to add these headers, to let you quietly bind
  666. this to a URL other than / and to an HTTP scheme that is
  667. different than what is used locally.
  668. http://flask.pocoo.org/snippets/35/
  669. In nginx:
  670. location /myprefix {
  671. proxy_pass http://127.0.0.1:8000;
  672. proxy_set_header Host $host;
  673. proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  674. proxy_set_header X-Scheme $scheme;
  675. proxy_set_header X-Script-Name /myprefix;
  676. }
  677. :param app: the WSGI application
  678. '''
  679. def __init__(self, app):
  680. self.app = app
  681. def __call__(self, environ, start_response):
  682. script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
  683. if script_name:
  684. environ['SCRIPT_NAME'] = script_name
  685. path_info = environ['PATH_INFO']
  686. if path_info.startswith(script_name):
  687. environ['PATH_INFO'] = path_info[len(script_name):]
  688. scheme = environ.get('HTTP_X_SCHEME', '')
  689. if scheme:
  690. environ['wsgi.url_scheme'] = scheme
  691. return self.app(environ, start_response)
  692. application = app
  693. # patch app to handle non root url-s behind proxy & wsgi
  694. app.wsgi_app = ReverseProxyPathFix(ProxyFix(application.wsgi_app))
  695. if __name__ == "__main__":
  696. run()