diff --git a/src/runtime/background/gm_api.ts b/src/runtime/background/gm_api.ts index a5f2df1f..c4fb28e7 100644 --- a/src/runtime/background/gm_api.ts +++ b/src/runtime/background/gm_api.ts @@ -17,7 +17,15 @@ import PermissionVerify, { ConfirmParam, IPermissionVerify, } from "./permission_verify"; -import { dealXhr, getIcon, listenerWebRequest, setXhrHeader } from "./utils"; +import { + dealFetch, + dealXhr, + getFetchHeader, + getIcon, + listenerWebRequest, + setXhrHeader, + uint8ToArray, +} from "./utils"; // GMApi,处理脚本的GM API调用请求 @@ -150,6 +158,49 @@ export default class GMApi { return this.valueManager.setValue(request.script, key, value, sender); } + // 处理GM_xmlhttpRequest fetch的情况,先只处理ReadableStream的情况 + // 且不考虑复杂的情况 + CAT_fetch(request: Request, channel: Channel): Promise { + const config = request.params[0]; + const { url } = config; + return fetch(url, { + method: config.method || "GET", + body: config.data, + headers: getFetchHeader(this.systemConfig.scriptCatFlag, config), + }) + .then((resp) => { + const send = dealFetch( + this.systemConfig.scriptCatFlag, + config, + resp, + 1 + ); + const reader = resp.body?.getReader(); + if (!reader) { + throw new Error("read is not found"); + } + const { scriptCatFlag } = this.systemConfig; + reader.read().then(function read({ done, value }) { + if (done) { + const data = dealFetch(scriptCatFlag, config, resp, 4); + channel.send({ event: "onreadystatechange", data }); + channel.send({ event: "onload", data }); + channel.send({ event: "onloadend", data }); + channel.disChannel(); + } else { + channel.send({ event: "onstream", data: Array.from(value) }); + reader.read().then(read); + } + }); + channel.send({ event: "onloadstart", data: send }); + send.readyState = 2; + channel.send({ event: "onreadystatechange", data: send }); + }) + .catch((e) => { + channel.throw(e); + }); + } + @PermissionVerify.API({ confirm: (request: Request) => { const config = request.params[0]; @@ -181,7 +232,10 @@ export default class GMApi { }) async GM_xmlhttpRequest(request: Request, channel: Channel): Promise { const config = request.params[0]; - + if (config.responseType === "stream") { + // 只有fetch支持ReadableStream + return this.CAT_fetch(request, channel); + } const xhr = new XMLHttpRequest(); xhr.open( config.method || "GET", diff --git a/src/runtime/background/utils.ts b/src/runtime/background/utils.ts index 372b9cbd..be5560c4 100644 --- a/src/runtime/background/utils.ts +++ b/src/runtime/background/utils.ts @@ -262,6 +262,39 @@ export function setXhrHeader( } } +export function getFetchHeader( + headerFlag: string, + config: GMSend.XHRDetails +): any { + const headers: { [key: string]: string } = {}; + headers[`${headerFlag}-gm-xhr`] = "true"; + if (config.headers) { + Object.keys(config.headers).forEach((key) => { + const lowKey = key.toLowerCase(); + if ( + unsafeHeaders[lowKey] || + lowKey.startsWith("sec-") || + lowKey.startsWith("proxy-") + ) { + headers[`${headerFlag}-${lowKey}`] = config.headers![key]!; + } else { + // 直接设置header + headers[key] = config.headers![key]!; + } + }); + } + if (config.maxRedirects !== undefined) { + headers[`${headerFlag}-max-redirects`] = config.maxRedirects.toString(); + } + if (config.cookie) { + headers[`${headerFlag}-cookie`] = config.cookie; + } + if (config.anonymous) { + headers[`${headerFlag}-anonymous`] = "true"; + } + return headers; +} + export async function dealXhr( headerFlag: string, config: GMSend.XHRDetails, @@ -329,6 +362,29 @@ export async function dealXhr( return Promise.resolve(respond); } +export function dealFetch( + headerFlag: string, + config: GMSend.XHRDetails, + response: Response, + readyState: 0 | 1 | 2 | 3 | 4 +) { + const removeXCat = new RegExp(`${headerFlag}-`, "g"); + let respHeader = ""; + response.headers && + response.headers.forEach((value, key) => { + respHeader += `${key.replace(removeXCat, "")}: ${value}\n`; + }); + const respond: GMTypes.XHRResponse = { + finalUrl: response.url || config.url, + readyState, + status: response.status, + statusText: response.statusText, + responseHeaders: respHeader, + responseType: config.responseType, + }; + return respond; +} + export function getIcon(script: Script): string { return ( (script.metadata.icon && script.metadata.icon[0]) || diff --git a/src/runtime/content/content.ts b/src/runtime/content/content.ts index 51536dcc..ed57b9f0 100644 --- a/src/runtime/content/content.ts +++ b/src/runtime/content/content.ts @@ -127,5 +127,19 @@ export default class ContentRuntime { return Promise.resolve(url); } ); + // 处理CAT_fetchDocument + this.contentMessage.setHandler("CAT_fetchDocument", (_action, data) => { + return new Promise((resolve) => { + const xhr = new XMLHttpRequest(); + xhr.responseType = "document"; + xhr.open("GET", data); + xhr.onload = () => { + resolve({ + relatedTarget: xhr.response, + }); + }; + xhr.send(); + }); + }); } } diff --git a/src/runtime/content/gm_api.ts b/src/runtime/content/gm_api.ts index d3b5f3c4..5993b03b 100644 --- a/src/runtime/content/gm_api.ts +++ b/src/runtime/content/gm_api.ts @@ -213,6 +213,27 @@ export default class GMApi { return this.message.syncSend("CAT_fetchBlob", url); } + @GMContext.API() + public CAT_fetchDocument(url: string): Promise { + return new Promise((resolve) => { + let el: Document | undefined; + (this.message).sendCallback( + "CAT_fetchDocument", + url, + (resp) => { + el = ( + (( + (this.message).getAndDelRelatedTarget( + resp.relatedTarget + ) + )) + ); + resolve(el); + } + ); + }); + } + // 辅助GM_xml发送blob数据 @GMContext.API() public CAT_createBlobUrl(blob: Blob): Promise { @@ -220,7 +241,9 @@ export default class GMApi { } // 用于脚本跨域请求,需要@connect domain指定允许的域名 - @GMContext.API({ depend: ["CAT_fetchBlob", "CAT_createBlobUrl"] }) + @GMContext.API({ + depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"], + }) public GM_xmlhttpRequest(details: GMTypes.XHRDetails) { let connect: Channel; @@ -300,24 +323,45 @@ export default class GMApi { } } + let readerStream: ReadableStream | undefined; + let controller: ReadableStreamDefaultController | undefined; // 如果返回类型是arraybuffer或者blob的情况下,需要将返回的数据转化为blob // 在background通过URL.createObjectURL转化为url,然后在content页读取url获取blob对象 + const responseType = details.responseType?.toLocaleLowerCase(); const warpResponse = (old: Function) => { + if (responseType === "stream") { + readerStream = new ReadableStream({ + start(ctrl) { + controller = ctrl; + }, + }); + } return async (xhr: GMTypes.XHRResponse) => { if (xhr.response) { - const resp = await this.CAT_fetchBlob(xhr.response); - if (details.responseType === "arraybuffer") { - xhr.response = await resp.arrayBuffer(); + if (responseType === "document") { + xhr.response = await this.CAT_fetchDocument(xhr.response); + xhr.responseXML = xhr.response; + xhr.responseType = "document"; } else { - xhr.response = resp; + const resp = await this.CAT_fetchBlob(xhr.response); + if (responseType === "arraybuffer") { + xhr.response = await resp.arrayBuffer(); + } else { + xhr.response = resp; + } } } + if (responseType === "stream") { + xhr.response = readerStream; + } old(xhr); }; }; if ( - details.responseType?.toLowerCase() === "arraybuffer" || - details.responseType?.toLocaleLowerCase() === "blob" + responseType === "arraybuffer" || + responseType === "blob" || + responseType === "document" || + responseType === "stream" ) { if (details.onload) { details.onload = warpResponse(details.onload); @@ -328,6 +372,15 @@ export default class GMApi { if (details.onloadend) { details.onloadend = warpResponse(details.onloadend); } + // document类型读取blob,然后在content页转化为document对象 + if (responseType === "document") { + param.responseType = "blob"; + } + if (responseType === "stream") { + if (details.onloadstart) { + details.onloadstart = warpResponse(details.onloadstart); + } + } } connect = this.connect("GM_xmlhttpRequest", [param], (resp: any) => { @@ -338,6 +391,9 @@ export default class GMApi { break; case "onloadend": details.onloadend && details.onloadend(data); + if (readerStream) { + controller?.close(); + } break; case "onloadstart": details.onloadstart && details.onloadstart(data); @@ -357,6 +413,9 @@ export default class GMApi { case "onabort": details.onabort && details.onabort(); break; + case "onstream": + controller?.enqueue(new Uint8Array(resp.data)); + break; default: LoggerCore.getLogger().warn("GM_xmlhttpRequest resp is error", { resp, diff --git a/src/types/main.d.ts b/src/types/main.d.ts index a152970b..19a7b1f6 100644 --- a/src/types/main.d.ts +++ b/src/types/main.d.ts @@ -17,7 +17,13 @@ declare namespace GMSend { binary?: boolean; timeout?: number; context?: CONTEXT_TYPE; - responseType?: "text" | "arraybuffer" | "blob" | "json"; + responseType?: + | "text" + | "arraybuffer" + | "blob" + | "json" + | "document" + | "stream"; overrideMimeType?: string; anonymous?: boolean; fetch?: boolean; diff --git a/src/types/scriptcat.d.ts b/src/types/scriptcat.d.ts index 67ae1aa9..d423e36a 100644 --- a/src/types/scriptcat.d.ts +++ b/src/types/scriptcat.d.ts @@ -253,10 +253,16 @@ declare namespace GMTypes { responseHeaders?: string; status?: number; statusText?: string; - response?: any; + response?: string | Blob | ArrayBuffer | Document | ReadableStream | null; responseText?: string; responseXML?: Document | null; - responseType?: "text" | "arraybuffer" | "blob" | "json"; + responseType?: + | "text" + | "arraybuffer" + | "blob" + | "json" + | "document" + | "stream"; } interface XHRProgress extends XHRResponse { @@ -280,7 +286,13 @@ declare namespace GMTypes { binary?: boolean; timeout?: number; context?: ContextType; - responseType?: "text" | "arraybuffer" | "blob" | "json"; + responseType?: + | "text" + | "arraybuffer" + | "blob" + | "json" + | "document" + | "stream"; // stream 在当前版本是一个较为简陋的实现 overrideMimeType?: string; anonymous?: boolean; fetch?: boolean;