├── session_csrf ├── models.py ├── __init__.py └── tests.py ├── .gitignore ├── MANIFEST.in ├── requirements.txt ├── setup.py ├── runtests.sh ├── LICENSE └── README.rst /session_csrf/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.sw[po] 3 | *~ 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Install these to run the tests: 2 | django 3 | mock 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | ROOT = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | setup( 8 | name='django-session-csrf', 9 | version='0.5', 10 | description='CSRF protection for Django without cookies.', 11 | long_description=open(os.path.join(ROOT, 'README.rst')).read(), 12 | author='Jeff Balogh', 13 | author_email='jbalogh@mozilla.com', 14 | url='http://github.com/mozilla/django-session-csrf', 15 | license='BSD', 16 | packages=find_packages(), 17 | include_package_data=True, 18 | install_requires=['django'], 19 | zip_safe=False, 20 | classifiers=[ 21 | 'Development Status :: 4 - Beta', 22 | 'Environment :: Web Environment', 23 | 'Environment :: Web Environment :: Mozilla', 24 | 'Framework :: Django', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: BSD License', 27 | 'Operating System :: OS Independent', 28 | 'Programming Language :: Python', 29 | 'Topic :: Software Development :: Libraries :: Python Modules', 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SETTINGS='settings.py' 4 | 5 | cat > $SETTINGS <`_ 84 | for more information. 85 | 86 | 87 | If you only want a view to have CSRF protection for logged-in users, you can 88 | use the ``anonymous_csrf_exempt`` decorator. This could be useful if the 89 | anonymous view is protected through a CAPTCHA, for example. 90 | 91 | :: 92 | 93 | from session_csrf import anonymous_csrf_exempt 94 | 95 | @anonymous_csrf_exempt 96 | def protected_in_another_way(request): 97 | ... 98 | 99 | 100 | If you want all views to have CSRF protection for anonymous users, use 101 | the following setting: 102 | 103 | ``ANON_ALWAYS`` 104 | always provide CSRF protection for anonymous users 105 | 106 | Default: False 107 | 108 | 109 | Why do I want this? 110 | ------------------- 111 | 112 | 1. Your site is on a subdomain with other sites that are not under your 113 | control, so cookies could come from anywhere. 114 | 2. You're worried about attackers using Flash to forge HTTP headers. 115 | 3. You're tired of requiring a Referer header. 116 | 117 | 118 | Why don't I want this? 119 | ---------------------- 120 | 121 | 1. Storing tokens in sessions means you have to hit your session store more 122 | often. 123 | 2. It's a little bit more work to CSRF-protect forms for anonymous users. 124 | -------------------------------------------------------------------------------- /session_csrf/__init__.py: -------------------------------------------------------------------------------- 1 | """CSRF protection without cookies.""" 2 | import functools 3 | 4 | from django.conf import settings 5 | from django.core.cache import cache 6 | from django.middleware import csrf as django_csrf 7 | from django.utils import crypto 8 | from django.utils.cache import patch_vary_headers 9 | 10 | 11 | ANON_COOKIE = getattr(settings, 'ANON_COOKIE', 'anoncsrf') 12 | ANON_TIMEOUT = getattr(settings, 'ANON_TIMEOUT', 60 * 60 * 2) # 2 hours. 13 | ANON_ALWAYS = getattr(settings, 'ANON_ALWAYS', False) 14 | PREFIX = 'sessioncsrf:' 15 | 16 | 17 | # This overrides django.core.context_processors.csrf to dump our csrf_token 18 | # into the template context. 19 | def context_processor(request): 20 | # Django warns about an empty token unless you call it NOTPROVIDED. 21 | return {'csrf_token': getattr(request, 'csrf_token', 'NOTPROVIDED')} 22 | 23 | 24 | class CsrfMiddleware(object): 25 | 26 | # csrf_processing_done prevents checking CSRF more than once. That could 27 | # happen if the requires_csrf_token decorator is used. 28 | def _accept(self, request): 29 | request.csrf_processing_done = True 30 | 31 | def _reject(self, request, reason): 32 | return django_csrf._get_failure_view()(request, reason) 33 | 34 | def process_request(self, request): 35 | """ 36 | Add a CSRF token to the session for logged-in users. 37 | 38 | The token is available at request.csrf_token. 39 | """ 40 | if hasattr(request, 'csrf_token'): 41 | return 42 | if request.user.is_authenticated(): 43 | if 'csrf_token' not in request.session: 44 | token = django_csrf._get_new_csrf_key() 45 | request.csrf_token = request.session['csrf_token'] = token 46 | else: 47 | request.csrf_token = request.session['csrf_token'] 48 | else: 49 | key = None 50 | token = '' 51 | if ANON_COOKIE in request.COOKIES: 52 | key = request.COOKIES[ANON_COOKIE] 53 | token = cache.get(PREFIX + key, '') 54 | if ANON_ALWAYS: 55 | if not key: 56 | key = django_csrf._get_new_csrf_key() 57 | if not token: 58 | token = django_csrf._get_new_csrf_key() 59 | request._anon_csrf_key = key 60 | cache.set(PREFIX + key, token, ANON_TIMEOUT) 61 | request.csrf_token = token 62 | 63 | def process_view(self, request, view_func, args, kwargs): 64 | """Check the CSRF token if this is a POST.""" 65 | if getattr(request, 'csrf_processing_done', False): 66 | return 67 | 68 | # Allow @csrf_exempt views. 69 | if getattr(view_func, 'csrf_exempt', False): 70 | return 71 | 72 | if (getattr(view_func, 'anonymous_csrf_exempt', False) 73 | and not request.user.is_authenticated()): 74 | return 75 | 76 | # Bail if this is a safe method. 77 | if request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): 78 | return self._accept(request) 79 | 80 | # The test client uses this to get around CSRF processing. 81 | if getattr(request, '_dont_enforce_csrf_checks', False): 82 | return self._accept(request) 83 | 84 | # Try to get the token from the POST and fall back to looking at the 85 | # X-CSRFTOKEN header. 86 | user_token = request.POST.get('csrfmiddlewaretoken', '') 87 | if user_token == '': 88 | user_token = request.META.get('HTTP_X_CSRFTOKEN', '') 89 | 90 | request_token = getattr(request, 'csrf_token', '') 91 | 92 | # Check that both strings aren't empty and then check for a match. 93 | if not ((user_token or request_token) 94 | and crypto.constant_time_compare(user_token, request_token)): 95 | reason = django_csrf.REASON_BAD_TOKEN 96 | django_csrf.logger.warning( 97 | 'Forbidden (%s): %s' % (reason, request.path), 98 | extra=dict(status_code=403, request=request)) 99 | return self._reject(request, reason) 100 | else: 101 | return self._accept(request) 102 | 103 | def process_response(self, request, response): 104 | if hasattr(request, '_anon_csrf_key'): 105 | # Set or reset the cache and cookie timeouts. 106 | response.set_cookie(ANON_COOKIE, request._anon_csrf_key, 107 | max_age=ANON_TIMEOUT, httponly=True, 108 | secure=request.is_secure()) 109 | patch_vary_headers(response, ['Cookie']) 110 | return response 111 | 112 | 113 | def anonymous_csrf(f): 114 | """Decorator that assigns a CSRF token to an anonymous user.""" 115 | @functools.wraps(f) 116 | def wrapper(request, *args, **kw): 117 | use_anon_cookie = not (request.user.is_authenticated() or ANON_ALWAYS) 118 | if use_anon_cookie: 119 | if ANON_COOKIE in request.COOKIES: 120 | key = request.COOKIES[ANON_COOKIE] 121 | token = cache.get(PREFIX + key) or django_csrf._get_new_csrf_key() 122 | else: 123 | key = django_csrf._get_new_csrf_key() 124 | token = django_csrf._get_new_csrf_key() 125 | cache.set(PREFIX + key, token, ANON_TIMEOUT) 126 | request.csrf_token = token 127 | response = f(request, *args, **kw) 128 | if use_anon_cookie: 129 | # Set or reset the cache and cookie timeouts. 130 | response.set_cookie(ANON_COOKIE, key, max_age=ANON_TIMEOUT, 131 | httponly=True, secure=request.is_secure()) 132 | patch_vary_headers(response, ['Cookie']) 133 | return response 134 | return wrapper 135 | 136 | 137 | def anonymous_csrf_exempt(f): 138 | """Like @csrf_exempt but only for anonymous requests.""" 139 | f.anonymous_csrf_exempt = True 140 | return f 141 | 142 | 143 | # Replace Django's middleware with our own. 144 | def monkeypatch(): 145 | from django.views.decorators import csrf as csrf_dec 146 | django_csrf.CsrfViewMiddleware = CsrfMiddleware 147 | csrf_dec.csrf_protect = csrf_dec.decorator_from_middleware(CsrfMiddleware) 148 | -------------------------------------------------------------------------------- /session_csrf/tests.py: -------------------------------------------------------------------------------- 1 | import django.test 2 | from django import http 3 | from django.conf.urls.defaults import patterns 4 | from django.contrib.auth import logout 5 | from django.contrib.auth.models import User 6 | from django.contrib.sessions.models import Session 7 | from django.core import signals 8 | from django.core.cache import cache 9 | from django.core.handlers.wsgi import WSGIRequest 10 | from django.db import close_connection 11 | from django.template import context 12 | 13 | import mock 14 | 15 | import session_csrf 16 | from session_csrf import (anonymous_csrf, anonymous_csrf_exempt, 17 | CsrfMiddleware, PREFIX) 18 | 19 | 20 | urlpatterns = patterns('', 21 | ('^$', lambda r: http.HttpResponse()), 22 | ('^anon$', anonymous_csrf(lambda r: http.HttpResponse())), 23 | ('^no-anon-csrf$', anonymous_csrf_exempt(lambda r: http.HttpResponse())), 24 | ('^logout$', anonymous_csrf(lambda r: logout(r) or http.HttpResponse())), 25 | ) 26 | 27 | 28 | class TestCsrfToken(django.test.TestCase): 29 | 30 | def setUp(self): 31 | self.client.handler = ClientHandler() 32 | User.objects.create_user('jbalogh', 'j@moz.com', 'password') 33 | self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS 34 | session_csrf.ANON_ALWAYS = False 35 | 36 | def tearDown(self): 37 | session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS 38 | 39 | def login(self): 40 | assert self.client.login(username='jbalogh', password='password') 41 | 42 | def test_csrftoken_unauthenticated(self): 43 | # request.csrf_token is '' for anonymous users. 44 | response = self.client.get('/', follow=True) 45 | self.assertEqual(response._request.csrf_token, '') 46 | 47 | def test_csrftoken_authenticated(self): 48 | # request.csrf_token is a random non-empty string for authed users. 49 | self.login() 50 | response = self.client.get('/', follow=True) 51 | # The CSRF token is a 32-character MD5 string. 52 | self.assertEqual(len(response._request.csrf_token), 32) 53 | 54 | def test_csrftoken_new_session(self): 55 | # The csrf_token is added to request.session the first time. 56 | self.login() 57 | response = self.client.get('/', follow=True) 58 | # The CSRF token is a 32-character MD5 string. 59 | token = response._request.session['csrf_token'] 60 | self.assertEqual(len(token), 32) 61 | self.assertEqual(token, response._request.csrf_token) 62 | 63 | def test_csrftoken_existing_session(self): 64 | # The csrf_token in request.session is reused on subsequent requests. 65 | self.login() 66 | r1 = self.client.get('/', follow=True) 67 | token = r1._request.session['csrf_token'] 68 | 69 | r2 = self.client.get('/', follow=True) 70 | self.assertEqual(r1._request.csrf_token, r2._request.csrf_token) 71 | self.assertEqual(token, r2._request.csrf_token) 72 | 73 | 74 | class TestCsrfMiddleware(django.test.TestCase): 75 | 76 | def setUp(self): 77 | self.token = 'a' * 32 78 | self.rf = django.test.RequestFactory() 79 | self.mw = CsrfMiddleware() 80 | 81 | def process_view(self, request, view=None): 82 | return self.mw.process_view(request, view, None, None) 83 | 84 | def test_anon_token_from_cookie(self): 85 | rf = django.test.RequestFactory() 86 | rf.cookies['anoncsrf'] = self.token 87 | cache.set(PREFIX + self.token, 'woo') 88 | request = rf.get('/') 89 | request.session = {} 90 | r = { 91 | 'wsgi.input': django.test.client.FakePayload('') 92 | } 93 | # Hack to set up request middleware. 94 | ClientHandler()(self.rf._base_environ(**r)) 95 | self.mw.process_request(request) 96 | self.assertEqual(request.csrf_token, 'woo') 97 | 98 | def test_set_csrftoken_once(self): 99 | # Make sure process_request only sets request.csrf_token once. 100 | request = self.rf.get('/') 101 | request.csrf_token = 'woo' 102 | self.mw.process_request(request) 103 | self.assertEqual(request.csrf_token, 'woo') 104 | 105 | def test_reject_view(self): 106 | # Check that the reject view returns a 403. 107 | response = self.process_view(self.rf.post('/')) 108 | self.assertEqual(response.status_code, 403) 109 | 110 | def test_csrf_exempt(self): 111 | # Make sure @csrf_exempt still works. 112 | view = type("", (), {'csrf_exempt': True})() 113 | self.assertEqual(self.process_view(self.rf.post('/'), view), None) 114 | 115 | def test_safe_whitelist(self): 116 | # CSRF should not get checked on these methods. 117 | self.assertEqual(self.process_view(self.rf.get('/')), None) 118 | self.assertEqual(self.process_view(self.rf.head('/')), None) 119 | self.assertEqual(self.process_view(self.rf.options('/')), None) 120 | 121 | def test_unsafe_methods(self): 122 | self.assertEqual(self.process_view(self.rf.post('/')).status_code, 123 | 403) 124 | self.assertEqual(self.process_view(self.rf.put('/')).status_code, 125 | 403) 126 | self.assertEqual(self.process_view(self.rf.delete('/')).status_code, 127 | 403) 128 | 129 | def test_csrfmiddlewaretoken(self): 130 | # The user token should be found in POST['csrfmiddlewaretoken']. 131 | request = self.rf.post('/', {'csrfmiddlewaretoken': self.token}) 132 | self.assertEqual(self.process_view(request).status_code, 403) 133 | 134 | request.csrf_token = self.token 135 | self.assertEqual(self.process_view(request), None) 136 | 137 | def test_x_csrftoken(self): 138 | # The user token can be found in the X-CSRFTOKEN header. 139 | request = self.rf.post('/', HTTP_X_CSRFTOKEN=self.token) 140 | self.assertEqual(self.process_view(request).status_code, 403) 141 | 142 | request.csrf_token = self.token 143 | self.assertEqual(self.process_view(request), None) 144 | 145 | def test_require_request_token_or_user_token(self): 146 | # Blank request and user tokens raise an error on POST. 147 | request = self.rf.post('/', HTTP_X_CSRFTOKEN='') 148 | request.csrf_token = '' 149 | self.assertEqual(self.process_view(request).status_code, 403) 150 | 151 | def test_token_no_match(self): 152 | # A 403 is returned when the tokens don't match. 153 | request = self.rf.post('/', HTTP_X_CSRFTOKEN='woo') 154 | request.csrf_token = '' 155 | self.assertEqual(self.process_view(request).status_code, 403) 156 | 157 | def test_csrf_token_context_processor(self): 158 | # Our CSRF token should be available in the template context. 159 | request = mock.Mock() 160 | request.csrf_token = self.token 161 | request.groups = [] 162 | ctx = {} 163 | for processor in context.get_standard_processors(): 164 | ctx.update(processor(request)) 165 | self.assertEqual(ctx['csrf_token'], self.token) 166 | 167 | 168 | class TestAnonymousCsrf(django.test.TestCase): 169 | urls = 'session_csrf.tests' 170 | 171 | def setUp(self): 172 | self.token = 'a' * 32 173 | self.rf = django.test.RequestFactory() 174 | User.objects.create_user('jbalogh', 'j@moz.com', 'password') 175 | self.client.handler = ClientHandler(enforce_csrf_checks=True) 176 | self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS 177 | session_csrf.ANON_ALWAYS = False 178 | 179 | def tearDown(self): 180 | session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS 181 | 182 | def login(self): 183 | assert self.client.login(username='jbalogh', password='password') 184 | 185 | def test_authenticated_request(self): 186 | # Nothing special happens, nothing breaks. 187 | # Find the CSRF token in the session. 188 | self.login() 189 | response = self.client.get('/anon') 190 | sessionid = response.cookies['sessionid'].value 191 | session = Session.objects.get(session_key=sessionid) 192 | token = session.get_decoded()['csrf_token'] 193 | 194 | response = self.client.post('/anon', HTTP_X_CSRFTOKEN=token) 195 | self.assertEqual(response.status_code, 200) 196 | 197 | def test_unauthenticated_request(self): 198 | # We get a 403 since we're not sending a token. 199 | response = self.client.post('/anon') 200 | self.assertEqual(response.status_code, 403) 201 | 202 | def test_no_anon_cookie(self): 203 | # We don't get an anon cookie on non-@anonymous_csrf views. 204 | response = self.client.get('/') 205 | self.assertEqual(response.cookies, {}) 206 | 207 | def test_new_anon_token_on_request(self): 208 | # A new anon user gets a key+token on the request and response. 209 | response = self.client.get('/anon') 210 | # Get the key from the cookie and find the token in the cache. 211 | key = response.cookies['anoncsrf'].value 212 | self.assertEqual(response._request.csrf_token, cache.get(PREFIX + key)) 213 | 214 | def test_existing_anon_cookie_on_request(self): 215 | # We reuse an existing anon cookie key+token. 216 | response = self.client.get('/anon') 217 | key = response.cookies['anoncsrf'].value 218 | 219 | # Now check that subsequent requests use that cookie. 220 | response = self.client.get('/anon') 221 | self.assertEqual(response.cookies['anoncsrf'].value, key) 222 | self.assertEqual(response._request.csrf_token, cache.get(PREFIX + key)) 223 | 224 | def test_new_anon_token_on_response(self): 225 | # The anon cookie is sent and we vary on Cookie. 226 | response = self.client.get('/anon') 227 | self.assertIn('anoncsrf', response.cookies) 228 | self.assertEqual(response['Vary'], 'Cookie') 229 | 230 | def test_existing_anon_token_on_response(self): 231 | # The anon cookie is sent and we vary on Cookie, reusing the old value. 232 | response = self.client.get('/anon') 233 | key = response.cookies['anoncsrf'].value 234 | 235 | response = self.client.get('/anon') 236 | self.assertEqual(response.cookies['anoncsrf'].value, key) 237 | self.assertIn('anoncsrf', response.cookies) 238 | self.assertEqual(response['Vary'], 'Cookie') 239 | 240 | def test_anon_csrf_logout(self): 241 | # Beware of views that logout the user. 242 | self.login() 243 | response = self.client.get('/logout') 244 | self.assertEqual(response.status_code, 200) 245 | 246 | def test_existing_anon_cookie_not_in_cache(self): 247 | response = self.client.get('/anon') 248 | self.assertEqual(len(response._request.csrf_token), 32) 249 | 250 | # Clear cache and make sure we still get a token 251 | cache.clear() 252 | response = self.client.get('/anon') 253 | self.assertEqual(len(response._request.csrf_token), 32) 254 | 255 | def test_anonymous_csrf_exempt(self): 256 | response = self.client.post('/no-anon-csrf') 257 | self.assertEqual(response.status_code, 200) 258 | 259 | self.login() 260 | response = self.client.post('/no-anon-csrf') 261 | self.assertEqual(response.status_code, 403) 262 | 263 | 264 | class TestAnonAlways(django.test.TestCase): 265 | # Repeats some tests with ANON_ALWAYS = True 266 | urls = 'session_csrf.tests' 267 | 268 | def setUp(self): 269 | self.token = 'a' * 32 270 | self.rf = django.test.RequestFactory() 271 | User.objects.create_user('jbalogh', 'j@moz.com', 'password') 272 | self.client.handler = ClientHandler(enforce_csrf_checks=True) 273 | self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS 274 | session_csrf.ANON_ALWAYS = True 275 | 276 | def tearDown(self): 277 | session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS 278 | 279 | def login(self): 280 | assert self.client.login(username='jbalogh', password='password') 281 | 282 | def test_csrftoken_unauthenticated(self): 283 | # request.csrf_token is set for anonymous users 284 | # when ANON_ALWAYS is enabled. 285 | response = self.client.get('/', follow=True) 286 | # The CSRF token is a 32-character MD5 string. 287 | self.assertEqual(len(response._request.csrf_token), 32) 288 | 289 | def test_authenticated_request(self): 290 | # Nothing special happens, nothing breaks. 291 | # Find the CSRF token in the session. 292 | self.login() 293 | response = self.client.get('/', follow=True) 294 | sessionid = response.cookies['sessionid'].value 295 | session = Session.objects.get(session_key=sessionid) 296 | token = session.get_decoded()['csrf_token'] 297 | 298 | response = self.client.post('/', follow=True, HTTP_X_CSRFTOKEN=token) 299 | self.assertEqual(response.status_code, 200) 300 | 301 | def test_unauthenticated_request(self): 302 | # We get a 403 since we're not sending a token. 303 | response = self.client.post('/') 304 | self.assertEqual(response.status_code, 403) 305 | 306 | def test_new_anon_token_on_request(self): 307 | # A new anon user gets a key+token on the request and response. 308 | response = self.client.get('/') 309 | # Get the key from the cookie and find the token in the cache. 310 | key = response.cookies['anoncsrf'].value 311 | self.assertEqual(response._request.csrf_token, cache.get(PREFIX + key)) 312 | 313 | def test_existing_anon_cookie_on_request(self): 314 | # We reuse an existing anon cookie key+token. 315 | response = self.client.get('/') 316 | key = response.cookies['anoncsrf'].value 317 | 318 | # Now check that subsequent requests use that cookie. 319 | response = self.client.get('/') 320 | self.assertEqual(response.cookies['anoncsrf'].value, key) 321 | self.assertEqual(response._request.csrf_token, cache.get(PREFIX + key)) 322 | self.assertEqual(response['Vary'], 'Cookie') 323 | 324 | def test_anon_csrf_logout(self): 325 | # Beware of views that logout the user. 326 | self.login() 327 | response = self.client.get('/logout') 328 | self.assertEqual(response.status_code, 200) 329 | 330 | def test_existing_anon_cookie_not_in_cache(self): 331 | response = self.client.get('/') 332 | self.assertEqual(len(response._request.csrf_token), 32) 333 | 334 | # Clear cache and make sure we still get a token 335 | cache.clear() 336 | response = self.client.get('/') 337 | self.assertEqual(len(response._request.csrf_token), 32) 338 | 339 | 340 | class ClientHandler(django.test.client.ClientHandler): 341 | """ 342 | Handler that stores the real request object on the response. 343 | 344 | Almost all the code comes from the parent class. 345 | """ 346 | 347 | def __call__(self, environ): 348 | # Set up middleware if needed. We couldn't do this earlier, because 349 | # settings weren't available. 350 | if self._request_middleware is None: 351 | self.load_middleware() 352 | 353 | signals.request_started.send(sender=self.__class__) 354 | try: 355 | request = WSGIRequest(environ) 356 | # sneaky little hack so that we can easily get round 357 | # CsrfViewMiddleware. This makes life easier, and is probably 358 | # required for backwards compatibility with external tests against 359 | # admin views. 360 | request._dont_enforce_csrf_checks = not self.enforce_csrf_checks 361 | response = self.get_response(request) 362 | finally: 363 | signals.request_finished.disconnect(close_connection) 364 | signals.request_finished.send(sender=self.__class__) 365 | signals.request_finished.connect(close_connection) 366 | 367 | # Store the request object. 368 | response._request = request 369 | return response 370 | --------------------------------------------------------------------------------