Projet

Général

Profil

0001-misc-let-django-generate-set-cookies-headers-72613.patch

Benjamin Dauvergne, 23 décembre 2022 11:01

Télécharger (8,05 ko)

Voir les différences:

Subject: [PATCH 1/4] 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.
 tests/form_pages/test_all.py |  4 ++--
 tests/test_auth_pages.py     |  2 +-
 tests/test_saml_auth.py      |  2 +-
 wcs/compat.py                | 32 ++++++++++++++++++++++++++++++++
 wcs/middleware.py            |  3 ++-
 wcs/qommon/http_response.py  | 24 +-----------------------
 6 files changed, 39 insertions(+), 28 deletions(-)
tests/form_pages/test_all.py
5711 5711
    create_formdef()
5712 5712
    app = get_app(pub)
5713 5713
    resp = app.get('/test/', status=200)
5714
    assert resp.headers['Set-Cookie'].startswith('sessionid-')
5714
    assert resp.headers['Set-Cookie'].strip().startswith('sessionid-')
5715 5715
    assert 'HttpOnly' in resp.headers['Set-Cookie']
5716 5716
    assert 'Secure' not in resp.headers['Set-Cookie']
5717 5717

  
5718 5718
    app = get_app(pub, https=True)
5719 5719
    resp = app.get('/test/', status=200)
5720
    assert resp.headers['Set-Cookie'].startswith('sessionid-')
5720
    assert resp.headers['Set-Cookie'].strip().startswith('sessionid-')
5721 5721
    assert 'HttpOnly' in resp.headers['Set-Cookie']
5722 5722
    assert 'Secure' in resp.headers['Set-Cookie']
5723 5723

  
tests/test_auth_pages.py
67 67
    assert list(cookie_store.keys()) == [cookie_name]
68 68
    assert 'HttpOnly' in resp.headers['Set-Cookie']
69 69
    assert 'SameSite=None' in resp.headers['Set-Cookie']
70
    assert 'path=/' in resp.headers['Set-Cookie']
70
    assert 'Path=/' in resp.headers['Set-Cookie']
71 71

  
72 72

  
73 73
def test_login_logout(pub):
tests/test_saml_auth.py
607 607
    assert 'Secure' in resp.headers['Set-Cookie']
608 608
    assert 'HttpOnly' in resp.headers['Set-Cookie']
609 609
    assert 'SameSite=None' in resp.headers['Set-Cookie']
610
    assert 'path=/' in resp.headers['Set-Cookie']
610
    assert 'Path=/' in resp.headers['Set-Cookie']
611 611
    assert resp.status_int == 302
612 612
    assert (
613 613
        resp.location
wcs/compat.py
33 33
from .qommon.publisher import set_publisher_class
34 34

  
35 35

  
36
def transfer_cookies(quixote_response, django_response):
37
    for name, attrs in quixote_response.cookies.items():
38
        value = str(attrs['value'])
39
        if 'samesite' not in attrs:
40
            attrs['samesite'] = 'None'
41
        kwargs = {}
42
        samesite_none = False
43
        for attr, val in attrs.items():
44
            attr = attr.lower()
45
            if val is None:
46
                continue
47
            if attr == 'comment':
48
                continue
49
            if attr == 'samesite' and val.lower() == 'none':
50
                samesite_none = True
51
            elif attr in ('expires', 'domain', 'path', 'max_age', 'samesite'):
52
                kwargs[attr] = val
53
            elif attr in ('httponly', 'secure') and val:
54
                kwargs[attr] = True
55
        django_response.set_cookie(name, value, **kwargs)
56
        # work around absent support for None in django 2.2
57
        if samesite_none:
58
            django_response.cookies[name]['samesite'] = 'None'
59

  
60

  
36 61
class TemplateWithFallbackView(TemplateView):
37 62
    quixote_response = None
38 63

  
......
70 95
        else:
71 96
            response = self.render_to_response(context)
72 97

  
98
            transfer_cookies(self.quixote_response, response)
99

  
73 100
        for name, value in self.quixote_response.generate_headers():
74 101
            if name in ('Connection', 'Content-Length'):
75 102
                continue
......
83 110
        if self.quixote_response and self.quixote_response.status_code != 200:
84 111
            django_response.status_code = self.quixote_response.status_code
85 112
            django_response.reason_phrase = self.quixote_response.reason_phrase
113
            transfer_cookies(self.quixote_response, django_response)
86 114
            for name, value in self.quixote_response.generate_headers():
87 115
                if name in ('Connection', 'Content-Length'):
88 116
                    continue
......
211 239
            self.session_manager.finish_successful_request()
212 240
            request.ignore_session = True  # no further changes
213 241

  
242
        transfer_cookies(response, django_response)
243

  
214 244
        for name, value in response.generate_headers():
215 245
            if name in ('Connection', 'Content-Length'):
216 246
                continue
......
264 294
            reason=request.response.reason_phrase,
265 295
        )
266 296

  
297
        transfer_cookies(request.response, django_response)
298

  
267 299
        for name, value in request.response.generate_headers():
268 300
            if name in ('Connection', 'Content-Length'):
269 301
                continue
wcs/middleware.py
26 26
from quixote import get_publisher
27 27
from quixote.errors import RequestError
28 28

  
29
from .compat import CompatHTTPRequest, CompatWcsPublisher
29
from .compat import CompatHTTPRequest, CompatWcsPublisher, transfer_cookies
30 30
from .qommon.http_response import HTTPResponse
31 31
from .qommon.publisher import ImmediateRedirectException
32 32

  
......
87 87
                if compat_request.form:
88 88
                    new_query_string = '?' + urllib.parse.urlencode(compat_request.form)
89 89
                response = HttpResponseRedirect(compat_request.get_path() + new_query_string)
90
                transfer_cookies(compat_request.response, response)
90 91
                for name, value in compat_request.response.generate_headers():
91 92
                    if name == 'Content-Length':
92 93
                        continue
wcs/qommon/http_response.py
35 35
            self.charset = get_publisher().site_charset
36 36

  
37 37
    def _gen_cookie_headers(self):
38
        cookie_headers = []
39
        for name, attrs in self.cookies.items():
40
            value = str(attrs['value'])
41
            if '"' in value:
42
                value = value.replace('"', '\\"')
43
            chunks = ['%s="%s"' % (name, value)]
44
            if 'samesite' not in attrs:
45
                attrs['samesite'] = 'None'
46
            for name, val in attrs.items():
47
                name = name.lower()
48
                if val is None:
49
                    continue
50
                if name in ('expires', 'domain', 'path', 'max_age', 'comment'):
51
                    name = name.replace('_', '-')
52
                    chunks.append('%s=%s' % (name, val))
53
                elif name == 'samesite':
54
                    chunks.append('SameSite=%s' % val)
55
                elif name == 'secure' and val:
56
                    chunks.append('Secure')
57
                elif name == 'httponly' and val:
58
                    chunks.append('HttpOnly')
59
            cookie_headers.append(('Set-Cookie', '; '.join(chunks)))
60
        return cookie_headers
38
        return []
61 39

  
62 40
    def reset_includes(self):
63 41
        self.javascript_scripts = None
64
-