Skip to content

Commit 1d6d52a

Browse files
cyfung1031CodFrm
andauthored
⚡️ GM注入优化 (#517)
* 简单代码清理 * 定义 IGM_Base * 重构 GM Api 内部代码处理 * __methodInject__ 处理内部化 * 优化注入代码 * 增加 testMode、integrity内部化 * 🧪 修复单元测试 * 🧪 添加sandbox的this测试 * 代码修正 * 简化代码,更清晰 * 修正ScriptCat this行为 * 改用=>避免.call * 单元测试修正 * 🧪 补充this单元测试 * 调整代码 --------- Co-authored-by: 王一之 <yz@ggnb.top>
1 parent b1d64f0 commit 1d6d52a

File tree

11 files changed

+680
-500
lines changed

11 files changed

+680
-500
lines changed
Lines changed: 40 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,65 @@
11
import { type ScriptRunResource } from "@App/app/repo/scripts";
22
import { v4 as uuidv4 } from "uuid";
3-
import type { ApiValue } from "./types";
43
import type { Message } from "@Packages/message/types";
54
import EventEmitter from "eventemitter3";
65
import { GMContextApiGet } from "./gm_context";
76
import { GM_Base } from "./gm_api";
87

9-
// 设置api依赖
10-
function setDepend(context: { [key: string]: any }, apiVal: ApiValue) {
11-
if (apiVal.param.depend) {
12-
for (let i = 0; i < apiVal.param.depend.length; i += 1) {
13-
const value = apiVal.param.depend[i];
14-
const dependApi = GMContextApiGet(value);
15-
if (!dependApi) {
16-
return;
17-
}
18-
if (value.startsWith("GM.")) {
19-
const [, t] = value.split(".");
20-
(<{ [key: string]: any }>context.GM)[t] = dependApi.api.bind(context);
21-
} else {
22-
context[value] = dependApi.api.bind(context);
23-
}
24-
setDepend(context, dependApi);
25-
}
26-
}
27-
}
28-
298
// 构建沙盒上下文
30-
export function createContext(scriptRes: ScriptRunResource, GMInfo: any, envPrefix: string, message: Message): GM_Base {
9+
export function createContext(
10+
scriptRes: ScriptRunResource,
11+
GMInfo: any,
12+
envPrefix: string,
13+
message: Message,
14+
scriptGrants: Set<string>
15+
) {
3116
// 按照GMApi构建
3217
const valueChangeListener = new Map<number, { name: string; listener: GMTypes.ValueChangeListener }>();
3318
const EE: EventEmitter = new EventEmitter();
34-
const context: GM_Base & { [key: string]: any } = new GM_Base(envPrefix, message, scriptRes, valueChangeListener, EE);
35-
Object.assign(context, {
19+
const context = GM_Base.create({
20+
prefix: envPrefix,
21+
message,
22+
scriptRes,
23+
valueChangeListener,
24+
EE,
3625
runFlag: uuidv4(),
3726
eventId: 10000,
3827
GM: { info: GMInfo },
3928
GM_info: GMInfo,
4029
window: {
4130
onurlchange: null,
4231
},
32+
grantSet: new Set(),
4333
});
44-
if (scriptRes.metadata.grant) {
45-
const GM_cookie = function (action: string) {
46-
return (
47-
details: GMTypes.CookieDetails,
48-
done: (cookie: GMTypes.Cookie[] | any, error: any | undefined) => void
49-
) => {
50-
return context["GM_cookie"](action, details, done);
51-
};
52-
};
53-
// 处理GM.与GM_,将GM_与GM.都复制一份
54-
const grant: string[] = [];
55-
scriptRes.metadata.grant.forEach((val) => {
56-
if (val.startsWith("GM_")) {
57-
const t = val.slice(3);
58-
grant.push(`GM.${t}`);
59-
} else if (val.startsWith("GM.")) {
60-
grant.push(val);
61-
}
62-
grant.push(val);
63-
});
64-
// 去重
65-
const uniqueGrant = new Set(grant);
66-
uniqueGrant.forEach((val) => {
67-
const api = GMContextApiGet(val);
68-
if (!api) {
69-
return;
34+
const __methodInject__ = (grant: string): boolean => {
35+
const grantSet: Set<string> = context.grantSet;
36+
const s = GMContextApiGet(grant);
37+
if (!s) return false; // @grant 的定义未实作,略过 (返回 false 表示 @grant 不存在)
38+
if (grantSet.has(grant)) return true; // 重覆的@grant,略过 (返回 true 表示 @grant 存在)
39+
grantSet.add(grant);
40+
for (const t of s) {
41+
const fnKeyArray = t.fnKey.split(".");
42+
const m = fnKeyArray.length - 1;
43+
let g = context;
44+
for (let i = 0; i < m; i++) {
45+
const part = fnKeyArray[i];
46+
g = g[part] || (g[part] = {});
7047
}
71-
if (/^(GM|window)\./.test(val)) {
72-
const [n, t] = val.split(".");
73-
if (t === "cookie") {
74-
const createGMCookePromise = (action: string) => {
75-
return (details: GMTypes.CookieDetails = {}) => {
76-
return new Promise((resolve, reject) => {
77-
const fn = GM_cookie(action);
78-
fn(details, function (cookie, error) {
79-
if (error) {
80-
reject(error);
81-
} else {
82-
resolve(cookie);
83-
}
84-
});
85-
});
86-
};
87-
};
88-
context[n][t] = {
89-
list: createGMCookePromise("list"),
90-
delete: createGMCookePromise("delete"),
91-
set: createGMCookePromise("set"),
92-
};
93-
context["GM_cookie"] = api.api.bind(context);
94-
} else {
95-
(<{ [key: string]: any }>context[n])[t] = api.api.bind(context);
48+
const finalPart = fnKeyArray[m];
49+
if (g[finalPart]) continue;
50+
g[finalPart] = t.api.bind(context);
51+
const depend = t?.param?.depend;
52+
if (depend) {
53+
for (const grant of depend) {
54+
__methodInject__(grant);
9655
}
97-
} else if (val === "GM_cookie") {
98-
// 特殊处理GM_cookie.list之类
99-
context[val] = api.api.bind(context);
100-
101-
context[val].list = GM_cookie("list");
102-
context[val].delete = GM_cookie("delete");
103-
context[val].set = GM_cookie("set");
104-
} else {
105-
context[val] = api.api.bind(context);
10656
}
107-
setDepend(context, api);
108-
});
57+
}
58+
return true;
59+
};
60+
for (const grant of scriptGrants) {
61+
__methodInject__(grant);
10962
}
11063
context.unsafeWindow = window;
111-
return <GM_Base>context;
64+
return context;
11265
}

src/app/service/content/exec_script.test.ts

Lines changed: 66 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@ const scriptRes = {
1212
id: 0,
1313
name: "test",
1414
metadata: {
15+
grant: ["none"],
1516
version: ["1.0.0"],
1617
},
1718
code: "console.log('test')",
1819
sourceCode: "sourceCode",
1920
value: {},
20-
grantMap: {
21-
none: true,
22-
},
2321
} as unknown as ScriptRunResource;
2422
const envInfo: GMInfoEnv = {
2523
sandboxMode: "raw",
@@ -32,7 +30,7 @@ const envInfo: GMInfoEnv = {
3230
};
3331

3432
// @ts-ignore
35-
const noneExec = new ExecScript(scriptRes, undefined, undefined, undefined, envInfo, undefined);
33+
const noneExec = new ExecScript(scriptRes, undefined, undefined, undefined, envInfo);
3634

3735
const scriptRes2 = {
3836
id: 0,
@@ -43,26 +41,27 @@ const scriptRes2 = {
4341
code: "console.log('test')",
4442
sourceCode: "sourceCode",
4543
value: {},
46-
grantMap: {},
4744
} as unknown as ScriptRunResource;
4845

4946
// @ts-ignore
50-
const sandboxExec = new ExecScript(scriptRes2, undefined, undefined, undefined, envInfo, undefined);
47+
const sandboxExec = new ExecScript(scriptRes2, undefined, undefined, undefined, envInfo);
5148

5249
describe("GM_info", () => {
5350
it("none", async () => {
54-
scriptRes.code = "return GM_info";
51+
scriptRes.code = "return {_this:this,GM_info};";
5552
noneExec.scriptFunc = compileScript(compileScriptCode(scriptRes));
5653
const ret = await noneExec.exec();
57-
expect(ret.version).toEqual(ExtVersion);
58-
expect(ret.script.version).toEqual("1.0.0");
54+
expect(ret.GM_info.version).toEqual(ExtVersion);
55+
expect(ret.GM_info.script.version).toEqual("1.0.0");
56+
expect(ret._this).toEqual(global);
5957
});
6058
it("sandbox", async () => {
61-
scriptRes2.code = "return GM_info";
59+
scriptRes2.code = "return {_this:this,GM_info};";
6260
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
6361
const ret = await sandboxExec.exec();
64-
expect(ret.version).toEqual(ExtVersion);
65-
expect(ret.script.version).toEqual("1.0.0");
62+
expect(ret.GM_info.version).toEqual(ExtVersion);
63+
expect(ret.GM_info.script.version).toEqual("1.0.0");
64+
expect(ret._this).toEqual(sandboxExec.proxyContent);
6665
});
6766
});
6867

@@ -129,3 +128,58 @@ describe("sandbox", () => {
129128
expect(ret).toEqual("3");
130129
});
131130
});
131+
132+
describe("this", () => {
133+
it("onload", async () => {
134+
scriptRes2.code = `onload = ()=>{};return onload;`;
135+
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
136+
const ret = await sandboxExec.exec();
137+
expect(ret).toEqual(expect.any(Function));
138+
// global.onload
139+
expect(global.onload).toBeNull();
140+
});
141+
it("this.onload", async () => {
142+
scriptRes2.code = `this.onload = () => "ok"; return this.onload;`;
143+
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
144+
const ret = await sandboxExec.exec();
145+
expect(ret).toEqual(expect.any(Function));
146+
// global.onload
147+
expect(global.onload).toBeNull();
148+
});
149+
it("undefined variable", async () => {
150+
scriptRes2.code = `return typeof testVar;`;
151+
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
152+
const ret = await sandboxExec.exec();
153+
expect(ret).toEqual("undefined");
154+
});
155+
it("undefined variable in global", async () => {
156+
scriptRes2.code = `return testVar;`;
157+
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
158+
// 在沙盒中访问未定义的变量会抛出错误
159+
try {
160+
await sandboxExec.exec();
161+
// 如果没有抛出错误,测试应该失败
162+
expect.fail("Expected an error to be thrown when accessing undefined variable");
163+
} catch (e: any) {
164+
expect(e.message).toContain("testVar is not defined");
165+
}
166+
});
167+
});
168+
169+
describe("none this", () => {
170+
it("onload", async () => {
171+
scriptRes2.code = `onload = ()=>{};return onload;`;
172+
noneExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
173+
const ret = await noneExec.exec();
174+
expect(ret).toEqual(expect.any(Function));
175+
// global.onload
176+
expect(global.onload).toEqual(expect.any(Function));
177+
global.onload = null; // 清理全局变量
178+
});
179+
it("this.test", async () => {
180+
scriptRes2.code = `this.test = "ok";return this.test;`;
181+
noneExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
182+
const ret = await noneExec.exec();
183+
expect(ret).toEqual("ok");
184+
});
185+
});

src/app/service/content/exec_script.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Message } from "@Packages/message/types";
77
import type { ScriptLoadInfo } from "../service_worker/types";
88
import type { ValueUpdateData } from "./types";
99
import { evaluateGMInfo } from "./gm_info";
10-
import { type GM_Base } from "./gm_api";
10+
import { type IGM_Base } from "./gm_api";
1111

1212
// 执行脚本,控制脚本执行与停止
1313
export default class ExecScript {
@@ -17,44 +17,47 @@ export default class ExecScript {
1717

1818
logger: Logger;
1919

20-
proxyContent: any;
20+
proxyContent: typeof globalThis;
2121

22-
sandboxContent?: GM_Base;
22+
sandboxContent?: IGM_Base & { [key: string]: any };
2323

24-
GM_info: any;
24+
named?: { [key: string]: any };
2525

2626
constructor(
2727
scriptRes: ScriptLoadInfo,
2828
envPrefix: "content" | "offscreen",
2929
message: Message,
3030
code: string | ScriptFunc,
3131
envInfo: GMInfoEnv,
32-
thisContext?: { [key: string]: any }
32+
globalInjection?: { [key: string]: any } // 主要是全域API. @grant none 时无效
3333
) {
3434
this.scriptRes = scriptRes;
3535
this.logger = LoggerCore.getInstance().logger({
3636
component: "exec",
3737
uuid: this.scriptRes.uuid,
3838
name: this.scriptRes.name,
3939
});
40-
this.GM_info = evaluateGMInfo(envInfo, this.scriptRes);
40+
const GM_info = evaluateGMInfo(envInfo, this.scriptRes);
4141
// 构建脚本资源
4242
if (typeof code === "string") {
4343
this.scriptFunc = compileScript(code);
4444
} else {
4545
this.scriptFunc = code;
4646
}
47-
const grantMap: { [key: string]: boolean } = {};
48-
scriptRes.metadata.grant?.forEach((key) => {
49-
grantMap[key] = true;
50-
});
51-
if (grantMap.none) {
47+
const grantSet = new Set(scriptRes.metadata.grant || []);
48+
if (grantSet.has("none")) {
5249
// 不注入任何GM api
5350
this.proxyContent = global;
51+
// ScriptCat行为:GM.info 和 GM_info 同时注入
52+
// 不改变Context情况下,以 named 传多於一个全域变量
53+
this.named = {GM: {info: GM_info}, GM_info};
5454
} else {
5555
// 构建脚本GM上下文
56-
this.sandboxContent = createContext(scriptRes, this.GM_info, envPrefix, message);
57-
this.proxyContent = proxyContext(global, this.sandboxContent, thisContext);
56+
this.sandboxContent = createContext(scriptRes, GM_info, envPrefix, message, grantSet);
57+
if (globalInjection) {
58+
Object.assign(this.sandboxContent, globalInjection);
59+
}
60+
this.proxyContent = proxyContext(global, this.sandboxContent);
5861
}
5962
}
6063

@@ -67,9 +70,14 @@ export default class ExecScript {
6770
this.sandboxContent?.valueUpdate(data);
6871
}
6972

73+
/**
74+
* @see {@link compileScriptCode}
75+
* @returns
76+
*/
7077
exec() {
7178
this.logger.debug("script start");
72-
return this.scriptFunc.apply(this.proxyContent, [this.proxyContent, this.GM_info]);
79+
const context = this.proxyContent;
80+
return this.scriptFunc.call(context, this.named, this.scriptRes.name);
7381
}
7482

7583
stop() {

0 commit comments

Comments
 (0)