comparison roundup/cgi/client.py @ 8185:e84d4585b16d

fix(web): issue2551356. Add etag header for not-modified (304) request. When a 304 is returned to a conditional request for a static file, print an ETag for the response. ETag was always sent with a 200 response. This also adds initial support for if-none-match conditional requests for static files. Changes: Refactors the if-modified-since code out to a method. It moves a file stat call from serve_static_file to _serve_file so that an etag can be generated by both serve_static_file and serve_file which call _serve_file. Tests added. This does not test the codepath where serve_file pulls content from the database rather than from a local file on disk. Test mocking _serve_file changed to account for 5th argument to serve_file BREAKING CHANGE: function signature for client.py-Client::_serve_file() now has 5 not 4 parameters (added etag param). Since this is a "hidden" method I am not too worried about it.
author John Rouillard <rouilj@ieee.org>
date Tue, 10 Dec 2024 16:06:13 -0500
parents bd628e64725f
children b938fd5223ae
comparison
equal deleted inserted replaced
8184:53dba022d4cd 8185:e84d4585b16d
1947 if not filename: 1947 if not filename:
1948 content = klass.get(nodeid, 'content') 1948 content = klass.get(nodeid, 'content')
1949 1949
1950 lmt = klass.get(nodeid, 'activity').timestamp() 1950 lmt = klass.get(nodeid, 'activity').timestamp()
1951 1951
1952 self._serve_file(lmt, mime_type, content, filename) 1952 self._serve_file(lmt, None, mime_type, content, filename)
1953 1953
1954 def serve_static_file(self, file): 1954 def serve_static_file(self, file):
1955 """ Serve up the file named from the templates dir 1955 """ Serve up the file named from the templates dir
1956 """ 1956 """
1957 # figure the filename - try STATIC_FILES, then TEMPLATES dir 1957 # figure the filename - try STATIC_FILES, then TEMPLATES dir
1987 break 1987 break
1988 1988
1989 if filename is None: # we didn't find a filename 1989 if filename is None: # we didn't find a filename
1990 raise NotFound(file) 1990 raise NotFound(file)
1991 1991
1992 # last-modified time
1993 lmt = os.stat(filename)[stat.ST_MTIME]
1994
1995 # detemine meta-type 1992 # detemine meta-type
1996 file = str(file) 1993 file = str(file)
1997 mime_type = mimetypes.guess_type(file)[0] 1994 mime_type = mimetypes.guess_type(file)[0]
1998 if not mime_type: 1995 if not mime_type:
1999 mime_type = 'text/css' if file.endswith('.css') else 'text/plain' 1996 mime_type = 'text/css' if file.endswith('.css') else 'text/plain'
2007 self.Cache_Control[fn] 2004 self.Cache_Control[fn]
2008 elif mime_type in self.Cache_Control: 2005 elif mime_type in self.Cache_Control:
2009 self.additional_headers['Cache-Control'] = \ 2006 self.additional_headers['Cache-Control'] = \
2010 self.Cache_Control[mime_type] 2007 self.Cache_Control[mime_type]
2011 2008
2012 self._serve_file(lmt, mime_type, '', filename) 2009 self._serve_file(None, None, mime_type, '', filename)
2013 2010
2014 def _serve_file(self, lmt, mime_type, content=None, filename=None): 2011 def _serve_file(self, lmt, etag, mime_type, content=None, filename=None):
2015 """ guts of serve_file() and serve_static_file() 2012 """guts of serve_file() and serve_static_file()
2013
2014 if lmt or etag are None, derive them from file filename.
2015
2016 Handles if-modified-since and if-none-match etag
2017 conditional gets.
2018
2019 It produces an raw etag header without encoding suffix.
2020 But it adds Accept-Encoding to the vary header.
2021
2016 """ 2022 """
2017 2023 if filename:
2018 # spit out headers 2024 stat_info = os.stat(filename)
2019 self.additional_headers['Last-Modified'] = email.utils.formatdate(lmt, 2025
2020 usegmt=True) 2026 if lmt is None:
2021 2027 # last-modified time
2022 ims = None 2028 lmt = stat_info[stat.ST_MTIME]
2023 # see if there's an if-modified-since... 2029 if etag is None:
2024 # used if this is run behind a non-caching http proxy 2030 # FIXME: maybe etag should depend on encoding.
2025 if hasattr(self.request, 'headers'): 2031 # it is an apache compatible etag without encoding.
2026 ims = self.request.headers.get('if-modified-since') 2032 etag = '"%x-%x-%x"' % (stat_info[stat.ST_INO],
2027 elif 'HTTP_IF_MODIFIED_SINCE' in self.env: 2033 stat_info[stat.ST_SIZE],
2028 # cgi will put the header in the env var 2034 stat_info[stat.ST_MTIME])
2029 ims = self.env['HTTP_IF_MODIFIED_SINCE'] 2035
2030 if ims: 2036 # spit out headers for conditional request
2031 ims = email.utils.parsedate(ims)[:6] 2037 self.setHeader("ETag", etag)
2032 lmtt = time.gmtime(lmt)[:6] 2038 self.additional_headers['Last-Modified'] = \
2033 if lmtt <= ims: 2039 email.utils.formatdate(lmt, usegmt=True)
2040
2041 inm = None
2042 # ETag is a more strict check than modified date. Use etag
2043 # check if available. Skip testing modified data.
2044 if hasattr(self.request, 'headers'):
2045 inm = self.request.headers.get('if-none-match')
2046 elif 'HTTP_IF_NONE_MATCH' in self.env:
2047 # maybe the cgi will put the header in the env var
2048 inm = self.env['HTTP_ETAG']
2049 if inm and etag == inm:
2050 # because we can compress, always set Accept-Encoding
2051 # value. Otherwise caches can serve up the wrong info
2052 # if their cached copy has no compression.
2053 self.setVary("Accept-Encoding")
2054 '''
2055 to solve issue2551356 I may need to determine
2056 the content encoding.
2057 if (self.determine_content_encoding()):
2058 '''
2059 raise NotModified
2060
2061 if self.if_not_modified_since(lmt):
2034 # because we can compress, always set Accept-Encoding 2062 # because we can compress, always set Accept-Encoding
2035 # value. Otherwise caches can serve up the wrong info 2063 # value. Otherwise caches can serve up the wrong info
2036 # if their cached copy has no compression. 2064 # if their cached copy has no compression.
2037 self.setVary("Accept-Encoding") 2065 self.setVary("Accept-Encoding")
2038 ''' 2066 '''
2048 if filename: 2076 if filename:
2049 self.write_file(filename) 2077 self.write_file(filename)
2050 else: 2078 else:
2051 self.additional_headers['Content-Length'] = str(len(content)) 2079 self.additional_headers['Content-Length'] = str(len(content))
2052 self.write(content) 2080 self.write(content)
2081
2082 def if_not_modified_since(self, lmt):
2083 ims = None
2084 # see if there's an if-modified-since...
2085 if hasattr(self.request, 'headers'):
2086 ims = self.request.headers.get('if-modified-since')
2087 elif 'HTTP_IF_MODIFIED_SINCE' in self.env:
2088 # cgi will put the header in the env var
2089 ims = self.env['HTTP_IF_MODIFIED_SINCE']
2090
2091 if ims:
2092 datestamp = email.utils.parsedate(ims)
2093 if datestamp is not None:
2094 ims = datestamp[:6]
2095 else:
2096 # set to beginning of time so whole file will be sent
2097 ims = (0, 0, 0, 0, 0, 0)
2098 lmtt = time.gmtime(lmt)[:6]
2099 return lmtt <= ims
2100
2101 return False
2053 2102
2054 def send_error_to_admin(self, subject, html, txt): 2103 def send_error_to_admin(self, subject, html, txt):
2055 """Send traceback information to admin via email. 2104 """Send traceback information to admin via email.
2056 We send both, the formatted html (with more information) and 2105 We send both, the formatted html (with more information) and
2057 the text version of the traceback. We use 2106 the text version of the traceback. We use

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