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{
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user