fix: force href to absolute http URLs and strip HSTS/CSP headers
Build and Push to GHCR / build-and-push (push) Has been cancelled

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 <[email protected]>
This commit is contained in:
Agent
2026-05-24 21:01:48 +08:00
co-authored by HAPI
parent 163e625495
commit 6fcde70a26
3 changed files with 71 additions and 6 deletions
+10 -1
View File
@@ -24,6 +24,8 @@ func DynamicProxy(targetURL string) http.Handler {
p := &httputil.ReverseProxy{ p := &httputil.ReverseProxy{
Director: func(req *http.Request) { Director: func(req *http.Request) {
saveProxyContext(req)
req.URL.Scheme = target.Scheme req.URL.Scheme = target.Scheme
req.URL.Host = target.Host req.URL.Host = target.Host
req.Host = target.Host req.Host = target.Host
@@ -36,6 +38,11 @@ func DynamicProxy(targetURL string) http.Handler {
req.Header.Del("X-Forwarded-For") req.Header.Del("X-Forwarded-For")
}, },
ModifyResponse: func(resp *http.Response) error { 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 前缀 // 从请求上下文中获取 token 前缀
tokenPrefix := "" tokenPrefix := ""
if resp.Request != nil { if resp.Request != nil {
@@ -48,6 +55,8 @@ func DynamicProxy(targetURL string) http.Handler {
return nil return nil
} }
host, proto := loadProxyInfo(resp.Request)
// 重写 Location header // 重写 Location header
if loc := resp.Header.Get("Location"); loc != "" { if loc := resp.Header.Get("Location"); loc != "" {
resp.Header.Set("Location", rewriteURL(loc, tokenPrefix)) resp.Header.Set("Location", rewriteURL(loc, tokenPrefix))
@@ -63,7 +72,7 @@ func DynamicProxy(targetURL string) http.Handler {
resp.Body.Close() resp.Body.Close()
// 1. 服务端重写所有已知 URL 属性(应对 CSP 禁止内联脚本的情况) // 1. 服务端重写所有已知 URL 属性(应对 CSP 禁止内联脚本的情况)
body = rewriteHTMLBody(body, tokenPrefix) body = rewriteHTMLBody(body, tokenPrefix, host, proto)
// 2. 注入 JS 处理动态添加的内容(无 CSP 时生效) // 2. 注入 JS 处理动态添加的内容(无 CSP 时生效)
body = injectTokenPrefixScript(body, tokenPrefix) body = injectTokenPrefixScript(body, tokenPrefix)
+49 -1
View File
@@ -2,6 +2,7 @@ package proxy
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -13,12 +14,52 @@ import (
"mirror-proxy/internal/auth" "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 主站反向代理 // NewGitHubProxy 创建 GitHub 主站反向代理
func NewGitHubProxy() http.Handler { func NewGitHubProxy() http.Handler {
target, _ := url.Parse("https://github.com") target, _ := url.Parse("https://github.com")
p := httputil.NewSingleHostReverseProxy(target) p := httputil.NewSingleHostReverseProxy(target)
p.Director = func(req *http.Request) { p.Director = func(req *http.Request) {
saveProxyContext(req)
req.URL.Scheme = target.Scheme req.URL.Scheme = target.Scheme
req.URL.Host = target.Host req.URL.Host = target.Host
req.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 == "" { if tokenPrefix == "" {
return nil return nil
} }
host, proto := loadProxyInfo(resp.Request)
// 重写 Location header // 重写 Location header
if loc := resp.Header.Get("Location"); loc != "" { if loc := resp.Header.Get("Location"); loc != "" {
resp.Header.Set("Location", rewriteURL(loc, tokenPrefix)) resp.Header.Set("Location", rewriteURL(loc, tokenPrefix))
@@ -59,7 +107,7 @@ func NewGitHubProxy() http.Handler {
resp.Body.Close() resp.Body.Close()
// 1. 服务端重写所有已知 URL 属性(应对 CSP 禁止内联脚本的情况) // 1. 服务端重写所有已知 URL 属性(应对 CSP 禁止内联脚本的情况)
body = rewriteHTMLBody(body, tokenPrefix) body = rewriteHTMLBody(body, tokenPrefix, host, proto)
// 2. 注入 JS 处理动态添加的内容(无 CSP 时生效) // 2. 注入 JS 处理动态添加的内容(无 CSP 时生效)
body = injectTokenPrefixScript(body, tokenPrefix) body = injectTokenPrefixScript(body, tokenPrefix)
+12 -4
View File
@@ -11,7 +11,7 @@ import (
// so they include the token prefix. This is needed because many sites (e.g. // 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 // GitHub) use Content-Security-Policy that blocks inline scripts, making the
// JS-injection approach unreliable. // JS-injection approach unreliable.
func rewriteHTMLBody(body []byte, prefix string) []byte { func rewriteHTMLBody(body []byte, prefix string, host string, proto string) []byte {
if prefix == "" { if prefix == "" {
return body return body
} }
@@ -21,7 +21,7 @@ func rewriteHTMLBody(body []byte, prefix string) []byte {
return body return body
} }
rewriteNode(doc, prefix) rewriteNode(doc, prefix, host, proto)
var buf bytes.Buffer var buf bytes.Buffer
if err := html.Render(&buf, doc); err != nil { if err := html.Render(&buf, doc); err != nil {
@@ -45,12 +45,20 @@ var urlAttrs = []string{
"data-href", "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 { if n.Type == html.ElementNode {
for i := range n.Attr { for i := range n.Attr {
attr := &n.Attr[i] attr := &n.Attr[i]
if isURLAttr(attr.Key) { if isURLAttr(attr.Key) {
attr.Val = rewriteURL(attr.Val, prefix) 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 continue
} }
// <meta http-equiv="refresh" content="0;url=/path"> // <meta http-equiv="refresh" content="0;url=/path">
@@ -69,7 +77,7 @@ func rewriteNode(n *html.Node, prefix string) {
} }
for c := n.FirstChild; c != nil; c = c.NextSibling { for c := n.FirstChild; c != nil; c = c.NextSibling {
rewriteNode(c, prefix) rewriteNode(c, prefix, host, proto)
} }
} }