Example #1
0
    def test_signal_sent(self, mock):
        """
        Test that the subscription signal was sent

        Based on http://stackoverflow.com/questions/3817213/
        """
        # pylint: disable=attribute-defined-outside-init, unused-variable
        responsemock = Mock()
        responsemock.read.return_value = 'Return Value'
        mock.return_value = responsemock
        notification = loader('subscriptionconfirmation')
        self.signal_count = 0

        @receiver(signals.subscription)
        def _signal_receiver(sender, **kwargs):
            """Signal Test Receiver"""
            self.signal_count += 1
            self.signal_sender = sender
            self.signal_notification = kwargs['notification']
            self.signal_result = kwargs['result']

        response = utils.approve_subscription(notification)

        self.assertEqual(response.content.decode('ascii'), 'Return Value')
        self.assertEqual(self.signal_count, 1)
        self.assertEqual(self.signal_result, 'Return Value')
        self.assertEqual(self.signal_notification, notification)
Example #2
0
    def test_approve_subscription(self, mock):
        """Test the subscription approval mechanism"""
        responsemock = Mock()
        responsemock.read.return_value = 'Return Value'
        mock.return_value = responsemock
        notification = loader('subscriptionconfirmation')

        response = utils.approve_subscription(notification)

        mock.assert_called_with(notification['SubscribeURL'])
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.content.decode('ascii'), 'Return Value')
Example #3
0
    def test_approve_subscription(self, mock):
        """Test the subscription approval mechanism"""
        responsemock = Mock()
        responsemock.read.return_value = 'Return Value'
        mock.return_value = responsemock
        notification = loader('subscriptionconfirmation')

        response = utils.approve_subscription(notification)

        mock.assert_called_with(notification['SubscribeURL'])
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.content.decode('ascii'), 'Return Value')
Example #4
0
    def test_bad_url(self):
        """Test to make sure an invalid URL isn't requested by our system"""
        old_setting = getattr(settings, 'BOUNCY_SUBSCRIBE_DOMAIN_REGEX', None)
        settings.BOUNCY_SUBSCRIBE_DOMAIN_REGEX = \
            r"sns.[a-z0-9\-]+.amazonaws.com$"
        notification = loader('bounce_notification')
        notification['SubscribeURL'] = 'http://bucket.s3.amazonaws.com'
        result = utils.approve_subscription(notification)

        self.assertEqual(result.status_code, 400)
        self.assertEqual(
            result.content.decode('ascii'), 'Improper Subscription Domain')

        if old_setting is not None:
            settings.BOUNCY_SUBSCRIBE_DOMAIN_REGEX = old_setting
Example #5
0
    def test_bad_url(self):
        """Test to make sure an invalid URL isn't requested by our system"""
        old_setting = getattr(settings, 'BOUNCY_SUBSCRIBE_DOMAIN_REGEX', None)
        settings.BOUNCY_SUBSCRIBE_DOMAIN_REGEX = \
            r"sns.[a-z0-9\-]+.amazonaws.com$"
        notification = loader('bounce_notification')
        notification['SubscribeURL'] = 'http://bucket.s3.amazonaws.com'
        result = utils.approve_subscription(notification)

        self.assertEqual(result.status_code, 400)
        self.assertEqual(result.content.decode('ascii'),
                         'Improper Subscription Domain')

        if old_setting is not None:
            settings.BOUNCY_SUBSCRIBE_DOMAIN_REGEX = old_setting
Example #6
0
def endpoint(request):
    """Endpoint that SNS accesses. Includes logic verifying request"""
    # pylint: disable=too-many-return-statements,too-many-branches

    # In order to 'hide' the endpoint, all non-POST requests should return
    # the site's default HTTP404
    if request.method != 'POST':
        raise Http404

    # If necessary, check that the topic is correct
    if hasattr(settings, 'BOUNCY_TOPIC_ARN'):
        # Confirm that the proper topic header was sent
        if 'HTTP_X_AMZ_SNS_TOPIC_ARN' not in request.META:
            return HttpResponseBadRequest('No TopicArn Header')

        # Check to see if the topic is in the settings
        # Because you can have bounces and complaints coming from multiple
        # topics, BOUNCY_TOPIC_ARN is a list
        if (not request.META['HTTP_X_AMZ_SNS_TOPIC_ARN']
                in settings.BOUNCY_TOPIC_ARN):
            return HttpResponseBadRequest('Bad Topic')

    # Load the JSON POST Body
    if isinstance(request.body, str):
        # requests return str in python 2.7
        request_body = request.body
    else:
        # and return bytes in python 3.4
        request_body = request.body.decode()
    try:
        data = json.loads(request_body)
    except ValueError:
        logger.warning('Notification Not Valid JSON: {}'.format(request_body))
        return HttpResponseBadRequest('Not Valid JSON')

    # Ensure that the JSON we're provided contains all the keys we expect
    # Comparison code from http://stackoverflow.com/questions/1285911/
    if not set(VITAL_NOTIFICATION_FIELDS) <= set(data):
        logger.warning('Request Missing Necessary Keys')
        return HttpResponseBadRequest('Request Missing Necessary Keys')

    # Ensure that the type of notification is one we'll accept
    if not data['Type'] in ALLOWED_TYPES:
        logger.info('Notification Type Not Known %s', data['Type'])
        return HttpResponseBadRequest('Unknown Notification Type')

    # Confirm that the signing certificate is hosted on a correct domain
    # AWS by default uses sns.{region}.amazonaws.com
    # On the off chance you need this to be a different domain, allow the
    # regex to be overridden in settings
    domain = urlparse(data['SigningCertURL']).netloc
    pattern = getattr(settings, 'BOUNCY_CERT_DOMAIN_REGEX',
                      r"sns.[a-z0-9\-]+.amazonaws.com$")
    if not re.search(pattern, domain):
        logger.warning('Improper Certificate Location %s',
                       data['SigningCertURL'])
        return HttpResponseBadRequest('Improper Certificate Location')

    # Verify that the notification is signed by Amazon
    if (getattr(settings, 'BOUNCY_VERIFY_CERTIFICATE', True)
            and not verify_notification(data)):
        logger.error('Verification Failure %s', )
        return HttpResponseBadRequest('Improper Signature')

    # Send a signal to say a valid notification has been received
    signals.notification.send(sender='bouncy_endpoint',
                              notification=data,
                              request=request)

    # Handle subscription-based messages.
    if data['Type'] == 'SubscriptionConfirmation':
        # Allow the disabling of the auto-subscription feature
        if not getattr(settings, 'BOUNCY_AUTO_SUBSCRIBE', True):
            raise Http404
        return approve_subscription(data)
    elif data['Type'] == 'UnsubscribeConfirmation':
        # We won't handle unsubscribe requests here. Return a 200 status code
        # so Amazon won't redeliver the request. If you want to remove this
        # endpoint, remove it either via the API or the AWS Console
        logger.info('UnsubscribeConfirmation Not Handled')
        return HttpResponse('UnsubscribeConfirmation Not Handled')

    try:
        message = json.loads(data['Message'])
    except ValueError:
        # This message is not JSON. But we need to return a 200 status code
        # so that Amazon doesn't attempt to deliver the message again
        logger.info('Non-Valid JSON Message Received')
        return HttpResponse('Message is not valid JSON')

    return process_message(message, data)
Example #7
0
def endpoint(request):
    """Endpoint that SNS accesses. Includes logic verifying request"""
    # pylint: disable=too-many-return-statements,too-many-branches

    # In order to 'hide' the endpoint, all non-POST requests should return
    # the site's default HTTP404
    if request.method != 'POST':
        raise Http404

    # If necessary, check that the topic is correct
    if hasattr(settings, 'BOUNCY_TOPIC_ARN'):
        # Confirm that the proper topic header was sent
        if 'HTTP_X_AMZ_SNS_TOPIC_ARN' not in request.META:
            return HttpResponseBadRequest('No TopicArn Header')

        # Check to see if the topic is in the settings
        # Because you can have bounces and complaints coming from multiple
        # topics, BOUNCY_TOPIC_ARN is a list
        if (not request.META['HTTP_X_AMZ_SNS_TOPIC_ARN']
                in settings.BOUNCY_TOPIC_ARN):
            return HttpResponseBadRequest('Bad Topic')

    # Load the JSON POST Body
    if isinstance(request.body, str):
        # requests return str in python 2.7
        request_body = request.body
    else:
        # and return bytes in python 3.4
        request_body = request.body.decode()
    try:
        data = json.loads(request_body)
    except ValueError:
        logger.warning('Notification Not Valid JSON: {}'.format(request_body))
        return HttpResponseBadRequest('Not Valid JSON')

    # Ensure that the JSON we're provided contains all the keys we expect
    # Comparison code from http://stackoverflow.com/questions/1285911/
    if not set(VITAL_NOTIFICATION_FIELDS) <= set(data):
        logger.warning('Request Missing Necessary Keys')
        return HttpResponseBadRequest('Request Missing Necessary Keys')

    # Ensure that the type of notification is one we'll accept
    if not data['Type'] in ALLOWED_TYPES:
        logger.info('Notification Type Not Known %s', data['Type'])
        return HttpResponseBadRequest('Unknown Notification Type')

    # Confirm that the signing certificate is hosted on a correct domain
    # AWS by default uses sns.{region}.amazonaws.com
    # On the off chance you need this to be a different domain, allow the
    # regex to be overridden in settings
    domain = urlparse(data['SigningCertURL']).netloc
    pattern = getattr(
        settings, 'BOUNCY_CERT_DOMAIN_REGEX', r"sns.[a-z0-9\-]+.amazonaws.com$"
    )
    if not re.search(pattern, domain):
        logger.warning(
            'Improper Certificate Location %s', data['SigningCertURL'])
        return HttpResponseBadRequest('Improper Certificate Location')

    # Verify that the notification is signed by Amazon
    if (getattr(settings, 'BOUNCY_VERIFY_CERTIFICATE', True)
            and not verify_notification(data)):
        logger.error('Verification Failure %s', )
        return HttpResponseBadRequest('Improper Signature')

    # Send a signal to say a valid notification has been received
    signals.notification.send(
        sender='bouncy_endpoint', notification=data, request=request)

    # Handle subscription-based messages.
    if data['Type'] == 'SubscriptionConfirmation':
        # Allow the disabling of the auto-subscription feature
        if not getattr(settings, 'BOUNCY_AUTO_SUBSCRIBE', True):
            raise Http404
        return approve_subscription(data)
    elif data['Type'] == 'UnsubscribeConfirmation':
        # We won't handle unsubscribe requests here. Return a 200 status code
        # so Amazon won't redeliver the request. If you want to remove this
        # endpoint, remove it either via the API or the AWS Console
        logger.info('UnsubscribeConfirmation Not Handled')
        return HttpResponse('UnsubscribeConfirmation Not Handled')

    try:
        message = json.loads(data['Message'])
    except ValueError:
        # This message is not JSON. But we need to return a 200 status code
        # so that Amazon doesn't attempt to deliver the message again
        logger.info('Non-Valid JSON Message Received')
        return HttpResponse('Message is not valid JSON')

    return process_message(message, data)