diff --git a/common/djangoapps/clean_headers/__init__.py b/common/djangoapps/clean_headers/__init__.py deleted file mode 100644 index 0c718ec7ad..0000000000 --- a/common/djangoapps/clean_headers/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -This middleware is used for cleaning headers from a response before it is sent to the end user. - -Due to the nature of how middleware runs, a piece of middleware high in the chain cannot ensure -that response headers won't be present on the final response body, as middleware further down -the chain could be adding them. - -This middleware is intended to sit as close as possible to the top of the list, so that it has -a chance on the reponse going out to strip the intended headers. -""" - - -def remove_headers_from_response(response, *headers): - """Removes the given headers from the response using the clean_headers middleware.""" - response.clean_headers = headers diff --git a/common/djangoapps/clean_headers/decorators.py b/common/djangoapps/clean_headers/decorators.py deleted file mode 100644 index dadba59619..0000000000 --- a/common/djangoapps/clean_headers/decorators.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Middleware decorator for removing headers. -""" - -from functools import wraps - - -def clean_headers(*headers): - """ - Decorator that removes any headers specified from the response. - Usage: - @clean_headers("Vary") - def myview(request): - ... - - The CleanHeadersMiddleware must be used and placed as closely as possible to the top - of the middleware chain, ideally after any caching middleware but before everything else. - - This decorator is not safe for multiple uses: each call will overwrite any previously set values. - """ - def _decorator(func): - """ - Decorates the given function. - """ - @wraps(func) - def _inner(*args, **kwargs): - """ - Alters the response. - """ - response = func(*args, **kwargs) - response.clean_headers = headers - return response - - return _inner - - return _decorator diff --git a/common/djangoapps/clean_headers/middleware.py b/common/djangoapps/clean_headers/middleware.py deleted file mode 100644 index d3f8e0be6a..0000000000 --- a/common/djangoapps/clean_headers/middleware.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Middleware used for cleaning headers from a response before it is sent to the end user. -""" - - -class CleanHeadersMiddleware(object): - """ - Middleware that can drop headers present in a response. - - This can be used, for example, to remove headers i.e. drop any Vary headers to improve cache performance. - """ - - def process_response(self, _request, response): - """ - Processes the given response, potentially stripping out any unwanted headers. - """ - - if len(getattr(response, 'clean_headers', [])) > 0: - for header in response.clean_headers: - try: - del response[header] - except KeyError: - pass - - return response diff --git a/common/djangoapps/clean_headers/tests/test_decorators.py b/common/djangoapps/clean_headers/tests/test_decorators.py deleted file mode 100644 index d9f0642405..0000000000 --- a/common/djangoapps/clean_headers/tests/test_decorators.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Tests for clean_headers decorator. """ -from django.http import HttpResponse, HttpRequest -from django.test import TestCase -from clean_headers.decorators import clean_headers - - -def fake_view(_request): - """Fake view that returns an empty response.""" - return HttpResponse() - - -class TestCleanHeaders(TestCase): - """Test the `clean_headers` decorator.""" - - def test_clean_headers(self): - request = HttpRequest() - wrapper = clean_headers('Vary', 'Accept-Encoding') - wrapped_view = wrapper(fake_view) - response = wrapped_view(request) - self.assertEqual(len(response.clean_headers), 2) diff --git a/common/djangoapps/clean_headers/tests/test_middleware.py b/common/djangoapps/clean_headers/tests/test_middleware.py deleted file mode 100644 index 3be79a6f11..0000000000 --- a/common/djangoapps/clean_headers/tests/test_middleware.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Tests for clean_headers middleware.""" -from django.http import HttpResponse, HttpRequest -from django.test import TestCase -from clean_headers.middleware import CleanHeadersMiddleware - - -class TestCleanHeadersMiddlewareProcessResponse(TestCase): - """Test the `clean_headers` middleware. """ - def setUp(self): - super(TestCleanHeadersMiddlewareProcessResponse, self).setUp() - self.middleware = CleanHeadersMiddleware() - - def test_cleans_intended_headers(self): - fake_request = HttpRequest() - - fake_response = HttpResponse() - fake_response['Vary'] = 'Cookie' - fake_response['Accept-Encoding'] = 'gzip' - fake_response.clean_headers = ['Vary'] - - result = self.middleware.process_response(fake_request, fake_response) - self.assertNotIn('Vary', result) - self.assertEquals('gzip', result['Accept-Encoding']) - - def test_does_not_mangle_undecorated_response(self): - fake_request = HttpRequest() - - fake_response = HttpResponse() - fake_response['Vary'] = 'Cookie' - fake_response['Accept-Encoding'] = 'gzip' - - result = self.middleware.process_response(fake_request, fake_response) - self.assertEquals('Cookie', result['Vary']) - self.assertEquals('gzip', result['Accept-Encoding']) diff --git a/common/djangoapps/header_control/__init__.py b/common/djangoapps/header_control/__init__.py new file mode 100644 index 0000000000..a33b8d2de9 --- /dev/null +++ b/common/djangoapps/header_control/__init__.py @@ -0,0 +1,21 @@ +""" +This middleware is used for adjusting the headers in a response before it is sent to the end user. + +This middleware is intended to sit as close as possible to the top of the middleare list as possible, +so that it is one of the last pieces of middleware to touch the response, and thus can most accurately +adjust/control the headers of the response. +""" + + +def remove_headers_from_response(response, *headers): + """Removes the given headers from the response using the header_control middleware.""" + response.remove_headers = headers + +def force_header_for_response(response, header, value): + """Forces the given header for the given response using the header_control middleware.""" + force_headers = {} + if hasattr(response, 'force_headers'): + force_headers = response.force_headers + force_headers[header] = value + + response.force_headers = force_headers diff --git a/common/djangoapps/header_control/decorators.py b/common/djangoapps/header_control/decorators.py new file mode 100644 index 0000000000..6a3ca9c221 --- /dev/null +++ b/common/djangoapps/header_control/decorators.py @@ -0,0 +1,67 @@ +""" +Middleware decorator for removing headers. +""" + +from functools import wraps +from header_control import remove_headers_from_response, force_header_for_response + +def remove_headers(*headers): + """ + Decorator that removes specific headers from the response. + Usage: + @remove_headers("Vary") + def myview(request): + ... + + The HeaderControlMiddleware must be used and placed as closely as possible to the top + of the middleware chain, ideally after any caching middleware but before everything else. + + This decorator is not safe for multiple uses: each call will overwrite any previously set values. + """ + def _decorator(func): + """ + Decorates the given function. + """ + @wraps(func) + def _inner(*args, **kwargs): + """ + Alters the response. + """ + response = func(*args, **kwargs) + remove_headers_from_response(response, *headers) + return response + + return _inner + + return _decorator + + +def force_header(header, value): + """ + Decorator that forces a header in the response to have a specific value. + Usage: + @force_header("Vary", "Origin") + def myview(request): + ... + + The HeaderControlMiddleware must be used and placed as closely as possible to the top + of the middleware chain, ideally after any caching middleware but before everything else. + + This decorator is not safe for multiple uses: each call will overwrite any previously set values. + """ + def _decorator(func): + """ + Decorates the given function. + """ + @wraps(func) + def _inner(*args, **kwargs): + """ + Alters the response. + """ + response = func(*args, **kwargs) + force_header_for_response(response, header, value) + return response + + return _inner + + return _decorator diff --git a/common/djangoapps/header_control/middleware.py b/common/djangoapps/header_control/middleware.py new file mode 100644 index 0000000000..c0f118fe81 --- /dev/null +++ b/common/djangoapps/header_control/middleware.py @@ -0,0 +1,34 @@ +""" +Middleware used for adjusting headers in a response before it is sent to the end user. +""" + + +class HeaderControlMiddleware(object): + """ + Middleware that can modify/remove headers in a response. + + This can be used, for example, to remove headers i.e. drop any Vary headers to improve cache performance. + """ + + def process_response(self, _request, response): + """ + Processes the given response, potentially remove or modifying headers. + """ + + if len(getattr(response, 'remove_headers', [])) > 0: + for header in response.remove_headers: + try: + del response[header] + except KeyError: + pass + + if len(getattr(response, 'force_headers', {})) > 0: + for header, value in response.force_headers.iteritems(): + try: + del response[header] + except KeyError: + pass + + response[header] = value + + return response diff --git a/common/djangoapps/header_control/tests/test_decorators.py b/common/djangoapps/header_control/tests/test_decorators.py new file mode 100644 index 0000000000..65b754ca1d --- /dev/null +++ b/common/djangoapps/header_control/tests/test_decorators.py @@ -0,0 +1,32 @@ +"""Tests for remove_headers and force_header decorator. """ +from django.http import HttpResponse, HttpRequest +from django.test import TestCase +from header_control.decorators import remove_headers, force_header + + +def fake_view(_request): + """Fake view that returns an empty response.""" + return HttpResponse() + + +class TestRemoveHeaders(TestCase): + """Test the `remove_headers` decorator.""" + + def test_remove_headers(self): + request = HttpRequest() + wrapper = remove_headers('Vary', 'Accept-Encoding') + wrapped_view = wrapper(fake_view) + response = wrapped_view(request) + self.assertEqual(len(response.remove_headers), 2) + + +class TestForceHeader(TestCase): + """Test the `force_header` decorator.""" + + def test_force_header(self): + request = HttpRequest() + wrapper = force_header('Vary', 'Origin') + wrapped_view = wrapper(fake_view) + response = wrapped_view(request) + self.assertEqual(len(response.force_headers), 1) + self.assertEqual(response.force_headers['Vary'], 'Origin') \ No newline at end of file diff --git a/common/djangoapps/header_control/tests/test_middleware.py b/common/djangoapps/header_control/tests/test_middleware.py new file mode 100644 index 0000000000..db3b77749f --- /dev/null +++ b/common/djangoapps/header_control/tests/test_middleware.py @@ -0,0 +1,47 @@ +"""Tests for header_control middleware.""" +from django.http import HttpResponse, HttpRequest +from django.test import TestCase +from header_control import remove_headers_from_response, force_header_for_response +from header_control.middleware import HeaderControlMiddleware + + +class TestHeaderControlMiddlewareProcessResponse(TestCase): + """Test the `header_control` middleware. """ + def setUp(self): + super(TestHeaderControlMiddlewareProcessResponse, self).setUp() + self.middleware = HeaderControlMiddleware() + + def test_removes_intended_headers(self): + fake_request = HttpRequest() + + fake_response = HttpResponse() + fake_response['Vary'] = 'Cookie' + fake_response['Accept-Encoding'] = 'gzip' + remove_headers_from_response(fake_response, 'Vary') + + result = self.middleware.process_response(fake_request, fake_response) + self.assertNotIn('Vary', result) + self.assertEquals('gzip', result['Accept-Encoding']) + + def test_forces_intended_header(self): + fake_request = HttpRequest() + + fake_response = HttpResponse() + fake_response['Vary'] = 'Cookie' + fake_response['Accept-Encoding'] = 'gzip' + force_header_for_response(fake_response, 'Vary', 'Origin') + + result = self.middleware.process_response(fake_request, fake_response) + self.assertEquals('Origin', result['Vary']) + self.assertEquals('gzip', result['Accept-Encoding']) + + def test_does_not_mangle_undecorated_response(self): + fake_request = HttpRequest() + + fake_response = HttpResponse() + fake_response['Vary'] = 'Cookie' + fake_response['Accept-Encoding'] = 'gzip' + + result = self.middleware.process_response(fake_request, fake_response) + self.assertEquals('Cookie', result['Vary']) + self.assertEquals('gzip', result['Accept-Encoding']) diff --git a/lms/envs/common.py b/lms/envs/common.py index c73eaae451..f14d9d406a 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1087,7 +1087,7 @@ simplefilter('ignore') MIDDLEWARE_CLASSES = ( 'request_cache.middleware.RequestCache', - 'clean_headers.middleware.CleanHeadersMiddleware', + 'header_control.middleware.HeaderControlMiddleware', 'microsite_configuration.middleware.MicrositeMiddleware', 'django_comment_client.middleware.AjaxExceptionMiddleware', 'django.middleware.common.CommonMiddleware',