misc: let django generate set-cookies headers (#72613)

Django's HttpResponse cannot hold more than one value for an HTTP
headers, so if multiple Set-Cookie are produced by a Quixote
HttpResponse the first set-cookie headers will be overwritten by the
last one.
This commit is contained in:
Benjamin Dauvergne 2022-12-19 19:07:06 +01:00 committed by Frédéric Péters
parent 49c1d439b5
commit 2fcdde657a
6 changed files with 39 additions and 28 deletions

View File

@ -5733,13 +5733,13 @@ def test_session_cookie_flags(pub):
create_formdef()
app = get_app(pub)
resp = app.get('/test/', status=200)
assert resp.headers['Set-Cookie'].startswith('sessionid-')
assert resp.headers['Set-Cookie'].strip().startswith('sessionid-')
assert 'HttpOnly' in resp.headers['Set-Cookie']
assert 'Secure' not in resp.headers['Set-Cookie']
app = get_app(pub, https=True)
resp = app.get('/test/', status=200)
assert resp.headers['Set-Cookie'].startswith('sessionid-')
assert resp.headers['Set-Cookie'].strip().startswith('sessionid-')
assert 'HttpOnly' in resp.headers['Set-Cookie']
assert 'Secure' in resp.headers['Set-Cookie']

View File

@ -67,7 +67,7 @@ def test_login_cookie(pub):
assert list(cookie_store.keys()) == [cookie_name]
assert 'HttpOnly' in resp.headers['Set-Cookie']
assert 'SameSite=None' in resp.headers['Set-Cookie']
assert 'path=/' in resp.headers['Set-Cookie']
assert 'Path=/' in resp.headers['Set-Cookie']
def test_login_logout(pub):

View File

@ -607,7 +607,7 @@ def test_opened_session_cookie(pub):
assert 'Secure' in resp.headers['Set-Cookie']
assert 'HttpOnly' in resp.headers['Set-Cookie']
assert 'SameSite=None' in resp.headers['Set-Cookie']
assert 'path=/' in resp.headers['Set-Cookie']
assert 'Path=/' in resp.headers['Set-Cookie']
assert resp.status_int == 302
assert (
resp.location

View File

@ -33,6 +33,31 @@ from .qommon.http_request import HTTPRequest
from .qommon.publisher import set_publisher_class
def transfer_cookies(quixote_response, django_response):
for name, attrs in quixote_response.cookies.items():
value = str(attrs['value'])
if 'samesite' not in attrs:
attrs['samesite'] = 'None'
kwargs = {}
samesite_none = False
for attr, val in attrs.items():
attr = attr.lower()
if val is None:
continue
if attr == 'comment':
continue
if attr == 'samesite' and val.lower() == 'none':
samesite_none = True
elif attr in ('expires', 'domain', 'path', 'max_age', 'samesite'):
kwargs[attr] = val
elif attr in ('httponly', 'secure') and val:
kwargs[attr] = True
django_response.set_cookie(name, value, **kwargs)
# work around absent support for None in django 2.2
if samesite_none:
django_response.cookies[name]['samesite'] = 'None'
class TemplateWithFallbackView(TemplateView):
quixote_response = None
@ -70,6 +95,8 @@ class TemplateWithFallbackView(TemplateView):
else:
response = self.render_to_response(context)
transfer_cookies(self.quixote_response, response)
for name, value in self.quixote_response.generate_headers():
if name in ('Connection', 'Content-Length'):
continue
@ -83,6 +110,7 @@ class TemplateWithFallbackView(TemplateView):
if self.quixote_response and self.quixote_response.status_code != 200:
django_response.status_code = self.quixote_response.status_code
django_response.reason_phrase = self.quixote_response.reason_phrase
transfer_cookies(self.quixote_response, django_response)
for name, value in self.quixote_response.generate_headers():
if name in ('Connection', 'Content-Length'):
continue
@ -211,6 +239,8 @@ class CompatWcsPublisher(WcsPublisher):
self.session_manager.finish_successful_request()
request.ignore_session = True # no further changes
transfer_cookies(response, django_response)
for name, value in response.generate_headers():
if name in ('Connection', 'Content-Length'):
continue
@ -264,6 +294,8 @@ class PublishErrorMiddleware(MiddlewareMixin):
reason=request.response.reason_phrase,
)
transfer_cookies(request.response, django_response)
for name, value in request.response.generate_headers():
if name in ('Connection', 'Content-Length'):
continue

View File

@ -26,7 +26,7 @@ from django.utils.deprecation import MiddlewareMixin
from quixote import get_publisher
from quixote.errors import RequestError
from .compat import CompatHTTPRequest, CompatWcsPublisher
from .compat import CompatHTTPRequest, CompatWcsPublisher, transfer_cookies
from .qommon.http_response import HTTPResponse
from .qommon.publisher import ImmediateRedirectException
@ -87,6 +87,7 @@ class PublisherInitialisationMiddleware(MiddlewareMixin):
if compat_request.form:
new_query_string = '?' + urllib.parse.urlencode(compat_request.form)
response = HttpResponseRedirect(compat_request.get_path() + new_query_string)
transfer_cookies(compat_request.response, response)
for name, value in compat_request.response.generate_headers():
if name == 'Content-Length':
continue

View File

@ -35,29 +35,7 @@ class HTTPResponse(quixote.http_response.HTTPResponse):
self.charset = get_publisher().site_charset
def _gen_cookie_headers(self):
cookie_headers = []
for name, attrs in self.cookies.items():
value = str(attrs['value'])
if '"' in value:
value = value.replace('"', '\\"')
chunks = ['%s="%s"' % (name, value)]
if 'samesite' not in attrs:
attrs['samesite'] = 'None'
for name, val in attrs.items():
name = name.lower()
if val is None:
continue
if name in ('expires', 'domain', 'path', 'max_age', 'comment'):
name = name.replace('_', '-')
chunks.append('%s=%s' % (name, val))
elif name == 'samesite':
chunks.append('SameSite=%s' % val)
elif name == 'secure' and val:
chunks.append('Secure')
elif name == 'httponly' and val:
chunks.append('HttpOnly')
cookie_headers.append(('Set-Cookie', '; '.join(chunks)))
return cookie_headers
return []
def reset_includes(self):
self.javascript_scripts = None