Mercurial > p > roundup > code
comparison roundup/cgi/client.py @ 7814:9adf37c63b56
chore(refactor): use with open, consolidate/un-nest if ...
Various cleanups:
use 'with open(...)' rather than 'try: .. finally' to close file.
use "return ..." rather than "x = ...; return x"
Change if/else to if ternaries
Swap ' for " to prevent escaping nested "
Add lint silencing comments
Fix bare return that needs to be return None.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sat, 16 Mar 2024 22:57:19 -0400 |
| parents | 928c20d4344b |
| children | f11c982f01c8 |
comparison
equal
deleted
inserted
replaced
| 7813:928c20d4344b | 7814:9adf37c63b56 |
|---|---|
| 440 # PATCH verbs. They are processed like POST. So FieldStorage | 440 # PATCH verbs. They are processed like POST. So FieldStorage |
| 441 # hangs on these verbs trying to read posted data that | 441 # hangs on these verbs trying to read posted data that |
| 442 # will never arrive. | 442 # will never arrive. |
| 443 # If not defined, set CONTENT_LENGTH to 0 so it doesn't | 443 # If not defined, set CONTENT_LENGTH to 0 so it doesn't |
| 444 # hang reading the data. | 444 # hang reading the data. |
| 445 if self.env['REQUEST_METHOD'] in ['OPTIONS', 'DELETE', 'PATCH']: | 445 if self.env['REQUEST_METHOD'] in ['OPTIONS', 'DELETE', 'PATCH'] \ |
| 446 if 'CONTENT_LENGTH' not in self.env: | 446 and 'CONTENT_LENGTH' not in self.env: |
| 447 self.env['CONTENT_LENGTH'] = 0 | 447 self.env['CONTENT_LENGTH'] = 0 |
| 448 logger.debug("Setting CONTENT_LENGTH to 0 for method: %s", | 448 logger.debug("Setting CONTENT_LENGTH to 0 for method: %s", |
| 449 self.env['REQUEST_METHOD']) | 449 self.env['REQUEST_METHOD']) |
| 450 | 450 |
| 451 # cgi.FieldStorage must save all data as | 451 # cgi.FieldStorage must save all data as |
| 452 # binary/bytes. Subclass BinaryFieldStorage does this. | 452 # binary/bytes. Subclass BinaryFieldStorage does this. |
| 453 # It's a workaround for a bug in cgi.FieldStorage. See class | 453 # It's a workaround for a bug in cgi.FieldStorage. See class |
| 454 # def for details. | 454 # def for details. |
| 494 self._ok_message = [] | 494 self._ok_message = [] |
| 495 self._error_message = [] | 495 self._error_message = [] |
| 496 | 496 |
| 497 def _gen_nonce(self): | 497 def _gen_nonce(self): |
| 498 """ generate a unique nonce """ | 498 """ generate a unique nonce """ |
| 499 n = b2s(base64.b32encode(random_.token_bytes(40))) | 499 return b2s(base64.b32encode(random_.token_bytes(40))) |
| 500 return n | |
| 501 | 500 |
| 502 def setTranslator(self, translator=None): | 501 def setTranslator(self, translator=None): |
| 503 """Replace the translation engine | 502 """Replace the translation engine |
| 504 | 503 |
| 505 'translator' | 504 'translator' |
| 692 # because handle_rest is called. Preflight requests | 691 # because handle_rest is called. Preflight requests |
| 693 # are unauthenticated, so no need to check permissions. | 692 # are unauthenticated, so no need to check permissions. |
| 694 if (self.is_cors_preflight()): | 693 if (self.is_cors_preflight()): |
| 695 self.handle_preflight() | 694 self.handle_preflight() |
| 696 return | 695 return |
| 697 elif not self.db.security.hasPermission('Rest Access', self.userid): | 696 |
| 697 if not self.db.security.hasPermission('Rest Access', self.userid): | |
| 698 output = s2b('{ "error": { "status": 403, "msg": "Forbidden." } }') | 698 output = s2b('{ "error": { "status": 403, "msg": "Forbidden." } }') |
| 699 self.reject_request(output, | 699 self.reject_request(output, |
| 700 message_type="application/json", | 700 message_type="application/json", |
| 701 status=403) | 701 status=403) |
| 702 return | 702 return |
| 809 except (UsageError, Unauthorised) as msg: | 809 except (UsageError, Unauthorised) as msg: |
| 810 csrf_ok = False | 810 csrf_ok = False |
| 811 self.form_wins = True | 811 self.form_wins = True |
| 812 self._error_message = msg.args | 812 self._error_message = msg.args |
| 813 | 813 |
| 814 if csrf_ok: | 814 # If csrf checks pass. Run actions etc. |
| 815 # csrf checks pass. Run actions etc. | 815 # handle_action() may handle a form submit action. |
| 816 # possibly handle a form submit action (may change | 816 # It can change self.classname and self.template, |
| 817 # self.classname and self.template, and may also | 817 # and may also append error/ok_messages. |
| 818 # append error/ok_messages) | 818 html = self.handle_action() if csrf_ok else None |
| 819 html = self.handle_action() | |
| 820 else: | |
| 821 html = None | |
| 822 | 819 |
| 823 if html: | 820 if html: |
| 824 self.write_html(html) | 821 self.write_html(html) |
| 825 return | 822 return |
| 826 | 823 |
| 881 # browser to prompt the user again. | 878 # browser to prompt the user again. |
| 882 if self.instance.config.WEB_HTTP_AUTH: | 879 if self.instance.config.WEB_HTTP_AUTH: |
| 883 self.response_code = http_.client.UNAUTHORIZED | 880 self.response_code = http_.client.UNAUTHORIZED |
| 884 realm = self.instance.config.TRACKER_NAME | 881 realm = self.instance.config.TRACKER_NAME |
| 885 self.setHeader("WWW-Authenticate", | 882 self.setHeader("WWW-Authenticate", |
| 886 "Basic realm=\"%s\"" % realm) | 883 'Basic realm="%s"' % realm) |
| 887 else: | 884 else: |
| 888 self.response_code = http_.client.FORBIDDEN | 885 self.response_code = http_.client.FORBIDDEN |
| 889 self.renderFrontPage(str(message)) | 886 self.renderFrontPage(str(message)) |
| 890 except Unauthorised as message: | 887 except Unauthorised as message: |
| 891 # users may always see the front page | 888 # users may always see the front page |
| 1039 encoder = codecs.getencoder(self.STORAGE_CHARSET) | 1036 encoder = codecs.getencoder(self.STORAGE_CHARSET) |
| 1040 re_charref = re.compile('&#([0-9]+|x[0-9a-f]+);', re.IGNORECASE) | 1037 re_charref = re.compile('&#([0-9]+|x[0-9a-f]+);', re.IGNORECASE) |
| 1041 | 1038 |
| 1042 def _decode_charref(matchobj): | 1039 def _decode_charref(matchobj): |
| 1043 num = matchobj.group(1) | 1040 num = matchobj.group(1) |
| 1044 if num[0].lower() == 'x': | 1041 uc = int(num[1:], 16) if num[0].lower() == 'x' else int(num) |
| 1045 uc = int(num[1:], 16) | |
| 1046 else: | |
| 1047 uc = int(num) | |
| 1048 return uchr(uc) | 1042 return uchr(uc) |
| 1049 | 1043 |
| 1050 for field_name in self.form: | 1044 for field_name in self.form: |
| 1051 field = self.form[field_name] | 1045 field = self.form[field_name] |
| 1052 if (field.type == 'text/plain') and not field.filename: | 1046 if (field.type == 'text/plain') and not field.filename: |
| 1233 if r.lower() not in all_rolenames: | 1227 if r.lower() not in all_rolenames: |
| 1234 raise LoginError("Token roles are invalid.") | 1228 raise LoginError("Token roles are invalid.") |
| 1235 | 1229 |
| 1236 # will be used later to override the get_roles method | 1230 # will be used later to override the get_roles method |
| 1237 # having it defined as truthy allows it to be used. | 1231 # having it defined as truthy allows it to be used. |
| 1238 override_get_roles = lambda self: iter_roles( # noqa: E731 | 1232 override_get_roles = lambda self: iter_roles( # noqa: ARG005 |
| 1239 ','.join(token['roles'])) | 1233 ','.join(token['roles'])) |
| 1240 | 1234 |
| 1241 # if user was not set by http authorization, try session lookup | 1235 # if user was not set by http authorization, try session lookup |
| 1242 if not user: | 1236 if not user: |
| 1243 user = self.session_api.get('user') | 1237 user = self.session_api.get('user') |
| 1289 pass | 1283 pass |
| 1290 if isinstance(action, list): | 1284 if isinstance(action, list): |
| 1291 raise SeriousError( | 1285 raise SeriousError( |
| 1292 self._('broken form: multiple @action values submitted')) | 1286 self._('broken form: multiple @action values submitted')) |
| 1293 elif action != '': | 1287 elif action != '': |
| 1288 # '' is value when no action parameter was found so run | |
| 1289 # this to extract action string value when action found. | |
| 1294 action = action.value.lower() | 1290 action = action.value.lower() |
| 1295 if action in ('login', 'register'): | 1291 if action in ('login', 'register'): |
| 1296 return | 1292 return |
| 1297 | 1293 |
| 1298 # allow Anonymous to view the "user" "register" template if they're | 1294 # allow Anonymous to view the "user" "register" template if they're |
| 1300 if (self.db.security.hasPermission('Register', self.userid, 'user') | 1296 if (self.db.security.hasPermission('Register', self.userid, 'user') |
| 1301 and self.classname == 'user' and self.template == 'register'): | 1297 and self.classname == 'user' and self.template == 'register'): |
| 1302 return | 1298 return |
| 1303 | 1299 |
| 1304 # otherwise for everything else | 1300 # otherwise for everything else |
| 1305 if self.user == 'anonymous': | 1301 if self.user == 'anonymous' and \ |
| 1306 if not self.db.security.hasPermission('Web Access', self.userid): | 1302 not self.db.security.hasPermission('Web Access', self.userid): |
| 1307 raise Unauthorised(self._("Anonymous users are not " | 1303 raise Unauthorised(self._("Anonymous users are not " |
| 1308 "allowed to use the web interface")) | 1304 "allowed to use the web interface")) |
| 1309 | 1305 |
| 1310 def is_origin_header_ok(self, api=False, credentials=False): | 1306 def is_origin_header_ok(self, api=False, credentials=False): |
| 1311 """Determine if origin is valid for the context | 1307 """Determine if origin is valid for the context |
| 1324 """ | 1320 """ |
| 1325 | 1321 |
| 1326 try: | 1322 try: |
| 1327 origin = self.env['HTTP_ORIGIN'] | 1323 origin = self.env['HTTP_ORIGIN'] |
| 1328 except KeyError: | 1324 except KeyError: |
| 1329 if self.env['REQUEST_METHOD'] == 'GET': | 1325 return self.env['REQUEST_METHOD'] == 'GET' |
| 1330 return True | |
| 1331 else: | |
| 1332 return False | |
| 1333 | 1326 |
| 1334 # note base https://host/... ends host with with a /, | 1327 # note base https://host/... ends host with with a /, |
| 1335 # so add it to origin. | 1328 # so add it to origin. |
| 1336 foundat = self.base.find(origin + '/') | 1329 foundat = self.base.find(origin + '/') |
| 1337 if foundat == 0: | 1330 if foundat == 0: |
| 1476 # same for example) so we don't include here. | 1469 # same for example) so we don't include here. |
| 1477 header_names = [ | 1470 header_names = [ |
| 1478 "ORIGIN", | 1471 "ORIGIN", |
| 1479 "REFERER", | 1472 "REFERER", |
| 1480 "X-FORWARDED-HOST", | 1473 "X-FORWARDED-HOST", |
| 1481 "HOST" | 1474 "HOST", |
| 1482 ] | 1475 ] |
| 1483 | 1476 |
| 1484 header_pass = 0 # count of passing header checks | 1477 header_pass = 0 # count of passing header checks |
| 1485 | 1478 |
| 1486 # If required headers are missing, raise an error | 1479 # If required headers are missing, raise an error |
| 1579 if header_pass < enforce: | 1572 if header_pass < enforce: |
| 1580 logger.error(self._("Csrf: unable to verify sufficient headers")) | 1573 logger.error(self._("Csrf: unable to verify sufficient headers")) |
| 1581 raise UsageError(self._("Unable to verify sufficient headers")) | 1574 raise UsageError(self._("Unable to verify sufficient headers")) |
| 1582 | 1575 |
| 1583 enforce = config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] | 1576 enforce = config['WEB_CSRF_ENFORCE_HEADER_X-REQUESTED-WITH'] |
| 1584 if api: | 1577 if api and enforce in ['required', 'yes']: |
| 1585 if enforce in ['required', 'yes']: | 1578 # if we get here we have usually passed at least one |
| 1586 # if we get here we have usually passed at least one | 1579 # header check. We check for presence of this custom |
| 1587 # header check. We check for presence of this custom | 1580 # header for xmlrpc/rest calls only. |
| 1588 # header for xmlrpc/rest calls only. | 1581 # E.G. X-Requested-With: XMLHttpRequest |
| 1589 # E.G. X-Requested-With: XMLHttpRequest | 1582 # Note we do not use CSRF nonces for xmlrpc/rest requests. |
| 1590 # Note we do not use CSRF nonces for xmlrpc/rest requests. | 1583 # |
| 1591 # | 1584 # see: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers |
| 1592 # see: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Protecting_REST_Services:_Use_of_Custom_Request_Headers | 1585 if 'HTTP_X_REQUESTED_WITH' not in self.env: |
| 1593 if 'HTTP_X_REQUESTED_WITH' not in self.env: | 1586 logger.error(self._( |
| 1594 logger.error(self._( | 1587 ''"csrf X-REQUESTED-WITH xmlrpc required header " |
| 1595 ''"csrf X-REQUESTED-WITH xmlrpc required header " | 1588 ''"check failed for user%s."), |
| 1596 ''"check failed for user%s."), | 1589 current_user) |
| 1597 current_user) | 1590 raise UsageError(self._("Required Header Missing")) |
| 1598 raise UsageError(self._("Required Header Missing")) | |
| 1599 | 1591 |
| 1600 # Expire old csrf tokens now so we don't use them. These will | 1592 # Expire old csrf tokens now so we don't use them. These will |
| 1601 # be committed after the otks.destroy below. Note that the | 1593 # be committed after the otks.destroy below. Note that the |
| 1602 # self.clean_up run as part of determine_user() will run only | 1594 # self.clean_up run as part of determine_user() will run only |
| 1603 # once an hour. If we have short lived (e.g. 5 minute) keys | 1595 # once an hour. If we have short lived (e.g. 5 minute) keys |
| 1701 | 1693 |
| 1702 # open the database or only set the user | 1694 # open the database or only set the user |
| 1703 if not hasattr(self, 'db'): | 1695 if not hasattr(self, 'db'): |
| 1704 self.db = self.instance.open(username) | 1696 self.db = self.instance.open(username) |
| 1705 self.db.tx_Source = "web" | 1697 self.db.tx_Source = "web" |
| 1706 else: | 1698 elif self.instance.optimize: |
| 1707 if self.instance.optimize: | |
| 1708 self.db.setCurrentUser(username) | 1699 self.db.setCurrentUser(username) |
| 1709 self.db.tx_Source = "web" | 1700 self.db.tx_Source = "web" |
| 1710 else: | 1701 else: |
| 1711 self.db.close() | 1702 self.db.close() |
| 1712 self.db = self.instance.open(username) | 1703 self.db = self.instance.open(username) |
| 1713 self.db.tx_Source = "web" | 1704 self.db.tx_Source = "web" |
| 1714 # The old session API refers to the closed database; | 1705 # The old session API refers to the closed database; |
| 1715 # we can no longer use it. | 1706 # we can no longer use it. |
| 1716 self.session_api = Session(self) | 1707 self.session_api = Session(self) |
| 1717 | 1708 |
| 1718 # match designator in URL stripping leading 0's. So: | 1709 # match designator in URL stripping leading 0's. So: |
| 1719 # https://issues.roundup-tracker.org/issue002551190 is the same as | 1710 # https://issues.roundup-tracker.org/issue002551190 is the same as |
| 1720 # https://issues.roundup-tracker.org/issue2551190 | 1711 # https://issues.roundup-tracker.org/issue2551190 |
| 1721 # Note: id's are strings not numbers so "02" != "2" but 02 == 2 | 1712 # Note: id's are strings not numbers so "02" != "2" but 02 == 2 |
| 1791 if template_override is not None: | 1782 if template_override is not None: |
| 1792 self.template = template_override | 1783 self.template = template_override |
| 1793 else: | 1784 else: |
| 1794 self.template = '' | 1785 self.template = '' |
| 1795 return | 1786 return |
| 1796 elif path[0] in ('_file', '@@file'): | 1787 if path[0] in ('_file', '@@file'): |
| 1797 raise SendStaticFile(os.path.join(*path[1:])) | 1788 raise SendStaticFile(os.path.join(*path[1:])) |
| 1798 else: | 1789 else: |
| 1799 self.classname = path[0] | 1790 self.classname = path[0] |
| 1800 if len(path) > 1: | 1791 if len(path) > 1: |
| 1801 # send the file identified by the designator in path[0] | 1792 # send the file identified by the designator in path[0] |
| 1971 | 1962 |
| 1972 # detemine meta-type | 1963 # detemine meta-type |
| 1973 file = str(file) | 1964 file = str(file) |
| 1974 mime_type = mimetypes.guess_type(file)[0] | 1965 mime_type = mimetypes.guess_type(file)[0] |
| 1975 if not mime_type: | 1966 if not mime_type: |
| 1976 if file.endswith('.css'): | 1967 mime_type = 'text/css' if file.endswith('.css') else 'text/plain' |
| 1977 mime_type = 'text/css' | |
| 1978 else: | |
| 1979 mime_type = 'text/plain' | |
| 1980 | 1968 |
| 1981 # get filename: given a/b/c.js extract c.js | 1969 # get filename: given a/b/c.js extract c.js |
| 1982 fn = file.rpartition("/")[2] | 1970 fn = file.rpartition("/")[2] |
| 1983 if fn in self.Cache_Control: | 1971 if fn in self.Cache_Control: |
| 1984 # if filename matches, don't use cache control | 1972 # if filename matches, don't use cache control |
| 2077 # right one to the view parameter. If we don't have alternate | 2065 # right one to the view parameter. If we don't have alternate |
| 2078 # templates, just leave view alone. | 2066 # templates, just leave view alone. |
| 2079 if (view and view.find('|') != -1): | 2067 if (view and view.find('|') != -1): |
| 2080 # we have alternate templates, parse them apart. | 2068 # we have alternate templates, parse them apart. |
| 2081 (oktmpl, errortmpl) = view.split("|", 2) | 2069 (oktmpl, errortmpl) = view.split("|", 2) |
| 2082 if self._error_message: | 2070 |
| 2083 # we have an error, use errortmpl | 2071 # Choose the right template |
| 2084 view = errortmpl | 2072 view = errortmpl if self._error_message else oktmpl |
| 2085 else: | |
| 2086 # no error message recorded, use oktmpl | |
| 2087 view = oktmpl | |
| 2088 | 2073 |
| 2089 loader = self.instance.templates | 2074 loader = self.instance.templates |
| 2090 | 2075 |
| 2091 # if classname is not set, use "home" template | 2076 # if classname is not set, use "home" template |
| 2092 if name is None: | 2077 if name is None: |
| 2133 tplname = self.selectTemplate(self.classname, self.template) | 2118 tplname = self.selectTemplate(self.classname, self.template) |
| 2134 | 2119 |
| 2135 # catch errors so we can handle PT rendering errors more nicely | 2120 # catch errors so we can handle PT rendering errors more nicely |
| 2136 args = { | 2121 args = { |
| 2137 'ok_message': self._ok_message, | 2122 'ok_message': self._ok_message, |
| 2138 'error_message': self._error_message | 2123 'error_message': self._error_message, |
| 2139 } | 2124 } |
| 2140 pt = self.instance.templates.load(tplname) | 2125 pt = self.instance.templates.load(tplname) |
| 2141 # let the template render figure stuff out | 2126 # let the template render figure stuff out |
| 2142 try: | 2127 try: |
| 2143 result = pt.render(self, None, None, **args) | 2128 result = pt.render(self, None, None, **args) |
| 2187 # receive an error message, and the adminstrator will | 2172 # receive an error message, and the adminstrator will |
| 2188 # receive a traceback, albeit with less information | 2173 # receive a traceback, albeit with less information |
| 2189 # than the one we tried to generate above. | 2174 # than the one we tried to generate above. |
| 2190 if sys.version_info[0] > 2: | 2175 if sys.version_info[0] > 2: |
| 2191 raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) | 2176 raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) |
| 2192 else: | 2177 exec('raise exc_info[0], exc_info[1], exc_info[2]') # nosec |
| 2193 exec('raise exc_info[0], exc_info[1], exc_info[2]') # nosec | |
| 2194 | 2178 |
| 2195 def renderError(self, error, response_code=400, use_template=True): | 2179 def renderError(self, error, response_code=400, use_template=True): |
| 2196 self.response_code = response_code | 2180 self.response_code = response_code |
| 2197 | 2181 |
| 2198 # see if error message already logged add if not | 2182 # see if error message already logged add if not |
| 2220 # response. | 2204 # response. |
| 2221 return str(SeriousError(error)) | 2205 return str(SeriousError(error)) |
| 2222 | 2206 |
| 2223 args = { | 2207 args = { |
| 2224 'ok_message': self._ok_message, | 2208 'ok_message': self._ok_message, |
| 2225 'error_message': self._error_message | 2209 'error_message': self._error_message, |
| 2226 } | 2210 } |
| 2227 | 2211 |
| 2228 try: | 2212 try: |
| 2229 pt = self.instance.templates.load(tplname) | 2213 pt = self.instance.templates.load(tplname) |
| 2230 return pt.render(self, None, None, **args) | 2214 return pt.render(self, None, None, **args) |
| 2571 or not if_range.endswith('"')): | 2555 or not if_range.endswith('"')): |
| 2572 return None | 2556 return None |
| 2573 # If the condition doesn't match the entity tag, then we | 2557 # If the condition doesn't match the entity tag, then we |
| 2574 # must send the client the entire file. | 2558 # must send the client the entire file. |
| 2575 if if_range != etag: | 2559 if if_range != etag: |
| 2576 return | 2560 return None |
| 2577 # The grammar for the Range header value is: | 2561 # The grammar for the Range header value is: |
| 2578 # | 2562 # |
| 2579 # ranges-specifier = byte-ranges-specifier | 2563 # ranges-specifier = byte-ranges-specifier |
| 2580 # byte-ranges-specifier = bytes-unit "=" byte-range-set | 2564 # byte-ranges-specifier = bytes-unit "=" byte-range-set |
| 2581 # byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec ) | 2565 # byte-range-set = 1#( byte-range-spec | suffix-byte-range-spec ) |
| 2759 if hasattr(self.request, "sendfile"): | 2743 if hasattr(self.request, "sendfile"): |
| 2760 self.header() | 2744 self.header() |
| 2761 self._socket_op(self.request.sendfile, filename, offset, length) | 2745 self._socket_op(self.request.sendfile, filename, offset, length) |
| 2762 return | 2746 return |
| 2763 # Fallback to the "write" operation. | 2747 # Fallback to the "write" operation. |
| 2764 f = open(filename, 'rb') | 2748 with open(filename, 'rb') as f: |
| 2765 try: | |
| 2766 if offset: | 2749 if offset: |
| 2767 f.seek(offset) | 2750 f.seek(offset) |
| 2768 content = f.read(length) | 2751 content = f.read(length) |
| 2769 finally: | |
| 2770 f.close() | |
| 2771 self.write(content) | 2752 self.write(content) |
| 2772 | 2753 |
| 2773 def setHeader(self, header, value): | 2754 def setHeader(self, header, value): |
| 2774 """Override or delete a header to be returned to the user's browser. | 2755 """Override or delete a header to be returned to the user's browser. |
| 2775 """ | 2756 """ |
