comparison test/test_liveserver.py @ 6693:9a1f5e496e6c

issue2551203 - Add support for CORS preflight request Add support for unauthenticated CORS preflight and fix headers for CORS. client.py: pass through unauthenticated CORS preflight to rest backend. Normal rest OPTION handlers (including tracker defined extensions) can see and handle the request. make some error cases return error json with crrect mime type rather than plain text tracebacks. create new functions to verify origin and referer that filter using allowed origins setting. remove tracker base url from error message is referer is not at an allowed origin. rest.py: fix up OPTION methods handlers to include Access-Control-Allow-Methods that are the same as the Allow header. set cache to one week for all Access-Control headers for CORS preflight only. remove self.client.setHeader("Access-Control-Allow-Origin", "*") and set Access-Control-Allow-Origin to the client supplied origin if it passes allowed origin checks. Required for CORS otherwise data isn't available to caller. Set for all responses. set Vary header now includes Origin as responses can differ based on Origin for all responses. set Access-Control-Allow-Credentials to true on all responses. test_liveserver.py: run server with setting to enforce origin csrf header check run server with setting to enforce x-requested-with csrf header check run server with setting for allowed_api_origins requests now set required csrf headers test preflight request on collections check new headers and Origin is no longer '*' rewrite all compression checks to use a single method with argument to use different compression methods. Reduce a lot of code duplication and makes updating for new headers easier. test_cgi: test new error messages in client.py account for new headers test preflight and new code paths
author John Rouillard <rouilj@ieee.org>
date Tue, 07 Jun 2022 09:39:35 -0400
parents a193653d6fa4
children d32d43e4a5ba
comparison
equal deleted inserted replaced
6692:1fbfb4a277d7 6693:9a1f5e496e6c
66 cls.db.config['TRACKER_WEB'] = "http://localhost:9001/" 66 cls.db.config['TRACKER_WEB'] = "http://localhost:9001/"
67 # set up mailhost so errors get reported to debuging capture file 67 # set up mailhost so errors get reported to debuging capture file
68 cls.db.config.MAILHOST = "localhost" 68 cls.db.config.MAILHOST = "localhost"
69 cls.db.config.MAIL_HOST = "localhost" 69 cls.db.config.MAIL_HOST = "localhost"
70 cls.db.config.MAIL_DEBUG = "../_test_tracker_mail.log" 70 cls.db.config.MAIL_DEBUG = "../_test_tracker_mail.log"
71 cls.db.config.WEB_CSRF_ENFORCE_HEADER_ORIGIN = "required"
72 cls.db.config.WEB_ALLOWED_API_ORIGINS = "https://client.com"
73
74 cls.db.config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] = "required"
71 75
72 # enable static precompressed files 76 # enable static precompressed files
73 cls.db.config.WEB_USE_PRECOMPRESSED_FILES = 1 77 cls.db.config.WEB_USE_PRECOMPRESSED_FILES = 1
74 78
75 cls.db.config.save() 79 cls.db.config.save()
235 f = requests.get(self.url_base() + "/@@file/style.css", headers=hdrs) 239 f = requests.get(self.url_base() + "/@@file/style.css", headers=hdrs)
236 self.assertEqual(f.status_code, 416) 240 self.assertEqual(f.status_code, 416)
237 self.assertEqual(f.headers['content-range'], 241 self.assertEqual(f.headers['content-range'],
238 "bytes */%s"%expected_length) 242 "bytes */%s"%expected_length)
239 243
244 def test_rest_preflight_collection(self):
245 # no auth for rest csrf preflight
246 f = requests.options(self.url_base() + '/rest/data/user',
247 headers = {'content-type': "",
248 'x-requested-with': "rest",
249 'Access-Control-Request-Headers':
250 "x-requested-with",
251 'Access-Control-Request-Method': "PUT",
252 'Origin': "https://client.com"})
253 print(f.status_code)
254 print(f.headers)
255 print(f.content)
256
257 self.assertEqual(f.status_code, 204)
258
259 expected = { 'Access-Control-Allow-Origin': 'https://client.com',
260 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
261 'Allow': 'OPTIONS, GET, POST',
262 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
263 'Access-Control-Allow-Credentials': 'true',
264 }
265
266 # use dict comprehension to filter headers to the ones we want to check
267 self.assertEqual({ key: value for (key, value) in
268 f.headers.items() if key in expected },
269 expected)
270
271 # use invalid Origin
272 f = requests.options(self.url_base() + '/rest/data/user',
273 headers = {'content-type': "application/json",
274 'x-requested-with': "rest",
275 'Access-Control-Request-Headers':
276 "x-requested-with",
277 'Access-Control-Request-Method': "PUT",
278 'Origin': "ZZZ"})
279
280 self.assertEqual(f.status_code, 400)
281
282 expected = '{ "error": { "status": 400, "msg": "Client is not ' \
283 'allowed to use Rest Interface." } }'
284 self.assertEqual(b2s(f.content), expected)
285
286
240 def test_rest_invalid_method_collection(self): 287 def test_rest_invalid_method_collection(self):
241 # use basic auth for rest endpoint 288 # use basic auth for rest endpoint
242 f = requests.put(self.url_base() + '/rest/data/user', 289 f = requests.put(self.url_base() + '/rest/data/user',
243 auth=('admin', 'sekrit'), 290 auth=('admin', 'sekrit'),
244 headers = {'content-type': "", 291 headers = {'content-type': "",
245 'x-requested-with': "rest"}) 292 'X-Requested-With': "rest",
293 'Origin': "https://client.com"})
246 print(f.status_code) 294 print(f.status_code)
247 print(f.headers) 295 print(f.headers)
248 print(f.content) 296 print(f.content)
249 297
250 self.assertEqual(f.status_code, 405) 298 self.assertEqual(f.status_code, 405)
251 expected = { 'Access-Control-Allow-Origin': '*', 299 expected = { 'Access-Control-Allow-Origin': 'https://client.com',
252 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override', 300 'Access-Control-Allow-Credentials': 'true',
253 'Allow': 'DELETE, GET, OPTIONS, POST', 301 'Allow': 'DELETE, GET, OPTIONS, POST',
254 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
255 } 302 }
256 303
257 print(f.headers) 304 print(f.headers)
258 # use dict comprehension to remove fields like date, 305 # use dict comprehension to remove fields like date,
259 # content-length etc. from f.headers. 306 # content-length etc. from f.headers.
275 322
276 def test_rest_endpoint_root_options(self): 323 def test_rest_endpoint_root_options(self):
277 # use basic auth for rest endpoint 324 # use basic auth for rest endpoint
278 f = requests.options(self.url_base() + '/rest', 325 f = requests.options(self.url_base() + '/rest',
279 auth=('admin', 'sekrit'), 326 auth=('admin', 'sekrit'),
280 headers = {'content-type': ""}) 327 headers = {'content-type': "",
328 'Origin': "http://localhost:9001",
329 })
281 print(f.status_code) 330 print(f.status_code)
282 print(f.headers) 331 print(f.headers)
283 332
284 self.assertEqual(f.status_code, 204) 333 self.assertEqual(f.status_code, 204)
285 expected = { 'Access-Control-Allow-Origin': '*', 334 expected = { 'Access-Control-Allow-Origin': 'http://localhost:9001',
286 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override', 335 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
287 'Allow': 'OPTIONS, GET', 336 'Allow': 'OPTIONS, GET',
288 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH', 337 'Access-Control-Allow-Credentials': 'true',
338 'Access-Control-Allow-Methods': 'OPTIONS, GET',
339 'Access-Control-Allow-Credentials': 'true',
289 } 340 }
290 341
291 # use dict comprehension to remove fields like date, 342 # use dict comprehension to remove fields like date,
292 # content-length etc. from f.headers. 343 # content-length etc. from f.headers.
293 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 344 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
294 345
295 def test_rest_endpoint_data_options(self): 346 def test_rest_endpoint_data_options(self):
296 # use basic auth for rest endpoint 347 # use basic auth for rest endpoint
297 f = requests.options(self.url_base() + '/rest/data', 348 f = requests.options(self.url_base() + '/rest/data',
298 auth=('admin', 'sekrit'), 349 auth=('admin', 'sekrit'),
299 headers = {'content-type': ""} 350 headers = {'content-type': "",
300 ) 351 'Origin': "http://localhost:9001",
352 })
301 print(f.status_code) 353 print(f.status_code)
302 print(f.headers) 354 print(f.headers)
303 355
304 self.assertEqual(f.status_code, 204) 356 self.assertEqual(f.status_code, 204)
305 expected = { 'Access-Control-Allow-Origin': '*', 357 expected = { 'Access-Control-Allow-Origin': 'http://localhost:9001',
306 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override', 358 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
307 'Allow': 'OPTIONS, GET', 359 'Allow': 'OPTIONS, GET',
308 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH', 360 'Access-Control-Allow-Methods': 'OPTIONS, GET',
361 'Access-Control-Allow-Credentials': 'true',
309 } 362 }
310 363
311 # use dict comprehension to remove fields like date, 364 # use dict comprehension to remove fields like date,
312 # content-length etc. from f.headers. 365 # content-length etc. from f.headers.
313 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 366 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
314 367
315 def test_rest_endpoint_collection_options(self): 368 def test_rest_endpoint_collection_options(self):
316 # use basic auth for rest endpoint 369 # use basic auth for rest endpoint
317 f = requests.options(self.url_base() + '/rest/data/user', 370 f = requests.options(self.url_base() + '/rest/data/user',
318 auth=('admin', 'sekrit'), 371 auth=('admin', 'sekrit'),
319 headers = {'content-type': ""}) 372 headers = {'content-type': "",
373 'Origin': "http://localhost:9001",
374 })
320 print(f.status_code) 375 print(f.status_code)
321 print(f.headers) 376 print(f.headers)
322 377
323 self.assertEqual(f.status_code, 204) 378 self.assertEqual(f.status_code, 204)
324 expected = { 'Access-Control-Allow-Origin': '*', 379 expected = { 'Access-Control-Allow-Origin': 'http://localhost:9001',
325 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override', 380 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
326 'Allow': 'OPTIONS, GET, POST', 381 'Allow': 'OPTIONS, GET, POST',
327 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST', 382 'Access-Control-Allow-Methods': 'OPTIONS, GET, POST',
383 'Access-Control-Allow-Credentials': 'true',
328 } 384 }
329 385
330 # use dict comprehension to remove fields like date, 386 # use dict comprehension to remove fields like date,
331 # content-length etc. from f.headers. 387 # content-length etc. from f.headers.
332 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 388 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
334 390
335 def test_rest_endpoint_item_options(self): 391 def test_rest_endpoint_item_options(self):
336 392
337 f = requests.options(self.url_base() + '/rest/data/user/1', 393 f = requests.options(self.url_base() + '/rest/data/user/1',
338 auth=('admin', 'sekrit'), 394 auth=('admin', 'sekrit'),
339 headers = {'content-type': ""}) 395 headers = {'content-type': "",
396 'Origin': "http://localhost:9001",
397 })
340 print(f.status_code) 398 print(f.status_code)
341 print(f.headers) 399 print(f.headers)
342 400
343 self.assertEqual(f.status_code, 204) 401 self.assertEqual(f.status_code, 204)
344 expected = { 'Access-Control-Allow-Origin': '*', 402 expected = { 'Access-Control-Allow-Origin': 'http://localhost:9001',
345 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override', 403 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
346 'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH', 404 'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH',
347 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH', 405 'Access-Control-Allow-Methods': 'OPTIONS, GET, PUT, DELETE, PATCH',
406 'Access-Control-Allow-Credentials': 'true',
348 } 407 }
349 408
350 # use dict comprehension to remove fields like date, 409 # use dict comprehension to remove fields like date,
351 # content-length etc. from f.headers. 410 # content-length etc. from f.headers.
352 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 411 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
353 412
354 def test_rest_endpoint_attribute_options(self): 413 def test_rest_endpoint_attribute_options(self):
355 # use basic auth for rest endpoint 414 # use basic auth for rest endpoint
356 f = requests.options(self.url_base() + '/rest/data/user/1/username', 415 f = requests.options(self.url_base() + '/rest/data/user/1/username',
357 auth=('admin', 'sekrit'), 416 auth=('admin', 'sekrit'),
358 headers = {'content-type': ""}) 417 headers = {'content-type': "",
418 'Origin': "http://localhost:9001",
419 })
359 print(f.status_code) 420 print(f.status_code)
360 print(f.headers) 421 print(f.headers)
361 422
362 self.assertEqual(f.status_code, 204) 423 self.assertEqual(f.status_code, 204)
363 expected = { 'Access-Control-Allow-Origin': '*', 424 expected = { 'Access-Control-Allow-Origin': 'http://localhost:9001',
364 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override', 425 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
365 'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH', 426 'Allow': 'OPTIONS, GET, PUT, DELETE, PATCH',
366 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH', 427 'Access-Control-Allow-Methods': 'OPTIONS, GET, PUT, DELETE, PATCH',
428 'Access-Control-Allow-Credentials': 'true',
367 } 429 }
368 430
369 # use dict comprehension to remove fields like date, 431 # use dict comprehension to remove fields like date,
370 # content-length etc. from f.headers. 432 # content-length etc. from f.headers.
371 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 433 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
372 434
373 ## test a read only property. 435 ## test a read only property.
374 436
375 f = requests.options(self.url_base() + '/rest/data/user/1/creator', 437 f = requests.options(self.url_base() + '/rest/data/user/1/creator',
376 auth=('admin', 'sekrit'), 438 auth=('admin', 'sekrit'),
377 headers = {'content-type': ""}) 439 headers = {'content-type': "",
440 'Origin': "http://localhost:9001",
441 })
378 print(f.status_code) 442 print(f.status_code)
379 print(f.headers) 443 print(f.headers)
380 444
381 self.assertEqual(f.status_code, 204) 445 self.assertEqual(f.status_code, 204)
382 expected1 = dict(expected) 446 expected1 = dict(expected)
383 expected1['Allow'] = 'OPTIONS, GET' 447 expected1['Allow'] = 'OPTIONS, GET'
448 expected1['Access-Control-Allow-Methods'] = 'OPTIONS, GET'
384 449
385 # use dict comprehension to remove fields like date, 450 # use dict comprehension to remove fields like date,
386 # content-length etc. from f.headers. 451 # content-length etc. from f.headers.
387 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected1) 452 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected1)
388 453
414 self.assertEqual(f.status_code, 204) 479 self.assertEqual(f.status_code, 204)
415 expected = { 'Access-Control-Allow-Origin': '*', 480 expected = { 'Access-Control-Allow-Origin': '*',
416 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override', 481 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
417 'Allow': 'OPTIONS, GET', 482 'Allow': 'OPTIONS, GET',
418 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH', 483 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
484 'Access-Control-Allow-Credentials': 'true',
419 } 485 }
420 486
421 for i in range(10): 487 for i in range(10):
422 # use basic auth for rest endpoint 488 # use basic auth for rest endpoint
423 489
583 print(f.status_code) 649 print(f.status_code)
584 print(f.headers) 650 print(f.headers)
585 651
586 self.assertEqual(f.status_code, 200) 652 self.assertEqual(f.status_code, 200)
587 expected = { 'Content-Type': 'application/json', 653 expected = { 'Content-Type': 'application/json',
588 'Access-Control-Allow-Origin': '*', 654 'Access-Control-Allow-Credentials': 'true',
589 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
590 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 655 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
591 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH'
592 } 656 }
593 657
594 content_str = '''{ "data": { 658 content_str = '''{ "data": {
595 "id": "1", 659 "id": "1",
596 "link": "http://localhost:9001/rest/data/user/1/username", 660 "link": "http://localhost:9001/rest/data/user/1/username",
622 # use dict comprehension to remove fields like date, 686 # use dict comprehension to remove fields like date,
623 # content-length etc. from f.headers. 687 # content-length etc. from f.headers.
624 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 688 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
625 689
626 690
627 def test_compression_gzip(self): 691 def test_compression_gzip(self, method='gzip'):
692 if method == 'gzip':
693 decompressor = None
694 elif method == 'br':
695 decompressor = brotli.decompress
696 elif method == 'zstd':
697 decompressor = zstd.decompress
698
628 # use basic auth for rest endpoint 699 # use basic auth for rest endpoint
629 f = requests.get(self.url_base() + '/rest/data/user/1/username', 700 f = requests.get(self.url_base() + '/rest/data/user/1/username',
630 auth=('admin', 'sekrit'), 701 auth=('admin', 'sekrit'),
631 headers = {'content-type': "", 702 headers = {'content-type': "",
632 'Accept-Encoding': 'gzip, foo', 703 'Accept-Encoding': '%s, foo'%method,
633 'Accept': '*/*'}) 704 'Accept': '*/*'})
634 print(f.status_code) 705 print(f.status_code)
635 print(f.headers) 706 print(f.headers)
636 707
637 self.assertEqual(f.status_code, 200) 708 self.assertEqual(f.status_code, 200)
638 expected = { 'Content-Type': 'application/json', 709 expected = { 'Content-Type': 'application/json',
639 'Access-Control-Allow-Origin': '*', 710 'Access-Control-Allow-Credentials': 'true',
640 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
641 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 711 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
642 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH', 712 'Content-Encoding': method,
643 'Content-Encoding': 'gzip', 713 'Vary': 'Origin, Accept-Encoding',
644 'Vary': 'Accept-Encoding',
645 } 714 }
646 715
647 content_str = '''{ "data": { 716 content_str = '''{ "data": {
648 "id": "1", 717 "id": "1",
649 "link": "http://localhost:9001/rest/data/user/1/username", 718 "link": "http://localhost:9001/rest/data/user/1/username",
650 "data": "admin" 719 "data": "admin"
651 } 720 }
652 }''' 721 }'''
653 content = json.loads(content_str) 722 content = json.loads(content_str)
654 723
655 724 print(f.content)
656 if (type("") == type(f.content)): 725 print(type(f.content))
657 json_dict = json.loads(f.content) 726
658 else: 727 try:
659 json_dict = json.loads(b2s(f.content)) 728 if (type("") == type(f.content)):
660 729 json_dict = json.loads(f.content)
661 # etag wil not match, creation date different 730 else:
731 json_dict = json.loads(b2s(f.content))
732 except (ValueError, UnicodeDecodeError):
733 # Handle error from trying to load compressed data as only
734 # gzip gets decompressed automatically
735 # ValueError - raised by loads on compressed content python2
736 # UnicodeDecodeError - raised by loads on compressed content
737 # python3
738 json_dict = json.loads(b2s(decompressor(f.content)))
739
740 # etag will not match, creation date different
662 del(json_dict['data']['@etag']) 741 del(json_dict['data']['@etag'])
663 742
664 # type is "class 'str'" under py3, "type 'str'" py2 743 # type is "class 'str'" under py3, "type 'str'" py2
665 # just skip comparing it. 744 # just skip comparing it.
666 del(json_dict['data']['type']) 745 del(json_dict['data']['type'])
667 746
668 self.assertDictEqual(json_dict, content) 747 self.assertDictEqual(json_dict, content)
669 748
670 # verify that ETag header ends with -gzip 749 # verify that ETag header ends with -<method>
671 try: 750 try:
672 self.assertRegex(f.headers['ETag'], r'^"[0-9a-f]{32}-gzip"$') 751 self.assertRegex(f.headers['ETag'], r'^"[0-9a-f]{32}-%s"$'%method)
673 except AttributeError: 752 except AttributeError:
674 # python2 no assertRegex so try substring match 753 # python2 no assertRegex so try substring match
675 self.assertEqual(33, f.headers['ETag'].rindex('-gzip"')) 754 self.assertEqual(33, f.headers['ETag'].rindex('-' + method))
676 755
677 # use dict comprehension to remove fields like date, 756 # use dict comprehension to remove fields like date,
678 # content-length etc. from f.headers. 757 # content-length etc. from f.headers.
679 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 758 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
680
681 759
682 760
683 # use basic auth for rest endpoint, error case, bad attribute 761 # use basic auth for rest endpoint, error case, bad attribute
684 f = requests.get(self.url_base() + '/rest/data/user/1/foo', 762 f = requests.get(self.url_base() + '/rest/data/user/1/foo',
685 auth=('admin', 'sekrit'), 763 auth=('admin', 'sekrit'),
686 headers = {'content-type': "", 764 headers = {'content-type': "",
687 'Accept-Encoding': 'gzip, foo', 765 'Accept-Encoding': '%s, foo'%method,
688 'Accept': '*/*'}) 766 'Accept': '*/*',
767 'Origin': 'ZZZZ'})
689 print(f.status_code) 768 print(f.status_code)
690 print(f.headers) 769 print(f.headers)
691 770
692 # NOTE: not compressed payload too small 771 # NOTE: not compressed payload too small
693 self.assertEqual(f.status_code, 400) 772 self.assertEqual(f.status_code, 400)
694 expected = { 'Content-Type': 'application/json', 773 expected = { 'Content-Type': 'application/json',
695 'Access-Control-Allow-Origin': '*', 774 'Access-Control-Allow-Credentials': 'true',
696 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override', 775 'Access-Control-Allow-Origin': 'ZZZZ',
697 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH', 776 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
698 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH', 777 'Vary': 'Origin'
699 } 778 }
700 779
701 content = { "error": 780 content = { "error":
702 { 781 {
703 "status": 400, 782 "status": 400,
712 # content-length etc. from f.headers. 791 # content-length etc. from f.headers.
713 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected) 792 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
714 793
715 # test file x-fer 794 # test file x-fer
716 f = requests.get(self.url_base() + '/@@file/user_utils.js', 795 f = requests.get(self.url_base() + '/@@file/user_utils.js',
717 headers = { 'Accept-Encoding': 'gzip, foo', 796 headers = { 'Accept-Encoding': '%s, foo'%method,
718 'Accept': '*/*'}) 797 'Accept': '*/*'})
719 print(f.status_code) 798 print(f.status_code)
720 print(f.headers) 799 print(f.headers)
721 800
722 self.assertEqual(f.status_code, 200) 801 self.assertEqual(f.status_code, 200)
723 expected = { 'Content-Type': 'application/javascript', 802 expected = { 'Content-Type': 'application/javascript',
724 'Content-Encoding': 'gzip', 803 'Content-Encoding': method,
725 'Vary': 'Accept-Encoding', 804 'Vary': 'Accept-Encoding',
726 } 805 }
727 806
807 # compare to byte string as f.content may be compressed.
808 # so running b2s on it will throw a UnicodeError
809 if f.content[0:25] == b'// User Editing Utilities':
810 # no need to decompress, urlib3.response did it for gzip and br
811 data = f.content
812 else:
813 # I need to decode
814 data = decompressor(f.content)
815
728 # check first few bytes. 816 # check first few bytes.
729 self.assertEqual(b2s(f.content[0:25]), '// User Editing Utilities') 817 self.assertEqual(b2s(data)[0:25], '// User Editing Utilities')
730 818
731 # use dict comprehension to remove fields like date, 819 # use dict comprehension to remove fields like date,
732 # content-length etc. from f.headers. 820 # content-length etc. from f.headers.
733 self.assertDictEqual({ key: value for (key, value) in 821 self.assertDictEqual({ key: value for (key, value) in
734 f.headers.items() if key in expected }, 822 f.headers.items() if key in expected },
735 expected) 823 expected)
736 824
737 # test file x-fer 825 # test file x-fer
738 f = requests.get(self.url_base() + '/user1', 826 f = requests.get(self.url_base() + '/user1',
739 headers = { 'Accept-Encoding': 'gzip, foo', 827 headers = { 'Accept-Encoding': '%s, foo'%method,
740 'Accept': '*/*'}) 828 'Accept': '*/*'})
741 print(f.status_code) 829 print(f.status_code)
742 print(f.headers) 830 print(f.headers)
743 831
744 self.assertEqual(f.status_code, 200) 832 self.assertEqual(f.status_code, 200)
745 expected = { 'Content-Type': 'text/html; charset=utf-8', 833 expected = { 'Content-Type': 'text/html; charset=utf-8',
746 'Content-Encoding': 'gzip', 834 'Content-Encoding': method,
747 'Vary': 'Accept-Encoding', 835 'Vary': 'Accept-Encoding',
748 } 836 }
749 837
838 if f.content[0:25] == b'<!-- dollarId: user.item,':
839 # no need to decompress, urlib3.response did it for gzip and br
840 data = f.content
841 else:
842 # I need to decode
843 data = decompressor(f.content)
844
750 # check first few bytes. 845 # check first few bytes.
751 self.assertEqual(b2s(f.content[0:25]), '<!-- dollarId: user.item,') 846 self.assertEqual(b2s(data[0:25]), '<!-- dollarId: user.item,')
752 847
753 # use dict comprehension to remove fields like date, 848 # use dict comprehension to remove fields like date,
754 # content-length etc. from f.headers. 849 # content-length etc. from f.headers.
755 self.assertDictEqual({ key: value for (key, value) in 850 self.assertDictEqual({ key: value for (key, value) in
756 f.headers.items() if key in expected }, 851 f.headers.items() if key in expected },
757 expected) 852 expected)
758 853
759 @skip_brotli 854 @skip_brotli
760 def test_compression_br(self): 855 def test_compression_br(self):
761 # use basic auth for rest endpoint 856 self.test_compression_gzip(method="br")
762 f = requests.get(self.url_base() + '/rest/data/user/1/username',
763 auth=('admin', 'sekrit'),
764 headers = {'content-type': "",
765 'Accept-Encoding': 'br, foo',
766 'Accept': '*/*'})
767 print(f.status_code)
768 print(f.headers)
769
770 self.assertEqual(f.status_code, 200)
771 expected = { 'Content-Type': 'application/json',
772 'Access-Control-Allow-Origin': '*',
773 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
774 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
775 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
776 'Content-Encoding': 'br',
777 'Vary': 'Accept-Encoding',
778 }
779
780 content_str = '''{ "data": {
781 "id": "1",
782 "link": "http://localhost:9001/rest/data/user/1/username",
783 "data": "admin"
784 }
785 }'''
786 content = json.loads(content_str)
787
788 print(f.content)
789 print(type(f.content))
790
791 try:
792 json_dict = json.loads(f.content)
793 except (ValueError, TypeError):
794 # Handle error from trying to load compressed data
795 json_dict = json.loads(b2s(brotli.decompress(f.content)))
796
797 # etag wil not match, creation date different
798 del(json_dict['data']['@etag'])
799
800 # type is "class 'str'" under py3, "type 'str'" py2
801 # just skip comparing it.
802 del(json_dict['data']['type'])
803
804 self.assertDictEqual(json_dict, content)
805
806 # verify that ETag header ends with -br
807 try:
808 self.assertRegex(f.headers['ETag'], r'^"[0-9a-f]{32}-br"$')
809 except AttributeError:
810 # python2 no assertRegex so try substring match
811 self.assertEqual(33, f.headers['ETag'].rindex('-br"'))
812
813 # use dict comprehension to remove fields like date,
814 # content-length etc. from f.headers.
815 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
816
817
818
819 # use basic auth for rest endpoint, error case, bad attribute
820 f = requests.get(self.url_base() + '/rest/data/user/1/foo',
821 auth=('admin', 'sekrit'),
822 headers = {'Accept-Encoding': 'br, foo',
823 'Accept': '*/*'})
824 print(f.status_code)
825 print(f.headers)
826
827 # Note: not compressed payload too small
828 self.assertEqual(f.status_code, 400)
829 expected = { 'Content-Type': 'application/json',
830 'Access-Control-Allow-Origin': '*',
831 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
832 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
833 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
834 }
835
836 content = { "error":
837 {
838 "status": 400,
839 "msg": "Invalid attribute foo"
840 }
841 }
842 json_dict = json.loads(b2s(f.content))
843
844 self.assertDictEqual(json_dict, content)
845
846 # use dict comprehension to remove fields like date,
847 # content-length etc. from f.headers.
848 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
849
850 # test file x-fer
851 f = requests.get(self.url_base() + '/@@file/user_utils.js',
852 headers = { 'Accept-Encoding': 'br, foo',
853 'Accept': '*/*'})
854 print(f.status_code)
855 print(f.headers)
856
857 self.assertEqual(f.status_code, 200)
858 expected = { 'Content-Type': 'application/javascript',
859 'Content-Encoding': 'br',
860 'Vary': 'Accept-Encoding',
861 }
862
863 try:
864 from urllib3.response import BrotliDecoder
865 # requests has decoded br to text for me
866 data = f.content
867 except ImportError:
868 # I need to decode
869 data = brotli.decompress(f.content)
870
871 # check first few bytes.
872 self.assertEqual(b2s(data)[0:25], '// User Editing Utilities')
873
874 # use dict comprehension to remove fields like date,
875 # content-length etc. from f.headers.
876 self.assertDictEqual({ key: value for (key, value) in
877 f.headers.items() if key in expected },
878 expected)
879
880 # test file x-fer
881 f = requests.get(self.url_base() + '/user1',
882 headers = { 'Accept-Encoding': 'br, foo',
883 'Accept': '*/*'})
884 print(f.status_code)
885 print(f.headers)
886
887 self.assertEqual(f.status_code, 200)
888 expected = { 'Content-Type': 'text/html; charset=utf-8',
889 'Content-Encoding': 'br',
890 'Vary': 'Accept-Encoding',
891 }
892
893 try:
894 from urllib3.response import BrotliDecoder
895 # requests has decoded br to text for me
896 data = f.content
897 except ImportError:
898 # I need to decode
899 data = brotli.decompress(f.content)
900
901 # check first few bytes.
902 self.assertEqual(b2s(data)[0:25],
903 '<!-- dollarId: user.item,')
904
905 # use dict comprehension to remove fields like date,
906 # content-length etc. from f.headers.
907 self.assertDictEqual({ key: value for (key, value) in
908 f.headers.items() if key in expected },
909 expected)
910
911 857
912 @skip_zstd 858 @skip_zstd
913 def test_compression_zstd(self): 859 def test_compression_zstd(self):
914 # use basic auth for rest endpoint 860 self.test_compression_gzip(method="zstd")
915 f = requests.get(self.url_base() + '/rest/data/user/1/username',
916 auth=('admin', 'sekrit'),
917 headers = {'content-type': "",
918 'Accept-Encoding': 'zstd, foo',
919 'Accept': '*/*'})
920 print(f.status_code)
921 print(f.headers)
922
923 self.assertEqual(f.status_code, 200)
924 expected = { 'Content-Type': 'application/json',
925 'Access-Control-Allow-Origin': '*',
926 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
927 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
928 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
929 'Content-Encoding': 'zstd',
930 'Vary': 'Accept-Encoding',
931 }
932
933 content_str = '''{ "data": {
934 "id": "1",
935 "link": "http://localhost:9001/rest/data/user/1/username",
936 "data": "admin"
937 }
938 }'''
939 content = json.loads(content_str)
940
941
942 try:
943 json_dict = json.loads(f.content)
944 except (ValueError, UnicodeDecodeError, TypeError):
945 # ValueError - raised by loads on compressed content python2
946 # UnicodeDecodeError - raised by loads on compressed content
947 # python3
948 json_dict = json.loads(b2s(zstd.decompress(f.content)))
949
950 # etag wil not match, creation date different
951 del(json_dict['data']['@etag'])
952
953 # type is "class 'str'" under py3, "type 'str'" py2
954 # just skip comparing it.
955 del(json_dict['data']['type'])
956
957 self.assertDictEqual(json_dict, content)
958
959 # verify that ETag header ends with -zstd
960 try:
961 self.assertRegex(f.headers['ETag'], r'^"[0-9a-f]{32}-zstd"$')
962 except AttributeError:
963 # python2 no assertRegex so try substring match
964 self.assertEqual(33, f.headers['ETag'].rindex('-zstd"'))
965
966 # use dict comprehension to remove fields like date,
967 # content-length etc. from f.headers.
968 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
969
970
971
972 # use basic auth for rest endpoint, error case, bad attribute
973 f = requests.get(self.url_base() + '/rest/data/user/1/foo',
974 auth=('admin', 'sekrit'),
975 headers = {'content-type': "",
976 'Accept-Encoding': 'zstd, foo',
977 'Accept': '*/*'})
978 print(f.status_code)
979 print(f.headers)
980
981 # Note: not compressed, payload too small
982 self.assertEqual(f.status_code, 400)
983 expected = { 'Content-Type': 'application/json',
984 'Access-Control-Allow-Origin': '*',
985 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, X-HTTP-Method-Override',
986 'Allow': 'OPTIONS, GET, POST, PUT, DELETE, PATCH',
987 'Access-Control-Allow-Methods': 'HEAD, OPTIONS, GET, POST, PUT, DELETE, PATCH',
988 }
989
990 content = { "error":
991 {
992 "status": 400,
993 "msg": "Invalid attribute foo"
994 }
995 }
996
997 json_dict = json.loads(b2s(f.content))
998 self.assertDictEqual(json_dict, content)
999
1000 # use dict comprehension to remove fields like date,
1001 # content-length etc. from f.headers.
1002 self.assertDictEqual({ key: value for (key, value) in f.headers.items() if key in expected }, expected)
1003
1004 # test file x-fer
1005 f = requests.get(self.url_base() + '/@@file/user_utils.js',
1006 headers = { 'Accept-Encoding': 'zstd, foo',
1007 'Accept': '*/*'})
1008 print(f.status_code)
1009 print(f.headers)
1010
1011 self.assertEqual(f.status_code, 200)
1012 expected = { 'Content-Type': 'application/javascript',
1013 'Content-Encoding': 'zstd',
1014 'Vary': 'Accept-Encoding',
1015 }
1016
1017 # check first few bytes.
1018 self.assertEqual(b2s(zstd.decompress(f.content)[0:25]), '// User Editing Utilities')
1019
1020 # use dict comprehension to remove fields like date,
1021 # content-length etc. from f.headers.
1022 self.assertDictEqual({ key: value for (key, value) in
1023 f.headers.items() if key in expected },
1024 expected)
1025
1026 # test file x-fer
1027 f = requests.get(self.url_base() + '/user1',
1028 headers = { 'Accept-Encoding': 'zstd, foo',
1029 'Accept': '*/*'})
1030 print(f.status_code)
1031 print(f.headers)
1032
1033 self.assertEqual(f.status_code, 200)
1034 expected = { 'Content-Type': 'text/html; charset=utf-8',
1035 'Content-Encoding': 'zstd',
1036 'Vary': 'Accept-Encoding',
1037 }
1038
1039 # check first few bytes.
1040 self.assertEqual(b2s(zstd.decompress(f.content)[0:25]),
1041 '<!-- dollarId: user.item,')
1042
1043 # use dict comprehension to remove fields like date,
1044 # content-length etc. from f.headers.
1045 self.assertDictEqual({ key: value for (key, value) in
1046 f.headers.items() if key in expected },
1047 expected)
1048 861
1049 def test_cache_control_css(self): 862 def test_cache_control_css(self):
1050 f = requests.get(self.url_base() + '/@@file/style.css', 863 f = requests.get(self.url_base() + '/@@file/style.css',
1051 headers = {'content-type': "", 864 headers = {'content-type': "",
1052 'Accept': '*/*'}) 865 'Accept': '*/*'})
1067 self.assertEqual(f.headers['Cache-Control'], 'public, max-age=1209600') 880 self.assertEqual(f.headers['Cache-Control'], 'public, max-age=1209600')
1068 881
1069 def test_new_issue_with_file_upload(self): 882 def test_new_issue_with_file_upload(self):
1070 # Set up session to manage cookies <insert blue monster here> 883 # Set up session to manage cookies <insert blue monster here>
1071 session = requests.Session() 884 session = requests.Session()
885 session.headers.update({'Origin': 'http://localhost:9001'})
1072 886
1073 # login using form 887 # login using form
1074 login = {"__login_name": 'admin', '__login_password': 'sekrit', 888 login = {"__login_name": 'admin', '__login_password': 'sekrit',
1075 "@action": "login"} 889 "@action": "login"}
1076 f = session.post(self.url_base()+'/', data=login) 890 f = session.post(self.url_base()+'/', data=login)
1110 url = self.url_base() + '/rest/data/' 924 url = self.url_base() + '/rest/data/'
1111 fname = 'a-bigger-testfile' 925 fname = 'a-bigger-testfile'
1112 d = dict(name = fname, type='application/octet-stream') 926 d = dict(name = fname, type='application/octet-stream')
1113 c = dict (content = r'xyzzy') 927 c = dict (content = r'xyzzy')
1114 r = session.post(url + 'file', files = c, data = d, 928 r = session.post(url + 'file', files = c, data = d,
1115 headers = {'x-requested-with': "rest"} 929 headers = {'x-requested-with': "rest",
930 'Origin': "http://localhost:9001"}
1116 ) 931 )
1117 932
1118 # was a 500 before fix for issue2551178 933 # was a 500 before fix for issue2551178
1119 self.assertEqual(r.status_code, 201) 934 self.assertEqual(r.status_code, 201)
1120 # just compare the path leave off the number 935 # just compare the path leave off the number
1122 r.headers["location"]) 937 r.headers["location"])
1123 json_dict = json.loads(r.text) 938 json_dict = json.loads(r.text)
1124 self.assertEqual(json_dict["data"]["link"], r.headers["location"]) 939 self.assertEqual(json_dict["data"]["link"], r.headers["location"])
1125 940
1126 # download file and verify content 941 # download file and verify content
1127 r = session.get(r.headers["location"] +'/content') 942 r = session.get(r.headers["location"] +'/content',
943 headers = {'x-requested-with': "rest",
944 'Origin': "http://localhost:9001"}
945 )
1128 json_dict = json.loads(r.text) 946 json_dict = json.loads(r.text)
1129 self.assertEqual(json_dict['data']['data'], c["content"]) 947 self.assertEqual(json_dict['data']['data'], c["content"])
1130 print(r.text) 948 print(r.text)
1131 949
1132 # Upload a file via rest interface - no auth 950 # Upload a file via rest interface - no auth
1133 session.auth = None 951 session.auth = None
1134 r = session.post(url + 'file', files = c, data = d, 952 r = session.post(url + 'file', files = c, data = d,
1135 headers = {'x-requested-with': "rest"} 953 headers = {'x-requested-with': "rest",
954 'Origin': "http://localhost:9001"}
1136 ) 955 )
1137 self.assertEqual(r.status_code, 403) 956 self.assertEqual(r.status_code, 403)
1138 957
1139 # get session variable from web form login 958 # get session variable from web form login
1140 # and use it to upload file 959 # and use it to upload file
1141 # login using form 960 # login using form
1142 login = {"__login_name": 'admin', '__login_password': 'sekrit', 961 login = {"__login_name": 'admin', '__login_password': 'sekrit',
1143 "@action": "login"} 962 "@action": "login"}
1144 f = session.post(self.url_base()+'/', data=login) 963 f = session.post(self.url_base()+'/', data=login,
964 headers = {'Origin': "http://localhost:9001"}
965 )
1145 # look for change in text in sidebar post login 966 # look for change in text in sidebar post login
1146 self.assertIn('Hello, admin', f.text) 967 self.assertIn('Hello, admin', f.text)
1147 968
1148 r = session.post(url + 'file', files = c, data = d, 969 r = session.post(url + 'file', files = c, data = d,
1149 headers = {'x-requested-with': "rest"} 970 headers = {'x-requested-with': "rest",
971 'Origin': "http://localhost:9001"}
1150 ) 972 )
1151 self.assertEqual(r.status_code, 201) 973 self.assertEqual(r.status_code, 201)
1152 print(r.status_code) 974 print(r.status_code)
1153 975
1154 976

Roundup Issue Tracker: http://roundup-tracker.org/