Files
httptoolkit-patcher/index.js
T

608 lines
21 KiB
JavaScript
Raw Normal View History

2025-10-23 20:31:50 +02:00
// @ts-check
import { spawn, execSync } from "child_process";
import 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";
// Version info
const LOCAL_VERSION = "2.0.8";
const GITHUB_REPO = "xenos1337/httptoolkit-patcher";
2025-10-23 20:31:50 +02:00
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
2025-10-23 20:31:50 +02:00
if (process.pkg) {
return process.execPath;
}
// ESM: use import.meta.url
return fileURLToPath(import.meta.url);
2025-10-23 20:31:50 +02:00
})();
// Check if running with elevated privileges
function isElevated() {
2025-10-23 20:31:50 +02:00
if (isWin) {
try {
execSync("net session", { stdio: "ignore" });
return true;
} catch {
return false;
}
} else {
return process.getuid && process.getuid() === 0;
}
}
2025-10-23 20:31:50 +02:00
// Helper function to prompt user for input
function prompt(question) {
2025-10-23 20:31:50 +02:00
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer);
});
});
}
2025-10-23 20:31:50 +02:00
/**
* 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);
});
}
/**
* Compare two semver version strings
* @param {string} v1
* @param {string} v2
* @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
*/
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.)
}
}
2025-10-23 20:31:50 +02:00
// Helper function to remove directory recursively
function rm(dirPath) {
2025-10-23 20:31:50 +02:00
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);
}
2025-10-23 20:31:50 +02:00
// 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"];
2025-10-23 20:31:50 +02:00
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;
}
2025-10-23 20:31:50 +02:00
// Request elevated permissions
async function requestElevation() {
2025-10-23 20:31:50 +02:00
console.log(chalk.yellowBright`[!] Requesting elevated permissions...`);
// @ts-ignore - pkg adds this property at runtime
const isBundled = process.pkg;
2025-10-23 20:31:50 +02:00
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`;
}
2025-10-23 20:31:50 +02:00
try {
console.log(chalk.greenBright`[+] Spawning PowerShell with script: ${script}`);
execSync(`powershell -Command "${script}"`, { stdio: "inherit" });
2025-10-23 20:31:50 +02:00
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);
2025-10-23 20:31:50 +02:00
} else {
// macOS: Try to elevate with sudo
2025-10-23 20:31:50 +02:00
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",
});
}
2025-10-23 20:31:50 +02:00
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);
}
}
}
2025-10-23 20:31:50 +02:00
// Check if we have write permissions
function checkPermissions(filePath) {
2025-10-23 20:31:50 +02:00
try {
// Check write access to the file/directory
2025-10-23 20:31:50 +02:00
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}`);
2025-10-23 20:31:50 +02:00
return true;
} catch (e) {
console.error(chalk.redBright`[-] Permissions check failed for ${filePath}: ${e.message}`);
2025-10-23 20:31:50 +02:00
return false;
}
}
2025-10-23 20:31:50 +02:00
// Kill HTTP Toolkit processes
async function killHttpToolkit() {
2025-10-23 20:31:50 +02:00
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`);
}
}
2025-10-23 20:31:50 +02:00
// Unpatch/restore function
async function unpatchApp() {
2025-10-23 20:31:50 +02:00
console.log(chalk.blueBright`[+] HTTP Toolkit Unpatcher Started`);
// Step 1: Find app path
const appPath = await findAppPath();
console.log(chalk.greenBright`[+] HTTP Toolkit found at ${appPath}`);
// Step 2: Kill HTTP Toolkit if running
await killHttpToolkit();
// Step 3: Check permissions
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();
2025-10-23 20:31:50 +02:00
}
// Step 4: Check if backup exists
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);
}
// Step 5: Restore from backup
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);
}
// Step 6: Clean up extracted files if they exist
if (fs.existsSync(extractPath)) {
console.log(chalk.yellowBright`[+] Removing extracted files...`);
rm(extractPath);
console.log(chalk.greenBright`[+] Cleaned up extracted files`);
}
// Step 7: Optionally remove backup
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!`);
}
2025-10-23 20:31:50 +02:00
// Main patching function
async function patchApp() {
2025-10-23 20:31:50 +02:00
console.log(chalk.blueBright`[+] HTTP Toolkit Patcher Started`);
// Step 1: Find app path
const appPath = await findAppPath();
console.log(chalk.greenBright`[+] HTTP Toolkit found at ${appPath}`);
// Step 2: Kill HTTP Toolkit if running
await killHttpToolkit();
// Step 3: Check permissions
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();
2025-10-23 20:31:50 +02:00
}
// Step 4: Backup app.asar
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}`);
}
// Step 5: Extract app.asar
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}`);
// Step 6: Check if preload.cjs exists
const preloadPath = path.join(extractPath, "build", "preload.cjs");
2025-10-23 20:31:50 +02:00
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);
2025-10-23 20:31:50 +02:00
}
console.log(chalk.greenBright`[+] Found preload.cjs`);
2025-10-23 20:31:50 +02:00
// Step 7: Read inject.js from local file (same directory as this script)
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}`);
2025-10-23 20:31:50 +02:00
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`);
const preloadPatchCode = `
(function loadInjectScript() {
const injectCode = ${JSON.stringify(injectCode)};
function injectViaWebFrame() {
try {
const { webFrame } = electron_1;
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 });
}
}
})();
`;
2025-10-23 20:31:50 +02:00
// Step 8: Read files and check if already patched
2025-10-23 20:31:50 +02:00
let preloadContent = fs.readFileSync(preloadPath, "utf-8");
const isPreloadPatched = preloadContent.includes("loadInjectScript");
2025-10-23 20:31:50 +02:00
if (isPreloadPatched) {
console.log(chalk.yellowBright`[!] Files already patched`);
2025-10-23 20:31:50 +02:00
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, "");
}
2025-10-23 20:31:50 +02:00
// Step 9: Patch preload file - find line with electron_1 and insert patch code below it
console.log(chalk.yellowBright`[+] Applying preload patch...`);
const preloadLines = preloadContent.split("\n");
let preloadInsertIndex = -1;
for (let i = 0; i < preloadLines.length; i++) {
if (preloadLines[i].includes("electron_1")) {
preloadInsertIndex = i + 1;
break;
2025-10-23 20:31:50 +02:00
}
}
2025-10-23 20:31:50 +02:00
if (preloadInsertIndex === -1) {
console.error(chalk.redBright`[-] Could not find insertion point (electron_1) in ${path.basename(preloadPath)}`);
rm(extractPath);
process.exit(1);
2025-10-23 20:31:50 +02:00
}
preloadLines.splice(preloadInsertIndex, 0, preloadPatchCode);
preloadContent = preloadLines.join("\n");
// Write patched preload file
2025-10-23 20:31:50 +02:00
fs.writeFileSync(preloadPath, preloadContent, "utf-8");
console.log(chalk.greenBright`[+] ${path.basename(preloadPath)} patched successfully`);
2025-10-23 20:31:50 +02:00
2025-10-23 20:31:50 +02:00
// Step 10: Repackage app.asar
console.log(chalk.yellowBright`[+] Repackaging app.asar...`);
await asar.createPackage(extractPath, asarPath);
console.log(chalk.greenBright`[+] app.asar repackaged successfully`);
// Step 11: Clean up
console.log(chalk.yellowBright`[+] Cleaning up temporary files...`);
rm(extractPath);
console.log(chalk.greenBright`[+] Successfully patched!`);
// Step 12: Open HTTP Toolkit as detached process
console.log(chalk.blueBright`[+] Opening HTTP Toolkit...`);
try {
if (isLinux) {
// Linux: Cannot auto-launch reliably, show manual instructions
console.log(chalk.yellowBright`[!] HTTP Toolkit has been successfully patched`);
console.log(chalk.yellowBright`[!] Please manually launch HTTP Toolkit from your applications menu or using the command:`);
console.log(chalk.blueBright` httptoolkit`);
} else {
const command = isWin ? `"${path.resolve(appPath, "..", "HTTP Toolkit.exe")}"` : isMac ? 'open -a "HTTP Toolkit"' : "httptoolkit";
const child = spawn(command, {
stdio: "ignore",
shell: true,
detached: true,
});
child.unref();
console.log(chalk.greenBright`[+] HTTP Toolkit launched successfully`);
}
2025-10-23 20:31:50 +02:00
} catch (e) {
console.error(chalk.yellowBright`[!] Could not auto-start HTTP Toolkit: ${e.message}`);
console.log(chalk.blueBright`[+] Please start HTTP Toolkit manually`);
}
}
2025-10-23 20:31:50 +02:00
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]}`;
}
})();
2025-10-23 20:31:50 +02:00
// Run the appropriate command
(async () => {
try {
// Check for updates from GitHub
await checkForUpdates();
2025-10-23 20:31:50 +02:00
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);
}
})();