13 Commits
Author SHA1 Message Date
xenos 359b707b44 Update for 1.24.2 2025-12-18 17:38:56 +01:00
xenosandGitHub 7537b33593 Merge pull request #6 from jokelbaf/master 2025-12-11 21:37:37 +01:00
JokelBafandGitHub fe97c803f5 feat: Support newer electron import name 2025-12-11 21:31:42 +02:00
xenos 61b3bdeb5e chore(version): update version to 2.0.8 and add author information in package.json; remove unused version comparison documentation in index.js
Signed-off-by: xenos <66328734+xenos1337@users.noreply.github.com>
2025-11-29 00:08:02 +01:00
xenos 91333c1a3e refactor(version): read LOCAL_VERSION from package.json dynamically 2025-11-29 00:06:55 +01:00
xenos d1a64774da feat(version): add automatic update check from GitHub tags 2025-11-29 00:06:24 +01:00
xenos fe7cae9dd9 refactor(patcher): use local inject.js and simplify injection mechanism 2025-11-29 00:04:54 +01:00
xenos cb8fd663b7 docs(readme): remove 'Can They Fix It' section 2025-11-29 00:04:09 +01:00
xenos 30ec13fbdc Refactor index.js to use function declarations instead of arrow functions for consistency and readability. Update README.md to clarify installation and usage instructions, removing the build section. Remove GitHub Actions release workflow as it is no longer needed.
Signed-off-by: xenos <66328734+xenos1337@users.noreply.github.com>
2025-11-24 05:47:59 +01:00
xenosandGitHub f139d9c2ba Merge pull request #4 from USLTD/patch-1 2025-11-24 05:06:23 +01:00
Luka MamukashviliandGitHub cde93879a2 Rename preload.js to preload.cjs in index.js 2025-11-21 14:24:32 +04:00
xenosandGitHub a991b56c01 Merge pull request #1 from xenos1337/fix/linux-compatibility 2025-11-12 19:06:25 +01:00
Zebratic 76decebe22 Fix Linux compatibility for permission elevation and HTTP Toolkit launching
- Show sudo instructions instead of auto-elevating on Linux
- Show manual launch instructions instead of auto-launching HTTP Toolkit on Linux
- Windows and macOS behavior unchanged
2025-11-12 19:01:34 +01:00
6 changed files with 560 additions and 2469 deletions
-120
View File
@@ -1,120 +0,0 @@
name: Release Build
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
build:
name: Build ${{ matrix.os }}
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include:
- os: linux
runner: ubuntu-latest
targets: node18-linux-x64,node18-linux-arm64
- os: macos
runner: macos-latest
targets: node18-macos-x64,node18-macos-arm64
- os: windows
runner: windows-latest
targets: node18-win-x64
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Create dist directory
run: mkdir -p dist
- name: Build for ${{ matrix.os }}
run: |
npm run build
npx pkg dist/bundle.cjs --targets ${{ matrix.targets }} --compress GZip --out-path dist/
- name: List built files (debug - Unix)
if: matrix.os != 'windows'
run: ls -lah dist/
- name: List built files (debug - Windows)
if: matrix.os == 'windows'
shell: pwsh
run: Get-ChildItem -Path dist/ | Format-Table -AutoSize
- name: Rename executables (Linux)
if: matrix.os == 'linux'
run: |
[ -f dist/bundle-x64 ] && mv dist/bundle-x64 dist/httptoolkit-patcher-linux-x64
[ -f dist/bundle-arm64 ] && mv dist/bundle-arm64 dist/httptoolkit-patcher-linux-arm64
rm -f dist/bundle dist/bundle.cjs 2>/dev/null || true
- name: Rename executables (macOS)
if: matrix.os == 'macos'
run: |
[ -f dist/bundle-x64 ] && mv dist/bundle-x64 dist/httptoolkit-patcher-macos-x64
[ -f dist/bundle-arm64 ] && mv dist/bundle-arm64 dist/httptoolkit-patcher-macos-arm64
rm -f dist/bundle dist/bundle.cjs 2>/dev/null || true
- name: Rename executables (Windows)
if: matrix.os == 'windows'
shell: pwsh
run: |
if (Test-Path dist/bundle.exe) { Move-Item -Force dist/bundle.exe dist/httptoolkit-patcher.exe }
if (Test-Path dist/bundle-x64.exe) { Move-Item -Force dist/bundle-x64.exe dist/httptoolkit-patcher-win-x64.exe }
Remove-Item -Force dist/bundle.cjs -ErrorAction SilentlyContinue
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: binaries-${{ matrix.os }}
path: dist/*
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
pattern: binaries-*
path: artifacts
merge-multiple: true
- name: Prepare release files
run: |
mkdir -p release
cp artifacts/* release/ 2>/dev/null || find artifacts -type f -exec cp {} release/ \;
ls -lah release/
- name: Extract version from tag
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: release/*
draft: false
prerelease: false
generate_release_notes: true
name: Release ${{ steps.get_version.outputs.VERSION }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+8 -67
View File
@@ -20,70 +20,32 @@ The patcher intercepts HTTP Toolkit's authentication functions:
By hooking these functions, we bypass the subscription checks entirely.
### Can They Fix It?
Yes, but they most likely won't. Fixing this would require changing their entire codebase architecture. And if they do? I'll just update the patcher.
## Installation
1. Install dependencies:
1. Install Node.js (if not already installed)
2. Install dependencies:
```bash
npm install
```
## Building
Build standalone executables for all platforms and architectures:
```bash
npm run build
```
This creates self-contained executables in the `dist/` directory:
**Windows:**
- `httptoolkit-patcher-win-x64.exe` (64-bit)
**Linux:**
- `httptoolkit-patcher-linux-x64` (x86_64/AMD64)
- `httptoolkit-patcher-linux-arm64` (ARM64/AArch64)
**macOS:**
- `httptoolkit-patcher-macos-x64` (Intel)
- `httptoolkit-patcher-macos-arm64` (Apple Silicon M Chip)
**Note:** These are standalone executables created with `pkg` that include Node.js runtime and all dependencies. No need to install Node.js separately!
## Usage
### From Source
**Patch:**
**Patch HTTP Toolkit:**
```bash
npm start
```
**Unpatch:**
**Unpatch/Restore:**
```bash
npm run unpatch
```
### Using Prebuilt Executables
Download the appropriate executable for your platform from [Releases](https://github.com/xenos1337/httptoolkit-patcher/releases), then:
**Windows:**
```cmd
httptoolkit-patcher-win-x64.exe
```
**Linux/macOS:**
**Show help:**
```bash
chmod +x httptoolkit-patcher-linux-x64 # or your architecture
./httptoolkit-patcher-linux-x64
npm start help
```
That's it. The patcher handles everything automatically.
That's it. The patcher handles everything automatically and will request elevated permissions if needed.
## Technical Details
@@ -96,7 +58,7 @@ That's it. The patcher handles everything automatically.
## Troubleshooting
**Permission errors?** Run as admin/sudo or let the patcher request elevation.
**Permission errors?** The patcher will automatically request elevated permissions (admin/sudo).
**Already patched?** The patcher will ask if you want to repatch.
@@ -104,27 +66,6 @@ That's it. The patcher handles everything automatically.
**Anything else?** Open an issue on the [GitHub repository](https://github.com/xenos1337/httptoolkit-patcher/issues).
## GitHub Release Workflow
This project includes an automated GitHub Actions workflow that builds and releases the patcher for all platforms.
### Creating a Release
1. Create and push a version tag:
```bash
git tag v2.0.1
git push origin v2.0.1
```
2. The GitHub Actions workflow will automatically:
- Build for all platforms and architectures (Windows x64, Linux x64/ARM64, macOS Intel/Apple Silicon)
- Create standalone native executables using pkg
- Create a GitHub release with all binaries as downloadable artifacts
### Manual Workflow Trigger
You can also manually trigger the release workflow from the GitHub Actions tab.
## Disclaimer
This tool is provided as-is. Use at your own risk. For educational purposes only.
+395 -113
View File
@@ -15,19 +15,28 @@ 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;
}
// Check if import.meta.url exists (ESM)
if (typeof import.meta !== 'undefined' && import.meta.url) {
return fileURLToPath(import.meta.url);
// 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";
}
// Fallback to __filename if in CommonJS
return typeof __filename !== 'undefined' ? __filename : process.argv[1];
})();
// Check if running with elevated privileges
const isElevated = () => {
function isElevated() {
if (isWin) {
try {
execSync("net session", { stdio: "ignore" });
@@ -38,10 +47,10 @@ const isElevated = () => {
} else {
return process.getuid && process.getuid() === 0;
}
};
}
// Helper function to prompt user for input
const prompt = question => {
function prompt(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
@@ -52,23 +61,83 @@ const prompt = question => {
resolve(answer);
});
});
};
}
// Helper function to fetch content from URL
const fetchUrl = url => {
/**
* 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, res => {
.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", () => resolve(data));
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
const rm = dirPath => {
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)) {
@@ -77,11 +146,15 @@ const rm = dirPath => {
else fs.rmSync(entryPath, { force: true });
}
fs.rmdirSync(dirPath);
};
}
// Find HTTP Toolkit installation path
const findAppPath = async () => {
const possiblePaths = isWin ? [path.join("C:", "Program Files", "HTTP Toolkit", "resources"), path.join("C:", "Program Files (x86)", "HTTP Toolkit", "resources")] : isMac ? ["/Applications/HTTP Toolkit.app/Contents/Resources"] : ["/opt/HTTP Toolkit/resources", "/opt/httptoolkit/resources"];
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"))) {
@@ -109,20 +182,27 @@ const findAppPath = async () => {
}
return resourcesPath;
};
}
// Request elevated permissions
const requestElevation = async () => {
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
const script = `Start-Process -FilePath "node" -ArgumentList '"${__filename}"' -Verb RunAs`;
let script;
if (isBundled) {
script = `Start-Process -FilePath "${__filename}" -Verb RunAs`;
} else {
script = `Start-Process -FilePath "node" -ArgumentList "${__filename}" -Verb RunAs`;
}
try {
spawn("powershell", ["-Command", script], {
stdio: "inherit",
shell: true,
});
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) {
@@ -130,12 +210,30 @@ const requestElevation = async () => {
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 {
const child = spawn("sudo", ["node", __filename], {
stdio: "inherit",
});
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}`);
@@ -143,20 +241,34 @@ const requestElevation = async () => {
process.exit(1);
}
}
};
}
// Check if we have write permissions
const checkPermissions = filePath => {
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
const killHttpToolkit = async () => {
async function killHttpToolkit() {
console.log(chalk.yellowBright`[+] Checking for running HTTP Toolkit processes...`);
try {
@@ -199,20 +311,132 @@ const killHttpToolkit = async () => {
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;
}
// Unpatch/restore function
const unpatchApp = async () => {
async function unpatchApp() {
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");
@@ -229,23 +453,15 @@ const unpatchApp = async () => {
}
console.log(chalk.yellowBright`[!] Administrator/sudo privileges required for unpatching`);
const answer = await prompt("Do you want to request elevated permissions? [Y/n]: ");
if (!answer || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
await requestElevation();
} else {
console.log(chalk.redBright`[-] Cannot proceed without write permissions`);
process.exit(1);
}
await requestElevation();
}
// 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);
@@ -255,14 +471,12 @@ const unpatchApp = async () => {
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 });
@@ -270,20 +484,17 @@ const unpatchApp = async () => {
}
console.log(chalk.greenBright`[+] Successfully unpatched!`);
};
}
// Main patching function
const patchApp = async () => {
async function patchApp() {
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
@@ -298,16 +509,9 @@ const patchApp = async () => {
}
console.log(chalk.yellowBright`[!] Administrator/sudo privileges required for patching`);
const answer = await prompt("Do you want to request elevated permissions? [Y/n]: ");
if (!answer || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
await requestElevation();
} else {
console.log(chalk.redBright`[-] Cannot proceed without write permissions`);
process.exit(1);
}
await requestElevation();
}
// Step 4: Backup app.asar
const backupPath = path.join(appPath, "app.asar.bak");
if (!fs.existsSync(backupPath)) {
console.log(chalk.yellowBright`[+] Creating backup...`);
@@ -315,37 +519,77 @@ const patchApp = async () => {
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.js exists
const preloadPath = path.join(extractPath, "build", "preload.js");
const preloadPath = path.join(extractPath, "build", "preload.cjs");
if (!fs.existsSync(preloadPath)) {
console.error(chalk.redBright`[-] preload.js not found at ${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`);
// Step 7: Fetch inject.js from GitHub
console.log(chalk.yellowBright`[+] Fetching inject code from GitHub...`);
const injectCode = await fetchUrl("https://raw.githubusercontent.com/xenos1337/httptoolkit-patcher/refs/heads/master/inject.js");
if (!injectCode || !injectCode.includes("injectPageContextHooks")) {
console.error(chalk.redBright`[-] Failed to fetch inject.js from GitHub`);
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);
}
console.log(chalk.greenBright`[+] Inject code fetched successfully`);
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`);
// Step 8: Read preload.js and check if already patched
let preloadContent = fs.readFileSync(preloadPath, "utf-8");
const isPatched = preloadContent.includes("injectPageContextHooks");
if (isPatched) {
console.log(chalk.yellowBright`[!] File already patched`);
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") {
@@ -354,74 +598,112 @@ const patchApp = async () => {
process.exit(0);
}
// Replace the existing injectPageContextHooks function
console.log(chalk.yellowBright`[+] Replacing existing patch...`);
const functionRegex = /\(function injectPageContextHooks\(\) \{[\s\S]*?\}\)\(\);/;
preloadContent = preloadContent.replace(functionRegex, injectCode);
} else {
// Find line with electron_1 and insert inject code below it
console.log(chalk.yellowBright`[+] Applying patch...`);
const lines = preloadContent.split("\n");
let insertIndex = -1;
// Remove existing patches
console.log(chalk.yellowBright`[+] Replacing existing patches...`);
for (let i = 0; i < lines.length; i++) {
if (lines[i].includes("electron_1")) {
insertIndex = i + 1;
break;
}
}
if (insertIndex === -1) {
console.error(chalk.redBright`[-] Could not find insertion point (electron_1) in preload.js`);
rm(extractPath);
process.exit(1);
}
lines.splice(insertIndex, 0, injectCode);
preloadContent = lines.join("\n");
// Remove preload patch
const preloadPatchRegex = /\n?\(function loadInjectScript\(\) \{[\s\S]*?\}\)\(\);/;
preloadContent = preloadContent.replace(preloadPatchRegex, "");
}
// Step 9: Write patched preload.js
fs.writeFileSync(preloadPath, preloadContent, "utf-8");
console.log(chalk.greenBright`[+] preload.js patched successfully`);
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`);
// 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
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 {
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 binary hash: ${e.message}`);
process.exit(1);
}
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 {
const command = isWin ? `"${path.resolve(appPath, "..", "HTTP Toolkit.exe")}"` : isMac ? 'open -a "HTTP Toolkit"' : "httptoolkit";
const child = spawn(command, {
const child = spawn(executablePath, {
stdio: "ignore",
shell: true,
shell: false,
detached: true,
});
child.unref(); // Completely detach the child process
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`);
}
};
}
// Parse command line arguments
const args = process.argv.slice(2);
const command = args[0];
// Get the command name for display
const commandName = process.pkg ? path.basename(__filename) : 'node index.js';
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") {
+145 -183
View File
@@ -1,206 +1,168 @@
(function injectPageContextHooks() {
const injectionCode = `
(function() {
console.log("[PAGE-INJECT] Installing hooks in page context");
(function () {
console.log("[PAGE-INJECT] Installing hooks in page context");
const propertyHooks = {
isPaidUser: true,
isLoggedIn: true,
userHasSubscription: true,
userEmail: "hi@httptoolkit.com",
mightBePaidUser: true,
isPastDueUser: false,
userSubscription: {
status: "active",
plan: "pro",
sku: "sku",
tierCode: "pro",
interval: "monthly",
quantity: 1,
expiry: new Date("2999-01-01"),
updateBillingDetailsUrl: "https://httptoolkit.com/",
cancelSubscriptionUrl: "https://httptoolkit.com/",
lastReceiptUrl: "https://httptoolkit.com/",
canManageSubscription: true
const propertyHooks = {
isPaidUser: true,
isLoggedIn: true,
userHasSubscription: true,
userEmail: "hi@httptoolkit.com",
mightBePaidUser: true,
isPastDueUser: false,
isStatusUnexpired: true,
userSubscription: {
state: "fulfilled",
status: "active",
plan: "pro",
sku: "sku",
tierCode: "pro",
interval: "monthly",
quantity: 1,
expiry: new Date(new Date().setFullYear(new Date().getFullYear() + 10)),
updateBillingDetailsUrl: "https://httptoolkit.com/",
cancelSubscriptionUrl: "https://httptoolkit.com/",
lastReceiptUrl: "https://httptoolkit.com/",
canManageSubscription: true,
},
};
const hookedObjects = new WeakSet();
// Override Object.defineProperty to intercept all property definitions
const originalDefineProperty = Object.defineProperty;
Object.defineProperty = function (target, prop, descriptor) {
// Intercept our target properties
if (prop in propertyHooks) {
console.log("[PAGE-INJECT] Intercepting defineProperty for: " + prop);
if (descriptor && descriptor.get) {
const originalGetter = descriptor.get;
descriptor.get = function () {
const originalValue = originalGetter.call(this);
console.log("[PAGE-INJECT] " + prop + " getter called, original=" + originalValue + ", returning=" + JSON.stringify(propertyHooks[prop]));
return propertyHooks[prop];
};
} else if (descriptor && descriptor.value !== undefined) {
console.log("[PAGE-INJECT] " + prop + " value being defined, overriding to " + JSON.stringify(propertyHooks[prop]));
descriptor.value = propertyHooks[prop];
}
}
return originalDefineProperty.call(this, target, prop, descriptor);
};
// Hook Object.defineProperties too
const originalDefineProperties = Object.defineProperties;
Object.defineProperties = function (target, props) {
for (let prop in props) {
if (prop in propertyHooks) {
console.log("[PAGE-INJECT] Intercepting defineProperties for: " + prop);
if (props[prop].get) {
const originalGetter = props[prop].get;
props[prop].get = function () {
const originalValue = originalGetter.call(this);
console.log("[PAGE-INJECT] " + prop + " getter called, original=" + originalValue + ", returning=" + JSON.stringify(propertyHooks[prop]));
return propertyHooks[prop];
};
} else if (props[prop].value !== undefined) {
props[prop].value = propertyHooks[prop];
}
};
}
}
return originalDefineProperties.call(this, target, props);
};
const hookedObjects = new WeakSet();
// Periodically scan and patch existing objects
function scanAndPatch() {
// Search through window and common store locations
const searchPaths = [window, window.accountStore, window.stores && window.stores.accountStore, window.appState && window.appState.accountStore];
// Override Object.defineProperty to intercept all property definitions
const originalDefineProperty = Object.defineProperty;
Object.defineProperty = function(target, prop, descriptor) {
// Intercept our target properties
if (prop in propertyHooks) {
console.log("[PAGE-INJECT] Intercepting defineProperty for: " + prop);
if (descriptor && descriptor.get) {
const originalGetter = descriptor.get;
descriptor.get = function() {
const originalValue = originalGetter.call(this);
console.log("[PAGE-INJECT] " + prop + " getter called, original=" + originalValue + ", returning=" + JSON.stringify(propertyHooks[prop]));
return propertyHooks[prop];
};
} else if (descriptor && descriptor.value !== undefined) {
console.log("[PAGE-INJECT] " + prop + " value being defined, overriding to " + JSON.stringify(propertyHooks[prop]));
descriptor.value = propertyHooks[prop];
}
}
return originalDefineProperty.call(this, target, prop, descriptor);
};
// Hook Object.defineProperties too
const originalDefineProperties = Object.defineProperties;
Object.defineProperties = function(target, props) {
for (let prop in props) {
if (prop in propertyHooks) {
console.log("[PAGE-INJECT] Intercepting defineProperties for: " + prop);
if (props[prop].get) {
const originalGetter = props[prop].get;
props[prop].get = function() {
const originalValue = originalGetter.call(this);
console.log("[PAGE-INJECT] " + prop + " getter called, original=" + originalValue + ", returning=" + JSON.stringify(propertyHooks[prop]));
return propertyHooks[prop];
};
} else if (props[prop].value !== undefined) {
props[prop].value = propertyHooks[prop];
}
}
}
return originalDefineProperties.call(this, target, props);
};
// Periodically scan and patch existing objects
function scanAndPatch() {
// Search through window and common store locations
const searchPaths = [
window,
window.accountStore,
window.stores && window.stores.accountStore,
window.appState && window.appState.accountStore,
];
searchPaths.forEach((obj, idx) => {
if (!obj || hookedObjects.has(obj)) return;
searchPaths.forEach((obj, idx) => {
if (!obj || hookedObjects.has(obj)) return;
try {
Object.keys(propertyHooks).forEach(prop => {
try {
Object.keys(propertyHooks).forEach(prop => {
try {
const desc = Object.getOwnPropertyDescriptor(obj, prop);
if (desc && desc.configurable) {
console.log("[PAGE-INJECT] Found " + prop + " on object #" + idx + ", patching...");
const desc = Object.getOwnPropertyDescriptor(obj, prop);
if (desc && desc.configurable) {
console.log("[PAGE-INJECT] Found " + prop + " on object #" + idx + ", patching...");
if (desc.get) {
if (desc.get) {
const originalGetter = desc.get;
Object.defineProperty(obj, prop, {
get: function () {
const originalValue = originalGetter.call(this);
console.log("[PAGE-INJECT] " + prop + " getter intercepted, original=" + originalValue + ", returning=" + JSON.stringify(propertyHooks[prop]));
return propertyHooks[prop];
},
set: desc.set,
configurable: true,
enumerable: desc.enumerable,
});
} else if (desc.writable) {
obj[prop] = propertyHooks[prop];
console.log("[PAGE-INJECT] " + prop + " value set to " + JSON.stringify(propertyHooks[prop]));
}
}
} catch (e) {
// Ignore individual property errors
}
});
hookedObjects.add(obj);
} catch (e) {
// Ignore object access errors
}
});
// Also try to find accountStore by scanning window properties
try {
for (let key in window) {
try {
const obj = window[key];
if (obj && typeof obj === "object" && "accountStore" in obj) {
console.log("[PAGE-INJECT] Found accountStore in window." + key);
const store = obj.accountStore;
if (store && !hookedObjects.has(store)) {
Object.keys(propertyHooks).forEach(prop => {
try {
const desc = Object.getOwnPropertyDescriptor(store, prop);
if (desc && desc.configurable && desc.get) {
const originalGetter = desc.get;
Object.defineProperty(obj, prop, {
get: function() {
Object.defineProperty(store, prop, {
get: function () {
const originalValue = originalGetter.call(this);
console.log("[PAGE-INJECT] " + prop + " getter intercepted, original=" + originalValue + ", returning=" + JSON.stringify(propertyHooks[prop]));
console.log("[PAGE-INJECT] accountStore." + prop + " intercepted, original=" + originalValue + ", returning=" + JSON.stringify(propertyHooks[prop]));
return propertyHooks[prop];
},
set: desc.set,
configurable: true,
enumerable: desc.enumerable
enumerable: desc.enumerable,
});
} else if (desc.writable) {
obj[prop] = propertyHooks[prop];
console.log("[PAGE-INJECT] " + prop + " value set to " + JSON.stringify(propertyHooks[prop]));
}
}
} catch (e) {
// Ignore individual property errors
}
});
hookedObjects.add(obj);
} catch (e) {
// Ignore object access errors
}
});
// Also try to find accountStore by scanning window properties
try {
for (let key in window) {
try {
const obj = window[key];
if (obj && typeof obj === 'object' && 'accountStore' in obj) {
console.log("[PAGE-INJECT] Found accountStore in window." + key);
const store = obj.accountStore;
if (store && !hookedObjects.has(store)) {
Object.keys(propertyHooks).forEach(prop => {
try {
const desc = Object.getOwnPropertyDescriptor(store, prop);
if (desc && desc.configurable && desc.get) {
const originalGetter = desc.get;
Object.defineProperty(store, prop, {
get: function() {
const originalValue = originalGetter.call(this);
console.log("[PAGE-INJECT] accountStore." + prop + " intercepted, original=" + originalValue + ", returning=" + JSON.stringify(propertyHooks[prop]));
return propertyHooks[prop];
},
set: desc.set,
configurable: true,
enumerable: desc.enumerable
});
}
} catch (e) {}
});
hookedObjects.add(store);
}
}
} catch (e) {}
} catch (e) {}
});
hookedObjects.add(store);
}
}
} catch (e) {}
}
// Run initial scan
scanAndPatch();
// Scan periodically for late-initialized stores
let scanCount = 0;
const scanInterval = setInterval(() => {
scanCount++;
scanAndPatch();
if (scanCount >= 10) {
clearInterval(scanInterval);
console.log("[PAGE-INJECT] Stopped periodic scanning after 10 attempts");
}
}, 100);
console.log("[PAGE-INJECT] Hooks installed successfully");
})();
`;
function injectScript() {
try {
const script = document.createElement('script');
script.textContent = injectionCode;
(document.head || document.documentElement).appendChild(script);
script.remove(); // Clean up the script tag
console.log("[PRELOAD] Injected page context hooks");
} catch (e) {
console.error("[PRELOAD] Failed to inject hooks:", e);
}
} catch (e) {}
}
// Try to inject immediately if DOM is ready, otherwise wait
if (document.documentElement) {
injectScript();
} else {
// Wait for document to be created
const observer = new MutationObserver(() => {
if (document.documentElement) {
observer.disconnect();
injectScript();
}
});
observer.observe(document, { childList: true, subtree: true });
// Run initial scan
scanAndPatch();
// Also try on DOMContentLoaded as backup
if (document.addEventListener) {
document.addEventListener('DOMContentLoaded', injectScript, { once: true });
// Scan periodically for late-initialized stores
let scanCount = 0;
const scanInterval = setInterval(() => {
scanCount++;
scanAndPatch();
if (scanCount >= 50) {
clearInterval(scanInterval);
console.log("[PAGE-INJECT] Stopped periodic scanning after 10 attempts");
}
}
}, 100);
console.log("[PAGE-INJECT] Hooks installed successfully");
})();
+3 -1965
View File
File diff suppressed because it is too large Load Diff
+4 -16
View File
@@ -1,26 +1,16 @@
{
"name": "httptoolkit-patcher",
"version": "2.0.5",
"version": "2.0.10",
"description": "HTTP Toolkit Pro Patcher",
"main": "index.js",
"type": "module",
"bin": "index.js",
"author": "xenos1337",
"scripts": {
"start": "node index.js",
"patch": "node index.js patch",
"unpatch": "node index.js unpatch",
"restore": "node index.js restore",
"build": "esbuild index.js --bundle --platform=node --format=cjs --target=node18 --outfile=dist/bundle.cjs --external:@electron/asar --external:chalk"
},
"pkg": {
"targets": [
"node18-win-x64",
"node18-linux-x64",
"node18-linux-arm64",
"node18-macos-x64",
"node18-macos-arm64"
],
"outputPath": "dist"
"restore": "node index.js restore"
},
"license": "MIT",
"dependencies": {
@@ -28,8 +18,6 @@
"chalk": "4"
},
"devDependencies": {
"@types/node": "^20.14.9",
"esbuild": "^0.25.11",
"pkg": "^5.8.1"
"@types/node": "^20.14.9"
}
}