Mercurial > libervia-web
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 |