changeset 1019:34240d08f682

pages: HTTP cache headers handling: when checkCache is used, HTTP headers handling cache are now used: - ETag is first checked, using a hash of the rendered content - Last-Modified is used as a fallback is client is not handling ETag When suitable, a HTTP 304 code (Not Modified) wihtout body is returned instead of the whole page.
author Goffi <goffi@goffi.org>
date Sun, 21 Jan 2018 13:14:06 +0100
parents 78af5457d3f8
children 1c9b6d2c30b5
files src/server/constants.py src/server/pages.py src/server/server.py
diffstat 3 files changed, 66 insertions(+), 13 deletions(-) [+]
line wrap: on
line diff
--- a/src/server/constants.py	Sun Jan 21 13:08:54 2018 +0100
+++ b/src/server/constants.py	Sun Jan 21 13:14:06 2018 +0100
@@ -72,6 +72,7 @@
 
     ## HTTP codes ##
     HTTP_SEE_OTHER = 303
+    HTTP_NOT_MODIFIED = 304
     HTTP_BAD_REQUEST = 400
     HTTP_UNAUTHORIZED = 401
     HTTP_NOT_FOUND = 404
@@ -80,3 +81,7 @@
 
     ## Cache ##
     CACHE_PUBSUB = 0
+
+    ## Date/Time ##
+    HTTP_DAYS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
+    HTTP_MONTH = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
--- a/src/server/pages.py	Sun Jan 21 13:08:54 2018 +0100
+++ b/src/server/pages.py	Sun Jan 21 13:14:06 2018 +0100
@@ -26,6 +26,7 @@
 from sat.core.i18n import _
 from sat.core import exceptions
 from sat.tools.common import uri as common_uri
+from sat.tools import utils
 from sat.core.log import getLogger
 log = getLogger(__name__)
 from libervia.server.constants import Const as C
@@ -38,6 +39,7 @@
 import os.path
 import urllib
 import time
+import hashlib
 
 WebsocketMeta = namedtuple("WebsocketMeta", ('url', 'token', 'debug'))
 
@@ -69,11 +71,15 @@
         self._created = time.time()
         self._last_access = self._created
         self._rendered = rendered
+        self._hash = hashlib.sha256(rendered).hexdigest()
 
     @property
     def rendered(self):
         return self._rendered
 
+    @property
+    def hash(self):
+        return self._hash
 
 
 class CacheURL(CacheBase):
@@ -488,6 +494,30 @@
         request.prepath.append(pathElement)
         return urllib.unquote(pathElement).decode('utf-8')
 
+    ## Cache handling ##
+
+    def _setCacheHeaders(self, request, cache):
+        """Set ETag and Last-Modified HTTP headers, used for caching"""
+        request.setHeader('ETag', cache.hash)
+        last_modified = self.host.getHTTPDate(cache.created)
+        request.setHeader('Last-Modified', last_modified)
+
+    def _checkCacheHeaders(self, request, cache):
+        """Check if a cache condition is set on the request
+
+        if condition is valid, C.HTTP_NOT_MODIFIED is returned
+        """
+        etag_match = request.getHeader('If-None-Match')
+        if etag_match is not None:
+            if cache.hash == etag_match:
+                self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True)
+        else:
+            modified_match = request.getHeader('If-Modified-Since')
+            if modified_match is not None:
+                modified = utils.date_parse(modified_match)
+                if modified >= int(cache.created):
+                    self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True)
+
     def checkCacheSubscribeCb(self, sub_id, service, node):
         self.cache_pubsub_sub.add((service, node, sub_id))
 
@@ -509,7 +539,7 @@
             C.CACHE_PUBSUB:
                 service: pubsub service
                 node: pubsub node
-                short: short name of feature (needed if node is empty)
+                short: short name of feature (needed if node is empty to find namespace)
 
         """
         if request.postpath:
@@ -547,6 +577,8 @@
             raise exceptions.InternalError(u'Unknown cache_type')
         log.debug(u'using cache for {page}'.format(page=self))
         cache.last_access = time.time()
+        self._setCacheHeaders(request, cache)
+        self._checkCacheHeaders(request, cache)
         request.write(cache.rendered)
         request.finish()
         raise failure.Failure(exceptions.CancelError(u'cache is used'))
@@ -696,23 +728,26 @@
         redirect_page.renderPage(request, skip_parse_url=skip_parse_url)
         raise failure.Failure(exceptions.CancelError(u'page redirection is used'))
 
-    def pageError(self, request, code=C.HTTP_NOT_FOUND):
+    def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False):
         """generate an error page and terminate the request
 
         @param request(server.Request): HTTP request
         @param core(int): error code to use
+        @param no_body: don't write body if True
         """
-        template = u'error/' + unicode(code) + '.html'
-
         request.setResponseCode(code)
+        if no_body:
+            request.finish()
+        else:
+            template = u'error/' + unicode(code) + '.html'
 
-        rendered = self.host.renderer.render(
-            template,
-            root_path = '/templates/',
-            error_code = code,
-            **request.template_data)
+            rendered = self.host.renderer.render(
+                template,
+                root_path = '/templates/',
+                error_code = code,
+                **request.template_data)
 
-        self.writeData(rendered, request)
+            self.writeData(rendered, request)
         raise failure.Failure(exceptions.CancelError(u'error page is used'))
 
     def writeData(self, data, request):
@@ -720,15 +755,19 @@
         if data is None:
             self.pageError(request)
         data_encoded = data.encode('utf-8')
-        request.write(data_encoded)
-        request.finish()
+
         if self._do_cache is not None:
             cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache)
-            cache[self] = Cache(data_encoded)
+            page_cache = cache[self] = CachePage(data_encoded)
+            self._setCacheHeaders(request, page_cache)
             log.debug(_(u'{page} put in cache for [{profile}]').format(
                 page=self,
                 profile=self._do_cache[0]))
             self._do_cache = None
+            self._checkCacheHeaders(request, page_cache)
+
+        request.write(data_encoded)
+        request.finish()
 
     def _subpagesHandler(self, dummy, request):
         """render subpage if suitable
--- a/src/server/server.py	Sun Jan 21 13:08:54 2018 +0100
+++ b/src/server/server.py	Sun Jan 21 13:14:06 2018 +0100
@@ -1909,6 +1909,15 @@
     def registerWSToken(self, token, page, request):
         websockets.LiberviaPageWSProtocol.registerToken(token, page, request)
 
+    ## Various utils ##
+
+    def getHTTPDate(self, timestamp=None):
+        now = time.gmtime(timestamp)
+        fmt_date = u"{day_name}, %d {month_name} %Y %H:%M:%S GMT".format(
+            day_name = C.HTTP_DAYS[now.tm_wday],
+            month_name = C.HTTP_MONTH[now.tm_mon-1])
+        return time.strftime(fmt_date, now)
+
     ## TLS related methods ##
 
     def _TLSOptionsCheck(self):