fix: force href to absolute http URLs and strip HSTS/CSP headers
Build and Push to GHCR / build-and-push (push) Has been cancelled
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
// <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 {
|
||||
rewriteNode(c, prefix)
|
||||
rewriteNode(c, prefix, host, proto)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user