# HG changeset patch # User Goffi # Date 1601320341 -7200 # Node ID 389a83eefe62f43d6ff5226a61d96a79bf160c51 # Parent 2da573bf3f8bc603f526ca04e36cd965e4927843 server: SàT applications integration: - a SàT Application can be added to the menu (if necessary values are exposed), by using the `sat-app:[application_name]` in `menu_json` or `menu_extra_json`. The application will then be started with Libervia, and embedded, i.e. Libervia menu will appear and application will be integrated under it. - the same `sat-app:[application_name]` thing can be used in redirection, in this case the redirection will reverse proxy directly the application, without embedding it (no Libervia menu will appear) - the ReverseProxy will replace headers if necessary to allow embedding in a iframe from the same domain - new `embed_app` page to embed a SàT Application diff -r 2da573bf3f8b -r 389a83eefe62 libervia/pages/embed/page_meta.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/pages/embed/page_meta.py Mon Sep 28 21:12:21 2020 +0200 @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +from sat.core.log import getLogger +from sat.core import exceptions +from libervia.server.constants import Const as C + +log = getLogger(__name__) + +name = "embed_app" +template = "embed/embed.html" + + +def parse_url(self, request): + self.getPathArgs(request, ["app_name"], min_args=1) + data = self.getRData(request) + app_name = data["app_name"] + try: + app_data = self.vhost_root.sat_apps[app_name] + except KeyError: + self.pageError(request, C.HTTP_BAD_REQUEST) + template_data = request.template_data + template_data['full_screen_body'] = True + try: + template_data["target_url"] = app_data["url_prefix"] + except KeyError: + raise exceptions.InternalError(f'"url_prefix" is missing for {app_name!r}') diff -r 2da573bf3f8b -r 389a83eefe62 libervia/server/proxy.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/server/proxy.py Mon Sep 28 21:12:21 2020 +0200 @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +# Libervia: a Salut à Toi frontend +# Copyright (C) 2011-2020 Jérôme Poisson + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +from twisted.web import proxy +from twisted.python.compat import urlquote +from sat.core.log import getLogger + +log = getLogger(__name__) + + + +class SatProxyClient(proxy.ProxyClient): + + def handleHeader(self, key, value): + if key.lower() == b"x-frame-options": + value = b"sameorigin" + elif key.lower() == b"content-security-policy": + value = value.replace(b"frame-ancestors 'none'", b"frame-ancestors 'self'") + + super().handleHeader(key, value) + + +class SatProxyClientFactory(proxy.ProxyClientFactory): + protocol = SatProxyClient + + +class SatReverseProxyResource(proxy.ReverseProxyResource): + """Resource Proxy rewritting headers to allow embedding in iframe on same domain""" + proxyClientFactoryClass = SatProxyClientFactory + + def getChild(self, path, request): + return SatReverseProxyResource( + self.host, self.port, + self.path + b'/' + urlquote(path, safe=b"").encode('utf-8'), + self.reactor + ) diff -r 2da573bf3f8b -r 389a83eefe62 libervia/server/server.py --- a/libervia/server/server.py Mon Sep 28 17:12:04 2020 +0200 +++ b/libervia/server/server.py Mon Sep 28 21:12:21 2020 +0200 @@ -31,6 +31,7 @@ from twisted.web import resource as web_resource from twisted.web import util as web_util from twisted.web import vhost +from . import proxy from twisted.python.components import registerAdapter from twisted.python import failure from twisted.python import filepath @@ -225,6 +226,8 @@ self.pages_redirects = {} self.cached_urls = {} self.main_menu = None + # map SàT application names => data + self.sat_apps = {} self.build_path = host.getBuildPath(site_name) self.build_path.mkdir(parents=True, exist_ok=True) self.dev_build_path = host.getBuildPath(site_name, dev=True) @@ -258,7 +261,66 @@ C.TEMPLATE_TPL_DIR, theme) - def _initRedirections(self, options): + def addResourceToPath(self, path: str, resource: web_resource.Resource) -> None: + """Add a resource to the given path + + A "NoResource" will be used for all intermediate segments + """ + segments, __, last_segment = path.rpartition("/") + url_segments = segments.split("/") if segments else [] + current = self + for segment in url_segments: + resource = web_resource.NoResource() + current.putChild(segment, resource) + current = resource + + current.putChild( + last_segment.encode('utf-8'), + resource + ) + + async def _startApp(self, app_name, extra=None): + if extra is None: + extra = {} + log.info(_( + "starting application {app_name}").format(app_name=app_name)) + await self.host.bridgeCall( + "applicationStart", app_name, data_format.serialise(extra) + ) + app_data = self.sat_apps[app_name] = data_format.deserialise( + await self.host.bridgeCall( + "applicationExposedGet", app_name, "", "")) + + try: + web_port = int(app_data['ports']['web'].split(':')[1]) + except (KeyError, ValueError): + log.warning(_( + "no web port found for application {app_name!r}, can't use it " + ).format(app_name=app_name)) + raise exceptions.DataError("no web port found") + + try: + url_prefix = app_data['url_prefix'].strip().rstrip('/') + except (KeyError, AttributeError) as e: + log.warning(_( + "no URL prefix specified for this application, we can't embed it: {msg}") + .format(msg=e)) + raise exceptions.DataError("no URL prefix") + + if not url_prefix.startswith('/'): + raise exceptions.DataError( + f"invalid URL prefix, it must start with '/': {url_prefix!r}") + + res = proxy.SatReverseProxyResource( + "localhost", + web_port, + url_prefix.encode() + ) + self.addResourceToPath(url_prefix, res) + + return app_data + + async def _initRedirections(self, options): url_redirections = options["url_redirections_dict"] url_redirections = url_redirections.get(self.site_name, {}) @@ -395,26 +457,40 @@ "file redirection must have an absolute path: e.g. " "file:/path/to/my/file") # for file redirection, we directly put child here - segments, __, last_segment = old.rpartition("/") - url_segments = segments.split("/") if segments else [] - current = self - for segment in url_segments: - resource = web_resource.NoResource() - current.putChild(segment, resource) - current = resource resource_class = ( ProtectedFile if new_data.get("protected", True) else static.File ) - current.putChild( - last_segment.encode('utf-8'), - resource_class(path, defaultType="application/octet-stream") - ) + res = resource_class(path, defaultType="application/octet-stream") + self.addResourceToPath(old, res) log.info("[{host_name}] Added redirection from /{old} to file system " "path {path}".format(host_name=self.host_name, old=old, path=path)) - continue # we don't want to use redirection system, so we continue here + + # we don't want to use redirection system, so we continue here + continue + + elif new_url.scheme == "sat-app": + # a SàT application + app_name = urllib.parse.unquote(new_url.path).lower().strip() + extra = {"url_prefix": f"/{old}"} + try: + await self._startApp(app_name, extra) + except Exception as e: + log.warning(_( + "Can't launch {app_name!r} for path /{old}: {e}").format( + app_name=app_name, old=old, e=e)) + continue + + log.info("[{host_name}] Added redirection from /{old} to application " + "{app_name}".format( + host_name=self.host_name, + old=old, + app_name=app_name)) + + # normal redirection system is not used here + continue else: raise NotImplementedError( "{scheme}: scheme is not managed for url_redirections_dict".format( @@ -432,7 +508,7 @@ if not "" in self.redirections: self.redirections[""] = self._getRequestData(C.LIBERVIA_PAGE_START) - def _setMenu(self, menus): + async def _setMenu(self, menus): menus = menus.get(self.site_name, []) main_menu = [] for menu in menus: @@ -448,6 +524,29 @@ log.error(msg) raise ValueError(msg) page_name, url = menu + elif menu.startswith("sat-app:"): + app_name = menu[8:].strip().lower() + app_data = await self._startApp(app_name) + front_url = app_data['front_url'] + options = self.host.options + url_redirections = options["url_redirections_dict"].setdefault( + self.site_name, {}) + if front_url in url_redirections: + raise exceptions.ConflictError( + f"There is already a redirection from {front_url!r}, can't add " + f"{app_name!r}") + + url_redirections[front_url] = { + "page": 'embed_app', + "path_args": [app_name] + } + + page_name = app_data.get('web_label', app_name).title() + url = front_url + + log.debug( + f"Application {app_name} added to menu of {self.site_name}" + ) else: page_name = menu try: @@ -861,7 +960,7 @@ await tasks_manager.parseTasks() await tasks_manager.runTasks() # FIXME: handle _setMenu in a more generic way, taking care of external sites - self.sat_root._setMenu(self.options["menu_json"]) + await self.sat_root._setMenu(self.options["menu_json"]) self.vhost_root.default = default_root existing_vhosts = {b'': default_root} @@ -912,7 +1011,7 @@ tasks_manager = TasksManager(self, res) await tasks_manager.parseTasks() await tasks_manager.runTasks() - res._setMenu(self.options["menu_json"]) + await res._setMenu(self.options["menu_json"]) self.vhost_root.addHost(host_name.encode('utf-8'), res) @@ -965,7 +1064,7 @@ # redirections for root in self.roots: - root._initRedirections(self.options) + await root._initRedirections(self.options) # no need to keep url_redirections_dict, it will not be used anymore del self.options["url_redirections_dict"]