1from datetime import datetime, timedelta, tzinfo 2import re 3 4import pytest 5 6import falcon 7import falcon.testing as testing 8from falcon.util import http_date_to_dt, TimezoneGMT 9from falcon.util.compat import http_cookies 10 11 12UNICODE_TEST_STRING = u'Unicode_\xc3\xa6\xc3\xb8' 13 14 15class TimezoneGMTPlus1(tzinfo): 16 17 def utcoffset(self, dt): 18 return timedelta(hours=1) 19 20 def tzname(self, dt): 21 return 'GMT+1' 22 23 def dst(self, dt): 24 return timedelta(hours=1) 25 26 27GMT_PLUS_ONE = TimezoneGMTPlus1() 28 29 30class CookieResource: 31 32 def on_get(self, req, resp): 33 resp.set_cookie('foo', 'bar', domain='example.com', path='/') 34 35 def on_head(self, req, resp): 36 resp.set_cookie('foo', 'bar', max_age=300) 37 resp.set_cookie('bar', 'baz', http_only=False) 38 resp.set_cookie('bad', 'cookie') 39 resp.unset_cookie('bad') 40 41 def on_post(self, req, resp): 42 e = datetime(year=2050, month=1, day=1) # naive 43 resp.set_cookie('foo', 'bar', http_only=False, secure=False, expires=e) 44 resp.unset_cookie('bad') 45 46 def on_put(self, req, resp): 47 e = datetime(year=2050, month=1, day=1, tzinfo=GMT_PLUS_ONE) # aware 48 resp.set_cookie('foo', 'bar', http_only=False, secure=False, expires=e) 49 resp.unset_cookie('bad') 50 51 52class CookieResourceMaxAgeFloatString: 53 54 def on_get(self, req, resp): 55 resp.set_cookie( 56 'foofloat', 'bar', max_age=15.3, secure=False, http_only=False) 57 resp.set_cookie( 58 'foostring', 'bar', max_age='15', secure=False, http_only=False) 59 60 61@pytest.fixture() 62def client(): 63 app = falcon.API() 64 app.add_route('/', CookieResource()) 65 app.add_route('/test-convert', CookieResourceMaxAgeFloatString()) 66 67 return testing.TestClient(app) 68 69 70# ===================================================================== 71# Response 72# ===================================================================== 73 74 75def test_response_base_case(client): 76 result = client.simulate_get('/') 77 78 cookie = result.cookies['foo'] 79 assert cookie.name == 'foo' 80 assert cookie.value == 'bar' 81 assert cookie.domain == 'example.com' 82 assert cookie.http_only 83 84 # NOTE(kgriffs): Explicitly test for None to ensure 85 # falcon.testing.Cookie is returning exactly what we 86 # expect. Apps using falcon.testing.Cookie can be a 87 # bit more cavalier if they wish. 88 assert cookie.max_age is None 89 assert cookie.expires is None 90 91 assert cookie.path == '/' 92 assert cookie.secure 93 94 95def test_response_disable_secure_globally(client): 96 client.app.resp_options.secure_cookies_by_default = False 97 result = client.simulate_get('/') 98 cookie = result.cookies['foo'] 99 assert not cookie.secure 100 101 client.app.resp_options.secure_cookies_by_default = True 102 result = client.simulate_get('/') 103 cookie = result.cookies['foo'] 104 assert cookie.secure 105 106 107def test_response_complex_case(client): 108 result = client.simulate_head('/') 109 110 assert len(result.cookies) == 3 111 112 cookie = result.cookies['foo'] 113 assert cookie.value == 'bar' 114 assert cookie.domain is None 115 assert cookie.expires is None 116 assert cookie.http_only 117 assert cookie.max_age == 300 118 assert cookie.path is None 119 assert cookie.secure 120 121 cookie = result.cookies['bar'] 122 assert cookie.value == 'baz' 123 assert cookie.domain is None 124 assert cookie.expires is None 125 assert not cookie.http_only 126 assert cookie.max_age is None 127 assert cookie.path is None 128 assert cookie.secure 129 130 cookie = result.cookies['bad'] 131 assert cookie.value == '' # An unset cookie has an empty value 132 assert cookie.domain is None 133 134 assert cookie.expires < datetime.utcnow() 135 136 # NOTE(kgriffs): I know accessing a private attr like this is 137 # naughty of me, but we just need to sanity-check that the 138 # string is GMT. 139 assert cookie._expires.endswith('GMT') 140 141 assert cookie.http_only 142 assert cookie.max_age is None 143 assert cookie.path is None 144 assert cookie.secure 145 146 147def test_cookie_expires_naive(client): 148 result = client.simulate_post('/') 149 150 cookie = result.cookies['foo'] 151 assert cookie.value == 'bar' 152 assert cookie.domain is None 153 assert cookie.expires == datetime(year=2050, month=1, day=1) 154 assert not cookie.http_only 155 assert cookie.max_age is None 156 assert cookie.path is None 157 assert not cookie.secure 158 159 160def test_cookie_expires_aware(client): 161 result = client.simulate_put('/') 162 163 cookie = result.cookies['foo'] 164 assert cookie.value == 'bar' 165 assert cookie.domain is None 166 assert cookie.expires == datetime(year=2049, month=12, day=31, hour=23) 167 assert not cookie.http_only 168 assert cookie.max_age is None 169 assert cookie.path is None 170 assert not cookie.secure 171 172 173def test_cookies_setable(client): 174 resp = falcon.Response() 175 176 assert resp._cookies is None 177 178 resp.set_cookie('foo', 'wrong-cookie', max_age=301) 179 resp.set_cookie('foo', 'bar', max_age=300) 180 morsel = resp._cookies['foo'] 181 182 assert isinstance(morsel, http_cookies.Morsel) 183 assert morsel.key == 'foo' 184 assert morsel.value == 'bar' 185 assert morsel['max-age'] == 300 186 187 188@pytest.mark.parametrize('cookie_name', ('foofloat', 'foostring')) 189def test_cookie_max_age_float_and_string(client, cookie_name): 190 # NOTE(tbug): Falcon implicitly converts max-age values to integers, 191 # to ensure RFC 6265-compliance of the attribute value. 192 193 result = client.simulate_get('/test-convert') 194 195 cookie = result.cookies[cookie_name] 196 assert cookie.value == 'bar' 197 assert cookie.domain is None 198 assert cookie.expires is None 199 assert not cookie.http_only 200 assert cookie.max_age == 15 201 assert cookie.path is None 202 assert not cookie.secure 203 204 205def test_response_unset_cookie(client): 206 resp = falcon.Response() 207 resp.unset_cookie('bad') 208 resp.set_cookie('bad', 'cookie', max_age=300) 209 resp.unset_cookie('bad') 210 211 morsels = list(resp._cookies.values()) 212 len(morsels) == 1 213 214 bad_cookie = morsels[0] 215 bad_cookie['expires'] == -1 216 217 output = bad_cookie.OutputString() 218 assert 'bad=;' in output or 'bad="";' in output 219 220 match = re.search('expires=([^;]+)', output) 221 assert match 222 223 expiration = http_date_to_dt(match.group(1), obs_date=True) 224 assert expiration < datetime.utcnow() 225 226 227def test_cookie_timezone(client): 228 tz = TimezoneGMT() 229 assert tz.tzname(timedelta(0)) == 'GMT' 230 231 232# ===================================================================== 233# Request 234# ===================================================================== 235 236 237def test_request_cookie_parsing(): 238 # testing with a github-ish set of cookies 239 headers = [ 240 ( 241 'Cookie', 242 """ 243 logged_in=no;_gh_sess=eyJzZXXzaW9uX2lkIjoiN2; 244 tz=Europe/Berlin; _ga =GA1.2.332347814.1422308165; 245 tz2=Europe/Paris ; _ga2="line1\\012line2"; 246 tz3=Europe/Madrid ;_ga3= GA3.2.332347814.1422308165; 247 _gat=1; 248 _octo=GH1.1.201722077.1422308165 249 """ 250 ), 251 ] 252 253 environ = testing.create_environ(headers=headers) 254 req = falcon.Request(environ) 255 256 # NOTE(kgriffs): Test case-sensitivity 257 assert req.get_cookie_values('TZ') is None 258 assert 'TZ' not in req.cookies 259 with pytest.raises(KeyError): 260 req.cookies['TZ'] 261 262 for name, value in [ 263 ('logged_in', 'no'), 264 ('_gh_sess', 'eyJzZXXzaW9uX2lkIjoiN2'), 265 ('tz', 'Europe/Berlin'), 266 ('tz2', 'Europe/Paris'), 267 ('tz3', 'Europe/Madrid'), 268 ('_ga', 'GA1.2.332347814.1422308165'), 269 ('_ga2', 'line1\nline2'), 270 ('_ga3', 'GA3.2.332347814.1422308165'), 271 ('_gat', '1'), 272 ('_octo', 'GH1.1.201722077.1422308165'), 273 ]: 274 assert name in req.cookies 275 assert req.cookies[name] == value 276 assert req.get_cookie_values(name) == [value] 277 278 279def test_invalid_cookies_are_ignored(): 280 vals = [chr(i) for i in range(0x1F)] 281 vals += [chr(i) for i in range(0x7F, 0xFF)] 282 vals += '()<>@,;:\\"/[]?={} \x09'.split() 283 284 for c in vals: 285 headers = [ 286 ( 287 'Cookie', 288 'good_cookie=foo;bad' + c + 'cookie=bar' 289 ), 290 ] 291 292 environ = testing.create_environ(headers=headers) 293 req = falcon.Request(environ) 294 295 assert req.cookies['good_cookie'] == 'foo' 296 assert 'bad' + c + 'cookie' not in req.cookies 297 298 299def test_duplicate_cookie(): 300 headers = [ 301 ( 302 'Cookie', 303 'x=1;bad{cookie=bar; x=2;x=3 ; x=4;' 304 ), 305 ] 306 307 environ = testing.create_environ(headers=headers) 308 req = falcon.Request(environ) 309 310 assert req.cookies['x'] == '1' 311 assert req.get_cookie_values('x') == ['1', '2', '3', '4'] 312 313 314def test_cookie_header_is_missing(): 315 environ = testing.create_environ(headers={}) 316 317 req = falcon.Request(environ) 318 assert req.cookies == {} 319 assert req.get_cookie_values('x') is None 320 321 # NOTE(kgriffs): Test again with a new object to cover calling in the 322 # opposite order. 323 req = falcon.Request(environ) 324 assert req.get_cookie_values('x') is None 325 assert req.cookies == {} 326 327 328def test_unicode_inside_ascii_range(): 329 resp = falcon.Response() 330 331 # should be ok 332 resp.set_cookie('non_unicode_ascii_name_1', 'ascii_value') 333 resp.set_cookie(u'unicode_ascii_name_1', 'ascii_value') 334 resp.set_cookie('non_unicode_ascii_name_2', u'unicode_ascii_value') 335 resp.set_cookie(u'unicode_ascii_name_2', u'unicode_ascii_value') 336 337 338@pytest.mark.parametrize( 339 'name', 340 ( 341 UNICODE_TEST_STRING, 342 UNICODE_TEST_STRING.encode('utf-8'), 343 42 344 ) 345) 346def test_non_ascii_name(name): 347 resp = falcon.Response() 348 with pytest.raises(KeyError): 349 resp.set_cookie(name, 'ok_value') 350 351 352@pytest.mark.parametrize( 353 'value', 354 ( 355 UNICODE_TEST_STRING, 356 UNICODE_TEST_STRING.encode('utf-8'), 357 42 358 ) 359) 360def test_non_ascii_value(value): 361 resp = falcon.Response() 362 363 # NOTE(tbug): we need to grab the exception to check 364 # that it is not instance of UnicodeEncodeError, so 365 # we cannot simply use pytest.raises 366 try: 367 resp.set_cookie('ok_name', value) 368 except ValueError as e: 369 assert isinstance(e, ValueError) 370 assert not isinstance(e, UnicodeEncodeError) 371 else: 372 pytest.fail('set_bad_cookie_value did not fail as expected') 373