diff --git a/packages/message/server.ts b/packages/message/server.ts index 7aed1f4..ade01be 100644 --- a/packages/message/server.ts +++ b/packages/message/server.ts @@ -39,9 +39,7 @@ export class Server { private logger = LoggerCore.getInstance().logger({ service: "messageServer" }); constructor(prefix: string, message: Message) { - console.log("constructor", prefix, message); message.onConnect((msg: any, con: MessageConnect) => { - console.log("onConnect", this.apiFunctionMap, this.apiFunctionMap.size); this.logger.trace("server onConnect", { msg }); if (msg.action.startsWith(prefix)) { return this.connectHandle(msg.action.slice(prefix.length + 1), msg.data, con); @@ -50,7 +48,6 @@ export class Server { }); message.onMessage((msg: { action: string; data: any }, sendResponse, sender) => { - console.log("onConnect", this.apiFunctionMap, this.apiFunctionMap.size); this.logger.trace("server onMessage", { msg: msg as any }); if (msg.action.startsWith(prefix)) { return this.messageHandle(msg.action.slice(prefix.length + 1), msg.data, sendResponse, sender); diff --git a/src/app/logger/core.ts b/src/app/logger/core.ts index 7be7787..530bf56 100644 --- a/src/app/logger/core.ts +++ b/src/app/logger/core.ts @@ -12,6 +12,10 @@ export interface Writer { write(level: LogLevel, message: string, label: LogLabel): void; } +export class EmptyWriter implements Writer { + write(): void {} +} + export default class LoggerCore { static instance: LoggerCore; diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index e0cd17d..9bce727 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -85,4 +85,8 @@ export class RuntimeClient extends Client { pageLoad(): Promise<{ flag: string; scripts: ScriptRunResouce[] }> { return this.do("pageLoad"); } + + scriptLoad(flag: string, uuid: string) { + return this.do("scriptLoad", { flag, uuid }); + } } diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index f2b9e70..c4c0f73 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -8,9 +8,9 @@ import { ScriptService } from "./script"; import { runScript, stopScript } from "../offscreen/client"; import { getRunAt } from "./utils"; import { randomString } from "@App/pkg/utils/utils"; -import { compileInjectScript, compileScriptCode } from "@App/runtime/content/utils"; +import { compileInjectScript, compileInjectScriptInfo, compileScriptCode } from "@App/runtime/content/utils"; import Cache from "@App/app/cache"; -import { dealMatches } from "@App/pkg/utils/match"; +import { dealPatternMatches } from "@App/pkg/utils/match"; export class RuntimeService { scriptDAO: ScriptDAO = new ScriptDAO(); @@ -92,16 +92,17 @@ export class RuntimeService { }); } - scriptFlag() { - return Cache.getInstance().getOrSet("scriptInjectFlag", () => { + messageFlag() { + return Cache.getInstance().getOrSet("scriptInjectMessageFlag", () => { return Promise.resolve(randomString(16)); }); } async pageLoad(_, sender: GetSender) { - const scriptFlag = await this.scriptFlag(); + const scriptFlag = await this.messageFlag(); const chromeSender = sender.getSender() as chrome.runtime.MessageSender; // 匹配当前页面的脚本 + console.log("pageLoad"); return Promise.resolve({ flag: scriptFlag }); } @@ -128,7 +129,7 @@ export class RuntimeService { .then((res) => res.text()) .then(async (injectJs) => { // 替换ScriptFlag - const code = `(function (ScriptFlag) {\n${injectJs}\n})('${await this.scriptFlag()}')`; + const code = `(function (MessageFlag) {\n${injectJs}\n})('${await this.messageFlag()}')`; chrome.userScripts.register([ { id: "scriptcat-inject", @@ -168,19 +169,23 @@ export class RuntimeService { scriptRes.code = compileInjectScript(scriptRes); matches.push(...(script.metadata["include"] || [])); + const patternMatches = dealPatternMatches(matches); const registerScript: chrome.userScripts.RegisteredUserScript = { id: scriptRes.uuid, js: [{ code: scriptRes.code }], - matches: dealMatches(matches), + matches: patternMatches.patternResult, world: "MAIN", }; if (!script.metadata["noframes"]) { registerScript.allFrames = true; } + if (script.metadata["exclude-match"]) { const excludeMatches = script.metadata["exclude-match"]; excludeMatches.push(...(script.metadata["exclude"] || [])); - registerScript.excludeMatches = dealMatches(excludeMatches); + const result= dealPatternMatches(excludeMatches); + + registerScript.excludeMatches =result.patternResult; } if (script.metadata["run-at"]) { registerScript.runAt = getRunAt(script.metadata["run-at"]); @@ -190,13 +195,22 @@ export class RuntimeService { Cache.getInstance().set("registryScript:" + script.uuid, true); }); console.log(registerScript); - // 把脚本uuid注册到content页面 + // 把脚本信息注入到USER_SCRIPT环境中 chrome.userScripts.register([ { - id: "content-" + scriptRes.uuid, - js: [{ code: "window.a=1;console.log('window.a',window,window.b)" }], - matches: dealMatches(matches), + id: "scriptinfo-" + scriptRes.uuid, + js: [ + { + code: compileInjectScriptInfo( + await this.messageFlag(), + scriptRes, + await (await fetch("inject_script_info.js")).text() + ), + }, + ], + matches: dealPatternMatches(matches), runAt: "document_start", + world: "USER_SCRIPT", }, ]); } diff --git a/src/content.ts b/src/content.ts index c14d11c..98e4e89 100644 --- a/src/content.ts +++ b/src/content.ts @@ -23,7 +23,3 @@ client.pageLoad().then((data) => { const runtime = new ContentRuntime(send, msg); runtime.start(data.scripts); }); - -chrome.storage.local.get((data) => { - console.log(data); -}); diff --git a/src/inject.ts b/src/inject.ts index d5fa843..c552361 100644 --- a/src/inject.ts +++ b/src/inject.ts @@ -5,10 +5,7 @@ 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); +const msg = new CustomEventMessage(MessageFlag, false); // 加载logger组件 const logger = new LoggerCore({ diff --git a/src/pkg/utils/match.test.ts b/src/pkg/utils/match.test.ts index fd09332..a0fb675 100644 --- a/src/pkg/utils/match.test.ts +++ b/src/pkg/utils/match.test.ts @@ -1,10 +1,157 @@ import { describe, expect, it } from "vitest"; -import { dealMatches } from "./match"; +import { dealPatternMatches, parsePatternMatchesURL, UrlMatch } from "./match"; // https://developer.chrome.com/docs/extensions/mv3/match_patterns/ -describe("dealMatches", () => { - it("*://link.17173.com*", () => { - const matches = dealMatches(["*://link.17173.com*"]); - expect(matches).toEqual(["*://link.17173.com/*"]); +describe("UrlMatch-google", () => { + const url = new UrlMatch(); + url.add("https://*/*", "ok1"); + url.add("https://*/foo*", "ok2"); + url.add("https://*.google.com/foo*bar", "ok3"); + url.add("https://example.org/foo/bar.html", "ok4"); + url.add("http://127.0.0.1/*", "ok5"); + url.add("*://mail.google.com/*", "ok6"); + it("match1", () => { + expect(url.match("https://www.google.com/")).toEqual(["ok1"]); + expect(url.match("https://example.org/foo/bar.html")).toEqual(["ok1", "ok2", "ok4"]); + }); + it("match2", () => { + expect(url.match("https://example.com/foo/bar.html")).toEqual(["ok1", "ok2"]); + expect(url.match("https://www.google.com/foo")).toEqual(["ok1", "ok2"]); + expect(url.match("https://www.google.com/foo2")).toEqual(["ok1", "ok2"]); + }); + it("match3", () => { + expect(url.match("https://www.google.com/foo/baz/bar")).toEqual(["ok1", "ok2", "ok3"]); + expect(url.match("https://docs.google.com/foobar")).toEqual(["ok1", "ok2", "ok3"]); + }); + it("match4", () => { + expect(url.match("https://example.org/foo/bar.html")).toEqual(["ok1", "ok2", "ok4"]); + }); + it("match5", () => { + expect(url.match("http://127.0.0.1/")).toEqual(["ok5"]); + expect(url.match("http://127.0.0.1/foo/bar.html")).toEqual(["ok5"]); + }); + it("match6", () => { + expect(url.match("http://mail.google.com/foo/baz/bar")).toEqual(["ok6"]); + expect(url.match("https://mail.google.com/foobar")).toEqual(["ok1", "ok2", "ok3", "ok6"]); + }); +}); + +describe("UrlMatch-google-error", () => { + const url = new UrlMatch(); + it("error-1", () => { + expect(() => { + url.add("https://*foo/bar", "ok1"); + }).toThrow(Error); + }); + it("error-2", () => { + expect(() => { + url.add("https://foo.*.bar/baz", "ok1"); + }).toThrow(Error); + }); + it("error-3", () => { + expect(() => { + url.add("http:/bar", "ok1"); + }).toThrow(Error); + }); +}); + +// 从tm找的一些特殊的匹配规则 +describe("UrlMatch-search", () => { + const url = new UrlMatch(); + url.add("https://www.google.com/search?q=*", "ok1"); + it("match1", () => { + expect(url.match("https://www.google.com/search?q=foo")).toEqual(["ok1"]); + expect(url.match("https://www.google.com/search?q1=foo")).toEqual([]); + }); + + url.add("https://bbs.tampermonkey.net.cn", "ok2"); + it("match2", () => { + expect(url.match("https://bbs.tampermonkey.net.cn")).toEqual(["ok2"]); + expect(url.match("https://bbs.tampermonkey.net.cn/")).toEqual(["ok2"]); + expect(url.match("https://bbs.tampermonkey.net.cn/foo/bar.html")).toEqual([]); + }); +}); + +describe("UrlMatch-port1", () => { + const url = new UrlMatch(); + url.add("http://test.list.ggnb.top/search", "ok1"); + it("match1", () => { + expect(url.match("http://test.list.ggnb.top/search")).toEqual(["ok1"]); + expect(url.match("http://test.list.ggnb.top/search?")).toEqual([]); + expect(url.match("http://test.list.ggnb.top/search?foo=bar")).toEqual([]); + }); + + it("port", () => { + expect(url.match("http://test.list.ggnb.top:80/search")).toEqual(["ok1"]); + }); +}); + +describe("UrlMatch-port2", () => { + const url = new UrlMatch(); + url.add("http://test.list.ggnb.top:80/search", "ok1"); + url.add("http://test.list.ggnb.top*/search", "ok2"); + url.add("http://test.list.ggnb.top:*/search", "ok3"); + url.add("http://localhost:3000/", "ok4"); + it("match1", () => { + expect(url.match("http://test.list.ggnb.top:80/search")).toEqual(["ok1", "ok2", "ok3"]); + expect(url.match("http://test.list.ggnb.top:81/search")).toEqual(["ok2", "ok3"]); + expect(url.match("http://test.list.ggnb.top/search")).toEqual(["ok1", "ok2", "ok3"]); + }); + it("case2", () => { + expect(url.match("http://localhost:3000/")).toEqual(["ok4"]); + expect(url.match("http://localhost:8000/")).toEqual([]); + }); +}); + +// https://developer.chrome.com/docs/extensions/mv3/match_patterns/ +describe("dealPatternMatches", () => { + it("https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn#examples", () => { + const matches = dealPatternMatches(["https://*/*", "http://127.0.0.1/*", "http://127.0.0.1/"]); + expect(matches.patternResult).toEqual(["https://*/*", "http://127.0.0.1/*", "http://127.0.0.1/"]); + }); + // 处理一些特殊情况 + it("*://link.17173.com*", () => { + const matches = dealPatternMatches(["*://link.17173.com*"]); + expect(matches.patternResult).toEqual(["*://link.17173.com/*"]); + }); +}); + +describe("parsePatternMatchesURL", () => { + it("https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn#examples", () => { + const matches = parsePatternMatchesURL("https://*/*"); + expect(matches).toEqual({ + scheme: "https", + host: "*", + path: "*", + }); + const matches2 = parsePatternMatchesURL("https://*/foo*"); + expect(matches2).toEqual({ + scheme: "https", + host: "*", + path: "foo*", + }); + const matches3 = parsePatternMatchesURL("http://127.0.0.1/"); + expect(matches3).toEqual({ + scheme: "http", + host: "127.0.0.1", + path: "", + }); + }); + it("search", () => { + // 会忽略掉search部分 + const matches = parsePatternMatchesURL("https://*/*?search"); + expect(matches).toEqual({ + scheme: "https", + host: "*", + path: "*", + }); + }); + it("*://link.17173.com*", () => { + const matches = parsePatternMatchesURL("*://link.17173.com*"); + expect(matches).toEqual({ + scheme: "*", + host: "link.17173.com", + path: "*", + }); }); }); diff --git a/src/pkg/utils/match.ts b/src/pkg/utils/match.ts index 2f03c68..f7a6651 100644 --- a/src/pkg/utils/match.ts +++ b/src/pkg/utils/match.ts @@ -1,3 +1,5 @@ +import Logger from "@App/app/logger/logger"; + export interface Url { scheme: string; host: string; @@ -5,50 +7,269 @@ export interface Url { search: string; } -// 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行匹配 -export class Match {} +export default class Match { + protected cache = new Map(); -export function parseURL(url: string): Url | undefined { - const match = /^(.+?):\/\/(.*?)((\/.*?)(\?.*?|)|)$/.exec(url); - if (match) { - return { - scheme: match[1], - host: match[2], - path: match[4] || (url[url.length - 1] === "*" ? "*" : "/"), - search: match[5], - }; - } - // 处理一些特殊情况 - switch (url) { - case "*": + protected rule = new Map(); + + protected parseURL(url: string): Url | undefined { + if (url.indexOf("*http") === 0) { + url = url.substring(1); + } + const match = /^(.+?):\/\/(.*?)((\/.*?)(\?.*?|)|)$/.exec(url); + if (match) { return { - scheme: "*", - host: "*", - path: "*", - search: "*", + scheme: match[1], + host: match[2], + path: match[4] || (url[url.length - 1] === "*" ? "*" : "/"), + search: match[5], }; - default: + } + // 处理一些特殊情况 + switch (url) { + case "*": + return { + scheme: "*", + host: "*", + path: "*", + search: "*", + }; + default: + } + return undefined; + } + + protected compileRe(url: string): string { + const u = this.parseURL(url); + if (!u) { + return ""; + } + switch (u.scheme) { + case "*": + u.scheme = ".+?"; + break; + case "http*": + u.scheme = "http[s]?"; + break; + default: + } + let pos = u.host.indexOf("*"); + if (u.host === "*" || u.host === "**") { + pos = -1; + } else if (u.host.endsWith("*")) { + // 处理*结尾 + if (!u.host.endsWith(":*")) { + u.host = u.host.substring(0, u.host.length - 1); + } + } else if (pos !== -1 && pos !== 0) { + return ""; + } + u.host = u.host.replace(/\*/g, "[^/]*?"); + // 处理 *.开头 + if (u.host.startsWith("[^/]*?.")) { + u.host = `([^/]*?\\.?)${u.host.substring(7)}`; + } else if (pos !== -1) { + if (u.host.indexOf(".") === -1) { + return ""; + } + } + // 处理顶域 + if (u.host.endsWith("tld")) { + u.host = `${u.host.substr(0, u.host.length - 3)}.*?`; + } + // 处理端口 + const pos2 = u.host.indexOf(":"); + if (pos2 === -1) { + u.host = `${u.host}(:\\d+)?`; + } else { + const port = u.host.substring(pos2 + 1); + if (port === "*") { + u.host = `${u.host.substring(0, pos2)}(:\\d+)?`; + } else { + u.host = `${u.host.substring(0, pos2)}(:${port})?`; + } + } + let re = `^${u.scheme}://${u.host}`; + if (u.path === "/") { + re += "[/]?"; + } else { + re += u.path.replace(/\*/g, ".*?"); + } + if (u.search) { + re += u.search.replace(/([\\?])/g, "\\$1").replace(/\*/g, ".*?"); + } + return `${re.replace(/\//g, "/")}$`; + } + + public add(url: string, val: T) { + const re = this.compileRe(url); + if (!re) { + throw new Error(`invalid url: ${url}`); + } + let rule = this.rule.get(re); + if (!rule) { + rule = []; + this.rule.set(re, rule); + } + rule.push(val); + this.delCache(); + } + + public match(url: string): T[] { + let ret = this.cache.get(url); + if (ret) { + return ret; + } + ret = []; + try { + this.rule.forEach((val, key) => { + const re = new RegExp(key); + if (re.test(url) && ret) { + ret.push(...val); + } + }); + } catch (e) { + // eslint-disable-next-line no-console + console.warn("bad match rule", Logger.E(e)); + // LoggerCore.getLogger({ component: "match" }).warn( + // "bad match rule", + // Logger.E(e) + // ); + } + this.cache.set(url, ret); + return ret; + } + + protected static getId(val: any): string { + if (typeof val === "object") { + return (<{ uuid: string }>(val)).uuid; + } + return (val); + } + + public del(val: T) { + const id = Match.getId(val); + this.rule.forEach((rules, key) => { + const tmp: T[] = []; + rules.forEach((rule) => { + if (Match.getId(rule) !== id) { + tmp.push(rule); + } + }); + if (tmp) { + this.rule.set(key, tmp); + } else { + this.rule.delete(key); + } + }); + this.delCache(); + } + + protected delCache() { + this.cache.clear(); } - return undefined; } -// 处理油猴的match和include为chrome的matches -export function dealMatches(matches: string[]) { - const result: string[] = []; - for (let i = 0; i < matches.length; i++) { - const url = parseURL(matches[i]); - if (url) { - // *开头但是不是*.的情况 - if (url.host.startsWith("*")) { - if (!url.host.startsWith("*.")) { - // 删除开头的*号 - url.host = url.host.slice(1); - } - } else if (url.host.endsWith("*")) { - url.host = url.host.slice(0, -1); +export class UrlMatch extends Match { + protected excludeMatch = new Match(); + + public exclude(url: string, val: T) { + this.excludeMatch.add(url, val); + } + + public del(val: T): void { + super.del(val); + this.excludeMatch.del(val); + this.cache.clear(); + } + + public match(url: string): T[] { + const cache = this.cache.get(url); + if (cache) { + return cache; + } + let ret = super.match(url); + // 排除 + const includeMap = new Map(); + ret.forEach((val) => { + includeMap.set(Match.getId(val), val); + }); + const exclude = this.excludeMatch.match(url); + const excludeMap = new Map(); + exclude.forEach((val) => { + excludeMap.set(Match.getId(val), 1); + }); + ret = []; + includeMap.forEach((val: T, key) => { + if (!excludeMap.has(key)) { + ret.push(val); + } + }); + this.cache.set(url, ret); + return ret; + } +} + +export interface PatternMatchesUrl { + scheme: string; + host: string; + path: string; +} + +// 解析URL, 根据https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns?hl=zh-cn进行处理 +export function parsePatternMatchesURL(url: string): PatternMatchesUrl | undefined { + let result: PatternMatchesUrl | undefined; + const match = /^(.+?):\/\/(.*?)(\/(.*?)(\?.*?|)|)$/.exec(url); + if (match) { + result = { + scheme: match[1], + host: match[2], + path: match[4] || (url[url.length - 1] === "*" ? "*" : ""), + }; + } else { + // 处理一些特殊情况 + switch (url) { + case "*": + result = { + scheme: "*", + host: "*", + path: "*", + }; + break; + default: + } + } + if (result) { + if (result.host !== "*") { + // *开头但是不是*.的情况 + if (result.host.startsWith("*")) { + if (!result.host.startsWith("*.")) { + // 删除开头的*号 + result.host = result.host.slice(1); + } + } + // 结尾是*的情况 + if (result.host.endsWith("*")) { + result.host = result.host.slice(0, -1); } - result.push(`${url.scheme}://${url.host}/${url.path}` + (url.search ? "?" + url.search : "")); } } return result; } + +// 处理油猴的match和include为chrome的pattern-matche +export function dealPatternMatches(matches: string[]) { + const patternResult: string[] = []; + const result: string[] = []; + for (let i = 0; i < matches.length; i++) { + const url = parsePatternMatchesURL(matches[i]); + if (url) { + patternResult.push(`${url.scheme}://${url.host}/${url.path}`); + result.push(matches[i]); + } + } + return { + patternResult, + result, + }; +} diff --git a/src/runtime/content/content.ts b/src/runtime/content/content.ts index f7074fd..05d2f3a 100644 --- a/src/runtime/content/content.ts +++ b/src/runtime/content/content.ts @@ -10,6 +10,10 @@ export default class ContentRuntime { ) {} start(scripts: ScriptRunResouce[]) { + console.log("onMessage"); + this.msg.onMessage((msg, sendResponse) => { + console.log("content onMessage", msg); + }); // 由content到background // 转发gmApi消息 // this.contentMessage.setHandler("gmApi", (action, data) => { diff --git a/src/runtime/content/utils.ts b/src/runtime/content/utils.ts index aba26d7..8cad84e 100644 --- a/src/runtime/content/utils.ts +++ b/src/runtime/content/utils.ts @@ -29,8 +29,18 @@ export function compileScript(code: string): ScriptFunc { } export function compileInjectScript(script: ScriptRunResouce): string { + return `window['${script.flag}']=function(context,GM_info){\n${script.code}\n}`; +} + +// 编译注入脚本信息 +export function compileInjectScriptInfo( + messageFlag: string, + script: ScriptRunResouce, + injectScriptInfoCode: string +): string { return ( - `console.log(window,'` + script.flag + `');window['${script.flag}']=function(context,GM_info){\n${script.code}\n}` + `(function (MessageFlag, ScriptFlag, ScriptUuid) {\n${injectScriptInfoCode}\n})` + + `('${messageFlag}', '${script.flag}', '${script.uuid}');` ); } diff --git a/src/types/main.d.ts b/src/types/main.d.ts index 78fcfc5..490c50a 100644 --- a/src/types/main.d.ts +++ b/src/types/main.d.ts @@ -7,7 +7,7 @@ declare const sandbox: Window; declare const self: ServiceWorkerGlobalScope; -declare const ScriptFlag: string; +declare const MessageFlag: string; // 可以让content与inject环境交换携带dom的对象 declare let cloneInto: ((detail: any, view: any) => any) | undefined;