Skip to content

Commit f13d4f6

Browse files
authored
Merge pull request chubin#260 from chubin/unboxing
Decouple Flask `app` from `gevent` monkeypatching
2 parents 121298d + 277b45e commit f13d4f6

File tree

2 files changed

+286
-280
lines changed

2 files changed

+286
-280
lines changed

bin/app.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
#!/usr/bin/env python
2+
# vim: set encoding=utf-8
3+
# pylint: disable=wrong-import-position,wrong-import-order
4+
5+
"""
6+
Main server program.
7+
8+
Configuration parameters:
9+
10+
path.internal.malformed
11+
path.internal.static
12+
path.internal.templates
13+
path.log.main
14+
path.log.queries
15+
"""
16+
17+
from __future__ import print_function
18+
19+
import sys
20+
if sys.version_info[0] < 3:
21+
reload(sys)
22+
sys.setdefaultencoding('utf8')
23+
24+
import sys
25+
import logging
26+
import os
27+
import requests
28+
import jinja2
29+
from flask import Flask, request, send_from_directory, redirect, Response
30+
31+
sys.path.append(os.path.abspath(os.path.join(__file__, "..", "..", "lib")))
32+
from config import CONFIG
33+
from limits import Limits
34+
from cheat_wrapper import cheat_wrapper
35+
from post import process_post_request
36+
from options import parse_args
37+
38+
from stateful_queries import save_query, last_query
39+
40+
if not os.path.exists(os.path.dirname(CONFIG["path.log.main"])):
41+
os.makedirs(os.path.dirname(CONFIG["path.log.main"]))
42+
logging.basicConfig(
43+
filename=CONFIG["path.log.main"],
44+
level=logging.DEBUG,
45+
format='%(asctime)s %(message)s')
46+
47+
app = Flask(__name__) # pylint: disable=invalid-name
48+
app.jinja_loader = jinja2.ChoiceLoader([
49+
app.jinja_loader,
50+
jinja2.FileSystemLoader(CONFIG["path.internal.templates"])])
51+
52+
LIMITS = Limits()
53+
54+
def is_html_needed(user_agent):
55+
"""
56+
Basing on `user_agent`, return whether it needs HTML or ANSI
57+
"""
58+
plaintext_clients = [
59+
'curl', 'wget', 'fetch', 'httpie', 'lwp-request', 'openbsd ftp', 'python-requests']
60+
return all([x not in user_agent for x in plaintext_clients])
61+
62+
def is_result_a_script(query):
63+
return query in [':cht.sh']
64+
65+
@app.route('/files/<path:path>')
66+
def send_static(path):
67+
"""
68+
Return static file `path`.
69+
Can be served by the HTTP frontend.
70+
"""
71+
return send_from_directory(CONFIG["path.internal.static"], path)
72+
73+
@app.route('/favicon.ico')
74+
def send_favicon():
75+
"""
76+
Return static file `favicon.ico`.
77+
Can be served by the HTTP frontend.
78+
"""
79+
return send_from_directory(CONFIG["path.internal.static"], 'favicon.ico')
80+
81+
@app.route('/malformed-response.html')
82+
def send_malformed():
83+
"""
84+
Return static file `malformed-response.html`.
85+
Can be served by the HTTP frontend.
86+
"""
87+
dirname, filename = os.path.split(CONFIG["path.internal.malformed"])
88+
return send_from_directory(dirname, filename)
89+
90+
def log_query(ip_addr, found, topic, user_agent):
91+
"""
92+
Log processed query and some internal data
93+
"""
94+
log_entry = "%s %s %s %s\n" % (ip_addr, found, topic, user_agent)
95+
with open(CONFIG["path.log.queries"], 'ab') as my_file:
96+
my_file.write(log_entry.encode('utf-8'))
97+
98+
def get_request_ip(req):
99+
"""
100+
Extract IP address from `request`
101+
"""
102+
103+
if req.headers.getlist("X-Forwarded-For"):
104+
ip_addr = req.headers.getlist("X-Forwarded-For")[0]
105+
if ip_addr.startswith('::ffff:'):
106+
ip_addr = ip_addr[7:]
107+
else:
108+
ip_addr = req.remote_addr
109+
if req.headers.getlist("X-Forwarded-For"):
110+
ip_addr = req.headers.getlist("X-Forwarded-For")[0]
111+
if ip_addr.startswith('::ffff:'):
112+
ip_addr = ip_addr[7:]
113+
else:
114+
ip_addr = req.remote_addr
115+
116+
return ip_addr
117+
118+
def get_answer_language(request):
119+
"""
120+
Return preferred answer language based on
121+
domain name, query arguments and headers
122+
"""
123+
124+
def _parse_accept_language(accept_language):
125+
languages = accept_language.split(",")
126+
locale_q_pairs = []
127+
128+
for language in languages:
129+
try:
130+
if language.split(";")[0] == language:
131+
# no q => q = 1
132+
locale_q_pairs.append((language.strip(), "1"))
133+
else:
134+
locale = language.split(";")[0].strip()
135+
weight = language.split(";")[1].split("=")[1]
136+
locale_q_pairs.append((locale, weight))
137+
except IndexError:
138+
pass
139+
140+
return locale_q_pairs
141+
142+
def _find_supported_language(accepted_languages):
143+
for lang_tuple in accepted_languages:
144+
lang = lang_tuple[0]
145+
if '-' in lang:
146+
lang = lang.split('-', 1)[0]
147+
return lang
148+
return None
149+
150+
lang = None
151+
hostname = request.headers['Host']
152+
if hostname.endswith('.cheat.sh'):
153+
lang = hostname[:-9]
154+
155+
if 'lang' in request.args:
156+
lang = request.args.get('lang')
157+
158+
header_accept_language = request.headers.get('Accept-Language', '')
159+
if lang is None and header_accept_language:
160+
lang = _find_supported_language(
161+
_parse_accept_language(header_accept_language))
162+
163+
return lang
164+
165+
def _proxy(*args, **kwargs):
166+
# print "method=", request.method,
167+
# print "url=", request.url.replace('/:shell-x/', ':3000/')
168+
# print "headers=", {key: value for (key, value) in request.headers if key != 'Host'}
169+
# print "data=", request.get_data()
170+
# print "cookies=", request.cookies
171+
# print "allow_redirects=", False
172+
173+
url_before, url_after = request.url.split('/:shell-x/', 1)
174+
url = url_before + ':3000/'
175+
176+
if 'q' in request.args:
177+
url_after = '?' + "&".join("arg=%s" % x for x in request.args['q'].split())
178+
179+
url += url_after
180+
print(url)
181+
print(request.get_data())
182+
resp = requests.request(
183+
method=request.method,
184+
url=url,
185+
headers={key: value for (key, value) in request.headers if key != 'Host'},
186+
data=request.get_data(),
187+
cookies=request.cookies,
188+
allow_redirects=False)
189+
190+
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
191+
headers = [(name, value) for (name, value) in resp.raw.headers.items()
192+
if name.lower() not in excluded_headers]
193+
194+
response = Response(resp.content, resp.status_code, headers)
195+
return response
196+
197+
198+
@app.route("/", methods=['GET', 'POST'])
199+
@app.route("/<path:topic>", methods=["GET", "POST"])
200+
def answer(topic=None):
201+
"""
202+
Main rendering function, it processes incoming weather queries.
203+
Depending on user agent it returns output in HTML or ANSI format.
204+
205+
Incoming data:
206+
request.args
207+
request.headers
208+
request.remote_addr
209+
request.referrer
210+
request.query_string
211+
"""
212+
213+
user_agent = request.headers.get('User-Agent', '').lower()
214+
html_needed = is_html_needed(user_agent)
215+
options = parse_args(request.args)
216+
217+
if topic in ['apple-touch-icon-precomposed.png', 'apple-touch-icon.png', 'apple-touch-icon-120x120-precomposed.png'] \
218+
or (topic is not None and any(topic.endswith('/'+x) for x in ['favicon.ico'])):
219+
return ''
220+
221+
request_id = request.cookies.get('id')
222+
if topic is not None and topic.lstrip('/') == ':last':
223+
if request_id:
224+
topic = last_query(request_id)
225+
else:
226+
return "ERROR: you have to set id for your requests to use /:last\n"
227+
else:
228+
if request_id:
229+
save_query(request_id, topic)
230+
231+
if request.method == 'POST':
232+
process_post_request(request, html_needed)
233+
if html_needed:
234+
return redirect("/")
235+
return "OK\n"
236+
237+
if 'topic' in request.args:
238+
return redirect("/%s" % request.args.get('topic'))
239+
240+
if topic is None:
241+
topic = ":firstpage"
242+
243+
if topic.startswith(':shell-x/'):
244+
return _proxy()
245+
#return requests.get('http://127.0.0.1:3000'+topic[8:]).text
246+
247+
lang = get_answer_language(request)
248+
if lang:
249+
options['lang'] = lang
250+
251+
ip_address = get_request_ip(request)
252+
if '+' in topic:
253+
not_allowed = LIMITS.check_ip(ip_address)
254+
if not_allowed:
255+
return "429 %s\n" % not_allowed, 429
256+
257+
html_is_needed = is_html_needed(user_agent) and not is_result_a_script(topic)
258+
if html_is_needed:
259+
output_format='html'
260+
else:
261+
output_format='ansi'
262+
result, found = cheat_wrapper(topic, request_options=options, output_format=output_format)
263+
if 'Please come back in several hours' in result and html_is_needed:
264+
malformed_response = open(os.path.join(CONFIG["path.internal.malformed"])).read()
265+
return malformed_response
266+
267+
log_query(ip_address, found, topic, user_agent)
268+
if html_is_needed:
269+
return result
270+
return Response(result, mimetype='text/plain')

0 commit comments

Comments
 (0)