OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 | 2 |
3 from ConfigParser import SafeConfigParser | 3 from ConfigParser import SafeConfigParser |
4 import hashlib | 4 import hashlib |
5 import hmac | 5 import hmac |
6 import json | 6 import json |
7 import os | 7 import os |
8 import re | 8 import re |
9 import sys | 9 import sys |
10 import urllib | 10 import urllib |
11 | 11 |
12 OAUTH2_AUTHURL = 'https://accounts.google.com/o/oauth2/auth' | 12 OAUTH2_AUTHURL = 'https://accounts.google.com/o/oauth2/auth' |
13 OAUTH2_TOKENURL = 'https://accounts.google.com/o/oauth2/token' | 13 OAUTH2_TOKENURL = 'https://accounts.google.com/o/oauth2/token' |
14 OAUTH2_DATAURL = 'https://www.googleapis.com/plus/v1/people/me' | 14 OAUTH2_DATAURL = 'https://www.googleapis.com/plus/v1/people/me' |
15 OAUTH2_SCOPE = 'email' | 15 OAUTH2_SCOPE = 'email' |
16 | 16 |
17 OAUTH2_TOKEN_EXPIRATION = 5 * 60 | 17 OAUTH2_TOKEN_EXPIRATION = 5 * 60 |
18 | 18 |
19 | |
20 def setup_paths(engine_dir): | 19 def setup_paths(engine_dir): |
21 sys.path.append(engine_dir) | 20 sys.path.append(engine_dir) |
22 | 21 |
23 import wrapper_util | 22 import wrapper_util |
24 paths = wrapper_util.Paths(engine_dir) | 23 paths = wrapper_util.Paths(engine_dir) |
25 script_name = os.path.basename(__file__) | 24 script_name = os.path.basename(__file__) |
26 sys.path[0:0] = paths.script_paths(script_name) | 25 sys.path[0:0] = paths.script_paths(script_name) |
27 return script_name, paths.script_file(script_name) | 26 return script_name, paths.script_file(script_name) |
28 | |
29 | 27 |
30 def adjust_server_id(): | 28 def adjust_server_id(): |
31 from google.appengine.tools.devappserver2 import http_runtime_constants | 29 from google.appengine.tools.devappserver2 import http_runtime_constants |
32 http_runtime_constants.SERVER_SOFTWARE = 'Production/2.0' | 30 http_runtime_constants.SERVER_SOFTWARE = 'Production/2.0' |
33 | |
34 | 31 |
35 def fix_request_scheme(): | 32 def fix_request_scheme(): |
36 from google.appengine.runtime.wsgi import WsgiRequest | 33 from google.appengine.runtime.wsgi import WsgiRequest |
37 orig_init = WsgiRequest.__init__ | 34 orig_init = WsgiRequest.__init__ |
38 | 35 def __init__(self, *args): |
39 def __init__(self, *args): | 36 orig_init(self, *args) |
40 orig_init(self, *args) | 37 self._environ['wsgi.url_scheme'] = self._environ.get('HTTP_X_FORWARDED_PROTO
', 'http') |
41 self._environ['wsgi.url_scheme'] = self._environ.get('HTTP_X_FORWARDED_P
ROTO', 'http') | 38 self._environ['HTTPS'] = 'on' if self._environ['wsgi.url_scheme'] == 'https'
else 'off' |
42 self._environ['HTTPS'] = 'on' if self._environ['wsgi.url_scheme'] == 'ht
tps' else 'off' | 39 WsgiRequest.__init__ = __init__ |
43 WsgiRequest.__init__ = __init__ | |
44 | |
45 | 40 |
46 def read_config(path): | 41 def read_config(path): |
47 config = SafeConfigParser() | 42 config = SafeConfigParser() |
48 config.read(path) | 43 config.read(path) |
49 return config | 44 return config |
50 | |
51 | 45 |
52 def set_storage_path(storage_path): | 46 def set_storage_path(storage_path): |
53 sys.argv.extend(['--storage_path', storage_path]) | 47 sys.argv.extend(['--storage_path', storage_path]) |
54 | |
55 | 48 |
56 def replace_runtime(): | 49 def replace_runtime(): |
57 from google.appengine.tools.devappserver2 import python_runtime | 50 from google.appengine.tools.devappserver2 import python_runtime |
58 runtime_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '_py
thon_runtime.py') | 51 runtime_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '_pyth
on_runtime.py') |
59 python_runtime._RUNTIME_PATH = runtime_path | 52 python_runtime._RUNTIME_PATH = runtime_path |
60 python_runtime._RUNTIME_ARGS = [sys.executable, runtime_path] | 53 python_runtime._RUNTIME_ARGS = [sys.executable, runtime_path] |
61 | |
62 | 54 |
63 def protect_cookies(cookie_secret): | 55 def protect_cookies(cookie_secret): |
64 from google.appengine.tools.devappserver2 import login | 56 from google.appengine.tools.devappserver2 import login |
65 | 57 |
66 def calculate_signature(message): | 58 def calculate_signature(message): |
67 return hmac.new(cookie_secret, message, hashlib.sha256).hexdigest() | 59 return hmac.new(cookie_secret, message, hashlib.sha256).hexdigest() |
68 | 60 |
69 def _get_user_info_from_dict(cookie_dict, cookie_name=login._COOKIE_NAME): | 61 def _get_user_info_from_dict(cookie_dict, cookie_name=login._COOKIE_NAME): |
70 cookie_value = cookie_dict.get(cookie_name, '') | 62 cookie_value = cookie_dict.get(cookie_name, '') |
71 | 63 |
72 email, admin, user_id, signature = (cookie_value.split(':') + ['', '', '
', ''])[:4] | 64 email, admin, user_id, signature = (cookie_value.split(':') + ['', '', '', '
'])[:4] |
73 if '@' not in email or signature != calculate_signature(':'.join([email,
admin, user_id])): | 65 if '@' not in email or signature != calculate_signature(':'.join([email, adm
in, user_id])): |
74 return '', False, '' | 66 return '', False, '' |
75 return email, (admin == 'True'), user_id | 67 return email, (admin == 'True'), user_id |
76 login._get_user_info_from_dict = _get_user_info_from_dict | 68 login._get_user_info_from_dict = _get_user_info_from_dict |
77 | 69 |
78 orig_create_cookie_data = login._create_cookie_data | 70 orig_create_cookie_data = login._create_cookie_data |
79 | 71 def _create_cookie_data(email, admin): |
80 def _create_cookie_data(email, admin): | 72 result = orig_create_cookie_data(email, admin) |
81 result = orig_create_cookie_data(email, admin) | 73 result += ':' + calculate_signature(result) |
82 result += ':' + calculate_signature(result) | 74 return result |
83 return result | 75 login._create_cookie_data = _create_cookie_data |
84 login._create_cookie_data = _create_cookie_data | |
85 | |
86 | 76 |
87 def enable_oauth2(client_id, client_secret, admins): | 77 def enable_oauth2(client_id, client_secret, admins): |
88 from google.appengine.tools.devappserver2 import login | 78 from google.appengine.tools.devappserver2 import login |
89 | 79 |
90 def request(method, url, data): | 80 def request(method, url, data): |
91 if method != 'POST': | 81 if method != 'POST': |
92 url += '?' + urllib.urlencode(data) | 82 url += '?' + urllib.urlencode(data) |
93 data = None | 83 data = None |
94 else: | 84 else: |
95 data = urllib.urlencode(data) | 85 data = urllib.urlencode(data) |
96 response = urllib.urlopen(url, data) | 86 response = urllib.urlopen(url, data) |
97 try: | 87 try: |
98 return json.loads(response.read()) | 88 return json.loads(response.read()) |
99 finally: | 89 finally: |
100 response.close() | 90 response.close() |
101 | 91 |
102 token_cache = {} | 92 token_cache = {} |
103 | 93 def get_user_info(access_token): |
104 def get_user_info(access_token): | 94 email, is_admin, expiration = token_cache.get(access_token, (None, None, 0)) |
105 email, is_admin, expiration = token_cache.get(access_token, (None, None,
0)) | 95 now = time.mktime(time.gmtime()) |
106 now = time.mktime(time.gmtime()) | 96 if now > expiration: |
| 97 get_params = { |
| 98 'access_token': access_token, |
| 99 } |
| 100 data = request('GET', OAUTH2_DATAURL, get_params) |
| 101 emails = [e for e in data.get('emails') if e['type'] == 'account'] |
| 102 if not emails: |
| 103 return None, None |
| 104 |
| 105 email = emails[0]['value'] |
| 106 is_admin = email in admins |
| 107 |
| 108 for token, (_, _, expiration) in token_cache.items(): |
107 if now > expiration: | 109 if now > expiration: |
108 get_params = { | 110 del token_cache[token] |
109 'access_token': access_token, | 111 token_cache[access_token] = (email, is_admin, now + OAUTH2_TOKEN_EXPIRATIO
N) |
110 } | 112 return email, is_admin |
111 data = request('GET', OAUTH2_DATAURL, get_params) | 113 |
112 emails = [e for e in data.get('emails') if e['type'] == 'account'] | 114 def get(self): |
113 if not emails: | 115 def error(text): |
114 return None, None | 116 self.response.status = 200 |
115 | 117 self.response.headers['Content-Type'] = 'text/plain' |
116 email = emails[0]['value'] | 118 self.response.write(text.encode('utf-8')) |
117 is_admin = email in admins | 119 |
118 | 120 def redirect(url): |
119 for token, (_, _, expiration) in token_cache.items(): | 121 self.response.status = 302 |
120 if now > expiration: | 122 self.response.status_message = 'Found' |
121 del token_cache[token] | 123 self.response.headers['Location'] = url.encode('utf-8') |
122 token_cache[access_token] = (email, is_admin, now + OAUTH2_TOKEN_EXP
IRATION) | 124 |
123 return email, is_admin | 125 def logout(continue_url): |
124 | 126 self.response.headers['Set-Cookie'] = login._clear_user_info_cookie() |
125 def get(self): | 127 redirect(continue_url) |
126 def error(text): | 128 |
127 self.response.status = 200 | 129 def login_step1(continue_url): |
128 self.response.headers['Content-Type'] = 'text/plain' | 130 # See https://stackoverflow.com/questions/10271110/python-oauth2-login-wit
h-google |
129 self.response.write(text.encode('utf-8')) | 131 authorize_params = { |
130 | 132 'response_type': 'code', |
131 def redirect(url): | 133 'client_id': client_id, |
132 self.response.status = 302 | 134 'redirect_uri': base_url + login.LOGIN_URL_RELATIVE, |
133 self.response.status_message = 'Found' | 135 'scope': OAUTH2_SCOPE, |
134 self.response.headers['Location'] = url.encode('utf-8') | 136 'state': continue_url, |
135 | 137 } |
136 def logout(continue_url): | 138 redirect(OAUTH2_AUTHURL + '?' + urllib.urlencode(authorize_params)) |
137 self.response.headers['Set-Cookie'] = login._clear_user_info_cookie(
) | 139 |
138 redirect(continue_url) | 140 def login_step2(code, continue_url): |
139 | 141 token_params = { |
140 def login_step1(continue_url): | 142 'code': code, |
141 # See https://stackoverflow.com/questions/10271110/python-oauth2-log
in-with-google | 143 'client_id': client_id, |
142 authorize_params = { | 144 'client_secret': client_secret, |
143 'response_type': 'code', | 145 'redirect_uri': base_url + login.LOGIN_URL_RELATIVE, |
144 'client_id': client_id, | 146 'grant_type':'authorization_code', |
145 'redirect_uri': base_url + login.LOGIN_URL_RELATIVE, | 147 } |
146 'scope': OAUTH2_SCOPE, | 148 data = request('POST', OAUTH2_TOKENURL, token_params) |
147 'state': continue_url, | 149 token = data.get('access_token') |
148 } | 150 if not token: |
149 redirect(OAUTH2_AUTHURL + '?' + urllib.urlencode(authorize_params)) | 151 error('No token in response: ' + str(data)) |
150 | 152 return |
151 def login_step2(code, continue_url): | 153 |
152 token_params = { | 154 email, is_admin = get_user_info(token) |
153 'code': code, | 155 if not email: |
154 'client_id': client_id, | 156 error('No email address in response: ' + str(data)) |
155 'client_secret': client_secret, | 157 return |
156 'redirect_uri': base_url + login.LOGIN_URL_RELATIVE, | 158 self.response.headers['Set-Cookie'] = login._set_user_info_cookie(email, i
s_admin) |
157 'grant_type': 'authorization_code', | 159 redirect(continue_url) |
158 } | 160 |
159 data = request('POST', OAUTH2_TOKENURL, token_params) | 161 action = self.request.get(login.ACTION_PARAM) |
160 token = data.get('access_token') | 162 continue_url = self.request.get(login.CONTINUE_PARAM) |
161 if not token: | 163 continue_url = re.sub(r'^http:', 'https:', continue_url) |
162 error('No token in response: ' + str(data)) | 164 base_url = 'https://%s/' % self.request.environ['HTTP_HOST'] |
163 return | 165 |
164 | 166 if action.lower() == login.LOGOUT_ACTION.lower(): |
165 email, is_admin = get_user_info(token) | 167 logout(continue_url or base_url) |
166 if not email: | 168 elif self.request.get('error'): |
167 error('No email address in response: ' + str(data)) | 169 error('Authorization failed: ' + self.request.get('error')) |
168 return | 170 else: |
169 self.response.headers['Set-Cookie'] = login._set_user_info_cookie(em
ail, is_admin) | 171 code = self.request.get('code') |
170 redirect(continue_url) | 172 if code: |
171 | 173 login_step2(code, self.request.get('state') or base_url) |
172 action = self.request.get(login.ACTION_PARAM) | 174 else: |
173 continue_url = self.request.get(login.CONTINUE_PARAM) | 175 login_step1(continue_url or base_url) |
174 continue_url = re.sub(r'^http:', 'https:', continue_url) | 176 |
175 base_url = 'https://%s/' % self.request.environ['HTTP_HOST'] | 177 login.Handler.get = get |
176 | 178 |
177 if action.lower() == login.LOGOUT_ACTION.lower(): | 179 from google.appengine.api import user_service_stub, user_service_pb |
178 logout(continue_url or base_url) | 180 from google.appengine.runtime import apiproxy_errors |
179 elif self.request.get('error'): | 181 def _Dynamic_GetOAuthUser(self, request, response, request_id): |
180 error('Authorization failed: ' + self.request.get('error')) | 182 environ = self.request_data.get_request_environ(request_id) |
181 else: | 183 match = re.search(r'^OAuth (\S+)', environ.get('HTTP_AUTHORIZATION', '')) |
182 code = self.request.get('code') | 184 if not match: |
183 if code: | 185 raise apiproxy_errors.ApplicationError( |
184 login_step2(code, self.request.get('state') or base_url) | 186 user_service_pb.UserServiceError.OAUTH_INVALID_REQUEST) |
185 else: | 187 |
186 login_step1(continue_url or base_url) | 188 email, is_admin = get_user_info(match.group(1)) |
187 | 189 if not email: |
188 login.Handler.get = get | 190 raise apiproxy_errors.ApplicationError( |
189 | 191 user_service_pb.UserServiceError.OAUTH_INVALID_TOKEN) |
190 from google.appengine.api import user_service_stub, user_service_pb | 192 |
191 from google.appengine.runtime import apiproxy_errors | 193 # User ID is based on email address, see appengine.tools.devappserver2.login |
192 | 194 user_id_digest = hashlib.md5(email.lower()).digest() |
193 def _Dynamic_GetOAuthUser(self, request, response, request_id): | 195 user_id = '1' + ''.join(['%02d' % ord(x) for x in user_id_digest])[:20] |
194 environ = self.request_data.get_request_environ(request_id) | 196 |
195 match = re.search(r'^OAuth (\S+)', environ.get('HTTP_AUTHORIZATION', '')
) | 197 response.set_email(email) |
196 if not match: | 198 response.set_user_id(user_id) |
197 raise apiproxy_errors.ApplicationError( | 199 response.set_auth_domain(user_service_stub._DEFAULT_AUTH_DOMAIN) |
198 user_service_pb.UserServiceError.OAUTH_INVALID_REQUEST) | 200 response.set_is_admin(is_admin) |
199 | 201 response.set_client_id(client_id) |
200 email, is_admin = get_user_info(match.group(1)) | 202 response.add_scopes(OAUTH2_SCOPE) |
201 if not email: | 203 |
202 raise apiproxy_errors.ApplicationError( | 204 user_service_stub.UserServiceStub._Dynamic_GetOAuthUser = _Dynamic_GetOAuthUse
r |
203 user_service_pb.UserServiceError.OAUTH_INVALID_TOKEN) | |
204 | |
205 # User ID is based on email address, see appengine.tools.devappserver2.l
ogin | |
206 user_id_digest = hashlib.md5(email.lower()).digest() | |
207 user_id = '1' + ''.join(['%02d' % ord(x) for x in user_id_digest])[:20] | |
208 | |
209 response.set_email(email) | |
210 response.set_user_id(user_id) | |
211 response.set_auth_domain(user_service_stub._DEFAULT_AUTH_DOMAIN) | |
212 response.set_is_admin(is_admin) | |
213 response.set_client_id(client_id) | |
214 response.add_scopes(OAUTH2_SCOPE) | |
215 | |
216 user_service_stub.UserServiceStub._Dynamic_GetOAuthUser = _Dynamic_GetOAuthU
ser | |
217 | |
218 | 205 |
219 def fix_target_resolution(): | 206 def fix_target_resolution(): |
220 """ | 207 """ |
221 By default, the dispatcher assumes port 80 for target authorities that | 208 By default, the dispatcher assumes port 80 for target authorities that |
222 only contain a hostname but no port part. This hard-coded behavior is | 209 only contain a hostname but no port part. This hard-coded behavior is |
223 altered in function fix_target_resolution() so that the port given | 210 altered in function fix_target_resolution() so that the port given |
224 as --port option to the appserver-script is used instead. Without this | 211 as --port option to the appserver-script is used instead. Without this |
225 monkey-patch, dispatching tasks from an application run behind a HTTP | 212 monkey-patch, dispatching tasks from an application run behind a HTTP |
226 proxy server on port 80 (or HTTPS on 443) will fail, because | 213 proxy server on port 80 (or HTTPS on 443) will fail, because |
227 applications will omit the default port when addressing resources. | 214 applications will omit the default port when addressing resources. |
228 """ | 215 """ |
229 from google.appengine.tools.devappserver2.dispatcher import Dispatcher | 216 from google.appengine.tools.devappserver2.dispatcher import Dispatcher |
230 orig_resolve_target = Dispatcher._resolve_target | 217 orig_resolve_target = Dispatcher._resolve_target |
231 | 218 |
232 def resolve_target(dispatcher, hostname, path): | 219 def resolve_target(dispatcher, hostname, path): |
233 new_hostname = hostname if ":" in hostname else "%s:%d" % (hostname, dis
patcher._port) | 220 new_hostname = hostname if ":" in hostname else "%s:%d" % (hostname, dispatc
her._port) |
234 return orig_resolve_target(dispatcher, new_hostname, path) | 221 return orig_resolve_target(dispatcher, new_hostname, path) |
235 | 222 |
236 Dispatcher._resolve_target = resolve_target | 223 Dispatcher._resolve_target = resolve_target |
237 | 224 |
238 if __name__ == '__main__': | 225 if __name__ == '__main__': |
239 engine_dir = '/opt/google_appengine' | 226 engine_dir = '/opt/google_appengine' |
240 storage_path = '/var/lib/rietveld' | 227 storage_path = '/var/lib/rietveld' |
241 | 228 |
242 script_name, script_file = setup_paths(engine_dir) | 229 script_name, script_file = setup_paths(engine_dir) |
243 adjust_server_id() | 230 adjust_server_id() |
244 fix_request_scheme() | 231 fix_request_scheme() |
245 | 232 |
246 if script_name == 'dev_appserver.py': | 233 if script_name == 'dev_appserver.py': |
247 config = read_config(os.path.join(storage_path, 'config.ini')) | 234 config = read_config(os.path.join(storage_path, 'config.ini')) |
248 | 235 |
249 set_storage_path(storage_path) | 236 set_storage_path(storage_path) |
250 replace_runtime() | 237 replace_runtime() |
251 protect_cookies(config.get('main', 'cookie_secret')) | 238 protect_cookies(config.get('main', 'cookie_secret')) |
252 enable_oauth2( | 239 enable_oauth2( |
253 config.get('oauth2', 'client_id'), | 240 config.get('oauth2', 'client_id'), |
254 config.get('oauth2', 'client_secret'), | 241 config.get('oauth2', 'client_secret'), |
255 config.get('main', 'admins').split() | 242 config.get('main', 'admins').split() |
256 ) | 243 ) |
257 fix_target_resolution() | 244 fix_target_resolution() |
258 | 245 |
259 execfile(script_file) | 246 execfile(script_file) |
OLD | NEW |