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%s)\s*=""" r"""\s*(?P["'])(?P.*?[^\\])\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