fox-send/app/serviceWorker.js

175 lines
4.5 KiB
JavaScript
Raw Permalink Normal View History

2018-11-16 17:32:29 +00:00
import assets from '../common/assets';
import { version } from '../package.json';
2018-07-05 19:40:49 +00:00
import Keychain from './keychain';
2018-07-11 23:52:46 +00:00
import { downloadStream } from './api';
2018-07-19 21:46:12 +00:00
import { transformStream } from './streams';
2018-07-26 05:26:11 +00:00
import Zip from './zip';
import contentDisposition from 'content-disposition';
2018-07-11 23:52:46 +00:00
let noSave = false;
const map = new Map();
2018-11-26 21:25:47 +00:00
const IMAGES = /.*\.(png|svg|jpg)$/;
const VERSIONED_ASSET = /\.[A-Fa-f0-9]{8}\.(js|css|png|svg|jpg)(#\w+)?$/;
2018-11-26 21:25:47 +00:00
const DOWNLOAD_URL = /\/api\/download\/([A-Fa-f0-9]{4,})/;
2019-09-05 22:57:07 +00:00
const FONT = /\.woff2?$/;
2018-07-05 19:40:49 +00:00
self.addEventListener('install', () => {
self.skipWaiting();
2018-07-05 19:40:49 +00:00
});
2018-07-11 23:52:46 +00:00
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim().then(precache));
2018-07-11 23:52:46 +00:00
});
async function decryptStream(id) {
const file = map.get(id);
if (!file) {
return new Response(null, { status: 400 });
}
2018-07-11 23:52:46 +00:00
try {
2018-07-26 05:26:11 +00:00
let size = file.size;
let type = file.type;
2018-07-19 20:20:10 +00:00
const keychain = new Keychain(file.key, file.nonce);
if (file.requiresPassword) {
keychain.setPassword(file.password, file.url);
}
2018-07-10 00:00:19 +00:00
2018-07-18 23:39:14 +00:00
file.download = downloadStream(id, keychain);
2018-07-05 19:40:49 +00:00
2018-07-18 23:39:14 +00:00
const body = await file.download.result;
2018-07-05 19:40:49 +00:00
const decrypted = keychain.decryptStream(body);
2018-07-26 05:26:11 +00:00
let zipStream = null;
if (file.type === 'send-archive') {
const zip = new Zip(file.manifest, decrypted);
zipStream = zip.stream;
type = 'application/zip';
size = zip.size;
}
const responseStream = transformStream(
2018-07-26 05:26:11 +00:00
zipStream || decrypted,
{
transform(chunk, controller) {
file.progress += chunk.length;
controller.enqueue(chunk);
}
},
function oncancel() {
// NOTE: cancel doesn't currently fire on chrome
// https://bugs.chromium.org/p/chromium/issues/detail?id=638494
file.download.cancel();
map.delete(id);
2018-07-11 23:52:46 +00:00
}
);
2018-07-09 22:39:06 +00:00
2018-07-11 23:52:46 +00:00
const headers = {
'Content-Disposition': contentDisposition(file.filename),
2018-07-26 05:26:11 +00:00
'Content-Type': type,
'Content-Length': size
2018-07-11 23:52:46 +00:00
};
return new Response(responseStream, { headers });
2018-07-11 23:52:46 +00:00
} catch (e) {
if (noSave) {
return new Response(null, { status: e.message });
2018-07-06 22:49:50 +00:00
}
2018-07-05 19:40:49 +00:00
return new Response(null, {
status: 302,
headers: {
2018-09-07 17:53:40 +00:00
Location: `/download/${id}/#${file.key}`
}
});
2018-07-11 23:52:46 +00:00
}
2018-07-05 19:40:49 +00:00
}
2018-11-16 17:32:29 +00:00
async function precache() {
try {
await cleanCache();
const cache = await caches.open(version);
const images = assets.match(IMAGES);
await cache.addAll(images);
} catch (e) {
console.error(e);
// cache will get populated on demand
}
}
async function cleanCache() {
2018-11-16 20:31:36 +00:00
const oldCaches = await caches.keys();
for (const c of oldCaches) {
if (c !== version) {
await caches.delete(c);
}
}
2018-11-16 17:32:29 +00:00
}
2019-09-05 22:57:07 +00:00
function cacheable(url) {
return VERSIONED_ASSET.test(url) || FONT.test(url);
}
2018-11-26 21:25:47 +00:00
async function cachedOrFetched(req) {
2018-11-16 17:32:29 +00:00
const cache = await caches.open(version);
const cached = await cache.match(req);
2018-11-26 21:25:47 +00:00
if (cached) {
return cached;
}
const fetched = await fetch(req);
2019-09-05 22:57:07 +00:00
if (fetched.ok && cacheable(req.url)) {
2018-11-26 21:25:47 +00:00
cache.put(req, fetched.clone());
}
return fetched;
2018-11-16 17:32:29 +00:00
}
2018-07-06 22:49:50 +00:00
self.onfetch = event => {
const req = event.request;
2018-11-26 21:25:47 +00:00
if (req.method !== 'GET') return;
const url = new URL(req.url);
2018-12-18 21:55:46 +00:00
const dlmatch = DOWNLOAD_URL.exec(url.pathname);
2018-11-26 21:25:47 +00:00
if (dlmatch) {
event.respondWith(decryptStream(dlmatch[1]));
2019-09-05 22:57:07 +00:00
} else if (cacheable(url.pathname)) {
2018-11-26 21:25:47 +00:00
event.respondWith(cachedOrFetched(req));
2018-07-05 19:40:49 +00:00
}
};
2018-07-06 22:49:50 +00:00
self.onmessage = event => {
2018-07-11 23:52:46 +00:00
if (event.data.request === 'init') {
noSave = event.data.noSave;
const info = {
2018-07-18 23:39:14 +00:00
key: event.data.key,
2018-07-19 20:20:10 +00:00
nonce: event.data.nonce,
2018-07-11 23:52:46 +00:00
filename: event.data.filename,
requiresPassword: event.data.requiresPassword,
password: event.data.password,
url: event.data.url,
2018-07-13 18:13:09 +00:00
type: event.data.type,
2018-07-26 05:26:11 +00:00
manifest: event.data.manifest,
2018-07-12 22:32:07 +00:00
size: event.data.size,
progress: 0
2018-07-11 23:52:46 +00:00
};
map.set(event.data.id, info);
2018-07-10 00:00:19 +00:00
event.ports[0].postMessage('file info received');
2018-07-11 23:52:46 +00:00
} else if (event.data.request === 'progress') {
const file = map.get(event.data.id);
2018-07-18 23:39:14 +00:00
if (!file) {
2018-07-10 00:00:19 +00:00
event.ports[0].postMessage({ error: 'cancelled' });
2018-07-09 22:39:06 +00:00
} else {
if (file.progress === file.size) {
map.delete(event.data.id);
}
2018-07-11 23:52:46 +00:00
event.ports[0].postMessage({ progress: file.progress });
2018-07-09 22:39:06 +00:00
}
2018-07-11 23:52:46 +00:00
} else if (event.data.request === 'cancel') {
const file = map.get(event.data.id);
2018-07-18 23:39:14 +00:00
if (file) {
if (file.download) {
file.download.cancel();
}
map.delete(event.data.id);
2018-07-09 22:39:06 +00:00
}
2018-07-10 00:00:19 +00:00
event.ports[0].postMessage('download cancelled');
2018-07-09 22:39:06 +00:00
}
2018-07-06 22:49:50 +00:00
};