From 1209a3962b776413a32817a4abc1d75c5a351788 Mon Sep 17 00:00:00 2001 From: "DESKTOP-6C3NK6N\\WXS" <815034831@qq.com> Date: Mon, 9 Sep 2024 11:08:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E8=B7=A8=E5=9F=9F=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E8=A7=A3=E5=86=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/mitm.html | 167 ++++++++++++++++++++++++++++++++++++++++++++ public/sw.js | 130 ++++++++++++++++++++++++++++++++++ src/utils/stream.js | 19 +++++ vue.config.js | 8 +-- 4 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 public/mitm.html create mode 100644 public/sw.js diff --git a/public/mitm.html b/public/mitm.html new file mode 100644 index 00000000..508a9c3b --- /dev/null +++ b/public/mitm.html @@ -0,0 +1,167 @@ + + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 00000000..dd75c1a5 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,130 @@ +/* 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', + 'Cross-Origin-Embedder-Policy': 'require-corp' + }) + + 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/utils/stream.js b/src/utils/stream.js index eef180e7..7f745ed2 100644 --- a/src/utils/stream.js +++ b/src/utils/stream.js @@ -1,10 +1,12 @@ import streamSaver from "streamsaver"; import "streamsaver/examples/zip-stream.js"; import store from '@/store' +streamSaver.mitm = `${window.location.origin}/mitm.html?version=2.0.0` // 下载文件并压缩 function zipFiles(zipName, files) { console.log("同步下载打包开始时间:" + new Date()); store.dispatch('trials/setUnLock', true) + files = formatFiles(files) // 创建压缩文件输出流 const zipFileOutputStream = streamSaver.createWriteStream(zipName); // 创建下载文件流 @@ -50,6 +52,23 @@ async function updateFile(file, name) { console.log(err) } } +// 同名文件修改名称 +function formatFiles(files) { + let fileObj = {}; + files.forEach(file => { + let arr = Object.keys(fileObj); + if (!~arr.indexOf(file.name)) { + fileObj[file.name] = 1; + } else { + let name = file.name; + file.name = name.split(".")[0] + `(${fileObj[name]})` + name + .substring(name.lastIndexOf('.')) + .toLocaleLowerCase() + fileObj[name]++; + } + }) + return files; +} function decodeUtf8(bytes) { let str = bytes.split('?'); let str2 = str[0].split('/'); diff --git a/vue.config.js b/vue.config.js index f7fb01e5..ea3e8bf9 100644 --- a/vue.config.js +++ b/vue.config.js @@ -24,10 +24,10 @@ module.exports = { productionSourceMap: false, devServer: { port: '8080', - // headers: { - // 'Cross-Origin-Opener-Policy': 'same-origin', - // 'Cross-Origin-Embedder-Policy': 'require-corp' - // }, + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp' + }, // open: true, overlay: { warnings: false,