实现gmxhr
Some checks failed
build / Build (push) Failing after 6s
test / Run tests (push) Failing after 8s

This commit is contained in:
王一之 2025-04-07 00:37:58 +08:00
parent a8054451ac
commit 1a55bb348f
13 changed files with 1255 additions and 517 deletions

View File

@ -13,14 +13,21 @@ const data = new FormData();
data.append("username", "admin");
data.append(
"file",
new File(["foo"], "foo.txt", {
type: "text/plain",
})
);
GM_xmlhttpRequest({
url: "https://bbs.tampermonkey.net.cn/",
method: "POST",
responseType: "blob",
data: data,
headers: {
"referer": "http://www.example.com/",
"origin": "www.example.com",
referer: "http://www.example.com/",
origin: "www.example.com",
// 为空将不会发送此header
"sec-ch-ua-mobile": "",
},

View File

@ -31,7 +31,7 @@
"pako": "^2.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^15.1.0",
"react-i18next": "^15.4.1",
"react-icons": "^5.3.0",
"react-joyride": "^2.9.3",
"react-redux": "^9.1.2",
@ -41,10 +41,10 @@
"yaml": "^2.6.1"
},
"devDependencies": {
"@eslint/compat": "^1.2.6",
"@eslint/js": "^9.19.0",
"@rspack/cli": "^1.2.3",
"@rspack/core": "^1.2.3",
"@eslint/compat": "^1.2.8",
"@eslint/js": "^9.24.0",
"@rspack/cli": "^1.3.2",
"@rspack/core": "^1.3.2",
"@rspack/plugin-react-refresh": "^1.0.1",
"@types/chrome": "^0.0.279",
"@types/crypto-js": "^4.2.2",
@ -57,22 +57,21 @@
"@unocss/postcss": "0.65.0-beta.2",
"@vitest/coverage-v8": "2.1.4",
"autoprefixer": "^10.4.20",
"compression-webpack-plugin": "^11.1.0",
"cross-env": "^7.0.3",
"eslint": "^9.19.0",
"eslint": "^9.24.0",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-hooks": "^5.2.0",
"fake-indexeddb": "^6.0.0",
"globals": "^15.14.0",
"globals": "^16.0.0",
"jsdom": "^25.0.1",
"mock-xmlhttprequest": "^8.4.1",
"postcss": "^8.4.49",
"postcss-loader": "^8.1.1",
"prettier": "^3.4.2",
"prettier": "^3.5.3",
"react-refresh": "^0.16.0",
"ts-node": "^10.9.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.22.0",
"typescript-eslint": "^8.29.0",
"unocss": "0.65.0-beta.2",
"vitest": "^2.1.4"
}

View File

@ -17,7 +17,7 @@ export class CustomEventMessage implements Message {
EE: EventEmitter = new EventEmitter();
// 关联dom目标
relatedTarget: Map<number, Element> = new Map();
relatedTarget: Map<number, Document> = new Map();
constructor(
protected flag: string,
@ -25,7 +25,7 @@ export class CustomEventMessage implements Message {
) {
window.addEventListener((isContent ? "ct" : "fd") + flag, (event) => {
if (event instanceof MouseEvent) {
this.relatedTarget.set(event.clientX, <Element>event.relatedTarget);
this.relatedTarget.set(event.clientX, <Document>event.relatedTarget);
return;
} else if (event instanceof CustomEvent) {
this.messageHandle(event.detail, new CustomEventPostMessage(this));
@ -130,4 +130,10 @@ export class CustomEventMessage implements Message {
this.nativeSend(body);
});
}
getAndDelRelatedTarget(id: number) {
const target = this.relatedTarget.get(id);
this.relatedTarget.delete(id);
return target;
}
}

View File

@ -114,9 +114,15 @@ export class Group {
}
// 转发消息
export function forwardMessage(prefix: string, path: string, from: Server, to: MessageSend) {
from.on(path, (params, fromCon) => {
export function forwardMessage(prefix: string, path: string, from: Server, to: MessageSend, middleware?: ApiFunction) {
from.on(path, async (params, fromCon) => {
console.log("forwardMessage", path, prefix, params);
if (middleware) {
const resp = await middleware(params, new GetSender(fromCon));
if (resp !== false) {
return resp;
}
}
if (fromCon) {
const fromConnect = fromCon.getConnect();
to.connect({ action: prefix + "/" + path, data: params }).then((toCon) => {

1288
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,6 @@ import * as path from "path";
import { defineConfig } from "@rspack/cli";
import { rspack } from "@rspack/core";
import { version } from "./package.json";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const CompressionPlugin = require("compression-webpack-plugin");
const isDev = process.env.NODE_ENV === "development";
const isBeta = version.includes("-");
@ -174,11 +172,6 @@ export default defineConfig({
minify: true,
chunks: ["sandbox"],
}),
new CompressionPlugin({
test: /ts.worker.js$/,
filename: () => "ts.worker.js",
deleteOriginalAssets: true,
}),
].filter(Boolean),
optimization: {
minimizer: [
@ -187,7 +180,6 @@ export default defineConfig({
minimizerOptions: { targets },
}),
],
realContentHash: true,
},
experiments: {
css: true,

View File

@ -1,9 +1,17 @@
import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { GetSender, Group, MessageConnect } from "@Packages/message/server";
export default class GMApi {
constructor(private group: Group) {}
dealXhrResponse(con: MessageConnect, details: GMSend.XHRDetails, event: string, xhr: XMLHttpRequest, data?: any) {
async dealXhrResponse(
con: MessageConnect,
details: GMSend.XHRDetails,
event: string,
xhr: XMLHttpRequest,
data?: any
) {
const finalUrl = xhr.responseURL || details.url;
// 判断是否有headerFlag-final-url,有则替换finalUrl
let response: GMTypes.XHRResponse = {
@ -11,9 +19,56 @@ export default class GMApi {
readyState: <any>xhr.readyState,
status: xhr.status,
statusText: xhr.statusText,
// header由service_worker处理
// responseHeaders: xhr.getAllResponseHeaders().replace(removeXCat, ""),
responseType: details.responseType,
};
if (xhr.readyState === 4) {
const responseType = details.responseType?.toLowerCase();
if (responseType === "arraybuffer" || responseType === "blob") {
let blob: Blob;
if (xhr.response instanceof ArrayBuffer) {
blob = new Blob([xhr.response]);
response.response = URL.createObjectURL(blob);
} else {
blob = <Blob>xhr.response;
response.response = URL.createObjectURL(blob);
}
try {
if (xhr.getResponseHeader("Content-Type")?.indexOf("text") !== -1) {
// 如果是文本类型,则尝试转换为文本
response.responseText = await blob.text();
}
} catch (e) {
LoggerCore.logger(Logger.E(e)).error("GM XHR getResponseHeader error");
}
setTimeout(() => {
URL.revokeObjectURL(<string>response.response);
}, 60 * 1000);
} else if (response.responseType === "json") {
try {
response.response = JSON.parse(xhr.responseText);
} catch (e) {
LoggerCore.logger(Logger.E(e)).error("GM XHR JSON parse error");
}
try {
response.responseText = xhr.responseText;
} catch (e) {
LoggerCore.logger(Logger.E(e)).error("GM XHR getResponseText error");
}
} else {
try {
response.response = xhr.response;
} catch (e) {
LoggerCore.logger(Logger.E(e)).error("GM XHR response error");
}
try {
response.responseText = xhr.responseText || undefined;
} catch (e) {
LoggerCore.logger(Logger.E(e)).error("GM XHR getResponseText error");
}
}
}
if (data) {
response = Object.assign(response, data);
}
@ -24,16 +79,36 @@ export default class GMApi {
return response;
}
xmlHttpRequest(details: GMSend.XHRDetails, sender: GetSender) {
CAT_fetch(details: GMSend.XHRDetails, sender: GetSender) {
throw new Error("Method not implemented.");
}
async xmlHttpRequest(details: GMSend.XHRDetails, sender: GetSender) {
if (details.responseType === "stream") {
// 只有fetch支持ReadableStream
return this.CAT_fetch(details, sender);
}
const xhr = new XMLHttpRequest();
const con = sender.getConnect();
xhr.open(details.method || "GET", details.url);
xhr.open(details.method || "GET", details.url, true, details.user || "", details.password || "");
// 添加header
if (details.headers) {
for (const key in details.headers) {
xhr.setRequestHeader(key, details.headers[key]);
}
}
//超时时间
if (details.timeout) {
xhr.timeout = details.timeout;
}
if (details.overrideMimeType) {
xhr.overrideMimeType(details.overrideMimeType);
}
//设置响应类型
if (details.responseType !== "json") {
xhr.responseType = details.responseType || "";
}
xhr.onload = () => {
this.dealXhrResponse(con, details, "onload", xhr);
};
@ -65,14 +140,32 @@ export default class GMApi {
xhr.ontimeout = () => {
con?.sendMessage({ action: "ontimeout", data: {} });
};
//处理数据
if (details.dataType === "FormData") {
const data = new FormData();
if (details.data && details.data instanceof Array) {
await Promise.all(
details.data.map(async (val: GMSend.XHRFormData) => {
if (val.type === "file") {
const file = new File([await (await fetch(val.val)).blob()], val.filename!);
data.append(val.key, file, val.filename);
} else {
data.append(val.key, val.val);
}
})
);
xhr.send(data);
}
} else if (details.dataType === "Blob") {
if (!details.data) {
throw new Error("Blob data is empty");
}
const resp = await (await fetch(<string>details.data)).blob();
xhr.send(resp);
} else {
xhr.send(<string>details.data);
}
if (details.timeout) {
xhr.timeout = details.timeout;
}
if (details.overrideMimeType) {
xhr.overrideMimeType(details.overrideMimeType);
}
xhr.send();
con?.onDisconnect(() => {
xhr.abort();
});

View File

@ -52,20 +52,5 @@ export class OffscreenManager {
const gmApi = new GMApi(this.windowApi.group("gmApi"));
gmApi.init();
// // 处理gm xhr请求
// this.api.on("gmXhr", (data) => {
// console.log("123");
// });
// // 测试xhr
// const ret = await sendMessage(this.extensionMessage, "serviceWorker/testGmApi");
// console.log("test xhr", ret);
// const xhr = new XMLHttpRequest();
// xhr.onload = () => {
// console.log(xhr);
// };
// xhr.open("GET", "https://scriptcat.org/zh-CN");
// xhr.send();
}
}

View File

@ -159,7 +159,7 @@ export class Runtime {
crontabScript(script: ScriptRunResouce) {
// 执行定时脚本 运行表达式
if (!script.metadata.crontab) {
throw new Error("错误的crontab表达式");
throw new Error(script.name + " - 错误的crontab表达式");
}
// 如果有nextruntime,则加入重试队列
this.joinRetryList(script);

View File

@ -95,11 +95,21 @@ export default class GMApi {
] as chrome.declarativeNetRequest.ModifyHeaderInfo[];
Object.keys(headers).forEach((key) => {
const lowKey = key.toLowerCase();
if (unsafeHeaders[lowKey] || lowKey.startsWith("sec-") || lowKey.startsWith("proxy-")) {
if (headers[key]) {
if (unsafeHeaders[lowKey] || lowKey.startsWith("sec-") || lowKey.startsWith("proxy-")) {
if (headers[key]) {
requestHeaders.push({
header: key,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
value: headers[key],
});
}
delete headers[key];
}
} else {
requestHeaders.push({
header: key,
operation: chrome.declarativeNetRequest.HeaderOperation.SET,
value: headers[key],
operation: chrome.declarativeNetRequest.HeaderOperation.REMOVE,
});
delete headers[key];
}
@ -125,7 +135,6 @@ export default class GMApi {
requestMethods: [(params.method || "GET").toLowerCase() as chrome.declarativeNetRequest.RequestMethod],
excludedTabIds: excludedTabIds,
};
console.log(rule);
await chrome.declarativeNetRequest.updateSessionRules({
removeRuleIds: [ruleId],
addRules: [rule],
@ -135,9 +144,9 @@ export default class GMApi {
gmXhrHeadersReceived: EventEmitter = new EventEmitter();
// TODO: maxRedirects实现
@PermissionVerify.API()
async GM_xmlhttpRequest(request: Request, con: GetSender) {
console.log("GM XHR", request);
if (request.params.length === 0) {
return Promise.reject(new Error("param is failed"));
}
@ -151,7 +160,6 @@ export default class GMApi {
}
params.headers["X-Scriptcat-GM-XHR-Request-Id"] = requestId.toString();
params.headers = await this.buildDNRRule(requestId, request.params[0]);
console.log(" params.headers", params.headers);
let responseHeader = "";
// 等待response
this.gmXhrHeadersReceived.addListener(
@ -160,6 +168,7 @@ export default class GMApi {
details.responseHeaders?.forEach((header) => {
responseHeader += header.name + ": " + header.value + "\n";
});
this.gmXhrHeadersReceived.removeAllListeners("headersReceived:" + requestId);
}
);
// 再发送到offscreen, 处理请求

View File

@ -2,7 +2,7 @@ import dts from "@App/types/scriptcat.d.ts";
import { languages } from "monaco-editor";
import pako from "pako";
import Cache from "@App/app/cache";
import { isFirefox } from "./utils";
import { isDebug, isFirefox } from "./utils";
import EventEmitter from "eventemitter3";
// 注册eslint
@ -15,7 +15,12 @@ export default function registerEditor() {
fetch(chrome.runtime.getURL(`/src/ts.worker.js${isFirefox() ? ".gz" : ""}`))
.then((resp) => resp.blob())
.then(async (blob) => {
const result = pako.inflate(await blob.arrayBuffer());
let result: ArrayBuffer;
if (isDebug()) {
result = await blob.arrayBuffer();
} else {
result = pako.inflate(await blob.arrayBuffer());
}
// @ts-ignore
window.tsUrl = URL.createObjectURL(new Blob([result]));
});

View File

@ -1,6 +1,6 @@
import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Client } from "@Packages/message/client";
import { forwardMessage, Message, MessageSend, Server } from "@Packages/message/server";
import { forwardMessage, GetSender, Message, MessageSend, Server } from "@Packages/message/server";
// content页的处理
export default class ContentRuntime {
@ -17,7 +17,43 @@ export default class ContentRuntime {
this.msg.onConnect((msg, connect) => {
console.log(msg, connect);
});
forwardMessage("serviceWorker", "runtime/gmApi", this.server, this.send);
forwardMessage(
"serviceWorker",
"runtime/gmApi",
this.server,
this.send,
(data: { api: string; params: any }, con: GetSender) => {
// 拦截关注的action
console.log("拦截", data);
switch (data.api) {
case "CAT_createBlobUrl": {
const file = data.params[0] as File;
const url = URL.createObjectURL(file);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 60 * 1000);
return Promise.resolve(url);
}
case "CAT_fetchBlob": {
return fetch(data.params[0]).then((res) => res.blob());
}
case "CAT_fetchDocument": {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.responseType = "document";
xhr.open("GET", data.params[0]);
xhr.onload = () => {
resolve({
relatedTarget: xhr.response,
});
};
xhr.send();
});
}
}
return Promise.resolve(false);
}
);
// 由content到background
// 转发gmApi消息
// this.contentMessage.setHandler("gmApi", (action, data) => {

View File

@ -4,6 +4,9 @@ import { ValueUpdateData } from "./exec_script";
import { ExtVersion } from "@App/app/const";
import { storageKey } from "../utils";
import { Message, MessageConnect } from "@Packages/message/server";
import { CustomEventMessage } from "@Packages/message/custom_event_message";
import LoggerCore from "@App/app/logger/core";
import { connect, sendMessage } from "@Packages/message/client";
interface ApiParam {
depend?: string[];
@ -65,25 +68,19 @@ export default class GMApi {
// 单次回调使用
public sendMessage(api: string, params: any[]) {
return this.message.sendMessage({
action: this.prefix + "/runtime/gmApi",
data: {
uuid: this.scriptRes.uuid,
api,
params,
},
return sendMessage(this.message, this.prefix + "/runtime/gmApi", {
uuid: this.scriptRes.uuid,
api,
params,
});
}
// 长连接使用,connect只用于接受消息,不发送消息
public connect(api: string, params: any[]) {
return this.message.connect({
action: this.prefix + "/runtime/gmApi",
data: {
uuid: this.scriptRes.uuid,
api,
params,
},
return connect(this.message, this.prefix + "/runtime/gmApi", {
uuid: this.scriptRes.uuid,
api,
params,
});
}
@ -184,6 +181,23 @@ export default class GMApi {
return this.sendMessage("GM_log", [message, level, labels]);
}
@GMContext.API()
public CAT_createBlobUrl(blob: Blob): Promise<string> {
return this.sendMessage("CAT_createBlobUrl", [blob]);
}
// 辅助GM_xml获取blob数据
@GMContext.API()
public CAT_fetchBlob(url: string): Promise<Blob> {
return this.sendMessage("CAT_fetchBlob", [url]);
}
@GMContext.API()
public async CAT_fetchDocument(url: string): Promise<Document | undefined> {
const data = await this.sendMessage("CAT_fetchDocument", [url]);
return (<CustomEventMessage>this.message).getAndDelRelatedTarget(data.relatedTarget);
}
// 用于脚本跨域请求,需要@connect domain指定允许的域名
@GMContext.API({
depend: ["CAT_fetchBlob", "CAT_createBlobUrl", "CAT_fetchDocument"],
@ -220,42 +234,154 @@ export default class GMApi {
param.headers["Cache-Control"] = "no-cache";
}
let connect: MessageConnect;
this.connect("GM_xmlhttpRequest", [param]).then((con) => {
connect = con;
con.onMessage((data: { action: string; data: any }) => {
// 处理返回
switch (data.action) {
case "onload":
details.onload?.(data.data);
break;
case "onloadend":
details.onloadend?.(data.data);
break;
case "onloadstart":
details.onloadstart?.(data.data);
break;
case "onprogress":
details.onprogress?.(data.data);
break;
case "onreadystatechange":
details.onreadystatechange && details.onreadystatechange(data.data);
break;
case "ontimeout":
details.ontimeout?.();
break;
case "onerror":
details.onerror?.("");
break;
case "onabort":
details.onabort?.();
break;
case "onstream":
// controller?.enqueue(new Uint8Array(resp.data));
break;
}
});
});
const handler = async () => {
// 处理数据
if (details.data instanceof FormData) {
// 处理FormData
param.dataType = "FormData";
const data: Array<GMSend.XHRFormData> = [];
const keys: { [key: string]: boolean } = {};
details.data.forEach((val, key) => {
keys[key] = true;
});
// 处理FormData中的数据
await Promise.all(
Object.keys(keys).map((key) => {
const values = (<FormData>details.data).getAll(key);
return Promise.all(
values.map(async (val) => {
if (val instanceof File) {
const url = await this.CAT_createBlobUrl(val);
console.log(url);
data.push({
key,
type: "file",
val: url,
filename: val.name,
});
} else {
data.push({
key,
type: "text",
val,
});
}
})
);
})
);
param.data = data;
} else if (details.data instanceof Blob) {
// 处理blob
param.dataType = "Blob";
param.data = await this.CAT_createBlobUrl(details.data);
}
// 处理返回数据
let readerStream: ReadableStream<Uint8Array> | undefined;
let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
// 如果返回类型是arraybuffer或者blob的情况下,需要将返回的数据转化为blob
// 在background通过URL.createObjectURL转化为url,然后在content页读取url获取blob对象
const responseType = details.responseType?.toLocaleLowerCase();
const warpResponse = (old: (xhr: GMTypes.XHRResponse) => void) => {
if (responseType === "stream") {
readerStream = new ReadableStream<Uint8Array>({
start(ctrl) {
controller = ctrl;
},
});
}
return async (xhr: GMTypes.XHRResponse) => {
if (xhr.response) {
if (responseType === "document") {
xhr.response = await this.CAT_fetchDocument(<string>xhr.response);
xhr.responseXML = xhr.response;
xhr.responseType = "document";
} else {
const resp = await this.CAT_fetchBlob(<string>xhr.response);
if (responseType === "arraybuffer") {
xhr.response = await resp.arrayBuffer();
} else {
xhr.response = resp;
}
}
}
if (responseType === "stream") {
xhr.response = readerStream;
}
old(xhr);
};
};
if (
responseType === "arraybuffer" ||
responseType === "blob" ||
responseType === "document" ||
responseType === "stream"
) {
if (details.onload) {
details.onload = warpResponse(details.onload);
}
if (details.onreadystatechange) {
details.onreadystatechange = warpResponse(details.onreadystatechange);
}
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);
}
}
}
// 发送信息
this.connect("GM_xmlhttpRequest", [param]).then((con) => {
connect = con;
con.onMessage((data: { action: string; data: any }) => {
// 处理返回
switch (data.action) {
case "onload":
details.onload?.(data.data);
break;
case "onloadend":
details.onloadend?.(data.data);
break;
case "onloadstart":
details.onloadstart?.(data.data);
break;
case "onprogress":
details.onprogress?.(data.data);
break;
case "onreadystatechange":
details.onreadystatechange && details.onreadystatechange(data.data);
break;
case "ontimeout":
details.ontimeout?.();
break;
case "onerror":
details.onerror?.("");
break;
case "onabort":
details.onabort?.();
break;
case "onstream":
controller?.enqueue(new Uint8Array(data.data));
break;
default:
LoggerCore.logger().warn("GM_xmlhttpRequest resp is error", {
action: data.action,
});
break;
}
});
});
};
// 由于需要同步返回一个abort但是一些操作是异步的所以需要在这里处理
handler();
return {
abort: () => {
if (connect) {