Fake SSL Middleware for Tests and Local Development

  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
import re

SECURE_GET_VARIABLE = 'FAKE_HTTPS'
HTTPS_PATTERN = re.compile(r'https://([\w./\-:\?=%_]+)', re.IGNORECASE)
FAKE_HTTPS_PATTERN = re.compile(
    r'http://([\w./\-:\?=%%]+)[\?&]%s=1' % SECURE_GET_VARIABLE, re.IGNORECASE)
ATTRIBUTE_NAMES = ('href', 'action')
ATTRIBUTE_PATTERN = re.compile(
    r"""(?P<attribute_name>%s)\s*="""
    r"""\s*(?P<quote_delimiter>["'])(?P<attribute_value>.*?[^\\])\2""" %
    '|'.join(ATTRIBUTE_NAMES), re.IGNORECASE)
ATTRIBUTE_FORMAT = (
    "%(attribute_name)s="
    "%(quote_delimiter)c%(attribute_value)s%(quote_delimiter)c")

# note that SECURE_GET_VARIABLE is currently assume to be at the end of
# the query string. I have not been bitten by this assumption yet...

def secure_url(url):
    """ Adds SECURE_GET_VARIABLE to query string within url if absent.

    >>> secure_url('http://localhost:8000')
    'http://localhost:8000?FAKE_HTTPS=1'
    >>> secure_url('http://localhost:8000?test=true')
    'http://localhost:8000?test=true&FAKE_HTTPS=1'
    >>> secure_url('http://localhost:8000?test=true&FAKE_HTTPS=1')
    'http://localhost:8000?test=true&FAKE_HTTPS=1'
    >>> secure_url('http://localhost:8000?FAKE_HTTPS=1')
    'http://localhost:8000?FAKE_HTTPS=1'
    >>> url = 'http://localhost:8000/login/?next=/profile/%3FFAKE_HTTPS%3D1'
    >>> secure_url(url)
    'http://localhost:8000/login/?next=/profile/%3FFAKE_HTTPS%3D1&FAKE_HTTPS=1'
    """
    if FAKE_HTTPS_PATTERN.match(url):
        return url # unnecessary
    if '?' in url:
        joiner = '&'
    else:
        joiner = '?'
    return '%s%c%s=1' % (url, joiner, SECURE_GET_VARIABLE)

def https_to_fake(string):
    """ Convert https:// URL to http:// with SECURE_GET_VARIABLE.

    >>> https_to_fake('https://localhost:8000')
    'http://localhost:8000?FAKE_HTTPS=1'
    >>> https_to_fake('https://localhost:8000?existing_variable=1')
    'http://localhost:8000?existing_variable=1&FAKE_HTTPS=1'
    >>> url = 'https://localhost:8000/login/?next=/profile/%3FFAKE_HTTPS%3D1'
    >>> https_to_fake(url)
    'http://localhost:8000/login/?next=/profile/%3FFAKE_HTTPS%3D1&FAKE_HTTPS=1'
    """
    def _secure_url(match):
        return secure_url('http://%s' % match.groups()[0])
    return HTTPS_PATTERN.sub(_secure_url, string)

def fake_to_https(string):
    """ Convert http:// URL to https:// if contains SECURE_GET_VARIABLE.

    >>> fake_to_https('http://localhost:8000?FAKE_HTTPS=1')
    'https://localhost:8000'
    >>> fake_to_https('http://localhost:8000?existing_variable=1&FAKE_HTTPS=1')
    'https://localhost:8000?existing_variable=1'
    >>> url = 'http://localhost:8000/go/?next=/%3FFAKE_HTTPS%3D1&FAKE_HTTPS=1'
    >>> fake_to_https(url)
    'https://localhost:8000/go/?next=/%3FFAKE_HTTPS%3D1'
    """
    return FAKE_HTTPS_PATTERN.sub(r'https://\1', string)

def secure_relative_urls(string):
    """ Convert add SECURE_GET_VARIABLE to relative URLs in href attributes.

    >>> secure_relative_urls('... href="/test" ...')
    '... href="/test?FAKE_HTTPS=1" ...'
    >>> secure_relative_urls('... href="http://example.com" ...')
    '... href="http://example.com" ...'
    >>> secure_relative_urls('... href="/test" ...')
    '... href="/test?FAKE_HTTPS=1" ...'
    """
    def _secure_relative_url(match):
        params = match.groupdict()
        if params['attribute_value'].find('://') >= 0:
            pass # ignore absolute URLs
        else:
            params['attribute_value'] = secure_url(params['attribute_value'])
        return ATTRIBUTE_FORMAT % params
    return ATTRIBUTE_PATTERN.sub(_secure_relative_url, string)

class FakeSSLMiddleware(object):
    """ Fake SSL For local/testing environments. """
    def __init__(self, *args, **kwargs):
        if not settings.DEBUG and not getattr(settings, 'TESTING', False):
            raise RuntimeError, "Disable on production site!"

    def process_request(self, request):
        """ Hack request object to fake security.

        - replace is_secure method with test for SECURE_GET_VARIABLE in GET
        - remove SECURE_GET_VARIABLE from query string
        - undo build_absolute_uri method's work of adding https:// to
          relative URIs where request.is_secure...
          (see django.http.utils.fix_location_header)
        - make HTTP_REFERER look secure if contained SECURE_GET_VARIABLE,
          so that CSRF doesn't catch us out.
        """

        GET = request.GET.copy() # immutable QueryDict
        secure = bool(GET.pop(SECURE_GET_VARIABLE, False))
        request.GET = GET
        request.is_secure = lambda: secure

        request.environ['QUERY_STRING'] = \
            re.sub(r'&?%s=1' % SECURE_GET_VARIABLE,'',
                   request.environ['QUERY_STRING'])

        setattr(request,'_build_absolute_uri', request.build_absolute_uri)
        def build_http_only_uri(location=None):
            uri = request._build_absolute_uri(location)
            return https_to_fake(uri)
        request.build_absolute_uri = build_http_only_uri

        if 'HTTP_REFERER' in request.META:
            request.META['HTTP_REFERER'] = \
                fake_to_https(request.META['HTTP_REFERER'])

    def process_response(self, request, response):
        """ Replace https:// with http:// within HTML responses + redirects """

        if response.status_code in (301, 302):
            headers = response._headers.copy() # immutable QueryDict
            headers['location'] = (
                'Location', https_to_fake(response['Location']))
            response._headers = headers
        elif response.status_code == 200:
            if response['Content-Type'].find('html') >= 0:
                try:
                    decoded_content = response.content.decode('utf-8')
                except UnicodeDecodeError:
                    decoded_content = response.content
                response.content = \
                    https_to_fake(decoded_content).encode('utf-8')
                if request.is_secure():
                    response.content = secure_relative_urls(response.content)
        return response

More like this

  1. SSL / HTTPS Middleware for Redirection and href Rewriting by DrMeers 3 years, 12 months ago
  2. SSL Middleware by sjzabel 7 years, 1 month ago
  3. TLS(SSL) middleware, per URL pattern or whole site by robmadole 3 years, 4 months ago
  4. SSL Redirect Middleware and testing by willhardy 5 years, 9 months ago
  5. SSL Middleware for Webfaction by parlar 6 years, 11 months ago

Comments

dmalinovsky (on March 21, 2012):

You forgot to add from django.conf import settings at the top of the snippet.

Otherwise, it's working great, thanks!

#

(Forgotten your password?)