| 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 |