From 6c54d310869f403ec4f5f4dfb01743674bd8157b Mon Sep 17 00:00:00 2001 From: Kevin Pham Date: Thu, 23 Nov 2023 08:14:52 -0600 Subject: [PATCH] add dynamic resource url patcher as standalone responsemodifier --- cmd/main.go | 5 +- handlers/proxy.go | 7 ++- proxychain/proxychain.go | 7 ++- .../patch_dynamic_resource_urls.go | 55 +++++++++++++++++++ ...iter.js => patch_dynamic_resource_urls.js} | 20 +++++-- .../rewriters/script_injector_rewriter.go | 31 +++++++++++ 6 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 proxychain/responsemodifers/patch_dynamic_resource_urls.go rename proxychain/responsemodifers/{rewriters/js_resource_url_rewriter.js => patch_dynamic_resource_urls.js} (95%) diff --git a/cmd/main.go b/cmd/main.go index 24c4895..6822c7d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -86,8 +86,9 @@ func main() { app := fiber.New( fiber.Config{ - Prefork: *prefork, - GETOnly: false, + Prefork: *prefork, + GETOnly: false, + ReadBufferSize: 4096 * 4, // increase max header size }, ) diff --git a/handlers/proxy.go b/handlers/proxy.go index 1df46f0..3d13f96 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -56,15 +56,16 @@ func NewProxySiteHandler(opts *ProxyOptions) fiber.Handler { SetFiberCtx(c). SetDebugLogging(opts.Verbose). SetRequestModifications( - //rx.DeleteOutgoingCookies(), + rx.DeleteOutgoingCookies(), //rx.RequestArchiveIs(), rx.MasqueradeAsGoogleBot(), ). AddResponseModifications( - //tx.DeleteIncomingCookies(), - tx.RewriteHTMLResourceURLs(), tx.BypassCORS(), tx.BypassContentSecurityPolicy(), + tx.DeleteIncomingCookies(), + tx.RewriteHTMLResourceURLs(), + tx.PatchDynamicResourceURLs(), ). Execute() diff --git a/proxychain/proxychain.go b/proxychain/proxychain.go index 545e35f..5b71c91 100644 --- a/proxychain/proxychain.go +++ b/proxychain/proxychain.go @@ -256,8 +256,11 @@ func (chain *ProxyChain) extractUrl() (*url.URL, error) { return reconstructUrlFromReferer(referer, relativePath) } -// AddBodyRewriter adds a HTMLTokenRewriter to the chain -// HTMLTokenRewriters modify the body response by parsing the HTML +// AddBodyRewriter adds a HTMLTokenRewriter to the chain. +// - HTMLTokenRewriters modify the body response by parsing the HTML +// and making changes to the DOM as it streams to the client +// - In most cases, you don't need to use this method. It's usually called by +// a ResponseModifier to batch queue changes for performance reasons. func (chain *ProxyChain) AddHTMLTokenRewriter(rr rr.IHTMLTokenRewriter) *ProxyChain { chain.htmlTokenRewriters = append(chain.htmlTokenRewriters, rr) return chain diff --git a/proxychain/responsemodifers/patch_dynamic_resource_urls.go b/proxychain/responsemodifers/patch_dynamic_resource_urls.go new file mode 100644 index 0000000..51f86e6 --- /dev/null +++ b/proxychain/responsemodifers/patch_dynamic_resource_urls.go @@ -0,0 +1,55 @@ +package responsemodifers + +import ( + _ "embed" + "fmt" + "ladder/proxychain" + "ladder/proxychain/responsemodifers/rewriters" + "strings" +) + +//go:embed patch_dynamic_resource_urls.js +var patchDynamicResourceURLsScript string + +// PatchDynamicResourceURLs patches the javascript runtime to rewrite URLs client-side. +// - This function is designed to allow the proxified page +// to still be browsible by routing all resource URLs through the proxy. +// - Native APIs capable of network requests will be hooked +// and the URLs arguments modified to point to the proxy instead. +// - fetch('/relative_path') -> fetch('/https://proxiedsite.com/relative_path') +// - Element.setAttribute('src', "/assets/img.jpg") -> Element.setAttribute('src', "/https://proxiedsite.com/assets/img.jpg") -> fetch('/https://proxiedsite.com/relative_path') +func PatchDynamicResourceURLs() proxychain.ResponseModification { + return func(chain *proxychain.ProxyChain) error { + // don't add rewriter if it's not even html + ct := chain.Response.Header.Get("content-type") + if !strings.HasPrefix(ct, "text/html") { + return nil + } + + // this is the original URL sent by client: + // http://localhost:8080/http://proxiedsite.com/foo/bar + originalURI := chain.Context.Request().URI() + + // this is the extracted URL that the client requests to proxy + // http://proxiedsite.com/foo/bar + reqURL := chain.Request.URL + + params := map[string]string{ + // ie: http://localhost:8080 + "{{PROXY_ORIGIN}}": fmt.Sprintf("%s://%s", originalURI.Scheme(), originalURI.Host()), + // ie: http://proxiedsite.com + "{{ORIGIN}}": fmt.Sprintf("%s://%s", reqURL.Scheme, reqURL.Host), + } + + // the rewriting actually happens in chain.Execute() as the client is streaming the response body back + rr := rewriters.NewScriptInjectorRewriterWithParams( + patchDynamicResourceURLsScript, + rewriters.BeforeDOMContentLoaded, + params, + ) + // we just queue it up here + chain.AddHTMLTokenRewriter(rr) + + return nil + } +} diff --git a/proxychain/responsemodifers/rewriters/js_resource_url_rewriter.js b/proxychain/responsemodifers/patch_dynamic_resource_urls.js similarity index 95% rename from proxychain/responsemodifers/rewriters/js_resource_url_rewriter.js rename to proxychain/responsemodifers/patch_dynamic_resource_urls.js index a6f09e1..cc4b4d1 100644 --- a/proxychain/responsemodifers/rewriters/js_resource_url_rewriter.js +++ b/proxychain/responsemodifers/patch_dynamic_resource_urls.js @@ -2,6 +2,19 @@ // 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:", @@ -24,11 +37,6 @@ // don't rewrite special URIs if (blacklistedSchemes.includes(url)) return url; - //const proxyOrigin = globalThis.window.location.origin; - const proxyOrigin = "R_PROXYURL"; - //const origin = (new URL(decodeURIComponent(globalThis.window.location.pathname.substring(1)))).origin - const origin = "R_BASEURL"; - // don't rewrite invalid URIs try { new URL(url, origin) } catch { return url } @@ -314,4 +322,4 @@ const originalSetters = {}; console.log('DOM is now idle. Executing...'); } -})(); \ No newline at end of file +})(); diff --git a/proxychain/responsemodifers/rewriters/script_injector_rewriter.go b/proxychain/responsemodifers/rewriters/script_injector_rewriter.go index b08498b..a1a0d9e 100644 --- a/proxychain/responsemodifers/rewriters/script_injector_rewriter.go +++ b/proxychain/responsemodifers/rewriters/script_injector_rewriter.go @@ -3,6 +3,7 @@ package rewriters import ( _ "embed" "fmt" + "sort" "strings" "golang.org/x/net/html" @@ -50,6 +51,22 @@ func (r *ScriptInjectorRewriter) ModifyToken(token *html.Token) (string, string) } } +// applies parameters by string replacement of the template script +func (r *ScriptInjectorRewriter) applyParams(params map[string]string) { + // Sort the keys by length in descending order + keys := make([]string, 0, len(params)) + for key := range params { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + return len(keys[i]) > len(keys[j]) + }) + + for _, key := range keys { + r.script = strings.ReplaceAll(r.script, key, params[key]) + } +} + // NewScriptInjectorRewriter implements a HtmlTokenRewriter // and injects JS into the page for execution at a particular time func NewScriptInjectorRewriter(script string, execTime ScriptExecTime) *ScriptInjectorRewriter { @@ -58,3 +75,17 @@ func NewScriptInjectorRewriter(script string, execTime ScriptExecTime) *ScriptIn script: script, } } + +// NewScriptInjectorRewriterWith implements a HtmlTokenRewriter +// and injects JS into the page for execution at a particular time +// accepting arguments into the script, which will be added via a string replace +// the params map represents the key-value pair of the params. +// the key will be string replaced with the value +func NewScriptInjectorRewriterWithParams(script string, execTime ScriptExecTime, params map[string]string) *ScriptInjectorRewriter { + rr := &ScriptInjectorRewriter{ + execTime: execTime, + script: script, + } + rr.applyParams(params) + return rr +}