Loading apps/demo/.gitignore +7 −0 Original line number Diff line number Diff line Loading @@ -32,3 +32,10 @@ yarn-error.log* # vercel .vercel public/manifest.json public/fallback-*.js public/sw.js public/sw.js.map public/workbox-*.js public/workbox-*.js.map apps/demo/next.config.js +61 −53 Original line number Diff line number Diff line Loading @@ -7,9 +7,16 @@ const withMDX = require("@next/mdx")({ }, }); const basePath = process.env.BASE_PATH ?? ""; const withPwa = require("@yume-chan/next-pwa")({ dest: "public", }); /** @type {import('next').NextConfig} */ module.exports = withMDX({ basePath: process.env.BASE_PATH || "", module.exports = withPwa( withMDX({ basePath, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], reactStrictMode: false, productionBrowserSourceMaps: true, Loading @@ -18,7 +25,7 @@ module.exports = withMDX({ esmExternals: "loose", }, publicRuntimeConfig: { basePath: process.env.BASE_PATH || "", basePath, }, webpack(config, options) { config.module.rules.push({ Loading Loading @@ -63,4 +70,5 @@ module.exports = withMDX({ }, ]; }, }); }) ); apps/demo/package.json +2 −1 Original line number Diff line number Diff line Loading @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { "postinstall": "fetch-scrcpy-server 1.25", "postinstall": "fetch-scrcpy-server 1.25 && node scripts/manifest.mjs", "dev": "next dev", "build": "next build", "start": "next start", Loading Loading @@ -49,6 +49,7 @@ "@next/mdx": "^13.2.4", "@types/node": "^18.15.0", "@types/react": "18.0.28", "@yume-chan/next-pwa": "^5.6.0", "eslint": "^8.36.0", "eslint-config-next": "13.2.4", "prettier": "^2.8.4", Loading apps/demo/public/StreamSaver/mitm.html +152 −145 Original line number Diff line number Diff line Loading @@ -38,7 +38,12 @@ let scope = '' function registerWorker() { return navigator.serviceWorker.getRegistration('./').then(swReg => { return swReg || navigator.serviceWorker.register('sw.js', { scope: './' }) let baseUrl = location.protocol + "//" + location.host + location.pathname; baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf("/")); if (swReg.scope === baseUrl) { return swReg; } return navigator.serviceWorker.register('sw.js', { scope: './' }) }).then(swReg => { const swRegTmp = swReg.installing || swReg.waiting Loading @@ -63,11 +68,13 @@ function onMessage (event) { // It's important to have a messageChannel, don't want to interfere // with other simultaneous downloads if (!ports || !ports.length) { throw new TypeError("[StreamSaver] You didn't send a messageChannel") console.error("[StreamSaver] You didn't send a messageChannel") return; } if (typeof data !== 'object') { throw new TypeError("[StreamSaver] You didn't send a object") console.error("[StreamSaver] You didn't send a object") return; } // the default public service worker for StreamSaver is shared among others. Loading apps/demo/public/StreamSaver/sw.js +148 −120 Original line number Diff line number Diff line /* global self ReadableStream Response */ self.addEventListener('install', () => { self.skipWaiting() }) self.addEventListener('activate', event => { event.waitUntil(self.clients.claim()) }) const map = new Map() self.addEventListener("install", () => { self.skipWaiting(); }); console.log("updated"); self.addEventListener("activate", (event) => { const url = serviceWorker.scriptURL; const baseUrl = url.substring(0, url.lastIndexOf("/")); event.waitUntil( (async () => { const cache = await caches.open("StreamSaver"); await cache.add(baseUrl + "/mitm.html"); await clients.claim(); })() ); }); const map = new Map(); // This should be called once per download // Each event has a dataChannel that the data will be piped through self.onmessage = event => { self.onmessage = (event) => { // We send a heartbeat every x second to keep the // service worker alive if a transferable stream is not sent if (event.data === 'ping') { return if (event.data === "ping") { return; } const data = event.data const downloadUrl = data.url || Math.random() + '/' + (typeof data === 'string' ? data : data.filename) const port = event.ports[0] const metadata = new Array(3) // [stream, data, port] const data = event.data; const downloadUrl = data.url || Math.random() + "/" + (typeof data === "string" ? data : data.filename); const port = event.ports[0]; const metadata = new Array(3); // [stream, data, port] metadata[1] = data metadata[2] = port metadata[1] = data; metadata[2] = port; // Note to self: // old streamsaver v1.2.0 might still use `readableStream`... // but v2.0.0 will always transfer the stream through MessageChannel #94 if (event.data.readableStream) { metadata[0] = event.data.readableStream metadata[0] = event.data.readableStream; } else if (event.data.transferringReadable) { port.onmessage = evt => { port.onmessage = null metadata[0] = evt.data.readableStream } port.onmessage = (evt) => { port.onmessage = null; metadata[0] = evt.data.readableStream; }; } else { metadata[0] = createStream(port) metadata[0] = createStream(port); } map.set(downloadUrl, metadata) port.postMessage({ download: downloadUrl }) } map.set(downloadUrl, metadata); port.postMessage({ download: downloadUrl }); }; function createStream(port) { // ReadableStream is only supported by chrome 52 Loading @@ -51,80 +63,96 @@ function createStream (port) { start(controller) { // When we receive data on the messageChannel, we write port.onmessage = ({ data }) => { if (data === 'end') { return controller.close() if (data === "end") { return controller.close(); } if (data === 'abort') { controller.error('Aborted the download') return if (data === "abort") { controller.error("Aborted the download"); return; } controller.enqueue(data) } controller.enqueue(data); }; }, cancel(reason) { console.log('user aborted', reason) port.postMessage({ abort: true }) } }) console.log("user aborted", reason); port.postMessage({ abort: true }); }, }); } self.onfetch = event => { const url = event.request.url self.onfetch = async (event) => { event.respondWith( (async () => { const url = event.request.url; // this only works for Firefox if (url.endsWith('/ping')) { return event.respondWith(new Response('pong')) const cache = await caches.open("StreamSaver"); const response = await cache.match(event.request); if (response) { return response; } const hijacke = map.get(url) if (!hijacke) return null // this only works for Firefox if (url.endsWith("/ping")) { return new Response("pong"); } const [ stream, data, port ] = hijacke const hijacked = map.get(url); if (!hijacked) return null; map.delete(url); map.delete(url) const [stream, data, port] = hijacked; // Not comfortable letting any user control all headers // so we only copy over the length & disposition const responseHeaders = new Headers({ 'Content-Type': 'application/octet-stream; charset=utf-8', "Content-Type": "application/octet-stream; charset=utf-8", // To be on the safe side, The link can be opened in a iframe. // but octet-stream should stop it. 'Content-Security-Policy': "default-src 'none'", 'X-Content-Security-Policy': "default-src 'none'", 'Cross-Origin-Embedder-Policy': 'require-corp', 'X-WebKit-CSP': "default-src 'none'", 'X-XSS-Protection': '1; mode=block' }) let headers = new Headers(data.headers || {}) if (headers.has('Content-Length')) { responseHeaders.set('Content-Length', headers.get('Content-Length')) "Content-Security-Policy": "default-src 'none'", "X-Content-Security-Policy": "default-src 'none'", "Cross-Origin-Embedder-Policy": "require-corp", "X-WebKit-CSP": "default-src 'none'", "X-XSS-Protection": "1; mode=block", }); const headers = new Headers(data.headers || {}); if (headers.has("Content-Length")) { responseHeaders.set( "Content-Length", headers.get("Content-Length") ); } if (headers.has('Content-Disposition')) { responseHeaders.set('Content-Disposition', headers.get('Content-Disposition')) if (headers.has("Content-Disposition")) { responseHeaders.set( "Content-Disposition", headers.get("Content-Disposition") ); } // data, data.filename and size should not be used anymore if (data.size) { console.warn('Depricated') responseHeaders.set('Content-Length', data.size) console.warn("Deprecated"); responseHeaders.set("Content-Length", data.size); } let fileName = typeof data === 'string' ? data : data.filename const fileName = typeof data === "string" ? data : data.filename; if (fileName) { console.warn('Depricated') console.warn("Deprecated"); // Make filename RFC5987 compatible fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A') responseHeaders.set('Content-Disposition', "attachment; filename*=UTF-8''" + fileName) fileName = encodeURIComponent(fileName) .replace(/['()]/g, escape) .replace(/\*/g, "%2A"); responseHeaders.set( "Content-Disposition", "attachment; filename*=UTF-8''" + fileName ); } event.respondWith(new Response(stream, { headers: responseHeaders })) port.postMessage({ debug: 'Download started' }) } port.postMessage({ debug: "Download started" }); return new Response(stream, { headers: responseHeaders }); })() ); }; Loading
apps/demo/.gitignore +7 −0 Original line number Diff line number Diff line Loading @@ -32,3 +32,10 @@ yarn-error.log* # vercel .vercel public/manifest.json public/fallback-*.js public/sw.js public/sw.js.map public/workbox-*.js public/workbox-*.js.map
apps/demo/next.config.js +61 −53 Original line number Diff line number Diff line Loading @@ -7,9 +7,16 @@ const withMDX = require("@next/mdx")({ }, }); const basePath = process.env.BASE_PATH ?? ""; const withPwa = require("@yume-chan/next-pwa")({ dest: "public", }); /** @type {import('next').NextConfig} */ module.exports = withMDX({ basePath: process.env.BASE_PATH || "", module.exports = withPwa( withMDX({ basePath, pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], reactStrictMode: false, productionBrowserSourceMaps: true, Loading @@ -18,7 +25,7 @@ module.exports = withMDX({ esmExternals: "loose", }, publicRuntimeConfig: { basePath: process.env.BASE_PATH || "", basePath, }, webpack(config, options) { config.module.rules.push({ Loading Loading @@ -63,4 +70,5 @@ module.exports = withMDX({ }, ]; }, }); }) );
apps/demo/package.json +2 −1 Original line number Diff line number Diff line Loading @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { "postinstall": "fetch-scrcpy-server 1.25", "postinstall": "fetch-scrcpy-server 1.25 && node scripts/manifest.mjs", "dev": "next dev", "build": "next build", "start": "next start", Loading Loading @@ -49,6 +49,7 @@ "@next/mdx": "^13.2.4", "@types/node": "^18.15.0", "@types/react": "18.0.28", "@yume-chan/next-pwa": "^5.6.0", "eslint": "^8.36.0", "eslint-config-next": "13.2.4", "prettier": "^2.8.4", Loading
apps/demo/public/StreamSaver/mitm.html +152 −145 Original line number Diff line number Diff line Loading @@ -38,7 +38,12 @@ let scope = '' function registerWorker() { return navigator.serviceWorker.getRegistration('./').then(swReg => { return swReg || navigator.serviceWorker.register('sw.js', { scope: './' }) let baseUrl = location.protocol + "//" + location.host + location.pathname; baseUrl = baseUrl.substring(0, baseUrl.lastIndexOf("/")); if (swReg.scope === baseUrl) { return swReg; } return navigator.serviceWorker.register('sw.js', { scope: './' }) }).then(swReg => { const swRegTmp = swReg.installing || swReg.waiting Loading @@ -63,11 +68,13 @@ function onMessage (event) { // It's important to have a messageChannel, don't want to interfere // with other simultaneous downloads if (!ports || !ports.length) { throw new TypeError("[StreamSaver] You didn't send a messageChannel") console.error("[StreamSaver] You didn't send a messageChannel") return; } if (typeof data !== 'object') { throw new TypeError("[StreamSaver] You didn't send a object") console.error("[StreamSaver] You didn't send a object") return; } // the default public service worker for StreamSaver is shared among others. Loading
apps/demo/public/StreamSaver/sw.js +148 −120 Original line number Diff line number Diff line /* global self ReadableStream Response */ self.addEventListener('install', () => { self.skipWaiting() }) self.addEventListener('activate', event => { event.waitUntil(self.clients.claim()) }) const map = new Map() self.addEventListener("install", () => { self.skipWaiting(); }); console.log("updated"); self.addEventListener("activate", (event) => { const url = serviceWorker.scriptURL; const baseUrl = url.substring(0, url.lastIndexOf("/")); event.waitUntil( (async () => { const cache = await caches.open("StreamSaver"); await cache.add(baseUrl + "/mitm.html"); await clients.claim(); })() ); }); const map = new Map(); // This should be called once per download // Each event has a dataChannel that the data will be piped through self.onmessage = event => { self.onmessage = (event) => { // We send a heartbeat every x second to keep the // service worker alive if a transferable stream is not sent if (event.data === 'ping') { return if (event.data === "ping") { return; } const data = event.data const downloadUrl = data.url || Math.random() + '/' + (typeof data === 'string' ? data : data.filename) const port = event.ports[0] const metadata = new Array(3) // [stream, data, port] const data = event.data; const downloadUrl = data.url || Math.random() + "/" + (typeof data === "string" ? data : data.filename); const port = event.ports[0]; const metadata = new Array(3); // [stream, data, port] metadata[1] = data metadata[2] = port metadata[1] = data; metadata[2] = port; // Note to self: // old streamsaver v1.2.0 might still use `readableStream`... // but v2.0.0 will always transfer the stream through MessageChannel #94 if (event.data.readableStream) { metadata[0] = event.data.readableStream metadata[0] = event.data.readableStream; } else if (event.data.transferringReadable) { port.onmessage = evt => { port.onmessage = null metadata[0] = evt.data.readableStream } port.onmessage = (evt) => { port.onmessage = null; metadata[0] = evt.data.readableStream; }; } else { metadata[0] = createStream(port) metadata[0] = createStream(port); } map.set(downloadUrl, metadata) port.postMessage({ download: downloadUrl }) } map.set(downloadUrl, metadata); port.postMessage({ download: downloadUrl }); }; function createStream(port) { // ReadableStream is only supported by chrome 52 Loading @@ -51,80 +63,96 @@ function createStream (port) { start(controller) { // When we receive data on the messageChannel, we write port.onmessage = ({ data }) => { if (data === 'end') { return controller.close() if (data === "end") { return controller.close(); } if (data === 'abort') { controller.error('Aborted the download') return if (data === "abort") { controller.error("Aborted the download"); return; } controller.enqueue(data) } controller.enqueue(data); }; }, cancel(reason) { console.log('user aborted', reason) port.postMessage({ abort: true }) } }) console.log("user aborted", reason); port.postMessage({ abort: true }); }, }); } self.onfetch = event => { const url = event.request.url self.onfetch = async (event) => { event.respondWith( (async () => { const url = event.request.url; // this only works for Firefox if (url.endsWith('/ping')) { return event.respondWith(new Response('pong')) const cache = await caches.open("StreamSaver"); const response = await cache.match(event.request); if (response) { return response; } const hijacke = map.get(url) if (!hijacke) return null // this only works for Firefox if (url.endsWith("/ping")) { return new Response("pong"); } const [ stream, data, port ] = hijacke const hijacked = map.get(url); if (!hijacked) return null; map.delete(url); map.delete(url) const [stream, data, port] = hijacked; // Not comfortable letting any user control all headers // so we only copy over the length & disposition const responseHeaders = new Headers({ 'Content-Type': 'application/octet-stream; charset=utf-8', "Content-Type": "application/octet-stream; charset=utf-8", // To be on the safe side, The link can be opened in a iframe. // but octet-stream should stop it. 'Content-Security-Policy': "default-src 'none'", 'X-Content-Security-Policy': "default-src 'none'", 'Cross-Origin-Embedder-Policy': 'require-corp', 'X-WebKit-CSP': "default-src 'none'", 'X-XSS-Protection': '1; mode=block' }) let headers = new Headers(data.headers || {}) if (headers.has('Content-Length')) { responseHeaders.set('Content-Length', headers.get('Content-Length')) "Content-Security-Policy": "default-src 'none'", "X-Content-Security-Policy": "default-src 'none'", "Cross-Origin-Embedder-Policy": "require-corp", "X-WebKit-CSP": "default-src 'none'", "X-XSS-Protection": "1; mode=block", }); const headers = new Headers(data.headers || {}); if (headers.has("Content-Length")) { responseHeaders.set( "Content-Length", headers.get("Content-Length") ); } if (headers.has('Content-Disposition')) { responseHeaders.set('Content-Disposition', headers.get('Content-Disposition')) if (headers.has("Content-Disposition")) { responseHeaders.set( "Content-Disposition", headers.get("Content-Disposition") ); } // data, data.filename and size should not be used anymore if (data.size) { console.warn('Depricated') responseHeaders.set('Content-Length', data.size) console.warn("Deprecated"); responseHeaders.set("Content-Length", data.size); } let fileName = typeof data === 'string' ? data : data.filename const fileName = typeof data === "string" ? data : data.filename; if (fileName) { console.warn('Depricated') console.warn("Deprecated"); // Make filename RFC5987 compatible fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A') responseHeaders.set('Content-Disposition', "attachment; filename*=UTF-8''" + fileName) fileName = encodeURIComponent(fileName) .replace(/['()]/g, escape) .replace(/\*/g, "%2A"); responseHeaders.set( "Content-Disposition", "attachment; filename*=UTF-8''" + fileName ); } event.respondWith(new Response(stream, { headers: responseHeaders })) port.postMessage({ debug: 'Download started' }) } port.postMessage({ debug: "Download started" }); return new Response(stream, { headers: responseHeaders }); })() ); };