def atom(gplus_id, page_id=None): """Return an Atom-format feed for the given G+ id, possibly from cache.""" if len(gplus_id) != 21: return 'Invalid G+ user ID (must be exactly 21 digits).', 404 # Not Found if page_id and len(page_id) != 21: return 'Invalid G+ page ID (must be exactly 21 digits).', 404 # Not Found cache_key = ATOM_CACHE_KEY_TEMPLATE % gplus_id if page_id: cache_key = '%s-%s' % (cache_key, page_id) response = Cache.get(cache_key) # A frozen Response object if response is None: try: response = generate_atom(gplus_id, page_id) except oauth2.UnavailableException as e: app.logger.warning("Feed request failed - %r", e) flask.abort(e.status) response.add_etag() response.freeze() Cache.set(cache_key, response, time=Config.getint('cache', 'stream-expire')) return response.make_conditional(flask.request)
def atom(gplus_id, page_id=None): """Return an Atom-format feed for the given G+ id, possibly from cache.""" if len(gplus_id) != 21: return 'Invalid G+ user ID (must be exactly 21 digits).', 404 # Not Found if page_id and len(page_id) != 21: return 'Invalid G+ page ID (must be exactly 21 digits).', 404 # Not Found # Google+ is no longer publicly available for consumers. return 'Google+ was sunset for consumer users in April 2019. This feed is no longer available.', 410 # Gone ##### CODE BELOW FOR HISTORICAL PURPOSES ONLY ##### cache_key = ATOM_CACHE_KEY_TEMPLATE % gplus_id if page_id: cache_key = '%s-%s' % (cache_key, page_id) response = Cache.get(cache_key) # A frozen Response object if response is None: try: response = generate_atom(gplus_id, page_id) except oauth2.UnavailableException as e: app.logger.info("Feed request failed - %r", e) flask.abort(e.status) response.add_etag() response.freeze() Cache.set(cache_key, response, time=Config.getint('cache', 'stream-expire')) return response.make_conditional(flask.request)
def get_access_token_for_id(gplus_id): """Get an access token for an id, potentially via refresh token if necessary.""" # Check the cache first. token = Cache.get(ACCESS_TOKEN_CACHE_KEY_TEMPLATE % gplus_id) if token: return token # If we don't have a cached token, see if we have a refresh token available. refresh_token = TokenIdMapping.lookup_refresh_token(gplus_id) if not refresh_token: raise UnavailableException( 'No tokens available for G+ id %s.' % gplus_id, 401) data = { 'client_id': Config.get('oauth', 'client-id'), 'client_secret': Config.get('oauth', 'client-secret'), 'refresh_token': refresh_token, 'grant_type': 'refresh_token', } try: response = session.post(OAUTH2_BASE + '/token', data=data, timeout=GOOGLE_API_TIMEOUT) result = response.json() except requests.exceptions.Timeout: raise UnavailableException('Access token API request timed out.', 504) except Exception as e: raise UnavailableException( 'Access token API request raised exception "%r".' % e, 502) if 'invalid_grant' in result: # The provided refresh token is invalid which means the user has revoked # access to their content - thus, pluss should forget about them. Cache.delete(ACCESS_TOKEN_CACHE_KEY_TEMPLATE % gplus_id) Cache.delete(PROFILE_CACHE_KEY_TEMPLATE % gplus_id) TokenIdMapping.remove_id(gplus_id) raise UnvailableException('Access revoked for G+ id %s.' % gplus_id) elif response.status_code != 200: app.logger.error( 'Non-200 response to access token refresh request (%s): "%r".', response.status_code, result) raise UnavailableException( 'Failed to refresh access token for G+ id %s.' % gplus_id, 502) elif result.get('token_type') != 'Bearer': app.logger.error('Unknown token type "%s" refreshed for G+ id %s.', result.get('token_type'), gplus_id) raise UnavailableException( 'Failed to refresh access token for G+ id %s.' % gplus_id, 502) token = result['access_token'] Cache.set(ACCESS_TOKEN_CACHE_KEY_TEMPLATE % gplus_id, token, time=result['expires_in']) return token
def get_person_by_access_token(token): """Fetch details about an individual from the G+ API and return a dict with the response.""" headers = { 'Authorization': 'Bearer %s' % token, } try: response = session.get(GPLUS_API_ME_ENDPOINT, headers=headers, timeout=GOOGLE_API_TIMEOUT) person = response.json() except requests.exceptions.Timeout: raise UnavailableException('Person API request timed out.', 504) except Exception as e: raise UnavailableException('Person API request raised exception "%r" for %s.' % (e, pprint.pformat(response).text), 502) Cache.set(PROFILE_CACHE_KEY_TEMPLATE % person['id'], person, time=Config.getint('cache', 'profile-expire')) return person
def get_access_token_for_id(gplus_id): """Get an access token for an id, potentially via refresh token if necessary.""" # Check the cache first. token = Cache.get(ACCESS_TOKEN_CACHE_KEY_TEMPLATE % gplus_id) if token: return token # If we don't have a cached token, see if we have a refresh token available. refresh_token = TokenIdMapping.lookup_refresh_token(gplus_id) if not refresh_token: raise UnavailableException('No tokens available for G+ id %s.' % gplus_id, 401) data = { 'client_id': Config.get('oauth', 'client-id'), 'client_secret': Config.get('oauth', 'client-secret'), 'refresh_token': refresh_token, 'grant_type': 'refresh_token', } try: response = session.post(OAUTH2_BASE + '/token', data=data, timeout=GOOGLE_API_TIMEOUT) result = response.json() except requests.exceptions.Timeout: raise UnavailableException('Access token API request timed out.', 504) except Exception as e: raise UnavailableException('Access token API request raised exception "%r".' % e, 502) if 'invalid_grant' in result or ('error' in result and result['error'] == 'invalid_grant'): # The provided refresh token is invalid which means the user has revoked # access to their content - thus, pluss should forget about them. Cache.delete(ACCESS_TOKEN_CACHE_KEY_TEMPLATE % gplus_id) Cache.delete(PROFILE_CACHE_KEY_TEMPLATE % gplus_id) TokenIdMapping.remove_id(gplus_id) raise UnavailableException('Access revoked for G+ id %s.' % gplus_id, 502) elif response.status_code != 200: app.logger.error('Non-200 response to access token refresh request (%s): "%r".', response.status_code, result) raise UnavailableException('Failed to refresh access token for G+ id %s.' % gplus_id, 502) elif result.get('token_type') != 'Bearer': app.logger.error('Unknown token type "%s" refreshed for G+ id %s.', result.get('token_type'), gplus_id) raise UnavailableException('Failed to refresh access token for G+ id %s.' % gplus_id, 502) token = result['access_token'] Cache.set(ACCESS_TOKEN_CACHE_KEY_TEMPLATE % gplus_id, token, time=result['expires_in']) return token
def get_person_by_access_token(token): """Fetch details about an individual from the G+ API and return a dict with the response.""" headers = { 'Authorization': 'Bearer %s' % token, } try: response = session.get(GPLUS_API_ME_ENDPOINT, headers=headers, timeout=GOOGLE_API_TIMEOUT) person = response.json() except requests.exceptions.Timeout: raise UnavailableException('Person API request timed out.', 504) except Exception as e: raise UnavailableException( 'Person API request raised exception "%r" for %s.' % (e, pprint.pformat(response).text), 502) Cache.set(PROFILE_CACHE_KEY_TEMPLATE % person['id'], person, time=Config.getint('cache', 'profile-expire')) return person
def wrapper(*args, **kwargs): ratelimit_key = RATE_LIMIT_CACHE_KEY_TEMPLATE % flask.request.remote_addr # Increment the existing minute's counter, or start a new one if none exists # (relies on the short-circuiting of 'or') remote_ip_rate = Cache.incr(ratelimit_key) or Cache.set(ratelimit_key, 1, time=60) if remote_ip_rate > 60: if remote_ip_rate in (61, 100, 1000, 10000): app.logging.info('Rate limited %s - %d requests/min.', flask.request.remote_addr, remote_ip_rate) message = 'Rate limit exceeded. Please do not make more than 60 requests per minute.' return message, 503, {'Retry-After': 60} # Service Unavailable return func(*args, **kwargs)
def wrapper(*args, **kwargs): ratelimit_key = RATE_LIMIT_CACHE_KEY_TEMPLATE % flask.request.remote_addr # Increment the existing minute's counter, or start a new one if none exists # (relies on the short-circuiting of 'or') remote_ip_rate = Cache.incr(ratelimit_key) or Cache.set( ratelimit_key, 1, time=60) if remote_ip_rate > 60: if remote_ip_rate in (61, 100, 1000, 10000): app.logging.info('Rate limited %s - %d requests/min.', flask.request.remote_addr, remote_ip_rate) message = 'Rate limit exceeded. Please do not make more than 60 requests per minute.' return message, 503, {'Retry-After': 60} # Service Unavailable return func(*args, **kwargs)
def oauth2(): """Google redirects the user back to this endpoint to continue the OAuth2 flow.""" # Check for errors from the OAuth2 process err = flask.request.args.get('error') if err == 'access_denied': return flask.redirect(flask.url_for('denied')) elif err is not None: app.logger.warning("OAuth2 callback received error: %s", err) # TODO: handle this better (flash message?) message = 'Whoops, something went wrong (error=%s). Please try again later.' return message % flask.escape(err), 500 # Okay, no errors, so we should have a valid authorization code. # Time to go get our server-side tokens for this user from Google. auth_code = flask.request.args['code'] if auth_code is None: return 'Authorization code is missing.', 400 # Bad Request data = { 'code': auth_code, 'client_id': Config.get('oauth', 'client-id'), 'client_secret': Config.get('oauth', 'client-secret'), 'redirect_uri': full_url_for('oauth2'), 'grant_type': 'authorization_code', } try: response = session.post(OAUTH2_BASE + '/token', data, timeout=GOOGLE_API_TIMEOUT) except requests.exceptions.Timeout: app.logger.error('OAuth2 token request timed out.') # TODO: handle this better (flash message?) message = 'Whoops, Google took too long to respond. Please try again later.' return message, 504 # Gateway Timeout if response.status_code != 200: app.logger.error( 'OAuth2 token request got HTTP response %s for code "%s".', response.status_code, auth_code) # TODO: handle this better (flash message?) message = ( 'Whoops, we failed to finish processing your authorization with Google.' ' Please try again later.') return message, 401 # Unauthorized try: result = response.json() except ValueError: app.logger.error( 'OAuth2 token request got non-JSON response for code "%s".', auth_code) # TODO: handle this better (flash message?) message = ( 'Whoops, we got an invalid response from Google for your authorization.' ' Please try again later.') return message, 502 # Bad Gateway # Sanity check: we always expect Bearer tokens. if result.get('token_type') != 'Bearer': app.logger.error( 'OAuth2 token request got unknown token type "%s" for code "%s".', result['token_type'], auth_code) # TODO: handle this better (flash message?) message = ( 'Whoops, we got an invalid response from Google for your authorization.' ' Please try again later.') return message, 502 # Bad Gateway # All non-error responses should have an access token. access_token = result['access_token'] refresh_token = result.get('refresh_token') # This is in seconds, but we convert it to an absolute timestamp so that we can # account for the potential delay it takes to look up the G+ id we should associate # the access tokens with. (Could be up to GOOGLE_API_TIMEOUT seconds later.) expiry = datetime.datetime.today() + datetime.timedelta( seconds=result['expires_in']) try: person = get_person_by_access_token(access_token) except UnavailableException as e: app.logger.error('Unable to finish OAuth2 flow: %r.' % e) message = ( 'Whoops, we got an invalid response from Google for your authorization.' ' Please try again later.') return message, 502 # Bad Gateway if refresh_token is not None: TokenIdMapping.update_refresh_token(person['id'], refresh_token) # Convert the absolute expiry timestamp back into a duration in seconds expires_in = int((expiry - datetime.datetime.today()).total_seconds()) Cache.set(ACCESS_TOKEN_CACHE_KEY_TEMPLATE % person['id'], access_token, time=expires_in) # Whew, all done! Set a cookie with the user's G+ id and send them back to the homepage. app.logger.info("Successfully authenticated G+ id %s.", person['id']) response = flask.make_response(flask.redirect(flask.url_for('main'))) response.set_cookie('gplus_id', person['id']) return response
def oauth2(): """Google redirects the user back to this endpoint to continue the OAuth2 flow.""" # Check for errors from the OAuth2 process err = flask.request.args.get('error') if err == 'access_denied': return flask.redirect(flask.url_for('denied')) elif err is not None: app.logger.warning("OAuth2 callback received error: %s", err) # TODO: handle this better (flash message?) message = 'Whoops, something went wrong (error=%s). Please try again later.' return message % flask.escape(err), 500 # Okay, no errors, so we should have a valid authorization code. # Time to go get our server-side tokens for this user from Google. auth_code = flask.request.args['code'] if auth_code is None: return 'Authorization code is missing.', 400 # Bad Request data = { 'code': auth_code, 'client_id': Config.get('oauth', 'client-id'), 'client_secret': Config.get('oauth', 'client-secret'), 'redirect_uri': full_url_for('oauth2'), 'grant_type': 'authorization_code', } try: response = session.post(OAUTH2_BASE + '/token', data, timeout=GOOGLE_API_TIMEOUT) except requests.exceptions.Timeout: app.logger.error('OAuth2 token request timed out.') # TODO: handle this better (flash message?) message = 'Whoops, Google took too long to respond. Please try again later.' return message, 504 # Gateway Timeout if response.status_code != 200: app.logger.error('OAuth2 token request got HTTP response %s for code "%s".', response.status_code, auth_code) # TODO: handle this better (flash message?) message = ('Whoops, we failed to finish processing your authorization with Google.' ' Please try again later.') return message, 401 # Unauthorized try: result = response.json() except ValueError: app.logger.error('OAuth2 token request got non-JSON response for code "%s".', auth_code) # TODO: handle this better (flash message?) message = ('Whoops, we got an invalid response from Google for your authorization.' ' Please try again later.') return message, 502 # Bad Gateway # Sanity check: we always expect Bearer tokens. if result.get('token_type') != 'Bearer': app.logger.error('OAuth2 token request got unknown token type "%s" for code "%s".', result['token_type'], auth_code) # TODO: handle this better (flash message?) message = ('Whoops, we got an invalid response from Google for your authorization.' ' Please try again later.') return message, 502 # Bad Gateway # All non-error responses should have an access token. access_token = result['access_token'] refresh_token = result.get('refresh_token') # This is in seconds, but we convert it to an absolute timestamp so that we can # account for the potential delay it takes to look up the G+ id we should associate # the access tokens with. (Could be up to GOOGLE_API_TIMEOUT seconds later.) expiry = datetime.datetime.today() + datetime.timedelta(seconds=result['expires_in']) try: person = get_person_by_access_token(access_token) except UnavailableException as e: app.logger.error('Unable to finish OAuth2 flow: %r.' % e) message = ('Whoops, we got an invalid response from Google for your authorization.' ' Please try again later.') return message, 502 # Bad Gateway if refresh_token is not None: TokenIdMapping.update_refresh_token(person['id'], refresh_token) # Convert the absolute expiry timestamp back into a duration in seconds expires_in = int((expiry - datetime.datetime.today()).total_seconds()) Cache.set(ACCESS_TOKEN_CACHE_KEY_TEMPLATE % person['id'], access_token, time=expires_in) # Whew, all done! Set a cookie with the user's G+ id and send them back to the homepage. app.logger.info("Successfully authenticated G+ id %s.", person['id']) response = flask.make_response(flask.redirect(flask.url_for('main'))) response.set_cookie('gplus_id', person['id']) return response