diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index a1b4d67..c879f39 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -1,10 +1,10 @@ -name: 'Publish to npm' +name: "Publish to npm" on: release: types: [published] push: tags: - - '*.*.*-build.*' + - "*.*.*-build.*" jobs: release: @@ -17,8 +17,8 @@ jobs: - name: Set up NodeJS LTS uses: actions/setup-node@v6 with: - node-version: 'lts/*' - registry-url: 'https://registry.npmjs.org' + node-version: "lts/*" + registry-url: "https://registry.npmjs.org" - name: Update npm package manager run: npm install -g npm@latest - name: Checkout @@ -29,11 +29,17 @@ jobs: run: mkdir -p ~/.pnpm-store && pnpm config set store-dir ~/.pnpm-store - name: Install dependencies run: pnpm install --no-lockfile - - name: Bump package.json version from tag + - name: Bump @filteringdev/tinyshield package.json version from tag uses: TypescriptPrime/bump-packagejson-version@72720c4d073ed0c5b9e9393d7334abfe3fabb47c + - name: Bump @filteringdev/tinyshield-lib package.json version from tag + run: npm pkg set version="${GITHUB_REF_NAME#v}" -w libs - name: Build run: npm run build - - name : Publish to npm + - name: Publish @filteringdev/tinyshield to npm + working-directory: . + run: npm publish --access public + - name: Publish @filteringdev/tinyshield-lib to npm + working-directory: libs run: npm publish --access public if: ${{ github.event_name == 'release' && github.event.release.prerelease == false }} beta-release: @@ -46,8 +52,8 @@ jobs: - name: Set up NodeJS LTS uses: actions/setup-node@v6 with: - node-version: 'lts/*' - registry-url: 'https://registry.npmjs.org' + node-version: "lts/*" + registry-url: "https://registry.npmjs.org" - name: Update npm package manager run: npm install -g npm@latest - name: Checkout @@ -58,11 +64,17 @@ jobs: run: mkdir -p ~/.pnpm-store && pnpm config set store-dir ~/.pnpm-store - name: Install dependencies run: pnpm install --no-lockfile - - name: Bump package.json version from tag + - name: Bump @filteringdev/tinyshield package.json version from tag uses: TypescriptPrime/bump-packagejson-version@72720c4d073ed0c5b9e9393d7334abfe3fabb47c + - name: Bump @filteringdev/tinyshield-lib package.json version from tag + run: npm pkg set version="${GITHUB_REF_NAME#v}" -w libs - name: Build run: npm run build - - name : Publish to npm + - name: Publish @filteringdev/tinyshield to npm + working-directory: . + run: npm publish --tag beta --access public + - name: Publish @filteringdev/tinyshield-lib to npm + working-directory: libs run: npm publish --tag beta --access public if: ${{ github.event_name == 'release' && github.event.release.prerelease == true && contains(github.event.release.tag_name, '-beta.') }} build-release: @@ -75,8 +87,8 @@ jobs: - name: Set up NodeJS LTS uses: actions/setup-node@v6 with: - node-version: 'lts/*' - registry-url: 'https://registry.npmjs.org' + node-version: "lts/*" + registry-url: "https://registry.npmjs.org" - name: Update npm package manager run: npm install -g npm@latest - name: Checkout @@ -87,11 +99,17 @@ jobs: run: mkdir -p ~/.pnpm-store && pnpm config set store-dir ~/.pnpm-store - name: Install dependencies run: pnpm install --no-lockfile - - name: Bump package.json version from tag + - name: Bump @filteringdev/tinyshield package.json version from tag uses: TypescriptPrime/bump-packagejson-version@72720c4d073ed0c5b9e9393d7334abfe3fabb47c + - name: Bump @filteringdev/tinyshield-lib package.json version from tag + run: npm pkg set version="${GITHUB_REF_NAME#v}" -w libs - name: Build run: npm run build - - name : Publish to npm + - name: Publish @filteringdev/tinyshield to npm + working-directory: . + run: npm publish --tag build --access public + - name: Publish @filteringdev/tinyshield-lib to npm + working-directory: libs run: npm publish --tag build --access public if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-build.') }} purge: @@ -104,10 +122,10 @@ jobs: - name: Set up NodeJS LTS uses: actions/setup-node@v6 with: - node-version: 'lts/*' + node-version: "lts/*" - name: Purge jsdelivr cache uses: FilteringDev/jsdelivr-purge-npm@0bee790e911f359243cd8d98cd87f6bbbc879e80 with: - package: '@filteringdev/tinyshield' - disttag: 'latest' - needs: [release] \ No newline at end of file + package: "@filteringdev/tinyshield" + disttag: "latest" + needs: [release] diff --git a/builder/package.json b/builder/package.json index a93d8c5..f7344bb 100644 --- a/builder/package.json +++ b/builder/package.json @@ -6,11 +6,12 @@ "lint": "tsc --noEmit && eslint **/*.ts", "build": "tsx source/buildci.ts", "debug": "tsx source/debug.ts", - "test": "npm run test:utils", - "test:utils": "ava test/utils/**/*.test.ts" + "test": "echo \"Builder tests moved to @filteringdev/tinyshield-lib.\"", + "test:utils": "echo \"Builder utility tests moved to @filteringdev/tinyshield-lib.\"" }, "dependencies": { - "@types/node": "^24.12.2" + "@types/node": "^24.12.2", + "@filteringdev/tinyshield-lib": "workspace:*" }, "ava": { "files": [ @@ -28,20 +29,15 @@ } }, "devDependencies": { - "@adguard/agtree": "^4.1.1", - "@ava/typescript": "^7.0.0", "@npmcli/package-json": "^8.0.0", "@types/npmcli__package-json": "^4.0.4", "@typescript-eslint/eslint-plugin": "^8.59.2", "@typescript-eslint/parser": "^8.59.2", "@typescriptprime/parsing": "^2.0.1", - "@typescriptprime/securereq": "^2.0.0", - "ava": "^8.0.0", "chokidar": "^5.0.0", "esbuild": "^0.28.0", "eslint": "^10.3.0", "piscina": "^5.1.4", - "tldts": "^7.0.30", "tsx": "^4.21.0", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2", diff --git a/builder/source/build-core.ts b/builder/source/build-core.ts index b4b018a..51b6719 100644 --- a/builder/source/build-core.ts +++ b/builder/source/build-core.ts @@ -1,6 +1,6 @@ import * as Zod from 'zod' import * as Process from 'node:process' -import { FetchAdShieldDomains, type TASDomainContainer } from './references/index.js' +import { FetchAdShieldDomains, type TASDomainContainer } from '@filteringdev/tinyshield-lib/references' import { SafeInitCwd } from './utils/safe-init-cwd.js' export type BuildOptions = { diff --git a/builder/source/debug.ts b/builder/source/debug.ts index 9a06985..0263f00 100644 --- a/builder/source/debug.ts +++ b/builder/source/debug.ts @@ -9,7 +9,7 @@ import { Build } from './build-core.js' let ProjectRoot = SafeInitCwd({ Cwd: Process.cwd(), InitCwd: Process.env.INIT_CWD }) const WatchingGlob: string[] = [] -for (const Dir of ['builder/', 'userscript/', '']) { +for (const Dir of ['builder/', 'userscript/', 'libs/', '']) { WatchingGlob.push(...Fs.globSync(`${ProjectRoot}/${Dir}source/**/*.ts`)) WatchingGlob.push(...Fs.globSync(`${ProjectRoot}/${Dir}source/**/*.json`)) WatchingGlob.push(...Fs.globSync(`${ProjectRoot}/${Dir}source/**/*.txt`)) diff --git a/libs/package.json b/libs/package.json new file mode 100644 index 0000000..20ddfbe --- /dev/null +++ b/libs/package.json @@ -0,0 +1,70 @@ +{ + "name": "@filteringdev/tinyshield-lib", + "version": "0.0.0", + "description": "Library APIs for tinyShield userscript monkey patches and reference domain loading.", + "type": "module", + "files": [ + "dist/**/*" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./monkey-patches": { + "types": "./dist/monkey-patches/index.d.ts", + "import": "./dist/monkey-patches/index.js" + }, + "./references": { + "types": "./dist/references/index.d.ts", + "import": "./dist/references/index.js" + } + }, + "license": "MPL-2.0", + "keywords": [ + "Ad-Shield" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/FilteringDev/tinyShield.git" + }, + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json", + "lint": "tsc --noEmit && eslint source/**/*.ts test/**/*.ts", + "test": "npm run build && ava test/**/*.test.ts" + }, + "dependencies": { + "@adguard/agtree": "^4.1.1", + "@typescriptprime/securereq": "^2.0.0", + "tldts": "^7.0.30", + "zod": "^4.4.3" + }, + "devDependencies": { + "@ava/typescript": "^7.0.0", + "@types/node": "^24.12.2", + "@types/web": "^0.0.349", + "@typescript-eslint/eslint-plugin": "^8.59.2", + "@typescript-eslint/parser": "^8.59.2", + "ava": "^8.0.0", + "eslint": "^10.3.0", + "tsx": "^4.21.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.2" + }, + "ava": { + "files": [ + "test/**/*.test.ts" + ], + "nodeArguments": [ + "--import=tsx" + ], + "workerThreads": false, + "typescript": { + "compile": false, + "rewritePaths": { + "source/": "dist/" + } + } + } +} diff --git a/libs/source/index.ts b/libs/source/index.ts new file mode 100644 index 0000000..6fbabfb --- /dev/null +++ b/libs/source/index.ts @@ -0,0 +1,10 @@ +export { + CreateTinyShieldController, + EnableTinyShield, + TinyShieldPatchIds, + type TinyShieldController, + type TinyShieldControllerOptions, + type TinyShieldPatchController, + type TinyShieldPatchId, + type TinyShieldWindow +} from './monkey-patches/index.js' diff --git a/userscript/source/as-weakmap.ts b/libs/source/monkey-patches/as-weakmap.ts similarity index 87% rename from userscript/source/as-weakmap.ts rename to libs/source/monkey-patches/as-weakmap.ts index 5f9d243..fd67400 100644 --- a/userscript/source/as-weakmap.ts +++ b/libs/source/monkey-patches/as-weakmap.ts @@ -8,16 +8,18 @@ * - See Git history at https://github.com/FilteringDev/tinyShield for detailed authorship information. */ -import { OriginalRegExpTest } from './index.js' - -type CheckDepthResult = { Status: 'matched' } | { Status: 'not-matched' } | { Status: 'too-expensive' } | { Status: 'unsafe-object'; Reason: unknown } - -type CheckBudget = { - MaxTopLevelKeys: number; - MaxArrayItems: number; - MaxInnerKeysPerObject: number; - MaxOperations: number; -}; +export type CheckDepthResult = + { Status: 'matched' } | + { Status: 'not-matched' } | + { Status: 'too-expensive' } | + { Status: 'unsafe-object'; Reason: unknown } + +export type CheckBudget = { + MaxTopLevelKeys: number + MaxArrayItems: number + MaxInnerKeysPerObject: number + MaxOperations: number +} const DefaultBudget: CheckBudget = { MaxTopLevelKeys: 300, @@ -48,6 +50,7 @@ function CountCommonKnownKeys(Obj: object): number { export function CheckDepthInASWeakMapBudgeted( Args: [object, unknown], Budget: CheckBudget = DefaultBudget, + OriginalRegExpTest: typeof RegExp.prototype.test = RegExp.prototype.test, ): CheckDepthResult { let Operations = 0 @@ -133,7 +136,7 @@ export function CheckDepthInASWeakMapBudgeted( } return { Status: 'not-matched' } - } catch (error) { - return { Status: 'unsafe-object', Reason: error } + } catch (Error) { + return { Status: 'unsafe-object', Reason: Error } } -} \ No newline at end of file +} diff --git a/libs/source/monkey-patches/index.ts b/libs/source/monkey-patches/index.ts new file mode 100644 index 0000000..e1323a9 --- /dev/null +++ b/libs/source/monkey-patches/index.ts @@ -0,0 +1,415 @@ +/*! + * @license MPL-2.0 + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * Contributors: + * - See Git history at https://github.com/FilteringDev/tinyShield for detailed authorship information. + */ + +import { CheckDepthInASWeakMapBudgeted } from './as-weakmap.js' +import { ShouldSkipRegExpTest } from './regexp-cheap-guard.js' +import { SafeArrayToString } from './safe-array-to-string.js' + +/* eslint-disable @typescript-eslint/naming-convention */ +export type TinyShieldWindow = { + RegExp: RegExpConstructor + Array: ArrayConstructor + String: StringConstructor + Object: ObjectConstructor + Function: FunctionConstructor + Map: MapConstructor + WeakMap: WeakMapConstructor + setTimeout: typeof globalThis.setTimeout + setInterval: typeof globalThis.setInterval +} +/* eslint-enable @typescript-eslint/naming-convention */ + +// eslint-disable-next-line @typescript-eslint/naming-convention +declare const unsafeWindow: TinyShieldWindow | undefined + +export const TinyShieldPatchIds = [ + 'FunctionToString', + 'MapGet', + 'MapSet', + 'WeakMapSet', + 'SetTimeout', + 'SetInterval', +] as const + +export type TinyShieldPatchId = typeof TinyShieldPatchIds[number] + +export type TinyShieldControllerOptions = { + Window?: TinyShieldWindow + PatchIds?: Iterable + UserscriptName?: string +} + +export type TinyShieldPatchController = { + Id: TinyShieldPatchId + Enable(): void + Disable(): void + IsEnabled(): boolean +} + +export type TinyShieldController = { + Enable(PatchIds?: Iterable): void + Disable(PatchIds?: Iterable): void + IsEnabled(PatchId?: TinyShieldPatchId): boolean + GetPatch(PatchId: TinyShieldPatchId): TinyShieldPatchController +} + +type TinyShieldPatchState = TinyShieldPatchController & { + Enabled: boolean + Installed: boolean + Install(): void +} + +type TinyShieldState = { + BrowserWindow: TinyShieldWindow + UserscriptName: string + OriginalRegExpTest: typeof RegExp.prototype.test + OriginalArrayMap: typeof Array.prototype.map + OriginalString: typeof String + OriginalArrayJoin: typeof Array.prototype.join + OriginalObjectGetPrototypeOf: typeof Object.getPrototypeOf + Patches: Map +} + +type ASReinsertedAdvInvenPossibleArgsType = { + Key: 'string' | 'number' | 'function' + Value: ['string', 'number', 'function'][number][] +} + +const DefaultUserscriptName = 'tinyShield' +const TinyShieldPatchIdSet: ReadonlySet = new Set(TinyShieldPatchIds) +const WindowStates = new WeakMap() +const ProtectedFunctionStrings: ReadonlySet = new Set(['toString', 'get', 'set']) +const FunctionNameProperty = 'name' + +const ASInitPositiveRegExps: RegExp[][] = [[ + /[a-zA-Z0-9]+ *=> *{ *const *[a-zA-Z0-9]+ *= *[a-zA-Z0-9]+ *; *if/, + /===? *[a-zA-Z0-9]+ *\[ *[a-zA-Z0-9]+\( *[0-9a-z]+ *\) *\] *\) *return *[a-zA-Z0-9]+ *\( *{ *('|")?inventoryId('|")? *:/, + /{ *('|")?inventoryId('|")? *: *this *\[[a-zA-Z0-9]+ *\( *[0-9a-z]+ *\) *\] *, *\.\.\. *[a-zA-Z0-9]+ *\[ *[a-zA-Z0-9]+ *\( *[0-9a-z]+ * *\) *\] *} *\)/ +]] + +const ASReinsertedAdvInvenPositiveRegExps: { Search: RegExp[], ArgsType: ASReinsertedAdvInvenPossibleArgsType }[] = [{ + Search: [ + /inventory_id,[a-zA-Z0-9-]+\/[a-zA-Z0-9]+\/[a-zA-Z0-9]+/, + /inventory_id,[a-zA-Z0-9-]+\/[a-zA-Z0-9]+\/[a-zA-Z0-9]+/, + /inventory_id,[a-zA-Z0-9-]+\/[a-zA-Z0-9]+\/[a-zA-Z0-9]+/ + ], + ArgsType: { Key: 'string', Value: ['string'] } +}, { + Search: [ + /[a-z0-9A-Z]+\.setAttribute\( *('|")onload('|") *, *('|")! *async *function\( *\) *\{ *let */, + /confirm\( *[A-Za-z0-9]+ *\) *\) *{ *const *[A-Za-z0-9]+ *= *new *[A-Za-z0-9]+\.URL\(('|")https:\/\/report\.error-report\.com\//, + /\.forEach *\( *\( *[A-Za-z0-9]+ *=> *[A-Za-z0-9]+\.remove *\( *\) *\) *\) *\) *, *[0-9a-f]+ *\) *; *const *[A-Za-z0-9]+ *= *awai,t *\( *await *fetch *\(/ + ], + ArgsType: { Key: 'string', Value: ['function'] } +}] + +const ASTimerRegExps: RegExp[][] = [[ + /async *\( *\) *=> *{ *const *[A-Za-z0-9]+ *= *[A-Za-z0-9]+ *; *await *[A-Za-z0-9]+ *\( *\)/, + /; *await *[A-Za-z0-9]+ *\( *\) *, *[A-Za-z0-9]+ *\( *! *1 *, *new *Error *\( *[A-Za-z0-9]+ *\( *[0-9a-f]+ *\) *\) *\) *}/, + / *\) *\) *\) *}/ +]] + +class TinyShieldControllerImpl implements TinyShieldController { + private State: TinyShieldState + private DefaultPatchIds: TinyShieldPatchId[] + + constructor(State: TinyShieldState, PatchIds: Iterable) { + this.State = State + this.DefaultPatchIds = NormalizePatchIds(PatchIds) + } + + Enable(PatchIds: Iterable = this.DefaultPatchIds): void { + for (const PatchId of NormalizePatchIds(PatchIds)) { + this.GetPatch(PatchId).Enable() + } + } + + Disable(PatchIds: Iterable = this.DefaultPatchIds): void { + for (const PatchId of NormalizePatchIds(PatchIds)) { + this.GetPatch(PatchId).Disable() + } + } + + IsEnabled(PatchId?: TinyShieldPatchId): boolean { + if (typeof PatchId === 'undefined') { + return this.DefaultPatchIds.every(CurrentPatchId => this.GetPatch(CurrentPatchId).IsEnabled()) + } + + return this.GetPatch(PatchId).IsEnabled() + } + + GetPatch(PatchId: TinyShieldPatchId): TinyShieldPatchController { + const Patch = this.State.Patches.get(PatchId) + + if (typeof Patch === 'undefined') { + throw new Error(`Unknown tinyShield patch id: ${PatchId}`) + } + + return Patch + } +} + +export function CreateTinyShieldController(Options: TinyShieldControllerOptions = {}): TinyShieldController { + const State = GetTinyShieldState(Options) + return new TinyShieldControllerImpl(State, Options.PatchIds ?? TinyShieldPatchIds) +} + +export function EnableTinyShield(Options: TinyShieldControllerOptions = {}): TinyShieldController { + const Controller = CreateTinyShieldController(Options) + Controller.Enable() + return Controller +} + +function ResolveBrowserWindow(WindowOption?: TinyShieldWindow): TinyShieldWindow { + if (typeof WindowOption !== 'undefined') { + return WindowOption + } + + if (typeof unsafeWindow !== 'undefined') { + return unsafeWindow + } + + return globalThis as unknown as TinyShieldWindow +} + +function GetTinyShieldState(Options: TinyShieldControllerOptions): TinyShieldState { + const BrowserWindow = ResolveBrowserWindow(Options.Window) + const ExistingState = WindowStates.get(BrowserWindow) + + if (typeof ExistingState !== 'undefined') { + if (typeof Options.UserscriptName !== 'undefined') { + ExistingState.UserscriptName = Options.UserscriptName + } + + return ExistingState + } + + const State: TinyShieldState = { + BrowserWindow, + UserscriptName: Options.UserscriptName ?? DefaultUserscriptName, + OriginalRegExpTest: BrowserWindow.RegExp.prototype.test, + OriginalArrayMap: BrowserWindow.Array.prototype.map, + OriginalString: BrowserWindow.String, + OriginalArrayJoin: BrowserWindow.Array.prototype.join, + OriginalObjectGetPrototypeOf: BrowserWindow.Object.getPrototypeOf, + Patches: new Map() + } + + State.Patches.set('FunctionToString', CreatePatchState('FunctionToString', () => InstallFunctionToStringPatch(State))) + State.Patches.set('MapGet', CreatePatchState('MapGet', () => InstallMapGetPatch(State))) + State.Patches.set('MapSet', CreatePatchState('MapSet', () => InstallMapSetPatch(State))) + State.Patches.set('WeakMapSet', CreatePatchState('WeakMapSet', () => InstallWeakMapSetPatch(State))) + State.Patches.set('SetTimeout', CreatePatchState('SetTimeout', () => InstallSetTimeoutPatch(State))) + State.Patches.set('SetInterval', CreatePatchState('SetInterval', () => InstallSetIntervalPatch(State))) + + WindowStates.set(BrowserWindow, State) + return State +} + +function CreatePatchState(PatchId: TinyShieldPatchId, Install: () => void): TinyShieldPatchState { + const Patch: TinyShieldPatchState = { + Id: PatchId, + Enabled: false, + Installed: false, + Install, + Enable() { + if (!Patch.Installed) { + Patch.Install() + Patch.Installed = true + } + + Patch.Enabled = true + }, + Disable() { + Patch.Enabled = false + }, + IsEnabled() { + return Patch.Enabled + } + } + + return Patch +} + +function NormalizePatchIds(PatchIds: Iterable): TinyShieldPatchId[] { + const Result = [...PatchIds] + + for (const PatchId of Result) { + if (!TinyShieldPatchIdSet.has(PatchId)) { + throw new Error(`Unknown tinyShield patch id: ${PatchId}`) + } + } + + return Result +} + +function IsPatchEnabled(State: TinyShieldState, PatchId: TinyShieldPatchId): boolean { + return State.Patches.get(PatchId)?.IsEnabled() === true +} + +function InstallFunctionToStringPatch(State: TinyShieldState): void { + const Target = State.BrowserWindow.Function.prototype.toString as () => string + State.BrowserWindow.Function.prototype.toString = new Proxy(Target, { + apply(Target, ThisArg: object, Args: []) { + if (!IsPatchEnabled(State, 'FunctionToString')) { + return Reflect.apply(Target, ThisArg, Args) + } + + const ThisArgName = Reflect.get(ThisArg, FunctionNameProperty) + + if (typeof ThisArgName === 'string' && ProtectedFunctionStrings.has(ThisArgName)) { + return 'function ' + ThisArgName + '() { [native code] }' + } + + return Reflect.apply(Target, ThisArg, Args) + } + }) as typeof State.BrowserWindow.Function.prototype.toString +} + +function InstallMapGetPatch(State: TinyShieldState): void { + const Target = State.BrowserWindow.Map.prototype.get as (Key: unknown) => unknown + State.BrowserWindow.Map.prototype.get = new Proxy(Target, { + apply(Target, ThisArg: Map, Args: [unknown]) { + if (!IsPatchEnabled(State, 'MapGet')) { + return Reflect.apply(Target, ThisArg, Args) + } + + if (Args.length > 0 && typeof Args[0] !== 'function') { + return Reflect.apply(Target, ThisArg, Args) + } + + const ArgText = SafeArrayToString(Args, { + OriginalArrayMap: State.OriginalArrayMap, + OriginalString: State.OriginalString, + OriginalArrayJoin: State.OriginalArrayJoin, + OriginalObjectGetPrototypeOf: State.OriginalObjectGetPrototypeOf + }) + + if (!ShouldSkipRegExpTest(ArgText) && ASInitPositiveRegExps.filter(ASInitPositiveRegExp => ASInitPositiveRegExp.filter(Index => State.OriginalRegExpTest.call(Index, ArgText) as boolean).length >= 2).length === 1) { + console.debug(`[${State.UserscriptName}]: Map.prototype.get:`, ThisArg, Args) + throw new Error() + } + + return Reflect.apply(Target, ThisArg, Args) + } + }) as typeof State.BrowserWindow.Map.prototype.get +} + +function InstallMapSetPatch(State: TinyShieldState): void { + const Target = State.BrowserWindow.Map.prototype.set as (Key: unknown, Value: unknown) => Map + State.BrowserWindow.Map.prototype.set = new Proxy(Target, { + apply(Target, ThisArg: Map, Args: [unknown, unknown]) { + if (!IsPatchEnabled(State, 'MapSet')) { + return Reflect.apply(Target, ThisArg, Args) + } + + const ArgsTypeMatchedRegExps = ASReinsertedAdvInvenPositiveRegExps.filter(ASReinsertedAdvInvenPositiveRegExp => + IsASReinsertedAdvInvenArgsTypeMatched(Args, ASReinsertedAdvInvenPositiveRegExp.ArgsType), + ) + + if (ArgsTypeMatchedRegExps.length === 0) { + return Reflect.apply(Target, ThisArg, Args) + } + + const ArgText = SafeArrayToString(Args, { + OriginalArrayMap: State.OriginalArrayMap, + OriginalString: State.OriginalString, + OriginalArrayJoin: State.OriginalArrayJoin, + OriginalObjectGetPrototypeOf: State.OriginalObjectGetPrototypeOf + }) + + if (!ShouldSkipRegExpTest(ArgText) && ArgsTypeMatchedRegExps.filter(ASReinsertedAdvInvenPositiveRegExp => ASReinsertedAdvInvenPositiveRegExp.Search.filter(Index => State.OriginalRegExpTest.call(Index, ArgText) as boolean).length >= 3).length === 1) { + console.debug(`[${State.UserscriptName}]: Map.prototype.set:`, ThisArg, Args) + throw new Error() + } + + return Reflect.apply(Target, ThisArg, Args) + } + }) as typeof State.BrowserWindow.Map.prototype.set +} + +function InstallWeakMapSetPatch(State: TinyShieldState): void { + const Target = State.BrowserWindow.WeakMap.prototype.set as (Key: object, Value: unknown) => WeakMap + State.BrowserWindow.WeakMap.prototype.set = new Proxy(Target, { + apply(Target, ThisArg: WeakMap, Args: [object, unknown]) { + if (!IsPatchEnabled(State, 'WeakMapSet')) { + return Reflect.apply(Target, ThisArg, Args) + } + + const CheckResult = CheckDepthInASWeakMapBudgeted(Args, undefined, State.OriginalRegExpTest) + switch (CheckResult.Status) { + case 'matched': + console.debug(`[${State.UserscriptName}]: WeakMap.prototype.set:`, ThisArg, Args) + throw new Error() + case 'not-matched': + break + case 'too-expensive': + console.warn(`[${State.UserscriptName}]: WeakMap.prototype.set: Check too expensive:`, ThisArg, Args) + break + case 'unsafe-object': + console.warn(`[${State.UserscriptName}]: WeakMap.prototype.set: Unsafe object:`, ThisArg, Args, CheckResult.Reason) + break + } + + return Reflect.apply(Target, ThisArg, Args) + } + }) as typeof State.BrowserWindow.WeakMap.prototype.set +} + +function InstallSetTimeoutPatch(State: TinyShieldState): void { + const Target = State.BrowserWindow.setTimeout + State.BrowserWindow.setTimeout = new Proxy(Target, { + apply(Target, ThisArg: unknown, Args: Parameters) { + if (!IsPatchEnabled(State, 'SetTimeout')) { + return Reflect.apply(Target, ThisArg, Args) + } + + if (IsASTimerMatched(State.OriginalString(Args[0]))) { + console.debug(`[${State.UserscriptName}]: setTimeout:`, Args) + return undefined as unknown as ReturnType + } + + return Reflect.apply(Target, ThisArg, Args) + } + }) as typeof State.BrowserWindow.setTimeout +} + +function InstallSetIntervalPatch(State: TinyShieldState): void { + const Target = State.BrowserWindow.setInterval + State.BrowserWindow.setInterval = new Proxy(Target, { + apply(Target, ThisArg: unknown, Args: Parameters) { + if (!IsPatchEnabled(State, 'SetInterval')) { + return Reflect.apply(Target, ThisArg, Args) + } + + if (IsASTimerMatched(State.OriginalString(Args[0]))) { + console.debug(`[${State.UserscriptName}]: setInterval:`, Args) + return undefined as unknown as ReturnType + } + + return Reflect.apply(Target, ThisArg, Args) + } + }) as typeof State.BrowserWindow.setInterval +} + +function IsASReinsertedAdvInvenArgsTypeMatched(Args: [unknown, unknown], ArgsType: ASReinsertedAdvInvenPossibleArgsType): boolean { + const KeyType = typeof Args[0] + const ValueType = typeof Args[1] + + if (KeyType !== ArgsType.Key) { + return false + } + + return ArgsType.Value.includes(ValueType as ['string', 'number', 'function'][number]) +} + +function IsASTimerMatched(ArgText: string): boolean { + return ASTimerRegExps.filter(ASTimerRegExp => ASTimerRegExp.filter(Index => Index.test(ArgText)).length >= 3).length === 1 +} diff --git a/userscript/source/regexp-cheap-guard.ts b/libs/source/monkey-patches/regexp-cheap-guard.ts similarity index 99% rename from userscript/source/regexp-cheap-guard.ts rename to libs/source/monkey-patches/regexp-cheap-guard.ts index 174ab3d..2af9f98 100644 --- a/userscript/source/regexp-cheap-guard.ts +++ b/libs/source/monkey-patches/regexp-cheap-guard.ts @@ -163,4 +163,4 @@ function CountOccurrences(Haystack: string, Needle: string): number { } return Count -} \ No newline at end of file +} diff --git a/userscript/source/safe-ArrayToString.ts b/libs/source/monkey-patches/safe-array-to-string.ts similarity index 99% rename from userscript/source/safe-ArrayToString.ts rename to libs/source/monkey-patches/safe-array-to-string.ts index 9fce6a7..fc6a659 100644 --- a/userscript/source/safe-ArrayToString.ts +++ b/libs/source/monkey-patches/safe-array-to-string.ts @@ -6,7 +6,6 @@ type OriginalAPI = { } export function SafeArrayToString(This: unknown[], OriginalAPI: OriginalAPI): string { - const Mapped = OriginalAPI.OriginalArrayMap.call( This, (Value: unknown) => { @@ -19,4 +18,4 @@ export function SafeArrayToString(This: unknown[], OriginalAPI: OriginalAPI): st ) as string[] return OriginalAPI.OriginalArrayJoin.call(Mapped) as string -} \ No newline at end of file +} diff --git a/builder/source/references/custom-defined.ts b/libs/source/references/custom-defined.ts similarity index 96% rename from builder/source/references/custom-defined.ts rename to libs/source/references/custom-defined.ts index f90625d..7b9cc8a 100644 --- a/builder/source/references/custom-defined.ts +++ b/libs/source/references/custom-defined.ts @@ -1,3 +1,3 @@ export const CustomDefinedMatches: Set = new Set(['nicovideo.jp']) -export const CustomExcludeMatches: Set = new Set(['kio.ac']) \ No newline at end of file +export const CustomExcludeMatches: Set = new Set(['kio.ac']) diff --git a/builder/source/references/filterslists.ts b/libs/source/references/filterslists.ts similarity index 99% rename from builder/source/references/filterslists.ts rename to libs/source/references/filterslists.ts index 7189599..188767a 100644 --- a/builder/source/references/filterslists.ts +++ b/libs/source/references/filterslists.ts @@ -9,4 +9,4 @@ export async function FetchAdShieldDomainsFromFiltersLists(): Promise([...AGDomains, ...UBODomains]) return CombinedDomains -} \ No newline at end of file +} diff --git a/builder/source/references/filterslists/ADG.ts b/libs/source/references/filterslists/ADG.ts similarity index 97% rename from builder/source/references/filterslists/ADG.ts rename to libs/source/references/filterslists/ADG.ts index dca4f80..6b91def 100644 --- a/builder/source/references/filterslists/ADG.ts +++ b/libs/source/references/filterslists/ADG.ts @@ -42,7 +42,7 @@ export async function IndexAdShieldDomainsFromAG(): Promise> { } } - let FilteredDomains = [...AdShieldDomains].filter(Domain => { + const FilteredDomains = [...AdShieldDomains].filter(Domain => { try { new URLPattern(`https://${Domain}/`) } catch { @@ -52,4 +52,4 @@ export async function IndexAdShieldDomainsFromAG(): Promise> { }) return new Set(FilteredDomains) -} \ No newline at end of file +} diff --git a/builder/source/references/filterslists/keywords.ts b/libs/source/references/filterslists/keywords.ts similarity index 99% rename from builder/source/references/filterslists/keywords.ts rename to libs/source/references/filterslists/keywords.ts index c1a7211..09503b3 100644 --- a/builder/source/references/filterslists/keywords.ts +++ b/libs/source/references/filterslists/keywords.ts @@ -10,4 +10,4 @@ export async function IsAdShieldCDNDomain(Domain: string): Promise { const AdShieldCDNCheckResponse = await SimpleSecureReq.Request(new URL(`https://${Domain}/`), { ExpectedAs: 'String' }).catch(() => false) return typeof AdShieldCDNCheckResponse !== 'boolean' && AdShieldCDNCheckResponse.StatusCode === 200 && AdShieldCDNCheckResponse.Body.includes('This domain is a part of the Ad-Shield (ad-shield.io) platform,') -} \ No newline at end of file +} diff --git a/builder/source/references/filterslists/uBO.ts b/libs/source/references/filterslists/uBO.ts similarity index 97% rename from builder/source/references/filterslists/uBO.ts rename to libs/source/references/filterslists/uBO.ts index 4f6a88d..a393907 100644 --- a/builder/source/references/filterslists/uBO.ts +++ b/libs/source/references/filterslists/uBO.ts @@ -42,7 +42,7 @@ export async function IndexAdShieldDomainsFromUBO(): Promise> { } } - let FilteredDomains = [...AdShieldDomains].filter(Domain => { + const FilteredDomains = [...AdShieldDomains].filter(Domain => { try { new URLPattern(`https://${Domain}/`) } catch { @@ -52,4 +52,4 @@ export async function IndexAdShieldDomainsFromUBO(): Promise> { }) return new Set(FilteredDomains) -} \ No newline at end of file +} diff --git a/builder/source/references/iabsellers.ts b/libs/source/references/iabsellers.ts similarity index 99% rename from builder/source/references/iabsellers.ts rename to libs/source/references/iabsellers.ts index 42c6192..18f10d4 100644 --- a/builder/source/references/iabsellers.ts +++ b/libs/source/references/iabsellers.ts @@ -1,7 +1,6 @@ import * as Zod from 'zod' import { SimpleSecureReq } from '@typescriptprime/securereq' - const IABSellersJsonURL = 'https://info.ad-shield.io/sellers.json' export async function FetchIABSellersJsonData(): Promise { @@ -35,4 +34,4 @@ export async function FetchIABSellersJsonData(): Promise { })) }).parseAsync(IABSellersJsonData) return [...new Set(IABSellersJsonData.sellers.map(S => S.domain))] -} \ No newline at end of file +} diff --git a/builder/source/references/index.ts b/libs/source/references/index.ts similarity index 65% rename from builder/source/references/index.ts rename to libs/source/references/index.ts index dc53cc6..cd921ac 100644 --- a/builder/source/references/index.ts +++ b/libs/source/references/index.ts @@ -1,12 +1,26 @@ import * as TLD from 'tldts' +import { CustomDefinedMatches, CustomExcludeMatches } from './custom-defined.js' import { FetchAdShieldDomainsFromFiltersLists } from './filterslists.js' import { FetchIABSellersJsonData } from './iabsellers.js' -import { DiscardResolvedDupWildcard } from '@builder/utils/discard-resolved-dup-wildcard.js' -import { RegroupDomainTldLevel } from '@builder/utils/regroup-domain-tldlevel.js' -import { ConvertWildcardSuffixToRegexPattern } from '@builder/utils/wildcard-suffix-converter.js' -import { CustomDefinedMatches, CustomExcludeMatches } from './custom-defined.js' +import { DiscardResolvedDupWildcard } from './utils/discard-resolved-dup-wildcard.js' +import { RegroupDomainTldLevel } from './utils/regroup-domain-tldlevel.js' +import { ConvertWildcardSuffixToRegexPattern } from './utils/wildcard-suffix-converter.js' + +export { CustomDefinedMatches, CustomExcludeMatches } from './custom-defined.js' +export { FetchAdShieldDomainsFromFiltersLists } from './filterslists.js' +export { FetchIABSellersJsonData } from './iabsellers.js' +export { IndexAdShieldDomainsFromAG } from './filterslists/ADG.js' +export { IndexAdShieldDomainsFromUBO } from './filterslists/uBO.js' +export { AdShieldCDNDomains, IsAdShieldCDNDomain } from './filterslists/keywords.js' +export { DiscardResolvedDupWildcard } from './utils/discard-resolved-dup-wildcard.js' +export { RegroupDomainTldLevel } from './utils/regroup-domain-tldlevel.js' +export { ConvertWildcardSuffixToRegexPattern, PublicSuffixList } from './utils/wildcard-suffix-converter.js' -export type TASDomainContainer = Map<'Normal', Set> & Map<'Full', Set> & Map<'EachDomain', Set>> & Map<'EachDomainFull', Set>> +export type TASDomainContainer = + Map<'Normal', Set> & + Map<'Full', Set> & + Map<'EachDomain', Set>> & + Map<'EachDomainFull', Set>> function ConvertToFlatFullDomains(Origin: Set): Set { return new Set([...Origin].flatMap(Domain => ConvertWildcardSuffixToRegexPattern(Domain))) diff --git a/builder/source/utils/discard-resolved-dup-wildcard.ts b/libs/source/references/utils/discard-resolved-dup-wildcard.ts similarity index 100% rename from builder/source/utils/discard-resolved-dup-wildcard.ts rename to libs/source/references/utils/discard-resolved-dup-wildcard.ts diff --git a/builder/source/utils/regroup-domain-tldlevel.ts b/libs/source/references/utils/regroup-domain-tldlevel.ts similarity index 100% rename from builder/source/utils/regroup-domain-tldlevel.ts rename to libs/source/references/utils/regroup-domain-tldlevel.ts diff --git a/builder/source/utils/wildcard-suffix-converter.ts b/libs/source/references/utils/wildcard-suffix-converter.ts similarity index 99% rename from builder/source/utils/wildcard-suffix-converter.ts rename to libs/source/references/utils/wildcard-suffix-converter.ts index 77cfc9b..0261193 100644 --- a/builder/source/utils/wildcard-suffix-converter.ts +++ b/libs/source/references/utils/wildcard-suffix-converter.ts @@ -9,4 +9,4 @@ export function ConvertWildcardSuffixToRegexPattern(Domain: string): string[] { Result.push(Domain.replaceAll(/\.\*$/g, '.' + Suffix)) }) return Result -} \ No newline at end of file +} diff --git a/libs/test/monkey-patches/controller.test.ts b/libs/test/monkey-patches/controller.test.ts new file mode 100644 index 0000000..45d90f1 --- /dev/null +++ b/libs/test/monkey-patches/controller.test.ts @@ -0,0 +1,70 @@ +import Test from 'ava' +import { + CreateTinyShieldController, + EnableTinyShield, + type TinyShieldWindow +} from '@filteringdev/tinyshield-lib' + +function CreateTestWindow(): TinyShieldWindow { + class TestFunction extends Function {} + class TestMap extends Map {} + class TestWeakMap extends WeakMap {} + + const TestSetTimeout = (() => 1) as unknown as typeof globalThis.setTimeout + const TestSetInterval = (() => 1) as unknown as typeof globalThis.setInterval + + return { + RegExp, + Array, + String, + Object, + Function: TestFunction as unknown as FunctionConstructor, + Map: TestMap as unknown as MapConstructor, + WeakMap: TestWeakMap as unknown as WeakMapConstructor, + setTimeout: TestSetTimeout, + setInterval: TestSetInterval + } +} + +Test('EnableTinyShield installs wrappers only once for the same window', T => { + const TestWindow = CreateTestWindow() + + const FirstController = EnableTinyShield({ Window: TestWindow }) + const FirstMapGet = TestWindow.Map.prototype.get + const SecondController = EnableTinyShield({ Window: TestWindow }) + + T.true(FirstController.IsEnabled()) + T.true(SecondController.IsEnabled()) + T.is(TestWindow.Map.prototype.get, FirstMapGet) +}) + +Test('Disable turns off behavior without restoring the installed wrapper', T => { + const TestWindow = CreateTestWindow() + const OriginalMapGet = TestWindow.Map.prototype.get + const Controller = CreateTinyShieldController({ Window: TestWindow, PatchIds: ['MapGet'] }) + + Controller.Enable() + const WrappedMapGet = TestWindow.Map.prototype.get + Controller.Disable() + + const MapInstance = new TestWindow.Map([['Key', 'Value']]) + + T.not(WrappedMapGet, OriginalMapGet) + T.is(TestWindow.Map.prototype.get, WrappedMapGet) + T.false(Controller.IsEnabled('MapGet')) + T.is(MapInstance.get('Key'), 'Value') +}) + +Test('Controller can enable and disable a selected patch subset', T => { + const TestWindow = CreateTestWindow() + const Controller = CreateTinyShieldController({ Window: TestWindow, PatchIds: ['SetTimeout'] }) + + Controller.Enable() + + T.true(Controller.IsEnabled('SetTimeout')) + T.false(Controller.IsEnabled('SetInterval')) + + Controller.Disable() + + T.false(Controller.IsEnabled('SetTimeout')) +}) diff --git a/builder/test/utils/discard-resolved-dup-wildcard.test.ts b/libs/test/references/discard-resolved-dup-wildcard.test.ts similarity index 92% rename from builder/test/utils/discard-resolved-dup-wildcard.test.ts rename to libs/test/references/discard-resolved-dup-wildcard.test.ts index ac4b7b8..1f72ac7 100644 --- a/builder/test/utils/discard-resolved-dup-wildcard.test.ts +++ b/libs/test/references/discard-resolved-dup-wildcard.test.ts @@ -1,5 +1,5 @@ import Test from 'ava' -import { DiscardResolvedDupWildcard } from '@builder/utils/discard-resolved-dup-wildcard.js' +import { DiscardResolvedDupWildcard } from '@filteringdev/tinyshield-lib/references' Test('DiscardResolvedDupWildcard removes resolved duplicate wildcards', T => { const Input = new Set(['google.com', 'google.co.kr', 'google.org', 'example.com', 'example.org', 'duck.com']) @@ -11,20 +11,20 @@ Test('DiscardResolvedDupWildcard removes resolved duplicate wildcards', T => { Test('DiscardResolvedDupWildcard does not remove non-duplicate wildcards', T => { const Input = new Set(['google.com', 'chatgpt.com', 'claude.ai', 'gemini.google.com', 'duck.com']) const Expected = new Set(['google.com', 'chatgpt.com', 'claude.ai', 'duck.com']) - + return T.deepEqual(DiscardResolvedDupWildcard(Input), Expected) }) Test('DiscardResolvedDupWildcard does not remove non-duplicate wildcards with multiple subdomains', T => { const Input = new Set(['access.chatgpt.com', 'info.chatgpt.com', 'access.claude.ai', 'info.claude.ai', 'access.huggingface.co', 'info.huggingface.co']) - + return T.deepEqual(DiscardResolvedDupWildcard(Input), Input) }) Test('DiscardResolvedDupWildcard removes resolved duplicate wildcards with multiple subdomains', T => { const Input = new Set(['google.*', 'access.google.*', 'google.com', 'google.co.kr']) const Expected = new Set(['google.*']) - + return T.deepEqual(DiscardResolvedDupWildcard(Input), Expected) }) @@ -37,7 +37,7 @@ Test('DiscardResolvedDupWildcard handles nested wildcard scenarios', T => { Test('DiscardResolvedDupWildcard handles complex wildcard scenarios', T => { const Input = new Set(['token.google.*', 'access.google.*', 'tools.google.com', 'google.google.co.kr', 'example.*', 'example.com', 'rust-lang.org']) - const Expected = new Set(['token.google.*', 'access.google.*', 'tools.google.*', 'google.google.*','example.*', 'rust-lang.org']) + const Expected = new Set(['token.google.*', 'access.google.*', 'tools.google.*', 'google.google.*', 'example.*', 'rust-lang.org']) return T.deepEqual(DiscardResolvedDupWildcard(Input), Expected) -}) \ No newline at end of file +}) diff --git a/builder/test/utils/regroup-domain-tldlevel.test.ts b/libs/test/references/regroup-domain-tldlevel.test.ts similarity index 93% rename from builder/test/utils/regroup-domain-tldlevel.test.ts rename to libs/test/references/regroup-domain-tldlevel.test.ts index 8717601..eb34190 100644 --- a/builder/test/utils/regroup-domain-tldlevel.test.ts +++ b/libs/test/references/regroup-domain-tldlevel.test.ts @@ -1,5 +1,5 @@ import Test from 'ava' -import { RegroupDomainTldLevel } from '@builder/utils/regroup-domain-tldlevel.js' +import { RegroupDomainTldLevel } from '@filteringdev/tinyshield-lib/references' Test('RegroupDomainTldLevel discard subdomain elements only if their parent domain exists', T => { const Origin = new Set(['duckduckgo.com', 'access.duckduckgo.com', 'google.com', 'www.google.com']) @@ -20,4 +20,4 @@ Test('RegroupDomainTldLevel throw error if multiple domains with the same TLD le const ErrorInstance = T.throws(() => RegroupDomainTldLevel(Origin)) const Message = 'RegroupDomainTldLevel: Found multiple domains with the same TLD level. Use DiscardResolvedDupWildcard func first before using RegroupDomainTldLevel.' return T.is(ErrorInstance?.message, Message) -}) \ No newline at end of file +}) diff --git a/libs/tsconfig.json b/libs/tsconfig.json new file mode 100644 index 0000000..5caa5db --- /dev/null +++ b/libs/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "source/**/*.ts" + ], + "compilerOptions": { + "rootDir": "./source", + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "skipLibCheck": true + } +} diff --git a/package.json b/package.json index 42547a2..948acf2 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,12 @@ "description": "", "type": "module", "scripts": { - "build:userscript": "npm run build -w builder -- --minify true --build-type production --SubscriptionUrl https://cdn.jsdelivr.net/npm/@filteringdev/tinyshield@latest/dist/tinyShield.user.js", + "build:libs": "npm run build -w libs", + "build:userscript": "npm run build:libs && npm run build -w builder -- --minify true --build-type production --SubscriptionUrl https://cdn.jsdelivr.net/npm/@filteringdev/tinyshield@latest/dist/tinyShield.user.js", "build": "npm run build:userscript", "debug": "npm run debug -w builder", - "lint": "npm run lint -w builder && npm run lint -w userscript", - "test": "npm run test -w builder" + "lint": "npm run lint -w libs && npm run lint -w builder && npm run lint -w userscript", + "test": "npm run test -w libs" }, "keywords": [ "Ad-Shield" @@ -22,6 +23,7 @@ }, "license": "MPL-2.0", "workspaces": [ + "libs", "userscript", "builder" ], diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a10dd63..d64b1fc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,7 @@ packages: + - "libs" - "builder" - "userscript" allowBuilds: - esbuild: true \ No newline at end of file + esbuild: true diff --git a/tsconfig.json b/tsconfig.json index c2e7c82..bfdd99a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,10 +6,30 @@ "removeComments": false, "skipLibCheck": true, "paths": { - "@builder/*": ["./builder/source/*"], - "@userscript/*": ["./userscript/source/*"], - "@root/*": ["./source/*"], - "@reporoot/*": ["./*"] + "@builder/*": [ + "./builder/source/*" + ], + "@userscript/*": [ + "./userscript/source/*" + ], + "@root/*": [ + "./source/*" + ], + "@reporoot/*": [ + "./*" + ], + "@filteringdev/tinyshield-lib": [ + "./libs/source/index.ts" + ], + "@filteringdev/tinyshield-lib/monkey-patches": [ + "./libs/source/monkey-patches/index.ts" + ], + "@filteringdev/tinyshield-lib/references": [ + "./libs/source/references/index.ts" + ], + "@filteringdev/tinyshield-lib/*": [ + "./libs/source/*" + ] } } -} \ No newline at end of file +} diff --git a/userscript/package.json b/userscript/package.json index 913b138..cbd2b2e 100644 --- a/userscript/package.json +++ b/userscript/package.json @@ -13,6 +13,7 @@ "typescript-eslint": "^8.59.2" }, "dependencies": { - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "@filteringdev/tinyshield-lib": "workspace:*" } } diff --git a/userscript/source/index.ts b/userscript/source/index.ts index 40a88be..f6604f8 100644 --- a/userscript/source/index.ts +++ b/userscript/source/index.ts @@ -8,148 +8,6 @@ * - See Git history at https://github.com/FilteringDev/tinyShield for detailed authorship information. */ -type unsafeWindow = typeof window -// eslint-disable-next-line @typescript-eslint/naming-convention -declare const unsafeWindow: unsafeWindow +import { EnableTinyShield } from '@filteringdev/tinyshield-lib' -const BrowserWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window -const UserscriptName = 'tinyShield' - -import { CheckDepthInASWeakMapBudgeted } from './as-weakmap.js' -import { ShouldSkipRegExpTest } from './regexp-cheap-guard.js' -import { SafeArrayToString } from './safe-ArrayToString.js' - -export const OriginalRegExpTest = BrowserWindow.RegExp.prototype.test -const OriginalArrayMap = BrowserWindow.Array.prototype.map -const OriginalString = BrowserWindow.String -const OriginalArrayJoin = BrowserWindow.Array.prototype.join -const OriginalObjectGetPrototypeOf = BrowserWindow.Object.getPrototypeOf - -const ProtectedFunctionStrings = ['toString', 'get', 'set'] - -BrowserWindow.Function.prototype.toString = new Proxy(BrowserWindow.Function.prototype.toString, { - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - apply(Target: () => string, ThisArg: Function, Args: []) { - if (ProtectedFunctionStrings.includes(ThisArg.name)) { - return `function ${ThisArg.name}() { [native code] }` - } else { - return Reflect.apply(Target, ThisArg, Args) - } - } -}) - -const ASInitPositiveRegExps: RegExp[][] = [[ - /[a-zA-Z0-9]+ *=> *{ *const *[a-zA-Z0-9]+ *= *[a-zA-Z0-9]+ *; *if/, - /===? *[a-zA-Z0-9]+ *\[ *[a-zA-Z0-9]+\( *[0-9a-z]+ *\) *\] *\) *return *[a-zA-Z0-9]+ *\( *{ *('|")?inventoryId('|")? *:/, - /{ *('|")?inventoryId('|")? *: *this *\[[a-zA-Z0-9]+ *\( *[0-9a-z]+ *\) *\] *, *\.\.\. *[a-zA-Z0-9]+ *\[ *[a-zA-Z0-9]+ *\( *[0-9a-z]+ * *\) *\] *} *\)/ -]] -BrowserWindow.Map.prototype.get = new Proxy(BrowserWindow.Map.prototype.get, { - apply(Target: (key: unknown) => unknown, ThisArg: Map, Args: [unknown]) { - if (Args.length > 0 && typeof Args[0] !== 'function') { - return Reflect.apply(Target, ThisArg, Args) - } - - let ArgText = SafeArrayToString(Args, { OriginalArrayMap, OriginalString, OriginalArrayJoin, OriginalObjectGetPrototypeOf }) - - if (!ShouldSkipRegExpTest(ArgText) && ASInitPositiveRegExps.filter(ASInitPositiveRegExp => ASInitPositiveRegExp.filter(Index => OriginalRegExpTest.call(Index, ArgText) as boolean).length >= 2).length === 1) { - console.debug(`[${UserscriptName}]: Map.prototype.get:`, ThisArg, Args) - throw new Error() - } - - return Reflect.apply(Target, ThisArg, Args) - } -}) - -type ASReinsertedAdvInvenPossibleArgsType = { Key: 'string' | 'number' | 'function', Value: ['string', 'number', 'function'][number][] } -const ASReinsertedAdvInvenPositiveRegExps: { Search: RegExp[], ArgsType: ASReinsertedAdvInvenPossibleArgsType }[] = [{ - Search: [ - /inventory_id,[a-zA-Z0-9-]+\/[a-zA-Z0-9]+\/[a-zA-Z0-9]+/, - /inventory_id,[a-zA-Z0-9-]+\/[a-zA-Z0-9]+\/[a-zA-Z0-9]+/, - /inventory_id,[a-zA-Z0-9-]+\/[a-zA-Z0-9]+\/[a-zA-Z0-9]+/ - ], - ArgsType: { Key: 'string', Value: ['string'] } -}, { - Search: [ - /[a-z0-9A-Z]+\.setAttribute\( *('|")onload('|") *, *('|")! *async *function\( *\) *\{ *let */, - /confirm\( *[A-Za-z0-9]+ *\) *\) *{ *const *[A-Za-z0-9]+ *= *new *[A-Za-z0-9]+\.URL\(('|")https:\/\/report\.error-report\.com\//, - /\.forEach *\( *\( *[A-Za-z0-9]+ *=> *[A-Za-z0-9]+\.remove *\( *\) *\) *\) *\) *, *[0-9a-f]+ *\) *; *const *[A-Za-z0-9]+ *= *awai,t *\( *await *fetch *\(/ - ], - ArgsType: { Key: 'string', Value: ['function'] } -}] - -function IsASReinsertedAdvInvenArgsTypeMatched(Args: [unknown, unknown], ArgsType: ASReinsertedAdvInvenPossibleArgsType): boolean { - const KeyType = typeof Args[0] - const ValueType = typeof Args[1] - - if (KeyType !== ArgsType.Key) { - return false - } - - return ArgsType.Value.includes(ValueType as ['string', 'number', 'function'][number]) -} - -BrowserWindow.Map.prototype.set = new Proxy(BrowserWindow.Map.prototype.set, { - apply(Target: (key: unknown, value: unknown) => Map, ThisArg: Map, Args: [unknown, unknown]) { - let ArgText = '' - - const ArgsTypeMatchedRegExps = ASReinsertedAdvInvenPositiveRegExps.filter(ASReinsertedAdvInvenPositiveRegExp => - IsASReinsertedAdvInvenArgsTypeMatched(Args, ASReinsertedAdvInvenPositiveRegExp.ArgsType), - ) - if (ArgsTypeMatchedRegExps.length === 0) { - return Reflect.apply(Target, ThisArg, Args) - } - - ArgText = SafeArrayToString(Args, { OriginalArrayMap, OriginalString, OriginalArrayJoin, OriginalObjectGetPrototypeOf }) - - if (!ShouldSkipRegExpTest(ArgText) && ArgsTypeMatchedRegExps.filter(ASReinsertedAdvInvenPositiveRegExp => ASReinsertedAdvInvenPositiveRegExp.Search.filter(Index => OriginalRegExpTest.call(Index, ArgText) as boolean).length >= 3).length === 1) { - console.debug(`[${UserscriptName}]: Map.prototype.set:`, ThisArg, Args) - throw new Error() - } - return Reflect.apply(Target, ThisArg, Args) - } -}) - -BrowserWindow.WeakMap.prototype.set = new Proxy(BrowserWindow.WeakMap.prototype.set, { - apply(Target: (key: object, value: unknown) => WeakMap, ThisArg: WeakMap, Args: [object, unknown]) { - let CheckResult = CheckDepthInASWeakMapBudgeted(Args) - switch (CheckResult.Status) { - case 'matched': - console.debug(`[${UserscriptName}]: WeakMap.prototype.set:`, ThisArg, Args) - throw new Error() - case 'not-matched': - break - case 'too-expensive': - console.warn(`[${UserscriptName}]: WeakMap.prototype.set: Check too expensive:`, ThisArg, Args) - break - case 'unsafe-object': - console.warn(`[${UserscriptName}]: WeakMap.prototype.set: Unsafe object:`, ThisArg, Args, CheckResult.Reason) - break - } - - return Reflect.apply(Target, ThisArg, Args) - } -}) - -let ASTimerRegExps: RegExp[][] = [[ - /async *\( *\) *=> *{ *const *[A-Za-z0-9]+ *= *[A-Za-z0-9]+ *; *await *[A-Za-z0-9]+ *\( *\)/, - /; *await *[A-Za-z0-9]+ *\( *\) *, *[A-Za-z0-9]+ *\( *! *1 *, *new *Error *\( *[A-Za-z0-9]+ *\( *[0-9a-f]+ *\) *\) *\) *}/, - / *\) *\) *\) *}/ -]] -BrowserWindow.setTimeout = new Proxy(BrowserWindow.setTimeout, { - apply(Target: typeof BrowserWindow.setTimeout, ThisArg: undefined, Args: Parameters) { - if (ASTimerRegExps.filter(ASTimerRegExp => ASTimerRegExp.filter(Index => Index.test(Args[0].toString())).length >= 3).length === 1) { - console.debug(`[${UserscriptName}]: setTimeout:`, Args) - return - } - return Reflect.apply(Target, ThisArg, Args) - } -}) -BrowserWindow.setInterval = new Proxy(BrowserWindow.setInterval, { - apply(Target: typeof BrowserWindow.setInterval, ThisArg: undefined, Args: Parameters) { - if (ASTimerRegExps.filter(ASTimerRegExp => ASTimerRegExp.filter(Index => Index.test(Args[0].toString())).length >= 3).length === 1) { - console.debug(`[${UserscriptName}]: setInterval:`, Args) - return - } - return Reflect.apply(Target, ThisArg, Args) - } -}) \ No newline at end of file +EnableTinyShield() diff --git a/userscript/tsconfig.json b/userscript/tsconfig.json index 228d800..f7966bf 100644 --- a/userscript/tsconfig.json +++ b/userscript/tsconfig.json @@ -4,9 +4,9 @@ "source/**/*.ts" ], "compilerOptions": { - "rootDir": "./source", "outDir": "../dist/", "declaration": true, - "skipLibCheck": true + "skipLibCheck": true, + "rootDir": ".." } -} \ No newline at end of file +}