Example #1
0
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
Example #2
0
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)
Example #3
0
File: atom.py Project: ayust/pluss
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)
Example #4
0
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
Example #5
0
def auth():
    """Redirect the user to Google to obtain authorization."""
    data = {
        # Basic OAuth2 parameters
        'client_id': Config.get('oauth', 'client-id'),
        'redirect_uri': full_url_for('oauth2'),
        'scope': OAUTH2_SCOPE,
        'response_type': 'code',

        # Settings necessary for daemon operation
        'access_type': 'offline',
        'approval_prompt': 'force',
    }
    return flask.redirect('%s/auth?%s' % (OAUTH2_BASE, urllib.urlencode(data)))
Example #6
0
def auth():
    """Redirect the user to Google to obtain authorization."""
    data = {
        # Basic OAuth2 parameters
        'client_id': Config.get('oauth', 'client-id'),
        'redirect_uri': full_url_for('oauth2'),
        'scope': OAUTH2_SCOPE,
        'response_type': 'code',

        # Settings necessary for daemon operation
        'access_type': 'offline',
        'approval_prompt': 'force',
    }
    return flask.redirect('%s/auth?%s' % (OAUTH2_BASE, urllib.urlencode(data)))
Example #7
0
class Cache(object):
	"""Wrapper around a singleton memcache client.

	Note: If the 'memcache' library is not available,
	this wrapper will do nothing - call() will transparently
	always call the provided function, and everything else
	will simply return None.
	"""

	client = memcache and memcache.Client([Config.get('cache', 'memcache-uri')], debug=0)

	@classmethod
	def call(cls, func, *args, **kwargs):
		if not cls.client:
			return func(*args, **kwargs)

		call_dump = json.dumps([func.__module__, func.__name__, args, kwargs])
		memcache_key = str('pluss--%s' % hashlib.md5(call_dump).hexdigest())
		result = cls.client.get(memcache_key)
		if not result:
			result = func(*args, **kwargs)
			cls.client.set(memcache_key, result)
		return result

	@classmethod
	def get(cls, *args, **kwargs):
		args = (str(args[0]),) + args[1:]
		return cls.client and cls.client.get(*args, **kwargs)

	@classmethod
	def set(cls, *args, **kwargs):
		args = (str(args[0]),) + args[1:]
		return cls.client and cls.client.set(*args, **kwargs)

	@classmethod
	def delete(cls, *args, **kwargs):
		args = (str(args[0]),) + args[1:]
		return cls.client and cls.client.delete(*args, **kwargs)

	@classmethod
	def incr(cls, *args, **kwargs):
		args = (str(args[0]),) + args[1:]
		return cls.client and cls.client.incr(*args, **kwargs)

	@classmethod
	def decr(cls, *args, **kwargs):
		args = (str(args[0]),) + args[1:]
		return cls.client and cls.client.decr(*args, **kwargs)
Example #8
0
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
Example #9
0
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
Example #10
0
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)
Example #11
0
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
Example #12
0
File: app.py Project: ayust/pluss
def full_url_for(*args, **kwargs):
    base = 'http://' + Config.get('server', 'host')
    return  base + flask.url_for(*args, **kwargs)
Example #13
0
File: app.py Project: ayust/pluss
import os

import flask

from pluss.util import db
from pluss.util.config import Config

def full_url_for(*args, **kwargs):
    base = 'http://' + Config.get('server', 'host')
    return  base + flask.url_for(*args, **kwargs)


db.init(os.path.expanduser(os.path.expandvars(Config.get('database', 'path'))))

app = flask.Flask("pluss")
import pluss.handlers
Example #14
0
def full_url_for(*args, **kwargs):
    base = 'http://' + Config.get('server', 'host')
    return base + flask.url_for(*args, **kwargs)
Example #15
0
import hashlib
import json
import logging

from pluss.util.config import Config

if Config.getboolean('cache', 'memcache'):
	try:
		import memcache
		_hush_pyflakes = (memcache,)
		del _hush_pyflakes
	except ImportError:
		logging.error("Config file has memcache enabled, but couldn't import memcache! Not caching data.")
		memcache = None
else:
	memcache = None

class Cache(object):
	"""Wrapper around a singleton memcache client.

	Note: If the 'memcache' library is not available,
	this wrapper will do nothing - call() will transparently
	always call the provided function, and everything else
	will simply return None.
	"""

	client = memcache and memcache.Client([Config.get('cache', 'memcache-uri')], debug=0)

	@classmethod
	def call(cls, func, *args, **kwargs):
		if not cls.client:
Example #16
0
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
Example #17
0
import os

import flask

from pluss.util import db
from pluss.util.config import Config


def full_url_for(*args, **kwargs):
    base = 'http://' + Config.get('server', 'host')
    return base + flask.url_for(*args, **kwargs)


db.init(os.path.expanduser(os.path.expandvars(Config.get('database', 'path'))))

app = flask.Flask("pluss")
import pluss.handlers