add dynamic resource url patcher as standalone responsemodifier
This commit is contained in:
325
proxychain/responsemodifers/patch_dynamic_resource_urls.js
Normal file
325
proxychain/responsemodifers/patch_dynamic_resource_urls.js
Normal file
@@ -0,0 +1,325 @@
|
||||
// Overrides the global fetch and XMLHttpRequest open methods to modify the request URLs.
|
||||
// Also overrides the attribute setter prototype to modify the request URLs
|
||||
// fetch("/relative_script.js") -> fetch("http://localhost:8080/relative_script.js")
|
||||
(() => {
|
||||
|
||||
// ============== PARAMS ===========================
|
||||
// if the original request was: http://localhost:8080/http://proxiedsite.com/foo/bar
|
||||
// proxyOrigin is http://localhost:8080
|
||||
const proxyOrigin = "{{PROXY_ORIGIN}}";
|
||||
//const proxyOrigin = globalThis.window.location.origin;
|
||||
|
||||
// if the original request was: http://localhost:8080/http://proxiedsite.com/foo/bar
|
||||
// origin is http://proxiedsite.com
|
||||
const origin = "{{ORIGIN}}";
|
||||
//const origin = (new URL(decodeURIComponent(globalThis.window.location.pathname.substring(1)))).origin
|
||||
// ============== END PARAMS ======================
|
||||
|
||||
const blacklistedSchemes = [
|
||||
"ftp:",
|
||||
"mailto:",
|
||||
"tel:",
|
||||
"file:",
|
||||
"blob:",
|
||||
"javascript:",
|
||||
"about:",
|
||||
"magnet:",
|
||||
"ws:",
|
||||
"wss:",
|
||||
];
|
||||
|
||||
function rewriteURL(url) {
|
||||
const oldUrl = url
|
||||
if (!url) return url
|
||||
let isStr = (typeof url.startsWith === 'function')
|
||||
if (!isStr) return url
|
||||
|
||||
// don't rewrite special URIs
|
||||
if (blacklistedSchemes.includes(url)) return url;
|
||||
|
||||
// don't rewrite invalid URIs
|
||||
try { new URL(url, origin) } catch { return url }
|
||||
|
||||
// don't double rewrite
|
||||
if (url.startsWith(proxyOrigin)) return url;
|
||||
if (url.startsWith(`/${proxyOrigin}`)) return url;
|
||||
if (url.startsWith(`/${origin}`)) return url;
|
||||
if (url.startsWith(`/http://`)) return url;
|
||||
if (url.startsWith(`/https://`)) return url;
|
||||
if (url.startsWith(`/http%3A%2F%2F`)) return url;
|
||||
if (url.startsWith(`/https%3A%2F%2F`)) return url;
|
||||
if (url.startsWith(`/%2Fhttp`)) return url;
|
||||
|
||||
//console.log(`proxychain: origin: ${origin} // proxyOrigin: ${proxyOrigin} // original: ${oldUrl}`)
|
||||
|
||||
if (url.startsWith("//")) {
|
||||
url = `/${origin}/${encodeURIComponent(url.substring(2))}`;
|
||||
} else if (url.startsWith("/")) {
|
||||
url = `/${origin}/${encodeURIComponent(url.substring(1))}`;
|
||||
} else if (url.startsWith(origin)) {
|
||||
url = `/${encodeURIComponent(url)}`
|
||||
} else if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
url = `/${proxyOrigin}/${encodeURIComponent(url)}`;
|
||||
}
|
||||
console.log(`proxychain: rewrite JS URL: ${oldUrl} -> ${url}`)
|
||||
return url;
|
||||
};
|
||||
|
||||
// sometimes anti-bot protections like cloudflare or akamai bot manager check if JS is hooked
|
||||
function hideMonkeyPatch(objectOrName, method, originalToString) {
|
||||
let obj;
|
||||
let isGlobalFunction = false;
|
||||
|
||||
if (typeof objectOrName === 'string') {
|
||||
obj = globalThis[objectOrName];
|
||||
isGlobalFunction = (typeof obj === 'function') && (method === objectOrName);
|
||||
} else {
|
||||
obj = objectOrName;
|
||||
}
|
||||
|
||||
if (isGlobalFunction) {
|
||||
const originalFunction = obj;
|
||||
globalThis[objectOrName] = function(...args) {
|
||||
return originalFunction.apply(this, args);
|
||||
};
|
||||
globalThis[objectOrName].toString = () => originalToString;
|
||||
} else if (obj && typeof obj[method] === 'function') {
|
||||
const originalMethod = obj[method];
|
||||
obj[method] = function(...args) {
|
||||
return originalMethod.apply(this, args);
|
||||
};
|
||||
obj[method].toString = () => originalToString;
|
||||
} else {
|
||||
console.warn(`proxychain: cannot hide monkey patch: ${method} is not a function on the provided object.`);
|
||||
}
|
||||
}
|
||||
|
||||
// monkey patch fetch
|
||||
const oldFetch = fetch;
|
||||
fetch = async (url, init) => {
|
||||
return oldFetch(rewriteURL(url), init)
|
||||
}
|
||||
hideMonkeyPatch('fetch', 'fetch', 'function fetch() { [native code] }')
|
||||
|
||||
// monkey patch xmlhttprequest
|
||||
const oldOpen = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function(method, url, async = true, user = null, password = null) {
|
||||
return oldOpen.call(this, method, rewriteURL(url), async, user, password);
|
||||
};
|
||||
hideMonkeyPatch(XMLHttpRequest.prototype, 'open', 'function(){if("function"==typeof eo)return eo.apply(this,arguments)}');
|
||||
|
||||
const oldSend = XMLHttpRequest.prototype.send;
|
||||
XMLHttpRequest.prototype.send = function(method, url) {
|
||||
return oldSend.call(this, method, rewriteURL(url));
|
||||
};
|
||||
hideMonkeyPatch(XMLHttpRequest.prototype, 'send', 'function(){if("function"==typeof eo)return eo.apply(this,arguments)}');
|
||||
|
||||
|
||||
// monkey patch service worker registration
|
||||
const oldRegister = ServiceWorkerContainer.prototype.register;
|
||||
ServiceWorkerContainer.prototype.register = function(scriptURL, options) {
|
||||
return oldRegister.call(this, rewriteURL(scriptURL), options)
|
||||
}
|
||||
hideMonkeyPatch(ServiceWorkerContainer.prototype, 'register', 'function register() { [native code] }')
|
||||
|
||||
// monkey patch URL.toString() method
|
||||
const oldToString = URL.prototype.toString
|
||||
URL.prototype.toString = function() {
|
||||
let originalURL = oldToString.call(this)
|
||||
return rewriteURL(originalURL)
|
||||
}
|
||||
hideMonkeyPatch(URL.prototype, 'toString', 'function toString() { [native code] }')
|
||||
|
||||
// monkey patch URL.toJSON() method
|
||||
const oldToJson = URL.prototype.toString
|
||||
URL.prototype.toString = function() {
|
||||
let originalURL = oldToJson.call(this)
|
||||
return rewriteURL(originalURL)
|
||||
}
|
||||
hideMonkeyPatch(URL.prototype, 'toString', 'function toJSON() { [native code] }')
|
||||
|
||||
// Monkey patch URL.href getter and setter
|
||||
const originalHrefDescriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'href');
|
||||
Object.defineProperty(URL.prototype, 'href', {
|
||||
get: function() {
|
||||
let originalHref = originalHrefDescriptor.get.call(this);
|
||||
return rewriteURL(originalHref)
|
||||
},
|
||||
set: function(newValue) {
|
||||
originalHrefDescriptor.set.call(this, rewriteURL(newValue));
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: do one more pass of this by manually traversing the DOM
|
||||
// AFTER all the JS and page has loaded just in case
|
||||
|
||||
// Monkey patch setter
|
||||
const elements = [
|
||||
{ tag: 'a', attribute: 'href' },
|
||||
{ tag: 'img', attribute: 'src' },
|
||||
// { tag: 'img', attribute: 'srcset' }, // TODO: handle srcset
|
||||
{ tag: 'script', attribute: 'src' },
|
||||
{ tag: 'link', attribute: 'href' },
|
||||
{ tag: 'link', attribute: 'icon' },
|
||||
{ tag: 'iframe', attribute: 'src' },
|
||||
{ tag: 'audio', attribute: 'src' },
|
||||
{ tag: 'video', attribute: 'src' },
|
||||
{ tag: 'source', attribute: 'src' },
|
||||
// { tag: 'source', attribute: 'srcset' }, // TODO: handle srcset
|
||||
{ tag: 'embed', attribute: 'src' },
|
||||
{ tag: 'embed', attribute: 'pluginspage' },
|
||||
{ tag: 'html', attribute: 'manifest' },
|
||||
{ tag: 'object', attribute: 'src' },
|
||||
{ tag: 'input', attribute: 'src' },
|
||||
{ tag: 'track', attribute: 'src' },
|
||||
{ tag: 'form', attribute: 'action' },
|
||||
{ tag: 'area', attribute: 'href' },
|
||||
{ tag: 'base', attribute: 'href' },
|
||||
{ tag: 'blockquote', attribute: 'cite' },
|
||||
{ tag: 'del', attribute: 'cite' },
|
||||
{ tag: 'ins', attribute: 'cite' },
|
||||
{ tag: 'q', attribute: 'cite' },
|
||||
{ tag: 'button', attribute: 'formaction' },
|
||||
{ tag: 'input', attribute: 'formaction' },
|
||||
{ tag: 'meta', attribute: 'content' },
|
||||
{ tag: 'object', attribute: 'data' },
|
||||
];
|
||||
|
||||
elements.forEach(({ tag, attribute }) => {
|
||||
const proto = document.createElement(tag).constructor.prototype;
|
||||
const descriptor = Object.getOwnPropertyDescriptor(proto, attribute);
|
||||
if (descriptor && descriptor.set) {
|
||||
Object.defineProperty(proto, attribute, {
|
||||
...descriptor,
|
||||
set(value) {
|
||||
// calling rewriteURL will end up calling a setter for href,
|
||||
// leading to a recusive loop and a Maximum call stack size exceeded
|
||||
// error, so we guard against this with a local semaphore flag
|
||||
const isRewritingSetKey = Symbol.for('isRewritingSet');
|
||||
if (!this[isRewritingSetKey]) {
|
||||
this[isRewritingSetKey] = true;
|
||||
descriptor.set.call(this, rewriteURL(value));
|
||||
//descriptor.set.call(this, value);
|
||||
this[isRewritingSetKey] = false;
|
||||
} else {
|
||||
// Directly set the value without rewriting
|
||||
descriptor.set.call(this, value);
|
||||
}
|
||||
},
|
||||
get() {
|
||||
const isRewritingGetKey = Symbol.for('isRewritingGet');
|
||||
if (!this[isRewritingGetKey]) {
|
||||
this[isRewritingGetKey] = true;
|
||||
let oldURL = descriptor.get.call(this);
|
||||
let newURL = rewriteURL(oldURL);
|
||||
this[isRewritingGetKey] = false;
|
||||
return newURL
|
||||
} else {
|
||||
return descriptor.get.call(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// sometimes, libraries will set the Element.innerHTML or Element.outerHTML directly with a string instead of setters.
|
||||
// in this case, we intercept it, create a fake DOM, parse it and then rewrite all attributes that could
|
||||
// contain a URL. Then we return the replacement innerHTML/outerHTML with redirected links.
|
||||
function rewriteInnerHTML(html, elements) {
|
||||
const isRewritingHTMLKey = Symbol.for('isRewritingHTML');
|
||||
|
||||
// Check if already processing
|
||||
if (document[isRewritingHTMLKey]) {
|
||||
return html;
|
||||
}
|
||||
|
||||
const tempContainer = document.createElement('div');
|
||||
document[isRewritingHTMLKey] = true;
|
||||
|
||||
try {
|
||||
tempContainer.innerHTML = html;
|
||||
|
||||
// Create a map for quick lookup
|
||||
const elementsMap = new Map(elements.map(e => [e.tag, e.attribute]));
|
||||
|
||||
// Loop-based DOM traversal
|
||||
const nodes = [...tempContainer.querySelectorAll('*')];
|
||||
for (const node of nodes) {
|
||||
const attribute = elementsMap.get(node.tagName.toLowerCase());
|
||||
if (attribute && node.hasAttribute(attribute)) {
|
||||
const originalUrl = node.getAttribute(attribute);
|
||||
const rewrittenUrl = rewriteURL(originalUrl);
|
||||
node.setAttribute(attribute, rewrittenUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return tempContainer.innerHTML;
|
||||
} finally {
|
||||
// Clear the flag
|
||||
document[isRewritingHTMLKey] = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Store original setters
|
||||
const originalSetters = {};
|
||||
|
||||
['innerHTML', 'outerHTML'].forEach(property => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Element.prototype, property);
|
||||
if (descriptor && descriptor.set) {
|
||||
originalSetters[property] = descriptor.set;
|
||||
|
||||
Object.defineProperty(Element.prototype, property, {
|
||||
...descriptor,
|
||||
set(value) {
|
||||
const isRewritingHTMLKey = Symbol.for('isRewritingHTML');
|
||||
if (!this[isRewritingHTMLKey]) {
|
||||
this[isRewritingHTMLKey] = true;
|
||||
try {
|
||||
// Use custom logic
|
||||
descriptor.set.call(this, rewriteInnerHTML(value, elements));
|
||||
} finally {
|
||||
this[isRewritingHTMLKey] = false;
|
||||
}
|
||||
} else {
|
||||
// Use original setter in recursive call
|
||||
originalSetters[property].call(this, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
})();
|
||||
|
||||
|
||||
|
||||
(() => {
|
||||
document.addEventListener('DOMContentLoaded', (event) => {
|
||||
initIdleMutationObserver();
|
||||
});
|
||||
|
||||
function initIdleMutationObserver() {
|
||||
let debounceTimer;
|
||||
const debounceDelay = 500; // adjust the delay as needed
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
// Clear the previous timer and set a new one
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
execute();
|
||||
observer.disconnect(); // Disconnect after first execution
|
||||
}, debounceDelay);
|
||||
});
|
||||
|
||||
const config = { attributes: false, childList: true, subtree: true };
|
||||
observer.observe(document.body, config);
|
||||
}
|
||||
|
||||
function execute() {
|
||||
console.log('DOM is now idle. Executing...');
|
||||
}
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user