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
|
import json
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from ..compat import b
|
||||||
from ..signals import webhook_event
|
from ..signals import webhook_event
|
||||||
|
|
||||||
|
|
||||||
@@ -39,6 +43,37 @@ class DjrillWebhookSecretMixinTests(TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
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):
|
class DjrillWebhookViewTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Test optional Mandrill webhook view
|
Test optional Mandrill webhook view
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
from base64 import b64encode
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@@ -12,6 +14,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from djrill import MANDRILL_API_URL, signals
|
from djrill import MANDRILL_API_URL, signals
|
||||||
|
from .compat import b
|
||||||
|
|
||||||
|
|
||||||
class DjrillAdminMedia(object):
|
class DjrillAdminMedia(object):
|
||||||
@@ -101,6 +104,41 @@ class DjrillWebhookSecretMixin(object):
|
|||||||
request, *args, **kwargs)
|
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):
|
class DjrillIndexView(DjrillApiMixin, TemplateView):
|
||||||
template_name = "djrill/status.html"
|
template_name = "djrill/status.html"
|
||||||
|
|
||||||
@@ -161,7 +199,7 @@ class DjrillUrlListView(DjrillAdminMedia, DjrillApiMixin,
|
|||||||
return self.render_to_response(context)
|
return self.render_to_response(context)
|
||||||
|
|
||||||
|
|
||||||
class DjrillWebhookView(DjrillWebhookSecretMixin, View):
|
class DjrillWebhookView(DjrillWebhookSecretMixin, DjrillWebhookSignatureMixin, View):
|
||||||
def head(self, request, *args, **kwargs):
|
def head(self, request, *args, **kwargs):
|
||||||
return HttpResponse()
|
return HttpResponse()
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ Release Notes
|
|||||||
|
|
||||||
Version 0.6 (development):
|
Version 0.6 (development):
|
||||||
|
|
||||||
|
* Support for signed webhooks
|
||||||
|
|
||||||
Version 0.5:
|
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
|
app and Mandrill. Djrill will verify calls to your webhook, and will
|
||||||
reject calls without the correct key.
|
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
|
.. _Mandrill webhooks: http://help.mandrill.com/entries/21738186-Introduction-to-Webhooks
|
||||||
.. _securing webhooks: http://apidocs.mailchimp.com/webhooks/#securing-webhooks
|
.. _securing webhooks: http://apidocs.mailchimp.com/webhooks/#securing-webhooks
|
||||||
|
.. _webhook signature: http://help.mandrill.com/entries/23704122-Authenticating-webhook-requests
|
||||||
|
|
||||||
.. _webhooks-config:
|
.. _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
|
the *name* of the "secret" query string parameter, you can set
|
||||||
:setting:`DJRILL_WEBHOOK_SECRET_NAME` in your :file:`settings.py`.
|
: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
|
.. _webhooks control panel: https://mandrillapp.com/settings/webhooks
|
||||||
.. _inbound settings: https://mandrillapp.com/inbound
|
.. _inbound settings: https://mandrillapp.com/inbound
|
||||||
|
|||||||
Reference in New Issue
Block a user