comparison src/server/pages.py @ 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 66a050b32df8
comparison
equal deleted inserted replaced
1018:78af5457d3f8 1019:34240d08f682
24 from twisted.python import failure 24 from twisted.python import failure
25 25
26 from sat.core.i18n import _ 26 from sat.core.i18n import _
27 from sat.core import exceptions 27 from sat.core import exceptions
28 from sat.tools.common import uri as common_uri 28 from sat.tools.common import uri as common_uri
29 from sat.tools import utils
29 from sat.core.log import getLogger 30 from sat.core.log import getLogger
30 log = getLogger(__name__) 31 log = getLogger(__name__)
31 from libervia.server.constants import Const as C 32 from libervia.server.constants import Const as C
32 from libervia.server import session_iface 33 from libervia.server import session_iface
33 from libervia.server.utils import quote 34 from libervia.server.utils import quote
36 from collections import namedtuple 37 from collections import namedtuple
37 import uuid 38 import uuid
38 import os.path 39 import os.path
39 import urllib 40 import urllib
40 import time 41 import time
42 import hashlib
41 43
42 WebsocketMeta = namedtuple("WebsocketMeta", ('url', 'token', 'debug')) 44 WebsocketMeta = namedtuple("WebsocketMeta", ('url', 'token', 'debug'))
43 45
44 46
45 47
67 def __init__(self, rendered): 69 def __init__(self, rendered):
68 super(CachePage, self).__init__() 70 super(CachePage, self).__init__()
69 self._created = time.time() 71 self._created = time.time()
70 self._last_access = self._created 72 self._last_access = self._created
71 self._rendered = rendered 73 self._rendered = rendered
74 self._hash = hashlib.sha256(rendered).hexdigest()
72 75
73 @property 76 @property
74 def rendered(self): 77 def rendered(self):
75 return self._rendered 78 return self._rendered
76 79
80 @property
81 def hash(self):
82 return self._hash
77 83
78 84
79 class CacheURL(CacheBase): 85 class CacheURL(CacheBase):
80 86
81 def __init__(self, request): 87 def __init__(self, request):
486 """ 492 """
487 pathElement = request.postpath.pop(0) 493 pathElement = request.postpath.pop(0)
488 request.prepath.append(pathElement) 494 request.prepath.append(pathElement)
489 return urllib.unquote(pathElement).decode('utf-8') 495 return urllib.unquote(pathElement).decode('utf-8')
490 496
497 ## Cache handling ##
498
499 def _setCacheHeaders(self, request, cache):
500 """Set ETag and Last-Modified HTTP headers, used for caching"""
501 request.setHeader('ETag', cache.hash)
502 last_modified = self.host.getHTTPDate(cache.created)
503 request.setHeader('Last-Modified', last_modified)
504
505 def _checkCacheHeaders(self, request, cache):
506 """Check if a cache condition is set on the request
507
508 if condition is valid, C.HTTP_NOT_MODIFIED is returned
509 """
510 etag_match = request.getHeader('If-None-Match')
511 if etag_match is not None:
512 if cache.hash == etag_match:
513 self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True)
514 else:
515 modified_match = request.getHeader('If-Modified-Since')
516 if modified_match is not None:
517 modified = utils.date_parse(modified_match)
518 if modified >= int(cache.created):
519 self.pageError(request, C.HTTP_NOT_MODIFIED, no_body=True)
520
491 def checkCacheSubscribeCb(self, sub_id, service, node): 521 def checkCacheSubscribeCb(self, sub_id, service, node):
492 self.cache_pubsub_sub.add((service, node, sub_id)) 522 self.cache_pubsub_sub.add((service, node, sub_id))
493 523
494 def checkCacheSubscribeEb(self, failure_, service, node): 524 def checkCacheSubscribeEb(self, failure_, service, node):
495 log.warning(_(u"Can't subscribe to node: {msg}").format(msg=failure_)) 525 log.warning(_(u"Can't subscribe to node: {msg}").format(msg=failure_))
507 @param cache_type(int): on of C.CACHE_* const. 537 @param cache_type(int): on of C.CACHE_* const.
508 @param **kwargs: args according to cache_type: 538 @param **kwargs: args according to cache_type:
509 C.CACHE_PUBSUB: 539 C.CACHE_PUBSUB:
510 service: pubsub service 540 service: pubsub service
511 node: pubsub node 541 node: pubsub node
512 short: short name of feature (needed if node is empty) 542 short: short name of feature (needed if node is empty to find namespace)
513 543
514 """ 544 """
515 if request.postpath: 545 if request.postpath:
516 # we are not on the final page, no need to go further 546 # we are not on the final page, no need to go further
517 return 547 return
545 575
546 else: 576 else:
547 raise exceptions.InternalError(u'Unknown cache_type') 577 raise exceptions.InternalError(u'Unknown cache_type')
548 log.debug(u'using cache for {page}'.format(page=self)) 578 log.debug(u'using cache for {page}'.format(page=self))
549 cache.last_access = time.time() 579 cache.last_access = time.time()
580 self._setCacheHeaders(request, cache)
581 self._checkCacheHeaders(request, cache)
550 request.write(cache.rendered) 582 request.write(cache.rendered)
551 request.finish() 583 request.finish()
552 raise failure.Failure(exceptions.CancelError(u'cache is used')) 584 raise failure.Failure(exceptions.CancelError(u'cache is used'))
553 585
554 def _cacheURL(self, dummy, request, profile): 586 def _cacheURL(self, dummy, request, profile):
694 redirect_page = redirect_page.original.children[subpage] 726 redirect_page = redirect_page.original.children[subpage]
695 727
696 redirect_page.renderPage(request, skip_parse_url=skip_parse_url) 728 redirect_page.renderPage(request, skip_parse_url=skip_parse_url)
697 raise failure.Failure(exceptions.CancelError(u'page redirection is used')) 729 raise failure.Failure(exceptions.CancelError(u'page redirection is used'))
698 730
699 def pageError(self, request, code=C.HTTP_NOT_FOUND): 731 def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False):
700 """generate an error page and terminate the request 732 """generate an error page and terminate the request
701 733
702 @param request(server.Request): HTTP request 734 @param request(server.Request): HTTP request
703 @param core(int): error code to use 735 @param core(int): error code to use
704 """ 736 @param no_body: don't write body if True
705 template = u'error/' + unicode(code) + '.html' 737 """
706
707 request.setResponseCode(code) 738 request.setResponseCode(code)
708 739 if no_body:
709 rendered = self.host.renderer.render( 740 request.finish()
710 template, 741 else:
711 root_path = '/templates/', 742 template = u'error/' + unicode(code) + '.html'
712 error_code = code, 743
713 **request.template_data) 744 rendered = self.host.renderer.render(
714 745 template,
715 self.writeData(rendered, request) 746 root_path = '/templates/',
747 error_code = code,
748 **request.template_data)
749
750 self.writeData(rendered, request)
716 raise failure.Failure(exceptions.CancelError(u'error page is used')) 751 raise failure.Failure(exceptions.CancelError(u'error page is used'))
717 752
718 def writeData(self, data, request): 753 def writeData(self, data, request):
719 """write data to transport and finish the request""" 754 """write data to transport and finish the request"""
720 if data is None: 755 if data is None:
721 self.pageError(request) 756 self.pageError(request)
722 data_encoded = data.encode('utf-8') 757 data_encoded = data.encode('utf-8')
723 request.write(data_encoded) 758
724 request.finish()
725 if self._do_cache is not None: 759 if self._do_cache is not None:
726 cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache) 760 cache = reduce(lambda d, k: d.setdefault(k, {}), self._do_cache, self.cache)
727 cache[self] = Cache(data_encoded) 761 page_cache = cache[self] = CachePage(data_encoded)
762 self._setCacheHeaders(request, page_cache)
728 log.debug(_(u'{page} put in cache for [{profile}]').format( 763 log.debug(_(u'{page} put in cache for [{profile}]').format(
729 page=self, 764 page=self,
730 profile=self._do_cache[0])) 765 profile=self._do_cache[0]))
731 self._do_cache = None 766 self._do_cache = None
767 self._checkCacheHeaders(request, page_cache)
768
769 request.write(data_encoded)
770 request.finish()
732 771
733 def _subpagesHandler(self, dummy, request): 772 def _subpagesHandler(self, dummy, request):
734 """render subpage if suitable 773 """render subpage if suitable
735 774
736 this method checks if there is still an unmanaged part of the path 775 this method checks if there is still an unmanaged part of the path