mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Mandrill: include auth in webhook signature calc
Mandrill's webhook signature calculation uses the *exact url* Mandrill is posting to. If HTTP basic auth is also used, that auth is included in the url. Anymail was using Django's request.build_absolute_uri, which doesn't include HTTP basic auth. Anymail now includes the auth in the calculation, if it was present in the request. This should eliminate the need to use the ANYMAIL_MANDRILL_WEBHOOK_URL override, if Django's SECURE_PROXY_SSL_HEADER and USE_X_FORWARDED_HOST (and/or USE_X_FORWARDED_PROTO) settings are correct for your server. (The calculated url is now also included in the validation failure error message, to aid debugging.) Fixes #48
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import base64
|
||||
import mimetypes
|
||||
from base64 import b64encode
|
||||
from collections import Mapping, MutableMapping
|
||||
@@ -12,6 +13,8 @@ from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_T
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.timezone import utc
|
||||
# noinspection PyUnresolvedReferences
|
||||
from six.moves.urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from .exceptions import AnymailConfigurationError, AnymailInvalidAddress
|
||||
|
||||
@@ -342,3 +345,33 @@ def force_non_lazy_dict(obj):
|
||||
return {key: force_non_lazy_dict(value) for key, value in obj.items()}
|
||||
except (AttributeError, TypeError):
|
||||
return force_non_lazy(obj)
|
||||
|
||||
|
||||
def get_request_basic_auth(request):
|
||||
"""Returns HTTP basic auth string sent with request, or None.
|
||||
|
||||
If request includes basic auth, result is string 'username:password'.
|
||||
"""
|
||||
try:
|
||||
authtype, authdata = request.META['HTTP_AUTHORIZATION'].split()
|
||||
if authtype.lower() == "basic":
|
||||
return base64.b64decode(authdata).decode('utf-8')
|
||||
except (IndexError, KeyError, TypeError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_request_uri(request):
|
||||
"""Returns the "exact" url used to call request.
|
||||
|
||||
Like :func:`django.http.request.HTTPRequest.build_absolute_uri`,
|
||||
but also inlines HTTP basic auth, if present.
|
||||
"""
|
||||
url = request.build_absolute_uri()
|
||||
basic_auth = get_request_basic_auth(request)
|
||||
if basic_auth is not None:
|
||||
# must reassemble url with auth
|
||||
parts = urlsplit(url)
|
||||
url = urlunsplit((parts.scheme, basic_auth + '@' + parts.netloc,
|
||||
parts.path, parts.query, parts.fragment))
|
||||
return url
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import base64
|
||||
import re
|
||||
import six
|
||||
import warnings
|
||||
|
||||
import six
|
||||
from django.http import HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import View
|
||||
|
||||
from ..exceptions import AnymailInsecureWebhookWarning, AnymailWebhookValidationFailure
|
||||
from ..utils import get_anymail_setting, collect_all_methods
|
||||
from ..utils import get_anymail_setting, collect_all_methods, get_request_basic_auth
|
||||
|
||||
|
||||
class AnymailBasicAuthMixin(object):
|
||||
@@ -42,16 +41,8 @@ class AnymailBasicAuthMixin(object):
|
||||
def validate_request(self, request):
|
||||
"""If configured for webhook basic auth, validate request has correct auth."""
|
||||
if self.basic_auth:
|
||||
valid = False
|
||||
try:
|
||||
authtype, authdata = request.META['HTTP_AUTHORIZATION'].split()
|
||||
if authtype.lower() == "basic":
|
||||
auth = base64.b64decode(authdata).decode('utf-8')
|
||||
if auth in self.basic_auth:
|
||||
valid = True
|
||||
except (IndexError, KeyError, TypeError, ValueError):
|
||||
valid = False
|
||||
if not valid:
|
||||
basic_auth = get_request_basic_auth(request)
|
||||
if basic_auth is None or basic_auth not in self.basic_auth:
|
||||
# noinspection PyUnresolvedReferences
|
||||
raise AnymailWebhookValidationFailure(
|
||||
"Missing or invalid basic auth in Anymail %s webhook" % self.esp_name)
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.utils.timezone import utc
|
||||
from .base import AnymailBaseWebhookView
|
||||
from ..exceptions import AnymailWebhookValidationFailure, AnymailConfigurationError
|
||||
from ..signals import tracking, AnymailTrackingEvent, EventType
|
||||
from ..utils import get_anymail_setting, getfirst
|
||||
from ..utils import get_anymail_setting, getfirst, get_request_uri
|
||||
|
||||
|
||||
class MandrillSignatureMixin(object):
|
||||
@@ -45,16 +45,18 @@ class MandrillSignatureMixin(object):
|
||||
except KeyError:
|
||||
raise AnymailWebhookValidationFailure("X-Mandrill-Signature header missing from webhook POST")
|
||||
|
||||
# Mandrill signs the exact URL plus the sorted POST params:
|
||||
signed_data = self.webhook_url or request.build_absolute_uri()
|
||||
# Mandrill signs the exact URL (including basic auth, if used) plus the sorted POST params:
|
||||
url = self.webhook_url or get_request_uri(request)
|
||||
params = request.POST.dict()
|
||||
signed_data = url
|
||||
for key in sorted(params.keys()):
|
||||
signed_data += key + params[key]
|
||||
|
||||
expected_signature = b64encode(hmac.new(key=self.webhook_key, msg=signed_data.encode('utf-8'),
|
||||
digestmod=hashlib.sha1).digest())
|
||||
if not constant_time_compare(signature, expected_signature):
|
||||
raise AnymailWebhookValidationFailure("Mandrill webhook called with incorrect signature")
|
||||
raise AnymailWebhookValidationFailure(
|
||||
"Mandrill webhook called with incorrect signature (for url %r)" % url)
|
||||
|
||||
|
||||
class MandrillBaseWebhookView(MandrillSignatureMixin, AnymailBaseWebhookView):
|
||||
|
||||
Reference in New Issue
Block a user