livebook/assets/js/hooks/js_view/iframe.js
2023-09-26 21:54:03 +07:00

72 lines
3 KiB
JavaScript

import { sha256Base64 } from "../../lib/utils";
// Loading iframe using `srcdoc` disables cookies and browser APIs,
// such as camera and microphone (1), the same applies to `src` with
// data URL, so we need to load the iframe through a regular request.
// Since the iframe is sandboxed we also need `allow-same-origin`.
// Additionally, we cannot load the iframe from the same origin as
// the app, because using `allow-same-origin` together with `allow-scripts`
// would be insecure (2). Consequently, we need to load the iframe
// from a different origin.
//
// When running Livebook on https:// we load the iframe from another
// https:// origin. On the other hand, when running on http:// we want
// to load the iframe from http:// as well, otherwise the browser could
// block asset requests from the https:// iframe to http:// Livebook.
// However, external http:// content is not considered a secure context (3),
// which implies no access to user media. Therefore, instead of using
// http://livebookusercontent.com we use another localhost endpoint. Note that
// this endpoint has a different port than the Livebook web app, that's
// because we need separate origins, as outlined above.
//
// To ensure integrity of the loaded content we manually verify the
// checksum against the expected value.
//
// (1): https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#document_source_security
// (2): https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
// (3): https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
const IFRAME_SHA256 = "wcqj5QWCo66osdAWDnEgPRFyL7nfe8oNqNggnw4vvW8=";
export function initializeIframeSource(iframe, iframePort, iframeUrl) {
const url = getIframeUrl(iframePort, iframeUrl);
return verifyIframeSource(url).then(() => {
iframe.sandbox =
"allow-scripts allow-same-origin allow-downloads allow-forms allow-modals allow-popups allow-top-navigation";
iframe.allow =
"accelerometer; ambient-light-sensor; camera; display-capture; encrypted-media; fullscreen; geolocation; gyroscope; microphone; midi; usb; xr-spatial-tracking; clipboard-read; clipboard-write";
iframe.src = url;
});
}
function getIframeUrl(iframePort, iframeUrl) {
const protocol = window.location.protocol;
if (iframeUrl) {
return iframeUrl.replace(/^https?:/, protocol);
}
return protocol === "https:"
? "https://livebookusercontent.com/iframe/v5.html"
: `http://${window.location.hostname}:${iframePort}/iframe/v5.html`;
}
let iframeVerificationPromise = null;
function verifyIframeSource(iframeUrl) {
if (!iframeVerificationPromise) {
iframeVerificationPromise = fetch(iframeUrl)
.then((response) => response.text())
.then((html) => {
if (sha256Base64(html) !== IFRAME_SHA256) {
throw new Error(
`The iframe loaded from ${iframeUrl} doesn't have the expected checksum ${IFRAME_SHA256}`
);
}
});
}
return iframeVerificationPromise;
}