1// Service worker for smart-mirror failover. 2// 3// Generated per-request by node/server.js (route GET /service-worker). 4// The backup domain is picked from the same auto-increment group of 10 5// in the `offlinedomains` collection and injected via EJS below. 6// 7// Behavior on top-level HTML navigations: 8// 1. Fire the original request immediately. 9// 2. In parallel, after BACKUP_TIMEOUT_MS, probe the backup mirror's 10// sentinel file. If it returns 2xx, build a 302 redirect to the same 11// path on the backup with ?od=<original host> appended. 12// 3. Original wins as soon as it returns a response that proves this domain 13// is reachable (see isReachable). 14// 4. Backup wins if the probe succeeds before the original proves reachable. 15// 5. If both fail, fall back to a synthetic 503 (or the non-ok original 16// response, which is more informative). 17// 18// A navigation that already carries ?od= is the product of a hop and is never 19// hopped again. Without that, any condition that is non-ok on both siblings 20// (a persistent upstream 5xx, a per-domain cookie missing on the sibling) 21// bounces the visitor between the two domains indefinitely. 22// 23// Non-navigation requests (assets, XHR, iframes, POST forms, â¦) are NOT 24// intercepted â the browser handles them normally. 25 26const BACKUP_DOMAIN = 'technology-new.ru'; 27const BACKUP_TIMEOUT_MS = 5000; 28const PROBE_PATH = '/test74382647832.txt'; 29 30const LOG = '[sw-mirror]'; 31const tnow = () => Date.now(); 32 33self.addEventListener('install', (event) => { 34 console.log(LOG, 'install; backupDomain=' + (BACKUP_DOMAIN || '(none)')); 35 event.waitUntil(self.skipWaiting()); 36}); 37 38self.addEventListener('activate', (event) => { 39 console.log(LOG, 'activate'); 40 event.waitUntil(self.clients.claim()); 41}); 42 43// A blocked domain fails at the network layer (DNS poisoning, RST, timeout), 44// so any answer from our own server proves reachability â except 5xx, which 45// is what an origin outage behind Cloudflare looks like. Two non-ok answers 46// still count: 47// - opaqueredirect: navigations are fetched with redirect:'manual', so every 48// 3xx (e.g. /p/https/bbc.com/ -> www.bbc.com) arrives as status 0, ok=false. 49// - the proof-of-work interstitial (503 + X-Mirror-Pow), which must be shown 50// here: the pass cookie is per-domain, so the sibling would gate too. 51function isReachable(response) { 52 if (!response) return false; 53 if (response.ok || response.type === 'opaqueredirect') return true; 54 if (response.headers.get('X-Mirror-Pow') === '1') return true; 55 return response.status > 0 && response.status < 500; 56} 57 58function shouldHandle(request) { 59 if (request.destination === 'iframe') return false; 60 if (request.mode === 'navigate') return true; 61 if (request.method !== 'GET') return false; 62 const accept = request.headers.get('accept') || ''; 63 return accept.includes('text/html'); 64} 65 66self.addEventListener('fetch', (event) => { 67 if (!shouldHandle(event.request)) return; 68 event.respondWith(handleNavigation(event.request)); 69}); 70 71let requestSeq = 0; 72 73async function handleNavigation(request) { 74 const id = (++requestSeq).toString(36); 75 const start = tnow(); 76 const ms = () => (tnow() - start) + 'ms'; 77 const tag = (msg) => `${LOG} [req#${id}] ${msg}`; 78 79 console.log(tag('navigation'), { 80 url: request.url, 81 mode: request.mode, 82 destination: request.destination, 83 method: request.method, 84 backupDomain: BACKUP_DOMAIN || '(none)', 85 }); 86 87 // Guard against degenerate backups (no partner found, or partner == self), 88 // and against hopping a request that is itself the result of a hop. 89 const requestUrl = new URL(request.url); 90 const sameAsSelf = BACKUP_DOMAIN && 91 BACKUP_DOMAIN.toLowerCase() === requestUrl.hostname.toLowerCase(); 92 const alreadyHopped = requestUrl.searchParams.has('od'); 93 if (!BACKUP_DOMAIN || sameAsSelf || alreadyHopped) { 94 const why = alreadyHopped ? 'already hopped (od=)' : sameAsSelf ? 'equals self' : 'empty'; 95 console.warn(tag(`no usable backup (${why}); passthrough`)); 96 try { 97 const r = await fetch(request); 98 console.log(tag(`passthrough status=${r.status} (${ms()})`)); 99 return r; 100 } catch (e) { 101 console.error(tag(`passthrough failed: ${(e && e.message) || e} (${ms()})`)); 102 return new Response('Service Unavailable', { status: 503 }); 103 } 104 } 105 106 let originalReachable = false;
107 108 // Both promises ALWAYS resolve (never reject) â see .catch() below. 109 const originalPromise = fetch(request) 110 .then((response) => { 111 originalReachable = isReachable(response); 112 console.log(tag(`original resolved status=${response.status} type=${response.type} reachable=${originalReachable} (${ms()})`)); 113 return response; 114 }) 115 .catch((e) => { 116 console.warn(tag(`original rejected: ${(e && e.message) || e} (${ms()})`)); 117 return null; 118 }); 119 120 const backupPromise = tryBackup(request, tag, ms, () => originalReachable); 121 122 // Decision logic: 123 // - As soon as original resolves reachable: use it. 124 // - As soon as backup produces a redirect: use it. 125 // - Otherwise wait for both, then pick best available (backup > non-ok original > 503). 126 return await new Promise((resolve) => { 127 let done = false; 128 const finish = (response, why) => { 129 if (done) return; 130 done = true; 131 console.log(tag(`final: ${why} (${ms()})`)); 132 resolve(response); 133 }; 134 135 originalPromise.then((r) => { 136 if (isReachable(r)) finish(r, `original reachable status=${r.status} type=${r.type}`); 137 }); 138 139 backupPromise.then((r) => { 140 if (r) finish(r, 'backup redirect'); 141 }); 142 143 Promise.all([originalPromise, backupPromise]).then(([orig, bkp]) => { 144 if (done) return; 145 if (bkp) { 146 finish(bkp, 'backup (late)'); 147 } else if (orig) { 148 finish(orig, `original non-ok status=${orig.status}`); 149 } else { 150 finish( 151 new Response('Service Unavailable (no mirror available)', { 152 status: 503, 153 headers: { 'Content-Type': 'text/plain' }, 154 }), 155 'both failed' 156 ); 157 } 158 }); 159 }); 160} 161 162async function tryBackup(request, tag, ms, isOriginalReachable) { 163 console.log(tag(`backup timer armed: ${BACKUP_TIMEOUT_MS}ms`)); 164 await new Promise((r) => setTimeout(r, BACKUP_TIMEOUT_MS)); 165 166 if (isOriginalReachable()) { 167 console.log(tag(`backup canceled: original already reachable (${ms()})`)); 168 return null; 169 } 170 171 const probeUrl = `https://${BACKUP_DOMAIN}${PROBE_PATH}`; 172 console.log(tag(`probing backup: ${probeUrl} (${ms()})`)); 173 174 let probe; 175 try { 176 probe = await fetch(probeUrl, { cache: 'no-store' }); 177 } catch (e) { 178 console.error(tag(`backup probe rejected: ${(e && e.message) || e} (${ms()})`)); 179 return null; 180 } 181 console.log(tag(`backup probe status=${probe.status} ok=${probe.ok} type=${probe.type} (${ms()})`)); 182 if (!probe.ok) return null; 183 184 if (isOriginalReachable()) { 185 console.log(tag(`original reachable during probe; not redirecting (${ms()})`)); 186 return null; 187 } 188 189 const target = new URL(request.url); 190 const originalHost = target.host; 191 target.hostname = BACKUP_DOMAIN; 192 target.port = ''; 193 target.searchParams.set('od', originalHost); 194 195 console.log(tag(`redirect -> ${target.toString()} (${ms()})`)); 196 return Response.redirect(target.toString(), 302); 197}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.