mirror of
https://github.com/xenos1337/httptoolkit-patcher.git
synced 2026-07-15 16:20:02 +02:00
On macOS, the integrity hash is stored in Info.plist, not embedded in the executable binary. Added patchInfoPlistHash() and platform branching so macOS patches Info.plist while Windows/Linux patch the binary. Also updated dependencies for compatibility: - @electron/asar 4.0.1 → 4.1.0 (named export change) - chalk 4 → 5.6.2 (tagged template → function call syntax) - @types/node 20 → 25 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
761 lines
25 KiB
JavaScript
761 lines
25 KiB
JavaScript
// @ts-check
|
|
import { spawn, execSync } from "child_process";
|
|
import * as asar from "@electron/asar";
|
|
import chalk from "chalk";
|
|
import path from "path";
|
|
import fs from "fs";
|
|
import readline from "readline";
|
|
import https from "https";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const isWin = process.platform === "win32";
|
|
const isMac = process.platform === "darwin";
|
|
const isLinux = process.platform === "linux";
|
|
|
|
// Handle both ESM and pkg-bundled scenarios
|
|
const __filename = (() => {
|
|
// Check if running as pkg bundle
|
|
// @ts-ignore - pkg adds this property at runtime
|
|
if (process.pkg) {
|
|
return process.execPath;
|
|
}
|
|
// ESM: use import.meta.url
|
|
return fileURLToPath(import.meta.url);
|
|
})();
|
|
|
|
// Version info - read from package.json
|
|
const GITHUB_REPO = "xenos1337/httptoolkit-patcher";
|
|
const LOCAL_VERSION = (() => {
|
|
try {
|
|
const packageJsonPath = path.join(path.dirname(__filename), "package.json");
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
return packageJson.version || "0.0.0";
|
|
} catch {
|
|
return "0.0.0";
|
|
}
|
|
})();
|
|
|
|
// Check if running with elevated privileges
|
|
function isElevated() {
|
|
if (isWin) {
|
|
try {
|
|
execSync("net session", { stdio: "ignore" });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
} else {
|
|
return process.getuid && process.getuid() === 0;
|
|
}
|
|
}
|
|
|
|
// Helper function to prompt user for input
|
|
function prompt(question) {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
return new Promise(resolve => {
|
|
rl.question(question, answer => {
|
|
rl.close();
|
|
resolve(answer);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch JSON from a URL
|
|
* @param {string} url
|
|
* @returns {Promise<Array<{name: string}>>}
|
|
*/
|
|
function fetchJson(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const options = {
|
|
headers: {
|
|
"User-Agent": "httptoolkit-patcher",
|
|
Accept: "application/vnd.github.v3+json",
|
|
},
|
|
};
|
|
https
|
|
.get(url, options, res => {
|
|
if (res.statusCode !== 200) {
|
|
reject(new Error(`HTTP ${res.statusCode}`));
|
|
return;
|
|
}
|
|
let data = "";
|
|
res.on("data", chunk => (data += chunk));
|
|
res.on("end", () => {
|
|
try {
|
|
resolve(JSON.parse(data));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
})
|
|
.on("error", reject);
|
|
});
|
|
}
|
|
|
|
function compareVersions(v1, v2) {
|
|
const normalize = (/** @type {string} */ v) => v.replace(/^v/, "").split(".").map(Number);
|
|
const parts1 = normalize(v1);
|
|
const parts2 = normalize(v2);
|
|
|
|
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
const num1 = parts1[i] || 0;
|
|
const num2 = parts2[i] || 0;
|
|
if (num1 > num2) return 1;
|
|
if (num1 < num2) return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Check for updates from GitHub
|
|
*/
|
|
async function checkForUpdates() {
|
|
try {
|
|
const tags = await fetchJson(`https://api.github.com/repos/${GITHUB_REPO}/tags`);
|
|
|
|
if (!tags || tags.length === 0) {
|
|
return; // No tags found, skip update check
|
|
}
|
|
|
|
// Tags are returned in order, first one is the latest
|
|
const latestTag = tags[0].name;
|
|
const latestVersion = latestTag.replace(/^v/, "");
|
|
|
|
if (compareVersions(latestVersion, LOCAL_VERSION) > 0) {
|
|
console.log(chalk.yellowBright('\n╔════════════════════════════════════════════════════════════╗'));
|
|
console.log(chalk.yellowBright('║') + chalk.white(' A new version is available: ') + chalk.greenBright(`v${latestVersion}`) + chalk.white(' (current: ') + chalk.gray(`v${LOCAL_VERSION}`) + chalk.white(') ') + chalk.yellowBright(' ║'));
|
|
console.log(chalk.yellowBright('║') + chalk.white(' Update: ') + chalk.cyanBright(`https://github.com/${GITHUB_REPO}`) + chalk.white(' ') + chalk.yellowBright('║'));
|
|
console.log(chalk.yellowBright('╚════════════════════════════════════════════════════════════╝\n'));
|
|
}
|
|
} catch (e) {
|
|
// Silently ignore update check errors (network issues, etc.)
|
|
}
|
|
}
|
|
|
|
// Helper function to remove directory recursively
|
|
function rm(dirPath) {
|
|
if (!fs.existsSync(dirPath)) return;
|
|
if (!fs.lstatSync(dirPath).isDirectory()) return fs.rmSync(dirPath, { force: true });
|
|
for (const entry of fs.readdirSync(dirPath)) {
|
|
const entryPath = path.join(dirPath, entry);
|
|
if (fs.lstatSync(entryPath).isDirectory()) rm(entryPath);
|
|
else fs.rmSync(entryPath, { force: true });
|
|
}
|
|
fs.rmdirSync(dirPath);
|
|
}
|
|
|
|
// Find HTTP Toolkit installation path
|
|
async function findAppPath() {
|
|
const possiblePaths = isWin
|
|
? [path.join("C:", "Program Files", "HTTP Toolkit", "resources"), path.join("C:", "Program Files (x86)", "HTTP Toolkit", "resources"), path.join(process.env.LOCALAPPDATA || path.join(process.env.USERPROFILE || "", "AppData", "Local"), "Programs", "HTTP Toolkit", "resources")]
|
|
: isMac
|
|
? ["/Applications/HTTP Toolkit.app/Contents/Resources"]
|
|
: ["/opt/HTTP Toolkit/resources", "/opt/httptoolkit/resources"];
|
|
|
|
for (const p of possiblePaths) {
|
|
if (fs.existsSync(path.join(p, "app.asar"))) {
|
|
return p;
|
|
}
|
|
}
|
|
|
|
console.log(chalk.yellowBright('[!] HTTP Toolkit not found in default locations'));
|
|
const userPath = await prompt("Please enter the path to HTTP Toolkit executable/app: ");
|
|
|
|
if (!userPath) {
|
|
console.error(chalk.redBright('[-] No path provided'));
|
|
process.exit(1);
|
|
}
|
|
|
|
// Extract resources path from user input
|
|
let resourcesPath = userPath.trim().replace(/['"]/g, "");
|
|
|
|
if (resourcesPath.endsWith(".exe") || resourcesPath.endsWith(".app")) resourcesPath = path.dirname(resourcesPath);
|
|
if (!resourcesPath.endsWith("resources")) resourcesPath = path.join(resourcesPath, "resources");
|
|
|
|
if (!fs.existsSync(path.join(resourcesPath, "app.asar"))) {
|
|
console.error(chalk.redBright(`[-] app.asar not found at ${resourcesPath}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
return resourcesPath;
|
|
}
|
|
|
|
// Request elevated permissions
|
|
async function requestElevation() {
|
|
console.log(chalk.yellowBright('[!] Requesting elevated permissions...'));
|
|
|
|
// @ts-ignore - pkg adds this property at runtime
|
|
const isBundled = process.pkg;
|
|
|
|
if (isWin) {
|
|
// Windows: Use PowerShell to run as administrator
|
|
let script;
|
|
if (isBundled) {
|
|
script = `Start-Process -FilePath "${__filename}" -Verb RunAs`;
|
|
} else {
|
|
script = `Start-Process -FilePath "node" -ArgumentList "${__filename}" -Verb RunAs`;
|
|
}
|
|
|
|
try {
|
|
console.log(chalk.greenBright(`[+] Spawning PowerShell with script: ${script}`));
|
|
execSync(`powershell -Command "${script}"`, { stdio: "inherit" });
|
|
console.log(chalk.blueBright('[+] Restarting with administrator privileges...'));
|
|
process.exit(0);
|
|
} catch (e) {
|
|
console.error(chalk.redBright(`[-] Failed to elevate permissions: ${e.message}`));
|
|
console.error(chalk.redBright('[-] Please run as administrator manually'));
|
|
process.exit(1);
|
|
}
|
|
} else if (isLinux) {
|
|
// Linux: Cannot auto-elevate with sudo, show instructions instead
|
|
console.log(chalk.yellowBright('[!] Elevated permissions are required for patching on Linux'));
|
|
console.log(chalk.yellowBright('[!] Please re-run this script with sudo:'));
|
|
if (isBundled) {
|
|
console.log(chalk.blueBright(` sudo ${__filename}`));
|
|
} else {
|
|
console.log(chalk.blueBright(` sudo node ${__filename}`));
|
|
}
|
|
process.exit(1);
|
|
} else {
|
|
// macOS: Try to elevate with sudo
|
|
console.log(chalk.blueBright('[+] Restarting with sudo...'));
|
|
try {
|
|
let child;
|
|
if (isBundled) {
|
|
child = spawn("sudo", [__filename], {
|
|
stdio: "inherit",
|
|
});
|
|
} else {
|
|
child = spawn("sudo", ["node", __filename], {
|
|
stdio: "inherit",
|
|
});
|
|
}
|
|
child.on("exit", code => process.exit(code || 0));
|
|
} catch (e) {
|
|
console.error(chalk.redBright(`[-] Failed to elevate permissions: ${e.message}`));
|
|
console.error(chalk.redBright('[-] Please run with sudo manually'));
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we have write permissions
|
|
function checkPermissions(filePath) {
|
|
try {
|
|
// Check write access to the file/directory
|
|
fs.accessSync(filePath, fs.constants.W_OK);
|
|
|
|
// Check if we can create directories
|
|
const testDirPath = path.join(path.dirname(filePath), `.test_${Date.now()}`);
|
|
try {
|
|
fs.mkdirSync(testDirPath, { recursive: true });
|
|
fs.rmdirSync(testDirPath);
|
|
} catch (dirError) {
|
|
console.error(chalk.redBright(`[-] Cannot create directories in ${path.dirname(filePath)}: ${dirError.message}`));
|
|
return false;
|
|
}
|
|
|
|
console.log(chalk.greenBright(`[+] Permissions check passed for ${filePath}`));
|
|
return true;
|
|
} catch (e) {
|
|
console.error(chalk.redBright(`[-] Permissions check failed for ${filePath}: ${e.message}`));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Kill HTTP Toolkit processes
|
|
async function killHttpToolkit() {
|
|
console.log(chalk.yellowBright('[+] Checking for running HTTP Toolkit processes...'));
|
|
|
|
try {
|
|
if (isWin) {
|
|
// Windows: Use tasklist to find and taskkill to terminate
|
|
const output = execSync('tasklist /FI "IMAGENAME eq HTTP Toolkit.exe" /FO CSV /NH', { encoding: "utf-8" });
|
|
if (output.includes("HTTP Toolkit.exe")) {
|
|
console.log(chalk.yellowBright('[!] HTTP Toolkit is running, attempting to close it...'));
|
|
execSync('taskkill /F /IM "HTTP Toolkit.exe" /T', { stdio: "ignore" });
|
|
console.log(chalk.greenBright('[+] HTTP Toolkit processes terminated'));
|
|
// Wait a moment for the process to fully close
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
} else {
|
|
console.log(chalk.greenBright('[+] HTTP Toolkit is not running'));
|
|
}
|
|
} else if (isMac) {
|
|
// macOS: Use pgrep and pkill
|
|
try {
|
|
execSync('pgrep -f "HTTP Toolkit"', { stdio: "ignore" });
|
|
console.log(chalk.yellowBright('[!] HTTP Toolkit is running, attempting to close it...'));
|
|
execSync('pkill -f "HTTP Toolkit"', { stdio: "ignore" });
|
|
console.log(chalk.greenBright('[+] HTTP Toolkit processes terminated'));
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
} catch (e) {
|
|
console.log(chalk.greenBright('[+] HTTP Toolkit is not running'));
|
|
}
|
|
} else {
|
|
// Linux: Use pgrep and pkill
|
|
try {
|
|
execSync('pgrep -f "HTTP Toolkit"', { stdio: "ignore" });
|
|
console.log(chalk.yellowBright('[!] HTTP Toolkit is running, attempting to close it...'));
|
|
execSync('pkill -f "HTTP Toolkit"', { stdio: "ignore" });
|
|
console.log(chalk.greenBright('[+] HTTP Toolkit processes terminated'));
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
} catch (e) {
|
|
console.log(chalk.greenBright('[+] HTTP Toolkit is not running'));
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log(chalk.yellowBright(`[!] Could not check/kill processes: ${e.message}`));
|
|
console.log(chalk.yellowBright('[!] If HTTP Toolkit is running, please close it manually'));
|
|
}
|
|
}
|
|
|
|
// Resolve the base installation directory (without the trailing resources folder)
|
|
function getBinaryBasePath(resourcesPath) {
|
|
const normalized = resourcesPath.replace(/[\\/]+$/, "");
|
|
if (normalized.toLowerCase().endsWith("resources")) {
|
|
return path.dirname(normalized);
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
// Determine the executable path for HTTP Toolkit based on platform
|
|
function getExecutablePath(resourcesPath) {
|
|
const basePath = getBinaryBasePath(resourcesPath);
|
|
const candidates = isWin ? [path.join(basePath, "HTTP Toolkit.exe"), path.join(basePath, "httptoolkit.exe")] : isMac ? [path.join(basePath, "MacOS", "HTTP Toolkit"), path.join(basePath, "MacOS", "HTTP Toolkit Preview")] : [path.join(basePath, "httptoolkit"), path.join(basePath, "HTTP Toolkit")];
|
|
|
|
for (const candidate of candidates) {
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
throw new Error(`Could not locate HTTP Toolkit executable near ${resourcesPath}`);
|
|
}
|
|
|
|
// Extract hashes from integrity check output
|
|
function extractIntegrityHashes(output) {
|
|
const regex = /Integrity check failed for asar archive[\s\S]*?\(\s*([0-9a-f]{64})\s*vs\s*([0-9a-f]{64})\s*\)/i;
|
|
const match = output.match(regex);
|
|
if (!match) return null;
|
|
return {
|
|
originalHash: match[1],
|
|
newHash: match[2],
|
|
};
|
|
}
|
|
|
|
// Launch the app once to grab integrity hashes from its crash output
|
|
async function captureIntegrityHashes(executablePath) {
|
|
return new Promise((resolve, reject) => {
|
|
let output = "";
|
|
let finished = false;
|
|
const child = spawn(executablePath, {
|
|
cwd: path.dirname(executablePath),
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
windowsHide: true,
|
|
});
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (!finished) {
|
|
finished = true;
|
|
child.kill();
|
|
reject(new Error("Timed out waiting for integrity check output"));
|
|
}
|
|
}, 20000);
|
|
|
|
const handleData = data => {
|
|
output += data.toString();
|
|
const hashes = extractIntegrityHashes(output);
|
|
if (hashes && !finished) {
|
|
finished = true;
|
|
clearTimeout(timeout);
|
|
child.kill();
|
|
resolve(hashes);
|
|
}
|
|
};
|
|
|
|
child.stdout.on("data", handleData);
|
|
child.stderr.on("data", handleData);
|
|
|
|
child.on("error", err => {
|
|
if (!finished) {
|
|
finished = true;
|
|
clearTimeout(timeout);
|
|
reject(err);
|
|
}
|
|
});
|
|
|
|
child.on("exit", () => {
|
|
if (!finished) {
|
|
clearTimeout(timeout);
|
|
const hashes = extractIntegrityHashes(output);
|
|
if (hashes) {
|
|
resolve(hashes);
|
|
} else {
|
|
reject(new Error("Could not find integrity hashes in HTTP Toolkit output"));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Replace all occurrences of the original hash with the new hash inside the binary
|
|
function patchExecutableHash(executablePath, originalHash, newHash) {
|
|
if (originalHash.length !== newHash.length) {
|
|
throw new Error("Hash lengths do not match; cannot safely patch binary");
|
|
}
|
|
|
|
const binary = fs.readFileSync(executablePath);
|
|
const originalBuf = Buffer.from(originalHash, "utf-8");
|
|
const newBuf = Buffer.from(newHash, "utf-8");
|
|
|
|
let occurrences = 0;
|
|
let idx = binary.indexOf(originalBuf);
|
|
while (idx !== -1) {
|
|
newBuf.copy(binary, idx);
|
|
occurrences += 1;
|
|
idx = binary.indexOf(originalBuf, idx + originalBuf.length);
|
|
}
|
|
|
|
if (occurrences === 0) {
|
|
throw new Error("Original hash not found in binary");
|
|
}
|
|
|
|
fs.writeFileSync(executablePath, binary);
|
|
return occurrences;
|
|
}
|
|
|
|
// Replace the asar integrity hash inside the macOS Info.plist
|
|
function patchInfoPlistHash(resourcesPath, originalHash, newHash) {
|
|
const basePath = getBinaryBasePath(resourcesPath);
|
|
const plistPath = path.join(basePath, "Info.plist");
|
|
|
|
if (!fs.existsSync(plistPath)) {
|
|
throw new Error(`Info.plist not found at ${plistPath}`);
|
|
}
|
|
|
|
let content = fs.readFileSync(plistPath, "utf-8");
|
|
|
|
if (!content.includes(originalHash)) {
|
|
throw new Error("Original hash not found in Info.plist");
|
|
}
|
|
|
|
content = content.replaceAll(originalHash, newHash);
|
|
fs.writeFileSync(plistPath, content, "utf-8");
|
|
}
|
|
|
|
// Unpatch/restore function
|
|
async function unpatchApp() {
|
|
console.log(chalk.blueBright('[+] HTTP Toolkit Unpatcher Started'));
|
|
|
|
const appPath = await findAppPath();
|
|
console.log(chalk.greenBright(`[+] HTTP Toolkit found at ${appPath}`));
|
|
|
|
await killHttpToolkit();
|
|
|
|
const asarPath = path.join(appPath, "app.asar");
|
|
const backupPath = path.join(appPath, "app.asar.bak");
|
|
const extractPath = path.join(appPath, "app.asar_extracted");
|
|
|
|
// Check if we have write permissions on both the directory and the file
|
|
const hasPermissions = checkPermissions(appPath) && checkPermissions(asarPath);
|
|
|
|
if (!hasPermissions) {
|
|
console.log(chalk.yellowBright(`[!] No write permissions for ${appPath}`));
|
|
|
|
if (isElevated()) {
|
|
console.error(chalk.redBright('[-] Still no permissions even with elevated privileges'));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.yellowBright('[!] Administrator/sudo privileges required for unpatching'));
|
|
await requestElevation();
|
|
}
|
|
|
|
if (!fs.existsSync(backupPath)) {
|
|
console.error(chalk.redBright(`[-] Backup file not found at ${backupPath}`));
|
|
console.error(chalk.redBright('[-] Cannot unpatch without backup file'));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.yellowBright('[+] Restoring from backup...'));
|
|
try {
|
|
fs.copyFileSync(backupPath, asarPath);
|
|
console.log(chalk.greenBright('[+] Restored app.asar from backup'));
|
|
} catch (e) {
|
|
console.error(chalk.redBright(`[-] Failed to restore backup: ${e.message}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
if (fs.existsSync(extractPath)) {
|
|
console.log(chalk.yellowBright('[+] Removing extracted files...'));
|
|
rm(extractPath);
|
|
console.log(chalk.greenBright('[+] Cleaned up extracted files'));
|
|
}
|
|
|
|
const removeBackup = await prompt("Do you want to remove the backup file? (y/n): ");
|
|
if (removeBackup.toLowerCase() === "y" || removeBackup.toLowerCase() === "yes") {
|
|
fs.rmSync(backupPath, { force: true });
|
|
console.log(chalk.greenBright('[+] Backup file removed'));
|
|
}
|
|
|
|
console.log(chalk.greenBright('[+] Successfully unpatched!'));
|
|
}
|
|
|
|
// Main patching function
|
|
async function patchApp() {
|
|
console.log(chalk.blueBright('[+] HTTP Toolkit Patcher Started'));
|
|
|
|
const appPath = await findAppPath();
|
|
console.log(chalk.greenBright(`[+] HTTP Toolkit found at ${appPath}`));
|
|
|
|
await killHttpToolkit();
|
|
|
|
const asarPath = path.join(appPath, "app.asar");
|
|
|
|
// Check if we have write permissions on both the directory and the file
|
|
const hasPermissions = checkPermissions(appPath) && checkPermissions(asarPath);
|
|
|
|
if (!hasPermissions) {
|
|
console.log(chalk.yellowBright(`[!] No write permissions for ${appPath}`));
|
|
|
|
if (isElevated()) {
|
|
console.error(chalk.redBright('[-] Still no permissions even with elevated privileges'));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.yellowBright('[!] Administrator/sudo privileges required for patching'));
|
|
await requestElevation();
|
|
}
|
|
|
|
const backupPath = path.join(appPath, "app.asar.bak");
|
|
if (!fs.existsSync(backupPath)) {
|
|
console.log(chalk.yellowBright('[+] Creating backup...'));
|
|
fs.copyFileSync(asarPath, backupPath);
|
|
console.log(chalk.greenBright(`[+] Backup created at ${backupPath}`));
|
|
}
|
|
|
|
const extractPath = path.join(appPath, "app.asar_extracted");
|
|
console.log(chalk.yellowBright('[+] Extracting app.asar...'));
|
|
rm(extractPath);
|
|
asar.extractAll(asarPath, extractPath);
|
|
console.log(chalk.greenBright(`[+] Extracted to ${extractPath}`));
|
|
|
|
const preloadPath = path.join(extractPath, "build", "preload.cjs");
|
|
if (!fs.existsSync(preloadPath)) {
|
|
console.error(chalk.redBright(`[-] preload.cjs not found in ${path.join(extractPath, "build")}`));
|
|
console.error(chalk.yellowBright('[!] Please download the latest version of HTTP Toolkit from https://httptoolkit.com/'));
|
|
rm(extractPath);
|
|
process.exit(1);
|
|
}
|
|
console.log(chalk.greenBright('[+] Found preload.cjs'));
|
|
|
|
console.log(chalk.yellowBright('[+] Reading inject code from local file...'));
|
|
const injectJsPath = path.join(path.dirname(__filename), "inject.js");
|
|
if (!fs.existsSync(injectJsPath)) {
|
|
console.error(chalk.redBright(`[-] inject.js not found at ${injectJsPath}`));
|
|
rm(extractPath);
|
|
process.exit(1);
|
|
}
|
|
const injectCode = fs.readFileSync(injectJsPath, "utf-8");
|
|
if (!injectCode || !injectCode.includes("PAGE-INJECT")) {
|
|
console.error(chalk.redBright('[-] Invalid inject.js file'));
|
|
rm(extractPath);
|
|
process.exit(1);
|
|
}
|
|
console.log(chalk.greenBright('[+] Inject code loaded successfully'));
|
|
|
|
let preloadContent = fs.readFileSync(preloadPath, "utf-8");
|
|
|
|
const electronVarName = preloadContent.includes("electron_1") ? "electron_1" : "electron";
|
|
console.log(chalk.greenBright(`[+] Detected electron variable: ${electronVarName}`));
|
|
|
|
const preloadPatchCode = `
|
|
(function loadInjectScript() {
|
|
const injectCode = ${JSON.stringify(injectCode)};
|
|
|
|
function injectViaWebFrame() {
|
|
try {
|
|
const { webFrame } = ${electronVarName};
|
|
if (webFrame && webFrame.executeJavaScript) {
|
|
webFrame.executeJavaScript(injectCode).then(() => console.log("[PRELOAD] Injected via webFrame.executeJavaScript")).catch(err => console.error("[PRELOAD] webFrame injection failed:", err));
|
|
return true;
|
|
}
|
|
} catch (e) {
|
|
console.error("[PRELOAD] webFrame not available:", e);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!injectViaWebFrame()) {
|
|
const tryInject = () => {
|
|
if (!injectViaWebFrame()) {
|
|
console.error("[PRELOAD] All injection methods failed");
|
|
}
|
|
};
|
|
|
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
|
tryInject();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', tryInject, { once: true });
|
|
}
|
|
}
|
|
})();
|
|
`;
|
|
const isPreloadPatched = preloadContent.includes("loadInjectScript");
|
|
|
|
if (isPreloadPatched) {
|
|
console.log(chalk.yellowBright('[!] Files already patched'));
|
|
const answer = await prompt("Do you want to repatch? (y/n): ");
|
|
|
|
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
|
|
console.log(chalk.blueBright('[+] Patching cancelled'));
|
|
rm(extractPath);
|
|
process.exit(0);
|
|
}
|
|
|
|
// Remove existing patches
|
|
console.log(chalk.yellowBright('[+] Replacing existing patches...'));
|
|
|
|
// Remove preload patch
|
|
const preloadPatchRegex = /\n?\(function loadInjectScript\(\) \{[\s\S]*?\}\)\(\);/;
|
|
preloadContent = preloadContent.replace(preloadPatchRegex, "");
|
|
}
|
|
|
|
console.log(chalk.yellowBright('[+] Applying preload patch...'));
|
|
const preloadLines = preloadContent.split("\n");
|
|
let preloadInsertIndex = -1;
|
|
|
|
for (let i = 0; i < preloadLines.length; i++) {
|
|
const line = preloadLines[i];
|
|
if (line.includes('require("electron")') || line.includes("require('electron')") || line.includes("electron_1")) {
|
|
preloadInsertIndex = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (preloadInsertIndex === -1) {
|
|
console.error(chalk.redBright(`[-] Could not find insertion point (electron import) in ${path.basename(preloadPath)}`));
|
|
rm(extractPath);
|
|
process.exit(1);
|
|
}
|
|
|
|
preloadLines.splice(preloadInsertIndex, 0, preloadPatchCode);
|
|
preloadContent = preloadLines.join("\n");
|
|
|
|
// Write patched preload file
|
|
fs.writeFileSync(preloadPath, preloadContent, "utf-8");
|
|
console.log(chalk.greenBright(`[+] ${path.basename(preloadPath)} patched successfully`));
|
|
|
|
console.log(chalk.yellowBright('[+] Repackaging app.asar...'));
|
|
await asar.createPackage(extractPath, asarPath);
|
|
console.log(chalk.greenBright('[+] app.asar repackaged successfully'));
|
|
|
|
let executablePath;
|
|
try {
|
|
executablePath = getExecutablePath(appPath);
|
|
} catch (e) {
|
|
rm(extractPath);
|
|
console.error(chalk.redBright(`[-] ${e.message}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.yellowBright('[+] Launching HTTP Toolkit to read integrity hashes...'));
|
|
let hashes;
|
|
try {
|
|
hashes = await captureIntegrityHashes(executablePath);
|
|
} catch (e) {
|
|
rm(extractPath);
|
|
console.error(chalk.redBright(`[-] Failed to capture integrity hashes: ${e.message}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.greenBright('[+] Integrity hashes captured'));
|
|
console.log(chalk.white(` Original hash: ${hashes.originalHash}`));
|
|
console.log(chalk.white(` New asar hash: ${hashes.newHash}`));
|
|
|
|
try {
|
|
if (isMac) {
|
|
patchInfoPlistHash(appPath, hashes.originalHash, hashes.newHash);
|
|
console.log(chalk.greenBright('[+] Patched Info.plist hash'));
|
|
} else {
|
|
const replacements = patchExecutableHash(executablePath, hashes.originalHash, hashes.newHash);
|
|
console.log(chalk.greenBright(`[+] Patched binary hash (${replacements} replacement${replacements === 1 ? "" : "s"})`));
|
|
}
|
|
} catch (e) {
|
|
rm(extractPath);
|
|
console.error(chalk.redBright(`[-] Failed to patch hash: ${e.message}`));
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(chalk.yellowBright('[+] Cleaning up temporary files...'));
|
|
rm(extractPath);
|
|
console.log(chalk.greenBright('[+] Successfully patched!'));
|
|
|
|
console.log(chalk.blueBright('[+] Opening HTTP Toolkit...'));
|
|
try {
|
|
const child = spawn(executablePath, {
|
|
stdio: "ignore",
|
|
shell: false,
|
|
detached: true,
|
|
});
|
|
child.unref();
|
|
console.log(chalk.greenBright('[+] HTTP Toolkit launched successfully'));
|
|
} catch (e) {
|
|
console.error(chalk.yellowBright(`[!] Could not auto-start HTTP Toolkit: ${e.message}`));
|
|
console.log(chalk.blueBright('[+] Please start HTTP Toolkit manually'));
|
|
}
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
const command = args[0];
|
|
|
|
const commandName = (() => {
|
|
// @ts-ignore - pkg adds this property at runtime
|
|
if (process.pkg) {
|
|
return path.basename(__filename);
|
|
} else {
|
|
return `node ${process.argv[1]}`;
|
|
}
|
|
})();
|
|
|
|
// Run the appropriate command
|
|
(async () => {
|
|
try {
|
|
// Check for updates from GitHub
|
|
await checkForUpdates();
|
|
|
|
if (command === "unpatch" || command === "restore") {
|
|
await unpatchApp();
|
|
} else if (command === "help" || command === "-h" || command === "--help") {
|
|
console.log(chalk.blueBright('HTTP Toolkit Patcher'));
|
|
console.log(chalk.white('\nUsage:'));
|
|
console.log(chalk.white(` ${commandName} [command]`));
|
|
console.log(chalk.white('\nCommands:'));
|
|
console.log(chalk.white(` patch ${chalk.gray('- Patch HTTP Toolkit (default)')}`));
|
|
console.log(chalk.white(` unpatch ${chalk.gray('- Restore original HTTP Toolkit from backup')}`));
|
|
console.log(chalk.white(` restore ${chalk.gray('- Alias for unpatch')}`));
|
|
console.log(chalk.white(` help ${chalk.gray('- Show this help message')}`));
|
|
process.exit(0);
|
|
} else if (!command || command === "patch") {
|
|
// Ask for confirmation before patching
|
|
const answer = await prompt("Do you want to patch HTTP Toolkit? [Y/n]: ");
|
|
if (answer.toLowerCase() === "n" || answer.toLowerCase() === "no") {
|
|
console.log(chalk.blueBright('[+] Patching cancelled'));
|
|
process.exit(0);
|
|
}
|
|
await patchApp();
|
|
} else {
|
|
console.error(chalk.redBright(`[-] Unknown command: ${command}`));
|
|
console.log(chalk.yellowBright('[!] Use \'help\' to see available commands'));
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
console.error(chalk.redBright(`[-] Error: ${error.message}`));
|
|
process.exit(1);
|
|
}
|
|
})();
|