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