Restrict new windows in replay + improved service worker loading + better error messages (#126)

* embeds:
- support target rewriting with named iframe as per webrecorder/wombat#94
- refactor sw init: detect additional errors, such as CORS issues in nested iframes
- support cross-origin replaybase for embeds
- add 'requirecrossoriginiframe' to restrict embed to only be loaded in subdomain iframes, otherwise showing
- rename noSandbox -> sandbox, not enabled by default

init: set appName on service worker init
docs:
- add example embed to embedding doc
- update docs for noSandbox -> sandbox
- add warning if deprecated 'noSandbox' flag is used, explaining that it is now deprecated
- update embedding and CHANGES.md with latest features
- add tweet-example.wacz
- add embed.html to test embeds

bump to 1.7.0-beta.1
This commit is contained in:
Ilya Kreymer
2022-10-03 20:04:35 -07:00
committed by GitHub
parent 764e1c9e21
commit 43122425e5
15 changed files with 341 additions and 208 deletions
+10 -6
View File
@@ -1,11 +1,10 @@
import { LitElement, html, css } from "lit";
import { wrapCss, rwpLogo, IS_APP, VERSION, clickOnSpacebarPress } from "./misc";
import { registerSW } from "./pageutils";
import fasHelp from "@fortawesome/fontawesome-free/svgs/solid/question-circle.svg";
import fasArrowLeft from "@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg";
import fasArrowRight from "@fortawesome/fontawesome-free/svgs/solid/arrow-right.svg";
import { SWManager } from "./swmanager";
// ===========================================================================
@@ -28,9 +27,8 @@ class ReplayWebApp extends LitElement
this.loadInfo = null;
if (swName) {
registerSW(swName);
}
this.swmanager = new SWManager({name: swName, appName: this.appName});
this.swmanager.register().catch(() => this.swErrorMsg = this.swmanager.renderErrorReport(this.mainLogo));
this.safariKeyframes();
}
@@ -56,7 +54,9 @@ class ReplayWebApp extends LitElement
collPageUrl: { type: String },
pageTitle: { type: String },
pageReplay: { type: Boolean },
source: { type: String }
source: { type: String },
swErrorMsg: { type: Object },
};
}
@@ -248,6 +248,10 @@ class ReplayWebApp extends LitElement
return html``;
}
if (this.embed && this.swErrorMsg) {
return this.swErrorMsg;
}
return html`
${!this.embed || this.embed === "full" ? this.renderNavBar() : ""}
+41 -19
View File
@@ -1,9 +1,8 @@
import { LitElement, html, css } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { registerSW } from "./pageutils";
import { wrapCss, rwpLogo } from "./misc";
import { SWManager } from "./swmanager";
var scriptSrc = document.currentScript && document.currentScript.src;
@@ -23,6 +22,7 @@ class Embed extends LitElement
// eslint-disable-next-line no-undef
this.swName = __SW_NAME__;
this.mainElementName = "replay-app-main";
this.appName = "ReplayWeb.page";
this.view = "replay";
this.ts = "";
this.url = "";
@@ -33,12 +33,14 @@ class Embed extends LitElement
this.paramString = null;
this.deepLink = false;
this.newWindowBase = "";
this.swInited = false;
this.inited = false;
this.embed = null;
this.reloadCount = 0;
this.noSandbox = false;
this.sandbox = false;
this.noWebWorker = false;
this.noCache = false;
// deprecated;
this.noSandbox = null;
this.logo = rwpLogo;
}
@@ -66,12 +68,13 @@ class Embed extends LitElement
coll: { type: String },
config: { type: String },
swInited: { type: Boolean },
inited: { type: Boolean },
paramString: { type: String },
hashString: { type: String },
deepLink: { type: Boolean },
sandbox: { type: Boolean },
noSandbox: { type: Boolean },
noWebWorker: { type: Boolean },
noCache: { type: Boolean },
@@ -79,16 +82,34 @@ class Embed extends LitElement
newWindowBase: { type: String },
errorMessage: { type: String }
errorMessage: { type: String },
requireSubdomainIframe: {type: Boolean}
};
}
async doRegister() {
const replaybaseURL = new URL(this.replaybase, window.location.href);
this.isCrossOrigin = replaybaseURL.origin !== window.location.origin;
if (this.isCrossOrigin) {
this.inited = true;
return;
}
const name = this.swName;
const appName = this.appName;
const scope = this.replaybase;
const requireSubdomainIframe = this.requireSubdomainIframe;
this.swmanager = new SWManager({name, scope, requireSubdomainIframe, appName});
try {
await registerSW(this.swName, this.replaybase);
this.swInited = true;
await this.swmanager.register();
this.inited = true;
} catch (e) {
this.errorMessage = e;
this.errorMessage = this.swmanager.renderErrorReport(this.logo);
}
}
@@ -116,6 +137,9 @@ class Embed extends LitElement
}
firstUpdated() {
if (this.noSandbox) {
console.warn("The noSandbox flag is deprecated. ReplayWeb.page does not add a sandbox by default. To enable sandboxing, use 'sandbox' flag instead. This may result in PDFs not loading and pages opening in new windows, but may be more secure in some situations");
}
this.doRegister();
window.addEventListener("message", (event) => this.handleMessage(event));
@@ -257,8 +281,8 @@ class Embed extends LitElement
render() {
return html`
${this.paramString && this.hashString && this.swInited ? html`
<iframe sandbox="${ifDefined(!this.noSandbox ?
${this.paramString && this.hashString && this.inited ? html`
<iframe sandbox="${ifDefined(this.sandbox ?
"allow-downloads allow-modals allow-orientation-lock allow-pointer-lock\
allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts\
allow-same-origin allow-forms" : undefined)}"
@@ -268,17 +292,15 @@ class Embed extends LitElement
` : html``}
${this.errorMessage ? html`
<section class="full-width">
<div class="has-text-centered">
<fa-icon class="logo" id="wrlogo" size="2.5rem" .svg=${this.logo} aria-hidden="true"></fa-icon>
</div>
<div class="error">${this.errorMessage}</div>
</section>
`: ""}`;
${this.errorMessage}
`;
}
onLoad(event) {
if (this.isCrossOrigin) {
return;
}
const win = event.target.contentWindow;
const doc = event.target.contentDocument;
+4 -4
View File
@@ -3,7 +3,7 @@ import { wrapCss, IS_APP } from "./misc";
import prettyBytes from "pretty-bytes";
import { getSWErrorMsg, parseURLSchemeHostPath } from "./pageutils";
import { parseURLSchemeHostPath } from "./pageutils";
// ===========================================================================
@@ -125,11 +125,11 @@ class Loader extends LitElement
this.percent = this.currentSize = this.totalSize = 0;
const noSWError = getSWErrorMsg();
// const noSWError = getSWErrorMsg();
if (noSWError) {
if (this.loadInfo && this.loadInfo.swError) {
this.state = "errored";
this.error = noSWError;
this.error = this.loadInfo.swError;
this.errorAllowRetry = false;
return;
}
+1 -58
View File
@@ -1,59 +1,3 @@
import { register } from "register-service-worker";
let appName = "ReplayWeb.page";
// ===========================================================================
function setAppName(newAppName) {
appName = newAppName;
}
// ===========================================================================
function registerSW(name = "sw.js", scope = "./") {
let resolve, reject;
const p = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
const errMsg = getSWErrorMsg();
if (errMsg) {
console.error(errMsg);
reject(errMsg);
}
register(scope + name, {
registrationOptions: { scope },
registered () {
console.log("Service worker is registered");
resolve();
},
error (error) {
console.error("Error during service worker registration:", error);
reject(`${appName} could not be loaded due to the following error:\n${error.toString()}`);
}
});
return p;
}
function getSWErrorMsg() {
if (navigator.serviceWorker) {
return null;
}
if (window.location.protocol === "http:") {
return `\
Sorry, the ${appName} system must be loaded from an HTTPS URL, but was loaded from: ${window.location.host}.
Please try loading this page from an HTTPS URL`;
} else {
return `Sorry, ${appName} won't work in this browser as Service Workers are not supported.
Please try a different browser.
(Service Workers are disabled in Firefox in Private Mode. If Using Private Mode in Firefox, try regular mode)`;
}
}
// ===========================================================================
async function digestMessage(message, hashtype) {
const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
@@ -155,5 +99,4 @@ function parseURLSchemeHostPath(url) {
}
export { digestMessage, tsToDate, getTS, getPageDateTS, getReplayLink, sourceToId, parseURLSchemeHostPath,
registerSW, getSWErrorMsg, setAppName };
export { digestMessage, tsToDate, getTS, getPageDateTS, getReplayLink, sourceToId, parseURLSchemeHostPath };
+1 -1
View File
@@ -298,7 +298,7 @@ class Replay extends LitElement
</div>` : html`
<div class="iframe-container">
<iframe class="iframe-main" @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
<iframe class="iframe-main" name="___wb_replay_top_frame" @message="${this.onReplayMessage}" allow="autoplay 'self'; fullscreen" allowfullscreen
src="${this.iframeUrl}" title="${title}"></iframe>
${this.showAuth ? html`
+132
View File
@@ -0,0 +1,132 @@
import { html } from "lit";
import { register } from "register-service-worker";
// ===========================================================================
export class SWManager
{
constructor({name = "sw.js", scope = "./", appName = "ReplayWeb.page", requireSubdomainIframe = false} = {}) {
this.name = name;
this.scope = scope;
this.appName = appName;
this.requireSubdomainIframe = requireSubdomainIframe;
this.errorMsg = null;
}
setAppName(newAppName) {
this.appName = newAppName;
}
register() {
let resolve, reject;
const p = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
this.errorMsg = this.getSWErrorMsg();
if (this.errorMsg) {
console.error(this.errorMsg);
reject(this.errorMsg);
}
const handleError = (error) => {
console.error("Error during service worker registration:", error);
this.errorMsg = this.getCrossOriginIframeMsg();
if (!this.errorMsg) {
this.errorMsg = `${this.appName} could not be loaded due to the following error:\n${error.toString()}`;
}
reject(this.errorMsg);
};
register(this.scope + this.name, {
registrationOptions: { scope: this.scope },
registered () {
console.log("Service worker is registered");
resolve();
},
error(errMsg) { handleError(errMsg); }
});
return p;
}
getCrossOriginIframeMsg() {
if (this.isCrossOriginIframe()) {
return `\
Sorry, Service Workers can not be used in cross-origin iframes.
This web archive embed is loaded from ${window.location.origin} but the top page is on a different origin.
The embed must be loaded from the same origin or a subdomain.`;
}
return null;
}
isCrossOriginIframe() {
if (window.parent === window) {
return false;
}
try {
return window.top.location.href === "";
} catch (e) {
return true;
}
}
getSWErrorMsg() {
if (navigator.serviceWorker) {
// must be loaded from a cross-origin (eg. subdomain)
if (this.requireSubdomainIframe && !this.isCrossOriginIframe()) {
return `Sorry, due to security settings, this ${this.appName} embed only be viewed within a subdomain iframe.`;
}
return null;
}
// loaded from a cross-origin iframe that's *not* a subdomain
const msg = this.getCrossOriginIframeMsg();
if (msg) {
return msg;
}
if (!window.isSecureContext) {
return `
Sorry, the ${this.appName} system must be loaded from an HTTPS URL (or localhost), but was loaded from: ${window.location.host}.
Please try loading this page from an HTTPS URL`;
}
// Detect firefox private mode
if ("MozAppearance" in document.documentElement.style) {
return "Sorry, Service Workers are disabled in Firefox in Private Mode. Please try loading this page in regular mode instead.";
}
return `Sorry, ${this.appName} won't work in this browser as Service Workers are not supported in this window.
Please try a different browser.`;
}
renderErrorReport(logo, override) {
const msg = this.errorMsg || override;
if (!msg) {
return "";
}
return html`
<section class="is-fullwidth">
<div class="has-text-centered">
<fa-icon style="margin: 1em;flex-grow: 1;" id="wrlogo" size="2.5rem" .svg=${logo} aria-hidden="true"></fa-icon>
</div>
<div style="white-space: pre-wrap; text-align: center" >${msg}</div>
</section>
`;
}
}