comparison roundup/cgi/client.py @ 7692:8fb42f41ef10 issue2550923_computed_property

merge in default branch to see if ti clears a travis-ci build error on 2.7 python; default branch builds fine
author John Rouillard <rouilj@ieee.org>
date Mon, 11 Sep 2023 00:10:29 -0400
parents 7102de2c8733
children cc4b11ab2f22
comparison
equal deleted inserted replaced
7528:14a8e11f3a87 7692:8fb42f41ef10
2 """ 2 """
3 __docformat__ = 'restructuredtext' 3 __docformat__ = 'restructuredtext'
4 4
5 import base64 5 import base64
6 import binascii 6 import binascii
7 import cgi
8 import codecs 7 import codecs
9 import email.utils 8 import email.utils
10 import errno 9 import errno
11 import logging 10 import logging
12 import mimetypes 11 import mimetypes
23 from OpenSSL.SSL import SysCallError 22 from OpenSSL.SSL import SysCallError
24 except ImportError: 23 except ImportError:
25 class SysCallError(Exception): 24 class SysCallError(Exception):
26 pass 25 pass
27 26
27 from roundup.anypy.cgi_ import cgi
28 import roundup.anypy.email_ # noqa: F401 -- patches for email library code 28 import roundup.anypy.email_ # noqa: F401 -- patches for email library code
29 import roundup.anypy.random_ as random_ # quality of random checked below 29 import roundup.anypy.random_ as random_ # quality of random checked below
30 30
31 from roundup import hyperdb, rest, xmlrpc 31 from roundup import hyperdb, rest, xmlrpc
32 32
41 from roundup.cgi.exceptions import ( 41 from roundup.cgi.exceptions import (
42 DetectorError, FormError, IndexerQueryError, NotFound, NotModified, 42 DetectorError, FormError, IndexerQueryError, NotFound, NotModified,
43 Redirect, SendFile, SendStaticFile, SeriousError) 43 Redirect, SendFile, SendStaticFile, SeriousError)
44 from roundup.cgi.form_parser import FormParser 44 from roundup.cgi.form_parser import FormParser
45 45
46 from roundup.exceptions import LoginError, Reject, RejectRaw, \ 46 from roundup.exceptions import LoginError, RateLimitExceeded, Reject, \
47 Unauthorised, UsageError 47 RejectRaw, Unauthorised, UsageError
48 48
49 from roundup.mailer import Mailer, MessageSendError 49 from roundup.mailer import Mailer, MessageSendError
50 50
51 logger = logging.getLogger('roundup') 51 logger = logging.getLogger('roundup')
52 52
529 """ 529 """
530 530
531 # strip HTTP_PROXY issue2550925 in case 531 # strip HTTP_PROXY issue2550925 in case
532 # PROXY header is set. 532 # PROXY header is set.
533 if 'HTTP_PROXY' in self.env: 533 if 'HTTP_PROXY' in self.env:
534 del(self.env['HTTP_PROXY']) 534 del (self.env['HTTP_PROXY'])
535 if 'HTTP_PROXY' in os.environ: 535 if 'HTTP_PROXY' in os.environ:
536 del(os.environ['HTTP_PROXY']) 536 del (os.environ['HTTP_PROXY'])
537 537
538 xmlrpc_enabled = self.instance.config.WEB_ENABLE_XMLRPC 538 xmlrpc_enabled = self.instance.config.WEB_ENABLE_XMLRPC
539 rest_enabled = self.instance.config.WEB_ENABLE_REST 539 rest_enabled = self.instance.config.WEB_ENABLE_REST
540 try: 540 try:
541 if xmlrpc_enabled and self.path == 'xmlrpc': 541 if xmlrpc_enabled and self.path == 'xmlrpc':
570 self.determine_charset() 570 self.determine_charset()
571 if self.instance.config["WEB_TRANSLATE_XMLRPC"]: 571 if self.instance.config["WEB_TRANSLATE_XMLRPC"]:
572 self.determine_language() 572 self.determine_language()
573 # Open the database as the correct user. 573 # Open the database as the correct user.
574 try: 574 try:
575 self.determine_user() 575 self.determine_user(is_api="xmlrpc")
576 self.db.tx_Source = "xmlrpc" 576 self.db.tx_Source = "xmlrpc"
577 self.db.i18n = self.translator 577 self.db.i18n = self.translator
578 except LoginError as msg: 578 except LoginError as msg:
579 output = xmlrpc_.client.dumps( 579 output = xmlrpc_.client.dumps(
580 xmlrpc_.client.Fault(401, "%s" % msg), 580 xmlrpc_.client.Fault(401, "%s" % msg),
581 allow_none=True)
582 self.setHeader("Content-Type", "text/xml")
583 self.setHeader("Content-Length", str(len(output)))
584 self.write(s2b(output))
585 return
586 except RateLimitExceeded as msg:
587 output = xmlrpc_.client.dumps(
588 xmlrpc_.client.Fault(429, "%s" % msg),
581 allow_none=True) 589 allow_none=True)
582 self.setHeader("Content-Type", "text/xml") 590 self.setHeader("Content-Type", "text/xml")
583 self.setHeader("Content-Length", str(len(output))) 591 self.setHeader("Content-Length", str(len(output)))
584 self.write(s2b(output)) 592 self.write(s2b(output))
585 return 593 return
653 if self.instance.config["WEB_TRANSLATE_REST"]: 661 if self.instance.config["WEB_TRANSLATE_REST"]:
654 self.determine_language() 662 self.determine_language()
655 # Open the database as the correct user. 663 # Open the database as the correct user.
656 # TODO: add everything to RestfulDispatcher 664 # TODO: add everything to RestfulDispatcher
657 try: 665 try:
658 self.determine_user() 666 self.determine_user(is_api="rest")
659 self.db.tx_Source = "rest" 667 self.db.tx_Source = "rest"
660 self.db.i18n = self.translator 668 self.db.i18n = self.translator
661 except LoginError as err: 669 except LoginError as err:
662 output = s2b("Invalid Login - %s" % str(err)) 670 output = s2b("Invalid Login - %s" % str(err))
663 self.reject_request(output, status=http_.client.UNAUTHORIZED) 671 self.reject_request(output, status=http_.client.UNAUTHORIZED)
672 return
673 except RateLimitExceeded as err:
674 output = s2b("%s" % str(err))
675 # PYTHON2:FIXME http_.client.TOO_MANY_REQUESTS missing
676 # python2 so use numeric code.
677 self.reject_request(output, status=429)
664 return 678 return
665 679
666 # verify Origin is allowed on all requests including GET. 680 # verify Origin is allowed on all requests including GET.
667 # If a GET, missing origin is allowed (i.e. same site GET request) 681 # If a GET, missing origin is allowed (i.e. same site GET request)
668 if not self.is_origin_header_ok(api=True): 682 if not self.is_origin_header_ok(api=True):
852 # receiving the reply. 866 # receiving the reply.
853 pass 867 pass
854 except SysCallError: 868 except SysCallError:
855 # OpenSSL.SSL.SysCallError is similar to IOError above 869 # OpenSSL.SSL.SysCallError is similar to IOError above
856 pass 870 pass
871 except RateLimitExceeded:
872 raise
857 873
858 except SeriousError as message: 874 except SeriousError as message:
859 self.write_html(str(message)) 875 self.write_html(str(message))
860 except Redirect as url: 876 except Redirect as url:
861 # let's redirect - if the url isn't None, then we need to do 877 # let's redirect - if the url isn't None, then we need to do
915 # handle it 931 # handle it
916 raise NotFound(e) 932 raise NotFound(e)
917 except FormError as e: 933 except FormError as e:
918 self.add_error_message(self._('Form Error: ') + str(e)) 934 self.add_error_message(self._('Form Error: ') + str(e))
919 self.write_html(self.renderContext()) 935 self.write_html(self.renderContext())
936 except RateLimitExceeded as e:
937 self.add_error_message(str(e))
938 self.write_html(self.renderContext())
920 except IOError: 939 except IOError:
921 # IOErrors here are due to the client disconnecting before 940 # IOErrors here are due to the client disconnecting before
922 # receiving the reply. 941 # receiving the reply.
923 # may happen during write_html and serve_file, too. 942 # may happen during write_html and serve_file, too.
924 pass 943 pass
1106 except jwt.exceptions.InvalidTokenError as err: 1125 except jwt.exceptions.InvalidTokenError as err:
1107 self.setHeader("WWW-Authenticate", "Basic, Bearer") 1126 self.setHeader("WWW-Authenticate", "Basic, Bearer")
1108 self.make_user_anonymous() 1127 self.make_user_anonymous()
1109 raise LoginError(str(err)) 1128 raise LoginError(str(err))
1110 1129
1111 return(token) 1130 return (token)
1112 1131
1113 def determine_user(self): 1132 def determine_user(self, is_api=False):
1114 """Determine who the user is""" 1133 """Determine who the user is"""
1115 self.opendb('admin') 1134 self.opendb('admin')
1116 1135
1117 # if we get a jwt, it includes the roles to be used for this session 1136 # if we get a jwt, it includes the roles to be used for this session
1118 # so we define a new function to encpsulate and return the jwt roles 1137 # so we define a new function to encpsulate and return the jwt roles
1169 # Current user may not be None, otherwise 1188 # Current user may not be None, otherwise
1170 # instatiation of the login action will fail. 1189 # instatiation of the login action will fail.
1171 # So we set the user to anonymous first. 1190 # So we set the user to anonymous first.
1172 self.make_user_anonymous() 1191 self.make_user_anonymous()
1173 login = self.get_action_class('login')(self) 1192 login = self.get_action_class('login')(self)
1174 login.verifyLogin(username, password) 1193 login.verifyLogin(username, password, is_api=is_api)
1175 except LoginError: 1194 except (LoginError, RateLimitExceeded):
1176 self.make_user_anonymous() 1195 self.make_user_anonymous()
1177 raise 1196 raise
1178 user = username 1197 user = username
1179 # try to seed with something harder to guess than 1198 # try to seed with something harder to guess than
1180 # just the time. If random is SystemRandom, 1199 # just the time. If random is SystemRandom,
1837 1856
1838 # --- mime-type security 1857 # --- mime-type security
1839 # mime type detection is performed in cgi.form_parser 1858 # mime type detection is performed in cgi.form_parser
1840 1859
1841 # everything not here is served as 'application/octet-stream' 1860 # everything not here is served as 'application/octet-stream'
1842 whitelist = [ 1861 mime_type_allowlist = [
1843 'text/plain', 1862 'text/plain',
1844 'text/x-csrc', # .c 1863 'text/x-csrc', # .c
1845 'text/x-chdr', # .h 1864 'text/x-chdr', # .h
1846 'text/x-patch', # .patch and .diff 1865 'text/x-patch', # .patch and .diff
1847 'text/x-python', # .py 1866 'text/x-python', # .py
1857 'audio/ogg', 1876 'audio/ogg',
1858 'video/webm', 1877 'video/webm',
1859 ] 1878 ]
1860 1879
1861 if self.instance.config['WEB_ALLOW_HTML_FILE']: 1880 if self.instance.config['WEB_ALLOW_HTML_FILE']:
1862 whitelist.append('text/html') 1881 mime_type_allowlist.append('text/html')
1863 1882
1864 try: 1883 try:
1865 mime_type = klass.get(nodeid, 'type') 1884 mime_type = klass.get(nodeid, 'type')
1866 except IndexError as e: 1885 except IndexError as e:
1867 raise NotFound(e) 1886 raise NotFound(e)
1868 # Can happen for msg class: 1887 # Can happen for msg class:
1869 if not mime_type: 1888 if not mime_type:
1870 mime_type = 'text/plain' 1889 mime_type = 'text/plain'
1871 1890
1872 if mime_type not in whitelist: 1891 if mime_type not in mime_type_allowlist:
1873 mime_type = 'application/octet-stream' 1892 mime_type = 'application/octet-stream'
1874 1893
1875 # --/ mime-type security 1894 # --/ mime-type security
1876 1895
1877 # If this object is a file (i.e., an instance of FileClass), 1896 # If this object is a file (i.e., an instance of FileClass),
2741 def setHeader(self, header, value): 2760 def setHeader(self, header, value):
2742 """Override or delete a header to be returned to the user's browser. 2761 """Override or delete a header to be returned to the user's browser.
2743 """ 2762 """
2744 if value is None: 2763 if value is None:
2745 try: 2764 try:
2746 del(self.additional_headers[header]) 2765 del (self.additional_headers[header])
2747 except KeyError: 2766 except KeyError:
2748 pass 2767 pass
2749 else: 2768 else:
2750 self.additional_headers[header] = value 2769 self.additional_headers[header] = value
2751 2770
2762 2781
2763 if headers.get('Content-Type', 'text/html') == 'text/html': 2782 if headers.get('Content-Type', 'text/html') == 'text/html':
2764 headers['Content-Type'] = 'text/html; charset=utf-8' 2783 headers['Content-Type'] = 'text/html; charset=utf-8'
2765 2784
2766 if response in [204, 304]: # has no body so no content-type 2785 if response in [204, 304]: # has no body so no content-type
2767 del(headers['Content-Type']) 2786 del (headers['Content-Type'])
2768 2787
2769 headers = list(headers.items()) 2788 headers = list(headers.items())
2770 2789
2771 for ((path, name), (value, expire)) in self._cookies.items(): 2790 for ((path, name), (value, expire)) in self._cookies.items():
2772 cookie = "%s=%s; Path=%s;" % (name, value, path) 2791 cookie = "%s=%s; Path=%s;" % (name, value, path)

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