diff --git a/handlers/proxy.go b/handlers/proxy.go index 84e046d..d781085 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -33,7 +33,7 @@ func NewProxySiteHandler(opts *ProxyOptions) fiber.Handler { SetRequestModifications( // rx.SpoofJA3fingerprint(ja3, "Googlebot"), // rx.MasqueradeAsFacebookBot(), - //rx.MasqueradeAsGoogleBot(), + rx.MasqueradeAsGoogleBot(), rx.DeleteOutgoingCookies(), rx.ForwardRequestHeaders(), //rx.SpoofReferrerFromGoogleSearch(), @@ -42,6 +42,7 @@ func NewProxySiteHandler(opts *ProxyOptions) fiber.Handler { // rx.RequestArchiveIs(), ). AddResponseModifications( + tx.InjectScriptBeforeDOMContentLoaded(`(() => {let d = document.createElement("div"); d.id = "adb-check"; document.body.append(d) })()`), tx.ForwardResponseHeaders(), tx.BypassCORS(), tx.BypassContentSecurityPolicy(), diff --git a/proxychain/responsemodifers/patch_dynamic_resource_urls.js b/proxychain/responsemodifers/patch_dynamic_resource_urls.js index ee08f75..3e04400 100644 --- a/proxychain/responsemodifers/patch_dynamic_resource_urls.js +++ b/proxychain/responsemodifers/patch_dynamic_resource_urls.js @@ -2,95 +2,98 @@ // 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) { - if (!url) return url; - - // fetch url might be string, url, or request object - // handle all three by downcasting to string - const isStr = typeof url === "string"; - if (!isStr) { - x = String(url); - if (x == "[object Request]") { - url = url.url; - } else { - url = String(url); - } - } + // ============== 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; - const oldUrl = url; + // 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 ====================== - // don't rewrite special URIs - if (blacklistedSchemes.includes(url)) return url; + const blacklistedSchemes = [ + "ftp:", + "mailto:", + "tel:", + "file:", + "blob:", + "javascript:", + "about:", + "magnet:", + "ws:", + "wss:", + ]; - // don't rewrite invalid URIs - try { - new URL(url, origin); - } catch { - return url; - } + function rewriteURL(url) { + if (!url) return url; - // don't double rewrite - if (url.startsWith(`${proxyOrigin}/http://`)) return url; - if (url.startsWith(`${proxyOrigin}/https://`)) 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(proxyOrigin) && !url.startsWith(`${proxyOrigin}/http`) - ) { - // edge case where client js uses current url host to write an absolute path - url = "".replace(proxyOrigin, `${proxyOrigin}/${origin}`); - } 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; + // fetch url might be string, url, or request object + // handle all three by downcasting to string + const isStr = typeof url === "string"; + if (!isStr) { + x = String(url); + if (x == "[object Request]") { + url = url.url; + } else { + url = String(url); + } + } + + const oldUrl = 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}/http://`)) return url; + if (url.startsWith(`${proxyOrigin}/https://`)) 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}`) + + //originDomain = origin.replace("https://", ""); + let scheme = origin.split(":")[0]; + + if (url.startsWith("//")) { + url = `/${scheme}://${encodeURIComponent(url.substring(2))}`; + } else if (url.startsWith("/")) { + url = `/${origin}/${encodeURIComponent(url.substring(1))}`; + } else if ( + url.startsWith(proxyOrigin) && !url.startsWith(`${proxyOrigin}/http`) + ) { + // edge case where client js uses current url host to write an absolute path + url = "".replace(proxyOrigin, `${proxyOrigin}/${origin}`); + } 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") && @@ -98,7 +101,7 @@ } else { obj = objectOrName; } - + if (isGlobalFunction) { const originalFunction = obj; globalThis[objectOrName] = function(...args) { @@ -118,233 +121,246 @@ } } */ - function hideMonkeyPatch(objectOrName, method, originalToString) { - return; - } + function hideMonkeyPatch(objectOrName, method, originalToString) { + return; + } - // 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)}', - ); + // monkey patch fetch + const oldFetch = fetch; + fetch = async (url, init) => { + return oldFetch(rewriteURL(url), init); + }; + hideMonkeyPatch("fetch", "fetch", "function fetch() { [native code] }"); - 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 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)}', + ); - // 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] }", - ); + 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 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 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.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.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.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); + // 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); + } }, - set: function(newValue) { - originalHrefDescriptor.set.call(this, rewriteURL(newValue)); + 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); + } }, - }); + }); + } + }); - // 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); - } - }, - }); - } + // monkey-patching Element.setAttribute + const originalSetAttribute = Element.prototype.setAttribute; + Element.prototype.setAttribute = function (name, value) { + const isMatchingElement = elements.some((element) => { + return this.tagName.toLowerCase() === element.tag && + name.toLowerCase() === element.attribute; }); + if (isMatchingElement) { + value = rewriteURL(value); + } + originalSetAttribute.call(this, name, value); + }; - // 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; - } + // 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"); - const tempContainer = document.createElement("div"); - document[isRewritingHTMLKey] = true; + // Check if already processing + if (document[isRewritingHTMLKey]) { + return html; + } - try { - tempContainer.innerHTML = html; + const tempContainer = document.createElement("div"); + document[isRewritingHTMLKey] = true; - // Create a map for quick lookup - const elementsMap = new Map(elements.map((e) => [e.tag, e.attribute])); + try { + tempContainer.innerHTML = html; - // 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); - } - } + // Create a map for quick lookup + const elementsMap = new Map(elements.map((e) => [e.tag, e.attribute])); - return tempContainer.innerHTML; - } finally { - // Clear the flag - document[isRewritingHTMLKey] = false; + // 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); - } - }, - }); - } - }); + // 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); + } + }, + }); + } + }); })(); diff --git a/proxychain/responsemodifers/patch_tracker_scripts.go b/proxychain/responsemodifers/patch_tracker_scripts.go index 88e1b43..045f7a4 100644 --- a/proxychain/responsemodifers/patch_tracker_scripts.go +++ b/proxychain/responsemodifers/patch_tracker_scripts.go @@ -66,6 +66,7 @@ func PatchTrackerScripts() proxychain.ResponseModification { // preflight checks reqURL := chain.Request.URL.String() isTracker := false + // var surrogateScript io.ReadCloser for domain, domainRules := range rules {