注入脚本和inject/content通信
Some checks failed
build / Build (push) Failing after 6s
test / Run tests (push) Failing after 2s

This commit is contained in:
2025-03-31 18:06:00 +08:00
parent 48f1b1f33b
commit 315f5f148c
17 changed files with 395 additions and 167 deletions

View File

@ -0,0 +1,133 @@
import { Message, MessageConnect } from "./server";
import { v4 as uuidv4 } from "uuid";
import { PostMessage, WindowMessageBody, WindowMessageConnect } from "./window_message";
import LoggerCore from "@App/app/logger/core";
import EventEmitter from "eventemitter3";
export class CustomEventPostMessage implements PostMessage {
constructor(private send: CustomEventMessage) {}
postMessage(message: any): void {
this.send.nativeSend(message);
}
}
// 使用CustomEvent来进行通讯, 可以在content与inject中传递一些dom对象
export class CustomEventMessage implements Message {
EE: EventEmitter = new EventEmitter();
// 关联dom目标
relatedTarget: Map<number, Element> = new Map();
constructor(
protected flag: string,
protected isContent: boolean
) {
window.addEventListener((isContent ? "ct" : "fd") + flag, (event) => {
if (event instanceof MouseEvent) {
this.relatedTarget.set(event.clientX, <Element>event.relatedTarget);
return;
} else if (event instanceof CustomEvent) {
this.messageHandle(event.detail, new CustomEventPostMessage(this));
}
});
}
messageHandle(data: WindowMessageBody, target: PostMessage) {
// 处理消息
if (data.type === "sendMessage") {
// 接收到消息
this.EE.emit("message", data.data, (resp: any) => {
// 发送响应消息
// 无消息id则不发送响应消息
if (!data.messageId) {
return;
}
const body: WindowMessageBody = {
messageId: data.messageId,
type: "respMessage",
data: resp,
};
target.postMessage(body);
});
} else if (data.type === "respMessage") {
// 接收到响应消息
this.EE.emit("response:" + data.messageId, data);
} else if (data.type === "connect") {
this.EE.emit("connect", data.data, new WindowMessageConnect(data.messageId, this.EE, target));
} else if (data.type === "disconnect") {
this.EE.emit("disconnect:" + data.messageId);
} else if (data.type === "connectMessage") {
this.EE.emit("connectMessage:" + data.messageId, data.data);
}
}
onConnect(callback: (data: any, con: MessageConnect) => void): void {
this.EE.addListener("connect", callback);
}
onMessage(callback: (data: any, sendResponse: (data: any) => void) => void): void {
this.EE.addListener("message", callback);
}
connect(data: any): Promise<MessageConnect> {
return new Promise((resolve) => {
const body: WindowMessageBody = {
messageId: uuidv4(),
type: "connect",
data,
};
this.nativeSend(body);
resolve(new WindowMessageConnect(body.messageId, this.EE, new CustomEventPostMessage(this)));
});
}
nativeSend(data: any) {
let detail = data;
// 特殊处理relatedTarget
if (detail.data && typeof detail.data.relatedTarget === "object") {
// 先将relatedTarget转换成id发送过去
const target = detail.data.relatedTarget;
delete detail.data.relatedTarget;
detail.data.relatedTarget = Math.ceil(Math.random() * 1000000);
// 可以使用此种方式交互element
const ev = new MouseEvent((this.isContent ? "fd" : "ct") + this.flag, {
clientX: detail.data.relatedTarget,
relatedTarget: target,
});
window.dispatchEvent(ev);
}
if (typeof cloneInto !== "undefined") {
try {
LoggerCore.logger().info("nativeSend");
detail = cloneInto(detail, document.defaultView);
} catch (e) {
console.log(e);
LoggerCore.logger().info("error data");
}
}
const ev = new CustomEvent((this.isContent ? "fd" : "ct") + this.flag, {
detail,
});
window.dispatchEvent(ev);
}
sendMessage(data: any): Promise<any> {
return new Promise((resolve) => {
const body: WindowMessageBody = {
messageId: uuidv4(),
type: "sendMessage",
data,
};
const callback = (body: WindowMessageBody) => {
this.EE.removeListener("response:" + body.messageId, callback);
resolve(body.data);
};
this.EE.addListener("response:" + body.messageId, callback);
this.nativeSend(body);
});
}
}

View File

@ -5,7 +5,7 @@ import { Message, MessageConnect, MessageSend } from "./server";
import EventEmitter from "eventemitter3";
interface PostMessage {
export interface PostMessage {
postMessage(message: any): void;
}

View File

@ -28,6 +28,8 @@ export default defineConfig({
service_worker: `${src}/service_worker.ts`,
offscreen: `${src}/offscreen.ts`,
sandbox: `${src}/sandbox.ts`,
content: `${src}/content.ts`,
inject: `${src}/inject.ts`,
popup: `${src}/pages/popup/main.tsx`,
install: `${src}/pages/install/main.tsx`,
options: `${src}/pages/options/main.tsx`,

View File

@ -6,7 +6,7 @@ import { ResourceClient, ScriptClient, ValueClient } from "../service_worker/cli
import { SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptRunResouce } from "@App/app/repo/scripts";
import { disableScript, enableScript, runScript, stopScript } from "../sandbox/client";
import { Group, MessageSend } from "@Packages/message/server";
import { subscribeScriptEnable, subscribeScriptInstall } from "../queue";
import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall } from "../queue";
export class ScriptService {
logger: Logger;
@ -34,7 +34,6 @@ export class ScriptService {
async init() {
subscribeScriptEnable(this.messageQueue, async (data) => {
console.log("subscribeScriptEnable", data);
const script = await this.scriptClient.info(data.uuid);
if (script.type === SCRIPT_TYPE_NORMAL) {
return;
@ -48,7 +47,6 @@ export class ScriptService {
}
});
subscribeScriptInstall(this.messageQueue, async (data) => {
console.log("subscribeScriptInstall", data);
// 判断是开启还是关闭
if (data.script.status === SCRIPT_STATUS_ENABLE) {
// 构造脚本运行资源,发送给沙盒运行
@ -58,6 +56,9 @@ export class ScriptService {
disableScript(this.windowMessage, data.script.uuid);
}
});
subscribeScriptDelete(this.messageQueue, async (data) => {
disableScript(this.windowMessage, data.uuid);
});
this.group.on("runScript", this.runScript.bind(this));
this.group.on("stopScript", this.stopScript.bind(this));

View File

@ -81,4 +81,8 @@ export class RuntimeClient extends Client {
stopScript(uuid: string) {
return this.do("stopScript", uuid);
}
pageLoad(): Promise<{ flag: string; scripts: ScriptRunResouce[] }> {
return this.do("pageLoad");
}
}

View File

@ -1,16 +1,29 @@
import { MessageQueue } from "@Packages/message/message_queue";
import { Group, MessageSend } from "@Packages/message/server";
import { Script, SCRIPT_STATUS_ENABLE, SCRIPT_TYPE_NORMAL, ScriptAndCode, ScriptDAO } from "@App/app/repo/scripts";
import {
Script,
SCRIPT_STATUS_ENABLE,
SCRIPT_TYPE_NORMAL,
ScriptDAO,
ScriptRunResouce,
} from "@App/app/repo/scripts";
import { ValueService } from "./value";
import GMApi from "./gm_api";
import { subscribeScriptEnable } from "../queue";
import { subscribeScriptDelete, subscribeScriptEnable, subscribeScriptInstall } from "../queue";
import { ScriptService } from "./script";
import { runScript, stopScript } from "../offscreen/client";
import { dealMatches } from "./utils";
import { dealMatches, getRunAt } from "./utils";
import { randomString } from "@App/pkg/utils/utils";
import { compileInjectScript, compileScriptCode } from "@App/runtime/content/utils";
export class RuntimeService {
scriptDAO: ScriptDAO = new ScriptDAO();
scriptFlag: string = randomString(8);
// 运行中的页面脚本
runningPageScript = new Map<string, ScriptRunResouce>();
constructor(
private group: Group,
private sender: MessageSend,
@ -20,6 +33,8 @@ export class RuntimeService {
) {}
async init() {
// 读取inject.js注入页面
this.registerInjectScript();
// 监听脚本开启
subscribeScriptEnable(this.mq, async (data) => {
const script = await this.scriptDAO.getAndCode(data.uuid);
@ -37,6 +52,26 @@ export class RuntimeService {
}
}
});
// 监听脚本安装
subscribeScriptInstall(this.mq, async (data) => {
const script = await this.scriptDAO.get(data.script.uuid);
if (!script) {
return;
}
if (script.type === SCRIPT_TYPE_NORMAL) {
this.registryPageScript(script);
}
});
// 监听脚本删除
subscribeScriptDelete(this.mq, async (data) => {
const script = await this.scriptDAO.get(data.uuid);
if (!script) {
return;
}
if (script.type === SCRIPT_TYPE_NORMAL) {
this.unregistryPageScript(script);
}
});
// 将开启的脚本发送一次enable消息
const scriptDao = new ScriptDAO();
@ -63,6 +98,11 @@ export class RuntimeService {
this.group.on("stopScript", this.stopScript.bind(this));
this.group.on("runScript", this.runScript.bind(this));
this.group.on("pageLoad", this.pageLoad.bind(this));
}
pageLoad() {
return Promise.resolve({ flag: this.scriptFlag });
}
// 停止脚本
@ -80,16 +120,41 @@ export class RuntimeService {
return runScript(this.sender, res);
}
registryPageScript(script: ScriptAndCode) {
console.log(script);
registerInjectScript() {
fetch("inject.js")
.then((res) => res.text())
.then((injectJs) => {
// 替换ScriptFlag
const code = `(function (ScriptFlag) {\n${injectJs}\n})('${this.scriptFlag}')`;
chrome.userScripts.register([
{
id: "scriptcat-inject",
js: [{ code }],
matches: ["<all_urls>"],
allFrames: true,
world: "MAIN",
runAt: "document_start",
},
]);
});
}
async registryPageScript(script: Script) {
const matches = script.metadata["match"];
if (!matches) {
return;
}
const scriptRes = await this.script.buildScriptRunResource(script);
scriptRes.code = compileScriptCode(scriptRes);
scriptRes.code = compileInjectScript(scriptRes);
this.runningPageScript.set(scriptRes.uuid, scriptRes);
matches.push(...(script.metadata["include"] || []));
const registerScript: chrome.userScripts.RegisteredUserScript = {
id: script.uuid,
js: [{ code: script.code }],
id: scriptRes.uuid,
js: [{ code: scriptRes.code }],
matches: dealMatches(matches),
world: "MAIN",
};
@ -102,7 +167,7 @@ export class RuntimeService {
registerScript.excludeMatches = dealMatches(excludeMatches);
}
if (script.metadata["run-at"]) {
registerScript.runAt = script.metadata["run-at"][0] as chrome.userScripts.RunAt;
registerScript.runAt = getRunAt(script.metadata["run-at"]);
}
chrome.userScripts.register([registerScript]);
}

View File

@ -273,7 +273,8 @@ export class ScriptService {
if (!code) {
throw new Error("code is null");
}
ret.code = compileScriptCode(ret, code.code);
ret.code = code.code;
ret.code = compileScriptCode(ret);
return Promise.resolve(ret);
}

View File

@ -9,3 +9,16 @@ export function isExtensionRequest(details: chrome.webRequest.ResourceRequest &
export function dealMatches(matches: string[]) {
return matches;
}
export function getRunAt(runAts: string[]): chrome.userScripts.RunAt {
if (runAts.length === 0) {
return "document_idle";
}
const runAt = runAts[0];
if (runAt === "document-start") {
return "document_start";
} else if (runAt === "document-end") {
return "document_end";
}
return "document_idle";
}

View File

@ -1,24 +1,25 @@
import LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer";
import { SandboxManager } from "./app/service/sandbox";
import { ExtensionMessageSend } from "@Packages/message/extension_message";
import { CustomEventMessage } from "@Packages/message/custom_event_message";
import { RuntimeClient } from "./app/service/service_worker/client";
import ContentRuntime from "./runtime/content/content";
function main() {
// 建立与service_worker页面的连接
const msg = new ExtensionMessageSend();
// 建立与service_worker页面的连接
const send = new ExtensionMessageSend();
// 初始化日志组件
const loggerCore = new LoggerCore({
writer: new MessageWriter(msg),
labels: { env: "content" },
});
// 初始化日志组件
const loggerCore = new LoggerCore({
writer: new MessageWriter(send),
labels: { env: "content" },
});
const client = new RuntimeClient(send);
client.pageLoad().then((data) => {
loggerCore.logger().debug("content start");
console.log("content", data);
const msg = new CustomEventMessage(data.flag, true);
// 初始化运行环境
const contentMessage = new MessageContent(scriptFlag, true);
const runtime = new ContentRuntime(contentMessage, internalMessage);
runtime.start();
}
main();
const runtime = new ContentRuntime(send, msg);
runtime.start(data.scripts);
});

26
src/inject.ts Normal file
View File

@ -0,0 +1,26 @@
import LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer";
import { CustomEventMessage } from "@Packages/message/custom_event_message";
import { Server } from "@Packages/message/server";
import { InjectRuntime } from "./runtime/content/inject";
import { ScriptRunResouce } from "./app/repo/scripts";
// 通过flag与content建立通讯,这个ScriptFlag是后端注入时候生成的
const flag = ScriptFlag;
const msg = new CustomEventMessage(flag, false);
// 加载logger组件
const logger = new LoggerCore({
writer: new MessageWriter(msg),
labels: { env: "inject", href: window.location.href },
});
const server = new Server("inject", msg);
server.on("pageLoad", (data: ScriptRunResouce[]) => {
logger.logger().debug("inject start");
console.log("inject", data);
const runtime = new InjectRuntime(msg, data);
runtime.start();
});

View File

@ -1,23 +0,0 @@
import LoggerCore from "./app/logger/core";
import MessageWriter from "./app/logger/message_writer";
import MessageContent from "./app/message/content";
import InjectRuntime from "./runtime/content/inject";
// 通过flag与content建立通讯,这个ScriptFlag是后端注入时候生成的
// eslint-disable-next-line no-undef
const flag = ScriptFlag;
const message = new MessageContent(flag, false);
// 加载logger组件
const logger = new LoggerCore({
debug: process.env.NODE_ENV === "development",
writer: new MessageWriter(message),
labels: { env: "inject", href: window.location.href },
});
message.setHandler("pageLoad", (_action, data) => {
logger.logger().debug("inject start");
const runtime = new InjectRuntime(message, data.scripts, flag);
runtime.start();
});

View File

@ -17,6 +17,18 @@
"128": "assets/logo.png"
}
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"src/content.js"
],
"run_at": "document_start",
"all_frames": true
}
],
"icons": {
"128": "assets/logo.png"
},

View File

@ -1,115 +1,94 @@
import { ExternalMessage } from "@App/app/const";
import MessageContent from "@App/app/message/content";
import MessageInternal from "@App/app/message/internal";
import { MessageHander, MessageManager } from "@App/app/message/message";
import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Client } from "@Packages/message/client";
import { Message, MessageSend } from "@Packages/message/server";
// content页的处理
export default class ContentRuntime {
contentMessage: MessageHander & MessageManager;
internalMessage: MessageInternal;
constructor(
contentMessage: MessageHander & MessageManager,
internalMessage: MessageInternal
) {
this.contentMessage = contentMessage;
this.internalMessage = internalMessage;
}
private send: MessageSend,
private msg: Message
) {}
start(resp: { scripts: ScriptRunResouce[] }) {
start(scripts: ScriptRunResouce[]) {
// 由content到background
// 转发gmApi消息
this.contentMessage.setHandler("gmApi", (action, data) => {
return this.internalMessage.syncSend(action, data);
});
// 转发log消息
this.contentMessage.setHandler("log", (action, data) => {
this.internalMessage.send(action, data);
});
// 转发externalMessage消息
this.contentMessage.setHandler(ExternalMessage, (action, data) => {
return this.internalMessage.syncSend(action, data);
});
// this.contentMessage.setHandler("gmApi", (action, data) => {
// return this.internalMessage.syncSend(action, data);
// });
// // 转发log消息
// this.contentMessage.setHandler("log", (action, data) => {
// this.internalMessage.send(action, data);
// });
// // 转发externalMessage消息
// this.contentMessage.setHandler(ExternalMessage, (action, data) => {
// return this.internalMessage.syncSend(action, data);
// });
// 处理GM_addElement
// @ts-ignore
this.contentMessage.setHandler("GM_addElement", (action, data) => {
const parma = data.param;
let attr: { [x: string]: any; textContent?: any };
let textContent = "";
if (!parma[1]) {
attr = {};
} else {
attr = { ...parma[1] };
if (attr.textContent) {
textContent = attr.textContent;
delete attr.textContent;
}
}
const el = <Element>document.createElement(parma[0]);
Object.keys(attr).forEach((key) => {
el.setAttribute(key, attr[key]);
});
if (textContent) {
el.innerHTML = textContent;
}
let parentNode;
if (data.relatedTarget) {
parentNode = (<MessageContent>(
this.contentMessage
)).getAndDelRelatedTarget(data.relatedTarget);
}
(
<Element>parentNode ||
document.head ||
document.body ||
document.querySelector("*")
).appendChild(el);
return {
relatedTarget: el,
};
});
// 转发长连接的gmApi消息
this.contentMessage.setHandlerWithChannel(
"gmApiChannel",
(inject, action, data) => {
const background = this.internalMessage.channel();
// 转发inject->background
inject.setHandler((req) => {
background.send(req.data);
});
inject.setCatch((err) => {
background.throw(err);
});
inject.setDisChannelHandler(() => {
background.disChannel();
});
// 转发background->inject
background.setHandler((bgResp) => {
inject.send(bgResp);
});
background.setCatch((err) => {
inject.throw(err);
});
background.setDisChannelHandler(() => {
inject.disChannel();
});
// 建立连接
background.channel(action, data);
}
);
this.listenCATApi();
// 由background到content
// 转发value更新事件
this.internalMessage.setHandler("valueUpdate", (action, data) => {
this.contentMessage.send(action, data);
});
this.contentMessage.send("pageLoad", resp);
// this.contentMessage.setHandler("GM_addElement", (action, data) => {
// const parma = data.param;
// let attr: { [x: string]: any; textContent?: any };
// let textContent = "";
// if (!parma[1]) {
// attr = {};
// } else {
// attr = { ...parma[1] };
// if (attr.textContent) {
// textContent = attr.textContent;
// delete attr.textContent;
// }
// }
// const el = <Element>document.createElement(parma[0]);
// Object.keys(attr).forEach((key) => {
// el.setAttribute(key, attr[key]);
// });
// if (textContent) {
// el.innerHTML = textContent;
// }
// let parentNode;
// if (data.relatedTarget) {
// parentNode = (<MessageContent>this.contentMessage).getAndDelRelatedTarget(data.relatedTarget);
// }
// (<Element>parentNode || document.head || document.body || document.querySelector("*")).appendChild(el);
// return {
// relatedTarget: el,
// };
// });
// // 转发长连接的gmApi消息
// this.contentMessage.setHandlerWithChannel("gmApiChannel", (inject, action, data) => {
// const background = this.internalMessage.channel();
// // 转发inject->background
// inject.setHandler((req) => {
// background.send(req.data);
// });
// inject.setCatch((err) => {
// background.throw(err);
// });
// inject.setDisChannelHandler(() => {
// background.disChannel();
// });
// // 转发background->inject
// background.setHandler((bgResp) => {
// inject.send(bgResp);
// });
// background.setCatch((err) => {
// inject.throw(err);
// });
// background.setDisChannelHandler(() => {
// inject.disChannel();
// });
// // 建立连接
// background.channel(action, data);
// });
// this.listenCATApi();
// // 由background到content
// // 转发value更新事件
// this.internalMessage.setHandler("valueUpdate", (action, data) => {
// this.contentMessage.send(action, data);
// });
// this.msg.sendMessage({ action: "pageLoad", data: { scripts } });
const client = new Client(this.msg, "inject");
client.do("pageLoad", { scripts });
}
listenCATApi() {
@ -117,16 +96,13 @@ export default class ContentRuntime {
this.contentMessage.setHandler("CAT_fetchBlob", (_action, data: string) => {
return fetch(data).then((res) => res.blob());
});
this.contentMessage.setHandler(
"CAT_createBlobUrl",
(_action, data: Blob) => {
const url = URL.createObjectURL(data);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 60 * 1000);
return Promise.resolve(url);
}
);
this.contentMessage.setHandler("CAT_createBlobUrl", (_action, data: Blob) => {
const url = URL.createObjectURL(data);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 60 * 1000);
return Promise.resolve(url);
});
// 处理CAT_fetchDocument
this.contentMessage.setHandler("CAT_fetchDocument", (_action, data) => {
return new Promise((resolve) => {

View File

@ -0,0 +1,11 @@
import { ScriptRunResouce } from "@App/app/repo/scripts";
import { Message } from "@Packages/message/server";
export class InjectRuntime {
constructor(
private msg: Message,
private scripts: ScriptRunResouce[]
) {}
start(){}
}

View File

@ -5,7 +5,7 @@ import { has } from "@App/pkg/utils/lodash";
import { Message } from "@Packages/message/server";
// 构建脚本运行代码
export function compileScriptCode(scriptRes: ScriptRunResouce, code: string): string {
export function compileScriptCode(scriptRes: ScriptRunResouce): string {
let require = "";
if (scriptRes.metadata.require) {
scriptRes.metadata.require.forEach((val) => {
@ -15,7 +15,7 @@ export function compileScriptCode(scriptRes: ScriptRunResouce, code: string): st
}
});
}
code = require + code;
const code = require + scriptRes.code;
return `with (context) return (async ()=>{\n${code}\n//# sourceURL=${chrome.runtime.getURL(
`/${encodeURI(scriptRes.name)}.user.js`
)}\n})()`;

View File

@ -55,7 +55,8 @@ async function main() {
});
loggerCore.logger().debug("service worker start");
// 初始化管理器
const server = new Server("serviceWorker", new ExtensionMessage());
const message = new ExtensionMessage();
const server = new Server("serviceWorker", message);
const manager = new ServiceWorkerManager(server, new MessageQueue(), new ServiceWorkerMessageSend());
manager.initManager();
// 初始化沙盒环境

5
src/types/main.d.ts vendored
View File

@ -7,6 +7,11 @@ declare const sandbox: Window;
declare const self: ServiceWorkerGlobalScope;
declare const ScriptFlag: string;
// 可以让content与inject环境交换携带dom的对象
declare let cloneInto: ((detail: any, view: any) => any) | undefined;
declare namespace GMSend {
interface XHRDetails {
method?: "GET" | "HEAD" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS";