comparison libervia/server/pages.py @ 1276:cad8f24e23d4

pages: use a coroutine for renderPage: renderPage is now a coroutine, and pages methods are launched using asDeferred, so they can be coroutines too. Full traceback is now logged for uncatched errors.
author Goffi <goffi@goffi.org>
date Fri, 19 Jun 2020 16:47:50 +0200
parents 334d044f2713
children 2e4fcd31f2a9
comparison
equal deleted inserted replaced
1275:334d044f2713 1276:cad8f24e23d4
23 import urllib.request, urllib.parse, urllib.error 23 import urllib.request, urllib.parse, urllib.error
24 import time 24 import time
25 import hashlib 25 import hashlib
26 import copy 26 import copy
27 import json 27 import json
28 import traceback
28 from pathlib import Path 29 from pathlib import Path
29 from functools import reduce 30 from functools import reduce
30 from typing import Optional, List 31 from typing import Optional, List
31 32
32 from twisted.web import server 33 from twisted.web import server
36 from twisted.words.protocols.jabber import jid 37 from twisted.words.protocols.jabber import jid
37 from twisted.python import failure 38 from twisted.python import failure
38 39
39 from sat.core.i18n import _ 40 from sat.core.i18n import _
40 from sat.core import exceptions 41 from sat.core import exceptions
42 from sat.tools.utils import asDeferred
41 from sat.tools.common import date_utils 43 from sat.tools.common import date_utils
42 from sat.tools.common import utils 44 from sat.tools.common import utils
43 from sat.core.log import getLogger 45 from sat.core.log import getLogger
44 from sat_frontends.bridge.bridge_frontend import BridgeException 46 from sat_frontends.bridge.bridge_frontend import BridgeException
45 47
226 if access == C.PAGES_ACCESS_NONE: 228 if access == C.PAGES_ACCESS_NONE:
227 # none pages just return a 404, no further check is needed 229 # none pages just return a 404, no further check is needed
228 return 230 return
229 if template is not None and render is not None: 231 if template is not None and render is not None:
230 log.error(_("render and template methods can't be used at the same time")) 232 log.error(_("render and template methods can't be used at the same time"))
231 if parse_url is not None and not callable(parse_url):
232 log.error(_("parse_url must be a callable"))
233 233
234 # if not None, next rendering will be cached 234 # if not None, next rendering will be cached
235 #  it must then contain a list of the the keys to use (without the page instance) 235 #  it must then contain a list of the the keys to use (without the page instance)
236 # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node] 236 # e.g. [C.SERVICE_PROFILE, "pubsub", server@example.tld, pubsub_node]
237 self._do_cache = None 237 self._do_cache = None
1041 self._checkCacheHeaders(request, cache) 1041 self._checkCacheHeaders(request, cache)
1042 request.write(cache.rendered) 1042 request.write(cache.rendered)
1043 request.finish() 1043 request.finish()
1044 raise failure.Failure(exceptions.CancelError("cache is used")) 1044 raise failure.Failure(exceptions.CancelError("cache is used"))
1045 1045
1046 def _cacheURL(self, __, request, profile): 1046 def _cacheURL(self, request, profile):
1047 self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request) 1047 self.cached_urls.setdefault(profile, {})[request.uri] = CacheURL(request)
1048 1048
1049 @classmethod 1049 @classmethod
1050 def onNodeEvent(cls, host, service, node, event_type, items, profile): 1050 def onNodeEvent(cls, host, service, node, event_type, items, profile):
1051 """Invalidate cache for all pages linked to this node""" 1051 """Invalidate cache for all pages linked to this node"""
1218 if self._do_cache: 1218 if self._do_cache:
1219 # if cache is needed, it will be handled by final page 1219 # if cache is needed, it will be handled by final page
1220 redirect_page._do_cache = self._do_cache 1220 redirect_page._do_cache = self._do_cache
1221 self._do_cache = None 1221 self._do_cache = None
1222 1222
1223 redirect_page.renderPage(request, skip_parse_url=skip_parse_url) 1223 defer.ensureDeferred(
1224 redirect_page.renderPage(request, skip_parse_url=skip_parse_url)
1225 )
1224 raise failure.Failure(exceptions.CancelError("page redirection is used")) 1226 raise failure.Failure(exceptions.CancelError("page redirection is used"))
1225 1227
1226 def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False): 1228 def pageError(self, request, code=C.HTTP_NOT_FOUND, no_body=False):
1227 """generate an error page and terminate the request 1229 """generate an error page and terminate the request
1228 1230
1278 log.warning(_("Can't write page, the request has probably been cancelled " 1280 log.warning(_("Can't write page, the request has probably been cancelled "
1279 "(browser tab closed or reloaded)")) 1281 "(browser tab closed or reloaded)"))
1280 return 1282 return
1281 request.finish() 1283 request.finish()
1282 1284
1283 def _subpagesHandler(self, __, request): 1285 def _subpagesHandler(self, request):
1284 """render subpage if suitable 1286 """render subpage if suitable
1285 1287
1286 this method checks if there is still an unmanaged part of the path 1288 this method checks if there is still an unmanaged part of the path
1287 and check if it corresponds to a subpage. If so, it render the subpage 1289 and check if it corresponds to a subpage. If so, it render the subpage
1288 else it render a NoResource. 1290 else it render a NoResource.
1296 self.pageError(request) 1298 self.pageError(request)
1297 else: 1299 else:
1298 child.render(request) 1300 child.render(request)
1299 raise failure.Failure(exceptions.CancelError("subpage page is used")) 1301 raise failure.Failure(exceptions.CancelError("subpage page is used"))
1300 1302
1301 def _prepare_dynamic(self, __, request): 1303 def _prepare_dynamic(self, request):
1302 # we need to activate dynamic page 1304 # we need to activate dynamic page
1303 # we set data for template, and create/register token 1305 # we set data for template, and create/register token
1304 socket_token = str(uuid.uuid4()) 1306 socket_token = str(uuid.uuid4())
1305 socket_url = self.host.getWebsocketURL(request) 1307 socket_url = self.host.getWebsocketURL(request)
1306 socket_debug = C.boolConst(self.host.debug) 1308 socket_debug = C.boolConst(self.host.debug)
1311 request._signals_registered = [] 1313 request._signals_registered = []
1312 # we will cache registered signals until socket is opened 1314 # we will cache registered signals until socket is opened
1313 request._signals_cache = [] 1315 request._signals_cache = []
1314 self.host.registerWSToken(socket_token, self, request) 1316 self.host.registerWSToken(socket_token, self, request)
1315 1317
1316 def _prepare_render(self, __, request): 1318 def _render_template(self, request):
1317 return defer.maybeDeferred(self.prepare_render, self, request)
1318
1319 def _render_method(self, __, request):
1320 return defer.maybeDeferred(self.render_method, self, request)
1321
1322 def _render_template(self, __, request):
1323 template_data = request.template_data 1319 template_data = request.template_data
1324 1320
1325 # if confirm variable is set in case of successfuly data post 1321 # if confirm variable is set in case of successfuly data post
1326 session_data = self.host.getSessionData(request, session_iface.ISATSession) 1322 session_data = self.host.getSessionData(request, session_iface.ISATSession)
1327 template_data['identities'] = session_data.identities 1323 template_data['identities'] = session_data.identities
1363 cache_path=session_data.cache_dir, 1359 cache_path=session_data.cache_dir,
1364 build_path=f"/{C.BUILD_DIR}/", 1360 build_path=f"/{C.BUILD_DIR}/",
1365 main_menu=self.main_menu, 1361 main_menu=self.main_menu,
1366 **template_data) 1362 **template_data)
1367 1363
1368 def _renderEb(self, failure_, request):
1369 """don't raise error on CancelError"""
1370 failure_.trap(exceptions.CancelError)
1371
1372 def _internalError(self, failure_, request):
1373 """called if an error is not catched"""
1374 if failure_.check(BridgeException) and failure_.value.condition == 'not-allowed':
1375 log.warning("not allowed exception catched")
1376 self.pageError(request, C.HTTP_FORBIDDEN)
1377 log.error(_("Uncatched error for HTTP request on {url}: {msg}")
1378 .format(url=request.URLPath(), msg=failure_))
1379 self.pageError(request, C.HTTP_INTERNAL_ERROR)
1380
1381 def _on_data_post_error(self, failure_, request):
1382 failure_.trap(exceptions.DataError)
1383 # something is wrong with the posted data, we re-display the page with a
1384 # warning notification
1385 session_data = self.host.getSessionData(request, session_iface.ISATSession)
1386 session_data.setPageNotification(self, failure_.value.message, C.LVL_WARNING)
1387 request.setResponseCode(C.HTTP_SEE_OTHER)
1388 request.setHeader("location", request.uri)
1389 request.finish()
1390 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used"))
1391
1392 def _on_data_post_redirect(self, ret, request): 1364 def _on_data_post_redirect(self, ret, request):
1393 """called when page's on_data_post has been done successfuly 1365 """called when page's on_data_post has been done successfuly
1394 1366
1395 This will do a Post/Redirect/Get pattern. 1367 This will do a Post/Redirect/Get pattern.
1396 this method redirect to the same page or to request.data['post_redirect_page'] 1368 this method redirect to the same page or to request.data['post_redirect_page']
1432 request.setResponseCode(C.HTTP_SEE_OTHER) 1404 request.setResponseCode(C.HTTP_SEE_OTHER)
1433 request.setHeader(b"location", redirect_uri) 1405 request.setHeader(b"location", redirect_uri)
1434 request.finish() 1406 request.finish()
1435 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used")) 1407 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used"))
1436 1408
1437 def _on_data_post(self, __, request): 1409 async def _on_data_post(self, request):
1438 csrf_token = self.host.getSessionData( 1410 csrf_token = self.host.getSessionData(
1439 request, session_iface.ISATSession 1411 request, session_iface.ISATSession
1440 ).csrf_token 1412 ).csrf_token
1441 try: 1413 try:
1442 given_csrf = self.getPostedData(request, "csrf_token") 1414 given_csrf = self.getPostedData(request, "csrf_token")
1447 _("invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( 1419 _("invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format(
1448 url=request.uri, ip=request.getClientIP() 1420 url=request.uri, ip=request.getClientIP()
1449 ) 1421 )
1450 ) 1422 )
1451 self.pageError(request, C.HTTP_FORBIDDEN) 1423 self.pageError(request, C.HTTP_FORBIDDEN)
1452 d = defer.maybeDeferred(self.on_data_post, self, request) 1424 try:
1453 d.addCallback(self._on_data_post_redirect, request) 1425 ret = await asDeferred(self.on_data_post, self, request)
1454 d.addErrback(self._on_data_post_error, request) 1426 except exceptions.DataError as e:
1455 return d 1427 # something is wrong with the posted data, we re-display the page with a
1428 # warning notification
1429 session_data = self.host.getSessionData(request, session_iface.ISATSession)
1430 session_data.setPageNotification(self, e.value.message, C.LVL_WARNING)
1431 request.setResponseCode(C.HTTP_SEE_OTHER)
1432 request.setHeader("location", request.uri)
1433 request.finish()
1434 raise failure.Failure(exceptions.CancelError("Post/Redirect/Get is used"))
1435 else:
1436 self._on_data_post_redirect(ret, request)
1456 1437
1457 def getPostedData(self, request, keys, multiple=False, raise_on_missing=True): 1438 def getPostedData(self, request, keys, multiple=False, raise_on_missing=True):
1458 """Get data from a POST request or from URL's query part and decode it 1439 """Get data from a POST request or from URL's query part and decode it
1459 1440
1460 @param request(server.Request): request linked to the session 1441 @param request(server.Request): request linked to the session
1540 return request.data 1521 return request.data
1541 except AttributeError: 1522 except AttributeError:
1542 request.data = {} 1523 request.data = {}
1543 return request.data 1524 return request.data
1544 1525
1545 def _checkAccess(self, data, request): 1526 def _checkAccess(self, request):
1546 """Check access according to self.access 1527 """Check access according to self.access
1547 1528
1548 if access is not granted, show a HTTP_FORBIDDEN pageError and stop request, 1529 if access is not granted, show a HTTP_FORBIDDEN pageError and stop request,
1549 else return data (so it can be inserted in deferred chain 1530 else return data (so it can be inserted in deferred chain
1550 """ 1531 """
1559 self.pageError(request, C.HTTP_FORBIDDEN) 1540 self.pageError(request, C.HTTP_FORBIDDEN)
1560 else: 1541 else:
1561 # registration allowed, we redirect to login page 1542 # registration allowed, we redirect to login page
1562 login_url = self.getPageRedirectURL(request) 1543 login_url = self.getPageRedirectURL(request)
1563 self.HTTPRedirect(request, login_url) 1544 self.HTTPRedirect(request, login_url)
1564
1565 return data
1566 1545
1567 def setBestLocale(self, request): 1546 def setBestLocale(self, request):
1568 """Guess the best locale when it is not specified explicitly by user 1547 """Guess the best locale when it is not specified explicitly by user
1569 1548
1570 This method will check "accept-language" header, and set locale to first 1549 This method will check "accept-language" header, and set locale to first
1640 "dom", selectors=selectors, update_type=update_type, html=html) 1619 "dom", selectors=selectors, update_type=update_type, html=html)
1641 except Exception as e: 1620 except Exception as e:
1642 log.error("Can't renderAndUpdate, html was: {html}".format(html=html)) 1621 log.error("Can't renderAndUpdate, html was: {html}".format(html=html))
1643 raise e 1622 raise e
1644 1623
1645 def renderPage(self, request, skip_parse_url=False): 1624 async def renderPage(self, request, skip_parse_url=False):
1646 """Main method to handle the workflow of a LiberviaPage""" 1625 """Main method to handle the workflow of a LiberviaPage"""
1647 1626
1648 # template_data are the variables passed to template 1627 # template_data are the variables passed to template
1649 if not hasattr(request, "template_data"): 1628 if not hasattr(request, "template_data"):
1650 # if template_data doesn't exist, it's the beginning of the request workflow 1629 # if template_data doesn't exist, it's the beginning of the request workflow
1693 .format(theme=theme, vhost=self.vhost_root))) 1672 .format(theme=theme, vhost=self.vhost_root)))
1694 else: 1673 else:
1695 session_data.theme = theme 1674 session_data.theme = theme
1696 1675
1697 1676
1698 d = defer.Deferred() 1677 try:
1699 d.addCallback(self._checkAccess, request) 1678
1700 1679 try:
1701 if self.redirect is not None: 1680 self._checkAccess(request)
1702 d.addCallback( 1681
1703 lambda __: self.pageRedirect( 1682 if self.redirect is not None:
1704 self.redirect, request, skip_parse_url=False 1683 self.pageRedirect(self.redirect, request, skip_parse_url=False)
1684
1685 if self.parse_url is not None and not skip_parse_url:
1686 if self.url_cache:
1687 profile = self.getProfile(request)
1688 try:
1689 cache_url = self.cached_urls[profile][request.uri]
1690 except KeyError:
1691 # no cache for this URI yet
1692 #  we do normal URL parsing, and then the cache
1693 await asDeferred(self.parse_url, self, request)
1694 self._cacheURL(request, profile)
1695 else:
1696 log.debug(f"using URI cache for {self}")
1697 cache_url.use(request)
1698 else:
1699 await asDeferred(self.parse_url, self, request)
1700
1701 self._subpagesHandler(request)
1702
1703 if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST):
1704 # only HTTP GET and POST are handled so far
1705 self.pageError(request, C.HTTP_BAD_REQUEST)
1706
1707 if request.method == C.HTTP_METHOD_POST:
1708 if self.on_data_post is None:
1709 # if we don't have on_data_post, the page was not expecting POST
1710 # so we return an error
1711 self.pageError(request, C.HTTP_BAD_REQUEST)
1712 else:
1713 await self._on_data_post(request)
1714 # by default, POST follow normal behaviour after on_data_post is called
1715 # this can be changed by a redirection or other method call in on_data_post
1716
1717 if self.dynamic:
1718 self._prepare_dynamic(request)
1719
1720 if self.prepare_render:
1721 await asDeferred(self.prepare_render, self, request)
1722
1723 if self.template:
1724 rendered = self._render_template(request)
1725 elif self.render_method:
1726 rendered = await asDeferred(self.render_method, self, request)
1727 else:
1728 raise exceptions.InternalError(
1729 "No method set to render page, please set a template or use a "
1730 "render method"
1731 )
1732
1733 self.writeData(rendered, request)
1734
1735 except failure.Failure as f:
1736 # we have to unpack to Failure to catch the right Exception
1737 raise f.value
1738
1739 except exceptions.CancelError:
1740 pass
1741 except BridgeException as e:
1742 if e.condition == 'not-allowed':
1743 log.warning("not allowed exception catched")
1744 self.pageError(request, C.HTTP_FORBIDDEN)
1745 else:
1746 log.error(_("Uncatched bridge exception for HTTP request on {url}: {e}")
1747 .format(url=request.URLPath(), e=e))
1748 try:
1749 self.pageError(request, C.HTTP_INTERNAL_ERROR)
1750 except exceptions.CancelError:
1751 pass
1752 except Exception as e:
1753 tb = traceback.format_exc()
1754 log.error(
1755 _("Uncatched error for HTTP request on {url}:\n{tb}")
1756 .format(
1757 url=request.URLPath(),
1758 e_name=e.__class__.__name__,
1759 e=e,
1760 tb=tb,
1705 ) 1761 )
1706 ) 1762 )
1707 1763 try:
1708 if self.parse_url is not None and not skip_parse_url: 1764 self.pageError(request, C.HTTP_INTERNAL_ERROR)
1709 if self.url_cache: 1765 except exceptions.CancelError:
1710 profile = self.getProfile(request) 1766 pass
1711 try: 1767
1712 cache_url = self.cached_urls[profile][request.uri] 1768 def render_GET(self, request):
1713 except KeyError: 1769 defer.ensureDeferred(self.renderPage(request))
1714 # no cache for this URI yet
1715 #  we do normal URL parsing, and then the cache
1716 d.addCallback(self.parse_url, request)
1717 d.addCallback(self._cacheURL, request, profile)
1718 else:
1719 log.debug(_("using URI cache for {page}").format(page=self))
1720 cache_url.use(request)
1721 else:
1722 d.addCallback(self.parse_url, request)
1723
1724 d.addCallback(self._subpagesHandler, request)
1725
1726 if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST):
1727 # only HTTP GET and POST are handled so far
1728 d.addCallback(lambda __: self.pageError(request, C.HTTP_BAD_REQUEST))
1729
1730 if request.method == C.HTTP_METHOD_POST:
1731 if self.on_data_post is None:
1732 # if we don't have on_data_post, the page was not expecting POST
1733 # so we return an error
1734 d.addCallback(lambda __: self.pageError(request, C.HTTP_BAD_REQUEST))
1735 else:
1736 d.addCallback(self._on_data_post, request)
1737 # by default, POST follow normal behaviour after on_data_post is called
1738 # this can be changed by a redirection or other method call in on_data_post
1739
1740 if self.dynamic:
1741 d.addCallback(self._prepare_dynamic, request)
1742
1743 if self.prepare_render:
1744 d.addCallback(self._prepare_render, request)
1745
1746 if self.template:
1747 d.addCallback(self._render_template, request)
1748 elif self.render_method:
1749 d.addCallback(self._render_method, request)
1750
1751 d.addCallback(self.writeData, request)
1752 d.addErrback(self._renderEb, request)
1753 d.addErrback(self._internalError, request)
1754 d.callback(self)
1755 return server.NOT_DONE_YET 1770 return server.NOT_DONE_YET
1756 1771
1757 def render_GET(self, request):
1758 return self.renderPage(request)
1759
1760 def render_POST(self, request): 1772 def render_POST(self, request):
1761 return self.renderPage(request) 1773 defer.ensureDeferred(self.renderPage(request))
1774 return server.NOT_DONE_YET