From 6fcde70a260769bdf4a9188e7aa9e2132c93859c Mon Sep 17 00:00:00 2001 From: Agent Date: Sun, 24 May 2026 21:01:48 +0800 Subject: [PATCH] fix: force href to absolute http URLs and strip HSTS/CSP headers Upstream servers (e.g. GitHub) send Strict-Transport-Security and Content-Security-Policy headers that cause browsers to upgrade HTTP URLs to HTTPS. Strip those headers in ModifyResponse. Also save the original request host/protocol in context before Director mutates req.Host, then rewrite href attributes to explicit absolute URLs (e.g. http://host/{token}/path) so the browser doesn't guess the scheme. via [HAPI](https://hapi.run) Co-Authored-By: HAPI --- internal/proxy/dynamic.go | 11 ++++++++- internal/proxy/github.go | 50 ++++++++++++++++++++++++++++++++++++++- internal/proxy/rewrite.go | 16 +++++++++---- 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/internal/proxy/dynamic.go b/internal/proxy/dynamic.go index a20b4dd..237a5c0 100644 --- a/internal/proxy/dynamic.go +++ b/internal/proxy/dynamic.go @@ -24,6 +24,8 @@ func DynamicProxy(targetURL string) http.Handler { p := &httputil.ReverseProxy{ Director: func(req *http.Request) { + saveProxyContext(req) + req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.Host = target.Host @@ -36,6 +38,11 @@ func DynamicProxy(targetURL string) http.Handler { req.Header.Del("X-Forwarded-For") }, ModifyResponse: func(resp *http.Response) error { + // 删除上游返回的 HSTS / CSP header,防止浏览器将代理域名标记为 + // HTTPS-only 或自动升级 HTTP URL。 + resp.Header.Del("Strict-Transport-Security") + resp.Header.Del("Content-Security-Policy") + // 从请求上下文中获取 token 前缀 tokenPrefix := "" if resp.Request != nil { @@ -48,6 +55,8 @@ func DynamicProxy(targetURL string) http.Handler { return nil } + host, proto := loadProxyInfo(resp.Request) + // 重写 Location header if loc := resp.Header.Get("Location"); loc != "" { resp.Header.Set("Location", rewriteURL(loc, tokenPrefix)) @@ -63,7 +72,7 @@ func DynamicProxy(targetURL string) http.Handler { resp.Body.Close() // 1. 服务端重写所有已知 URL 属性(应对 CSP 禁止内联脚本的情况) - body = rewriteHTMLBody(body, tokenPrefix) + body = rewriteHTMLBody(body, tokenPrefix, host, proto) // 2. 注入 JS 处理动态添加的内容(无 CSP 时生效) body = injectTokenPrefixScript(body, tokenPrefix) diff --git a/internal/proxy/github.go b/internal/proxy/github.go index 36db381..8919130 100644 --- a/internal/proxy/github.go +++ b/internal/proxy/github.go @@ -2,6 +2,7 @@ package proxy import ( "bytes" + "context" "fmt" "io" "net/http" @@ -13,12 +14,52 @@ import ( "mirror-proxy/internal/auth" ) +// context keys for preserving original request info through the proxy +type proxyHostKey string +const proxyHostCtxKey proxyHostKey = "proxyHost" + +type proxyProtoKey string +const proxyProtoCtxKey proxyProtoKey = "proxyProto" + +// saveProxyContext saves the original request host and protocol into the +// request context before Director mutates req.Host / req.URL. +func saveProxyContext(req *http.Request) { + host := req.Host + proto := "http" + if req.TLS != nil { + proto = "https" + } + if fp := req.Header.Get("X-Forwarded-Proto"); fp != "" { + proto = fp + } + ctx := context.WithValue(req.Context(), proxyHostCtxKey, host) + ctx = context.WithValue(ctx, proxyProtoCtxKey, proto) + *req = *req.WithContext(ctx) +} + +// loadProxyInfo reads the original host/protocol back from the outbound +// request's context (injected by saveProxyContext in Director). +func loadProxyInfo(req *http.Request) (host, proto string) { + if req == nil { + return "", "http" + } + if h, ok := req.Context().Value(proxyHostCtxKey).(string); ok { + host = h + } + if p, ok := req.Context().Value(proxyProtoCtxKey).(string); ok { + proto = p + } + return +} + // NewGitHubProxy 创建 GitHub 主站反向代理 func NewGitHubProxy() http.Handler { target, _ := url.Parse("https://github.com") p := httputil.NewSingleHostReverseProxy(target) p.Director = func(req *http.Request) { + saveProxyContext(req) + req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.Host = target.Host @@ -40,10 +81,17 @@ func NewGitHubProxy() http.Handler { } } + // 删除上游返回的 HSTS / CSP header,防止浏览器将代理域名标记为 + // HTTPS-only 或自动升级 HTTP URL。 + resp.Header.Del("Strict-Transport-Security") + resp.Header.Del("Content-Security-Policy") + if tokenPrefix == "" { return nil } + host, proto := loadProxyInfo(resp.Request) + // 重写 Location header if loc := resp.Header.Get("Location"); loc != "" { resp.Header.Set("Location", rewriteURL(loc, tokenPrefix)) @@ -59,7 +107,7 @@ func NewGitHubProxy() http.Handler { resp.Body.Close() // 1. 服务端重写所有已知 URL 属性(应对 CSP 禁止内联脚本的情况) - body = rewriteHTMLBody(body, tokenPrefix) + body = rewriteHTMLBody(body, tokenPrefix, host, proto) // 2. 注入 JS 处理动态添加的内容(无 CSP 时生效) body = injectTokenPrefixScript(body, tokenPrefix) diff --git a/internal/proxy/rewrite.go b/internal/proxy/rewrite.go index b24c51a..2bf5f10 100644 --- a/internal/proxy/rewrite.go +++ b/internal/proxy/rewrite.go @@ -11,7 +11,7 @@ import ( // so they include the token prefix. This is needed because many sites (e.g. // GitHub) use Content-Security-Policy that blocks inline scripts, making the // JS-injection approach unreliable. -func rewriteHTMLBody(body []byte, prefix string) []byte { +func rewriteHTMLBody(body []byte, prefix string, host string, proto string) []byte { if prefix == "" { return body } @@ -21,7 +21,7 @@ func rewriteHTMLBody(body []byte, prefix string) []byte { return body } - rewriteNode(doc, prefix) + rewriteNode(doc, prefix, host, proto) var buf bytes.Buffer if err := html.Render(&buf, doc); err != nil { @@ -45,12 +45,20 @@ var urlAttrs = []string{ "data-href", } -func rewriteNode(n *html.Node, prefix string) { +func rewriteNode(n *html.Node, prefix string, host string, proto string) { if n.Type == html.ElementNode { for i := range n.Attr { attr := &n.Attr[i] if isURLAttr(attr.Key) { attr.Val = rewriteURL(attr.Val, prefix) + // 将 href 的相对路径显式写成绝对路径,避免浏览器根据当前 + // 页面协议猜测(防止 http 页面中的链接被解析成 https)。 + if host != "" && proto != "" && + strings.EqualFold(attr.Key, "href") && + strings.HasPrefix(attr.Val, "/") && + !strings.HasPrefix(attr.Val, "//") { + attr.Val = proto + "://" + host + attr.Val + } continue } // @@ -69,7 +77,7 @@ func rewriteNode(n *html.Node, prefix string) { } for c := n.FirstChild; c != nil; c = c.NextSibling { - rewriteNode(c, prefix) + rewriteNode(c, prefix, host, proto) } }