From 4b026c618ed6b40a22c0bf601b45e1da96d5bc5e Mon Sep 17 00:00:00 2001 From: metamuffin Date: Mon, 10 Apr 2023 14:15:33 +0200 Subject: new (useless) sw --- client-web/source/download_stream.ts | 85 +++++++++++++++++++++++ client-web/source/index.ts | 4 +- client-web/source/keybinds.ts | 2 +- client-web/source/resource/file.ts | 2 +- client-web/source/sw/client.ts | 56 ++++++++++++++++ client-web/source/sw/download.ts | 36 ++++++++++ client-web/source/sw/download_stream.ts | 81 ---------------------- client-web/source/sw/init.ts | 35 ---------- client-web/source/sw/protocol.ts | 9 +++ client-web/source/sw/worker.ts | 115 +++++++++++++++++++++----------- 10 files changed, 266 insertions(+), 159 deletions(-) create mode 100644 client-web/source/download_stream.ts create mode 100644 client-web/source/sw/client.ts create mode 100644 client-web/source/sw/download.ts delete mode 100644 client-web/source/sw/download_stream.ts delete mode 100644 client-web/source/sw/init.ts create mode 100644 client-web/source/sw/protocol.ts (limited to 'client-web/source') diff --git a/client-web/source/download_stream.ts b/client-web/source/download_stream.ts new file mode 100644 index 0000000..5aafde1 --- /dev/null +++ b/client-web/source/download_stream.ts @@ -0,0 +1,85 @@ +/* + This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2022 metamuffin +*/ +/// +import { log } from "./logger.ts" +import { send_sw_message, SW_ENABLED } from "./sw/client.ts" + +function FallbackStreamDownload(size: number, filename?: string, progress?: (position: number) => void) { + log({ scope: "*", warn: true }, "downloading to memory because serviceworker is not available") + let position = 0 + let buffer = new Uint8Array(size) + return { + close() { + const a = document.createElement("a") + a.href = URL.createObjectURL(new Blob([buffer], { type: "text/plain" })) + a.download = filename ?? "file" + a.click() + }, + abort() { buffer = new Uint8Array(); /* have fun gc */ }, + write(chunk: Blob) { + const reader = new FileReader(); + reader.onload = function (event) { + const arr = new Uint8Array(event.target!.result as ArrayBuffer); + for (let i = 0; i < arr.length; i++, position++) { + buffer[position] = arr[i] + } + if (progress) progress(position) + }; + reader.readAsArrayBuffer(chunk); + } + } +} + +export function StreamDownload({ size, filename, cancel, progress }: { + size: number, + filename: string, + cancel: () => void, + progress: (position: number) => void +}) { + if (!SW_ENABLED) FallbackStreamDownload(size, filename, progress) + let position = 0 + + // the sw will handle this download + const path = `/download/${encodeURIComponent(filename ?? "file")}` + + const { port1, port2 } = new MessageChannel() + send_sw_message({ download: { path, size } }, [port2]) + + const a = document.createElement("a") + a.href = path + a.download = filename ?? "file" + a.target = "_blank" + // TODO this delay is part of a race condition btw + setTimeout(() => { + a.click() + }, 100) + + port1.onmessage = ev => { + if (ev.data.abort) { + cancel() + port1.close() + } + } + + return { + close() { + port1.postMessage("end") + }, + abort() { + port1.postMessage("abort") + }, + write(chunk: Blob) { + const reader = new FileReader(); + reader.onload = function (event) { + const arr = new Uint8Array(event.target!.result as ArrayBuffer); + port1.postMessage(arr) + position += arr.length + if (progress) progress(position) + }; + reader.readAsArrayBuffer(chunk); + } + } +} diff --git a/client-web/source/index.ts b/client-web/source/index.ts index c92ca2b..8da99ea 100644 --- a/client-web/source/index.ts +++ b/client-web/source/index.ts @@ -5,7 +5,7 @@ */ /// -import { init_serviceworker } from "./sw/init.ts"; +import { init_serviceworker } from "./sw/client.ts"; import { esection, OVERLAYS } from "./helper.ts"; import { setup_keybinds } from "./keybinds.ts"; import { log, LOGGER_CONTAINER } from "./logger.ts" @@ -65,7 +65,7 @@ export async function main() { if (!globalThis.RTCPeerConnection) return log({ scope: "webrtc", error: true }, "WebRTC not supported.") if (!globalThis.isSecureContext) log({ scope: "*", warn: true }, "This page is not in a 'Secure Context'") if (!globalThis.crypto.subtle) return log({ scope: "crypto", error: true }, "SubtleCrypto not availible") - if (!globalThis.navigator.serviceWorker) log({ scope: "*", warn: true }, "Your browser does not support the Service Worker API, some features dont work without it.") + if (!globalThis.navigator.serviceWorker) log({ scope: "*", warn: true }, "Your browser does not support the Service Worker API, forced automatic updates are unavoidable.") if (room_secret.length < 8) log({ scope: "crypto", warn: true }, "Room name is very short. e2ee is insecure!") if (room_secret.length == 0) return window.location.href = "/" // send them back to the start page if (PREFS.warn_redirect) log({ scope: "crypto", warn: true }, "You were redirected from the old URL format. The server knows the room secret now - e2ee is insecure!") diff --git a/client-web/source/keybinds.ts b/client-web/source/keybinds.ts index d096501..28bd263 100644 --- a/client-web/source/keybinds.ts +++ b/client-web/source/keybinds.ts @@ -7,7 +7,7 @@ import { create_camera_res, create_mic_res, create_screencast_res } from "./resource/track.ts"; import { Room } from "./room.ts" -import { update_serviceworker } from "./sw/init.ts"; +import { update_serviceworker } from "./sw/client.ts"; export function setup_keybinds(room: Room) { // let command_mode = false diff --git a/client-web/source/resource/file.ts b/client-web/source/resource/file.ts index 38bc566..f577580 100644 --- a/client-web/source/resource/file.ts +++ b/client-web/source/resource/file.ts @@ -7,7 +7,7 @@ import { display_filesize, ebutton, ediv, espan, sleep } from "../helper.ts"; import { log } from "../logger.ts"; -import { StreamDownload } from "../sw/download_stream.ts"; +import { StreamDownload } from "../download_stream.ts"; import { RemoteUser } from "../user/remote.ts"; import { LocalResource, ResourceHandlerDecl } from "./mod.ts"; diff --git a/client-web/source/sw/client.ts b/client-web/source/sw/client.ts new file mode 100644 index 0000000..1396826 --- /dev/null +++ b/client-web/source/sw/client.ts @@ -0,0 +1,56 @@ +/* + This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2022 metamuffin +*/ +/// + +import { log } from "../logger.ts" +import { SWMessage } from "./protocol.ts" + +export let SW_ENABLED = false + +export async function init_serviceworker() { + let reg = await globalThis.navigator.serviceWorker.getRegistration() + if (reg) { + log("sw", "service worker already installed") + SW_ENABLED = true + } else { + log("sw", "registering service worker") + await globalThis.navigator.serviceWorker.register("/sw.js", { scope: "/", type: "module" }) + log("sw", "worker installed") + reg = await globalThis.navigator.serviceWorker.getRegistration(); + if (!reg) throw new Error("we just registered the sw!?"); + SW_ENABLED = !!reg + } + start_handler() + log("sw", "checking for updates") + send_sw_message({ check_version: true }) +} + +export async function send_sw_message(message: SWMessage, transfer?: Transferable[]) { + const reg = await globalThis.navigator.serviceWorker.getRegistration(); + if (!reg) throw new Error("no sw"); + if (!reg.active) throw new Error("no sw"); + if (transfer) reg.active.postMessage(message, transfer) + else reg.active.postMessage(message, transfer) +} + +export async function update_serviceworker() { + const regs = await globalThis.navigator.serviceWorker.getRegistrations() + for (const r of regs) await r.unregister() + log("sw", "cleared all workers") + setTimeout(() => window.location.reload(), 500) +} + +function start_handler() { + globalThis.navigator.serviceWorker.addEventListener("message", event => { + const message: SWMessage = event.data; + if (message.version_info) { + log("sw", JSON.stringify(message.version_info)) + } + if (message.updated) { + log("*", "updated") + } + }) +} diff --git a/client-web/source/sw/download.ts b/client-web/source/sw/download.ts new file mode 100644 index 0000000..02c5e1a --- /dev/null +++ b/client-web/source/sw/download.ts @@ -0,0 +1,36 @@ +export const streams = new Map() + +export function handle_download_request(path: string, event: FetchEvent) { + const stream = streams.get(path) + if (stream) { + streams.delete(path) + console.log(`-> stream response`); + event.respondWith( + new Response( + stream.readable, + { + headers: new Headers({ + "content-type": "application/octet-stream; charset=utf-8", // TODO transmit and set accordingly + "content-security-policy": "default-src 'none'", + "content-length": `${stream.size}`, + }) + } + ) + ) + } + event.respondWith(new Response("download failed", { status: 400, headers: new Headers({ "content-type": "text/plain" }) })) +} + +export function port_to_readable(port: MessagePort): ReadableStream { + return new ReadableStream({ + start(controller) { + console.log("ReadableStream started"); + port.onmessage = event => { + if (event.data === "end") controller.close() + else if (event.data === "abort") controller.error("aborted") + else controller.enqueue(event.data) + } + }, + cancel() { console.log("ReadableStream cancelled"); port.postMessage({ abort: true }) }, + }) +} diff --git a/client-web/source/sw/download_stream.ts b/client-web/source/sw/download_stream.ts deleted file mode 100644 index 35a3c7e..0000000 --- a/client-web/source/sw/download_stream.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* - This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2022 metamuffin -*/ -/// -import { log } from "../logger.ts" -import { SW } from "./init.ts" - -function FallbackStreamDownload(size: number, filename?: string, progress?: (position: number) => void) { - log({ scope: "*", warn: true }, "downloading to memory because serviceworker is not available") - let position = 0 - let buffer = new Uint8Array(size) - return { - close() { - const a = document.createElement("a") - a.href = URL.createObjectURL(new Blob([buffer], { type: "text/plain" })) - a.download = filename ?? "file" - a.click() - }, - abort() { buffer = new Uint8Array(); /* have fun gc */ }, - write(chunk: Blob) { - const reader = new FileReader(); - reader.onload = function (event) { - const arr = new Uint8Array(event.target!.result as ArrayBuffer); - for (let i = 0; i < arr.length; i++, position++) { - buffer[position] = arr[i] - } - if (progress) progress(position) - }; - reader.readAsArrayBuffer(chunk); - } - } -} - -export function StreamDownload({ size, filename, cancel, progress }: { - size: number, - filename: string, - cancel: () => void, - progress: (position: number) => void -}) { - if (!SW) FallbackStreamDownload(size, filename, progress) - let position = 0 - - const path = `/download/${encodeURIComponent(filename ?? "file")}` - - const { port1, port2 } = new MessageChannel() - SW!.postMessage({ path, size }, [port2]) - - const a = document.createElement("a") - a.href = path - a.download = filename ?? "file" - a.target = "_blank" - a.click() - - port1.onmessage = ev => { - if (ev.data.abort) { - cancel() - port1.close() - } - } - - return { - close() { - port1.postMessage("end") - }, - abort() { - port1.postMessage("abort") - }, - write(chunk: Blob) { - const reader = new FileReader(); - reader.onload = function (event) { - const arr = new Uint8Array(event.target!.result as ArrayBuffer); - port1.postMessage(arr) - position += arr.length - if (progress) progress(position) - }; - reader.readAsArrayBuffer(chunk); - } - } -} diff --git a/client-web/source/sw/init.ts b/client-web/source/sw/init.ts deleted file mode 100644 index d082038..0000000 --- a/client-web/source/sw/init.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - This file is part of keks-meet (https://codeberg.org/metamuffin/keks-meet) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2022 metamuffin -*/ -/// - -import { log } from "../logger.ts" - -export let SW: ServiceWorker | undefined -export async function init_serviceworker() { - let reg = await globalThis.navigator.serviceWorker.getRegistration() - if (reg) { - log("sw", "service worker already installed") - } else { - log("sw", "registering service worker") - await globalThis.navigator.serviceWorker.register("/sw.js", { scope: "/", type: "module" }) - log("sw", "worker installed") - reg = await globalThis.navigator.serviceWorker.getRegistration(); - if (!reg) throw new Error("we just registered the sw!?"); - } - const i = setInterval(() => { - if (reg!.active) { - SW = reg!.active - clearInterval(i) - } - }, 100) -} - -export async function update_serviceworker() { - const regs = await globalThis.navigator.serviceWorker.getRegistrations() - for (const r of regs) await r.unregister() - log("sw", "cleared all workers") - setTimeout(() => window.location.reload(), 500) -} diff --git a/client-web/source/sw/protocol.ts b/client-web/source/sw/protocol.ts new file mode 100644 index 0000000..7cca220 --- /dev/null +++ b/client-web/source/sw/protocol.ts @@ -0,0 +1,9 @@ + +export interface SWMessage { + download?: { path: string, size: number } + check_version?: boolean, + update?: boolean, + + version_info?: { installed_version: string, available_version: string } + updated?: boolean +} diff --git a/client-web/source/sw/worker.ts b/client-web/source/sw/worker.ts index a06f3db..442f08a 100644 --- a/client-web/source/sw/worker.ts +++ b/client-web/source/sw/worker.ts @@ -4,9 +4,12 @@ Copyright (C) 2022 metamuffin */ /// - /// /// + +import { handle_download_request, port_to_readable, streams } from "./download.ts"; +import { SWMessage } from "./protocol.ts"; + declare const self: ServiceWorkerGlobalScope; export { }; console.log("hello from the keks-meet service worker"); @@ -25,53 +28,87 @@ self.addEventListener("unload", () => { console.log("unload") }) -const streams = new Map() - -self.addEventListener("message", ev => { - const { path, size } = ev.data, port = ev.ports[0] - const readable = port_to_readable(port) - streams.set(path, { readable, size }) +self.addEventListener("message", async ev => { + const message: SWMessage = ev.data; + console.log("incoming message", message); + if (message.download) { + const { path, size } = message.download, port = ev.ports[0] + const readable = port_to_readable(port) + streams.set(path, { readable, size }) + } + if (message.check_version) { + broadcast_response(await check_for_updates()) + } + if (message.update) { + broadcast_response(await update()) + } }) -function port_to_readable(port: MessagePort): ReadableStream { - return new ReadableStream({ - start(controller) { - console.log("ReadableStream started"); - port.onmessage = event => { - if (event.data === "end") controller.close() - else if (event.data === "abort") controller.error("aborted") - else controller.enqueue(event.data) - } - }, - cancel() { console.log("ReadableStream cancelled"); port.postMessage({ abort: true }) }, - }) +async function broadcast_response(message: SWMessage) { + const clients = await self.clients.matchAll({}) + console.log(clients); + clients.forEach(c => c.postMessage(message)) } -self.addEventListener("fetch", event => { +self.addEventListener("fetch", async event => { const { request } = event; if (!request.url.startsWith(self.origin)) return const path = request.url.substring(self.origin.length) console.log(request.method, path); - const stream = streams.get(path) - if (stream) { - streams.delete(path) - console.log(`-> stream response`); - return event.respondWith( - new Response( - stream.readable, - { - headers: new Headers({ - "content-type": "application/octet-stream; charset=utf-8", // TODO transmit and set accordingly - "content-security-policy": "default-src 'none'", - "content-length": `${stream.size}`, - }) - } - ) - ) - } - + if (path.startsWith("/download")) return handle_download_request(path, event) + if (path.startsWith("/signaling")) return event.respondWith(fetch(request)) if (path == "/swtest") return event.respondWith(new Response("works!", { headers: new Headers({ "content-type": "text/plain" }) })) - event.respondWith(fetch(request)) + const cache = await caches.open("v1") + // const cached = await cache.match(request) + // if (cached) { + // console.log("-> cached"); + // return cached + // } + console.log("-> forwarding to the server"); + const response = await fetch(request); + cache.put(request, response.clone()) + event.respondWith(response.clone()) }) + + +async function update(): Promise { + console.log("updating..."); + await caches.delete("v1") + await Promise.all( + [ + "/", + "/room", + "/config.json", + "/assets/bundle.js", + "/favicon.ico" + ] + .map(cache_preload) + ) + return { updated: true } +} + +async function cache_preload(path: string) { + const cache = await caches.open("v1") + const req = new Request(path) + const res = await fetch(req) + await cache.put(req, res) +} + +async function check_for_updates(): Promise { + console.log("checking for updates"); + const cache = await caches.open("v1") + const res = await fetch("/version") + const res2 = await cache.match(new Request("/version")); + const available_version = await res.text() + const installed_version = res2 ? await res2.text() : "none"; + console.log({ available_version, installed_version }); + return { + version_info: { + available_version, + installed_version + } + } +} + -- cgit v1.2.3-70-g09d2