TLS(SSL) middleware, per URL pattern or whole site

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""
Allows url patterns to include a boolean indicating whether a view requires
TLS(SSL).  The accompanying middleware handles the redirects needed to make
sure that it upholds this requirement.

WARNING: this monkey-patches some Django internals and is difficult to test
since Django's TestClient does not support TLS.  If you use this make sure you
test it thouroughly.

Add this to your Django settings::

    USE_TLS = True   # The default for this setting is False.

URL pattern usage::

    url(r'^login$', 'myproject.login.index',
        {'require_tls': True}, name='login-index'),

Use ``require_tls`` True to force the middleware to perform redirects needed to
make sure your are serving this view using https.

Use ``require_tls`` False to force the middleware to redirect to http.  Be
careful with this setting, this may not behave as you expect.  If you don't
care if the view is served via https or http then do not include
``require_tls`` in the pattern.

If you wish to have every view in the site served with TLS then specify the
following Django setting::

    ALWAYS_USE_TLS = True   # Django setting, use TLS for every view

"""
from django.conf import settings
from django.core import urlresolvers
from django.http import HttpResponsePermanentRedirect
from django.http import get_host
from django.utils.encoding import smart_str

ALWAYS_USE_TLS = getattr(settings, 'ALWAYS_USE_TLS', False)
USE_TLS = getattr(settings, 'USE_TLS', ALWAYS_USE_TLS)


def resolve_pattern(path, urlconf=None):
    """
    Works the same as Django's url resolver but instead of return a view
    function that handles a specific path it gives you the pattern that matches
    it
    """
    return urlresolvers.get_resolver(urlconf).resolve_pattern(path)


def resolve_pattern_method(self, path):
    """
    Monkey-patchable to the RegexURLResolver class

    The difference is that this method will return the pattern instead of what
    Django's RegexURLResolver.resolve returns
    """
    tried = []
    match = self.regex.search(path)
    if match:
        new_path = path[match.end():]
        for pattern in self.url_patterns:
            try:
                if hasattr(pattern, 'resolve_pattern'):
                    # Use our resolve_pattern method
                    sub_match = pattern.resolve_pattern(new_path)
                    if isinstance(sub_match, urlresolvers.RegexURLPattern):
                        return sub_match
                else:
                    sub_match = pattern.resolve(new_path)
            except urlresolvers.Resolver404, e:
                sub_tried = e.args[0].get('tried')
                if sub_tried is not None:
                    tried.extend([(pattern.regex.pattern + '   ' + t) for t in sub_tried])
                else:
                    tried.append(pattern.regex.pattern)
            else:
                if sub_match:
                    if hasattr(pattern, 'callback'):
                        # Found our pattern
                        return pattern
                    sub_match_dict = dict([(smart_str(k), v) for k, v in match.groupdict().items()])
                    sub_match_dict.update(self.default_kwargs)
                    for k, v in sub_match[2].iteritems():
                        sub_match_dict[smart_str(k)] = v
                    return sub_match[0], sub_match[1], sub_match_dict
                tried.append(pattern.regex.pattern)
        raise urlresolvers.Resolver404, {'tried': tried, 'path': new_path}
    raise urlresolvers.Resolver404, {'path' : path}

# Monkey patch the additional methods
urlresolvers.RegexURLResolver.resolve_pattern = resolve_pattern_method


def reverse_decorator(func):
    def inner(*args, **kwargs):
        if 'request' in kwargs:
            request = kwargs['request']
            del kwargs['request']
        else:
            request = None

        # Call the real reverse function
        abs_path = func(*args, **kwargs)

        if not request or not USE_TLS:
            # Short-circuit, no need to continue becuase there is no request we
            # can get smart with or TLS is turned off
            return abs_path

        # Let's figure out if we need this URL to be secure or not
        pattern = resolve_pattern(abs_path)
        if 'require_tls' in pattern.default_args:
            secure = pattern.default_args['require_tls']
        else:
            secure = None

        if not secure is None and request and \
           not secure == is_secure(request):
            # Secure indicates either http or https and the request is not
            # currently being served that way
            protocol = 'https' if secure else 'http'
            abs_path = '%s://%s%s' % (protocol, get_host(request), abs_path,)
        return abs_path        
    return inner
urlresolvers.reverse = reverse_decorator(urlresolvers.reverse)


def is_secure(request):
    """
    Determines if the given request is over HTTPS or not
    """
    if request.is_secure():
        return True

    return False


class TlsRedirect(object):
    def process_view(self, request, view_func, view_args, view_kwargs):
        if 'require_tls' in view_kwargs:
            secure = view_kwargs['require_tls']
            del view_kwargs['require_tls']
        else:
            secure = None

        if not any([USE_TLS, ALWAYS_USE_TLS]):
            return None
        
        if ALWAYS_USE_TLS:
            # This site is setup to always use TLS so we set the secure bit to
            # always be true
            secure = True

        # If secure is None, the require_tls was not present in the view
        # keyword args at all.  In this case, we don't want to redirect.
        # Leaving this keyword off probably indicates that the particular url
        # doesn't require http or https, we go with whatever we got
        if not secure is None and \
           not secure == is_secure(request):
            return self._redirect(request, secure)

    def _redirect(self, request, secure):
        protocol = secure and 'https' or 'http'
        newurl = '%s://%s%s' % (
            protocol, get_host(request), request.get_full_path(),)
        if settings.DEBUG and request.method == 'POST':
            raise RuntimeError('Django can\'t perform a TLS redirect while '
                'maintaining POST data.  Please structure your views so that '
                'redirects only occur during GETs.')

        return HttpResponsePermanentRedirect(newurl)

More like this

  1. SSL Middleware by sjzabel 7 years, 1 month ago
  2. HTTPS redirections middleware with updated URL template tag by xlq 1 year, 6 months ago
  3. SSL Middleware for Webfaction by parlar 6 years, 11 months ago
  4. Smart append slash middleware by akaihola 6 years, 2 months ago
  5. Enable AWS ELB with SSL Termination by zvikico 2 years, 9 months ago

Comments

cricogik (on December 7, 2010):

Hmm, I think that better (more generic) way to specify which method shoud be protected using SSL is decorator for view and/or routers:

eg,

url(r'^login/$', use_ssl('myproject.login.index')),

...

@use_ssl
def my_view(request):
    pass

#

robmadole (on December 20, 2010):

Yep, a decorator is simpler but we needed a bit more control at the time and the option to secure the entire site. And with the monkey-patching this one is certainly not the most comfortable.

#

(Forgotten your password?)