mirror of
https://github.com/e9x/electron-injector.git
synced 2025-12-15 15:07:41 +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