Mercurial > p > roundup > code
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) |
