mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Merge pull request #39 from ProReNata/master
Added support for signed webhooks
This commit is contained in:
11
djrill/compat.py
Normal file
11
djrill/compat.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# For python 3 compatibility, see http://python3porting.com/problems.html#nicer-solutions
|
||||
import sys
|
||||
|
||||
if sys.version < '3':
|
||||
def b(x):
|
||||
return x
|
||||
else:
|
||||
import codecs
|
||||
|
||||
def b(x):
|
||||
return codecs.latin_1_encode(x)[0]
|
||||
@@ -1,9 +1,13 @@
|
||||
from base64 import b64encode
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
|
||||
from ..compat import b
|
||||
from ..signals import webhook_event
|
||||
|
||||
|
||||
@@ -39,6 +43,37 @@ class DjrillWebhookSecretMixinTests(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class DjrillWebhookSignatureMixinTests(TestCase):
|
||||
"""
|
||||
Test mixin used in optional Mandrill webhook signature support
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
settings.DJRILL_WEBHOOK_SECRET = 'abc123'
|
||||
settings.DJRILL_WEBHOOK_SIGNATURE_KEY = "signature"
|
||||
settings.DJRILL_WEBHOOK_URL = "/webhook/?secret=abc123"
|
||||
|
||||
def test_incorrect_settings(self):
|
||||
del settings.DJRILL_WEBHOOK_URL
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
self.client.post('/webhook/?secret=abc123')
|
||||
settings.DJRILL_WEBHOOK_URL = "/webhook/?secret=abc123"
|
||||
|
||||
def test_unauthorized(self):
|
||||
settings.DJRILL_WEBHOOK_SIGNATURE_KEY = "anothersignature"
|
||||
response = self.client.post(settings.DJRILL_WEBHOOK_URL)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_signature(self):
|
||||
signature = hmac.new(key=b(settings.DJRILL_WEBHOOK_SIGNATURE_KEY), msg = b(settings.DJRILL_WEBHOOK_URL+"mandrill_events[]"), digestmod=hashlib.sha1)
|
||||
hash_string = b64encode(signature.digest())
|
||||
response = self.client.post('/webhook/?secret=abc123', data={"mandrill_events":"[]"}, **{"HTTP_X_MANDRILL_SIGNATURE" : hash_string})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def tearDown(self):
|
||||
del settings.DJRILL_WEBHOOK_SIGNATURE_KEY
|
||||
del settings.DJRILL_WEBHOOK_URL
|
||||
|
||||
class DjrillWebhookViewTests(TestCase):
|
||||
"""
|
||||
Test optional Mandrill webhook view
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from base64 import b64encode
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -12,6 +14,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
import requests
|
||||
|
||||
from djrill import MANDRILL_API_URL, signals
|
||||
from .compat import b
|
||||
|
||||
|
||||
class DjrillAdminMedia(object):
|
||||
@@ -101,6 +104,41 @@ class DjrillWebhookSecretMixin(object):
|
||||
request, *args, **kwargs)
|
||||
|
||||
|
||||
class DjrillWebhookSignatureMixin(object):
|
||||
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
signature_key = getattr(settings, 'DJRILL_WEBHOOK_SIGNATURE_KEY', None)
|
||||
|
||||
if signature_key and request.method == "POST":
|
||||
|
||||
# Make webhook url an explicit setting to make sure that this is the exact same string
|
||||
# that the user entered in Mandrill
|
||||
post_string = getattr(settings, "DJRILL_WEBHOOK_URL", None)
|
||||
if post_string is None:
|
||||
raise ImproperlyConfigured(
|
||||
"You have set DJRILL_WEBHOOK_SIGNATURE_KEY, but haven't set DJRILL_WEBHOOK_URL in the settings file.")
|
||||
|
||||
signature = request.META.get("HTTP_X_MANDRILL_SIGNATURE", None)
|
||||
if not signature:
|
||||
return HttpResponse(status=403, content="X-Mandrill-Signature not set")
|
||||
|
||||
# The querydict is a bit special, see https://docs.djangoproject.com/en/dev/ref/request-response/#querydict-objects
|
||||
# Mandrill needs it to be sorted and added to the hash
|
||||
post_lists = sorted(request.POST.lists())
|
||||
for value_list in post_lists:
|
||||
for item in value_list[1]:
|
||||
post_string += "%s%s" % (value_list[0], item)
|
||||
|
||||
hash_string = b64encode(hmac.new(key=b(signature_key), msg=b(post_string), digestmod=hashlib.sha1).digest())
|
||||
if signature != hash_string:
|
||||
return HttpResponse(status=403, content="Signature doesn't match")
|
||||
|
||||
return super(DjrillWebhookSignatureMixin, self).dispatch(
|
||||
request, *args, **kwargs)
|
||||
|
||||
|
||||
class DjrillIndexView(DjrillApiMixin, TemplateView):
|
||||
template_name = "djrill/status.html"
|
||||
|
||||
@@ -161,7 +199,7 @@ class DjrillUrlListView(DjrillAdminMedia, DjrillApiMixin,
|
||||
return self.render_to_response(context)
|
||||
|
||||
|
||||
class DjrillWebhookView(DjrillWebhookSecretMixin, View):
|
||||
class DjrillWebhookView(DjrillWebhookSecretMixin, DjrillWebhookSignatureMixin, View):
|
||||
def head(self, request, *args, **kwargs):
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ Release Notes
|
||||
|
||||
Version 0.6 (development):
|
||||
|
||||
* Support for signed webhooks
|
||||
|
||||
Version 0.5:
|
||||
|
||||
|
||||
@@ -29,10 +29,13 @@ Your code can connect to this signal for further processing.
|
||||
app and Mandrill. Djrill will verify calls to your webhook, and will
|
||||
reject calls without the correct key.
|
||||
|
||||
* You can, optionally include the two settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY`
|
||||
and :setting:`DJRILL_WEBHOOK_URL` to enforce `webhook signature`_ checking
|
||||
|
||||
|
||||
.. _Mandrill webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks
|
||||
.. _securing webhooks: http://apidocs.mailchimp.com/webhooks/#securing-webhooks
|
||||
|
||||
.. _webhook signature: http://help.mandrill.com/entries/23704122-Authenticating-webhook-requests
|
||||
|
||||
.. _webhooks-config:
|
||||
|
||||
@@ -97,6 +100,12 @@ the url config in step 2. And if you'd like to change
|
||||
the *name* of the "secret" query string parameter, you can set
|
||||
:setting:`DJRILL_WEBHOOK_SECRET_NAME` in your :file:`settings.py`.
|
||||
|
||||
For extra security, Mandrill provides a signature in the request header
|
||||
X-Mandrill-Signature. If you want to verify this signature, you need to provide
|
||||
the settings :setting:`DJRILL_WEBHOOK_SIGNATURE_KEY` with the webhook-specific
|
||||
signature key that can be found in the Mandrill admin panel and
|
||||
:setting:`DJRILL_WEBHOOK_URL` where you should enter the exact URL, including
|
||||
that you entered in Mandrill when creating the webhook.
|
||||
|
||||
.. _webhooks control panel: https://mandrillapp.com/settings/webhooks
|
||||
.. _inbound settings: https://mandrillapp.com/inbound
|
||||
|
||||
Reference in New Issue
Block a user