# -*- coding: utf-8 -*-
"""
|gae| Extras
------------

Utilities you can use when using this library on |gae|_.
"""

from google.appengine.ext import ndb
from webapp2_extras import sessions

from authomatic import exceptions
from authomatic.extras import interfaces
from authomatic.extras.gae.openid import NDBOpenIDStore

__all__ = ['ndb_config', 'Webapp2Session']

class GAEError(exceptions.BaseError):
    pass

class Webapp2Session(interfaces.BaseSession):
    """
    A simple wrapper for |webapp2|_ sessions. If you provide a session it wraps
    it and adds the :meth:`.save` method.

    If you don't provide a session it creates a new one but you must provide
    the :data:`.secret`.

    For more about |webapp2| sessions see:
    http://webapp-improved.appspot.com/api/webapp2_extras/sessions.html.

    """

    def __init__(self, handler, session=None, secret=None,
                 cookie_name='webapp2authomatic', backend='memcache',
                 config=None):
        """
        .. warning::

            Do not use the ``'securecookie'`` backend with
            :class:`.providers.OpenID` provider. The
            `python-openid`_ library saves **non json serializable** objects
            to session which the ``'securecookie'`` backend cannot cope with.

        :param handler:
            A :class:`webapp2.RequestHandler` instance.

        :param session:
            A :class:`webapp2_extras.session.SessionDict` instance.

        :param str secret:
            The session secret.

        :param str cookie_name:
            The name of the session cookie.

        :param backend:
            The session backend. One of ``'memcache'`` or ``'datastore'``.

        :param config:
            The session config.

        """

        self.handler = handler

        if session is None:
            if not secret:
                raise GAEError('Either session or secret must be specified!')
            else:
                # Create new session.
                cfg = config or dict(
                    secret_key=secret, cookie_name=cookie_name)
                session_store = sessions.SessionStore(handler.request, cfg)
                self.session_dict = session_store.get_session(backend=backend)
        else:
            # Use supplied session.
            self.session_dict = session

    def save(self):
        return self.session_dict.container.save_session(self.handler.response)

    def __setitem__(self, key, value):
        return self.session_dict.__setitem__(key, value)

    def __getitem__(self, key):
        return self.session_dict.__getitem__(key)

    def __delitem__(self, key):
        return self.session_dict.__delitem__(key)

    def get(self, key):
        return self.session_dict.get(key)

class NDBConfig(ndb.Model):
    """
    |gae| `NDB <https://developers.google.com/appengine/docs/python/ndb/>`_
    based :doc:`config`.

    .. note::

        By :class:`.OpenID` provider uses :class:`.NDBOpenIDStore`
        as default :attr:`.OpenID.store`.

    """

    # General properties
    provider_name = ndb.StringProperty()
    class_ = ndb.StringProperty()

    # AuthorizationProvider properties
    provider_id = ndb.IntegerProperty()
    consumer_key = ndb.StringProperty()
    consumer_secret = ndb.StringProperty()

    # OAuth2 properties
    scope = ndb.StringProperty()
    offline = ndb.BooleanProperty()

    # AuthenticationProvider properties
    identifier_param = ndb.StringProperty()

    @classmethod
    def get(cls, key, default=None):
        """
        Resembles the :meth:`dict.get` method.

        :returns:
            A configuration dictionary for specified provider.

        """

        # Query datastore.
        result = cls.query(cls.provider_name == key).get()

        if result:
            result_dict = result.to_dict()

            # Use NDBOpenIDStore by default
            result_dict['store'] = NDBOpenIDStore

            # Convert coma-separated values to list. Currently only scope is
            # csv.
            for i in ('scope', ):
                prop = result_dict.get(i)
                if prop:
                    result_dict[i] = [s.strip() for s in prop.split(',')]
                else:
                    result_dict[i] = None

            return result_dict
        else:
            return default

    @classmethod
    def values(cls):
        """
        Resembles the :meth:`dict.values` method.
        """

        # get all items
        results = cls.query().fetch()
        # return list of dictionaries
        return [result.to_dict() for result in results]

    @classmethod
    def initialize(cls):
        """
        Creates an **"Example"** entity of kind **"NDBConfig"** in the
        datastore if the model is empty and raises and error to inform you that
        you should populate the model with data.

        .. note::

            The *Datastore Viewer* in the ``_ah/admin/`` won't let you add
            properties to a model if there is not an entity with that
            property already. Therefore it is a good idea to keep the
            **"Example"** entity (which has all possible properties set) in
            the datastore.

        """

        if not len(cls.query().fetch()):

            example = cls.get_or_insert('Example')

            example.class_ = 'Provider class e.g. ' + \
                             '"authomatic.providers.oauth2.Facebook".'
            example.provider_name = 'Your custom provider name e.g. "fb".'

            # AuthorizationProvider
            example.consumer_key = 'Consumer key.'
            example.consumer_secret = 'Consumer secret'
            example.provider_id = 1

            # OAuth2
            example.scope = 'coma, separated, list, of, scopes'

            # AuthenticationProvider
            example.identifier_param = 'Querystring parameter for claimed ' + \
                                       'id. default is "id"'

            # Save the example
            example.put()

            # Raise an information error.
            raise GAEError(
                'A NDBConfig data model was created! Go to Datastore Viewer '
                'in your dashboard and populate it with data!')

def ndb_config():
    """
    Allows you to have a **datastore** :doc:`config` instead of a hardcoded
    one.

    This function creates an **"Example"** entity of kind **"NDBConfig"** in
    the datastore if the model is empty and raises and error to inform you
    that you should populate the model with data.

    .. note::

        The *Datastore Viewer* of the |gae|_ admin won't let you add
        properties to a model if there is not an entity with that property
        already. Therefore it is a good idea to keep the **"Example"**
        entity (which has all properties set) in the datastore.

    :raises:
        :exc:`.GAEError`

    :returns:
        :class:`.NDBConfig`

    """

    NDBConfig.initialize()
    return NDBConfig


# -*- coding: utf-8 -*-
"""
Google App Engine OpenID Providers
----------------------------------

|openid|_ provider implementations based on the |gae_users_api|_.

.. note::

    When using the :class:`GAEOpenID` provider, the :class:`.User` object
    will always have only the
    :attr:`.User.user_id`,
    :attr:`.User.email`,
    :attr:`.User.gae_user`
    attributes populated with data.
    Moreover the :attr:`.User.user_id` will always be empty on the
    `GAE Development Server
    <https://developers.google.com/appengine/docs/python/tools/devserver>`_.

.. autosummary::

    GAEOpenID
    Yahoo
    Google

"""

import logging

from google.appengine.api import users

import authomatic.core as core
from authomatic import providers
from authomatic.exceptions import FailureError

__all__ = ['GAEOpenID', 'Yahoo', 'Google']

class GAEOpenID(providers.AuthenticationProvider):
    """
    |openid|_ provider based on the |gae_users_api|_.

    Accepts additional keyword arguments inherited from
    :class:`.AuthenticationProvider`.

    """

    @providers.login_decorator
    def login(self):
        """
        Launches the OpenID authentication procedure.
        """

        if self.params.get(self.identifier_param):
            # =================================================================
            # Phase 1 before redirect.
            # =================================================================
            self._log(
                logging.INFO,
                u'Starting OpenID authentication procedure.')

            url = users.create_login_url(
                dest_url=self.url, federated_identity=self.identifier)

            self._log(logging.INFO, u'Redirecting user to {0}.'.format(url))

            self.redirect(url)
        else:
            # =================================================================
            # Phase 2 after redirect.
            # =================================================================

            self._log(
                logging.INFO,
                u'Continuing OpenID authentication procedure after redirect.')

            user = users.get_current_user()

            if user:
                self._log(logging.INFO, u'Authentication successful.')
                self._log(logging.INFO, u'Creating user.')
                self.user = core.User(self,
                                      id=user.federated_identity(),
                                      email=user.email(),
                                      gae_user=user)

                # =============================================================
                # We're done
                # =============================================================
            else:
                raise FailureError(
                    'Unable to authenticate identifier "{0}"!'.format(
                        self.identifier))

class Yahoo(GAEOpenID):
    """
    :class:`.GAEOpenID` provider with the :attr:`.identifier` set to
    ``"me.yahoo.com"``.
    """

    identifier = 'me.yahoo.com'

class Google(GAEOpenID):
    """
    :class:`.GAEOpenID` provider with the :attr:`.identifier` set to
    ``"https://www.google.com/accounts/o8/id"``.
    """

    identifier = 'https://www.google.com/accounts/o8/id'


# -*- coding: utf-8 -*-
"""
Abstract Classes for Providers
------------------------------

Abstract base classes for implementation of protocol specific providers.

.. note::

    Attributes prefixed with ``_x_`` serve the purpose of unification
    of differences across providers.

.. autosummary::

    login_decorator
    BaseProvider
    AuthorizationProvider
    AuthenticationProvider

"""

import abc
import base64
import hashlib
import logging
import random
import sys
import traceback
import uuid

import authomatic.core
from authomatic.exceptions import (
    ConfigError,
    FetchError,
    CredentialsError,
)
from authomatic import six
from authomatic.six.moves import urllib_parse as parse
from authomatic.six.moves import http_client
from authomatic.exceptions import CancellationError

__all__ = [
    'BaseProvider',
    'AuthorizationProvider',
    'AuthenticationProvider',
    'login_decorator']

def _error_traceback_html(exc_info, traceback_):
    """
    Generates error traceback HTML.

    :param tuple exc_info:
        Output of :func:`sys.exc_info` function.

    :param traceback:
        Output of :func:`traceback.format_exc` function.

    """

    html = """

            <title>ERROR: {error}</title>

The Authomatic library encountered an error!

{error}

{traceback}

    """

    return html.format(error=exc_info[1], traceback=traceback_)

def login_decorator(func):
    """
    Decorate the :meth:`.BaseProvider.login` implementations with this
    decorator.

    Provides mechanism for error reporting and returning result which
    makes the :meth:`.BaseProvider.login` implementation cleaner.

    """

    def wrap(provider, *args, **kwargs):
        error = None
        result = authomatic.core.LoginResult(provider)

        try:
            func(provider, *args, **kwargs)
        except Exception as e:  # pylint:disable=broad-except
            if provider.settings.report_errors:
                error = e
                if not isinstance(error, CancellationError):
                    provider._log(
                        logging.ERROR,
                        u'Reported suppressed exception: {0}!'.format(
                            repr(error)),
                        exc_info=1)
            else:
                if provider.settings.debug:
                    # TODO: Check whether it actually works without middleware
                    provider.write(
                        _error_traceback_html(
                            sys.exc_info(),
                            traceback.format_exc()))
                raise

        # If there is user or error the login procedure has finished
        if provider.user or error:
            result = authomatic.core.LoginResult(provider)
            # Add error to result
            result.error = error

            # delete session cookie
            if isinstance(provider.session, authomatic.core.Session):
                provider.session.delete()

            provider._log(logging.INFO, u'Procedure finished.')

            if provider.callback:
                provider.callback(result)
            return result
        else:
            # Save session
            provider.save_session()

    return wrap

class BaseProvider(object):
    """
    Abstract base class for all providers.
    """

    PROVIDER_TYPE_ID = 0

    _repr_ignore = ('user',)

    __metaclass__ = abc.ABCMeta

    supported_user_attributes = authomatic.core.SupportedUserAttributes()

    def __init__(self, settings, adapter, provider_name, session=None,
                 session_saver=None, callback=None, js_callback=None,
                 prefix='authomatic', **kwargs):

        self.settings = settings
        self.adapter = adapter

        self.session = session
        self.save_session = session_saver

        #: :class:`str` The provider name as specified in the :doc:`config`.
        self.name = provider_name

        #: :class:`callable` An optional callback called when the login
        #: procedure is finished with :class:`.core.LoginResult` passed as
        #: argument.
        self.callback = callback

        #: :class:`str` Name of an optional javascript callback.
        self.js_callback = js_callback

        #: :class:`.core.User`.
        self.user = None

        #: :class:`bool` If ``True``, the
        #: :attr:`.BaseProvider.user_authorization_url` will be displayed
        #: in a *popup mode*, if the **provider** supports it.
        self.popup = self._kwarg(kwargs, 'popup')

    @property
    def url(self):
        return self.adapter.url

    @property
    def params(self):
        return self.adapter.params

    def write(self, value):
        self.adapter.write(value)

    def set_header(self, key, value):
        self.adapter.set_header(key, value)

    def set_status(self, status):
        self.adapter.set_status(status)

    def redirect(self, url):
        self.set_status('302 Found')
        self.set_header('Location', url)

    # ========================================================================
    # Abstract methods
    # ========================================================================

    @abc.abstractmethod
    def login(self):
        """
        Launches the *login procedure* to get **user's credentials** from
        **provider**.

        Should be decorated with :func:`.login_decorator`. The *login
        procedure* is considered finished when the :attr:`.user`
        attribute is not empty when the method runs out of it's flow or
        when there are errors.

        """

    # ========================================================================
    # Exposed methods
    # ========================================================================

    def to_dict(self):
        """
        Converts the provider instance to a :class:`dict`.

        :returns:
            :class:`dict`

        """

        return dict(name=self.name,
                    id=getattr(self, 'id', None),
                    type_id=self.type_id,
                    type=self.get_type(),
                    scope=getattr(self, 'scope', None),
                    user=self.user.id if self.user else None)

    @classmethod
    def get_type(cls):
        """
        Returns the provider type.

        :returns:
            :class:`str` The full dotted path to base class e.g.
            :literal:`"authomatic.providers.oauth2.OAuth2"`.

        """

        return cls.__module__ + '.' + cls.__bases__[0].__name__

    def update_user(self):
        """
        Updates and returns :attr:`.user`.

        :returns:
            :class:`.User`

        """

    # ========================================================================
    # Internal methods
    # ========================================================================

    @property
    def type_id(self):
        pass

    def _kwarg(self, kwargs, kwname, default=None):
        """
        Resolves keyword arguments from constructor or :doc:`config`.

        .. note::

            The keyword arguments take this order of precedence:

            1. Arguments passed to constructor through the
               :func:`authomatic.login`.
            2. Provider specific arguments from :doc:`config`.
            3. Arguments from :doc:`config` set in the ``__defaults__`` key.
            2. The value from :data:`default` argument.

        :param dict kwargs:
            Keyword arguments dictionary.
        :param str kwname:
            Name of the desired keyword argument.

        """

        return kwargs.get(kwname) or \
            self.settings.config.get(self.name, {}).get(kwname) or \
            self.settings.config.get('__defaults__', {}).get(kwname) or \
            default

    def _session_key(self, key):
        """
        Generates session key string.

        :param str key:
            e.g. ``"authomatic:facebook:key"``

        """

        return '{0}:{1}:{2}'.format(self.settings.prefix, self.name, key)

    def _session_set(self, key, value):
        """
        Saves a value to session.
        """

        self.session[self._session_key(key)] = value

    def _session_get(self, key):
        """
        Retrieves a value from session.
        """

        return self.session.get(self._session_key(key))

    @staticmethod
    def csrf_generator(secret):
        """
        Generates CSRF token.

        Inspired by this article:
        http://blog.ptsecurity.com/2012/10/random-number-security-in-python.html

        :returns:
            :class:`str` Random unguessable string.

        """

        # Create hash from random string plus salt.
        hashed = hashlib.md5(uuid.uuid4().bytes + six.b(secret)).hexdigest()

        # Each time return random portion of the hash.
        span = 5
        shift = random.randint(0, span)
        return hashed[shift:shift - span - 1]

    @classmethod
    def _log(cls, level, msg, **kwargs):
        """
        Logs a message with pre-formatted prefix.

        :param int level:
            Logging level as specified in the
            `login module <http://docs.python.org/2/library/logging.html>`_ of
            Python standard library.

        :param str msg:
            The actual message.

        """

        logger = getattr(cls, '_logger', None) or authomatic.core._logger
        logger.log(
            level, ': '.join(
                ('authomatic', cls.__name__, msg)), **kwargs)

    def _fetch(self, url, method='GET', params=None, headers=None,
               body='', max_redirects=5, content_parser=None):
        """
        Fetches a URL.

        :param str url:
            The URL to fetch.

        :param str method:
            HTTP method of the request.

        :param dict params:
            Dictionary of request parameters.

        :param dict headers:
            HTTP headers of the request.

        :param str body:
            Body of ``POST``, ``PUT`` and ``PATCH`` requests.

        :param int max_redirects:
            Number of maximum HTTP redirects to follow.

        :param function content_parser:
            A callable to be used to parse the :attr:`.Response.data`
            from :attr:`.Response.content`.

        """
        # 'magic' using _kwarg method
        # pylint:disable=no-member
        params = params or {}
        params.update(self.access_params)

        headers = headers or {}
        headers.update(self.access_headers)

        scheme, host, path, query, fragment = parse.urlsplit(url)
        query = parse.urlencode(params)

        if method in ('POST', 'PUT', 'PATCH'):
            if not body:
                # Put querystring to body
                body = query
                query = ''
                headers.update(
                    {'Content-Type': 'application/x-www-form-urlencoded'})
        request_path = parse.urlunsplit(('', '', path or '', query or '', ''))

        self._log(logging.DEBUG, u' \u251C\u2500 host: {0}'.format(host))
        self._log(
            logging.DEBUG,
            u' \u251C\u2500 path: {0}'.format(request_path))
        self._log(logging.DEBUG, u' \u251C\u2500 method: {0}'.format(method))
        self._log(logging.DEBUG, u' \u251C\u2500 body: {0}'.format(body))
        self._log(logging.DEBUG, u' \u251C\u2500 params: {0}'.format(params))
        self._log(logging.DEBUG, u' \u2514\u2500 headers: {0}'.format(headers))

        # Connect
        if scheme.lower() == 'https':
            connection = http_client.HTTPSConnection(host)
        else:
            connection = http_client.HTTPConnection(host)

        try:
            connection.request(method, request_path, body, headers)
        except Exception as e:
            raise FetchError('Fetching URL failed',
                             original_message=str(e),
                             url=request_path)

        response = connection.getresponse()
        location = response.getheader('Location')

        if response.status in (300, 301, 302, 303, 307) and location:
            if location == url:
                raise FetchError('Url redirects to itself!',
                                 url=location,
                                 status=response.status)

            elif max_redirects > 0:
                remaining_redirects = max_redirects - 1

                self._log(logging.DEBUG, u'Redirecting to {0}'.format(url))
                self._log(logging.DEBUG, u'Remaining redirects: {0}'
                          .format(remaining_redirects))

                # Call this method again.
                response = self._fetch(url=location,
                                       params=params,
                                       method=method,
                                       headers=headers,
                                       max_redirects=remaining_redirects)

            else:
                raise FetchError('Max redirects reached!',
                                 url=location,
                                 status=response.status)
        else:
            self._log(logging.DEBUG, u'Got response:')
            self._log(logging.DEBUG, u' \u251C\u2500 url: {0}'.format(url))
            self._log(
                logging.DEBUG,
                u' \u251C\u2500 status: {0}'.format(
                    response.status))
            self._log(
                logging.DEBUG,
                u' \u2514\u2500 headers: {0}'.format(
                    response.getheaders()))

        return authomatic.core.Response(response, content_parser)

    def _update_or_create_user(self, data, credentials=None, content=None):
        """
        Updates or creates :attr:`.user`.

        :returns:
            :class:`.User`

        """

        if not self.user:
            self.user = authomatic.core.User(self, credentials=credentials)

        self.user.content = content
        self.user.data = data

        # Update.
        for key in self.user.__dict__:
            # Exclude data.
            if key not in ('data', 'content'):
                # Extract every data item whose key matches the user
                # property name, but only if it has a value.
                value = data.get(key)
                if value:
                    setattr(self.user, key, value)

        # Handle different structure of data by different providers.
        self.user = self._x_user_parser(self.user, data)

        if self.user.id:
            self.user.id = str(self.user.id)

        # TODO: Move to User
        # If there is no user.name,
        if not self.user.name:
            if self.user.first_name and self.user.last_name:
                # Create it from first name and last name if available.
                self.user.name = ' '.join((self.user.first_name,
                                           self.user.last_name))
            else:
                # Or use one of these.
                self.user.name = (self.user.username or self.user.nickname or
                                  self.user.first_name or self.user.last_name)

        if not self.user.location:
            if self.user.city and self.user.country:
                self.user.location = '{0}, {1}'.format(self.user.city,
                                                       self.user.country)
            else:
                self.user.location = self.user.city or self.user.country

        return self.user

    @staticmethod
    def _x_user_parser(user, data):
        """
        Handles different structure of user info data by different providers.

        :param user:
            :class:`.User`
        :param dict data:
            User info data returned by provider.

        """

        return user

    @staticmethod
    def _http_status_in_category(status, category):
        """
        Checks whether a HTTP status code is in the category denoted by the
        hundreds digit.
        """

        assert category < 10, 'HTTP status category must be a one-digit int!'
        cat = category * 100
        return status >= cat and status < cat + 100

class AuthorizationProvider(BaseProvider):
    """
    Base provider for *authorization protocols* i.e. protocols which allow a
    **provider** to authorize a **consumer** to access **protected resources**
    of a **user**.

    e.g. `OAuth 2.0 <http://oauth.net/2/>`_ or `OAuth 1.0a
    <http://oauth.net/core/1.0a/>`_.

    """

    USER_AUTHORIZATION_REQUEST_TYPE = 2
    ACCESS_TOKEN_REQUEST_TYPE = 3
    PROTECTED_RESOURCE_REQUEST_TYPE = 4
    REFRESH_TOKEN_REQUEST_TYPE = 5

    BEARER = 'Bearer'

    _x_term_dict = {}

    #: If ``True`` the provider doesn't support Cross-site HTTP requests.
    same_origin = True

    #: :class:`bool` Whether the provider supports JSONP requests.
    supports_jsonp = False

    # Whether to use the HTTP Authorization header.
    _x_use_authorization_header = True

    def __init__(self, *args, **kwargs):
        """
        Accepts additional keyword arguments:

        :arg str consumer_key:
            The *key* assigned to our application (**consumer**) by the
            **provider**.

        :arg str consumer_secret:
            The *secret* assigned to our application (**consumer**) by the
            **provider**.

        :arg int id:
            A unique numeric ID used to serialize :class:`.Credentials`.

        :arg dict user_authorization_params:
            A dictionary of additional request parameters for
            **user authorization request**.

        :arg dict access_token_params:
            A dictionary of additional request parameters for
            **access_with_credentials token request**.

        :arg dict access_headers:
            A dictionary of default HTTP headers that will be used when
            accessing **user's** protected resources.
            Applied by :meth:`.access()`, :meth:`.update_user()` and
            :meth:`.User.update()`

        :arg dict access_params:
            A dictionary of default query string parameters that will be used
            when accessing **user's** protected resources.
            Applied by :meth:`.access()`, :meth:`.update_user()` and
            :meth:`.User.update()`

        """

        super(AuthorizationProvider, self).__init__(*args, **kwargs)

        self.consumer_key = self._kwarg(kwargs, 'consumer_key')
        self.consumer_secret = self._kwarg(kwargs, 'consumer_secret')

        self.user_authorization_params = self._kwarg(
            kwargs, 'user_authorization_params', {})

        self.access_token_headers = self._kwarg(
            kwargs, 'user_authorization_headers', {})
        self.access_token_params = self._kwarg(
            kwargs, 'access_token_params', {})

        self.id = self._kwarg(kwargs, 'id')

        self.access_headers = self._kwarg(kwargs, 'access_headers', {})
        self.access_params = self._kwarg(kwargs, 'access_params', {})

        #: :class:`.Credentials` to access **user's protected resources**.
        self.credentials = authomatic.core.Credentials(
            self.settings.config, provider=self)

        #: Response of the *access token request*.
        self.access_token_response = None

    # ========================================================================
    # Abstract properties
    # ========================================================================

    @abc.abstractproperty
    def user_authorization_url(self):
        """
        :class:`str` URL to which we redirect the **user** to grant our app
        i.e. the **consumer** an **authorization** to access his
        **protected resources**. See
        http://tools.ietf.org/html/rfc6749#section-4.1.1 and
        http://oauth.net/core/1.0a/#auth_step2.
        """

    @abc.abstractproperty
    def access_token_url(self):
        """
        :class:`str` URL where we can get the *access token* to access
        **protected resources** of a **user**. See
        http://tools.ietf.org/html/rfc6749#section-4.1.3 and
        http://oauth.net/core/1.0a/#auth_step3.
        """

    @abc.abstractproperty
    def user_info_url(self):
        """
        :class:`str` URL where we can get the **user** info.
        see http://tools.ietf.org/html/rfc6749#section-7 and
        http://oauth.net/core/1.0a/#anchor12.
        """

    # ========================================================================
    # Abstract methods
    # ========================================================================

    @abc.abstractmethod
    def to_tuple(self, credentials):
        """
        Must convert :data:`credentials` to a :class:`tuple` to be used by
        :meth:`.Credentials.serialize`.

        .. warning::

            |classmethod|

        :param credentials:
            :class:`.Credentials`

        :returns:
            :class:`tuple`

        """

    @abc.abstractmethod
    def reconstruct(self, deserialized_tuple, credentials, cfg):
        """
        Must convert the :data:`deserialized_tuple` back to
        :class:`.Credentials`.

        .. warning::

            |classmethod|

        :param tuple deserialized_tuple:
            A tuple whose first index is the :attr:`.id` and the rest
            are all the items of the :class:`tuple` created by
            :meth:`.to_tuple`.

        :param credentials:
            A :class:`.Credentials` instance.

        :param dict cfg:
            Provider configuration from :doc:`config`.

        """

    @abc.abstractmethod
    def create_request_elements(self, request_type, credentials,
                                url, method='GET', params=None, headers=None,
                                body=''):
        """
        Must return :class:`.RequestElements`.

        .. warning::

            |classmethod|

        :param int request_type:
            Type of the request specified by one of the class's constants.

        :param credentials:
            :class:`.Credentials` of the **user** whose
            **protected resource** we want to access.

        :param str url:
            URL of the request.

        :param str method:
            HTTP method of the request.

        :param dict params:
            Dictionary of request parameters.

        :param dict headers:
            Dictionary of request headers.

        :param str body:
            Body of ``POST``, ``PUT`` and ``PATCH`` requests.

        :returns:
            :class:`.RequestElements`

        """

    # ========================================================================
    # Exposed methods
    # ========================================================================

    @property
    def type_id(self):
        """
        A short string representing the provider implementation id used for
        serialization of :class:`.Credentials` and to identify the type of
        provider in JavaScript.

        The part before hyphen denotes the type of the provider, the part
        after hyphen denotes the class id e.g.
        ``oauth2.Facebook.type_id = '2-5'``,
        ``oauth1.Twitter.type_id = '1-5'``.

        """

        cls = self.__class__
        mod = sys.modules.get(cls.__module__)

        return str(self.PROVIDER_TYPE_ID) + '-' + \
            str(mod.PROVIDER_ID_MAP.index(cls))

    def access(self, url, params=None, method='GET', headers=None,
               body='', max_redirects=5, content_parser=None):
        """
        Fetches the **protected resource** of an authenticated **user**.

        :param credentials:
            The **user's** :class:`.Credentials` (serialized or normal).

        :param str url:
            The URL of the **protected resource**.

        :param str method:
            HTTP method of the request.

        :param dict headers:
            HTTP headers of the request.

        :param str body:
            Body of ``POST``, ``PUT`` and ``PATCH`` requests.

        :param int max_redirects:
            Maximum number of HTTP redirects to follow.

        :param function content_parser:
            A function to be used to parse the :attr:`.Response.data`
            from :attr:`.Response.content`.

        :returns:
            :class:`.Response`

        """

        if not self.user and not self.credentials:
            raise CredentialsError(u'There is no authenticated user!')

        headers = headers or {}

        self._log(
            logging.INFO,
            u'Accessing protected resource {0}.'.format(url))

        request_elements = self.create_request_elements(
            request_type=self.PROTECTED_RESOURCE_REQUEST_TYPE,
            credentials=self.credentials,
            url=url,
            body=body,
            params=params,
            headers=headers,
            method=method
        )

        response = self._fetch(*request_elements,
                               max_redirects=max_redirects,
                               content_parser=content_parser)

        self._log(
            logging.INFO,
            u'Got response. HTTP status = {0}.'.format(
                response.status))
        return response

    def async_access(self, *args, **kwargs):
        """
        Same as :meth:`.access` but runs asynchronously in a separate thread.

        .. warning::

            |async|

        :returns:
            :class:`.Future` instance representing the separate thread.

        """

        return authomatic.core.Future(self.access, *args, **kwargs)

    def update_user(self):
        """
        Updates the :attr:`.BaseProvider.user`.

        .. warning::
            Fetches the :attr:`.user_info_url`!

        :returns:
            :class:`.UserInfoResponse`

        """
        if self.user_info_url:
            response = self._access_user_info()
            self.user = self._update_or_create_user(response.data,
                                                    content=response.content)
            return authomatic.core.UserInfoResponse(self.user,
                                                    response.httplib_response)

    # ========================================================================
    # Internal methods
    # ========================================================================

    @classmethod
    def _authorization_header(cls, credentials):
        """
        Creates authorization headers if the provider supports it. See:
        http://en.wikipedia.org/wiki/Basic_access_authentication.

        :param credentials:
            :class:`.Credentials`

        :returns:
            Headers as :class:`dict`.

        """

        if cls._x_use_authorization_header:
            res = ':'.join(
                (credentials.consumer_key,
                 credentials.consumer_secret))
            res = base64.b64encode(six.b(res)).decode()
            return {'Authorization': 'Basic {0}'.format(res)}
        else:
            return {}

    def _check_consumer(self):
        """
        Validates the :attr:`.consumer`.
        """

        # 'magic' using _kwarg method
        # pylint:disable=no-member
        if not self.consumer.key:
            raise ConfigError(
                'Consumer key not specified for provider {0}!'.format(
                    self.name))

        if not self.consumer.secret:
            raise ConfigError(
                'Consumer secret not specified for provider {0}!'.format(
                    self.name))

    @staticmethod
    def _split_url(url):
        """
        Splits given url to url base and params converted to list of tuples.
        """

        split = parse.urlsplit(url)
        base = parse.urlunsplit((split.scheme, split.netloc, split.path, 0, 0))
        params = parse.parse_qsl(split.query, True)

        return base, params

    @classmethod
    def _x_request_elements_filter(
            cls, request_type, request_elements, credentials):
        """
        Override this to handle special request requirements of zealous
        providers.

        .. warning::

            |classmethod|

        :param int request_type:
            Type of request.

        :param request_elements:
            :class:`.RequestElements`

        :param credentials:
            :class:`.Credentials`

        :returns:
            :class:`.RequestElements`

        """

        return request_elements

    @staticmethod
    def _x_credentials_parser(credentials, data):
        """
        Override this to handle differences in naming conventions across
        providers.

        :param credentials:
            :class:`.Credentials`

        :param dict data:
            Response data dictionary.

        :returns:
            :class:`.Credentials`

        """
        return credentials

    def _access_user_info(self):
        """
        Accesses the :attr:`.user_info_url`.

        :returns:
            :class:`.UserInfoResponse`

        """
        url = self.user_info_url.format(**self.user.__dict__)
        return self.access(url)

class AuthenticationProvider(BaseProvider):
    """
    Base provider for *authentication protocols* i.e. protocols which allow a
    **provider** to authenticate a *claimed identity* of a **user**.

    e.g. `OpenID <http://openid.net/>`_.

    """

    #: Indicates whether the **provider** supports access_with_credentials to
    #: **user's** protected resources.
    # TODO: Useless
    has_protected_resources = False

    def __init__(self, *args, **kwargs):
        super(AuthenticationProvider, self).__init__(*args, **kwargs)

        # Lookup default identifier, if available in provider
        default_identifier = getattr(self, 'identifier', None)

        # Allow for custom name for the "id" querystring parameter.
        self.identifier_param = kwargs.get('identifier_param', 'id')

        # Get the identifier from request params, or use default as fallback.
        self.identifier = self.params.get(
            self.identifier_param, default_identifier)

PROVIDER_ID_MAP = [
    AuthenticationProvider,
    AuthorizationProvider,
    BaseProvider,
]


# -*- coding: utf-8 -*-
"""
    decorator
    ~~~~
    This unit exposes a single decorator which should be used to wrap a
    Sanic route with. It accepts all parameters and options as
    the CORS extension.

    :copyright: (c) 2018 by Ashley Sommer (based on flask-cors by Cory Dolphin).
    :license: MIT, see LICENSE for more details.
"""

from spf import SanicPluginsFramework
from .core import *
from .extension import cors

def cross_origin(app, *args, **kwargs):
    """
    This function is the decorator which is used to wrap a Sanic route with.
    In the simplest case, simply use the default parameters to allow all
    origins in what is the most permissive configuration. If this method
    modifies state or performs authentication which may be brute-forced, you
    should add some degree of protection, such as Cross Site Forgery
    Request protection.

    :param origins:
        The origin, or list of origins to allow requests from.
        The origin(s) may be regular expressions, case-sensitive strings,
        or else an asterisk

        Default : '*'
    :type origins: list, string or regex

    :param methods:
        The method or list of methods which the allowed origins are allowed to
        access for non-simple requests.

        Default : [GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]
    :type methods: list or string

    :param expose_headers:
        The header or list which are safe to expose to the API of a CORS API
        specification.

        Default : None
    :type expose_headers: list or string

    :param allow_headers:
        The header or list of header field names which can be used when this
        resource is accessed by allowed origins. The header(s) may be regular
        expressions, case-sensitive strings, or else an asterisk.

        Default : '*', allow all headers
    :type allow_headers: list, string or regex

    :param supports_credentials:
        Allows users to make authenticated requests. If true, injects the
        `Access-Control-Allow-Credentials` header in responses. This allows
        cookies and credentials to be submitted across domains.

        :note: This option cannot be used in conjuction with a '*' origin

        Default : False
    :type supports_credentials: bool

    :param max_age:
        The maximum time for which this CORS request maybe cached. This value
        is set as the `Access-Control-Max-Age` header.

        Default : None
    :type max_age: timedelta, integer, string or None

    :param send_wildcard: If True, and the origins parameter is `*`, a wildcard
        `Access-Control-Allow-Origin` header is sent, rather than the
        request's `Origin` header.

        Default : False
    :type send_wildcard: bool

    :param vary_header:
        If True, the header Vary: Origin will be returned as per the W3
        implementation guidelines.

        Setting this header when the `Access-Control-Allow-Origin` is
        dynamically generated (e.g. when there is more than one allowed
        origin, and an Origin than '*' is returned) informs CDNs and other
        caches that the CORS headers are dynamic, and cannot be cached.

        If False, the Vary header will never be injected or altered.

        Default : True
    :type vary_header: bool

    :param automatic_options:
        Only applies to the `cross_origin` decorator. If True, Sanic-CORS will
        override Sanic's default OPTIONS handling to return CORS headers for
        OPTIONS requests.

        Default : True
    :type automatic_options: bool

    """
    _options = kwargs
    _real_decorator = cors.decorate(app, *args, run_middleware=False, with_context=False, **kwargs)

    def wrapped_decorator(f):
        spf = SanicPluginsFramework(app)  # get the singleton from the app
        try:
            plugin = spf.register_plugin(cors, skip_reg=True)
        except ValueError as e:
            # this is normal, if this plugin has been registered previously
            assert e.args and len(e.args) > 1
            plugin = e.args[1]
        context = cors.get_context_from_spf(spf)
        log = context.log
        log(logging.DEBUG, "Enabled {:s} for cross_origin using options: {}".format(str(f), str(_options)))
        return _real_decorator(f)

    return wrapped_decorator


# -*- coding: utf-8 -*-
"""
    core
    ~~~~
    Core functionality shared between the extension and the decorator.

    :copyright: (c) 2018 by Ashley Sommer (based on flask-cors by Cory Dolphin).
    :license: MIT, see LICENSE for more details.
"""
import re
import logging
import collections
from datetime import timedelta
try:
    from sanic.server import CIMultiDict
except ImportError:
    from sanic.server import CIDict as CIMultiDict

LOG = logging.getLogger(__name__)

# Response Headers
ACL_ORIGIN = 'Access-Control-Allow-Origin'
ACL_METHODS = 'Access-Control-Allow-Methods'
ACL_ALLOW_HEADERS = 'Access-Control-Allow-Headers'
ACL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'
ACL_CREDENTIALS = 'Access-Control-Allow-Credentials'
ACL_MAX_AGE = 'Access-Control-Max-Age'

# Request Header
ACL_REQUEST_METHOD = 'Access-Control-Request-Method'
ACL_REQUEST_HEADERS = 'Access-Control-Request-Headers'

ALL_METHODS = ['GET', 'HEAD', 'POST', 'OPTIONS', 'PUT', 'PATCH', 'DELETE']
CONFIG_OPTIONS = ['CORS_ORIGINS', 'CORS_METHODS', 'CORS_ALLOW_HEADERS',
                  'CORS_EXPOSE_HEADERS', 'CORS_SUPPORTS_CREDENTIALS',
                  'CORS_MAX_AGE', 'CORS_SEND_WILDCARD',
                  'CORS_AUTOMATIC_OPTIONS', 'CORS_VARY_HEADER',
                  'CORS_RESOURCES', 'CORS_INTERCEPT_EXCEPTIONS',
                  'CORS_ALWAYS_SEND']
# Attribute added to request object by decorator to indicate that CORS
# was evaluated, in case the decorator and extension are both applied
# to a view.
# TODO: Refactor these two flags down into one flag.
SANIC_CORS_EVALUATED = '_sanic_cors_e'
SANIC_CORS_SKIP_RESPONSE_MIDDLEWARE = "_sanic_cors_srm"

# Strange, but this gets the type of a compiled regex, which is otherwise not
# exposed in a public API.
RegexObject = type(re.compile(''))
DEFAULT_OPTIONS = dict(origins='*',
                       methods=ALL_METHODS,
                       allow_headers='*',
                       expose_headers=None,
                       supports_credentials=False,
                       max_age=None,
                       send_wildcard=False,
                       automatic_options=False,
                       vary_header=True,
                       resources=r'/*',
                       intercept_exceptions=True,
                       always_send=True)

def parse_resources(resources):
    if isinstance(resources, dict):
        # To make the API more consistent with the decorator, allow a
        # resource of '*', which is not actually a valid regexp.
        resources = [(re_fix(k), v) for k, v in resources.items()]

        # Sort by regex length to provide consistency of matching and
        # to provide a proxy for specificity of match. E.G. longer
        # regular expressions are tried first.
        def pattern_length(pair):
            maybe_regex, _ = pair
            return len(get_regexp_pattern(maybe_regex))

        return sorted(resources,
                      key=pattern_length,
                      reverse=True)

    elif isinstance(resources, str):
        return [(re_fix(resources), {})]

    elif isinstance(resources, collections.abc.Iterable):
        return [(re_fix(r), {}) for r in resources]

    # Type of compiled regex is not part of the public API. Test for this
    # at runtime.
    elif isinstance(resources,  RegexObject):
        return [(re_fix(resources), {})]

    else:
        raise ValueError("Unexpected value for resources argument.")

def get_regexp_pattern(regexp):
    """
    Helper that returns regexp pattern from given value.

    :param regexp: regular expression to stringify
    :type regexp: _sre.SRE_Pattern or str
    :returns: string representation of given regexp pattern
    :rtype: str
    """
    try:
        return regexp.pattern
    except AttributeError:
        return str(regexp)

def get_cors_origins(options, request_origin):
    origins = options.get('origins')
    wildcard = r'.*' in origins

    # If the Origin header is not present terminate this set of steps.
    # The request is outside the scope of this specification.-- W3Spec
    if request_origin:
        LOG.debug("CORS request received with 'Origin' %s", request_origin)

        # If the allowed origins is an asterisk or 'wildcard', always match
        if wildcard and options.get('send_wildcard'):
            LOG.debug("Allowed origins are set to '*'. Sending wildcard CORS header.")
            return ['*']
        # If the value of the Origin header is a case-sensitive match
        # for any of the values in list of origins
        elif try_match_any(request_origin, origins):
            LOG.debug("The request's Origin header matches. Sending CORS headers.", )
            # Add a single Access-Control-Allow-Origin header, with either
            # the value of the Origin header or the string "*" as value.
            # -- W3Spec
            return [request_origin]
        else:
            LOG.debug("The request's Origin header does not match any of allowed origins.")
            return None

    elif options.get('always_send'):
        if wildcard:
            # If wildcard is in the origins, even if 'send_wildcard' is False,
            # simply send the wildcard. It is the most-likely to be correct
            # thing to do (the only other option is to return nothing, which)
            # pretty is probably not whawt you want if you specify origins as
            # '*'
            return ['*']
        else:
            # Return all origins that are not regexes.
            return sorted([o for o in origins if not probably_regex(o)])

    # Terminate these steps, return the original request untouched.
    else:
        LOG.debug("The request did not contain an 'Origin' header. "
                  "This means the browser or client did not request CORS, ensure the Origin Header is set.")
        return None

def get_allow_headers(options, acl_request_headers):
    if acl_request_headers:
        request_headers = [h.strip() for h in acl_request_headers.split(',')]

        # any header that matches in the allow_headers
        matching_headers = filter(
            lambda h: try_match_any(h, options.get('allow_headers')),
            request_headers
        )

        return ', '.join(sorted(matching_headers))

    return None

def get_cors_headers(options, request_headers, request_method):
    origins_to_set = get_cors_origins(options, request_headers.get('Origin'))
    headers = CIMultiDict()

    if not origins_to_set:  # CORS is not enabled for this route
        return headers

    for origin in origins_to_set:
        # TODO, with CIDict, with will only allow one origin
        # With CIMultiDict it should work with multiple
        headers[ACL_ORIGIN] = origin

    headers[ACL_EXPOSE_HEADERS] = options.get('expose_headers')

    if options.get('supports_credentials'):
        headers[ACL_CREDENTIALS] = 'true'  # case sensative

    # This is a preflight request
    # http://www.w3.org/TR/cors/#resource-preflight-requests
    if request_method == 'OPTIONS':
        acl_request_method = request_headers.get(ACL_REQUEST_METHOD, '').upper()

        # If there is no Access-Control-Request-Method header or if parsing
        # failed, do not set any additional headers
        if acl_request_method and acl_request_method in options.get('methods'):

            # If method is not a case-sensitive match for any of the values in
            # list of methods do not set any additional headers and terminate
            # this set of steps.
            headers[ACL_ALLOW_HEADERS] = get_allow_headers(options, request_headers.get(ACL_REQUEST_HEADERS))
            headers[ACL_MAX_AGE] = str(options.get('max_age'))  # sanic cannot handle integers in header values.
            headers[ACL_METHODS] = options.get('methods')
        else:
            LOG.info("The request's Access-Control-Request-Method header does not match allowed methods. "
                     "CORS headers will not be applied.")

    # http://www.w3.org/TR/cors/#resource-implementation
    if options.get('vary_header'):
        # Only set header if the origin returned will vary dynamically,
        # i.e. if we are not returning an asterisk, and there are multiple
        # origins that can be matched.
        if headers[ACL_ORIGIN] == '*':
            pass
        elif (len(options.get('origins')) > 1 or
              len(origins_to_set) > 1 or
              any(map(probably_regex, options.get('origins')))):
            headers['Vary'] = 'Origin'

    return CIMultiDict((k, v) for k, v in headers.items() if v)

def set_cors_headers(req, resp, context, options):
    """
    Performs the actual evaluation of Sanic-CORS options and actually
    modifies the response object.

    This function is used both in the decorator and the after_request
    callback
    :param sanic.request.Request req:

    """
    try:
        request_context = context.request[id(req)]
    except AttributeError:
        LOG.debug("Cannot find the request context. Is request already finished?")
        return resp
    # If CORS has already been evaluated via the decorator, skip
    evaluated = request_context.get(SANIC_CORS_EVALUATED, False)
    if evaluated:
        LOG.debug('CORS have been already evaluated, skipping')
        return resp

    # `resp` can be None in the case of using Websockets
    # however this case should have been handled in the `extension` and `decorator` methods
    # before getting here. This is a final failsafe check to prevent crashing
    if resp is None:
        return None

    if resp.headers is None:
        resp.headers = CIMultiDict()

    headers_to_set = get_cors_headers(options, req.headers, req.method)

    LOG.debug('Settings CORS headers: %s', str(headers_to_set))

    # dict .extend() does not work on CIDict so
    # iterate over them and add them individually.
    try:
        resp.headers.extend(headers_to_set)
    except Exception as e1:
        for k, v in headers_to_set.items():
            try:
                resp.headers.add(k, v)
            except Exception as e2:
                resp.headers[k] = v
        return resp

def probably_regex(maybe_regex):
    if isinstance(maybe_regex, RegexObject):
        return True
    else:
        common_regex_chars = ['*', '\\',']', '?']
        # Use common characters used in regular expressions as a proxy
        # for if this string is in fact a regex.
        return any((c in maybe_regex for c in common_regex_chars))

def re_fix(reg):
    """
        Replace the invalid regex r'*' with the valid, wildcard regex r'/.*' to
        enable the CORS app extension to have a more user friendly api.
    """
    return r'.*' if reg == r'*' else reg

def try_match_any(inst, patterns):
    return any(try_match(inst, pattern) for pattern in patterns)

def try_match(request_origin, maybe_regex):
    """Safely attempts to match a pattern or string to a request origin."""
    if isinstance(maybe_regex, RegexObject):
        return re.match(maybe_regex, request_origin)
    elif probably_regex(maybe_regex):
        return re.match(maybe_regex, request_origin, flags=re.IGNORECASE)
    else:
        try:
            return request_origin.lower() == maybe_regex.lower()
        except AttributeError:
            return request_origin == maybe_regex

def get_cors_options(appInstance, *dicts):
    """
    Compute CORS options for an application by combining the DEFAULT_OPTIONS,
    the app's configuration-specified options and any dictionaries passed. The
    last specified option wins.
    """
    options = DEFAULT_OPTIONS.copy()
    options.update(get_app_kwarg_dict(appInstance))
    if dicts:
        for d in dicts:
            options.update(d)

    return serialize_options(options)

def get_app_kwarg_dict(appInstance):
    """Returns the dictionary of CORS specific app configurations."""
    # In order to support blueprints which do not have a config attribute
    app_config = getattr(appInstance, 'config', {})
    return dict(
        (k.lower().replace('cors_', ''), app_config.get(k))
        for k in CONFIG_OPTIONS
        if app_config.get(k) is not None
    )

def flexible_str(obj):
    """
    A more flexible str function which intelligently handles stringifying
    strings, lists and other iterables. The results are lexographically sorted
    to ensure generated responses are consistent when iterables such as Set
    are used.
    """
    if obj is None:
        return None
    elif(not isinstance(obj, str)
            and isinstance(obj, collections.abc.Iterable)):
        return ', '.join(str(item) for item in sorted(obj))
    else:
        return str(obj)

def serialize_option(options_dict, key, upper=False):
    if key in options_dict:
        value = flexible_str(options_dict[key])
        options_dict[key] = value.upper() if upper else value

def ensure_iterable(inst):
    """
    Wraps scalars or string types as a list, or returns the iterable instance.
    """
    if isinstance(inst, str):
        return [inst]
    elif not isinstance(inst, collections.abc.Iterable):
        return [inst]
    else:
        return inst

def sanitize_regex_param(param):
    return [re_fix(x) for x in ensure_iterable(param)]

def serialize_options(opts):
    """
    A helper method to serialize and processes the options dictionary.
    """
    options = (opts or {}).copy()

    for key in opts.keys():
        if key not in DEFAULT_OPTIONS:
            LOG.warning("Unknown option passed to Sanic-CORS: %s", key)

    # Ensure origins is a list of allowed origins with at least one entry.
    options['origins'] = sanitize_regex_param(options.get('origins'))
    options['allow_headers'] = sanitize_regex_param(options.get('allow_headers'))

    # This is expressly forbidden by the spec. Raise a value error so people
    # don't get burned in production.
    if r'.*' in options['origins'] and options['supports_credentials'] and options['send_wildcard']:
        raise ValueError("Cannot use supports_credentials in conjunction with"
                         "an origin string of '*'. See: "
                         "http://www.w3.org/TR/cors/#resource-requests")

    serialize_option(options, 'expose_headers')
    serialize_option(options, 'methods', upper=True)

    if isinstance(options.get('max_age'), timedelta):
        options['max_age'] = str(int(options['max_age'].total_seconds()))

    return options


"""
Utility module for miscellaneous stuff
"""
import math

def isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0):
    """
    Python 3.4 does not have math.isclose, so we need to steal it and add it here.
    """
    try:
        return math.isclose(a, b, rel_tol=rel_tol, abs_tol=abs_tol)
    except AttributeError:
        # Running on older version of python, fall back to hand-rolled implementation
        if (rel_tol < 0.0) or (abs_tol < 0.0):
            raise ValueError("Tolerances must be non-negative, but are rel_tol: {} and abs_tol: {}".format(rel_tol, abs_tol))
        if math.isnan(a) or math.isnan(b):
            return False  # NaNs are never close to anything, even other NaNs
        if (a == b):
            return True
        if math.isinf(a) or math.isinf(b):
            return False  # Infinity is only close to itself, and we already handled that case
        diff = abs(a - b)
        return (diff <= rel_tol * abs(b)) or (diff <= rel_tol * abs(a)) or (diff <= abs_tol)