webapp.py 26 KB

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