From aed7651e48c4ecc689b9f25b7333dcf3a3f027a7 Mon Sep 17 00:00:00 2001 From: Tamotsu Takahashi Date: Mon, 4 Dec 2023 12:08:28 +0900 Subject: [PATCH 1/5] Avoid infinite loops using sessionStorage https://github.com/gzuidhof/coi-serviceworker/issues/24 coi.shouldRegister() can use firstTime --- coi-serviceworker.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coi-serviceworker.js b/coi-serviceworker.js index af2caf1..6f1624d 100644 --- a/coi-serviceworker.js +++ b/coi-serviceworker.js @@ -60,9 +60,12 @@ if (typeof window === 'undefined') { } else { (() => { + const firstTime = !window.sessionStorage.getItem("coiReloadedBySelf"); + window.sessionStorage.removeItem("coiReloadedBySelf"); + // You can customize the behavior of this script through a global `coi` variable. const coi = { - shouldRegister: () => true, + shouldRegister: () => firstTime, shouldDeregister: () => false, coepCredentialless: () => (window.chrome !== undefined || window.netscape !== undefined), doReload: () => window.location.reload(), @@ -97,6 +100,7 @@ if (typeof window === 'undefined') { n.serviceWorker.register(window.document.currentScript.src).then( (registration) => { !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope); + window.sessionStorage.setItem("coiReloadedBySelf", "true"); registration.addEventListener("updatefound", () => { !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker."); From 00025289ae2948fad5ab0f6afb63db5e1c908810 Mon Sep 17 00:00:00 2001 From: Tamotsu Takahashi Date: Mon, 4 Dec 2023 14:11:46 +0900 Subject: [PATCH 2/5] Degrade COEP Implement coi.coepDegrade() to retry without credentialless. Record coiCoepFailed in sessionStorage. Avoid degrading-loops by sessionStorage.setItem("coiDegrading"). --- coi-serviceworker.js | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/coi-serviceworker.js b/coi-serviceworker.js index 6f1624d..9989721 100644 --- a/coi-serviceworker.js +++ b/coi-serviceworker.js @@ -67,19 +67,42 @@ if (typeof window === 'undefined') { const coi = { shouldRegister: () => firstTime, shouldDeregister: () => false, - coepCredentialless: () => (window.chrome !== undefined || window.netscape !== undefined), + coepCredentialless: () => true, + coepDegrade: () => true, doReload: () => window.location.reload(), quiet: false, ...window.coi }; const n = navigator; + const controlling = n.serviceWorker && n.serviceWorker.controller; - if (n.serviceWorker && n.serviceWorker.controller) { + // Record the failure if the page is served by serviceWorker. + if (controlling && !window.crossOriginIsolated) { + window.sessionStorage.setItem("coiCoepHasFailed", "true"); + } + const coepHasFailed = window.sessionStorage.getItem("coiCoepHasFailed"); + + // See if it is in degradation process using a volatile storage item. + const coepDegrading = window.sessionStorage.getItem("coiCoepDegrading"); + window.sessionStorage.removeItem("coiCoepDegrading"); + + if (controlling) { + // Reload only on the first failure. + const reloadToDegrade = coi.coepDegrade() && !( + coepDegrading || window.crossOriginIsolated + ); n.serviceWorker.controller.postMessage({ type: "coepCredentialless", - value: coi.coepCredentialless(), + value: (reloadToDegrade || coepHasFailed && coi.coepDegrade()) + ? false + : coi.coepCredentialless(), }); + if (reloadToDegrade) { + window.sessionStorage.setItem("coiCoepDegrading", "true"); + !coi.quiet && console.log("Reloading page to degrade COEP."); + coi.doReload("coepdegrade"); + } if (coi.shouldDeregister()) { n.serviceWorker.controller.postMessage({ type: "deregister" }); From c9832f164fa79cd2a348bb2b8ca1d5fe049f9007 Mon Sep 17 00:00:00 2001 From: Tamotsu Takahashi Date: Sun, 3 Dec 2023 20:35:56 +0900 Subject: [PATCH 3/5] README: Add coepDegrade and firstTime Now we can retry with require-corp after credentialless fails. Also, we can use firstTime to get rid of register-loops. --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 034031b..126f936 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,17 @@ You can customize the behavior by defining a variable `coi` in the global scope ```javascript window.coi = { - // // A function that is run to decide whether to register the SW or not. + // A function that is run to decide whether to register the SW or not. // You could for instance make this return a value based on whether you actually need to be cross origin isolated or not. - shouldRegister: () => true, + // The variable "firstTime" is false only if it has been reloaded by itself. + shouldRegister: () => firstTime, // If this function returns true, any existing service worker will be deregistered (and nothing else will happen). shouldDeregister: () => false, // A function that is run to decide whether to use "Cross-Origin-Embedder-Policy: credentialless" or not. // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy#browser_compatibility - coepCredentialless: () => !(window.chrome || window.netscape), + coepCredentialless: () => true, + // A function to decide whether to retry with require-corp if credentialless fails. + coepDegrade: () => true, // Override this if you want to prompt the user and do reload at your own leisure. Maybe show the user a message saying: // "Click OK to refresh the page to enable <...>" doReload: () => window.location.reload(), From 0a06be08164bae9b37fd88e60420d355ab2e6170 Mon Sep 17 00:00:00 2001 From: Tamotsu Takahashi Date: Mon, 4 Dec 2023 13:53:44 +0900 Subject: [PATCH 4/5] Minify --- coi-serviceworker.min.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coi-serviceworker.min.js b/coi-serviceworker.min.js index e79fce1..10dda4e 100644 --- a/coi-serviceworker.min.js +++ b/coi-serviceworker.min.js @@ -1,2 +1,2 @@ /*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ -let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const r=e.request;if("only-if-cached"===r.cache&&"same-origin"!==r.mode)return;const s=coepCredentialless&&"no-cors"===r.mode?new Request(r,{credentials:"omit"}):r;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const r=new Headers(e.headers);return r.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||r.set("Cross-Origin-Resource-Policy","cross-origin"),r.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:r})})).catch((e=>console.error(e))))}))):(()=>{const e={shouldRegister:()=>!0,shouldDeregister:()=>!1,coepCredentialless:()=>(window.chrome!==undefined||window.netscape!==undefined),doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator;r.serviceWorker&&r.serviceWorker.controller&&(r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:e.coepCredentialless()}),e.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})),!1===window.crossOriginIsolated&&e.shouldRegister()&&(window.isSecureContext?r.serviceWorker&&r.serviceWorker.register(window.document.currentScript.src).then((s=>{!e.quiet&&console.log("COOP/COEP Service Worker registered",s.scope),s.addEventListener("updatefound",(()=>{!e.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),e.doReload()})),s.active&&!r.serviceWorker.controller&&(!e.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),e.doReload())}),(r=>{!e.quiet&&console.error("COOP/COEP Service Worker failed to register:",r)})):!e.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})(); +let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=!window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o={shouldRegister:()=>e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},s=navigator,r=s.serviceWorker&&s.serviceWorker.controller;r&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const t=window.sessionStorage.getItem("coiCoepHasFailed"),i=window.sessionStorage.getItem("coiCoepDegrading");if(window.sessionStorage.removeItem("coiCoepDegrading"),r){const e=o.coepDegrade()&&!(i||window.crossOriginIsolated);s.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||t&&o.coepDegrade())&&o.coepCredentialless()}),e&&(window.sessionStorage.setItem("coiCoepDegrading","true"),!o.quiet&&console.log("Reloading page to degrade COEP."),o.doReload("coepdegrade")),o.shouldDeregister()&&s.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&o.shouldRegister()&&(window.isSecureContext?s.serviceWorker&&s.serviceWorker.register(window.document.currentScript.src).then((e=>{!o.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),window.sessionStorage.setItem("coiReloadedBySelf","true"),e.addEventListener("updatefound",(()=>{!o.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),o.doReload()})),e.active&&!s.serviceWorker.controller&&(!o.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),o.doReload())}),(e=>{!o.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!o.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})(); \ No newline at end of file From 5c727fe61dfdba39b2cd13de60029eedab44a132 Mon Sep 17 00:00:00 2001 From: Tamotsu Takahashi Date: Mon, 4 Dec 2023 23:42:58 +0900 Subject: [PATCH 5/5] Store the reason to reload Rename firstTime to !reloadedBySelf. coepDegrading and reloadedBySelf share the same storage item. Document that coi.doReload() can use the storage item to see the reason to reload. --- README.md | 5 +++-- coi-serviceworker.js | 14 ++++++-------- coi-serviceworker.min.js | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 126f936..11b5eaa 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ You can customize the behavior by defining a variable `coi` in the global scope window.coi = { // A function that is run to decide whether to register the SW or not. // You could for instance make this return a value based on whether you actually need to be cross origin isolated or not. - // The variable "firstTime" is false only if it has been reloaded by itself. - shouldRegister: () => firstTime, + // Using "!reloadedBySelf" you can avoid infinite loops of reloading. + shouldRegister: () => !reloadedBySelf, // If this function returns true, any existing service worker will be deregistered (and nothing else will happen). shouldDeregister: () => false, // A function that is run to decide whether to use "Cross-Origin-Embedder-Policy: credentialless" or not. @@ -52,6 +52,7 @@ window.coi = { coepDegrade: () => true, // Override this if you want to prompt the user and do reload at your own leisure. Maybe show the user a message saying: // "Click OK to refresh the page to enable <...>" + // You can see window.sessionStorage.getItem("coiReloadedBySelf") for the reason to reload. doReload: () => window.location.reload(), // Set to true if you don't want coi to log anything to the console. quiet: false diff --git a/coi-serviceworker.js b/coi-serviceworker.js index 9989721..6bf07b4 100644 --- a/coi-serviceworker.js +++ b/coi-serviceworker.js @@ -60,12 +60,13 @@ if (typeof window === 'undefined') { } else { (() => { - const firstTime = !window.sessionStorage.getItem("coiReloadedBySelf"); + const reloadedBySelf = window.sessionStorage.getItem("coiReloadedBySelf"); window.sessionStorage.removeItem("coiReloadedBySelf"); + const coepDegrading = (reloadedBySelf == "coepdegrade"); // You can customize the behavior of this script through a global `coi` variable. const coi = { - shouldRegister: () => firstTime, + shouldRegister: () => !reloadedBySelf, shouldDeregister: () => false, coepCredentialless: () => true, coepDegrade: () => true, @@ -83,10 +84,6 @@ if (typeof window === 'undefined') { } const coepHasFailed = window.sessionStorage.getItem("coiCoepHasFailed"); - // See if it is in degradation process using a volatile storage item. - const coepDegrading = window.sessionStorage.getItem("coiCoepDegrading"); - window.sessionStorage.removeItem("coiCoepDegrading"); - if (controlling) { // Reload only on the first failure. const reloadToDegrade = coi.coepDegrade() && !( @@ -99,8 +96,8 @@ if (typeof window === 'undefined') { : coi.coepCredentialless(), }); if (reloadToDegrade) { - window.sessionStorage.setItem("coiCoepDegrading", "true"); !coi.quiet && console.log("Reloading page to degrade COEP."); + window.sessionStorage.setItem("coiReloadedBySelf", "coepdegrade"); coi.doReload("coepdegrade"); } @@ -123,16 +120,17 @@ if (typeof window === 'undefined') { n.serviceWorker.register(window.document.currentScript.src).then( (registration) => { !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope); - window.sessionStorage.setItem("coiReloadedBySelf", "true"); registration.addEventListener("updatefound", () => { !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker."); + window.sessionStorage.setItem("coiReloadedBySelf", "updatefound"); coi.doReload(); }); // If the registration is active, but it's not controlling the page if (registration.active && !n.serviceWorker.controller) { !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker."); + window.sessionStorage.setItem("coiReloadedBySelf", "notcontrolling"); coi.doReload(); } }, diff --git a/coi-serviceworker.min.js b/coi-serviceworker.min.js index 10dda4e..aba7e46 100644 --- a/coi-serviceworker.min.js +++ b/coi-serviceworker.min.js @@ -1,2 +1,2 @@ /*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */ -let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=!window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o={shouldRegister:()=>e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},s=navigator,r=s.serviceWorker&&s.serviceWorker.controller;r&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const t=window.sessionStorage.getItem("coiCoepHasFailed"),i=window.sessionStorage.getItem("coiCoepDegrading");if(window.sessionStorage.removeItem("coiCoepDegrading"),r){const e=o.coepDegrade()&&!(i||window.crossOriginIsolated);s.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||t&&o.coepDegrade())&&o.coepCredentialless()}),e&&(window.sessionStorage.setItem("coiCoepDegrading","true"),!o.quiet&&console.log("Reloading page to degrade COEP."),o.doReload("coepdegrade")),o.shouldDeregister()&&s.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&o.shouldRegister()&&(window.isSecureContext?s.serviceWorker&&s.serviceWorker.register(window.document.currentScript.src).then((e=>{!o.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),window.sessionStorage.setItem("coiReloadedBySelf","true"),e.addEventListener("updatefound",(()=>{!o.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),o.doReload()})),e.active&&!s.serviceWorker.controller&&(!o.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),o.doReload())}),(e=>{!o.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!o.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})(); \ No newline at end of file +let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker&&r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})(); \ No newline at end of file