webapp.py 24 KB

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