From ddf0098f996150b5a169e4bfaf80b2c7bed8c185 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Tue, 14 May 2024 01:10:06 +0200 Subject: [PATCH 1/5] wip --- voila/server_extension.py | 8 ++++++-- voila/static_file_handler.py | 39 +++++++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/voila/server_extension.py b/voila/server_extension.py index 959990c05..5f76f33d9 100644 --- a/voila/server_extension.py +++ b/voila/server_extension.py @@ -14,7 +14,9 @@ from jupyter_server.base.handlers import FileFindHandler, path_regex from jupyter_server.utils import url_path_join from jupyterlab_server.themes_handler import ThemesHandler - +from jupyter_core.paths import jupyter_config_path +from jupyter_server.serverapp import ServerApp +from jupyter_core.application import JupyterApp from .tornado.contentshandler import VoilaContentsHandler from .configuration import VoilaConfiguration @@ -38,9 +40,11 @@ def _jupyter_server_extension_points(): return [{"module": "voila.server_extension"}] -def _load_jupyter_server_extension(server_app): +def _load_jupyter_server_extension(server_app: ServerApp): web_app = server_app.web_app # common configuration options between the server extension and the application + config_file_paths = [os.getcwd(), *jupyter_config_path()] + super(JupyterApp, server_app).load_config_file("voila", path=config_file_paths) voila_configuration = VoilaConfiguration(parent=server_app) template_name = voila_configuration.template template_paths = collect_template_paths(["voila", "nbconvert"], template_name) diff --git a/voila/static_file_handler.py b/voila/static_file_handler.py index 800ad1fe9..e1ac30bb5 100644 --- a/voila/static_file_handler.py +++ b/voila/static_file_handler.py @@ -11,7 +11,7 @@ import re import tornado.web - +from typing import cast from .paths import collect_static_paths @@ -111,3 +111,40 @@ def get_absolute_path(self, root, path): if denylisted: raise tornado.web.HTTPError(403, "File denylisted") return super().get_absolute_path(root, path) + + @property + def content_security_policy(self) -> str: + """The default Content-Security-Policy header + + Can be overridden by defining Content-Security-Policy in settings['headers'] + """ + if "Content-Security-Policy" in self.settings.get("headers", {}): + # user-specified, don't override + return cast(str, self.settings["headers"]["Content-Security-Policy"]) + + return "; ".join( + [ + "frame-ancestors 'self'", + "sandbox allow-scripts", + ] + ) + + def set_default_headers(self) -> None: + """Set the default headers.""" + headers = {} + headers["X-Content-Type-Options"] = "nosniff" + headers.update(self.settings.get("headers", {})) + + headers["Content-Security-Policy"] = self.content_security_policy + + # Allow for overriding headers + for header_name, value in headers.items(): + try: + self.set_header(header_name, value) + except Exception as e: + # tornado raise Exception (not a subclass) + # if method is unsupported (websocket and Access-Control-Allow-Origin + # for example, so just ignore) + self.log.exception( # type:ignore[attr-defined] + "Could not set default headers: %s", e + ) From eeee2996ff2f5df0224d329e5f43394a82e95dec Mon Sep 17 00:00:00 2001 From: Duc Trung LE Date: Tue, 14 May 2024 18:37:39 +0200 Subject: [PATCH 2/5] allow disable iframe sandbox mode --- packages/jupyterlab-preview/src/preview.tsx | 65 ++++++++++++++------- voila/static_file_handler.py | 39 +------------ 2 files changed, 44 insertions(+), 60 deletions(-) diff --git a/packages/jupyterlab-preview/src/preview.tsx b/packages/jupyterlab-preview/src/preview.tsx index 1142549e3..46d6ca0e8 100644 --- a/packages/jupyterlab-preview/src/preview.tsx +++ b/packages/jupyterlab-preview/src/preview.tsx @@ -1,24 +1,18 @@ -import { - IFrame, - ToolbarButton, - ReactWidget, - IWidgetTracker -} from '@jupyterlab/apputils'; - +import { IWidgetTracker } from '@jupyterlab/apputils'; import { ABCWidgetFactory, DocumentRegistry, DocumentWidget } from '@jupyterlab/docregistry'; - import { INotebookModel } from '@jupyterlab/notebook'; - -import { refreshIcon } from '@jupyterlab/ui-components'; - +import { + IFrame, + ReactWidget, + refreshIcon, + ToolbarButton +} from '@jupyterlab/ui-components'; import { Token } from '@lumino/coreutils'; - import { Signal } from '@lumino/signaling'; - import * as React from 'react'; import { voilaIcon } from './icons'; @@ -35,6 +29,12 @@ export const IVoilaPreviewTracker = new Token( '@voila-dashboards/jupyterlab-preview:IVoilaPreviewTracker' ); +const IFRAME_SANDBOX: IFrame.SandboxExceptions[] = [ + 'allow-same-origin', + 'allow-scripts', + 'allow-downloads', + 'allow-modals' +]; /** * A DocumentWidget that shows a VoilĂ  preview in an IFrame. */ @@ -47,16 +47,9 @@ export class VoilaPreview extends DocumentWidget { super({ ...options, content: new IFrame({ - sandbox: [ - 'allow-same-origin', - 'allow-scripts', - 'allow-downloads', - 'allow-modals', - 'allow-popups' - ] + sandbox: IFRAME_SANDBOX }) }); - window.onmessage = (event: any) => { //console.log("EVENT: ", event); const level = event?.data?.level; @@ -93,7 +86,7 @@ export class VoilaPreview extends DocumentWidget { this.content.title.icon = voilaIcon; this._renderOnSave = renderOnSave ?? false; - + this._disableSandbox = false; context.pathChanged.connect(() => { this.content.url = getVoilaUrl(context.path); }); @@ -125,6 +118,32 @@ export class VoilaPreview extends DocumentWidget { ); + const disableIFrameSandbox = ReactWidget.create( + + ); + this.toolbar.addItem('reload', reloadButton); if (context) { @@ -137,6 +156,7 @@ export class VoilaPreview extends DocumentWidget { }); }); } + this.toolbar.addItem('disableIFrameSandbox', disableIFrameSandbox); } /** @@ -175,6 +195,7 @@ export class VoilaPreview extends DocumentWidget { } private _renderOnSave: boolean; + private _disableSandbox: boolean; } /** diff --git a/voila/static_file_handler.py b/voila/static_file_handler.py index e1ac30bb5..800ad1fe9 100644 --- a/voila/static_file_handler.py +++ b/voila/static_file_handler.py @@ -11,7 +11,7 @@ import re import tornado.web -from typing import cast + from .paths import collect_static_paths @@ -111,40 +111,3 @@ def get_absolute_path(self, root, path): if denylisted: raise tornado.web.HTTPError(403, "File denylisted") return super().get_absolute_path(root, path) - - @property - def content_security_policy(self) -> str: - """The default Content-Security-Policy header - - Can be overridden by defining Content-Security-Policy in settings['headers'] - """ - if "Content-Security-Policy" in self.settings.get("headers", {}): - # user-specified, don't override - return cast(str, self.settings["headers"]["Content-Security-Policy"]) - - return "; ".join( - [ - "frame-ancestors 'self'", - "sandbox allow-scripts", - ] - ) - - def set_default_headers(self) -> None: - """Set the default headers.""" - headers = {} - headers["X-Content-Type-Options"] = "nosniff" - headers.update(self.settings.get("headers", {})) - - headers["Content-Security-Policy"] = self.content_security_policy - - # Allow for overriding headers - for header_name, value in headers.items(): - try: - self.set_header(header_name, value) - except Exception as e: - # tornado raise Exception (not a subclass) - # if method is unsupported (websocket and Access-Control-Allow-Origin - # for example, so just ignore) - self.log.exception( # type:ignore[attr-defined] - "Could not set default headers: %s", e - ) From a3f606e74bf08b1d274e0fcdb7cb653032f53f72 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Tue, 14 May 2024 23:21:03 +0200 Subject: [PATCH 3/5] Update config loading logic --- voila/server_extension.py | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/voila/server_extension.py b/voila/server_extension.py index 5f76f33d9..6a5a99b47 100644 --- a/voila/server_extension.py +++ b/voila/server_extension.py @@ -16,9 +16,13 @@ from jupyterlab_server.themes_handler import ThemesHandler from jupyter_core.paths import jupyter_config_path from jupyter_server.serverapp import ServerApp -from jupyter_core.application import JupyterApp from .tornado.contentshandler import VoilaContentsHandler - +from traitlets.config import ( + JSONFileConfigLoader, + PyFileConfigLoader, + Config, + ConfigFileNotFound, +) from .configuration import VoilaConfiguration from .tornado.handler import TornadoVoilaHandler from .paths import ROOT, collect_static_paths, collect_template_paths, jupyter_path @@ -40,12 +44,44 @@ def _jupyter_server_extension_points(): return [{"module": "voila.server_extension"}] +def load_config_file() -> Config: + """ + Loads voila.json and voila.py config file from current working + directory and other Jupyter paths + """ + + new_config = Config() + base_file_name = "voila" + config_file_paths = [*jupyter_config_path(), os.getcwd()] + + for current in config_file_paths: + py_loader = PyFileConfigLoader(filename=f"{base_file_name}.py", path=current) + try: + py_config = py_loader.load_config() + new_config.merge(py_config) + except ConfigFileNotFound: + pass + + json_loader = JSONFileConfigLoader( + filename=f"{base_file_name}.json", path=current + ) + try: + json_config = json_loader.load_config() + new_config.merge(json_config) + except ConfigFileNotFound: + pass + + return new_config + + def _load_jupyter_server_extension(server_app: ServerApp): web_app = server_app.web_app # common configuration options between the server extension and the application - config_file_paths = [os.getcwd(), *jupyter_config_path()] - super(JupyterApp, server_app).load_config_file("voila", path=config_file_paths) - voila_configuration = VoilaConfiguration(parent=server_app) + + voila_configuration = VoilaConfiguration( + parent=server_app, config=load_config_file() + ) + template_name = voila_configuration.template template_paths = collect_template_paths(["voila", "nbconvert"], template_name) static_paths = collect_static_paths(["voila", "nbconvert"], template_name) From c2f75603fbd41b5faa7eb7b4df8febe0f24b41a7 Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Thu, 16 May 2024 09:41:15 +0200 Subject: [PATCH 4/5] Remove sandbox switch button --- packages/jupyterlab-preview/src/preview.tsx | 65 +++++++-------------- 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/packages/jupyterlab-preview/src/preview.tsx b/packages/jupyterlab-preview/src/preview.tsx index 46d6ca0e8..1142549e3 100644 --- a/packages/jupyterlab-preview/src/preview.tsx +++ b/packages/jupyterlab-preview/src/preview.tsx @@ -1,18 +1,24 @@ -import { IWidgetTracker } from '@jupyterlab/apputils'; +import { + IFrame, + ToolbarButton, + ReactWidget, + IWidgetTracker +} from '@jupyterlab/apputils'; + import { ABCWidgetFactory, DocumentRegistry, DocumentWidget } from '@jupyterlab/docregistry'; + import { INotebookModel } from '@jupyterlab/notebook'; -import { - IFrame, - ReactWidget, - refreshIcon, - ToolbarButton -} from '@jupyterlab/ui-components'; + +import { refreshIcon } from '@jupyterlab/ui-components'; + import { Token } from '@lumino/coreutils'; + import { Signal } from '@lumino/signaling'; + import * as React from 'react'; import { voilaIcon } from './icons'; @@ -29,12 +35,6 @@ export const IVoilaPreviewTracker = new Token( '@voila-dashboards/jupyterlab-preview:IVoilaPreviewTracker' ); -const IFRAME_SANDBOX: IFrame.SandboxExceptions[] = [ - 'allow-same-origin', - 'allow-scripts', - 'allow-downloads', - 'allow-modals' -]; /** * A DocumentWidget that shows a VoilĂ  preview in an IFrame. */ @@ -47,9 +47,16 @@ export class VoilaPreview extends DocumentWidget { super({ ...options, content: new IFrame({ - sandbox: IFRAME_SANDBOX + sandbox: [ + 'allow-same-origin', + 'allow-scripts', + 'allow-downloads', + 'allow-modals', + 'allow-popups' + ] }) }); + window.onmessage = (event: any) => { //console.log("EVENT: ", event); const level = event?.data?.level; @@ -86,7 +93,7 @@ export class VoilaPreview extends DocumentWidget { this.content.title.icon = voilaIcon; this._renderOnSave = renderOnSave ?? false; - this._disableSandbox = false; + context.pathChanged.connect(() => { this.content.url = getVoilaUrl(context.path); }); @@ -118,32 +125,6 @@ export class VoilaPreview extends DocumentWidget { ); - const disableIFrameSandbox = ReactWidget.create( - - ); - this.toolbar.addItem('reload', reloadButton); if (context) { @@ -156,7 +137,6 @@ export class VoilaPreview extends DocumentWidget { }); }); } - this.toolbar.addItem('disableIFrameSandbox', disableIFrameSandbox); } /** @@ -195,7 +175,6 @@ export class VoilaPreview extends DocumentWidget { } private _renderOnSave: boolean; - private _disableSandbox: boolean; } /** From 189b48eed7b5e7e595d0b81112f28557e4db34ce Mon Sep 17 00:00:00 2001 From: Duc Trung Le Date: Wed, 22 May 2024 15:13:51 +0200 Subject: [PATCH 5/5] Remove sandbox option from voila preview iframe --- packages/jupyterlab-preview/src/preview.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/jupyterlab-preview/src/preview.tsx b/packages/jupyterlab-preview/src/preview.tsx index 1142549e3..f55cab4e5 100644 --- a/packages/jupyterlab-preview/src/preview.tsx +++ b/packages/jupyterlab-preview/src/preview.tsx @@ -46,17 +46,14 @@ export class VoilaPreview extends DocumentWidget { constructor(options: VoilaPreview.IOptions) { super({ ...options, - content: new IFrame({ - sandbox: [ - 'allow-same-origin', - 'allow-scripts', - 'allow-downloads', - 'allow-modals', - 'allow-popups' - ] - }) + content: new IFrame() }); + const iframe = this.content.node.querySelector('iframe'); + if (iframe) { + iframe.removeAttribute('sandbox'); + } + window.onmessage = (event: any) => { //console.log("EVENT: ", event); const level = event?.data?.level;