mirror of
https://github.com/e9x/electron-injector.git
synced 2025-12-16 15:37:42 +01:00
initial commit
This commit is contained in:
15
.eslintrc
Normal file
15
.eslintrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"env": { "browser": true, "es2020": true },
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:react-hooks/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": { "ecmaVersion": "latest", "sourceType": "module" },
|
||||||
|
"plugins": ["react-refresh"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/consistent-type-imports": "error",
|
||||||
|
"react-refresh/only-export-components": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
18
README.md
Normal file
18
README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Client Injector
|
||||||
|
|
||||||
|
A web-based Asar preload injector.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- No .exe
|
||||||
|
- Simple
|
||||||
|
- Fast
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/e9x/client-injector
|
||||||
|
cd client-injector
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/patcher.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Electron Injector</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3279
package-lock.json
generated
Normal file
3279
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
package.json
Normal file
36
package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "electron-injector",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@monaco-editor/react": "^4.5.1",
|
||||||
|
"bootswatch": "^5.2.3",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"pretty-bytes": "^6.1.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-dropzone": "^14.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "^3.7.2",
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||||
|
"@typescript-eslint/parser": "^5.57.1",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
|
"sass": "^1.62.1",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "^4.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
public/patcher.svg
Normal file
1
public/patcher.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#CCD6DD" d="M31 32c0 2.209-1.791 4-4 4H5c-2.209 0-4-1.791-4-4V4c0-2.209 1.791-4 4-4h22c2.209 0 4 1.791 4 4v28z"/><path fill="#99AAB5" d="M27 24c0 .553-.447 1-1 1H6c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm-16 4c0 .553-.448 1-1 1H6c-.552 0-1-.447-1-1 0-.553.448-1 1-1h4c.552 0 1 .447 1 1zM27 8c0 .552-.447 1-1 1H6c-.552 0-1-.448-1-1s.448-1 1-1h20c.553 0 1 .448 1 1zm0 4c0 .553-.447 1-1 1H6c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H6c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1zm0 4c0 .553-.447 1-1 1H6c-.552 0-1-.447-1-1 0-.553.448-1 1-1h20c.553 0 1 .447 1 1z"/><path fill="#66757F" d="M31 6.272c-.827-.535-1.837-.579-2.521-.023l-.792.646-1.484 1.211-.1.08-2.376 1.938-11.878 9.686c-.437.357-.793 1.219-1.173 2.074-.378.85-.969 2.852-1.443 4.391-.148.25-1.065 1.846-.551 2.453.52.615 2.326.01 2.568-.076 1.626-.174 3.731-.373 4.648-.58.924-.211 1.854-.395 2.291-.752.008-.006.01-.018.017-.023l11.858-9.666.792-.646.144-.118V6.272z"/><path fill="#D99E82" d="M18.145 22.526s-1.274-1.881-2.117-2.553c-.672-.843-2.549-2.116-2.549-2.116-.448-.446-1.191-.48-1.629-.043-.437.438-.793 1.366-1.173 2.291-.472 1.146-1.276 4.154-1.768 5.752-.083.272.517-.45.503-.21-.01.187.027.394.074.581l-.146.159.208.067c.025.082.05.154.068.21l.159-.146c.187.047.394.084.58.074.24-.014-.483.587-.21.503 1.598-.493 4.607-1.296 5.752-1.768.924-.381 1.854-.736 2.291-1.174.439-.435.406-1.178-.043-1.627z"/><path fill="#EA596E" d="M25.312 4.351c-.876.875-.876 2.293 0 3.168l3.167 3.168c.876.874 2.294.874 3.168 0l3.169-3.168c.874-.875.874-2.293 0-3.168l-3.169-3.168c-.874-.875-2.292-.875-3.168 0l-3.167 3.168z"/><path fill="#FFCC4D" d="M11.849 17.815l3.17 3.17 3.165 3.166 11.881-11.879-6.337-6.336-11.879 11.879z"/><path fill="#292F33" d="M11.298 26.742s-2.06 1.133-2.616.576c-.557-.558.581-2.611.581-2.611s1.951.036 2.035 2.035z"/><path fill="#CCD6DD" d="M23.728 5.935l3.96-3.96 6.336 6.337-3.96 3.96z"/><path fill="#99AAB5" d="M26.103 3.558l.792-.792 6.336 6.335-.792.792zM24.52 5.142l.791-.791 6.336 6.335-.792.792z"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
61
src/App.tsx
Normal file
61
src/App.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import SourcePicker from "./components/SourcePicker";
|
||||||
|
import dropzoneStyles from "./dropzone.module.scss";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const {
|
||||||
|
getRootProps,
|
||||||
|
getInputProps,
|
||||||
|
acceptedFiles,
|
||||||
|
isFocused,
|
||||||
|
isDragAccept,
|
||||||
|
isDragReject,
|
||||||
|
} = useDropzone({
|
||||||
|
multiple: false,
|
||||||
|
});
|
||||||
|
const selectedAsar =
|
||||||
|
acceptedFiles.length === 1 ? acceptedFiles[0] : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mt-5">
|
||||||
|
<h1>Electron Injector</h1>
|
||||||
|
<div
|
||||||
|
{...getRootProps({
|
||||||
|
className: dropzoneStyles.dropzone,
|
||||||
|
style: {
|
||||||
|
borderColor: isDragAccept
|
||||||
|
? "#00e676"
|
||||||
|
: isFocused
|
||||||
|
? "#2196f3"
|
||||||
|
: isDragReject
|
||||||
|
? "#ff1744"
|
||||||
|
: "#eeeeee",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
{selectedAsar ? (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Drag and drop another .asar file here to replace the current one
|
||||||
|
</p>
|
||||||
|
<em>
|
||||||
|
{selectedAsar.name} - {prettyBytes(selectedAsar.size)}
|
||||||
|
</em>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>Drag and drop .asar file here or click to browse</p>
|
||||||
|
<em>
|
||||||
|
No .asar file selected. Please drag and drop or click to browse.
|
||||||
|
</em>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<SourcePicker selectedAsar={selectedAsar} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
211
src/Asar.ts
Normal file
211
src/Asar.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace AsarRaw {
|
||||||
|
export interface FileIntegrity {
|
||||||
|
algorithm: string;
|
||||||
|
blockSize: number;
|
||||||
|
blocks: string[];
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface File {
|
||||||
|
integrity?: FileIntegrity;
|
||||||
|
offset: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Folder {
|
||||||
|
files: Record<string, Dirent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Dirent = File | Folder;
|
||||||
|
|
||||||
|
export function isFolder(dirent: Dirent): dirent is Folder {
|
||||||
|
return "files" in dirent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Folder {
|
||||||
|
files: Record<string, File>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface File extends AsarRaw.File {
|
||||||
|
newValue?: Blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Dirent = File | Folder;
|
||||||
|
|
||||||
|
export function isFolder(dirent: Dirent): dirent is Folder {
|
||||||
|
return "files" in dirent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function modifyFile(file: File, newValue: Blob) {
|
||||||
|
file.newValue = newValue;
|
||||||
|
file.size = newValue.size;
|
||||||
|
file.offset = "";
|
||||||
|
|
||||||
|
if (file.integrity) {
|
||||||
|
const hash = await digestSHA256(await newValue.arrayBuffer());
|
||||||
|
|
||||||
|
file.integrity = {
|
||||||
|
algorithm: "SHA256",
|
||||||
|
blockSize: newValue.size,
|
||||||
|
blocks: [hash],
|
||||||
|
hash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFile(
|
||||||
|
folder: Folder,
|
||||||
|
name: string,
|
||||||
|
newValue: Blob,
|
||||||
|
integrity = false
|
||||||
|
) {
|
||||||
|
const file: File = {
|
||||||
|
newValue,
|
||||||
|
size: newValue.size,
|
||||||
|
offset: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (integrity) {
|
||||||
|
const hash = await digestSHA256(await newValue.arrayBuffer());
|
||||||
|
|
||||||
|
file.integrity = {
|
||||||
|
algorithm: "SHA256",
|
||||||
|
blockSize: newValue.size,
|
||||||
|
blocks: [hash],
|
||||||
|
hash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
folder.files[name] = file;
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function digestSHA256(data: ArrayBuffer) {
|
||||||
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data); // hash the message
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
|
||||||
|
const hashHex = hashArray
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join(""); // convert bytes to hex string
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compileRoot(asar: Asar) {
|
||||||
|
const newRoot: AsarRaw.Folder = { files: {} };
|
||||||
|
|
||||||
|
const parts: BlobPart[] = [];
|
||||||
|
let partsOffset = 0;
|
||||||
|
|
||||||
|
interface StackItem {
|
||||||
|
oldFolder: Folder;
|
||||||
|
newFolder: AsarRaw.Folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stack: StackItem[] = [
|
||||||
|
{
|
||||||
|
oldFolder: asar,
|
||||||
|
newFolder: newRoot,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let e: StackItem | undefined;
|
||||||
|
|
||||||
|
while ((e = stack.pop())) {
|
||||||
|
for (const name in e.oldFolder.files) {
|
||||||
|
const dirent = e.oldFolder.files[name];
|
||||||
|
|
||||||
|
if (isFolder(dirent)) {
|
||||||
|
const newFolder: AsarRaw.Folder = {
|
||||||
|
files: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
e.newFolder.files[name] = newFolder;
|
||||||
|
|
||||||
|
stack.push({
|
||||||
|
oldFolder: dirent,
|
||||||
|
newFolder,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const data = asar.getFile(dirent);
|
||||||
|
|
||||||
|
const newFile: AsarRaw.File = {
|
||||||
|
offset: partsOffset.toString(),
|
||||||
|
size: data.size,
|
||||||
|
integrity: dirent.integrity,
|
||||||
|
};
|
||||||
|
|
||||||
|
parts.push(data);
|
||||||
|
|
||||||
|
partsOffset += newFile.size;
|
||||||
|
|
||||||
|
e.newFolder.files[name] = newFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
root: newRoot,
|
||||||
|
data: new Blob(parts, { type: "application/octet-stream" }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function compileAsar(asar: Asar) {
|
||||||
|
const header = new ArrayBuffer(16);
|
||||||
|
const headerView = new DataView(header);
|
||||||
|
|
||||||
|
const compiled = await compileRoot(asar);
|
||||||
|
const rootEncoded = new TextEncoder().encode(JSON.stringify(compiled.root));
|
||||||
|
|
||||||
|
headerView.setUint32(0, 0x4, true);
|
||||||
|
headerView.setUint32(4, rootEncoded.byteLength + 8, true);
|
||||||
|
headerView.setUint32(8, rootEncoded.byteLength + 4, true);
|
||||||
|
headerView.setUint32(12, rootEncoded.byteLength, true);
|
||||||
|
|
||||||
|
return new Blob([headerView, rootEncoded, compiled.data], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Asar implements Folder {
|
||||||
|
private headerSize: number;
|
||||||
|
private blob: Blob;
|
||||||
|
files: Folder["files"];
|
||||||
|
constructor(blob: Blob, headerSize: number, root: Folder) {
|
||||||
|
this.blob = blob;
|
||||||
|
this.headerSize = headerSize;
|
||||||
|
this.files = root.files;
|
||||||
|
}
|
||||||
|
getFile(file: File) {
|
||||||
|
if (file.newValue) return file.newValue;
|
||||||
|
const offset = 8 + this.headerSize + parseInt(file.offset);
|
||||||
|
return this.blob.slice(offset, offset + file.size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const asarURL = "http://test";
|
||||||
|
|
||||||
|
export function resolvePath(folder: Folder, path: string) {
|
||||||
|
const { pathname } = new URL(path, asarURL);
|
||||||
|
|
||||||
|
let depth: Dirent = folder;
|
||||||
|
|
||||||
|
for (const file of pathname.split("/").slice(1)) {
|
||||||
|
if (!isFolder(depth))
|
||||||
|
throw new TypeError("Attempt to read file inside file");
|
||||||
|
depth = depth.files[file];
|
||||||
|
if (!depth) throw new TypeError("File not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openAsar(blob: Blob) {
|
||||||
|
const header = blob.slice(0, 8);
|
||||||
|
const rootSize = new DataView(await header.arrayBuffer()).getUint32(4, true);
|
||||||
|
|
||||||
|
const root = JSON.parse(await blob.slice(16, 8 + rootSize).text()) as Folder;
|
||||||
|
|
||||||
|
return new Asar(blob, rootSize, root);
|
||||||
|
}
|
||||||
11
src/components/SourcePicker.module.scss
Normal file
11
src/components/SourcePicker.module.scss
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
.tab:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sourceNavItem {
|
||||||
|
flex: auto;
|
||||||
|
text-align: center;
|
||||||
|
@media only screen and (max-width: 770px) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
316
src/components/SourcePicker.tsx
Normal file
316
src/components/SourcePicker.tsx
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
import dropzoneStyles from "../dropzone.module.scss";
|
||||||
|
import { SourceType, injectScript } from "../inject";
|
||||||
|
import styles from "./SourcePicker.module.scss";
|
||||||
|
import { Editor } from "@monaco-editor/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import type { editor } from "monaco-editor";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import type { MouseEventHandler } from "react";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
|
||||||
|
async function download(blob: Blob, name: string) {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement("a");
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = name;
|
||||||
|
document.documentElement.append(anchor);
|
||||||
|
anchor.click();
|
||||||
|
anchor.remove();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InjectButton({
|
||||||
|
busy,
|
||||||
|
pickedAsar,
|
||||||
|
setScriptURL,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
busy: boolean;
|
||||||
|
pickedAsar: boolean;
|
||||||
|
setScriptURL?: boolean;
|
||||||
|
onClick: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}) {
|
||||||
|
const [mouseEnter, setMouseEnter] = useState(false);
|
||||||
|
|
||||||
|
let tooltipMessage: string | undefined;
|
||||||
|
|
||||||
|
if (busy) tooltipMessage = "Please wait";
|
||||||
|
else if (!pickedAsar) tooltipMessage = "Please add an .asar file";
|
||||||
|
else if (setScriptURL === false) tooltipMessage = "Please add a script URL";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="mt-2"
|
||||||
|
style={{ display: "inline-block", position: "relative" }}
|
||||||
|
onMouseOver={() => setMouseEnter(true)}
|
||||||
|
onMouseLeave={() => setMouseEnter(false)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
disabled={typeof tooltipMessage === "string"}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
Inject Script
|
||||||
|
</button>
|
||||||
|
{typeof tooltipMessage === "string" && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"tooltip",
|
||||||
|
"bs-tooltip-auto",
|
||||||
|
"fade",
|
||||||
|
mouseEnter && "show"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: "100%",
|
||||||
|
width: "max-content",
|
||||||
|
pointerEvents: "none",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
role="tooltip"
|
||||||
|
data-popper-placement="right"
|
||||||
|
>
|
||||||
|
<div className="tooltip-inner">{tooltipMessage}</div>
|
||||||
|
<div
|
||||||
|
className="tooltip-arrow"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "100%",
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
margin: "auto",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SourcePicker({
|
||||||
|
selectedAsar,
|
||||||
|
}: {
|
||||||
|
selectedAsar: File | undefined;
|
||||||
|
}) {
|
||||||
|
const [output, setOutput] = useState<
|
||||||
|
{ blob: Blob; name: string } | undefined
|
||||||
|
>();
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [openDevTools, setOpenDevTools] = useState(false);
|
||||||
|
const [disableWebSecurity, setDisableWebSecurity] = useState(false);
|
||||||
|
const { getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } =
|
||||||
|
useDropzone({
|
||||||
|
accept: { "application/javascript": [".js"] },
|
||||||
|
multiple: true,
|
||||||
|
onDropAccepted: async (scripts) => {
|
||||||
|
// combine all the selected scripts into one
|
||||||
|
editorRef.current?.setValue(
|
||||||
|
(await Promise.all(scripts.map((script) => script.text()))).join("\n")
|
||||||
|
);
|
||||||
|
setActiveTab(1); // go to code editor tab
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const [url, setURL] = useState("");
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
|
||||||
|
const editorRef = useRef<editor.IStandaloneCodeEditor>();
|
||||||
|
|
||||||
|
const handleInject = async (type: SourceType, value: string) => {
|
||||||
|
if (!selectedAsar) return;
|
||||||
|
|
||||||
|
setBusy(true);
|
||||||
|
|
||||||
|
injectScript(selectedAsar, type, value, openDevTools, disableWebSecurity)
|
||||||
|
.then((blob) => {
|
||||||
|
setOutput({
|
||||||
|
name: `injected.${selectedAsar.name}`,
|
||||||
|
blob,
|
||||||
|
});
|
||||||
|
setActiveTab(3); // go to output tab
|
||||||
|
})
|
||||||
|
.finally(() => setBusy(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
title: <>Script URLs</>,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<div className="form-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className="form-control"
|
||||||
|
id="exampleInputEmail1"
|
||||||
|
aria-describedby="scriptURLHelp"
|
||||||
|
placeholder="https://example.com/script1.js,https://example.com/script2.js"
|
||||||
|
onChange={(e) => setURL(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<small id="scriptURLHelp" className="form-text text-muted">
|
||||||
|
Please provide a comma-separated list of script URLs without
|
||||||
|
spaces. Each URL should point to a JavaScript file. The files will
|
||||||
|
be fetched and executed each time a new page is loaded in the
|
||||||
|
Electron app.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<InjectButton
|
||||||
|
busy={busy}
|
||||||
|
pickedAsar={selectedAsar !== undefined}
|
||||||
|
setScriptURL={url !== ""}
|
||||||
|
onClick={() => handleInject(SourceType.url, url)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <>Copy & Paste JavaScript Code</>,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<Editor
|
||||||
|
onMount={(e) => (editorRef.current = e)}
|
||||||
|
height="200px"
|
||||||
|
defaultLanguage="javascript"
|
||||||
|
defaultValue={code}
|
||||||
|
onChange={(value) => setCode(value || "")}
|
||||||
|
options={{ readOnly: busy }}
|
||||||
|
/>
|
||||||
|
<InjectButton
|
||||||
|
busy={busy}
|
||||||
|
pickedAsar={selectedAsar !== undefined}
|
||||||
|
onClick={() => handleInject(SourceType.url, url)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <>Upload JavaScript Files</>,
|
||||||
|
content: (
|
||||||
|
<div
|
||||||
|
{...getRootProps({
|
||||||
|
className: dropzoneStyles.dropzone,
|
||||||
|
style: {
|
||||||
|
borderColor: isDragAccept
|
||||||
|
? "#00e676"
|
||||||
|
: isDragReject
|
||||||
|
? "#ff1744"
|
||||||
|
: isFocused
|
||||||
|
? "#2196f3"
|
||||||
|
: "#eeeeee",
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<p>
|
||||||
|
Drag and drop your JavaScript files here or click to browse multiple
|
||||||
|
files
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <>Output</>,
|
||||||
|
content: output && (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
<em>
|
||||||
|
{output.name} - {prettyBytes(output.blob.size)}
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => {
|
||||||
|
download(output.blob, output.name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="card bg-secondary mb-3 mt-2"
|
||||||
|
style={{ overflow: "hidden" }}
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
className={clsx("nav", "nav-tabs", styles.sourceNavTabs)}
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<li
|
||||||
|
className={clsx("nav-item", styles.sourceNavItem)}
|
||||||
|
role="presentation"
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
className={clsx(
|
||||||
|
styles.tab,
|
||||||
|
"nav-link",
|
||||||
|
i === activeTab && "active"
|
||||||
|
)}
|
||||||
|
data-bs-toggle="tab"
|
||||||
|
aria-selected="false"
|
||||||
|
role="tab"
|
||||||
|
onClick={() => setActiveTab(i)}
|
||||||
|
>
|
||||||
|
{tab.title}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="tab-content">
|
||||||
|
{tabs.map((tab, i) => (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"tab-pane",
|
||||||
|
"fade",
|
||||||
|
i === activeTab && ["active", "show"]
|
||||||
|
)}
|
||||||
|
role="tabpanel"
|
||||||
|
key={i}
|
||||||
|
>
|
||||||
|
{tab.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<fieldset className="form-group">
|
||||||
|
<legend className="mt-4">Options</legend>
|
||||||
|
<div className="form-check">
|
||||||
|
<label className="form-check-label">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={openDevTools}
|
||||||
|
onChange={(e) => setOpenDevTools(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
Open DevTools
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="form-check">
|
||||||
|
<label className="form-check-label">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={disableWebSecurity}
|
||||||
|
onChange={(e) => setDisableWebSecurity(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
Disable WebSecurity
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
src/dropzone.module.scss
Normal file
17
src/dropzone.module.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.dropzone {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
border-width: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-style: dashed;
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #bdbdbd;
|
||||||
|
outline: none;
|
||||||
|
transition: border 0.24s ease-in-out;
|
||||||
|
user-select: none;
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/index.css
Normal file
0
src/index.css
Normal file
99
src/inject.ts
Normal file
99
src/inject.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import {
|
||||||
|
compileAsar,
|
||||||
|
isFolder,
|
||||||
|
createFile,
|
||||||
|
openAsar,
|
||||||
|
resolvePath,
|
||||||
|
modifyFile,
|
||||||
|
} from "./Asar";
|
||||||
|
|
||||||
|
interface ElectronPackage {
|
||||||
|
main: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SourceType {
|
||||||
|
url,
|
||||||
|
code,
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateHook(
|
||||||
|
preload: string,
|
||||||
|
openDevTools: boolean,
|
||||||
|
disableWebSecurity: boolean
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
"(() => {" +
|
||||||
|
'"use strict";' +
|
||||||
|
'const { join } = require("path");' +
|
||||||
|
'const { BrowserWindow } = require("electron");' +
|
||||||
|
"const electronModule = require.cache.electron;" +
|
||||||
|
"const electronExports = electronModule.exports;" +
|
||||||
|
"const descs = Object.getOwnPropertyDescriptors(electronExports);" +
|
||||||
|
`const customPreload = join(__dirname, ${JSON.stringify(preload)});` +
|
||||||
|
"function hooked(options) {" +
|
||||||
|
"const oldPreload = options && options.webPreferences ? options.webPreferences.preload : undefined;" +
|
||||||
|
`const bw = new BrowserWindow({ ...(options || {}), webPreferences: { ...(options.webPreferences || {}), preload: customPreload, sandbox: false${
|
||||||
|
disableWebSecurity ? ", webSecurity: false" : ""
|
||||||
|
} } });` +
|
||||||
|
(openDevTools ? 'bw.webContents.openDevTools({ mode: "undocked" });' : "") +
|
||||||
|
'bw.webContents.on("ipc-message-sync", (event, channel) => { if (channel === "original-preload") event.returnValue = oldPreload; });' +
|
||||||
|
"return bw;" +
|
||||||
|
"};" +
|
||||||
|
"descs.BrowserWindow.get = () => hooked;" +
|
||||||
|
"const newExports = Object.defineProperties({}, descs);" +
|
||||||
|
'Object.defineProperty(electronModule, "exports", { get: () => newExports, configurable: true, enumerable: true });' +
|
||||||
|
"})();"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function injectScript(
|
||||||
|
blob: Blob,
|
||||||
|
sourceType: SourceType,
|
||||||
|
value: string,
|
||||||
|
openDevTools: boolean,
|
||||||
|
disableWebSecurity: boolean
|
||||||
|
) {
|
||||||
|
const asar = await openAsar(blob);
|
||||||
|
|
||||||
|
const preloadFile = "custom-preload.js";
|
||||||
|
|
||||||
|
// we use eval() to expose require() and other nodejs functions to the userscripts
|
||||||
|
// this is dangerous but very good for making cheats harder to detect
|
||||||
|
const preloadEval =
|
||||||
|
sourceType === SourceType.code
|
||||||
|
? `try { eval(${JSON.stringify(
|
||||||
|
value + "\n//# sourceURL=injected"
|
||||||
|
)}) } catch (err) { console.error(err) }`
|
||||||
|
: `for (const src of ${JSON.stringify(
|
||||||
|
value.split(",").map((val) => val.trim())
|
||||||
|
)}) try {` +
|
||||||
|
"const http = new XMLHttpRequest();" +
|
||||||
|
'http.open("GET", src, false);' +
|
||||||
|
"http.send();" +
|
||||||
|
'try{eval(http.responseText + "\\n//# sourceURL=" + src) } catch (err) { console.error(err) }' +
|
||||||
|
"} catch (err) { console.error(err) }";
|
||||||
|
|
||||||
|
const preload =
|
||||||
|
preloadEval +
|
||||||
|
'const originalPreload = require("electron").ipcRenderer.sendSync("original-preload");' +
|
||||||
|
"if (originalPreload) require(originalPreload);";
|
||||||
|
|
||||||
|
const main = generateHook(preloadFile, openDevTools, disableWebSecurity);
|
||||||
|
|
||||||
|
const pkgFile = resolvePath(asar, "package.json");
|
||||||
|
if (isFolder(pkgFile)) throw new Error("package.json was a folder");
|
||||||
|
|
||||||
|
const pkg = JSON.parse(await asar.getFile(pkgFile).text()) as ElectronPackage;
|
||||||
|
|
||||||
|
const mainFile = resolvePath(asar, pkg.main);
|
||||||
|
|
||||||
|
if (isFolder(mainFile)) throw new Error("entry point was a folder");
|
||||||
|
|
||||||
|
const mainJS = await asar.getFile(mainFile).text();
|
||||||
|
|
||||||
|
await createFile(asar, preloadFile, new Blob([preload]));
|
||||||
|
|
||||||
|
await modifyFile(mainFile, new Blob([main + mainJS]));
|
||||||
|
|
||||||
|
return await compileAsar(asar);
|
||||||
|
}
|
||||||
11
src/main.tsx
Normal file
11
src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import App from "./App";
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import "bootswatch/dist/zephyr/bootstrap.min.css";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
vite.config.ts
Normal file
7
vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user