Cross-frame cross-site scripting - creating a webpage reloader/watchdog - javascript

Setup:
There are remote measurement stations, there is centralized collection/processing/presentation server (with a webserver) and there are observation stations which are to display the collected data for the customers.
These observation stations consist of bare bones simple embedded computer equipped with web browser working in kiosk mode displaying one specific webpage from the central server each. This webpage is updated with AJAX displaying latest measurements of given measurement station. Connected to a fixed monitor, these stations should run almost maintenance-free for years.
Now we've worked out most of the kinks but there's the matter: what if the webserver fails?
The browser will load a "unreachable", "404", "Permission denied", "500", or whatever mode of failure the server took at that point, and remain there until someone manually reboots the observation station.
The general solution I came up with is to set the browser's home page not to the observed page, but to an always-available local HTML file which would perform periodic checks if the remote page was loaded and updates correctly, and reload it if it fails to perform for any reason.
Problem:
the problem lies in cross-frame-scripting. I guess the target webpage would have to load as a frame, iframe, object of type text/HTML, or some other way that will make it show up without removing/disabling the local "container" file. I wrote a cross-frame-scripting page a couple years ago, and circumventing the security counter-measures wasn't easy. Since then the security must have been tightened.
So, the page loaded from remote server contains a piece of javascript that is launched periodically (some setInterval) if everything went well, or doesn't if something was broken. Periodic arrival of this signal to the container frame makes it reset its timeout and not take any other action.
In case the signal does not arrive, as the timeout expires, the container begins periodically refreshing the loaded webpage, until the server is fixed and proper content is loaded, signalling that to the loader.
How do I get the remote page to signal "alive" (say, setting a variable) to the local (container) page loaded from a file:// URL each time a specific function is triggered?

There is a library called porthole which basically does what SF.'s answer describes but in a more formal form. I just wrote a web page to switch showing one of two iframes. In the top level web page I have
var windowProxy;
windowProxy = new Porthole.WindowProxy(baseURL + '/porthole/proxy.html', frameId);
windowProxy.addEventListener(onMessage);
...
function onMessage(messageEvent) {
if (messageEvent.origin !== baseURL) {
$log.error(logPrefix + ': onMessage: invalid origin');
console.dir(messageEvent);
return;
}
if (messageEvent.data.pong) {
pongReceived();
return;
}
$log.log(logPrefix + ': onMessage: unknown message');
console.dir(messageEvent);
}
...
var sendPing = function () {
$log.log(logPrefix + ': ping to ' + baseURL);
...
windowProxy.post({ 'ping': true });
};
plus some additional control logic. In the child web page the following is everything I had to add (plus a call to portholeService.init() from a controller):
// This service takes care of porthole (https://github.com/ternarylabs/porthole)
// communication if this is invoked from a parent frame having this web page
// as a child iframe. Usage of porthole is completely optional, and should
// have no impact on anything outside this service. The purpose of this
// service is to enable some failover service to be build on top of this
// using two iframes to switch between.
services.factory('portholeService', ['$rootScope', '$log', '$location', function ($rootScope, $log, $location) {
$log.log('Hello from portholeService');
function betterOffWithFailover() {
...
}
function onMessage(messageEvent) {
$rootScope.$apply(function () {
if (messageEvent.origin !== baseUrl) {
$log.error('onMessage: invalid origin');
console.dir(messageEvent);
return;
}
if (!messageEvent.data.ping) {
$log.error('unknown message');
console.dir(messageEvent.data);
return;
}
if (betterOffWithFailover()) {
$log.log('not sending pong');
return;
}
windowProxy.post({ 'pong': true });
});
}
var windowProxy;
var baseUrl;
function init() {
baseUrl = $location.protocol() + '://' + $location.host() + ':' + $location.port();
windowProxy = new Porthole.WindowProxy(baseUrl + '/porthole/proxy.html');
windowProxy.addEventListener(onMessage);
}
return {
init: init
};
}]);
For reference these pages are using AngularJS in case $rootScope.$apply etc was unfamiliar to you.

The method for cross-frame, cross-site communication is using postMessage.
The contained frame, on each correct execution should perform:
window.top.postMessage('tyrp', '*');
The container document should contain:
window.onmessage = function(e)
{
if (e.data == 'tyrp') {
//reset timeout here
}
};

Related

service worker inactive issue due to which port gets closed [duplicate]

I need to define my Service Worker as persistent in my Chrome extension because I'm using the webRequest API to intercept some data passed in a form for a specific request, but I don't know how I can do that. I've tried everything, but my Service Worker keeps unloading.
How can I keep it loaded and waiting until the request is intercepted?
Service worker (SW) can't be persistent by definition and the browser must forcibly terminate all of SW connections such as network requests or runtime ports after a certain time, which in Chrome is 5 minutes. The inactivity timer when no such requests or ports are open is even shorter: 30 seconds.
Chromium team currently considers this behavior intentional and good, however this only applies to extensions that observe infrequent events, so they'll run just a few times a day thus reducing browser memory footprint between the runs e.g. webRequest/webNavigation events with urls filter for a rarely visited site. These extensions can be reworked to maintain the state, example. Unfortunately, such an idyll is unsustainable in many cases.
Known problems
Problem 1: Chrome 106 and older doesn't wake up SW for webRequest events.
Although you can try to subscribe to an API like chrome.webNavigation as shown in the other answers, but it helps only with events that occur after the worker starts.
Problem 2: the worker randomly stops waking up for events.
The workaround may be to call chrome.runtime.reload().
Problem 3: Chrome 109 and older doesn't prolong SW lifetime for a new chrome API event in an already running background script. It means that when the event occurred in the last milliseconds of the 30-second inactivity timeout your code won't be able to run anything asynchronous reliably. It means that your extension will be perceived as unreliable by the user.
Problem 4: worse performance than MV2 in case the extension maintains a socket connection or the state (variables) takes a long time to rebuild or you observe frequent events like these:
chrome.tabs.onUpdated/onActivated,
chrome.webNavigation if not scoped to a rare url,
chrome.webRequest if not scoped to a rare url or type,
chrome.runtime.onMessage/onConnect for messages from content script in all tabs.
Starting SW for a new event is essentially like opening a new tab. Creating the environment takes ~50ms, running the entire SW script may take 100ms (or even 1000ms depending on the amount of code), reading the state from storage and rebuilding/hydrating it may take 1ms (or 1000ms depending on the complexity of data). Even with an almost empty script it'd be at least 50ms, which is quite a huge overhead to call the event listener, which takes only 1ms.
SW may restart hundreds of times a day, because such events are generated in response to user actions that have natural gaps in them e.g. clicked a tab then wrote something, during which the SW is terminated and restarted again for a new event thus wearing down CPU, disk, battery, often introducing a frequent perceivable lag of the extension's reaction.
"Persistent" service worker with offscreen API
Courtesy of Keven Augusto.
In Chrome 109 and newer you can use offscreen API to create an offscreen document and send some message from it every 30 second or less, to keep service worker running. Currently this document's lifetime is not limited (only audio playback is limited, which we don't use), but it's likely to change in the future.
manifest.json
"permissions": ["offscreen"]
offscreen.html
<script src="offscreen.js"></script>
offscreen.js
// send a message every 20 sec to service worker
setInterval(() => {
chrome.runtime.sendMessage({ keepAlive: true });
}, 20000);
background.js
// create the offscreen document if it doesn't already exist
async function createOffscreen() {
if (await chrome.offscreen.hasDocument?.()) return;
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['BLOBS'],
justification: 'keep service worker running',
});
}
chrome.runtime.onStartup.addListener(() => {
createOffscreen();
});
// a message from an offscreen document every 20 second resets the inactivity timer
chrome.runtime.onMessage.addListener(msg => {
if (msg.keepAlive) console.log('keepAlive');
});
"Persistent" service worker while nativeMessaging host is connected
In Chrome 105 and newer the service worker will run as long as it's connected to a nativeMessaging host via chrome.runtime.connectNative. If the host process is terminated due to a crash or user action, the port will be closed, and the SW will terminate as usual. You can guard against it by listening to port's onDisconnect event and call chrome.runtime.connectNative again.
"Persistent" service worker while a connectable tab is present
Downsides:
The need for an open web page tab
Broad host permissions (like <all_urls> or *://*/*) for content scripts which puts most extensions into the slow review queue in the web store.
Warning! If you already connect ports, don't use this workaround, use another one for ports below.
Warning! Also implement the workaround for sendMessage (below) if you use sendMessage.
manifest.json, the relevant part:
"permissions": ["scripting"],
"host_permissions": ["<all_urls>"],
"background": {"service_worker": "bg.js"}
background service worker bg.js:
const onUpdate = (tabId, info, tab) => /^https?:/.test(info.url) && findTab([tab]);
findTab();
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'keepAlive') {
setTimeout(() => port.disconnect(), 250e3);
port.onDisconnect.addListener(() => findTab());
}
});
async function findTab(tabs) {
if (chrome.runtime.lastError) { /* tab was closed before setTimeout ran */ }
for (const {id: tabId} of tabs || await chrome.tabs.query({url: '*://*/*'})) {
try {
await chrome.scripting.executeScript({target: {tabId}, func: connect});
chrome.tabs.onUpdated.removeListener(onUpdate);
return;
} catch (e) {}
}
chrome.tabs.onUpdated.addListener(onUpdate);
}
function connect() {
chrome.runtime.connect({name: 'keepAlive'})
.onDisconnect.addListener(connect);
}
all your other extension pages like the popup or options:
;(function connect() {
chrome.runtime.connect({name: 'keepAlive'})
.onDisconnect.addListener(connect);
})();
If you also use sendMessage
In Chrome 99-101 you need to always call sendResponse() in your chrome.runtime.onMessage listener even if you don't need the response. This is a bug in MV3. Also, make sure you do it in less than 5 minutes time, otherwise call sendResponse immediately and send a new message back via chrome.tabs.sendMessage (to the tab) or chrome.runtime.sendMessage (to the popup) after the work is done.
If you already use ports e.g. chrome.runtime.connect
Warning! If you also connect more ports to the service worker you need to reconnect each one before its 5 minutes elapse e.g. in 295 seconds. This is crucial in Chrome versions before 104, which killed SW regardless of additional connected ports. In Chrome 104 and newer this bug is fixed but you'll still need to reconnect them, because their 5-minute lifetime hasn't changed, so the easiest solution is to reconnect the same way in all versions of Chrome: e.g. every 295 seconds.
background script example:
chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'foo') return;
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(deleteTimer);
port._timer = setTimeout(forceReconnect, 250e3, port);
});
function onMessage(msg, port) {
console.log('received', msg, 'from', port.sender);
}
function forceReconnect(port) {
deleteTimer(port);
port.disconnect();
}
function deleteTimer(port) {
if (port._timer) {
clearTimeout(port._timer);
delete port._timer;
}
}
client script example e.g. a content script:
let port;
function connect() {
port = chrome.runtime.connect({name: 'foo'});
port.onDisconnect.addListener(connect);
port.onMessage.addListener(msg => {
console.log('received', msg, 'from bg');
});
}
connect();
"Forever", via a dedicated tab, while the tab is open
Instead of using the SW, open a new tab with an extension page inside, so this page will act as a "visible background page" i.e. the only thing the SW would do is open this tab. You can also open it from the action popup.
chrome.tabs.create({url: 'bg.html'})
It'll have the same abilities as the persistent background page of ManifestV2 but a) it's visible and b) not accessible via chrome.extension.getBackgroundPage (which can be replaced with chrome.extension.getViews).
Downsides:
consumes more memory,
wastes space in the tab strip,
distracts the user,
when multiple extensions open such a tab, the downsides snowball and become a real PITA.
You can make it a little more bearable for your users by adding info/logs/charts/dashboard to the page and also add a beforeunload listener to prevent the tab from being accidentally closed.
Caution regarding persistence
You still need to save/restore the state (variables) because there's no such thing as a persistent service worker and those workarounds have limits as described above, so the worker can terminate. You can maintain the state in a storage, example.
Note that you shouldn't make your worker persistent just to simplify state/variable management. Do it only to restore the performance worsened by restarting the worker in case your state is very expensive to rebuild or if you hook into frequent events listed in the beginning of this answer.
Future of ManifestV3
Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.
unlike the chrome.webRequest API the chrome.webNavigation API works perfectly because the chrome.webNavigation API can wake up the service worker, for now you can try putting the chrome.webRequest API api inside the chrome.webNavigation.
chrome.webNavigation.onBeforeNavigate.addListener(function(){
chrome.webRequest.onResponseStarted.addListener(function(details){
//.............
//.............
},{urls: ["*://domain/*"],types: ["main_frame"]});
},{
url: [{hostContains:"domain"}]
});
If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:
manifest v3
"permissions": [
"alarms"
],
service worker background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })
chrome.alarms.onAlarm.addListener(() => {
console.log('log for debug')
});
Unfortunately this is not my problem and may be you have different problem too. When i refresh dev extension or stop and run prod extension some time service worker die at all. When i close and open browser worker doesn't run and any listeners inside worker doesn't run it too. It tried register worker manually. Fore example:
// override.html
<!DOCTYPE html>
<html lang="en">
<head>...<head>
<body>
...
<script defer src="override.js"></script>
<body>
<html>
// override.js - this code is running in new tab page
navigator.serviceWorker.getRegistrations().then((res) => {
for (let worker of res) {
console.log(worker)
if (worker.active.scriptURL.includes('background.js')) {
return
}
}
navigator.serviceWorker
.register(chrome.runtime.getURL('background.js'))
.then((registration) => {
console.log('Service worker success:', registration)
}).catch((error) => {
console.log('Error service:', error)
})
})
This solution partially helped me but it does not matter because i have to register worker on different tabs. May be somebody know decision. I will pleasure.
I found a different solution to keeping the extension alive. It improves on wOxxOm's answer by using a secondary extension to open the connection port to our main extension. Then both extensions try to communicate with each other in the event that any disconnects, hence keeping both alive.
The reason this was needed was that according to another team in my company, wOxxOm's answer turned out to be unreliable. Reportedly, their SW would eventually fail in an nondeterministic manner.
Then again, my solution works for my company as we are deploying enterprise security software, and we will be force installing the extensions. Having the user install 2 extensions may still be undesirable in other use-cases.
As Clairzil Bawon samdi's answer that chrome.webNavigation could wake up the service worker in MV3, here are workaround in my case:
// manifest.json
...
"background": {
"service_worker": "background.js"
},
"host_permissions": ["https://example.com/api/*"],
"permissions": ["webRequest", "webNavigation"]
...
In my case it listens onHistoryStateUpdated event to wake up the service worker:
// background.js
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log('wake me up');
});
chrome.webRequest.onSendHeaders.addListener(
(details) => {
// code here
},
{
urls: ['https://example.com/api/*'],
types: ['xmlhttprequest'],
},
['requestHeaders']
);
IMHO (and direct experience) a well structured SW will work forever.
Obviously there are some particular cases, like uninterruptible connections, which may suffer a lot once SW falls asleep, but still if the code is not prepared to handle the specific behaviour.
It seems like a battle against windmills, punctually after 30 seconds SW stops doing anything, falls asleep, several events are not honored anymore and the problems start... if our SW has nothing else pressing to think about.
From "The art of War" (Sun Tzu): if you can't fight it, make friends with it.
so... ok, lets try to give something consistent to think about from time to time to our SW and put a "patch" (because this IS A PATCH!) to this issue.
Obviously I don't assure this solution will work for all of you, but it worked for me in the past, before I decided to review the whole logic and code of my SW.
So I decided to share it for your own tests.
This doesn't require any special permission in manifest V3.
Remember to call the StayAlive() function below at SW start.
To perform reliable tests remember to not open any DevTools page. Use chrome://serviceworker-internals instead and find the log (Scope) of your extension ID.
EDIT:
Since the logic of the code may not be clear to some, I will try to explain it to dispel doubts:
Any extension's SW can attempt to make a connection and send messages through a named port and, if something fails, generate an error.
The code below connects to a named port and tries to send a message through it to a nonexistent listener (so it will generate errors).
While doing this, SW is active and running (it has something to do, that is, it has to send a message through a port).
Because noone is listening, it generates a (catched and logged) error (in onDisconnect) and terminates (normal behaviour happening in whatever code).
But after 25 secs it does the same iter from start, thus keeping SW active forever.
It works fine. It is a simple trick to keep the service worker active.
// Forcing service worker to stay alive by sending a "ping" to a port where noone is listening
// Essentially it prevents SW to fall asleep after the first 30 secs of work.
const INTERNAL_STAYALIVE_PORT = "Whatever_Port_Name_You_Want"
var alivePort = null;
...
StayAlive();
...
async function StayAlive() {
var lastCall = Date.now();
var wakeup = setInterval( () => {
const now = Date.now();
const age = now - lastCall;
console.log(`(DEBUG StayAlive) ----------------------- time elapsed: ${age}`)
if (alivePort == null) {
alivePort = chrome.runtime.connect({name:INTERNAL_STAYALIVE_PORT})
alivePort.onDisconnect.addListener( (p) => {
if (chrome.runtime.lastError){
console.log(`(DEBUG StayAlive) Disconnected due to an error: ${chrome.runtime.lastError.message}`);
} else {
console.log(`(DEBUG StayAlive): port disconnected`);
}
alivePort = null;
});
}
if (alivePort) {
alivePort.postMessage({content: "ping"});
if (chrome.runtime.lastError) {
console.log(`(DEBUG StayAlive): postMessage error: ${chrome.runtime.lastError.message}`)
} else {
console.log(`(DEBUG StayAlive): "ping" sent through ${alivePort.name} port`)
}
}
//lastCall = Date.now();
}, 25000);
}
Hoping this will help someone.
Anyway, I still recommend, where possible, to review the logic and the code of your SW, because, as I mentioned at the beginning of this post, any well structured SW will work perfectly in MV3 even without tricks like this one.
EDIT (jan 17, 2023)
when you think you've hit bottom, watch out for the trapdoor that might suddenly open under your feet.
Sun Tzu
This revision of the StayAlive() function above still keeps the service worker active, but avoids calling the function every 25 seconds, so as not to burden it with unnecessary work.
In practice, it appears that by running the Highlander() function below at predefined intervals, the service worker will still live forever.
How it works
The first call of Highlander() is executed before the expiration of the fateful 30 seconds (here it is executed after 4 seconds from the start of the service worker).
Subsequent calls are performed before the expiration of the fateful 5 minutes (here they are executed every 270 seconds).
The service worker, in this way, will never go to sleep and will always respond to all events.
It thus appears that, per Chromium design, after the first Highlander() call within the first 30 seconds, the internal logic that manages the life of the (MV3) service worker extends the period of full activity until the next 5 minutes.
This is really really hilarious...
anyway... this is the ServiceWorker.js I used for my tests.
// -----------------
// SERVICEWORKER.JS
// -----------------
const INTERNAL_STAYALIVE_PORT = "CT_Internal_port_alive"
var alivePort = null;
const SECONDS = 1000;
var lastCall = Date.now();
var isFirstStart = true;
var timer = 4*SECONDS;
// -------------------------------------------------------
var wakeup = setInterval(Highlander, timer);
// -------------------------------------------------------
async function Highlander() {
const now = Date.now();
const age = now - lastCall;
console.log(`(DEBUG Highlander) ------------- time elapsed from first start: ${convertNoDate(age)}`)
if (alivePort == null) {
alivePort = chrome.runtime.connect({name:INTERNAL_STAYALIVE_PORT})
alivePort.onDisconnect.addListener( (p) => {
if (chrome.runtime.lastError){
console.log(`(DEBUG Highlander) Expected disconnect (on error). SW should be still running.`);
} else {
console.log(`(DEBUG Highlander): port disconnected`);
}
alivePort = null;
});
}
if (alivePort) {
alivePort.postMessage({content: "ping"});
if (chrome.runtime.lastError) {
console.log(`(DEBUG Highlander): postMessage error: ${chrome.runtime.lastError.message}`)
} else {
console.log(`(DEBUG Highlander): "ping" sent through ${alivePort.name} port`)
}
}
//lastCall = Date.now();
if (isFirstStart) {
isFirstStart = false;
clearInterval(wakeup);
timer = 270*SECONDS;
wakeup = setInterval(Highlander, timer);
}
}
function convertNoDate(long) {
var dt = new Date(long).toISOString()
return dt.slice(-13, -5) // HH:MM:SS only
}
EDIT (jan 20, 2023):
On Github, I created a repository for a practical example of how to properly use the Highlander function in a real world extension. For the implementation of this repo, I also took into account wOxxOm's comments to my post (many thanks to him).
Still on Github, I created another repository to demonstrate in another real world extension how a service worker can immediately start by itself (put itself in RUNNING status), without the aid of external content scripts, and how it can live on forever using the usual Highlander function.
This repository includes a local WebSocket Echo Test server used by the extension in its client communication sample and useful to externally debug the extension when the extension's host browser has been closed. That's right, because, depending on the type of configuration applied, when the host browser is closed Highlander-DNA can either shut down with the browser or continue to live forever, with all functionality connected and managed (e.g. the included WebSocket client/server communications test sample).
EDIT (jan 22, 2023)
I tested memory and CPU consumption while a Service Worker is always in RUNNING state due to the use of Highlander. The consumption to keep it running all the time is practically ZERO. I really don't understand why the Chromium team is persisting in wanting to unnecessarily complicate everyone's life.
WebSocket callbacks registered from within the chrome.runtime listener registrations of my extensions's service worker would not get invoked, which sounds like almost the same problem.
I approached this problem by making sure that my service worker never ends, by adding the following code to it:
function keepServiceRunning() {
setTimeout(keepServiceRunning, 2000);
}
keepServiceRunning()
After this, my callbacks now get invoked as expected.

Can you disable a service worker before the page loads?

My team and I have a project that was originally built as a PWA, but have since decided to scrap that idea as we realized it would need to change much more frequently than originally intended. However, the service worker is already live, as well as a newly redesigned landing page for the website. Despite all our efforts to clear the PWA caching, our clients are still reporting that they are receiving the old cached version of the website.
Currently, we have the service worker set up to delete all caches upon install (and whenever anything at all happens as a precaution), as well as some JavaScript to unregister the service worker when the new page actually loads. However, the problem is that none of this runs until the user makes a request to the website, and at that point the browser is already loading the cached content. Is it possible to clear this cache and prevent the browser from loading any content that was already cached?
Current service-worker.js
// Caching
var cacheCore = 'mkeSculptCore-0330121058';
var cacheAssets = 'mkeSculptAssets-0330121058';
self.addEventListener('install', function (event) {
self.skipWaiting();
caches.keys().then(function (names) {
for (let name of names)
caches.delete(name);
});
});
self.addEventListener('activate', function (event) {
caches.keys().then(function (names) {
for (let name of names)
caches.delete(name);
});
});
self.addEventListener('fetch', function (event) {
caches.keys().then(function (names) {
for (let name of names)
caches.delete(name);
});
});
Script in index.html
(function () {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function (registrations) {
//returns installed service workers
if (registrations.length) {
for (let registration of registrations) {
registration.unregister();
}
}
});
}
})();
So far, I've read a few other similar StackOverflow answers, including this one, but they tend to rely on users manually doing something to fetch the new content, ie. via a hard reload or disabling the service worker manually through the browser settings. However, in my case, we cannot rely on manual user actions.
One way to solve this issue is to add a timestamp at end of the file(js, css) name so each time when it is making a request, the cache key is not available in the service worker and thus it tends to get a new version of the file at each load.
<script type="text/javascript" src="/js/scipt1.js?t=05042018121212"/>
For appending a new timestamp dynamically in the file name, please check this answer
But this may not be reliable if HTML itself is cached.
add this before to "update" all contents:
$.each(['index.html','file1.js','file2.js','file3.js'],function(index,file) {
$.get(file+'?t='+new Date().getTime(), function(){});
});
location.reload(true);
for the ServiceWorker to stop all windows using it must be closed.
If it's a webapp you can use window.close();
This code just loads a fresh version of the files in the list.
If there are any internal caches they will be all updated.

Service Worker is "installing" with every page reload though the sw.js did not change

I am using a service worker to cache the JS and CSS files of a web app.
I mostly use the code from a Google Chrome example, the only thing I added is a notification and countdown that refreshes the window when "new or updated content is available".
The Google example: https://github.com/GoogleChrome/sw-precache/blob/5699e5d049235ef0f668e8e2aa3bf2646ba3872f/demo/app/js/service-worker-registration.js
However, this happens far too often and I don't understand why.
There has definitely not been any changes to the files on the server, but still sometimes I see the notification and the window reloads.
I expected this would only happen when any of the files governed by the service worker actually changes (they are all hashed via Webpack and have definitely not changed in between).
This is the code I use, inlined in index.html:
/* eslint-env browser */
'use strict';
function reloadApp(delay) {
var t = delay || 0;
var message = 'New or updated content is available. Reloading app in {s} seconds';
var getMessage = function() { return message.replace('{s}', t) }
var div = document.createElement('div');
div.id = 'update-notification';
div.innerHTML = getMessage();
document.body.appendChild(div);
var intervalID = setInterval(function() {
t = t - 1;
if (t <= 0) {
clearInterval(intervalID);
window.location.reload();
}
else {
div.innerHTML = getMessage();
}
}, 1000);
}
if ('serviceWorker' in navigator && window.location.protocol === 'https:') {
navigator.serviceWorker.register('/service-worker.js').then(function(reg) {
console.info('serviceWorker registered');
reg.onupdatefound = function() {
var installingWorker = reg.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
reloadApp(5);
} else {
console.log('Content is now available offline!');
}
break;
case 'redundant':
console.error('The installing service worker became redundant.');
break;
}
};
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}
This is the change I made:
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and the fresh content will
// have been added to the cache.
// It's the perfect time to display a "New content is available; please refresh."
// message in the page's interface.
console.log('New or updated content is available.');
} else {
became:
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
reloadApp(5);
The service worker itself is generated via sw-precache-webpack-plugin and the hashed files look like this:
var precacheConfig = [["cms.CrudFactory.144344a2.js","6a65526f764f3caa4b8c6e0b84c1b31b"],["cms.routes.c20796b4.js","f8018476ceffa8b8f6ec00b297a6492d"],["common.cms-main.0f2db9ff.js","92017e838aff992e9f47f721cb07b6f0"],["common.licensing-main.8000b17d.js","0d43abd063567d5edaccdcf9c0e4c362"],["common.mediaplayer-main.314be5d2.js","2061501465a56e82d968d7998b9ac364"],["common.ordering-main.783e8605.js","0531c4a90a0fa9ea63fbe1f82e86f8c6"],["common.shared-main.0224b0ea.js","956ae4d2ddbddb09fb51804c09c83b22"],["common.stores-main.98246b60.js","cbdc46bc3abeac89f37e8f64b1737a22"],["component.App.284fec19.js","07f1923f1a0098bf9eba28fc7b307c18"],["component.AppToolbar.00e371de.js","9b542d4a85bdeece9d36ee4e389f6206"],["component.DevToolbar.652bf856.js","1744126e32774a93796146ac878ddd8e"],["component.Grid.4b964e52.js","755819ca2c7f911805e372401d638351"],["component.MediaPlayer.54db6e85.js","d0d8ae269665e810d1b997b473005f76"],["component.Search.05476f89.js","0cae8928aff533a6726dfaeb0661456a"],["data.country-list.d54f29a7.js","e27746418e02f75593a93b958a60807e"],["dev.sendSlackMessage.da8e22f4.js","ccb10829f18a727b79a5e8631bc4b2a2"],["index.html","02ff43eabc33c600e98785cffe7597d9"],["lib.gemini-scrollbar.df2fbf63.js","3941036bacb4a1432d22151ea7da073b"],["lib.isotope-horizontal.1604d974.js","6ac56d4296468c4428b5b5301a65938a"],["lib.isotope-packery.fabb73c3.js","808061641596e4b0ea2158b42d65915a"],["lib.react-color.265a6de0.js","f23f63d7e6934e381ffdc0405ecb449a"],["lib.react-date-picker.0a81fec3.js","778d1626645e439ad01e182ee589971a"],["lib.react-select.c768cf77.js","c8782991a161790ef2c9a2c7677da313"],["main.43e29bc6.js","fe6a6277acaa2a369ef235961be7bbcf"],["route.admin.CleanupPage.8b4bbf8e.js","8ab20412329e1ba4adc327713135be97"],["route.app.CmsPage.8a4303fb.js","0bf1506869db24bb9789c8a46449c8ad"],["route.app.CmsPageWrapper.accdebcc.js","c91e77aa8b518e1878761269deac22b6"],["route.app.ContactPage.75693d32.js","a530b00a5230a44897c2bf0aa9f402a8"],["route.app.PasswordResetExpiredDialog.65431bae.js","b5eef791dbd68edd4769bd6da022a857"],["route.app.SeriesDetailsPage.11a6989b.js","c52178b57767ae1bf94a9594cb32718e"],["route.cms.MetadataFormsPage.636188d2.js","e1e592b7e3dd82af774ac215524465c0"],["route.cms.PermissionsListPage.0e1e3075.js","9a3cc340a64238a1ab3ba1c0d483b7bd"],["route.cms.PermissionsPage.78a69f60.js","4b18e646715d6268e7aba3932f4e04a9"],["route.cms.SysconfigPage.f699b871.js","79bd1275213478f2ff1970e0b7341c49"],["styles.43e29bc620615f854714.css","b2e9e55e9ee2def2ae577ee1aaebda8f"],["styles.e0c12bb1c77e645e92d3.css","626778177949d72221e83b601c6c2c0f"],["translations.en.83fced0e.js","7e5509c23b5aafc1744a28a85c2950bb"],["translations.nl.4ae0b3bb.js","515ec7be7352a0c61e4aba99f0014a39"],["vendor.e0c12bb1.js","36ce9e0a59c7b4577b2ac4a637cb4238"]];
var cacheName = 'sw-precache-v3-nisv-client-' + (self.registration ? self.registration.scope : '');
Questions:
Why is the 'new or updated content' case happening so often?
Is the assumption correct that case 'installed': if (navigator.serviceWorker.controller) { means New or updated content is available?
How should I debug the problem?
A service worker has a life cycle:
- registered
- installing
- installed
- waiting
- activated
- redundant
When you register a service worker, it is fetched from the server, and it's lifespan in the disk cache depends on the cache-control response headers that your web server sends it with.
It is considered best practice, to have a cache-control : no-store must-revalidate along with a max-age : 0 if possible.
If you forget, the service worker is usually considered stale after 24 hours of registration, and is refetched and disk caches updated.
There are manual upgrades you can do, by bumping up a cache version on the service worker, or changing the service worker code. Even a byte change makes the browser view the service worker as a new one, and it installs the new service worker.
This however, does not mean, the old service-worker is discarded. It is imperative that any user close all the tabs / navigate away from your site, for the browser to discard the old service worker, and activate the new one.
you can bypass this behavior, by a self.skipWaiting in your install handler
That said,
While developing, you have the option of opening the browser's (chromium)
dev panel, navigate to the application tab, and check the box that says
update on reload
What this does is, immediately discard the old service worker irregardless of whether there is a change in your service worker code.
Regarding your questions:
Is the assumption correct that case 'installed': if (navigator.serviceWorker.controller) { means New or updated content is available?
NO. This means, that the new service worker, say sw-v1, is installed, and is currently waiting to activate.
Why is the 'new or updated content' case happening so often?
you say, you are using sw-precache. Since, I haven't used it myself, I have to add, a counter question
Is the plugin configured, to pick up changes to your static assets, and automatically trigger a code change or a cache bump in your service-worker ?
If so, then, any new version of your service worker, must only be waiting and not activating on a change.
The probable reason for this skipping of the waiting stage is,
you have a self.skipWaiting inside your install event inside your service worker.
Note the above is true, ONLY if
the change to your content happens. AND
that change triggers a service worker update
In this scenario, you might want to consider commenting out the self.skipWaiting statement. Or, maybe you could configure your plugin, to not skip on update - this matters on your content, and you need to take a call
In lieu of the fact that your assets changed and/ or triggered an update,
The other (most probable) reason would have to be,
you have an update on reload box checked under the application - > service workers tab on your dev console.
Uncheck that box, if checked, and then clear all caches, unregister all workers. After , open a new tab and continue testing the behavior.

Retrieve html content of a page several seconds after it's loaded

I'm coding a script in nodejs to automatically retrieve data from an online directory.
Knowing that I had never done this, I chose javascript because it is a language I use every day.
I therefore from the few tips I could find on google use request with cheerios to easily access components of dom of the page.
I found and retrieved all the necessary information, the only missing step is to recover the link to the next page except that the one is generated 4 seconds after loading of page and link contains a hash so that this step Is unavoidable.
What I would like to do is to recover dom of page 4-5 seconds after its loading to be able to recover the link
I looked on the internet, and much advice to use PhantomJS for this manipulation, but I can not get it to work after many attempts with node.
This is my code :
#!/usr/bin/env node
require('babel-register');
import request from 'request'
import cheerio from 'cheerio'
import phantom from 'node-phantom'
phantom.create(function(err,ph) {
return ph.createPage(function(err,page) {
return page.open(url, function(err,status) {
console.log("opened site? ", status);
page.includeJs('http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js', function(err) {
//jQuery Loaded.
//Wait for a bit for AJAX content to load on the page. Here, we are waiting 5 seconds.
setTimeout(function() {
return page.evaluate(function() {
var tt = cheerio.load($this.html())
console.log(tt)
}, function(err,result) {
console.log(result);
ph.exit();
});
}, 5000);
});
});
});
});
but i get this error :
return ph.createPage(function (page) {
^
TypeError: ph.createPage is not a function
Is what I am about to do is the best way to do what I want to do? If not what is the simplest way? If so, where does my error come from?
If You dont have to use phantomjs You can use nightmare to do it.
It is pretty neat library to solve problems like yours, it uses electron as web browser and You can run it with or without showing window (You can also open developer tools like in Google Chrome)
It has only one flaw if You want to run it on server without graphical interface that You must install at least framebuffer.
Nightmare has method like wait(cssSelector) that will wait until some element appears on website.
Your code would be something like:
const Nightmare = require('nightmare');
const nightmare = Nightmare({
show: true, // will show browser window
openDevTools: true // will open dev tools in browser window
});
const url = 'http://hakier.pl';
const selector = '#someElementSelectorWitchWillAppearAfterSomeDelay';
nightmare
.goto(url)
.wait(selector)
.evaluate(selector => {
return {
nextPage: document.querySelector(selector).getAttribute('href')
};
}, selector)
.then(extracted => {
console.log(extracted.nextPage); //Your extracted data from evaluate
});
//this variable will be injected into evaluate callback
//it is required to inject required variables like this,
// because You have different - browser scope inside this
// callback and You will not has access to node.js variables not injected
Happy hacking!

iframe content doesn't always load

So I have a system that essentially enabled communication between two computers, and uses a WebRTC framework to achieve this:
"The Host": This is the control computer, and clients connect to this. They control the clients window.
"The Client": The is the user on the other end. They are having their window controlled by the server.
What I mean by control, is that the host can:
change CSS on the clients open window.
control the URL of an iframe on the clients open window
There are variations on these but essentially thats the amount of control there is.
When "the client" logs in, the host sends a web address to the client. This web address will then be displayed in an iframe, as such:
$('#iframe_id').attr("src", URL);
there is also the ability to send a new web address to the client, in the form of a message. The same code is used above in order to navigate to that URL.
The problem I am having is that on, roughly 1 in 4 computers the iframe doesn't actually load. It either displays a white screen, or it shows the little "page could not be displayed" icon:
I have been unable to reliably duplicate this bug
I have not seen a clear pattern between computers that can and cannot view the iframe content.
All clients are running google chrome, most on an apple powermac. The only semi-link I have made is that windows computers seem slightly more susceptible to it, but not in a way I can reproduce. Sometimes refreshing the page works...
Are there any known bugs that could possibly cause this to happen? I have read about iframe white flashes but I am confident it isn't that issue. I am confident it isn't a problem with jQuery loading because that produces issues before this and would be easy to spot.
Thanks so much.
Alex
edit: Ok so here is the code that is collecting data from the server. Upon inspection the data being received is correct.
conn.on('data', function(data) {
var data_array = JSON.parse(data);
console.log(data_array);
// initialisation
if(data_array.type=='init' && inititated === false) {
if(data_array.duration > 0) {
set_timeleft(data_array.duration); // how long is the exam? (minutes)
} else {
$('#connection_remainingtime').html('No limits');
}
$('#content_frame').attr("src", data_array.uri); // url to navigate to
//timestarted = data_array.start.replace(/ /g,''); // start time
ob = data_array.ob; // is it open book? Doesnt do anything really... why use it if it isnt open book?
snd = data_array.snd; // is sound allowed?
inititated = true;
}
}
It is definitele trying to make the iframe navigate somewhere as when the client launches the iframe changes - its trying to load something but failing.
EDIT: Update on this issue: It does actually work, just not with google forms. And again it isn't everybody's computers, it is only a few people. If they navigate elsewhere (http://www.bit-tech.net for example) then it works just fine.
** FURTHER UPDATE **: It seems on the ones that fail, there is an 'X-Frames-Origin' issue, in that its set the 'SAMEORIGIN'. I dont understand why some students would get this problem and some wouldn't... surely it depends upon the page you are navigating to, and if one person can get it all should be able to?
So the problem here was that the students were trying to load this behind a proxy server which has an issue with cookies. Although the site does not use cookies, the proxy does, and when the student had blocked "third party cookies" in their settings then the proxy was not allowing the site to load.
Simply allowed cookies and it worked :)
iframes are one of the last things to load in the DOM, so wrap your iframe dependent code in this:
document.getElementById('content_frame').onload = function() {...}
If that doesn't work then it's the document within the iframe. If you own the page inside the iframe then you have options. If not...setTimeout? Or window.onload...?
SNIPPET
conn.on('data', function(data) {
var data_array = JSON.parse(data);
console.log(data_array);
// initialisation
if (data_array.type == 'init' && inititated === false) {
if (data_array.duration > 0) {
set_timeleft(data_array.duration); // how long is the exam? (minutes)
} else {
$('#connection_remainingtime').html('No limits');
}
document.getElementById('content_frame').onload = function() {
$('#content_frame').attr("src", data_array.uri); // url to navigate to
//timestarted = data_array.start.replace(/ /g,''); // start time
ob = data_array.ob; // is it open book? Doesnt do anything really... why use it if it isnt open book?
snd = data_array.snd; // is sound allowed?
inititated = true;
}
}
}

Categories