diff --git a/package-lock.json b/package-lock.json index 324af0ec8..736f37b54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -153,6 +153,12 @@ "integrity": "sha512-DssMqTV9UnnoxDWu959sDLZzfvqCF0qDNRjaWeYSui9xkFe61kKo4l1TWNTQONpuXEm+gLMRvdlzvNHBamzmEw==", "dev": true }, + "@types/streamsaver": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/streamsaver/-/streamsaver-2.0.1.tgz", + "integrity": "sha512-I49NtT8w6syBI3Zg3ixCyygTHoTVMY0z2TMRcTgccdIsVd2MwlKk7ITLHLsJtgchUHcOd7QEARG9h0ifcA6l2Q==", + "dev": true + }, "@types/video.js": { "version": "7.3.27", "resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.27.tgz", @@ -1484,6 +1490,11 @@ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, + "streamsaver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/streamsaver/-/streamsaver-2.0.5.tgz", + "integrity": "sha512-KIWtBvi8A6FiFZGNSyuIZRZM6C8AvnWTiCx/TYa7so420vC5sQwcBKkdqInuGWoWMfeWy/P+/cRqMtWVf4RW9w==" + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index c9f59e76a..2d4730ed4 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,13 @@ "clipboard": "^2.0.8", "dplayer": "^1.26.0", "plyr": "^3.6.9", + "streamsaver": "^2.0.5", "vue": "^3.2.16", "vue-router": "^4.0.12" }, "devDependencies": { "@types/dplayer": "^1.25.2", + "@types/streamsaver": "^2.0.1", "@types/video.js": "^7.3.27", "@vicons/tabler": "^0.11.0", "@vitejs/plugin-vue": "^1.9.3", diff --git a/public/mitm.html b/public/mitm.html new file mode 100644 index 000000000..04450a929 --- /dev/null +++ b/public/mitm.html @@ -0,0 +1,166 @@ + + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 000000000..605af6308 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,128 @@ +/* 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 () { + console.log('user aborted') + } + }) +} + +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' }) +} diff --git a/src/assets/logo1.png b/src/assets/logo1.png new file mode 100644 index 000000000..5a4835391 Binary files /dev/null and b/src/assets/logo1.png differ diff --git a/src/components/Task.vue b/src/components/Task.vue index e8edb0b5f..8342d0637 100644 --- a/src/components/Task.vue +++ b/src/components/Task.vue @@ -139,7 +139,6 @@ import { CircleX } from '@vicons/tabler' vertical-align: baseline; } .content { - margin-top: 20px; width: 375px; max-height: 530px; min-height: 54px; diff --git a/src/index.d.ts b/src/index.d.ts index b12b82a72..d5c14db13 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,3 +1,4 @@ interface Window { - $message: any + $message: any, + $downId: string[] } \ No newline at end of file diff --git a/src/views/layout/index.vue b/src/views/layout/index.vue index 0cebf7012..58214f4a2 100644 --- a/src/views/layout/index.vue +++ b/src/views/layout/index.vue @@ -25,19 +25,28 @@ :percentage="Number((aboutInfo?.quota.usage / aboutInfo?.quota.limit * 100).toFixed(2))" :indicator-placement="'inside'" :height="14" + :color="vipInfo?.status === 'ok' ? '#d1ae6a' : undefined" processing> -