commit 4171ec219638ad226c0b04662baa1eab66249c25 Author: ayyo42069 Date: Mon Jan 12 13:40:16 2026 +0100 Upload diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..c40bb53 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# HTTP Toolkit Pro Patcher + +A simple patcher to enable Pro features in HTTP Toolkit. + +**Updated by [kristof.best](https://kristof.best) ([@ayyo42069](https://github.com/ayyo42069))** +**Original by ([@XielQ](https://github.com/XielQs))** + +## Requirements + +- Node.js 15 or higher +- HTTP Toolkit installed + +## Usage + +```bash +# Install dependencies +npm install + +# Apply the patch +node . patch + +# Start HTTP Toolkit +node . start + +# Restore original (if needed) +node . restore +``` + +## How it works + +1. Extracts the app.asar from HTTP Toolkit +2. Injects a local proxy server that intercepts UI requests +3. Patches the subscription data on-the-fly to enable Pro features +4. Flips Electron fuses to bypass ASAR integrity checks +5. Repacks the app + +## Notes + +- Creates a backup at `app.asar.bak` before patching +- You can set a custom proxy with the `PROXY` environment variable +- Works offline after first run (caches UI files locally) + +## License + +MIT diff --git a/index.js b/index.js new file mode 100644 index 0000000..baa7e15 --- /dev/null +++ b/index.js @@ -0,0 +1,271 @@ +// @ts-check +import { spawn } from 'child_process' +import { flipFuses, FuseVersion, FuseV1Options } from '@electron/fuses' +import asar from '@electron/asar' +import prompts from 'prompts' +import yargs from 'yargs' +import chalk from 'chalk' +import path from 'path' +import fs from 'fs' +import os from 'os' + +const argv = await yargs(process.argv.slice(2)) + .usage(`Usage: ${process.argv0} . [options]`) + .command('patch', 'Patch HTTP Toolkit using the specified script') + .command('restore', 'Restore HTTP Toolkit files to their original state') + .command('start', 'Start HTTP Toolkit') + .demandCommand(1, 'You need at least one command before moving on') + .alias('h', 'help') + .parse() + +const isWin = process.platform === 'win32' +const isMac = process.platform === 'darwin' + +// Try to find where HTTP Toolkit is installed +const appPath = + isWin ? path.join(process.env.LOCALAPPDATA ?? '', 'Programs', 'HTTP Toolkit', 'resources') + : isMac ? '/Applications/HTTP Toolkit.app/Contents/Resources' + : fs.existsSync('/opt/HTTP Toolkit/resources') ? '/opt/HTTP Toolkit/resources' + : '/opt/httptoolkit/resources' + +const exePath = + isWin ? path.join(process.env.LOCALAPPDATA ?? '', 'Programs', 'HTTP Toolkit', 'HTTP Toolkit.exe') + : isMac ? '/Applications/HTTP Toolkit.app/Contents/MacOS/HTTP Toolkit' + : fs.existsSync('/opt/HTTP Toolkit/httptoolkit') ? '/opt/HTTP Toolkit/httptoolkit' + : '/opt/httptoolkit/httptoolkit' + +const isSudo = !isWin && (process.getuid || (() => process.env.SUDO_UID ? 0 : null))() === 0 + +if (+(process.versions.node.split('.')[0]) < 15) { + console.error(chalk.redBright`Node.js version 15 or higher is recommended, you are using version {bold ${process.versions.node}}`) +} + +if (!fs.existsSync(path.join(appPath, 'app.asar'))) { + console.error(chalk.redBright`Couldn't find HTTP Toolkit! Make sure it's installed.`) + process.exit(1) +} + +console.log(chalk.blueBright`Found HTTP Toolkit at {bold ${path.dirname(appPath)}}`) + +// Helper to recursively delete directories +const 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 }) + } +} + +/** @type {Array} */ +const activeProcesses = [] +let isCancelled = false + +const cleanUp = async () => { + isCancelled = true + console.log(chalk.redBright`Cancelled! Cleaning up...`) + if (activeProcesses.length) { + console.log(chalk.yellowBright`Stopping active processes...`) + for (const proc of activeProcesses) { + proc.kill('SIGINT') + console.log(chalk.yellowBright`Process {bold ${proc.pid ? process.pid + ' ' : ''}}stopped`) + } + await new Promise(resolve => setTimeout(resolve, 1000)) + } + const paths = [ + path.resolve(os.tmpdir(), 'httptoolkit-patch'), + path.resolve(appPath, 'app') + ] + try { + for (const p of paths) { + if (fs.existsSync(p)) { + console.log(chalk.yellowBright`Removing {bold ${p}}`) + rm(p) + } + } + } catch (e) { + console.error(chalk.redBright`Error while cleaning up`, e) + } + process.exit(1) +} + +const patchApp = async () => { + const filePath = path.join(appPath, 'app.asar') + const tempPath = path.join(appPath, 'app') + + if (fs.readFileSync(filePath).includes('Injected by HTTP Toolkit Patcher')) { + console.log(chalk.greenBright`App is already patched!`) + return + } + + const globalProxy = process.env.PROXY + + console.log(chalk.blueBright`Starting the patch process...`) + + if (globalProxy) { + if (!globalProxy.match(/^https?:/)) { + console.error(chalk.redBright`Global proxy must start with http:// or https://`) + process.exit(1) + } + console.log(chalk.yellowBright`Using custom proxy: {bold ${globalProxy}}`) + } + + console.log(chalk.yellowBright`Extracting app files...`) + + ;['SIGINT', 'SIGTERM'].forEach(signal => process.on(signal, cleanUp)) + + try { + rm(tempPath) + asar.extractAll(filePath, tempPath) + } catch (e) { + if (!isSudo && /** @type {any} */ (e).errno === -13) { // Permission denied + console.error(chalk.redBright`Permission denied${!isWin ? ', try running with sudo' : ', try running node as administrator'}`) + process.exit(1) + } + console.error(chalk.redBright`Error extracting app`, e) + process.exit(1) + } + + const indexPath = path.join(tempPath, 'build', 'index.js') + if (!fs.existsSync(indexPath)) { + console.error(chalk.redBright`Couldn't find index.js file`) + cleanUp() + } + const data = fs.readFileSync(indexPath, 'utf-8') + ;['SIGINT', 'SIGTERM'].forEach(signal => process.off(signal, cleanUp)) + + const { email } = await prompts({ + type: 'text', + name: 'email', + message: 'Enter an email for the pro plan', + validate: value => value.includes('@') || 'Invalid email' + }) + + if (!email || typeof email !== 'string') { + console.error(chalk.redBright`No email provided`) + cleanUp() + } + + ;['SIGINT', 'SIGTERM'].forEach(signal => process.on(signal, cleanUp)) + const patchContent = fs.readFileSync('patch.js', 'utf-8') + + // Separate imports from the rest of the patch code + const patchImportsMatch = patchContent.match(/\/\/ --- Patcher Imports[\s\S]*?\/\/ --- End Patcher Imports ---\n?/) + const patchImports = patchImportsMatch ? patchImportsMatch[0] : '' + const patchCode = patchContent.replace(patchImports, '') + + // Find where to put our imports (right after the last existing import) + const lastImportMatch = data.match(/^import .+$/gm) + const lastImport = lastImportMatch ? lastImportMatch[lastImportMatch.length - 1] : null + + let patchedData = data + + // Inject imports at the top + if (patchImports && lastImport) { + patchedData = patchedData.replace(lastImport, `${lastImport}\n// ------- Patcher Imports -------\n${patchImports.trim()}\n// ------- End Patcher Imports -------`) + } + + // Inject the main patch code where APP_URL is defined + patchedData = patchedData + .replace('const APP_URL =', `// ------- Injected by HTTP Toolkit Patcher -------\nconst email = \`${email.replace(/`/g, '\\`')}\`\nconst globalProxy = process.env.PROXY ?? \`${globalProxy ? globalProxy.replace(/`/g, '\\`') : ''}\`\n${patchCode}\n// ------- End patched content -------\nconst APP_URL =`) + + if (data === patchedData || !patchedData) { + console.error(chalk.redBright`Patch failed - couldn't find injection point`) + cleanUp() + } + + fs.writeFileSync(indexPath, patchedData, 'utf-8') + console.log(chalk.greenBright`Patched index.js successfully`) + console.log(chalk.yellowBright`Installing dependencies...`) + + try { + const proc = spawn('npm install express https-proxy-agent', { cwd: tempPath, stdio: 'inherit', shell: true }) + activeProcesses.push(proc) + await new Promise(resolve => + proc.on('close', resolve) + ) + activeProcesses.splice(activeProcesses.indexOf(proc), 1) + if (isCancelled) return + } catch (e) { + console.error(chalk.redBright`Error installing dependencies`, e) + cleanUp() + } + + rm(path.join(tempPath, 'package-lock.json')) + fs.copyFileSync(filePath, `${filePath}.bak`) + console.log(chalk.greenBright`Created backup at {bold ${filePath}.bak}`) + + console.log(chalk.yellowBright`Repacking the app...`) + await asar.createPackage(tempPath, filePath) + rm(tempPath) + console.log(chalk.greenBright`App patched!`) + + // Disable ASAR integrity checks so Electron doesn't complain + await flipElectronFuses() +} + +const flipElectronFuses = async () => { + console.log(chalk.yellowBright`Disabling ASAR integrity checks...`) + + if (!fs.existsSync(exePath)) { + console.log(chalk.yellowBright`Executable not found at ${exePath}, skipping fuse flipping`) + return + } + + try { + await flipFuses(exePath, { + version: FuseVersion.V1, + [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: false, + [FuseV1Options.OnlyLoadAppFromAsar]: false, + }) + console.log(chalk.greenBright`Integrity checks disabled successfully`) + } catch (e) { + console.error(chalk.yellowBright`Failed to flip fuses (might already be done or not supported): ${/** @type {Error} */ (e).message}`) + } +} + +switch (argv._[0]) { + case 'patch': + await patchApp() + break + case 'restore': + try { + console.log(chalk.blueBright`Restoring app...`) + if (!fs.existsSync(path.join(appPath, 'app.asar.bak'))) + console.error(chalk.redBright`Backup file not found - maybe the app wasn't patched?`) + else { + fs.copyFileSync(path.join(appPath, 'app.asar.bak'), path.join(appPath, 'app.asar')) + console.log(chalk.greenBright`App restored successfully`) + } + rm(path.join(os.tmpdir(), 'httptoolkit-patch')) + } catch (e) { + if (!isSudo && /** @type {any} */ (e).errno === -13) { // Permission denied + console.error(chalk.redBright`Permission denied${!isWin ? ', try running with sudo' : ', try running node as administrator'}`) + process.exit(1) + } + console.error(chalk.redBright`Error restoring app`, e) + process.exit(1) + } + break + case 'start': + console.log(chalk.blueBright`Starting HTTP Toolkit...`) + try { + const command = + isWin ? `"${path.resolve(appPath, '..', 'HTTP Toolkit.exe')}"` + : isMac ? 'open -a "HTTP Toolkit"' + : 'httptoolkit' + const proc = spawn(command, { stdio: 'inherit', shell: true }) + proc.on('close', code => process.exit(code)) + } catch (e) { + console.error(chalk.redBright`Error starting app`, e) + if (isSudo) console.error(chalk.redBright`Try running without sudo`) + process.exit(1) + } + break + default: + console.error(chalk.redBright`Unknown command`) + process.exit(1) +} + +if (!isCancelled) console.log(chalk.greenBright`All done!`) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..467dba4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,514 @@ +{ + "name": "httptoolkit-pro-patcher", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "httptoolkit-pro-patcher", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.9", + "@electron/fuses": "^1.8.0", + "chalk": "4", + "prompts": "^2.4.2", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/node": "^20.14.9", + "@types/prompts": "^2.4.9", + "@types/yargs": "^17.0.32" + } + }, + "node_modules/@electron/asar": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "license": "MIT", + "dependencies": { + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@types/node": { + "version": "20.19.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.28.tgz", + "integrity": "sha512-VyKBr25BuFDzBFCK5sUM6ZXiWfqgCTwTAOK8qzGV/m9FCirXYDlmczJ+d5dXBAQALGCdRRdbteKYfJ84NGEusw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prompts": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", + "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "kleur": "^3.0.3" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..47e39df --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "httptoolkit-patcher", + "version": "1.0.0", + "description": "Patches HTTP Tool Kit.", + "main": "index.js", + "type": "module", + "scripts": { + "start": "node . patch" + }, + "author": "ayyo42069", + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.2.9", + "@electron/fuses": "^1.8.0", + "chalk": "4", + "prompts": "^2.4.2", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@types/node": "^20.14.9", + "@types/prompts": "^2.4.9", + "@types/yargs": "^17.0.32" + } +} \ No newline at end of file diff --git a/patch.js b/patch.js new file mode 100644 index 0000000..29e685e --- /dev/null +++ b/patch.js @@ -0,0 +1,179 @@ +// Patcher imports - need these for ESM compatibility +import { createRequire as __patcherCreateRequire } from 'module'; +import { fileURLToPath as __patcherFileURLToPath } from 'url'; +import { dirname as __patcherDirname, join as __patcherJoin } from 'path'; + +// Load modules using require (works better with the app's setup) +const __patcherRequire = __patcherCreateRequire(import.meta.url); +const __patcherHttpsProxyAgent = __patcherRequire('https-proxy-agent').HttpsProxyAgent; +const __patcherExpress = __patcherRequire('express'); +const __patcherHttps = __patcherRequire('https'); +const __patcherFs = __patcherRequire('fs'); +const __patcherOs = __patcherRequire('os'); +const __patcherPath = __patcherRequire('path'); + +// Simple HTTP request helper with redirect support +const __patcherRequest = (method, url, redirectCount = 0) => new Promise((resolve, reject) => { + const agent = globalProxy ? new __patcherHttpsProxyAgent(globalProxy.startsWith('http') ? globalProxy.replace(/^http:/, 'https:') : 'https://' + globalProxy) : undefined; + + const req = __patcherHttps.request(url, { method, agent }, res => { + let data = Buffer.alloc(0); + res.on('data', chunk => data = Buffer.concat([data, chunk])); + res.on('end', () => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + if (redirectCount >= 5) return reject(new Error('Too many redirects')); + return resolve(__patcherRequest(method, res.headers.location, redirectCount + 1)); + } + resolve({ data, statusCode: res.statusCode, headers: res.headers }); + }); + }); + req.on('error', reject); + req.end(); +}); + +// Quick check if we can reach the HTTP Toolkit servers +const __patcherHasInternet = () => __patcherRequest('HEAD', 'https://app.httptoolkit.tech') + .then(r => r.statusCode >= 200 && r.statusCode < 400) + .catch(() => false); + +const __patcherPort = process.env.PATCHER_PORT || 5067; +const __patcherTempPath = __patcherPath.join(__patcherOs.tmpdir(), 'httptoolkit-patch'); + +// Point the app to our local server instead of the real one +process.env.APP_URL = `http://localhost:${__patcherPort}`; +console.log(`[Patcher] Temp path: ${__patcherTempPath}`); + +const __patcherApp = __patcherExpress(); +__patcherApp.disable('x-powered-by'); + +// Catch all requests and proxy them through our server +__patcherApp.all(/(.*)/, async (req, res) => { + console.log(`[Patcher] ${req.url}`); + + let filePath = __patcherPath.join(__patcherTempPath, new URL(req.url, process.env.APP_URL).pathname === '/' ? 'index.html' : new URL(req.url, process.env.APP_URL).pathname); + + // Some routes need .html extension + if (['/view', '/intercept', '/settings', '/mock'].includes(new URL(req.url, process.env.APP_URL).pathname)) { + filePath += '.html'; + } + + // Block service worker - it causes caching headaches + if (new URL(req.url, process.env.APP_URL).pathname === '/ui-update-worker.js') { + return res.status(404).send('Not found'); + } + + if (!__patcherFs.existsSync(__patcherTempPath)) { + __patcherFs.mkdirSync(__patcherTempPath); + } + + // Offline mode - serve from cache if no internet + if (!(await __patcherHasInternet())) { + if (__patcherFs.existsSync(filePath)) { + return res.sendFile(filePath); + } + return res.status(404).send('No internet and file not cached'); + } + + try { + // Check if we have a cached copy that's still fresh + if (__patcherFs.existsSync(filePath)) { + try { + const remoteDate = await __patcherRequest('HEAD', `https://app.httptoolkit.tech${req.url}`).then(r => new Date(r.headers['last-modified'])); + if (remoteDate < new Date(__patcherFs.statSync(filePath).mtime)) { + return res.sendFile(filePath); + } + } catch (e) { /* ignore, just redownload */ } + } + + // Download from the real server + const remoteFile = await __patcherRequest('GET', `https://app.httptoolkit.tech${req.url}`); + for (const [key, value] of Object.entries(remoteFile.headers)) res.setHeader(key, value); + + // Create directories if needed + const mkdirp = dir => { + if (!__patcherFs.existsSync(dir)) { + mkdirp(__patcherPath.dirname(dir)); + __patcherFs.mkdirSync(dir); + } + }; + mkdirp(__patcherPath.dirname(filePath)); + + let data = remoteFile.data; + + // This is where the magic happens - patch main.js to inject pro subscription + if (new URL(req.url, process.env.APP_URL).pathname === '/main.js') { + console.log(`[Patcher] Patching main.js...`); + res.setHeader('Cache-Control', 'no-store'); + + data = data.toString(); + + // Find the account store class and module names (they're minified so we gotta search for them) + const accStoreName = data.match(/class ([0-9A-Za-z_]+){constructor\(e\){this\.goToSettings=e/)?.[1]; + const modName = data.match(/([0-9A-Za-z_]+).(getLatestUserData|getLastUserData)/)?.[1]; + + if (!accStoreName) console.error(`[Patcher] Couldn't find account store class`); + else if (!modName) console.error(`[Patcher] Couldn't find user data module`); + else { + // Override the user data functions to return our fake pro user + let patched = data.replace( + `class ${accStoreName}{`, + `["getLatestUserData","getLastUserData"].forEach(p=>Object.defineProperty(${modName},p,{value:()=>user}));class ${accStoreName}{` + ); + + if (patched === data) { + console.error(`[Patcher] Patch failed - couldn't find injection point`); + } else { + // Inject our pro user at the top + patched = `const user=${JSON.stringify({ + email, + subscription: { + status: 'active', + expiry: new Date('6767-06-07').toISOString(), + sku: 'pro-annual', + }, + userId: 'patcher', + banned: false, + featureFlags: [], + teamSubscription: null, + })};user.subscription.expiry=new Date(user.subscription.expiry);` + patched; + + data = patched; + console.log(`[Patcher] main.js patched successfully!`); + } + } + } + + __patcherFs.writeFileSync(filePath, data); + res.sendFile(filePath); + } catch (e) { + console.error(`[Patcher] Error: ${e.message}`); + res.status(500).send('Something went wrong'); + } +}); + +__patcherApp.listen(__patcherPort, () => console.log(`[Patcher] Running on port ${__patcherPort}`)); + +// Fix CORS so the app can talk to our local server +app.on('ready', () => { + session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { + // Block telemetry + const blocked = ['events.httptoolkit.tech']; + try { + const host = new URL(details.url).hostname; + if (blocked.includes(host) || details.url?.includes('sentry')) { + return callback({ cancel: true }); + } + } catch (e) { } + + details.requestHeaders.Origin = 'https://app.httptoolkit.tech'; + callback({ requestHeaders: details.requestHeaders }); + }); + + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + if (details.responseHeaders) { + details.responseHeaders['Access-Control-Allow-Origin'] = [`http://localhost:${__patcherPort}`]; + delete details.responseHeaders['access-control-allow-origin']; + } + callback({ responseHeaders: details.responseHeaders }); + }); +}); \ No newline at end of file