refactor(patcher): use local inject.js and simplify injection mechanism

This commit is contained in:
xenos
2025-11-29 00:04:54 +01:00
parent cb8fd663b7
commit fe7cae9dd9
2 changed files with 238 additions and 252 deletions
+87 -63
View File
@@ -51,19 +51,6 @@ function prompt(question) {
}); });
} }
// Helper function to fetch content from URL
function fetchUrl(url) {
return new Promise((resolve, reject) => {
https
.get(url, res => {
let data = "";
res.on("data", chunk => (data += chunk));
res.on("end", () => resolve(data));
})
.on("error", reject);
});
}
// Helper function to remove directory recursively // Helper function to remove directory recursively
function rm(dirPath) { function rm(dirPath) {
if (!fs.existsSync(dirPath)) return; if (!fs.existsSync(dirPath)) return;
@@ -78,7 +65,7 @@ function rm(dirPath) {
// Find HTTP Toolkit installation path // Find HTTP Toolkit installation path
async function findAppPath() { async function findAppPath() {
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"]; 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) { for (const p of possiblePaths) {
if (fs.existsSync(path.join(p, "app.asar"))) { if (fs.existsSync(path.join(p, "app.asar"))) {
@@ -346,37 +333,71 @@ async function patchApp() {
asar.extractAll(asarPath, extractPath); asar.extractAll(asarPath, extractPath);
console.log(chalk.greenBright`[+] Extracted to ${extractPath}`); console.log(chalk.greenBright`[+] Extracted to ${extractPath}`);
// Step 6: Check if preload.cjs or preload.js exists // Step 6: Check if preload.cjs exists
let preloadPath = path.join(extractPath, "build", "preload.cjs"); const preloadPath = path.join(extractPath, "build", "preload.cjs");
if (!fs.existsSync(preloadPath)) { if (!fs.existsSync(preloadPath)) {
console.log(chalk.yellowBright`[!] preload.cjs not found, checking for preload.js...`); console.error(chalk.redBright`[-] preload.cjs not found in ${path.join(extractPath, "build")}`);
preloadPath = path.join(extractPath, "build", "preload.js"); console.error(chalk.yellowBright`[!] Please download the latest version of HTTP Toolkit from https://httptoolkit.com/`);
if (!fs.existsSync(preloadPath)) {
console.error(chalk.redBright`[-] Neither preload.cjs nor preload.js found in ${path.join(extractPath, "build")}`);
rm(extractPath);
process.exit(1);
}
console.log(chalk.greenBright`[+] Found preload.js`);
} else {
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`);
rm(extractPath); rm(extractPath);
process.exit(1); process.exit(1);
} }
console.log(chalk.greenBright`[+] Inject code fetched successfully`); console.log(chalk.greenBright`[+] Found preload.cjs`);
// Step 8: Read preload file and check if already patched // 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}`);
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 });
}
}
})();
`;
// Step 8: Read files and check if already patched
let preloadContent = fs.readFileSync(preloadPath, "utf-8"); let preloadContent = fs.readFileSync(preloadPath, "utf-8");
const isPatched = preloadContent.includes("injectPageContextHooks"); const isPreloadPatched = preloadContent.includes("loadInjectScript");
if (isPatched) { if (isPreloadPatched) {
console.log(chalk.yellowBright`[!] File already patched`); console.log(chalk.yellowBright`[!] Files already patched`);
const answer = await prompt("Do you want to repatch? (y/n): "); const answer = await prompt("Do you want to repatch? (y/n): ");
if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") { if (answer.toLowerCase() !== "y" && answer.toLowerCase() !== "yes") {
@@ -385,37 +406,40 @@ async function patchApp() {
process.exit(0); process.exit(0);
} }
// Replace the existing injectPageContextHooks function // Remove existing patches
console.log(chalk.yellowBright`[+] Replacing existing patch...`); console.log(chalk.yellowBright`[+] Replacing existing patches...`);
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;
for (let i = 0; i < lines.length; i++) { // Remove preload patch
if (lines[i].includes("electron_1")) { const preloadPatchRegex = /\n?\(function loadInjectScript\(\) \{[\s\S]*?\}\)\(\);/;
insertIndex = i + 1; preloadContent = preloadContent.replace(preloadPatchRegex, "");
break;
}
}
if (insertIndex === -1) {
console.error(chalk.redBright`[-] Could not find insertion point (electron_1) in ${path.basename(preloadPath)}`);
rm(extractPath);
process.exit(1);
}
lines.splice(insertIndex, 0, injectCode);
preloadContent = lines.join("\n");
} }
// Step 9: Write patched preload file // 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;
}
}
if (preloadInsertIndex === -1) {
console.error(chalk.redBright`[-] Could not find insertion point (electron_1) 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"); fs.writeFileSync(preloadPath, preloadContent, "utf-8");
console.log(chalk.greenBright`[+] ${path.basename(preloadPath)} patched successfully`); console.log(chalk.greenBright`[+] ${path.basename(preloadPath)} patched successfully`);
// Step 10: Repackage app.asar // Step 10: Repackage app.asar
console.log(chalk.yellowBright`[+] Repackaging app.asar...`); console.log(chalk.yellowBright`[+] Repackaging app.asar...`);
await asar.createPackage(extractPath, asarPath); await asar.createPackage(extractPath, asarPath);
+145 -183
View File
@@ -1,206 +1,168 @@
(function injectPageContextHooks() { (function () {
const injectionCode = ` console.log("[PAGE-INJECT] Installing hooks in page context");
(function() {
console.log("[PAGE-INJECT] Installing hooks in page context");
const propertyHooks = { const propertyHooks = {
isPaidUser: true, isPaidUser: true,
isLoggedIn: true, isLoggedIn: true,
userHasSubscription: true, userHasSubscription: true,
userEmail: "hi@httptoolkit.com", userEmail: "hi@httptoolkit.com",
mightBePaidUser: true, mightBePaidUser: true,
isPastDueUser: false, isPastDueUser: false,
userSubscription: { isStatusUnexpired: true,
status: "active", userSubscription: {
plan: "pro", state: "fulfilled",
sku: "sku", status: "active",
tierCode: "pro", plan: "pro",
interval: "monthly", sku: "sku",
quantity: 1, tierCode: "pro",
expiry: new Date("2999-01-01"), interval: "monthly",
updateBillingDetailsUrl: "https://httptoolkit.com/", quantity: 1,
cancelSubscriptionUrl: "https://httptoolkit.com/", expiry: new Date(new Date().setFullYear(new Date().getFullYear() + 10)),
lastReceiptUrl: "https://httptoolkit.com/", updateBillingDetailsUrl: "https://httptoolkit.com/",
canManageSubscription: true 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 searchPaths.forEach((obj, idx) => {
const originalDefineProperty = Object.defineProperty; if (!obj || hookedObjects.has(obj)) return;
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;
try {
Object.keys(propertyHooks).forEach(prop => {
try { try {
Object.keys(propertyHooks).forEach(prop => { const desc = Object.getOwnPropertyDescriptor(obj, prop);
try { if (desc && desc.configurable) {
const desc = Object.getOwnPropertyDescriptor(obj, prop); console.log("[PAGE-INJECT] Found " + prop + " on object #" + idx + ", patching...");
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; const originalGetter = desc.get;
Object.defineProperty(obj, prop, { Object.defineProperty(store, prop, {
get: function() { get: function () {
const originalValue = originalGetter.call(this); 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]; return propertyHooks[prop];
}, },
set: desc.set, set: desc.set,
configurable: true, 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) {}
} catch (e) { });
// Ignore individual property errors hookedObjects.add(store);
} }
});
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) {} } catch (e) {}
} }
} 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);
}
} }
// Try to inject immediately if DOM is ready, otherwise wait // Run initial scan
if (document.documentElement) { scanAndPatch();
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 });
// Also try on DOMContentLoaded as backup // Scan periodically for late-initialized stores
if (document.addEventListener) { let scanCount = 0;
document.addEventListener('DOMContentLoaded', injectScript, { once: true }); 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");
})(); })();