From de4d46f3be11a6ce9fbe6483ab1090184862a995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Wed, 7 Jun 2023 14:26:51 +0200 Subject: [PATCH] Load JS view assets from CDN when applicable (#1958) --- assets/js/hooks/js_view.js | 58 ++++++++++++++++++---- lib/livebook/runtime.ex | 3 +- lib/livebook_web/live/js_view_component.ex | 4 ++ 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/assets/js/hooks/js_view.js b/assets/js/hooks/js_view.js index c5fc04b53..bd4f47cae 100644 --- a/assets/js/hooks/js_view.js +++ b/assets/js/hooks/js_view.js @@ -40,8 +40,12 @@ import { initializeIframeSource } from "./js_view/iframe"; * * * `data-ref` - a unique identifier used as messages scope * - * * `data-assets-base-path` - the path to resolve all relative paths - * against in the iframe + * * `data-assets-base-path` - the base path to fetch assets from + * within the iframe (and resolve all relative paths against) + * + * * `data-assets-cdn-url` - a URL to CDN location to fetch assets + * from. Only used if specified and the entrypoint script can be + * successfully accessed, also only when Livebook runs on https * * * `data-js-path` - a relative path for the initial view-specific * JS module @@ -179,6 +183,7 @@ const JSView = { return { ref: getAttributeOrThrow(this.el, "data-ref"), assetsBasePath: getAttributeOrThrow(this.el, "data-assets-base-path"), + assetsCdnUrl: getAttributeOrDefault(this.el, "data-assets-cdn-url", null), jsPath: getAttributeOrThrow(this.el, "data-js-path"), sessionToken: getAttributeOrThrow(this.el, "data-session-token"), connectToken: getAttributeOrThrow(this.el, "data-connect-token"), @@ -334,17 +339,17 @@ const JSView = { handleChildMessage(message, onReady) { if (message.type === "ready" && !this.childReady) { - const assetsBaseUrl = window.location.origin + this.props.assetsBasePath; + this.getAssetsBaseUrl().then((assetsBaseUrl) => { + this.postMessage({ + type: "readyReply", + token: this.childToken, + baseUrl: assetsBaseUrl, + jsPath: this.props.jsPath, + }); - this.postMessage({ - type: "readyReply", - token: this.childToken, - baseUrl: assetsBaseUrl, - jsPath: this.props.jsPath, + this.childReady = true; + onReady(); }); - - this.childReady = true; - onReady(); } else { // Note: we use a random token to authorize child messages // and do our best to make this token unavailable for the @@ -386,6 +391,21 @@ const JSView = { } }, + getAssetsBaseUrl() { + // Livebook may be running behind an authentication proxy, in + // which case the internal assets URL is not accessible from + // within the iframe (served from a different origin). To + // workaround this, we fallback to a CDN for the assets if + // available for the given package. + return cachedPublicEndpointCheck().then((isPublicAccessible) => { + if (!isPublicAccessible && this.props.assetsCdnUrl) { + return this.props.assetsCdnUrl; + } else { + return window.location.origin + this.props.assetsBasePath; + } + }); + }, + postMessage(message) { this.iframe.contentWindow.postMessage(message, "*"); }, @@ -486,4 +506,20 @@ const JSView = { }, }; +/** + * Checks if Livebook public endpoint is accessible without auth cookies. + * + * Returns a promise that resolves to a boolean. The request is sent only + * once and the response is cached. + */ +function cachedPublicEndpointCheck() { + cachedPublicEndpointCheck.promise = + cachedPublicEndpointCheck.promise || + fetch("/public/health") + .then((response) => response.status === 200) + .catch((error) => false); + + return cachedPublicEndpointCheck.promise; +} + export default JSView; diff --git a/lib/livebook/runtime.ex b/lib/livebook/runtime.ex index 8c873fad6..c596b974a 100644 --- a/lib/livebook/runtime.ex +++ b/lib/livebook/runtime.ex @@ -285,7 +285,8 @@ defprotocol Livebook.Runtime do assets: %{ archive_path: String.t(), hash: String.t(), - js_path: String.t() + js_path: String.t(), + cdn_url: String.t() | nil } } diff --git a/lib/livebook_web/live/js_view_component.ex b/lib/livebook_web/live/js_view_component.ex index f2c022024..e853d3cf9 100644 --- a/lib/livebook_web/live/js_view_component.ex +++ b/lib/livebook_web/live/js_view_component.ex @@ -18,6 +18,7 @@ defmodule LivebookWeb.JSViewComponent do phx-update="ignore" data-ref={@js_view.ref} data-assets-base-path={~p"/public/sessions/#{@session_id}/assets/#{@js_view.assets.hash}/"} + data-assets-cdn-url={cdn_url(@js_view.assets[:cdn_url])} data-js-path={@js_view.assets.js_path} data-session-token={session_token(@session_id, @client_id)} data-connect-token={connect_token(@js_view.pid)} @@ -29,6 +30,9 @@ defmodule LivebookWeb.JSViewComponent do """ end + defp cdn_url(nil), do: nil + defp cdn_url(url), do: url <> "/" + defp session_token(session_id, client_id) do Phoenix.Token.sign(LivebookWeb.Endpoint, "session", %{ session_id: session_id,