Files
HTTPToolkit-Patcher-2026/index.js
T

288 lines
10 KiB
JavaScript
Raw Normal View History

2026-01-12 13:40:16 +01:00
// @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} . <command> [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'
2026-01-21 03:33:00 +01:00
const userAgent = process.env.npm_config_user_agent
const pm = userAgent ? userAgent.split('/')[0] : 'npm'
const installCmd = pm === 'npm' ? 'install' : 'add'
2026-01-12 13:40:16 +01:00
// 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<import('child_process').ChildProcess>} */
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)
}
2026-02-19 00:06:37 +01:00
// Remove @prisma/instrumentation - it has a broken nested node_modules reference
// to @opentelemetry/instrumentation that causes a crash after repacking.
// It's only telemetry and not needed for the app to function.
const prismaInstrPath = path.join(tempPath, 'node_modules', '@prisma', 'instrumentation')
if (fs.existsSync(prismaInstrPath)) {
rm(prismaInstrPath)
console.log(chalk.yellowBright`Removed broken @prisma/instrumentation package`)
}
2026-01-12 13:40:16 +01:00
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 {
2026-01-21 03:33:00 +01:00
const proc = spawn(`${pm} ${installCmd} express https-proxy-agent`, { cwd: tempPath, stdio: 'inherit', shell: true })
2026-01-12 13:40:16 +01:00
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'))
2026-01-21 03:33:00 +01:00
rm(path.join(tempPath, 'yarn.lock'))
rm(path.join(tempPath, 'pnpm-lock.yaml'))
rm(path.join(tempPath, 'bun.lockb'))
2026-01-12 13:40:16 +01:00
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!`)