1
1
mirror of https://github.com/chubin/cheat.sh.git synced 2024-11-30 10:03:13 +03:00
cheat.sh/bin/srv.py

269 lines
8.1 KiB
Python
Raw Normal View History

#!/usr/bin/env python
# vim: set encoding=utf-8
2018-05-05 15:50:24 +03:00
"""
Main server program.
"""
from __future__ import print_function
2018-05-05 15:50:24 +03:00
from gevent.monkey import patch_all
from gevent.pywsgi import WSGIServer
patch_all()
2018-05-05 15:50:24 +03:00
# pylint: disable=wrong-import-position,wrong-import-order
import sys
import logging
import os
2018-07-03 13:32:22 +03:00
import requests
import jinja2
2018-07-03 13:32:22 +03:00
from flask import Flask, request, send_from_directory, redirect, Response
2018-07-14 00:59:01 +03:00
MYDIR = os.path.abspath(os.path.join(__file__, '..', '..'))
sys.path.append("%s/lib/" % MYDIR)
from globals import FILE_QUERIES_LOG, LOG_FILE, TEMPLATES, STATIC, MALFORMED_RESPONSE_HTML_PAGE, SERVER_ADDRESS, SERVER_PORT
2018-05-05 15:50:24 +03:00
from limits import Limits
from cheat_wrapper import cheat_wrapper
from post import process_post_request
from options import parse_args
2018-05-20 00:03:16 +03:00
from stateful_queries import save_query, last_query
2018-05-05 15:50:24 +03:00
# pylint: disable=wrong-import-position,wrong-import-order
if not os.path.exists(os.path.dirname(LOG_FILE)):
os.makedirs(os.path.dirname(LOG_FILE))
logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s %(message)s')
2018-05-05 15:50:24 +03:00
app = Flask(__name__) # pylint: disable=invalid-name
app.jinja_loader = jinja2.ChoiceLoader([
app.jinja_loader,
jinja2.FileSystemLoader(TEMPLATES),
])
2018-05-05 15:50:24 +03:00
LIMITS = Limits()
def is_html_needed(user_agent):
2018-05-05 15:50:24 +03:00
"""
Basing on `user_agent`, return whether it needs HTML or ANSI
"""
plaintext_clients = ['curl', 'wget', 'fetch', 'httpie', 'lwp-request', 'openbsd ftp', 'python-requests']
return all([x not in user_agent for x in plaintext_clients])
def is_result_a_script(query):
return query in [':cht.sh']
@app.route('/files/<path:path>')
def send_static(path):
2018-05-05 15:50:24 +03:00
"""
Return static file `path`.
Can be served by the HTTP frontend.
"""
return send_from_directory(STATIC, path)
@app.route('/favicon.ico')
def send_favicon():
2018-05-05 15:50:24 +03:00
"""
Return static file `favicon.ico`.
Can be served by the HTTP frontend.
"""
return send_from_directory(STATIC, 'favicon.ico')
@app.route('/malformed-response.html')
def send_malformed():
2018-05-05 15:50:24 +03:00
"""
Return static file `malformed-response.html`.
Can be served by the HTTP frontend.
"""
return send_from_directory(STATIC, 'malformed-response.html')
2018-05-05 15:50:24 +03:00
def log_query(ip_addr, found, topic, user_agent):
"""
Log processed query and some internal data
"""
log_entry = "%s %s %s %s" % (ip_addr, found, topic, user_agent)
with open(FILE_QUERIES_LOG, 'a') as my_file:
2017-06-02 18:47:18 +03:00
my_file.write(log_entry.encode('utf-8')+"\n")
2018-05-05 15:50:24 +03:00
def get_request_ip(req):
"""
Extract IP address from `request`
"""
if req.headers.getlist("X-Forwarded-For"):
ip_addr = req.headers.getlist("X-Forwarded-For")[0]
if ip_addr.startswith('::ffff:'):
ip_addr = ip_addr[7:]
else:
ip_addr = req.remote_addr
if req.headers.getlist("X-Forwarded-For"):
ip_addr = req.headers.getlist("X-Forwarded-For")[0]
if ip_addr.startswith('::ffff:'):
ip_addr = ip_addr[7:]
else:
ip_addr = req.remote_addr
return ip_addr
def get_answer_language(request):
"""
Return preferred answer language based on
domain name, query arguments and headers
"""
def _parse_accept_language(accept_language):
languages = accept_language.split(",")
locale_q_pairs = []
for language in languages:
try:
if language.split(";")[0] == language:
# no q => q = 1
locale_q_pairs.append((language.strip(), "1"))
else:
locale = language.split(";")[0].strip()
weight = language.split(";")[1].split("=")[1]
locale_q_pairs.append((locale, weight))
except IndexError:
pass
return locale_q_pairs
def _find_supported_language(accepted_languages):
for lang_tuple in accepted_languages:
lang = lang_tuple[0]
if '-' in lang:
lang = lang.split('-', 1)[0]
return lang
return None
lang = None
hostname = request.headers['Host']
if hostname.endswith('.cheat.sh'):
lang = hostname[:-9]
if 'lang' in request.args:
lang = request.args.get('lang')
header_accept_language = request.headers.get('Accept-Language', '')
if lang is None and header_accept_language:
lang = _find_supported_language(
_parse_accept_language(header_accept_language))
return lang
2018-07-03 13:32:22 +03:00
def _proxy(*args, **kwargs):
# print "method=", request.method,
# print "url=", request.url.replace('/:shell-x/', ':3000/')
# print "headers=", {key: value for (key, value) in request.headers if key != 'Host'}
# print "data=", request.get_data()
# print "cookies=", request.cookies
# print "allow_redirects=", False
url_before, url_after = request.url.split('/:shell-x/', 1)
url = url_before + ':3000/'
if 'q' in request.args:
url_after = '?' + "&".join("arg=%s" % x for x in request.args['q'].split())
url += url_after
print(url)
print(request.get_data())
2018-07-03 13:32:22 +03:00
resp = requests.request(
method=request.method,
url=url,
headers={key: value for (key, value) in request.headers if key != 'Host'},
data=request.get_data(),
cookies=request.cookies,
allow_redirects=False)
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
headers = [(name, value) for (name, value) in resp.raw.headers.items()
if name.lower() not in excluded_headers]
response = Response(resp.content, resp.status_code, headers)
return response
@app.route("/", methods=['GET', 'POST'])
@app.route("/<path:topic>", methods=["GET", "POST"])
2018-05-05 15:50:24 +03:00
def answer(topic=None):
"""
Main rendering function, it processes incoming weather queries.
Depending on user agent it returns output in HTML or ANSI format.
Incoming data:
request.args
request.headers
request.remote_addr
request.referrer
request.query_string
"""
user_agent = request.headers.get('User-Agent', '').lower()
html_needed = is_html_needed(user_agent)
options = parse_args(request.args)
2018-09-23 22:11:16 +03:00
if topic in ['apple-touch-icon-precomposed.png', 'apple-touch-icon.png', 'apple-touch-icon-120x120-precomposed.png'] \
or (topic is not None and any(topic.endswith('/'+x) for x in ['favicon.ico'])):
2018-07-03 13:32:22 +03:00
return ''
2018-05-20 00:03:16 +03:00
request_id = request.cookies.get('id')
if topic is not None and topic.lstrip('/') == ':last':
if request_id:
topic = last_query(request_id)
else:
return "ERROR: you have to set id for your requests to use /:last\n"
else:
if request_id:
save_query(request_id, topic)
if request.method == 'POST':
2018-05-05 15:50:24 +03:00
process_post_request(request, html_needed)
if html_needed:
return redirect("/")
2018-05-05 15:50:24 +03:00
return "OK\n"
if 'topic' in request.args:
return redirect("/%s" % request.args.get('topic'))
if topic is None:
topic = ":firstpage"
2018-07-03 13:32:22 +03:00
if topic.startswith(':shell-x/'):
return _proxy()
#return requests.get('http://127.0.0.1:3000'+topic[8:]).text
lang = get_answer_language(request)
if lang:
options['lang'] = lang
2018-05-20 00:03:16 +03:00
ip_address = get_request_ip(request)
if '+' in topic:
not_allowed = LIMITS.check_ip(ip_address)
if not_allowed:
return "429 %s\n" % not_allowed, 429
html_is_needed = is_html_needed(user_agent) and not is_result_a_script(topic)
if html_is_needed:
output_format='html'
else:
output_format='ansi'
result, found = cheat_wrapper(topic, request_options=options, output_format=output_format)
if 'Please come back in several hours' in result and html_is_needed:
return MALFORMED_RESPONSE_HTML_PAGE
2018-05-05 15:50:24 +03:00
log_query(ip_address, found, topic, user_agent)
if html_is_needed:
return result
return Response(result, mimetype='text/plain')
if 'CHEATSH_PORT' in os.environ:
SRV = WSGIServer((SERVER_ADDRESS, int(os.environ.get('CHEATSH_PORT'))), app) # log=None)
SRV.serve_forever()
else:
SRV = WSGIServer((SERVER_ADDRESS, SERVER_PORT), app) # log=None)
SRV.serve_forever()