comparison libervia/server/server.py @ 1360:389a83eefe62

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
author Goffi <goffi@goffi.org>
date Mon, 28 Sep 2020 21:12:21 +0200
parents 2da573bf3f8b
children 626b7bbb7f90
comparison
equal deleted inserted replaced
1359:2da573bf3f8b 1360:389a83eefe62
29 from twisted.web import server 29 from twisted.web import server
30 from twisted.web import static 30 from twisted.web import static
31 from twisted.web import resource as web_resource 31 from twisted.web import resource as web_resource
32 from twisted.web import util as web_util 32 from twisted.web import util as web_util
33 from twisted.web import vhost 33 from twisted.web import vhost
34 from . import proxy
34 from twisted.python.components import registerAdapter 35 from twisted.python.components import registerAdapter
35 from twisted.python import failure 36 from twisted.python import failure
36 from twisted.python import filepath 37 from twisted.python import filepath
37 from twisted.words.protocols.jabber import jid 38 from twisted.words.protocols.jabber import jid
38 39
223 224
224 self.uri_callbacks = {} 225 self.uri_callbacks = {}
225 self.pages_redirects = {} 226 self.pages_redirects = {}
226 self.cached_urls = {} 227 self.cached_urls = {}
227 self.main_menu = None 228 self.main_menu = None
229 # map SàT application names => data
230 self.sat_apps = {}
228 self.build_path = host.getBuildPath(site_name) 231 self.build_path = host.getBuildPath(site_name)
229 self.build_path.mkdir(parents=True, exist_ok=True) 232 self.build_path.mkdir(parents=True, exist_ok=True)
230 self.dev_build_path = host.getBuildPath(site_name, dev=True) 233 self.dev_build_path = host.getBuildPath(site_name, dev=True)
231 self.dev_build_path.mkdir(parents=True, exist_ok=True) 234 self.dev_build_path.mkdir(parents=True, exist_ok=True)
232 self.putChild( 235 self.putChild(
256 C.TPL_RESOURCE, 259 C.TPL_RESOURCE,
257 self.site_name or C.SITE_NAME_DEFAULT, 260 self.site_name or C.SITE_NAME_DEFAULT,
258 C.TEMPLATE_TPL_DIR, 261 C.TEMPLATE_TPL_DIR,
259 theme) 262 theme)
260 263
261 def _initRedirections(self, options): 264 def addResourceToPath(self, path: str, resource: web_resource.Resource) -> None:
265 """Add a resource to the given path
266
267 A "NoResource" will be used for all intermediate segments
268 """
269 segments, __, last_segment = path.rpartition("/")
270 url_segments = segments.split("/") if segments else []
271 current = self
272 for segment in url_segments:
273 resource = web_resource.NoResource()
274 current.putChild(segment, resource)
275 current = resource
276
277 current.putChild(
278 last_segment.encode('utf-8'),
279 resource
280 )
281
282 async def _startApp(self, app_name, extra=None):
283 if extra is None:
284 extra = {}
285 log.info(_(
286 "starting application {app_name}").format(app_name=app_name))
287 await self.host.bridgeCall(
288 "applicationStart", app_name, data_format.serialise(extra)
289 )
290 app_data = self.sat_apps[app_name] = data_format.deserialise(
291 await self.host.bridgeCall(
292 "applicationExposedGet", app_name, "", ""))
293
294 try:
295 web_port = int(app_data['ports']['web'].split(':')[1])
296 except (KeyError, ValueError):
297 log.warning(_(
298 "no web port found for application {app_name!r}, can't use it "
299 ).format(app_name=app_name))
300 raise exceptions.DataError("no web port found")
301
302 try:
303 url_prefix = app_data['url_prefix'].strip().rstrip('/')
304 except (KeyError, AttributeError) as e:
305 log.warning(_(
306 "no URL prefix specified for this application, we can't embed it: {msg}")
307 .format(msg=e))
308 raise exceptions.DataError("no URL prefix")
309
310 if not url_prefix.startswith('/'):
311 raise exceptions.DataError(
312 f"invalid URL prefix, it must start with '/': {url_prefix!r}")
313
314 res = proxy.SatReverseProxyResource(
315 "localhost",
316 web_port,
317 url_prefix.encode()
318 )
319 self.addResourceToPath(url_prefix, res)
320
321 return app_data
322
323 async def _initRedirections(self, options):
262 url_redirections = options["url_redirections_dict"] 324 url_redirections = options["url_redirections_dict"]
263 325
264 url_redirections = url_redirections.get(self.site_name, {}) 326 url_redirections = url_redirections.get(self.site_name, {})
265 327
266 ## redirections 328 ## redirections
393 if not os.path.isabs(path): 455 if not os.path.isabs(path):
394 raise ValueError( 456 raise ValueError(
395 "file redirection must have an absolute path: e.g. " 457 "file redirection must have an absolute path: e.g. "
396 "file:/path/to/my/file") 458 "file:/path/to/my/file")
397 # for file redirection, we directly put child here 459 # for file redirection, we directly put child here
398 segments, __, last_segment = old.rpartition("/")
399 url_segments = segments.split("/") if segments else []
400 current = self
401 for segment in url_segments:
402 resource = web_resource.NoResource()
403 current.putChild(segment, resource)
404 current = resource
405 resource_class = ( 460 resource_class = (
406 ProtectedFile if new_data.get("protected", True) else static.File 461 ProtectedFile if new_data.get("protected", True) else static.File
407 ) 462 )
408 current.putChild( 463 res = resource_class(path, defaultType="application/octet-stream")
409 last_segment.encode('utf-8'), 464 self.addResourceToPath(old, res)
410 resource_class(path, defaultType="application/octet-stream")
411 )
412 log.info("[{host_name}] Added redirection from /{old} to file system " 465 log.info("[{host_name}] Added redirection from /{old} to file system "
413 "path {path}".format(host_name=self.host_name, 466 "path {path}".format(host_name=self.host_name,
414 old=old, 467 old=old,
415 path=path)) 468 path=path))
416 continue # we don't want to use redirection system, so we continue here 469
417 470 # we don't want to use redirection system, so we continue here
471 continue
472
473 elif new_url.scheme == "sat-app":
474 # a SàT application
475
476 app_name = urllib.parse.unquote(new_url.path).lower().strip()
477 extra = {"url_prefix": f"/{old}"}
478 try:
479 await self._startApp(app_name, extra)
480 except Exception as e:
481 log.warning(_(
482 "Can't launch {app_name!r} for path /{old}: {e}").format(
483 app_name=app_name, old=old, e=e))
484 continue
485
486 log.info("[{host_name}] Added redirection from /{old} to application "
487 "{app_name}".format(
488 host_name=self.host_name,
489 old=old,
490 app_name=app_name))
491
492 # normal redirection system is not used here
493 continue
418 else: 494 else:
419 raise NotImplementedError( 495 raise NotImplementedError(
420 "{scheme}: scheme is not managed for url_redirections_dict".format( 496 "{scheme}: scheme is not managed for url_redirections_dict".format(
421 scheme=new_url.scheme 497 scheme=new_url.scheme
422 ) 498 )
430 506
431 # the default root URL, if not redirected 507 # the default root URL, if not redirected
432 if not "" in self.redirections: 508 if not "" in self.redirections:
433 self.redirections[""] = self._getRequestData(C.LIBERVIA_PAGE_START) 509 self.redirections[""] = self._getRequestData(C.LIBERVIA_PAGE_START)
434 510
435 def _setMenu(self, menus): 511 async def _setMenu(self, menus):
436 menus = menus.get(self.site_name, []) 512 menus = menus.get(self.site_name, [])
437 main_menu = [] 513 main_menu = []
438 for menu in menus: 514 for menu in menus:
439 if not menu: 515 if not menu:
440 msg = _("menu item can't be empty") 516 msg = _("menu item can't be empty")
446 "menu item as list must be in the form [page_name, absolue URL]" 522 "menu item as list must be in the form [page_name, absolue URL]"
447 ) 523 )
448 log.error(msg) 524 log.error(msg)
449 raise ValueError(msg) 525 raise ValueError(msg)
450 page_name, url = menu 526 page_name, url = menu
527 elif menu.startswith("sat-app:"):
528 app_name = menu[8:].strip().lower()
529 app_data = await self._startApp(app_name)
530 front_url = app_data['front_url']
531 options = self.host.options
532 url_redirections = options["url_redirections_dict"].setdefault(
533 self.site_name, {})
534 if front_url in url_redirections:
535 raise exceptions.ConflictError(
536 f"There is already a redirection from {front_url!r}, can't add "
537 f"{app_name!r}")
538
539 url_redirections[front_url] = {
540 "page": 'embed_app',
541 "path_args": [app_name]
542 }
543
544 page_name = app_data.get('web_label', app_name).title()
545 url = front_url
546
547 log.debug(
548 f"Application {app_name} added to menu of {self.site_name}"
549 )
451 else: 550 else:
452 page_name = menu 551 page_name = menu
453 try: 552 try:
454 url = self.getPageByName(page_name).url 553 url = self.getPageByName(page_name).url
455 except KeyError as e: 554 except KeyError as e:
859 LiberviaPage.importPages(self, self.sat_root) 958 LiberviaPage.importPages(self, self.sat_root)
860 tasks_manager = TasksManager(self, self.sat_root) 959 tasks_manager = TasksManager(self, self.sat_root)
861 await tasks_manager.parseTasks() 960 await tasks_manager.parseTasks()
862 await tasks_manager.runTasks() 961 await tasks_manager.runTasks()
863 # FIXME: handle _setMenu in a more generic way, taking care of external sites 962 # FIXME: handle _setMenu in a more generic way, taking care of external sites
864 self.sat_root._setMenu(self.options["menu_json"]) 963 await self.sat_root._setMenu(self.options["menu_json"])
865 self.vhost_root.default = default_root 964 self.vhost_root.default = default_root
866 existing_vhosts = {b'': default_root} 965 existing_vhosts = {b'': default_root}
867 966
868 for host_name, site_name in self.options["vhosts_dict"].items(): 967 for host_name, site_name in self.options["vhosts_dict"].items():
869 if site_name == C.SITE_NAME_DEFAULT: 968 if site_name == C.SITE_NAME_DEFAULT:
910 # (e.g. /blog disabled except if called by external site) 1009 # (e.g. /blog disabled except if called by external site)
911 LiberviaPage.importPages(self, res, root_path=default_site_path) 1010 LiberviaPage.importPages(self, res, root_path=default_site_path)
912 tasks_manager = TasksManager(self, res) 1011 tasks_manager = TasksManager(self, res)
913 await tasks_manager.parseTasks() 1012 await tasks_manager.parseTasks()
914 await tasks_manager.runTasks() 1013 await tasks_manager.runTasks()
915 res._setMenu(self.options["menu_json"]) 1014 await res._setMenu(self.options["menu_json"])
916 1015
917 self.vhost_root.addHost(host_name.encode('utf-8'), res) 1016 self.vhost_root.addHost(host_name.encode('utf-8'), res)
918 1017
919 templates_res = web_resource.Resource() 1018 templates_res = web_resource.Resource()
920 self.putChildAll(C.TPL_RESOURCE.encode('utf-8'), templates_res) 1019 self.putChildAll(C.TPL_RESOURCE.encode('utf-8'), templates_res)
963 self.cache_resource.putChild( 1062 self.cache_resource.putChild(
964 b"common", ProtectedFile(str(self.cache_root_dir / Path("common")))) 1063 b"common", ProtectedFile(str(self.cache_root_dir / Path("common"))))
965 1064
966 # redirections 1065 # redirections
967 for root in self.roots: 1066 for root in self.roots:
968 root._initRedirections(self.options) 1067 await root._initRedirections(self.options)
969 1068
970 # no need to keep url_redirections_dict, it will not be used anymore 1069 # no need to keep url_redirections_dict, it will not be used anymore
971 del self.options["url_redirections_dict"] 1070 del self.options["url_redirections_dict"]
972 1071
973 server.Request.defaultContentType = "text/html; charset=utf-8" 1072 server.Request.defaultContentType = "text/html; charset=utf-8"