Commits

Jody McIntyre  committed 3138c2f

Initial commit of open source HSTS middleware. BugzID: 303

  • Participants

Comments (0)

Files changed (10)

+syntax: glob
+*.pyc
+*~
+*.egg
+*.egg-info
+
+syntax: regexp
+^dist/
+Copyright (c) 2011, TrustCentric
+Portions Copyright (c) 2009, Akoha, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of TrustCentric nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+include README.rst
+include LICENSE
+django-hstsmiddleware
+=====================
+
+Forces the use of `HTTPS` using `HTTP Strict Transport Security`
+(HSTS).
+
+
+Installation and Usage
+----------------------
+
+Install the package, add ``django_hstsmiddleware`` to
+``settings.INSTALLED_APPS``, and add
+``django_hstsmiddleware.middleware.HSTSMiddleware`` to the top of
+``settings.MIDDLEWARE_CLASSES``.
+
+The following Django settings control its default behaviour:
+
+`settings.HSTS_REDIRECT_TO`:
+    Specifies the URI to redirect a User Agent to, if it tries
+    to use a non-secure connection. Responds with HTTP Moved
+    Permanently.
+
+    Defaults to ``None``, so no redirect occurs. Instead, responds
+    with HTTP Bad Request.
+
+`settings.HSTS_MAX_AGE`:
+    The maximum number of seconds that a User Agent will remember
+    that this server must be contacted over HTTPS.
+
+    Defaults to ``31536000``, or approximately one year.
+
+`settings.HSTS_INCLUDE_SUBDOMAINS`:
+    If true, tells a User Agent that all subdomains must also be
+    contacted over HTTPS, in addition to the current domain.
+
+    Defaults to ``False``

File django_hstsmiddleware/__init__.py

+# -*- mode: django; coding: utf-8 -*-
+#
+# Copyright © 2011, TrustCentric
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of TrustCentric nor the names of its contributors
+#       may be used to endorse or promote products derived from this software
+#       without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.

File django_hstsmiddleware/middleware.py

+# -*- mode: django; coding: utf-8 -*-
+#
+# Copyright © 2011, TrustCentric
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of TrustCentric nor the names of its contributors
+#       may be used to endorse or promote products derived from this software
+#       without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+"""
+django-hstsmiddleware
+=====================
+
+Forces the use of `HTTPS` using `HTTP Strict Transport Security` (HSTS).
+
+Strict Transport Security
+`````````````````````````
+
+.. _HTTP Strict Transport Security:
+   http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
+"""
+
+
+from django.conf import settings
+from django.http import HttpResponseBadRequest, HttpResponsePermanentRedirect
+
+
+class HSTSMiddleware(object):
+    """
+    Forces the use of `HTTPS`_ using `HTTP Strict Transport Security`_ (HSTS).
+
+    Install it at the top of :data:`settings.MIDDLEWARE_CLASSES`.
+
+    :param int max_age: override :data:`settings.HSTS_MAX_AGE`
+    :param bool include_subdomains: override
+                                    :data:`settings.HSTS_INCLUDE_SUBDOMAINS`
+    :param str redirect_to: override :data:`settings.HSTS_REDIRECT_TO`
+    :param bool disable_under_test: don't enforce HTTPS when running unit tests
+    :param bool disable_under_debug: don't enforce HTTPS in ``settings.DEBUG``
+                                     mode.
+
+    The following Django settings control its default behaviour:
+
+    :data:`settings.HSTS_REDIRECT_TO`:
+        Specifies the |URI| to redirect a User Agent to, if it tries
+        to use a non-secure connection. Responds with |HTTP Moved
+        Permanently|.
+
+        Defaults to ``None``, so no redirect occurs. Instead, responds
+        with |HTTP Bad Request|.
+
+    :data:`settings.HSTS_MAX_AGE`:
+        The maximum number of seconds that a User Agent will remember
+        that this server must be contacted over HTTPS.
+
+        Defaults to ``31536000``, or approximately one year.
+
+    :data:`settings.HSTS_INCLUDE_SUBDOMAINS`:
+        If true, tells a User Agent that all subdomains must also be
+        contacted over HTTPS, in addition to the current domain.
+
+        Defaults to ``False``
+    """
+
+    def __init__(self, max_age=None, include_subdomains=None,
+                 redirect_to=None, disable_under_test=True,
+                 disable_under_debug=None):
+        self.redirect_to = (getattr(settings, 'HSTS_REDIRECT_TO', None)
+                            if redirect_to is None
+                            else redirect_to)
+        self.max_age = (getattr(settings, 'HSTS_MAX_AGE', 31536000)
+                        if max_age is None
+                        else max_age)
+        self.include_subdomains = (getattr(settings, 'HSTS_INCLUDE_SUBDOMAINS',
+                                           False)
+                                   if include_subdomains is None
+                                   else include_subdomains)
+        self.disable_under_debug = (getattr(settings,
+                                            'HSTS_DISABLE_UNDER_DEBUG',
+                                            False)
+                                    if disable_under_debug is None
+                                    else disable_under_debug)
+        # Do not force SSL under Django TestCase, defaults to True
+        self.disable_under_test = disable_under_test
+
+    def process_request(self, request):
+        """
+        Rejects any non-`HTTPS`_ requests with |HTTP Bad Request|.
+
+        If :data:`redirect_to` is not ``None``, any non-HTTPS requests
+        get permanently redirected to its value, using |HTTP Moved
+        Permanently|. It should be set to an absolute |URI| that is
+        served securely.
+
+        When running under the test suite, does nothing, unless
+        :data:`disable_under_test` is ``False``.
+        """
+        if self.disable_under_debug and settings.DEBUG:
+            # DEBUG mode, continue processing...
+            return None
+        if request.is_secure():
+            # Secure connection, continue processing...
+            return None
+        from django.core import mail
+        if self.disable_under_test and hasattr(mail, 'outbox'):
+            # If django.core.mail.outbox exists, then we must
+            # be running under the test suite.
+            return None
+        if self.redirect_to is not None:
+            # Redirect to a secure absolute URL.
+            return HttpResponsePermanentRedirect(self.redirect_to)
+        return HttpResponseBadRequest()
+
+    def process_response(self, request, response):
+        """
+        Add the ``Strict-Transport-Security`` header to *response*.
+
+        :param HttpResponse response: `HttpResponse`_
+
+        This header contains :data:`max_age` and
+        :data:`include_subdomains`.
+
+        See `HTTP Strict Transport Security`_.
+        """
+        hsts = 'max-age=%d' % self.max_age
+        if self.include_subdomains:
+            hsts += ' ; includeSubDomains'
+        response['Strict-Transport-Security'] = hsts
+        return response

File django_hstsmiddleware/models.py

+# -*- mode: django; coding: utf-8 -*-
+#
+# Copyright © 2011, TrustCentric
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of TrustCentric nor the names of its contributors
+#       may be used to endorse or promote products derived from this software
+#       without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+# No models are required.

File django_hstsmiddleware/patch.py

+# Copyright (c) 2009, Akoha, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#   * Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#
+#   * Redistributions in binary form must reproduce the above
+#     copyright notice, this list of conditions and the following
+#     disclaimer in the documentation and/or other materials provided
+#     with the distribution.
+#
+#   * Neither the name of django-lean nor the names of its
+#     contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from contextlib import contextmanager
+
+from django.utils.functional import LazyObject
+
+
+@contextmanager
+def patch(namespace, **values):
+    """Patches `namespace`.`name` with `value` for (name, value) in values"""
+
+    originals = {}
+
+    if isinstance(namespace, LazyObject):
+        if namespace._wrapped is None:
+            namespace._setup()
+        namespace = namespace._wrapped
+
+    for (name, value) in values.iteritems():
+        try:
+            originals[name] = getattr(namespace, name)
+        except AttributeError:
+            originals[name] = NotImplemented
+        if value is NotImplemented:
+            if originals[name] is not NotImplemented:
+                delattr(namespace, name)
+        else:
+            setattr(namespace, name, value)
+
+    try:
+        yield
+    finally:
+        for (name, original_value) in originals.iteritems():
+            if original_value is NotImplemented:
+                if values[name] is not NotImplemented:
+                    delattr(namespace, name)
+            else:
+                setattr(namespace, name, original_value)

File django_hstsmiddleware/tests.py

+# -*- mode: django; coding: utf-8 -*-
+#
+# Copyright © 2011, TrustCentric
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#     * Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#     * Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#     * Neither the name of TrustCentric nor the names of its contributors
+#       may be used to endorse or promote products derived from this software
+#       without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from django.conf import settings
+from django.http import HttpResponse
+from django.test import TestCase
+from django.test.client import RequestFactory
+
+from django_hstsmiddleware.patch import patch
+from django_hstsmiddleware.middleware import HSTSMiddleware
+
+
+class HSTSMiddlewareTest(TestCase):
+    def setUp(self, *args, **kwargs):
+        super(HSTSMiddlewareTest, self).setUp(*args, **kwargs)
+        self.middleware = HSTSMiddleware(disable_under_test=False)
+        self.factory = RequestFactory()
+
+    def test_request_secure(self):
+        request = self.factory.get('/')
+        request.is_secure = (lambda: True)
+        result = self.middleware.process_request(request)
+        self.assertEqual(result, None)
+
+    def test_request_insecure(self):
+        request = self.factory.get('/')
+        response = self.middleware.process_request(request)
+        self.assertEqual(response.status_code, 400, response.content)
+
+    def test_request_disable_under_test(self):
+        self.middleware.disable_under_test = True
+        request = self.factory.get('/')
+        result = self.middleware.process_request(request)
+        self.assertEqual(result, None)
+
+    def test_request_disable_under_debug(self):
+        with patch(settings, DEBUG=True):
+            # When HSTS_DISABLE_UNDER_DEBUG is True, do nothing
+            self.middleware.disable_under_debug = True
+            request = self.factory.get('/')
+            result = self.middleware.process_request(request)
+            self.assertEqual(result, None)
+            # When HSTS_DISABLE_UNDER_DEBUG is False, operate normally
+            self.middleware.disable_under_debug = False
+            request = self.factory.get('/')
+            response = self.middleware.process_request(request)
+            self.assertEqual(response.status_code, 400, response.content)
+
+    def test_request_redirect_to(self):
+        url = 'https://testserver/path/'
+        self.middleware.redirect_to = url
+        request = self.factory.get('/')
+        response = self.middleware.process_request(request)
+        self.assertEqual(response.status_code, 301, response.content)
+        self.assertEqual(response['Location'], url)
+
+    def test_request_bad_request(self):
+        request = self.factory.get('/')
+        response = self.middleware.process_request(request)
+        self.assertEqual(response.status_code, 400, response.content)
+        self.assertEqual(response.content, '')
+
+    def test_response_max_age(self):
+        request = self.factory.get('/')
+        response = HttpResponse('')
+        self.middleware.max_age = 42
+        self.middleware.include_subdomains = False
+        self.middleware.process_response(request, response)
+        self.assertEqual(response['Strict-Transport-Security'],
+                         'max-age=42')
+
+    def test_response_include_subdomains(self):
+        request = self.factory.get('/')
+        response = HttpResponse('')
+        self.middleware.max_age = 42
+        self.middleware.include_subdomains = True
+        self.middleware.process_response(request, response)
+        self.assertEqual(response['Strict-Transport-Security'],
+                         'max-age=42 ; includeSubDomains')
+#!/usr/bin/env python
+from setuptools import find_packages, setup
+
+setup(
+    name='django-hstsmiddleware',
+    version='1.0',
+    description=('Implement HSTS to force the use of HTTPS.'),
+    long_description=open('README.rst', 'r').read(),
+    author='TrustCentric',
+    author_email='admin@trustcentric.com',
+    url='http://bitbucket.org/trustcentric/django-hstsmiddleware/',
+    classifiers=[
+        'Development Status :: 5 - Production/Stable',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+    ],
+    packages=['django_hstsmiddleware'],
+    zip_safe=True,
+    install_requires=['Django>=1.0',
+                      'django-digest>=1.8'],
+)