<commit_before>#####################################################################################
#
#  Copyright (C) Tavendo GmbH
#
#  Unless a separate license agreement exists between you and Tavendo GmbH (e.g. you
#  have purchased a commercial license), the license terms below apply.
#
#  Should you enter into a separate license agreement after having received a copy of
#  this software, then the terms of such license agreement replace the terms below at
#  the time at which such license agreement becomes effective.
#
#  In case a separate license agreement ends, and such agreement ends without being
#  replaced by another separate license agreement, the license terms below apply
#  from the time at which said agreement ends.
#
#  LICENSE TERMS
#
#  This program is free software: you can redistribute it and/or modify it under the
#  terms of the GNU Affero General Public License, version 3, as published by the
#  Free Software Foundation. This program is distributed in the hope that it will be
#  useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
#  See the GNU Affero General Public License Version 3 for more details.
#
#  You should have received a copy of the GNU Affero General Public license along
#  with this program. If not, see <http://www.gnu.org/licenses/agpl-3.0.en.html>.
#
#####################################################################################

from __future__ import absolute_import

from twisted.trial.unittest import TestCase
from twisted.python.compat import nativeString

from crossbar.adapter.rest import PublisherResource
from crossbar.adapter.rest.test import MockPublisherSession, renderResource

publishBody = b'{"topic": "com.test.messages", "args": [1]}'

class IPWhitelistingTestCase(TestCase):
    """
    Unit tests for the IP address checking parts of L{_CommonResource}.
    """
    def test_allowed_IP(self):
        """
        The client having an allowed IP address allows the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_ip": ["127.0.0.1"]}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 202)

    def test_allowed_IP_range(self):
        """
        The client having an IP in an allowed address range allows the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_ip": ["127.0.0.0/8"]}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 202)

    def test_disallowed_IP_range(self):
        """
        The client having an IP not in allowed address range denies the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_ip": ["192.168.0.0/16", "10.0.0.0/8"]}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn(b"request denied based on IP address",
                      request.getWrittenData())

class SecureTransportTestCase(TestCase):
    """
    Unit tests for the transport security testing parts of L{_CommonResource}.
    """
    def test_required_tls_with_tls(self):
        """
        Required TLS, plus a request over TLS, will allow the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_tls": True}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody, isSecure=True))

        self.assertEqual(request.code, 202)

    def test_not_required_tls_with_tls(self):
        """
        A request over TLS even when not required, will allow the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody, isSecure=True))

        self.assertEqual(request.code, 202)

    def test_required_tls_without_tls(self):
        """
        Required TLS, plus a request NOT over TLS, will deny the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_tls": True}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody, isSecure=False))

        self.assertEqual(request.code, 400)

class RequestBodyTestCase(TestCase):
    """
    Unit tests for the body validation parts of L{_CommonResource}.
    """
    def test_bad_content_type(self):
        """
        An incorrect content type will mean the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/text"]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn(b"bad or missing content type ('application/text')",
                      request.getWrittenData())

    def test_bad_method(self):
        """
        An incorrect method will mean the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"PUT",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 405)
        self.assertIn(b"HTTP/PUT not allowed",
                      request.getWrittenData())

    def test_too_large_body(self):
        """
        A too large body will mean the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"post_body_limit": 1}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn("HTTP/POST body length ({}) exceeds maximum ({})".format(len(publishBody), 1),
                      nativeString(request.getWrittenData()))

    def test_not_matching_bodylength(self):
        """
        A body length that is different than the Content-Length header will mean
        the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"post_body_limit": 1}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"],
                     b"Content-Length": [1]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn("HTTP/POST body length ({}) is different to Content-Length ({})".format(len(publishBody), 1),
                      nativeString(request.getWrittenData()))

    def test_invalid_JSON_body(self):
        """
        A body that is not valid JSON will be rejected by the server.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=b"sometext"))

        self.assertEqual(request.code, 400)
        self.assertIn(b"invalid request event - HTTP/POST body must be valid JSON:",
                      request.getWrittenData())

    def test_JSON_list_body(self):
        """
        A body that is not a JSON dict will be rejected by the server.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=b"[{},{}]"))

        self.assertEqual(request.code, 400)
        self.assertIn(b"invalid request event - HTTP/POST body must be JSON dict",
                      request.getWrittenData())
<commit_msg>content-type tests<commit_after>#####################################################################################
#
#  Copyright (C) Tavendo GmbH
#
#  Unless a separate license agreement exists between you and Tavendo GmbH (e.g. you
#  have purchased a commercial license), the license terms below apply.
#
#  Should you enter into a separate license agreement after having received a copy of
#  this software, then the terms of such license agreement replace the terms below at
#  the time at which such license agreement becomes effective.
#
#  In case a separate license agreement ends, and such agreement ends without being
#  replaced by another separate license agreement, the license terms below apply
#  from the time at which said agreement ends.
#
#  LICENSE TERMS
#
#  This program is free software: you can redistribute it and/or modify it under the
#  terms of the GNU Affero General Public License, version 3, as published by the
#  Free Software Foundation. This program is distributed in the hope that it will be
#  useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
#  See the GNU Affero General Public License Version 3 for more details.
#
#  You should have received a copy of the GNU Affero General Public license along
#  with this program. If not, see <http://www.gnu.org/licenses/agpl-3.0.en.html>.
#
#####################################################################################

from __future__ import absolute_import

from twisted.trial.unittest import TestCase
from twisted.python.compat import nativeString

from crossbar.adapter.rest import PublisherResource
from crossbar.adapter.rest.test import MockPublisherSession, renderResource

publishBody = b'{"topic": "com.test.messages", "args": [1]}'

class IPWhitelistingTestCase(TestCase):
    """
    Unit tests for the IP address checking parts of L{_CommonResource}.
    """
    def test_allowed_IP(self):
        """
        The client having an allowed IP address allows the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_ip": ["127.0.0.1"]}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 202)

    def test_allowed_IP_range(self):
        """
        The client having an IP in an allowed address range allows the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_ip": ["127.0.0.0/8"]}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 202)

    def test_disallowed_IP_range(self):
        """
        The client having an IP not in allowed address range denies the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_ip": ["192.168.0.0/16", "10.0.0.0/8"]}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn(b"request denied based on IP address",
                      request.getWrittenData())

class SecureTransportTestCase(TestCase):
    """
    Unit tests for the transport security testing parts of L{_CommonResource}.
    """
    def test_required_tls_with_tls(self):
        """
        Required TLS, plus a request over TLS, will allow the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_tls": True}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody, isSecure=True))

        self.assertEqual(request.code, 202)

    def test_not_required_tls_with_tls(self):
        """
        A request over TLS even when not required, will allow the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody, isSecure=True))

        self.assertEqual(request.code, 202)

    def test_required_tls_without_tls(self):
        """
        Required TLS, plus a request NOT over TLS, will deny the request.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"require_tls": True}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody, isSecure=False))

        self.assertEqual(request.code, 400)

class RequestBodyTestCase(TestCase):
    """
    Unit tests for the body validation parts of L{_CommonResource}.
    """
    def test_empty_content_type(self):
        """
        A request lacking a content-type header will be rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST", headers={},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertEqual((b"bad or missing content type (''), "
                          b"should be 'application/json'\n"),
                      request.getWrittenData())

    def test_allow_charset_in_content_type(self):
        """
        A charset in the content-type will be allowed.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json; charset=utf-8"]},
            body=publishBody))

        self.assertEqual(request.code, 202)
        self.assertIn(b'{"id":',
                      request.getWrittenData())

    def test_allow_caps_in_content_type(self):
        """
        Differently-capitalised content-type headers will be allowed.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"CONTENT-TYPE": [b"APPLICATION/JSON"]},
            body=publishBody))

        self.assertEqual(request.code, 202)
        self.assertIn(b'{"id":',
                      request.getWrittenData())

    def test_bad_content_type(self):
        """
        An incorrect content type will mean the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/text"]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn(b"bad or missing content type ('application/text')",
                      request.getWrittenData())

    def test_bad_method(self):
        """
        An incorrect method will mean the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"PUT",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 405)
        self.assertIn(b"HTTP/PUT not allowed",
                      request.getWrittenData())

    def test_too_large_body(self):
        """
        A too large body will mean the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"post_body_limit": 1}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn("HTTP/POST body length ({}) exceeds maximum ({})".format(len(publishBody), 1),
                      nativeString(request.getWrittenData()))

    def test_not_matching_bodylength(self):
        """
        A body length that is different than the Content-Length header will mean
        the request is rejected.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({"post_body_limit": 1}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"],
                     b"Content-Length": [1]},
            body=publishBody))

        self.assertEqual(request.code, 400)
        self.assertIn("HTTP/POST body length ({}) is different to Content-Length ({})".format(len(publishBody), 1),
                      nativeString(request.getWrittenData()))

    def test_invalid_JSON_body(self):
        """
        A body that is not valid JSON will be rejected by the server.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=b"sometext"))

        self.assertEqual(request.code, 400)
        self.assertIn(b"invalid request event - HTTP/POST body must be valid JSON:",
                      request.getWrittenData())

    def test_JSON_list_body(self):
        """
        A body that is not a JSON dict will be rejected by the server.
        """
        session = MockPublisherSession(self)
        resource = PublisherResource({}, session)

        request = self.successResultOf(renderResource(
            resource, b"/", method=b"POST",
            headers={b"Content-Type": [b"application/json"]},
            body=b"[{},{}]"))

        self.assertEqual(request.code, 400)
        self.assertIn(b"invalid request event - HTTP/POST body must be JSON dict",
                      request.getWrittenData())
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
"""
    Flask-Micropub
    ==============

    This extension adds the ability to login to a Flask-based website
    using [IndieAuth](https://indiewebcamp.com/IndieAuth), and to request
    an [Micropub](https://indiewebcamp.com/Micropub) access token.
"""

import requests
import bs4
import flask
import functools
import uuid

import sys
if sys.version < '3':
    from urlparse import parse_qs
    from urllib import urlencode
else:
    from urllib.parse import urlencode, parse_qs

DEFAULT_AUTH_URL = 'https://indieauth.com/auth'

class MicropubClient:
    """Flask-Micropub provides support for IndieAuth/Micropub
    authentication and authorization.
    """

    def __init__(self, app=None, client_id=None):
        """Initialize the Micropub extension

        Args:
          app (flask.Flask, optional): the flask application to extend.
          client_id (string, optional): the IndieAuth client id, will be displayed
            when the user is asked to authorize this client.
        """
        self.app = app
        self.client_id = client_id
        if app is not None:
            self.init_app(app, client_id)

    def init_app(self, app, client_id=None):
        """Initialize the Micropub extension if it was not given app
        in the constructor.

        Args:
          app (flask.Flask): the flask application to extend.
          client_id (string, optional): the IndieAuth client id, will be
            displayed when the user is asked to authorize this client. If not
            provided, the app name will be used.
        """
        if not self.client_id:
            if client_id:
                self.client_id = client_id
            else:
                self.client_id = app.name

    def authenticate(self, me, state=None, next_url=None):
        """Authenticate a user via IndieAuth.

        Args:
          me (string): the authing user's URL. if it does not begin with
            https?://, http:// will be prepended.
          state (string, optional): passed through the whole auth process,
            useful if you want to maintain some state, e.g. the starting page
            to return to when auth is complete.
          next_url (string, optional): deprecated and replaced by the more
            general "state". still here for backward compatibility.

        Returns:
          a redirect to the user's specified authorization url, or
          https://indieauth.com/auth if none is provided.
        """
        redirect_url = flask.url_for(
            self.flask_endpoint_for_function(self._authenticated_handler),
            _external=True)
        return self._start_indieauth(me, redirect_url, state or next_url, None)

    def authorize(self, me, state=None, next_url=None, scope='read'):
        """Authorize a user via Micropub.

        Args:
          me (string): the authing user's URL. if it does not begin with
            https?://, http:// will be prepended.
          state (string, optional): passed through the whole auth process,
            useful if you want to maintain some state, e.g. the starting page
            to return to when auth is complete.
          next_url (string, optional): deprecated and replaced by the more
            general "state". still here for backward compatibility.
          scope (string, optional): a space-separated string of micropub
            scopes. 'read' by default.

        Returns:
          a redirect to the user's specified authorization
          https://indieauth.com/auth if none is provided.
        """
        redirect_url = flask.url_for(
            self.flask_endpoint_for_function(self._authorized_handler),
            _external=True)
        return self._start_indieauth(
            me, redirect_url, state or next_url, scope)

    def _start_indieauth(self, me, redirect_url, state, scope):
        """Helper for both authentication and authorization. Kicks off
        IndieAuth by fetching the authorization endpoint from the user's
        homepage and redirecting to it.

        Args:
          me (string): the authing user's URL. if it does not begin with
            https?://, http:// will be prepended.
          redirect_url: the callback URL that we pass to the auth endpoint.
          state (string, optional): passed through the whole auth process,
            useful if you want to maintain some state, e.g. the url to return
            to when the process is complete.
          scope (string): a space-separated string of micropub scopes.

        Returns:
          a redirect to the user's specified authorization
          https://indieauth.com/auth if none is provided.
        """

        if not me.startswith('http://') and not me.startswith('https://'):
            me = 'http://' + me
        auth_url, token_url, micropub_url = self._discover_endpoints(me)
        if not auth_url:
            auth_url = DEFAULT_AUTH_URL

        csrf_token = uuid.uuid4().hex
        flask.session['_micropub_csrf_token'] = csrf_token

        auth_params = {
            'me': me,
            'client_id': self.client_id,
            'redirect_uri': redirect_url,
            'state': '{}|{}'.format(csrf_token, state or ''),
        }
        if scope:
            auth_params['scope'] = scope

        auth_url = auth_url + '?' + urlencode(auth_params)
        flask.current_app.logger.debug('redirecting to %s', auth_url)

        return flask.redirect(auth_url)

    def authenticated_handler(self, f):
        """Decorates the authentication callback endpoint. The endpoint should
        take one argument, a flask.ext.micropub.AuthResponse.
        """
        @functools.wraps(f)
        def decorated():
            resp = self._handle_authorize_response()
            return f(resp)
        self._authenticated_handler = decorated
        return decorated

    def authorized_handler(self, f):
        """Decorates the authorization callback endpoint. The endpoint should
        take one argument, a flask.ext.micropub.AuthResponse.
        """
        @functools.wraps(f)
        def decorated():
            resp = self._handle_authorize_response()
            return f(resp)
        self._authorized_handler = decorated
        return decorated

    def _handle_authenticate_response(self):
        code = flask.request.args.get('code')
        wrapped_state = flask.request.args.get('state')
        me = flask.request.args.get('me')
        redirect_uri = flask.url_for(flask.request.endpoint, _external=True)

        if wrapped_state and '|' in wrapped_state:
            csrf_token, state = wrapped_state.split('|', 1)
        else:
            csrf_token = state = None

        if not csrf_token:
            return AuthResponse(
                state=state, error='no CSRF token in response')

        if csrf_token != flask.session.get('_micropub_csrf_token'):
            return AuthResponse(
                state=state, error='mismatched CSRF token')

        auth_url = self._discover_endpoints(me)[0]
        if not auth_url:
            auth_url = DEFAULT_AUTH_URL

        # validate the authorization code
        auth_data = {
            'code': code,
            'client_id': self.client_id,
            'redirect_uri': redirect_uri,
            'state': wrapped_state,
        }
        flask.current_app.logger.debug(
            'Flask-Micropub: checking code against auth url: %s, data: %s',
            auth_url, auth_data)
        response = requests.post(auth_url, data=auth_data)
        flask.current_app.logger.debug(
            'Flask-Micropub: auth response: %d - %s', response.status_code,
            response.text)

        rdata = parse_qs(response.text)
        if response.status_code != 200:
            error_vals = rdata.get('error')
            error_descs = rdata.get('error_description')
            return AuthResponse(
                state=state,
                error='authorization failed. {}: {}'.format(
                    error_vals[0] if error_vals else 'Unknown Error',
                    error_descs[0] if error_descs else 'Unknown Error'))

        if 'me' not in rdata:
            return AuthResponse(
                state=state,
                error='missing "me" in response')

        confirmed_me = rdata.get('me')[0]
        return AuthResponse(me=confirmed_me, state=state)

    def _handle_authorize_response(self):
        authenticate_response = self._handle_authenticate_response()
        code = flask.request.args.get('code')
        wrapped_state = flask.request.args.get('state')
        me = flask.request.args.get('me')
        redirect_uri = flask.url_for(flask.request.endpoint, _external=True)

        if authenticate_response.error:
            return authenticate_response

        token_url, micropub_url = self._discover_endpoints(me)[1:]

        if not token_url or not micropub_url:
            # successfully auth'ed user, no micropub endpoint
            return AuthResponse(
                me=authenticate_response.me,
                state=authenticate_response.state,
                error='no micropub endpoint found.')

        # request an access token
        token_data = {
            'code': code,
            'me': authenticate_response.me,
            'redirect_uri': redirect_uri,
            'client_id': self.client_id,
            'state': wrapped_state,
        }
        flask.current_app.logger.debug(
            'Flask-Micropub: requesting access token from: %s, data: %s',
            token_url, token_data)
        token_response = requests.post(token_url, data=token_data)
        flask.current_app.logger.debug(
            'Flask-Micropub: token response: %d - %s',
            token_response.status_code, token_response.text)

        if token_response.status_code != 200:
            return AuthResponse(
                me=authenticate_response.me,
                state=authenticate_response.state,
                error='bad response from token endpoint: {}'
                .format(token_response))

        tdata = parse_qs(token_response.text)
        if 'access_token' not in tdata:
            return AuthResponse(
                me=authenticate_response.me,
                state=authenticate_response.state,
                error='response from token endpoint missing access_token: {}'
                .format(tdata))

        # success!
        access_token = tdata.get('access_token')[0]
        return AuthResponse(
            me=authenticate_response.me,
            micropub_endpoint=micropub_url,
            access_token=access_token,
            state=authenticate_response.state)

    def _discover_endpoints(self, me):
        me_response = requests.get(me)
        if me_response.status_code != 200:
            return None, None, None

        soup = bs4.BeautifulSoup(me_response.text)
        auth_endpoint = soup.find('link', {'rel': 'authorization_endpoint'})
        token_endpoint = soup.find('link', {'rel': 'token_endpoint'})
        micropub_endpoint = soup.find('link', {'rel': 'micropub'})

        return (auth_endpoint and auth_endpoint['href'],
                token_endpoint and token_endpoint['href'],
                micropub_endpoint and micropub_endpoint['href'])

    @staticmethod
    def flask_endpoint_for_function(func):
        for endpt, view_func in flask.current_app.view_functions.items():
            if func == view_func:
                return endpt

class AuthResponse:
    """Authorization response, passed to the authorized_handler endpoint.

    Attributes:
      me (string): The authenticated user's URL. This will be non-None if and
        only if the user was successfully authenticated.
      micropub_endpoint (string): The endpoint to POST micropub requests to.
      access_token (string): The authorized user's micropub access token.
      state (string): The optional state that was passed to authorize.
      error (string): describes the error encountered if any. It is possible
        that the authentication step will succeed but the access token step
        will fail, in which case me will be non-None, and error will describe
        this condition.
    """
    def __init__(self, me=None, micropub_endpoint=None,
                 access_token=None, state=None, error=None):
        self.me = me
        self.micropub_endpoint = micropub_endpoint
        self.access_token = access_token
        self.next_url = self.state = state
        self.error = error
<commit_msg>bugfix: call handle_authenticate_response from authentication_handler<commit_after># -*- coding: utf-8 -*-
"""
    Flask-Micropub
    ==============

    This extension adds the ability to login to a Flask-based website
    using [IndieAuth](https://indiewebcamp.com/IndieAuth), and to request
    an [Micropub](https://indiewebcamp.com/Micropub) access token.
"""

import requests
import bs4
import flask
import functools
import uuid

import sys
if sys.version < '3':
    from urlparse import parse_qs
    from urllib import urlencode
else:
    from urllib.parse import urlencode, parse_qs

DEFAULT_AUTH_URL = 'https://indieauth.com/auth'

class MicropubClient:
    """Flask-Micropub provides support for IndieAuth/Micropub
    authentication and authorization.
    """

    def __init__(self, app=None, client_id=None):
        """Initialize the Micropub extension

        Args:
          app (flask.Flask, optional): the flask application to extend.
          client_id (string, optional): the IndieAuth client id, will be displayed
            when the user is asked to authorize this client.
        """
        self.app = app
        self.client_id = client_id
        if app is not None:
            self.init_app(app, client_id)

    def init_app(self, app, client_id=None):
        """Initialize the Micropub extension if it was not given app
        in the constructor.

        Args:
          app (flask.Flask): the flask application to extend.
          client_id (string, optional): the IndieAuth client id, will be
            displayed when the user is asked to authorize this client. If not
            provided, the app name will be used.
        """
        if not self.client_id:
            if client_id:
                self.client_id = client_id
            else:
                self.client_id = app.name

    def authenticate(self, me, state=None, next_url=None):
        """Authenticate a user via IndieAuth.

        Args:
          me (string): the authing user's URL. if it does not begin with
            https?://, http:// will be prepended.
          state (string, optional): passed through the whole auth process,
            useful if you want to maintain some state, e.g. the starting page
            to return to when auth is complete.
          next_url (string, optional): deprecated and replaced by the more
            general "state". still here for backward compatibility.

        Returns:
          a redirect to the user's specified authorization url, or
          https://indieauth.com/auth if none is provided.
        """
        redirect_url = flask.url_for(
            self.flask_endpoint_for_function(self._authenticated_handler),
            _external=True)
        return self._start_indieauth(me, redirect_url, state or next_url, None)

    def authorize(self, me, state=None, next_url=None, scope='read'):
        """Authorize a user via Micropub.

        Args:
          me (string): the authing user's URL. if it does not begin with
            https?://, http:// will be prepended.
          state (string, optional): passed through the whole auth process,
            useful if you want to maintain some state, e.g. the starting page
            to return to when auth is complete.
          next_url (string, optional): deprecated and replaced by the more
            general "state". still here for backward compatibility.
          scope (string, optional): a space-separated string of micropub
            scopes. 'read' by default.

        Returns:
          a redirect to the user's specified authorization
          https://indieauth.com/auth if none is provided.
        """
        redirect_url = flask.url_for(
            self.flask_endpoint_for_function(self._authorized_handler),
            _external=True)
        return self._start_indieauth(
            me, redirect_url, state or next_url, scope)

    def _start_indieauth(self, me, redirect_url, state, scope):
        """Helper for both authentication and authorization. Kicks off
        IndieAuth by fetching the authorization endpoint from the user's
        homepage and redirecting to it.

        Args:
          me (string): the authing user's URL. if it does not begin with
            https?://, http:// will be prepended.
          redirect_url: the callback URL that we pass to the auth endpoint.
          state (string, optional): passed through the whole auth process,
            useful if you want to maintain some state, e.g. the url to return
            to when the process is complete.
          scope (string): a space-separated string of micropub scopes.

        Returns:
          a redirect to the user's specified authorization
          https://indieauth.com/auth if none is provided.
        """

        if not me.startswith('http://') and not me.startswith('https://'):
            me = 'http://' + me
        auth_url, token_url, micropub_url = self._discover_endpoints(me)
        if not auth_url:
            auth_url = DEFAULT_AUTH_URL

        csrf_token = uuid.uuid4().hex
        flask.session['_micropub_csrf_token'] = csrf_token

        auth_params = {
            'me': me,
            'client_id': self.client_id,
            'redirect_uri': redirect_url,
            'state': '{}|{}'.format(csrf_token, state or ''),
        }
        if scope:
            auth_params['scope'] = scope

        auth_url = auth_url + '?' + urlencode(auth_params)
        flask.current_app.logger.debug('redirecting to %s', auth_url)

        return flask.redirect(auth_url)

    def authenticated_handler(self, f):
        """Decorates the authentication callback endpoint. The endpoint should
        take one argument, a flask.ext.micropub.AuthResponse.
        """
        @functools.wraps(f)
        def decorated():
            resp = self._handle_authenticate_response()
            return f(resp)
        self._authenticated_handler = decorated
        return decorated

    def authorized_handler(self, f):
        """Decorates the authorization callback endpoint. The endpoint should
        take one argument, a flask.ext.micropub.AuthResponse.
        """
        @functools.wraps(f)
        def decorated():
            resp = self._handle_authorize_response()
            return f(resp)
        self._authorized_handler = decorated
        return decorated

    def _handle_authenticate_response(self):
        code = flask.request.args.get('code')
        wrapped_state = flask.request.args.get('state')
        me = flask.request.args.get('me')
        redirect_uri = flask.url_for(flask.request.endpoint, _external=True)

        if wrapped_state and '|' in wrapped_state:
            csrf_token, state = wrapped_state.split('|', 1)
        else:
            csrf_token = state = None

        if not csrf_token:
            return AuthResponse(
                state=state, error='no CSRF token in response')

        if csrf_token != flask.session.get('_micropub_csrf_token'):
            return AuthResponse(
                state=state, error='mismatched CSRF token')

        auth_url = self._discover_endpoints(me)[0]
        if not auth_url:
            auth_url = DEFAULT_AUTH_URL

        # validate the authorization code
        auth_data = {
            'code': code,
            'client_id': self.client_id,
            'redirect_uri': redirect_uri,
            'state': wrapped_state,
        }
        flask.current_app.logger.debug(
            'Flask-Micropub: checking code against auth url: %s, data: %s',
            auth_url, auth_data)
        response = requests.post(auth_url, data=auth_data)
        flask.current_app.logger.debug(
            'Flask-Micropub: auth response: %d - %s', response.status_code,
            response.text)

        rdata = parse_qs(response.text)
        if response.status_code != 200:
            error_vals = rdata.get('error')
            error_descs = rdata.get('error_description')
            return AuthResponse(
                state=state,
                error='authorization failed. {}: {}'.format(
                    error_vals[0] if error_vals else 'Unknown Error',
                    error_descs[0] if error_descs else 'Unknown Error'))

        if 'me' not in rdata:
            return AuthResponse(
                state=state,
                error='missing "me" in response')

        confirmed_me = rdata.get('me')[0]
        return AuthResponse(me=confirmed_me, state=state)

    def _handle_authorize_response(self):
        authenticate_response = self._handle_authenticate_response()
        code = flask.request.args.get('code')
        wrapped_state = flask.request.args.get('state')
        me = flask.request.args.get('me')
        redirect_uri = flask.url_for(flask.request.endpoint, _external=True)

        if authenticate_response.error:
            return authenticate_response

        token_url, micropub_url = self._discover_endpoints(me)[1:]

        if not token_url or not micropub_url:
            # successfully auth'ed user, no micropub endpoint
            return AuthResponse(
                me=authenticate_response.me,
                state=authenticate_response.state,
                error='no micropub endpoint found.')

        # request an access token
        token_data = {
            'code': code,
            'me': authenticate_response.me,
            'redirect_uri': redirect_uri,
            'client_id': self.client_id,
            'state': wrapped_state,
        }
        flask.current_app.logger.debug(
            'Flask-Micropub: requesting access token from: %s, data: %s',
            token_url, token_data)
        token_response = requests.post(token_url, data=token_data)
        flask.current_app.logger.debug(
            'Flask-Micropub: token response: %d - %s',
            token_response.status_code, token_response.text)

        if token_response.status_code != 200:
            return AuthResponse(
                me=authenticate_response.me,
                state=authenticate_response.state,
                error='bad response from token endpoint: {}'
                .format(token_response))

        tdata = parse_qs(token_response.text)
        if 'access_token' not in tdata:
            return AuthResponse(
                me=authenticate_response.me,
                state=authenticate_response.state,
                error='response from token endpoint missing access_token: {}'
                .format(tdata))

        # success!
        access_token = tdata.get('access_token')[0]
        return AuthResponse(
            me=authenticate_response.me,
            micropub_endpoint=micropub_url,
            access_token=access_token,
            state=authenticate_response.state)

    def _discover_endpoints(self, me):
        me_response = requests.get(me)
        if me_response.status_code != 200:
            return None, None, None

        soup = bs4.BeautifulSoup(me_response.text)
        auth_endpoint = soup.find('link', {'rel': 'authorization_endpoint'})
        token_endpoint = soup.find('link', {'rel': 'token_endpoint'})
        micropub_endpoint = soup.find('link', {'rel': 'micropub'})

        return (auth_endpoint and auth_endpoint['href'],
                token_endpoint and token_endpoint['href'],
                micropub_endpoint and micropub_endpoint['href'])

    @staticmethod
    def flask_endpoint_for_function(func):
        for endpt, view_func in flask.current_app.view_functions.items():
            if func == view_func:
                return endpt

class AuthResponse:
    """Authorization response, passed to the authorized_handler endpoint.

    Attributes:
      me (string): The authenticated user's URL. This will be non-None if and
        only if the user was successfully authenticated.
      micropub_endpoint (string): The endpoint to POST micropub requests to.
      access_token (string): The authorized user's micropub access token.
      state (string): The optional state that was passed to authorize.
      error (string): describes the error encountered if any. It is possible
        that the authentication step will succeed but the access token step
        will fail, in which case me will be non-None, and error will describe
        this condition.
    """
    def __init__(self, me=None, micropub_endpoint=None,
                 access_token=None, state=None, error=None):
        self.me = me
        self.micropub_endpoint = micropub_endpoint
        self.access_token = access_token
        self.next_url = self.state = state
        self.error = error
<|endoftext|>


<commit_before>from django.utils import unittest
from django.db.models import Model
from django.forms import Form, ModelForm

from vies import fields
from vies import models

class VIESModel(Model):
    vat = models.VIESField()

class VIESModelForm(ModelForm):
    class Meta:
        model = VIESModel

class VIESForm(Form):
    vat = fields.VIESField()

class ModelTestCase(unittest.TestCase):
    def setUp(self):
        pass

    def test_create(self):
        """Object is correctly created."""
        vies = VIESModel.objects.create(vat='GB802311782')
        self.assertNotEqual(VIESModel.objects.count(), 0)
        self.assertEqual(vies.vat, 'GB802311782')

    def test_save(self):
        """Object is correctly saved."""
        vies_saved = VIESModel()
        vies_saved.vat = 'GB802311782'
        vies_saved.save()

        vies_received = VIESModel.objects.get(pk=vies_saved.pk)
        self.assertNotEqual(VIESModel.objects.count(), 0)
        self.assertEqual(vies_received.vat, 'GB802311782')

class ModelFormTestCase(unittest.TestCase):
    def test_is_valid(self):
        """Form is valid"""
        form = VIESModelForm({'vat_0': 'GB', 'vat_1': '802311782'})
        self.assertTrue(form.is_valid())

        vies = form.save()
        self.assertEqual(vies.vat, 'GB802311782')

    def test_is_not_valid_country(self):
        """Invalid country"""
        form = VIESModelForm({'vat_0': 'xx', 'vat_1': '802311782'})
        self.assertFalse(form.is_valid())

    def test_is_not_valid_numbers(self):
        """Invalid number"""
        form = VIESModelForm({'vat_0': 'GB', 'vat_1': 'xx123+-'})
        self.assertFalse(form.is_valid())

    def test_is_not_valid_numbers(self):
        """Invalid number"""
        form = VIESModelForm({'vat_0': 'GB', 'vat_1': '000000000'})
        self.assertFalse(form.is_valid())

    def test_save(self):
        """Form is valid"""
        form = VIESModelForm({'vat_0': 'GB', 'vat_1': '802311782'})
        vies_saved = form.save()

        vies_received = VIESModel.objects.get(pk=vies_saved.pk)
        self.assertEqual(vies_received, vies_saved)
        self.assertNotEqual(VIESModel.objects.count(), 0)
        self.assertEqual(vies_received.vat, 'GB802311782')<commit_msg>fixes: test shadowing<commit_after>from django.utils import unittest
from django.db.models import Model
from django.forms import Form, ModelForm

from vies import fields
from vies import models

class VIESModel(Model):
    vat = models.VIESField()

class VIESModelForm(ModelForm):
    class Meta:
        model = VIESModel

class VIESForm(Form):
    vat = fields.VIESField()

class ModelTestCase(unittest.TestCase):
    def setUp(self):
        pass

    def test_create(self):
        """Object is correctly created."""
        vies = VIESModel.objects.create(vat='GB802311782')
        self.assertNotEqual(VIESModel.objects.count(), 0)
        self.assertEqual(vies.vat, 'GB802311782')

    def test_save(self):
        """Object is correctly saved."""
        vies_saved = VIESModel()
        vies_saved.vat = 'GB802311782'
        vies_saved.save()

        vies_received = VIESModel.objects.get(pk=vies_saved.pk)
        self.assertNotEqual(VIESModel.objects.count(), 0)
        self.assertEqual(vies_received.vat, 'GB802311782')

class ModelFormTestCase(unittest.TestCase):
    def test_is_valid(self):
        """Form is valid"""
        form = VIESModelForm({'vat_0': 'GB', 'vat_1': '802311782'})
        self.assertTrue(form.is_valid())

        vies = form.save()
        self.assertEqual(vies.vat, 'GB802311782')

    def test_is_not_valid_country(self):
        """Invalid country"""
        form = VIESModelForm({'vat_0': 'xx', 'vat_1': '802311782'})
        self.assertFalse(form.is_valid())

    def test_is_not_valid_numbers(self):
        """Invalid number"""
        form = VIESModelForm({'vat_0': 'GB', 'vat_1': 'xx123+-'})
        self.assertFalse(form.is_valid())

    def test_is_not_valid(self):
        """Invalid number"""
        form = VIESModelForm({'vat_0': 'GB', 'vat_1': '000000000'})
        self.assertFalse(form.is_valid())

    def test_save(self):
        """Form is valid"""
        form = VIESModelForm({'vat_0': 'GB', 'vat_1': '802311782'})
        self.assertFalse(form.is_valid())
        vies_saved = form.save()

        vies_received = VIESModel.objects.get(pk=vies_saved.pk)
        self.assertEqual(vies_received, vies_saved)
        self.assertNotEqual(VIESModel.objects.count(), 0)
        self.assertEqual(vies_received.vat, 'GB802311782')<|endoftext|>


<commit_before># Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cffi

import six
from six.moves import xrange

from cryptography.hazmat.primitives import interfaces

_ffi = cffi.FFI()
_ffi.cdef("""
unsigned int Cryptography_constant_time_lt(unsigned int, unsigned int);
""")
_lib = _ffi.verify("""
/* Returns the value of the input with the most-significant-bit copied to all
   of the bits. This relies on implementation details of computers with 2's
   complement representations of integers, which is not required by the C
   standard. */
static unsigned int Cryptography_DUPLICATE_MSB_TO_ALL(unsigned int a) {
    return (unsigned int)((int)(a) >> (sizeof(int) * 8 - 1));
}

/* This returns 0xFF if a < b else 0x00, but does so in a constant time
   fashion */
unsigned int Cryptography_constant_time_lt(unsigned int a, unsigned int b) {
    a -= b;
    return Cryptography_DUPLICATE_MSB_TO_ALL(a);
}
""")

class PKCS7(object):
    def __init__(self, block_size):
        super(PKCS7, self).__init__()
        if not (0 <= block_size < 256):
            raise ValueError("block_size must be in range(0, 256)")

        if block_size % 8 != 0:
            raise ValueError("block_size must be a multiple of 8")

        self.block_size = block_size

    def padder(self):
        return _PKCS7PaddingContext(self.block_size)

    def unpadder(self):
        return _PKCS7UnpaddingContext(self.block_size)

@interfaces.register(interfaces.PaddingContext)
class _PKCS7PaddingContext(object):
    def __init__(self, block_size):
        super(_PKCS7PaddingContext, self).__init__()
        self.block_size = block_size
        # TODO: O(n ** 2) complexity for repeated concatentation, we should use
        # zero-buffer (#193)
        self._buffer = b""

    def update(self, data):
        if self._buffer is None:
            raise ValueError("Context was already finalized")

        if isinstance(data, six.text_type):
            raise TypeError("Unicode-objects must be encoded before padding")

        self._buffer += data

        finished_blocks = len(self._buffer) // (self.block_size // 8)

        result = self._buffer[:finished_blocks * (self.block_size // 8)]
        self._buffer = self._buffer[finished_blocks * (self.block_size // 8):]

        return result

    def finalize(self):
        if self._buffer is None:
            raise ValueError("Context was already finalized")

        pad_size = self.block_size // 8 - len(self._buffer)
        result = self._buffer + six.int2byte(pad_size) * pad_size
        self._buffer = None
        return result

@interfaces.register(interfaces.PaddingContext)
class _PKCS7UnpaddingContext(object):
    def __init__(self, block_size):
        super(_PKCS7UnpaddingContext, self).__init__()
        self.block_size = block_size
        # TODO: O(n ** 2) complexity for repeated concatentation, we should use
        # zero-buffer (#193)
        self._buffer = b""

    def update(self, data):
        if self._buffer is None:
            raise ValueError("Context was already finalized")

        if isinstance(data, six.text_type):
            raise TypeError("Unicode-objects must be encoded before unpadding")

        self._buffer += data

        finished_blocks = max(
            len(self._buffer) // (self.block_size // 8) - 1,
            0
        )

        result = self._buffer[:finished_blocks * (self.block_size // 8)]
        self._buffer = self._buffer[finished_blocks * (self.block_size // 8):]

        return result

    def finalize(self):
        if self._buffer is None:
            raise ValueError("Context was already finalized")

        if len(self._buffer) != self.block_size // 8:
            raise ValueError("Invalid padding bytes")

        pad_size = six.indexbytes(self._buffer, -1)

        mismatch = 0
        for i in xrange(self.block_size // 8):
            mask = _lib.Cryptography_constant_time_lt(i, pad_size)
            b = six.indexbytes(self._buffer, self.block_size // 8 - 1 - i)
            mismatch |= (mask & (pad_size ^ b))

        if mismatch != 0 or not (0 < pad_size <= self.block_size // 8):
            raise ValueError("Invalid padding bytes")

        res = self._buffer[:-pad_size]
        self._buffer = None
        return res
<commit_msg>Move all unpadding logic to C<commit_after># Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cffi

import six
from six.moves import xrange

from cryptography.hazmat.primitives import interfaces

_ffi = cffi.FFI()
_ffi.cdef("""
bool Cryptography_check_padding(uint8_t *, unsigned int);
""")
_lib = _ffi.verify("""
#include <stdbool.h>

/* Returns the value of the input with the most-significant-bit copied to all
   of the bits. This relies on implementation details of computers with 2's
   complement representations of integers, which is not required by the C
   standard. */
static unsigned int Cryptography_DUPLICATE_MSB_TO_ALL(unsigned int a) {
    return (unsigned int)((int)(a) >> (sizeof(int) * 8 - 1));
}

/* This returns 0xFF if a < b else 0x00, but does so in a constant time
   fashion */
unsigned int Cryptography_constant_time_lt(unsigned int a, unsigned int b) {
    a -= b;
    return Cryptography_DUPLICATE_MSB_TO_ALL(a);
}

bool Cryptography_check_padding(uint8_t *data, unsigned int block_len) {
    unsigned int i;
    uint8_t pad_size = data[block_len - 1];
    uint8_t mismatch = 0;
    for (i = 0; i < block_len; i++) {
        unsigned int mask = Cryptography_constant_time_lt(i, pad_size);
        uint8_t b = data[block_len - 1 - i];
        mismatch |= (mask & (pad_size ^ b));
    }
    return mismatch == 0;
}
""")

class PKCS7(object):
    def __init__(self, block_size):
        super(PKCS7, self).__init__()
        if not (0 <= block_size < 256):
            raise ValueError("block_size must be in range(0, 256)")

        if block_size % 8 != 0:
            raise ValueError("block_size must be a multiple of 8")

        self.block_size = block_size

    def padder(self):
        return _PKCS7PaddingContext(self.block_size)

    def unpadder(self):
        return _PKCS7UnpaddingContext(self.block_size)

@interfaces.register(interfaces.PaddingContext)
class _PKCS7PaddingContext(object):
    def __init__(self, block_size):
        super(_PKCS7PaddingContext, self).__init__()
        self.block_size = block_size
        # TODO: O(n ** 2) complexity for repeated concatentation, we should use
        # zero-buffer (#193)
        self._buffer = b""

    def update(self, data):
        if self._buffer is None:
            raise ValueError("Context was already finalized")

        if isinstance(data, six.text_type):
            raise TypeError("Unicode-objects must be encoded before padding")

        self._buffer += data

        finished_blocks = len(self._buffer) // (self.block_size // 8)

        result = self._buffer[:finished_blocks * (self.block_size // 8)]
        self._buffer = self._buffer[finished_blocks * (self.block_size // 8):]

        return result

    def finalize(self):
        if self._buffer is None:
            raise ValueError("Context was already finalized")

        pad_size = self.block_size // 8 - len(self._buffer)
        result = self._buffer + six.int2byte(pad_size) * pad_size
        self._buffer = None
        return result

@interfaces.register(interfaces.PaddingContext)
class _PKCS7UnpaddingContext(object):
    def __init__(self, block_size):
        super(_PKCS7UnpaddingContext, self).__init__()
        self.block_size = block_size
        # TODO: O(n ** 2) complexity for repeated concatentation, we should use
        # zero-buffer (#193)
        self._buffer = b""

    def update(self, data):
        if self._buffer is None:
            raise ValueError("Context was already finalized")

        if isinstance(data, six.text_type):
            raise TypeError("Unicode-objects must be encoded before unpadding")

        self._buffer += data

        finished_blocks = max(
            len(self._buffer) // (self.block_size // 8) - 1,
            0
        )

        result = self._buffer[:finished_blocks * (self.block_size // 8)]
        self._buffer = self._buffer[finished_blocks * (self.block_size // 8):]

        return result

    def finalize(self):
        if self._buffer is None:
            raise ValueError("Context was already finalized")

        if len(self._buffer) != self.block_size // 8:
            raise ValueError("Invalid padding bytes")

        pad_size = six.indexbytes(self._buffer, -1)

        valid = _lib.Cryptography_check_padding(self._buffer, self.block_size // 8)

        if not valid or not (0 < pad_size <= self.block_size // 8):
            raise ValueError("Invalid padding bytes")

        res = self._buffer[:-pad_size]
        self._buffer = None
        return res
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
"""
    flaskext.dbxml
    ~~~~~~~~~~~~~~

    Wrapper around DB-XML for Flask.

    :copyright: (c) 2011 by Julen Ruiz Aizpuru.
    :license: BSD, see LICENSE for more details.
"""
from __future__ import absolute_import

from flask import _request_ctx_stack, abort, current_app, render_template_string

from werkzeug.utils import cached_property

from bsddb3.db import *
from dbxml import *

import math
import os

def xmlresult(fn):
    """Requires the result passed to be an instance of XmlResults."""
    def wrapper(obj, *args, **kwargs):
        if isinstance(obj.xmlresults, XmlResults):
            return fn(obj, *args, **kwargs)
    return wrapper

class Result(object):

    def __init__(self, xmlresults):
        self.xmlresults = xmlresults
        self.resultset = []
        self.filter = lambda x: x

    def as_str(self):
        self.filter = lambda x: x.asString().decode('utf-8')

        return self

    def as_rendered(self):
        self.filter = lambda x: render_template_string(x.asString()
                                                        .decode('utf-8'))

        return self

    def as_callback(self, fn):
        self.filter = lambda x: fn(x.asString().decode('utf-8'))

        return self

    @xmlresult
    def all(self, first=-1, last=-1):
        for i, xmlresult in enumerate(self.xmlresults):
            if (first == -1 or i >= first) and (last == -1 or i < last):
                self.resultset.append(self.filter(xmlresult))
            else:
                self.resultset.append(None)

        del self.xmlresults

        return self.resultset

    @xmlresult
    def first(self):
        self.xmlresults.reset()

        if self.xmlresults.hasNext():
            self.resultset.append(self.filter(self.xmlresults.next()))

        del self.xmlresults

        try:
            return self.resultset[0]
        except IndexError:
            return None

    @xmlresult
    def first_or_404(self):
        result = self.first()

        if result is None:
            abort(404)

        return result

    @xmlresult
    def paginate(self, page, per_page, error_out=True):

        if error_out and page < 1:
            abort(404)

        offset = (page - 1) * per_page
        last = offset + per_page
        items = self.all(offset, last)[offset:last]

        if not items and page != 1 and error_out:
            abort(404)

        return Pagination(self, page, per_page, len(self.resultset), items)

class DBXML(object):

    def __init__(self):
        self.manager = None
        self.container = None

    def connect(self, app):
        self.env = DBEnv()
        self.env.open(app.config['DBXML_ENV'],
                      DB_CREATE|DB_INIT_LOCK|DB_INIT_LOG| \
                      DB_INIT_MPOOL|DB_INIT_TXN|DB_THREAD|DB_RECOVER, 0)

        self.manager = XmlManager(self.env, DBXML_ALLOW_EXTERNAL_ACCESS)

        self.db = DB(self.env)
        self.db.open(app.config['DBXML_ENV'] + 'seq.db', DB_BTREE,
                     DB_AUTO_COMMIT|DB_CREATE)
        try:
            self.container = self.manager. \
                openContainer(app.config['DBXML_DATABASE'],
                              DB_CREATE|DB_THREAD|DBXML_INDEX_NODES|DBXML_TRANSACTIONAL)

            uc = self.manager.createUpdateContext()
            self.container.setAutoIndexing(False, uc)
        except XmlException:
            self.cleanup()
            raise

    def cleanup(self):
        if hasattr(self, 'container'):
            del self.container
        if hasattr(self, 'manager'):
            del self.manager
        if hasattr(self, 'env'):
            self.env.close(0)
            del self.env

    def init_app(self, app):
        app.config.setdefault('DBXML_DATABASE', 'default.dbxml')

        self.connect(app)

        @app.before_request
        def before_request():
            ctx = _request_ctx_stack.top
            ctx.dbxml = self

        @app.after_request
        def after_request(response):
            ctx = _request_ctx_stack.top
            del ctx.dbxml
            return response

    def get_db(self):
        ctx = _request_ctx_stack.top
        if ctx is not None:
            return ctx.dbxml

    @cached_property
    def collection(self):
        return 'dbxml:///' + current_app.config['DBXML_DATABASE']

    def init_dbxml(self, filename=None, docname=None):
        if filename is None:
            return

        filename = os.path.abspath(filename)

        update_context = self.manager.createUpdateContext()
        txn = self.manager.createTransaction()

        if docname is None:
            docname = os.path.basename(filename)

        xml_input = self.manager.createLocalFileInputStream(filename)

        try:
            self.container.putDocument(txn, docname, xml_input, update_context)
            txn.commit()
            print 'Document added successfully.'
        except XmlUniqueError:
            print 'Document already in container. Skipping.'
        except XmlException:
            txn.abort()
            print 'Transaction failed. Aborting.'

    def add_indexes(self, indexes):
        """Programatically adds new indexes to the container.

        `indexes` is a list of tuples which contains three elements:
        namespace, the element to be indexed, and the index string.

        It's highly recommended to perform this action right after creating
        the container. Otherwise, containers need to be reindexed and
        depending on the size of the current container, it can be a very
        expensive operation.
        """

        txn = self.manager.createTransaction()
        uc = self.manager.createUpdateContext()
        index_spec = self.container.getIndexSpecification()

        for (ns, element, index_string) in indexes:
            index_spec.addIndex(ns, element, index_string)

        try:
            self.container.setIndexSpecification(txn, index_spec, uc)
            txn.commit()
            print 'Indexes added successfully.'
        except XmlException:
            txn.abort()
            print 'Failed to add new indexes.'

    def generate_id(self, key):
        seq = DBSequence(self.db)
        seq.open(key, txn=None, flags=DB_CREATE)

        return seq.get()

    def query(self, query_string, context={}):
        query_string = query_string.encode('utf-8')

        query = "collection('{0}'){1}".format(self.collection, query_string)

        return self.raw_query(query, context)

    def template_query(self, template_name, context={}):

        # Open the template source, and pass it as the XQuery query
        jinja_env = current_app.jinja_env
        (query, filename, uptodate) = jinja_env.loader \
            .get_source(jinja_env, template_name)
        query = str(query.encode('utf-8'))

        return self.raw_query(query, context)

    def raw_query(self, query, context={}):

        def _encoded_xml_value(val):

            if isinstance(val, unicode):
                newval = val.encode('utf-8')
            else:
                newval = str(val)

            return XmlValue(newval)

        def _populate_context(qc, ctx):
            for key, value in ctx.iteritems():
                if value is None:
                    continue

                if isinstance(value, dict):
                    _populate_context(qc, value)
                elif isinstance(value, list):
                    for val in value:
                        newval = self.manager.createResults()
                        newval.add(_encoded_xml_value(val))
                else:
                    newval = _encoded_xml_value(value)

                qc.setVariableValue(key, newval)

        context.update({'collection': self.collection})

        query_context = self.manager.createQueryContext()
        query_context.setEvaluationType(query_context.Lazy)

        query_context.setBaseURI(current_app.config['DBXML_BASE_URI'])

        _populate_context(query_context, context)

        query_expression = self.manager.prepare(query, query_context)

        try:
            result = query_expression.execute(query_context)
        except XmlException:
            result = []
        finally:
            del query_context
            del query_expression

        return Result(result)

    def insert_before(self, xml, where):
        query = u'insert nodes {0} before collection("{1}"){2}'. \
                format(xml, self.collection, where).encode('utf-8')

        return self.insert_raw(query)

    def insert_after(self, xml, where):
        query = u'insert nodes {0} after collection("{1}"){2}'. \
                format(xml, self.collection, where).encode('utf-8')

        return self.insert_raw(query)

    def insert_as_first(self, xml, where):
        query = u'insert nodes {0} as first into collection("{1}"){2}'. \
                format(xml, self.collection, where).encode('utf-8')

        return self.insert_raw(query)

    def insert_as_last(self, xml, where):
        query = u'insert nodes {0} as last into collection("{1}"){2}'. \
                format(xml, self.collection, where).encode('utf-8')

        return self.insert_raw(query)

    def replace(self, old, new):
        query = u'replace node collection("{0}"){1} with {2}'. \
                format(self.collection, old, new).encode('utf-8')

        return self.insert_raw(query)

    def replace_value(self, old, new):
        query = u'replace value of node collection("{0}"){1} with "{2}"'. \
                format(self.collection, old, new).encode('utf-8')

        return self.insert_raw(query)

    def insert_raw(self, query):

        query_context = self.manager.createQueryContext()
        query_context.setEvaluationType(query_context.Lazy)

        txn = self.manager.createTransaction()

        try:
            result = self.manager.query(txn, query, query_context)
            txn.commit()
            return True
        except XmlException, e:
            txn.abort()
            result = []
            return False
        finally:
            del query_context
            del result

class Pagination(object):

    def __init__(self, queryset, page, per_page, total, items):

        self.queryset = queryset
        self.page = page
        self.per_page = per_page
        self.total = total
        self.items = items

    @property
    def pages(self):
        """The total number of pages"""
        return int(math.ceil(self.total / float(self.per_page)))

    def prev(self, error_out=False):
        """Returns a :class:`Pagination` object for the previous page."""
        assert self.queryset is not None, 'a query object is required ' \
                                       'for this method to work'
        return self.queryset.paginate(self.page - 1, self.per_page, error_out)

    @property
    def prev_num(self):
        """Number of the previous page."""
        return self.page - 1

    @property
    def has_prev(self):
        """True if a previous page exists"""
        return self.page > 1

    def next(self, error_out=False):
        """Returns a :class:`Pagination` object for the next page."""
        assert self.queryset is not None, 'a query object is required ' \
                                       'for this method to work'
        return self.queryset.paginate(self.page + 1, self.per_page, error_out)

    @property
    def has_next(self):
        """True if a next page exists."""
        return self.page < self.pages

    @property
    def next_num(self):
        """Number of the next page"""
        return self.page + 1

    def iter_pages(self, left_edge=2, left_current=2,
                   right_current=5, right_edge=2):
        """Iterates over the page numbers in the pagination.  The four
        parameters control the thresholds how many numbers should be produced
        from the sides.  Skipped page numbers are represented as `None`.
        This is how you could render such a pagination in the templates:

        .. sourcecode:: html+jinja

            {% macro render_pagination(pagination, endpoint) %}

              {%- for page in pagination.iter_pages() %}
                {% if page %}
                  {% if page != pagination.page %}
                    {{ page }}
                  {% else %}
                    {{ page }}
                  {% endif %}
                {% else %}
                  …
                {% endif %}
              {%- endfor %}

            {% endmacro %}
        """
        last = 0
        for num in xrange(1, self.pages + 1):
            if num <= left_edge or \
               (num > self.page - left_current - 1 and \
                num < self.page + right_current) or \
               num > self.pages - right_edge:
                if last + 1 != num:
                    yield None
                yield num
                last = num
<commit_msg>Allow specifying document name in queries.<commit_after># -*- coding: utf-8 -*-
"""
    flaskext.dbxml
    ~~~~~~~~~~~~~~

    Wrapper around DB-XML for Flask.

    :copyright: (c) 2011 by Julen Ruiz Aizpuru.
    :license: BSD, see LICENSE for more details.
"""
from __future__ import absolute_import

from flask import _request_ctx_stack, abort, current_app, render_template_string

from werkzeug.utils import cached_property

from bsddb3.db import *
from dbxml import *

import math
import os

def xmlresult(fn):
    """Requires the result passed to be an instance of XmlResults."""
    def wrapper(obj, *args, **kwargs):
        if isinstance(obj.xmlresults, XmlResults):
            return fn(obj, *args, **kwargs)
    return wrapper

class Result(object):

    def __init__(self, xmlresults):
        self.xmlresults = xmlresults
        self.resultset = []
        self.filter = lambda x: x

    def as_str(self):
        self.filter = lambda x: x.asString().decode('utf-8')

        return self

    def as_rendered(self):
        self.filter = lambda x: render_template_string(x.asString()
                                                        .decode('utf-8'))

        return self

    def as_callback(self, fn):
        self.filter = lambda x: fn(x.asString().decode('utf-8'))

        return self

    @xmlresult
    def all(self, first=-1, last=-1):
        for i, xmlresult in enumerate(self.xmlresults):
            if (first == -1 or i >= first) and (last == -1 or i < last):
                self.resultset.append(self.filter(xmlresult))
            else:
                self.resultset.append(None)

        del self.xmlresults

        return self.resultset

    @xmlresult
    def first(self):
        self.xmlresults.reset()

        if self.xmlresults.hasNext():
            self.resultset.append(self.filter(self.xmlresults.next()))

        del self.xmlresults

        try:
            return self.resultset[0]
        except IndexError:
            return None

    @xmlresult
    def first_or_404(self):
        result = self.first()

        if result is None:
            abort(404)

        return result

    @xmlresult
    def paginate(self, page, per_page, error_out=True):

        if error_out and page < 1:
            abort(404)

        offset = (page - 1) * per_page
        last = offset + per_page
        items = self.all(offset, last)[offset:last]

        if not items and page != 1 and error_out:
            abort(404)

        return Pagination(self, page, per_page, len(self.resultset), items)

class DBXML(object):

    def __init__(self):
        self.manager = None
        self.container = None

    def connect(self, app):
        self.env = DBEnv()
        self.env.open(app.config['DBXML_ENV'],
                      DB_CREATE|DB_INIT_LOCK|DB_INIT_LOG| \
                      DB_INIT_MPOOL|DB_INIT_TXN|DB_THREAD|DB_RECOVER, 0)

        self.manager = XmlManager(self.env, DBXML_ALLOW_EXTERNAL_ACCESS)

        self.db = DB(self.env)
        self.db.open(app.config['DBXML_ENV'] + 'seq.db', DB_BTREE,
                     DB_AUTO_COMMIT|DB_CREATE)
        try:
            self.container = self.manager. \
                openContainer(app.config['DBXML_DATABASE'],
                              DB_CREATE|DB_THREAD|DBXML_INDEX_NODES|DBXML_TRANSACTIONAL)

            uc = self.manager.createUpdateContext()
            self.container.setAutoIndexing(False, uc)
        except XmlException:
            self.cleanup()
            raise

    def cleanup(self):
        if hasattr(self, 'container'):
            del self.container
        if hasattr(self, 'manager'):
            del self.manager
        if hasattr(self, 'env'):
            self.env.close(0)
            del self.env

    def init_app(self, app):
        app.config.setdefault('DBXML_DATABASE', 'default.dbxml')

        self.connect(app)

        @app.before_request
        def before_request():
            ctx = _request_ctx_stack.top
            ctx.dbxml = self

        @app.after_request
        def after_request(response):
            ctx = _request_ctx_stack.top
            del ctx.dbxml
            return response

    def get_db(self):
        ctx = _request_ctx_stack.top
        if ctx is not None:
            return ctx.dbxml

    @cached_property
    def collection(self):
        return 'dbxml:///' + current_app.config['DBXML_DATABASE']

    def init_dbxml(self, filename=None, docname=None):
        if filename is None:
            return

        filename = os.path.abspath(filename)

        update_context = self.manager.createUpdateContext()
        txn = self.manager.createTransaction()

        if docname is None:
            docname = os.path.basename(filename)

        xml_input = self.manager.createLocalFileInputStream(filename)

        try:
            self.container.putDocument(txn, docname, xml_input, update_context)
            txn.commit()
            print 'Document added successfully.'
        except XmlUniqueError:
            print 'Document already in container. Skipping.'
        except XmlException:
            txn.abort()
            print 'Transaction failed. Aborting.'

    def add_indexes(self, indexes):
        """Programatically adds new indexes to the container.

        `indexes` is a list of tuples which contains three elements:
        namespace, the element to be indexed, and the index string.

        It's highly recommended to perform this action right after creating
        the container. Otherwise, containers need to be reindexed and
        depending on the size of the current container, it can be a very
        expensive operation.
        """

        txn = self.manager.createTransaction()
        uc = self.manager.createUpdateContext()
        index_spec = self.container.getIndexSpecification()

        for (ns, element, index_string) in indexes:
            index_spec.addIndex(ns, element, index_string)

        try:
            self.container.setIndexSpecification(txn, index_spec, uc)
            txn.commit()
            print 'Indexes added successfully.'
        except XmlException:
            txn.abort()
            print 'Failed to add new indexes.'

    def generate_id(self, key):
        seq = DBSequence(self.db)
        seq.open(key, txn=None, flags=DB_CREATE)

        return seq.get()

    def query(self, query_string, context={}, document=None):
        query_string = query_string.encode('utf-8')

        if document:
            query = u"document('{0}'){1}".format(document,
                                                 query_string)
        else:
            query = u"collection('{0}'){1}".format(self.collection,
                                                   query_string)

        return self.raw_query(query.encode('utf-8'), context)

    def template_query(self, template_name, context={}):

        # Open the template source, and pass it as the XQuery query
        jinja_env = current_app.jinja_env
        (query, filename, uptodate) = jinja_env.loader \
            .get_source(jinja_env, template_name)
        query = str(query.encode('utf-8'))

        return self.raw_query(query, context)

    def raw_query(self, query, context={}):

        def _encoded_xml_value(val):

            if isinstance(val, unicode):
                newval = val.encode('utf-8')
            else:
                newval = str(val)

            return XmlValue(newval)

        def _populate_context(qc, ctx):
            for key, value in ctx.iteritems():
                if value is None:
                    continue

                if isinstance(value, dict):
                    _populate_context(qc, value)
                elif isinstance(value, list):
                    for val in value:
                        newval = self.manager.createResults()
                        newval.add(_encoded_xml_value(val))
                else:
                    newval = _encoded_xml_value(value)

                qc.setVariableValue(key, newval)

        context.update({'collection': self.collection})

        query_context = self.manager.createQueryContext()
        query_context.setEvaluationType(query_context.Lazy)

        query_context.setBaseURI(current_app.config['DBXML_BASE_URI'])

        _populate_context(query_context, context)

        query_expression = self.manager.prepare(query, query_context)

        try:
            result = query_expression.execute(query_context)
        except XmlException:
            result = []
        finally:
            del query_context
            del query_expression

        return Result(result)

    def insert_before(self, xml, where, document=None):

        if document:
            query = u'insert nodes {0} before document("{1}"){2}'. \
                    format(xml, document, where)
        else:
            query = u'insert nodes {0} before collection("{1}"){2}'. \
                    format(xml, self.collection, where)

        return self.insert_raw(query.encode('utf-8'))

    def insert_after(self, xml, where, document=None):

        if document:
            query = u'insert nodes {0} after document("{1}"){2}'. \
                    format(xml, document, where)
        else:
            query = u'insert nodes {0} after collection("{1}"){2}'. \
                    format(xml, self.collection, where)

        return self.insert_raw(query.encode('utf-8'))

    def insert_as_first(self, xml, where, document=None):

        if document:
            query = u'insert nodes {0} as first into document("{1}"){2}'. \
                    format(xml, document, where)
        else:
            query = u'insert nodes {0} as first into collection("{1}"){2}'. \
                    format(xml, self.collection, where)

        return self.insert_raw(query.encode('utf-8'))

    def insert_as_last(self, xml, where, document=None):

        if document:
            query = u'insert nodes {0} as last into document("{1}"){2}'. \
                    format(xml, document, where)
        else:
            query = u'insert nodes {0} as last into collection("{1}"){2}'. \
                    format(xml, self.collection, where)

        return self.insert_raw(query.encode('utf-8'))

    def replace(self, old, new, document=None):

        if document:
            query = u'replace node document("{0}"){1} with {2}'. \
                    format(document, old, new)
        else:
            query = u'replace node collection("{0}"){1} with {2}'. \
                    format(self.collection, old, new)

        return self.insert_raw(query.encode('utf-8'))

    def replace_value(self, old, new, document=None):

        if document:
            query = u'replace value of node document("{0}"){1} with "{2}"'. \
                    format(document, old, new)
        else:
            query = u'replace value of node collection("{0}"){1} with "{2}"'. \
                    format(self.collection, old, new)

        return self.insert_raw(query.encode('utf-8'))

    def insert_raw(self, query):

        query_context = self.manager.createQueryContext()
        query_context.setEvaluationType(query_context.Lazy)

        txn = self.manager.createTransaction()

        try:
            result = self.manager.query(txn, query, query_context)
            txn.commit()
            return True
        except XmlException, e:
            txn.abort()
            result = []
            return False
        finally:
            del query_context
            del result

class Pagination(object):

    def __init__(self, queryset, page, per_page, total, items):

        self.queryset = queryset
        self.page = page
        self.per_page = per_page
        self.total = total
        self.items = items

    @property
    def pages(self):
        """The total number of pages"""
        return int(math.ceil(self.total / float(self.per_page)))

    def prev(self, error_out=False):
        """Returns a :class:`Pagination` object for the previous page."""
        assert self.queryset is not None, 'a query object is required ' \
                                       'for this method to work'
        return self.queryset.paginate(self.page - 1, self.per_page, error_out)

    @property
    def prev_num(self):
        """Number of the previous page."""
        return self.page - 1

    @property
    def has_prev(self):
        """True if a previous page exists"""
        return self.page > 1

    def next(self, error_out=False):
        """Returns a :class:`Pagination` object for the next page."""
        assert self.queryset is not None, 'a query object is required ' \
                                       'for this method to work'
        return self.queryset.paginate(self.page + 1, self.per_page, error_out)

    @property
    def has_next(self):
        """True if a next page exists."""
        return self.page < self.pages

    @property
    def next_num(self):
        """Number of the next page"""
        return self.page + 1

    def iter_pages(self, left_edge=2, left_current=2,
                   right_current=5, right_edge=2):
        """Iterates over the page numbers in the pagination.  The four
        parameters control the thresholds how many numbers should be produced
        from the sides.  Skipped page numbers are represented as `None`.
        This is how you could render such a pagination in the templates:

        .. sourcecode:: html+jinja

            {% macro render_pagination(pagination, endpoint) %}

              {%- for page in pagination.iter_pages() %}
                {% if page %}
                  {% if page != pagination.page %}
                    {{ page }}
                  {% else %}
                    {{ page }}
                  {% endif %}
                {% else %}
                  …
                {% endif %}
              {%- endfor %}

            {% endmacro %}
        """
        last = 0
        for num in xrange(1, self.pages + 1):
            if num <= left_edge or \
               (num > self.page - left_current - 1 and \
                num < self.page + right_current) or \
               num > self.pages - right_edge:
                if last + 1 != num:
                    yield None
                yield num
                last = num
<|endoftext|>