/
server.py
171 lines (156 loc) · 7.18 KB
/
server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/usr/bin/python
# -*- coding: UTF-8 -*-
# Copyright (C) 2016 Michel Müller
# This file is part of Guetzli.
# Guetzli is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Guetzli is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with Guetzli. If not, see <http://www.gnu.org/licenses/>.
# Contributors:
# * Mark Schmitz
import json, logging, os
from flask import Flask, abort, redirect, url_for, request
from tools.guetzli import \
NotFoundError, UsageError, NotAllowedError, \
set_site, get_site, \
get_repo_path, get_page_path, get_post_path, get_content_config, \
get_context_with_rendered_content, render_with_template
_autopull_key = None
_autopull_branch = None
_untrusted_autopull = False
logging.getLogger().setLevel(logging.INFO)
if __name__ == '__main__': #Option parsing for toy server startup
import optparse
optparser = optparse.OptionParser()
optparser.add_option("--site", dest="site")
optparser.add_option("--autopull-key", dest="autopull_key")
optparser.add_option("--autopull-branch", dest="autopull_branch")
optparser.add_option("--enable-untrusted-autopull", dest="untrusted_autopull", default=False, action='store_true')
(options, args) = optparser.parse_args()
if options.site:
set_site(options.site)
if options.autopull_key:
_autopull_key = options.autopull_key.encode() if type(_autopull_key) == unicode else options.autopull_key
_autopull_branch = options.autopull_branch if options.autopull_branch else "master"
_untrusted_autopull = options.untrusted_autopull
else: #Option parsing for wsgi startup
import sys
if len(sys.argv) > 1:
set_site(sys.argv[1])
if len(sys.argv) > 2:
_autopull_key = sys.argv[2]
_autopull_branch = sys.argv[3] if len(sys.argv) > 3 else "master"
_untrusted_autopull = len(sys.argv) > 4 and sys.argv[4] == "true"
logging.info("Autopulling is set up: %s; Branch: %s; Untrusted connections allowed: %s" %(
_autopull_key != None or _untrusted_autopull,
_autopull_branch,
_untrusted_autopull
))
app = Flask(__name__, static_folder="./sites/%s/design/static" %(get_site()), static_url_path="/choco")
extensions_directory_list = os.listdir("extensions")
for file_name in extensions_directory_list:
basename_and_file_ext = os.path.splitext(file_name)
if basename_and_file_ext[1] != ".py":
continue
if basename_and_file_ext[0] == "__init__":
continue
guetzli_extension_name = basename_and_file_ext[0]
guetzli_extension_endpoint = "/%s" %(guetzli_extension_name.replace('_', '-'))
guetzli_extension_module = __import__("extensions.%s" %(guetzli_extension_name), fromlist=["extensions"])
logging.info("registering %s extension with endpoint %s" %(guetzli_extension_name, guetzli_extension_endpoint))
app.register_blueprint(getattr(guetzli_extension_module, guetzli_extension_name), url_prefix=guetzli_extension_endpoint)
@app.errorhandler(500)
def custom_error_handler(error):
if hasattr(error, "description"):
return error.description.get('message', "")
return str(error)
@app.route('/', defaults={'pagename': None, 'language': None, 'post_id': None}, methods=['GET'])
@app.route('/bisc', defaults={'pagename': None, 'language': None, 'post_id': None}, methods=['GET'])
@app.route('/bisc/<language>', defaults={'pagename': None, 'post_id': None}, methods=['GET'])
@app.route("/bisc/<language>/<pagename>", defaults={'post_id': None}, methods = ['GET'])
@app.route("/bisc/<language>/<pagename>/<post_id>", methods = ['GET'])
def page_view(pagename, language, post_id):
try:
return render_with_template(get_context_with_rendered_content(
language=language,
page_or_post_type=pagename,
post_id=post_id
))
except NotAllowedError:
abort(403)
except UsageError as e:
abort(500, {'message': str(e)})
except NotFoundError as e:
if not pagename or not language \
or (pagename == 'index') \
or (post_id == None and str(e) != get_page_path(pagename, language)) \
or (post_id and str(e) != get_post_path(pagename, language, post_id)):
abort(500, {'message': 'sorry, %s could not be found' %(str(e))})
elif pagename == get_content_config().get('default_pagename', 'index'):
abort(404)
else:
return redirect(url_for('page_view', language=language))
raise Exception("Well that's embarassing - this should never happen.")
@app.route("/autopull", methods=['GET', 'POST'])
def autopull_view():
#adapted from https://github.com/razius/github-webhook-handler
import ipaddress, requests
if not _untrusted_autopull and not _autopull_key:
logging.warning("someone tried request autopull, but there is no autopull secret configured for this guetzli instance - aborting")
abort(500)
if not _untrusted_autopull:
request_ip = ipaddress.ip_address(u'{0}'.format(request.remote_addr))
hook_blocks = requests.get('https://api.github.com/meta').json()['hooks']
for block in hook_blocks:
if ipaddress.ip_address(request_ip) in ipaddress.ip_network(block):
break
else:
logging.info("someone tried request autopull from unsupported IP %s" %(request_ip))
abort(403)
if request.headers.get('X-GitHub-Event') == "ping":
logging.info("Received and answered Ping event from github")
return json.dumps({'msg': 'Hi!'})
if request.headers.get('X-GitHub-Event') != "push" \
and request.headers.get('X-Gitlab-Event') != "Push Hook":
logging.warning("We have received a webhook, but it is the wrong event type - aborting")
abort(403)
if request.method == 'POST':
if not _untrusted_autopull and _autopull_key: # Check if POST request signature is valid
import hmac, hashlib
signature = request.headers.get('X-Hub-Signature').split('=')[1]
mac = hmac.new(_autopull_key, msg=request.data, digestmod=hashlib.sha1)
try:
if not hmac.compare_digest(mac.hexdigest(), signature):
logging.info("hash did not match for autopull" %(request_ip))
abort(403)
except AttributeError:
# What compare_digest provides is protection against timing attacks,
# fallback to string comparison for python < 2.7.7
logging.warning("webhooks are used without timing attack protection available from python 2.7.7 - please update if possible")
if not str(mac.hexdigest()) == str(signature):
abort(403)
import subprocess
logging.info("pulling latest repo version upon request by github webhook")
branch = _autopull_branch if _autopull_branch else "master"
subp = subprocess.Popen(
["git", "checkout", branch],
cwd=get_repo_path()
)
subp.wait()
subp = subprocess.Popen(
["git", "pull", "origin", branch],
cwd=get_repo_path()
)
subp.wait()
return 'OK'
if __name__ == "__main__":
logging.info("setup completed as main application for site %s; running on port 5000 accessible from localhost" %(get_site()))
app.run(debug=True)
else:
logging.info("setup completed as embedded application for site %s, passed arguments %s; running on your chosen port accessible from everywhere" %(get_site(), sys.argv))