aboutsummaryrefslogtreecommitdiffstats
path: root/packages/service-inject
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-24 19:00:07 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-24 19:13:56 +0100
commit90471302835dad5251ea568091cfd6c21d757fd3 (patch)
tree66ba2d5181ef4c4cb552c53936369bcb530c524c /packages/service-inject
parentfix: Typings in js config files (diff)
downloadsophie-90471302835dad5251ea568091cfd6c21d757fd3.tar.gz
sophie-90471302835dad5251ea568091cfd6c21d757fd3.tar.zst
sophie-90471302835dad5251ea568091cfd6c21d757fd3.zip
feat: User agent data simulator
Diffstat (limited to 'packages/service-inject')
-rw-r--r--packages/service-inject/package.json21
-rw-r--r--packages/service-inject/src/index.ts27
-rw-r--r--packages/service-inject/src/shims/userAgentData.ts102
-rw-r--r--packages/service-inject/src/utils.ts104
-rw-r--r--packages/service-inject/tsconfig.json22
-rw-r--r--packages/service-inject/vite.config.js28
6 files changed, 304 insertions, 0 deletions
diff --git a/packages/service-inject/package.json b/packages/service-inject/package.json
new file mode 100644
index 0000000..e2d03bf
--- /dev/null
+++ b/packages/service-inject/package.json
@@ -0,0 +1,21 @@
1{
2 "name": "@sophie/service-inject",
3 "version": "0.1.0",
4 "private": true,
5 "sideEffects": false,
6 "main": "dist/index.cjs",
7 "types": "dist-types/index.d.ts",
8 "scripts": {
9 "clean": "rimraf dist dist-types tsconfig.tsbuildinfo",
10 "build": "vite build",
11 "typecheck": "tsc"
12 },
13 "dependencies": {
14 "@sophie/service-shared": "workspace:*"
15 },
16 "devDependencies": {
17 "rimraf": "^3.0.2",
18 "typescript": "^4.5.4",
19 "vite": "^2.7.6"
20 }
21}
diff --git a/packages/service-inject/src/index.ts b/packages/service-inject/src/index.ts
new file mode 100644
index 0000000..7f48fdf
--- /dev/null
+++ b/packages/service-inject/src/index.ts
@@ -0,0 +1,27 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import { shimUserAgentData } from './shims/userAgentData';
22
23try {
24 shimUserAgentData();
25} catch (err) {
26 console.log('Failed to execute injected script:', err);
27}
diff --git a/packages/service-inject/src/shims/userAgentData.ts b/packages/service-inject/src/shims/userAgentData.ts
new file mode 100644
index 0000000..be43823
--- /dev/null
+++ b/packages/service-inject/src/shims/userAgentData.ts
@@ -0,0 +1,102 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21import {
22 defineProtoProperty,
23 deleteProtoProperty,
24 simulateNativeClass,
25 simulateNativeFunction,
26} from '../utils';
27
28export function shimUserAgentData(): void {
29 const brands = [
30 {
31 brand: ' Not A; Brand',
32 version: '99',
33 },
34 {
35 brand: 'Chromium',
36 version: '96',
37 }
38 ];
39 const mobile = false;
40 const platform = 'Linux';
41
42 const simulatedNavigatorUa = simulateNativeClass('NavigatorUAData', function NavigatorUAData() {
43 // Nothing to initiailize.
44 }, {
45 brands: {
46 configurable: true,
47 enumerable: true,
48 get: simulateNativeFunction('brands', () => brands),
49 },
50 mobile: {
51 configurable: true,
52 enumerable: true,
53 get: simulateNativeFunction('mobile', () => mobile),
54 },
55 platform: {
56 configurable: true,
57 enumerable: true,
58 get: simulateNativeFunction('platform', () => platform),
59 },
60 getHighEntropyValues: {
61 configurable: true,
62 enumerable: false,
63 value: simulateNativeFunction('getHighEntropyValues', (...args: unknown[]) => {
64 if (args.length == 0) {
65 throw new TypeError("Failed to execute 'getHighEntropyValues' on 'NavigatorUAData': 1 argument required, but only 0 present.");
66 }
67 const hints = Array.from(args[0] as Iterable<string>);
68 if (hints.length === 0) {
69 return {};
70 }
71 const data: Record<string, unknown> = {
72 brands,
73 mobile,
74 }
75 if (hints.includes('platform')) {
76 data['platform'] = platform;
77 }
78 return Promise.resolve(data);
79 })
80 },
81 toJSON: {
82 configurable: true,
83 enumerable: false,
84 value: simulateNativeFunction('toJSON', () => ({
85 brands,
86 mobile,
87 })),
88 writable: false,
89 },
90 });
91
92 const simulatedUserAgentData = Reflect.construct(simulatedNavigatorUa, []);
93 defineProtoProperty(window.navigator, 'userAgentData', {
94 configurable: true,
95 enumerable: true,
96 get: simulateNativeFunction('userAgentData', () => simulatedUserAgentData),
97 });
98}
99
100export function deleteUserAgentData(): void {
101 deleteProtoProperty(window.navigator, 'userAgentData');
102}
diff --git a/packages/service-inject/src/utils.ts b/packages/service-inject/src/utils.ts
new file mode 100644
index 0000000..4bb3fba
--- /dev/null
+++ b/packages/service-inject/src/utils.ts
@@ -0,0 +1,104 @@
1/*
2 * Copyright (C) 2021-2022 Kristóf Marussy <kristof@marussy.com>
3 *
4 * This file is part of Sophie.
5 *
6 * Sophie is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as
8 * published by the Free Software Foundation, version 3.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Affero General Public License for more details.
14 *
15 * You should have received a copy of the GNU Affero General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: AGPL-3.0-only
19 */
20
21/**
22 * Simulates a function defined in native code, i.e., one with
23 * `[native code]` in its `toString`.
24 *
25 * @param name The name of the function.
26 * @param f The function to transform.
27 * @return The transformed function.
28 */
29export function simulateNativeFunction<T, P extends unknown[]>(
30 name: string,
31 f: (this: null, ...args: P) => T,
32): (...args: P) => T {
33 // Bound functions say `[native code]`, but unfortunately they omit the function name:
34 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/toString#description
35 // The type of `f` contains type variables, so we need some magic type casting.
36 const boundFunc = f.bind(null as ThisParameterType<typeof f>);
37 Object.defineProperty(boundFunc, 'name', {
38 configurable: true,
39 enumerable: false,
40 value: name,
41 writable: false,
42 });
43 return boundFunc;
44}
45
46/**
47 * Simulates a native class available on `globalThis`.
48 *
49 * @param name The name of the class.
50 * @param constructor The constructor function. Must already be a constructor (a named `function`).
51 * @param properties The properties to define on the prototype.
52 */
53export function simulateNativeClass(
54 name: string,
55 constructor: () => void,
56 properties: PropertyDescriptorMap,
57) {
58 Object.defineProperties(constructor.prototype, {
59 [Symbol.toStringTag]: {
60 configurable: true,
61 enumerable: false,
62 value: name,
63 writable: false,
64 },
65 ...properties,
66 });
67 const simulatedConstructor = simulateNativeFunction(name, constructor);
68 Object.defineProperty(globalThis, name, {
69 configurable: true,
70 enumerable: true,
71 value: simulatedConstructor,
72 writable: true,
73 });
74 return simulatedConstructor;
75}
76
77/**
78 * Defines a property on the prototype of an object.
79 *
80 * Only use this with singleton objects, e.g., `window.navigator`.
81 *
82 * @param o The object to modify. Must be a singleton.
83 * @param property The key of the property being defined or modified.
84 * @param attributes The descriptor of the property being defined or modified.
85 */
86export function defineProtoProperty(
87 o: object,
88 property: PropertyKey,
89 attributes: PropertyDescriptor,
90): void {
91 Object.defineProperty(Object.getPrototypeOf(o), property, attributes);
92}
93
94/**
95 * Deletes a property from the prototype of an object.
96 *
97 * Only use this with singleton objects, e.g., `window.navigator`.
98 *
99 * @param o The object to modify. Must be a singleton.
100 * @param property The key of the property being deleted.
101 */
102export function deleteProtoProperty(o: object, property: PropertyKey): void {
103 Reflect.deleteProperty(Object.getPrototypeOf(o), property);
104}
diff --git a/packages/service-inject/tsconfig.json b/packages/service-inject/tsconfig.json
new file mode 100644
index 0000000..4007f60
--- /dev/null
+++ b/packages/service-inject/tsconfig.json
@@ -0,0 +1,22 @@
1{
2 "extends": "../../tsconfig.json",
3 "compilerOptions": {
4 "composite": true,
5 "declarationDir": "dist-types",
6 "emitDeclarationOnly": true,
7 "rootDir": "src",
8 "lib": [
9 "dom",
10 "dom.iterable",
11 "esnext"
12 ]
13 },
14 "references": [
15 {
16 "path": "../service-shared"
17 }
18 ],
19 "include": [
20 "src/**/*.ts"
21 ]
22}
diff --git a/packages/service-inject/vite.config.js b/packages/service-inject/vite.config.js
new file mode 100644
index 0000000..9c65c15
--- /dev/null
+++ b/packages/service-inject/vite.config.js
@@ -0,0 +1,28 @@
1// @ts-check
2
3import { builtinModules } from 'module';
4
5import { chrome, makeConfig } from '../../config/vite-common';
6
7/** @type {string} */
8const PACKAGE_ROOT = __dirname;
9
10/**
11 * @type {import('vite').UserConfig}
12 * @see https://vitejs.dev/config/
13 */
14const config = makeConfig({
15 root: PACKAGE_ROOT,
16 build: {
17 target: chrome,
18 lib: {
19 entry: 'src/index.ts',
20 formats: ['cjs'],
21 },
22 rollupOptions: {
23 external: builtinModules,
24 },
25 },
26});
27
28export default config;