From bb7b36b5bd7497e9292ce1b48cc43b0557e504db Mon Sep 17 00:00:00 2001 From: xiaocp2009 <39615122+xiaocp2009@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:16:18 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E8=80=81=E6=97=A7=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8(=E7=81=AB=E7=8B=90)=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cds-fontend-2025.V1/package.json | 1 + cds-fontend-2025.V1/pnpm-lock.yaml | 9 + cds-fontend-2025.V1/public/mitm.html | 166 ++++++++++++++++++ cds-fontend-2025.V1/public/sw.js | 130 ++++++++++++++ .../src/hooks/business/download.ts | 10 ++ 5 files changed, 316 insertions(+) create mode 100644 cds-fontend-2025.V1/public/mitm.html create mode 100644 cds-fontend-2025.V1/public/sw.js diff --git a/cds-fontend-2025.V1/package.json b/cds-fontend-2025.V1/package.json index f9245f2..ac56832 100644 --- a/cds-fontend-2025.V1/package.json +++ b/cds-fontend-2025.V1/package.json @@ -52,6 +52,7 @@ "echarts": "5.6.0", "highlight.js": "^11.11.1", "jsencrypt": "^3.3.2", + "web-streams-polyfill": "^3.2.1", "json5": "2.2.3", "monaco-editor": "^0.52.2", "naive-ui": "2.42.0", diff --git a/cds-fontend-2025.V1/pnpm-lock.yaml b/cds-fontend-2025.V1/pnpm-lock.yaml index d0ace8c..a939fb6 100644 --- a/cds-fontend-2025.V1/pnpm-lock.yaml +++ b/cds-fontend-2025.V1/pnpm-lock.yaml @@ -92,6 +92,9 @@ importers: vue-router: specifier: 4.5.1 version: 4.5.1(vue@3.5.17(typescript@5.8.3)) + web-streams-polyfill: + specifier: ^3.2.1 + version: 3.3.3 devDependencies: '@elegant-router/vue': specifier: 0.3.8 @@ -4098,6 +4101,10 @@ packages: peerDependencies: vue: ^3.0.11 + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + webpack-sources@3.3.3: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} @@ -8218,6 +8225,8 @@ snapshots: vooks: 0.2.12(vue@3.5.17(typescript@5.8.3)) vue: 3.5.17(typescript@5.8.3) + web-streams-polyfill@3.3.3: {} + webpack-sources@3.3.3: {} webpack-virtual-modules@0.6.2: {} diff --git a/cds-fontend-2025.V1/public/mitm.html b/cds-fontend-2025.V1/public/mitm.html new file mode 100644 index 0000000..04450a9 --- /dev/null +++ b/cds-fontend-2025.V1/public/mitm.html @@ -0,0 +1,166 @@ + + diff --git a/cds-fontend-2025.V1/public/sw.js b/cds-fontend-2025.V1/public/sw.js new file mode 100644 index 0000000..dd75c1a --- /dev/null +++ b/cds-fontend-2025.V1/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/cds-fontend-2025.V1/src/hooks/business/download.ts b/cds-fontend-2025.V1/src/hooks/business/download.ts index 210e125..a859c23 100644 --- a/cds-fontend-2025.V1/src/hooks/business/download.ts +++ b/cds-fontend-2025.V1/src/hooks/business/download.ts @@ -3,6 +3,7 @@ import { errorCodeRecord } from '@/constants/common'; import { localStg } from '@/utils/storage'; import { getServiceBaseURL } from '@/utils/service'; import { transformToURLSearchParams } from '@/utils/common'; +import { WritableStream } from 'web-streams-polyfill/ponyfill'; interface RequestConfig { method: 'GET' | 'POST'; @@ -51,6 +52,15 @@ export function useDownload() { contentLength?: number ): Promise { window.$loading?.endLoading(); + + // 检查浏览器是否原生支持 WritableStream,若不支持则使用polyfill + if (typeof window.WritableStream === 'undefined') { + window.WritableStream = WritableStream; + // 确保StreamSaver也使用相同的Polyfill + StreamSaver.WritableStream = WritableStream; + } + + StreamSaver.mitm = '/mitm.html' // 增加此行代码 const fileStream = StreamSaver.createWriteStream(filename, { size: contentLength }); if (window.WritableStream && readableStream?.pipeTo) {