From caf9ce943f55820a5a419ef7fba31bbbf9a7f21b Mon Sep 17 00:00:00 2001 From: Steve Seguin Date: Sun, 13 Feb 2022 03:08:34 -0500 Subject: [PATCH] Add files via upload --- thirdparty/StreamSaver.js | 317 +++++++++++++++++++++++++++++++++++++- thirdparty/mitm.html | 165 +++++++++++++++++++- thirdparty/sw.js | 134 +++++++++++++++- 3 files changed, 605 insertions(+), 11 deletions(-) diff --git a/thirdparty/StreamSaver.js b/thirdparty/StreamSaver.js index 45ab918..2f5bd10 100644 --- a/thirdparty/StreamSaver.js +++ b/thirdparty/StreamSaver.js @@ -1,4 +1,313 @@ -/* global chrome location ReadableStream define MessageChannel TransformStream */ -// https://github.com/jimmywarting/StreamSaver.js -// MIT License -((e,t)=>{"undefined"!=typeof module?module.exports=t():"function"==typeof define&&"object"==typeof define.amd?define(t):this.streamSaver=t()})(0,()=>{"use strict";const e="object"==typeof window?window:this;e.HTMLElement||console.warn("streamsaver is meant to run on browsers main thread");let t=null,a=!1;const r=e.WebStreamsPolyfill||{},n=e.isSecureContext;let o=/constructor/i.test(e.HTMLElement)||!!e.safari||!!e.WebKitPoint;const s=n||"MozAppearance"in document.documentElement.style?"iframe":"navigate",i={createWriteStream:function(r,m,d){let c={size:null,pathname:null,writableStrategy:void 0,readableStrategy:void 0},p=0,f=null,u=null,w=null;Number.isFinite(m)?([d,m]=[m,d],console.warn("[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream"),c.size=d,c.writableStrategy=m):m&&m.highWaterMark?(console.warn("[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream"),c.size=d,c.writableStrategy=m):c=m||{};if(!o){t||(t=n?l(i.mitm):function(t){const a=document.createDocumentFragment(),r={frame:e.open(t,"popup","width=200,height=100"),loaded:!1,isIframe:!1,isPopup:!0,remove(){r.frame.close()},addEventListener(...e){a.addEventListener(...e)},dispatchEvent(...e){a.dispatchEvent(...e)},removeEventListener(...e){a.removeEventListener(...e)},postMessage(...e){r.frame.postMessage(...e)}},n=t=>{t.source===r.frame&&(r.loaded=!0,e.removeEventListener("message",n),r.dispatchEvent(new Event("load")))};return e.addEventListener("message",n),r}(i.mitm)),u=new MessageChannel,r=encodeURIComponent(r.replace(/\//g,":")).replace(/['()]/g,escape).replace(/\*/g,"%2A");const o={transferringReadable:a,pathname:c.pathname||Math.random().toString().slice(-6)+"/"+r,headers:{"Content-Type":"application/octet-stream; charset=utf-8","Content-Disposition":"attachment; filename*=UTF-8''"+r}};c.size&&(o.headers["Content-Length"]=c.size);const m=[o,"*",[u.port2]];if(a){const e="iframe"===s?void 0:{transform(e,t){if(!(e instanceof Uint8Array))throw new TypeError("Can only wirte Uint8Arrays");p+=e.length,t.enqueue(e),f&&(location.href=f,f=null)},flush(){f&&(location.href=f)}},t=(w=new i.TransformStream(e,c.writableStrategy,c.readableStrategy)).readable;u.port1.postMessage({readableStream:t},[t])}u.port1.onmessage=(e=>{e.data.download&&("navigate"===s?(t.remove(),t=null,p?location.href=e.data.download:f=e.data.download):(t.isPopup&&(t.remove(),t=null,"iframe"===s&&l(i.mitm)),l(e.data.download)))}),t.loaded?t.postMessage(...m):t.addEventListener("load",()=>{t.postMessage(...m)},{once:!0})}let g=[];return!o&&w&&w.writable||new i.WritableStream({write(e){if(!(e instanceof Uint8Array))throw new TypeError("Can only wirte Uint8Arrays");o?g.push(e):(u.port1.postMessage(e),p+=e.length,f&&(location.href=f,f=null))},close(){if(o){const e=new Blob(g,{type:"application/octet-stream; charset=utf-8"}),t=document.createElement("a");t.href=URL.createObjectURL(e),t.download=r,t.click()}else u.port1.postMessage("end")},abort(){g=[],u.port1.postMessage("abort"),u.port1.onmessage=null,u.port1.close(),u.port2.close(),u=null}},c.writableStrategy)},WritableStream:e.WritableStream||r.WritableStream,supported:!0,version:{full:"2.0.5",major:2,minor:0,dot:5},mitm:"./thirdparty/mitm.html?version=2.0.0"};function l(e){if(!e)throw new Error("meh");const t=document.createElement("iframe");return t.hidden=!0,t.src=e,t.loaded=!1,t.name="iframe",t.isIframe=!0,t.postMessage=((...e)=>t.contentWindow.postMessage(...e)),t.addEventListener("load",()=>{t.loaded=!0},{once:!0}),document.body.appendChild(t),t}try{new Response(new ReadableStream),!n||"serviceWorker"in navigator||(o=!0)}catch(e){o=!0}return(e=>{try{e()}catch(e){}})(()=>{const{readable:e}=new TransformStream,t=new MessageChannel;t.port1.postMessage(e,[e]),t.port1.close(),t.port2.close(),a=!0,Object.defineProperty(i,"TransformStream",{configurable:!1,writable:!1,value:TransformStream})}),i}); \ No newline at end of file +/*! streamsaver. MIT License. Jimmy Wärting */ + +/* global chrome location ReadableStream define MessageChannel TransformStream */ + +;((name, definition) => { + typeof module !== 'undefined' + ? module.exports = definition() + : typeof define === 'function' && typeof define.amd === 'object' + ? define(definition) + : this[name] = definition() +})('streamSaver', () => { + 'use strict' + + const global = typeof window === 'object' ? window : this + if (!global.HTMLElement) console.warn('streamsaver is meant to run on browsers main thread') + + let mitmTransporter = null + let supportsTransferable = false + const test = fn => { try { fn() } catch (e) {} } + const ponyfill = global.WebStreamsPolyfill || {} + const isSecureContext = global.isSecureContext + // TODO: Must come up with a real detection test (#69) + let useBlobFallback = /constructor/i.test(global.HTMLElement) || !!global.safari || !!global.WebKitPoint + const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style + ? 'iframe' + : 'navigate' + + const streamSaver = { + createWriteStream, + WritableStream: global.WritableStream || ponyfill.WritableStream, + supported: true, + version: { full: '2.0.6', major: 2, minor: 0, dot: 6 }, + mitm: 'https://steveseguin.github.io/StreamSaver.js/mitm.html?version=2.0.6' + } + + /** + * create a hidden iframe and append it to the DOM (body) + * + * @param {string} src page to load + * @return {HTMLIFrameElement} page to load + */ + function makeIframe (src) { + if (!src) throw new Error('meh') + const iframe = document.createElement('iframe') + iframe.hidden = true + iframe.src = src + iframe.loaded = false + iframe.name = 'iframe' + iframe.isIframe = true + iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args) + iframe.addEventListener('load', () => { + iframe.loaded = true + }, { once: true }) + document.body.appendChild(iframe) + return iframe + } + + /** + * create a popup that simulates the basic things + * of what a iframe can do + * + * @param {string} src page to load + * @return {object} iframe like object + */ + function makePopup (src) { + const options = 'width=200,height=100' + const delegate = document.createDocumentFragment() + const popup = { + frame: global.open(src, 'popup', options), + loaded: false, + isIframe: false, + isPopup: true, + remove () { popup.frame.close() }, + addEventListener (...args) { delegate.addEventListener(...args) }, + dispatchEvent (...args) { delegate.dispatchEvent(...args) }, + removeEventListener (...args) { delegate.removeEventListener(...args) }, + postMessage (...args) { popup.frame.postMessage(...args) } + } + + const onReady = evt => { + if (evt.source === popup.frame) { + popup.loaded = true + global.removeEventListener('message', onReady) + popup.dispatchEvent(new Event('load')) + } + } + + global.addEventListener('message', onReady) + + return popup + } + + try { + // We can't look for service worker since it may still work on http + new Response(new ReadableStream()) + if (isSecureContext && !('serviceWorker' in navigator)) { + useBlobFallback = true + } + } catch (err) { + useBlobFallback = true + } + + test(() => { + // Transferable stream was first enabled in chrome v73 behind a flag + const { readable } = new TransformStream() + const mc = new MessageChannel() + mc.port1.postMessage(readable, [readable]) + mc.port1.close() + mc.port2.close() + supportsTransferable = true + // Freeze TransformStream object (can only work with native) + Object.defineProperty(streamSaver, 'TransformStream', { + configurable: false, + writable: false, + value: TransformStream + }) + }) + + function loadTransporter () { + if (!mitmTransporter) { + mitmTransporter = isSecureContext + ? makeIframe(streamSaver.mitm) + : makePopup(streamSaver.mitm) + } + } + + /** + * @param {string} filename filename that should be used + * @param {object} options [description] + * @param {number} size deprecated + * @return {WritableStream} + */ + function createWriteStream (filename, stopStream){ + let opts = { + size: null, + pathname: null, + writableStrategy: undefined, + readableStrategy: undefined + } + + let bytesWritten = 0 // by StreamSaver.js (not the service worker) + let downloadUrl = null + let channel = null + let ts = null + + if (!useBlobFallback) { + loadTransporter() + + channel = new MessageChannel() + + // Make filename RFC5987 compatible + filename = encodeURIComponent(filename.replace(/\//g, ':')) + .replace(/['()]/g, escape) + .replace(/\*/g, '%2A') + + const response = { + transferringReadable: supportsTransferable, + pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename, + headers: { + 'Content-Type': 'application/octet-stream; charset=utf-8', + 'Content-Disposition': "attachment; filename*=UTF-8''" + filename + } + } + + if (opts.size) { + response.headers['Content-Length'] = opts.size + } + + const args = [ response, '*', [ channel.port2 ] ] + + if (supportsTransferable) { + const transformer = downloadStrategy === 'iframe' ? undefined : { + // This transformer & flush method is only used by insecure context. + transform (chunk, controller) { + if (!(chunk instanceof Uint8Array)) { + throw new TypeError('Can only write Uint8Arrays') + } + bytesWritten += chunk.length + controller.enqueue(chunk) + + if (downloadUrl) { + location.href = downloadUrl + downloadUrl = null + } + }, + flush () { + if (downloadUrl) { + location.href = downloadUrl + } + } + } + ts = new streamSaver.TransformStream( + transformer, + opts.writableStrategy, + opts.readableStrategy + ) + const readableStream = ts.readable + + channel.port1.postMessage({ readableStream }, [ readableStream ]) + } + + channel.port1.onmessage = evt => { + // Service worker sent us a link that we should open. + if (evt.data.download) { + // Special treatment for popup... + if (downloadStrategy === 'navigate') { + mitmTransporter.remove() + mitmTransporter = null + if (bytesWritten) { + location.href = evt.data.download + } else { + downloadUrl = evt.data.download + } + } else { + if (mitmTransporter.isPopup) { + mitmTransporter.remove() + mitmTransporter = null + // Special case for firefox, they can keep sw alive with fetch + if (downloadStrategy === 'iframe') { + makeIframe(streamSaver.mitm) + } + } + + // We never remove this iframes b/c it can interrupt saving + makeIframe(evt.data.download) + } + } else if (evt.data.abort) { + stopStream(false, true); + chunks = [] + channel.port1.postMessage('abort') //send back so controller is aborted + channel.port1.onmessage = null + + setTimeout(function(channel){ + channel.port1.close() + channel.port2.close() + channel = null + },1300,channel); + } + } + + if (mitmTransporter.loaded) { + mitmTransporter.postMessage(...args) + } else { + mitmTransporter.addEventListener('load', () => { + mitmTransporter.postMessage(...args) + }, { once: true }) + } + } + + let chunks = [] + + return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({ + write (chunk) { + if (!(chunk instanceof Uint8Array)) { + throw new TypeError('Can only write Uint8Arrays') + } + if (useBlobFallback) { + // Safari... The new IE6 + // https://github.com/jimmywarting/StreamSaver.js/issues/69 + // + // even though it has everything it fails to download anything + // that comes from the service worker..! + chunks.push(chunk) + return + } + + // is called when a new chunk of data is ready to be written + // to the underlying sink. It can return a promise to signal + // success or failure of the write operation. The stream + // implementation guarantees that this method will be called + // only after previous writes have succeeded, and never after + // close or abort is called. + + // TODO: Kind of important that service worker respond back when + // it has been written. Otherwise we can't handle backpressure + // EDIT: Transferable streams solves this... + try { + channel.port1.postMessage(chunk) + } catch(e){ + + }; + bytesWritten += chunk.length + if (downloadUrl) { + location.href = downloadUrl + downloadUrl = null + } + }, + close () { + if (useBlobFallback) { + const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = filename + link.click() + } else { + channel.port1.postMessage('end') + } + }, + abort () { + chunks = [] + channel.port1.postMessage('abort') + channel.port1.onmessage = null + setTimeout(function(channel){ + channel.port1.close() + channel.port2.close() + channel = null + },1300,channel); + } + }, opts.writableStrategy) + } + + return streamSaver +}) diff --git a/thirdparty/mitm.html b/thirdparty/mitm.html index 835147a..04450a9 100644 --- a/thirdparty/mitm.html +++ b/thirdparty/mitm.html @@ -1,7 +1,166 @@ diff --git a/thirdparty/sw.js b/thirdparty/sw.js index 311f75d..35fd7e3 100644 --- a/thirdparty/sw.js +++ b/thirdparty/sw.js @@ -1,4 +1,130 @@ -/* global self ReadableStream Response */ -// https://github.com/jimmywarting/StreamSaver.js/blob/master/sw.js -// MIT License -self.addEventListener("install",()=>{self.skipWaiting()}),self.addEventListener("activate",e=>{e.waitUntil(self.clients.claim())});const map=new Map;function createStream(e){return new ReadableStream({start(t){e.onmessage=(({data:e})=>{if("end"===e)return t.close();"abort"!==e?t.enqueue(e):t.error("Aborted the download")})},cancel(){console.log("user aborted")}})}self.onmessage=(e=>{if("ping"===e.data)return;const t=e.data,n=t.url||self.registration.scope+Math.random()+"/"+("string"==typeof t?t:t.filename),a=e.ports[0],s=new Array(3);s[1]=t,s[2]=a,e.data.readableStream?s[0]=e.data.readableStream:e.data.transferringReadable?a.onmessage=(e=>{a.onmessage=null,s[0]=e.data.readableStream}):s[0]=createStream(a),map.set(n,s),a.postMessage({download:n})}),self.onfetch=(e=>{const t=e.request.url;if(t.endsWith("/ping"))return e.respondWith(new Response("pong"));const n=map.get(t);if(!n)return null;const[a,s,o]=n;map.delete(t);const r=new Headers({"Content-Type":"application/octet-stream; charset=utf-8","Content-Security-Policy":"default-src 'none'","X-Content-Security-Policy":"default-src 'none'","X-WebKit-CSP":"default-src 'none'","X-XSS-Protection":"1; mode=block"});let i=new Headers(s.headers||{});i.has("Content-Length")&&r.set("Content-Length",i.get("Content-Length")),i.has("Content-Disposition")&&r.set("Content-Disposition",i.get("Content-Disposition")),s.size&&(console.warn("Depricated"),r.set("Content-Length",s.size));let l="string"==typeof s?s:s.filename;l&&(console.warn("Depricated"),l=encodeURIComponent(l).replace(/['()]/g,escape).replace(/\*/g,"%2A"),r.set("Content-Disposition","attachment; filename*=UTF-8''"+l)),e.respondWith(new Response(a,{headers:r})),o.postMessage({debug:"Download started"})}); \ No newline at end of file +/*! streamsaver. MIT License. Jimmy Wärting */ +/* global self ReadableStream Response */ + +self.addEventListener('install', () => { + self.skipWaiting() +}) + +self.addEventListener('activate', event => { + event.waitUntil(self.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 => { + // 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 + } + + const data = event.data + const downloadUrl = data.url || self.registration.scope + 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 + + // 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 + } else if (event.data.transferringReadable) { + port.onmessage = evt => { + port.onmessage = null + metadata[0] = evt.data.readableStream + } + } else { + metadata[0] = createStream(port) + } + + map.set(downloadUrl, metadata) + port.postMessage({ download: downloadUrl }) +} + +function createStream (port) { + // ReadableStream is only supported by chrome 52 + return new ReadableStream({ + start (controller) { + // When we receive data on the messageChannel, we write + port.onmessage = ({ data }) => { + if (data === 'end') { + return controller.close() + } + + if (data === 'abort') { + controller.error('Aborted the download') + return + } + + controller.enqueue(data) + } + }, + cancel (reason) { + console.log('user aborted', reason) + port.postMessage({ abort: true }) + } + }) +} + +self.onfetch = event => { + const url = event.request.url + + // this only works for Firefox + if (url.endsWith('/ping')) { + return event.respondWith(new Response('pong')) + } + + const hijacke = map.get(url) + + if (!hijacke) return null + + const [ stream, data, port ] = hijacke + + map.delete(url) + + // 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', + + // 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'", + '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')) + } + + 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) + } + + let fileName = typeof data === 'string' ? data : data.filename + if (fileName) { + console.warn('Depricated') + // Make filename RFC5987 compatible + 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' }) +}