# HG changeset patch # User Goffi # Date 1516536846 -3600 # Node ID 34240d08f682c1062a2bb9c7c27c27618dc39809 # Parent 78af5457d3f80ecdffd37e89f8091cdc9068dc80 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. diff -r 78af5457d3f8 -r 34240d08f682 src/server/constants.py --- 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") diff -r 78af5457d3f8 -r 34240d08f682 src/server/pages.py --- 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 diff -r 78af5457d3f8 -r 34240d08f682 src/server/server.py --- 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):