fix: patch fetch Request objects, EventSource, WebSocket; inject script earlier
Build and Push to GHCR / build-and-push (push) Has been cancelled

- Handle Request objects and URL objects passed to fetch()
- Patch EventSource and WebSocket constructors
- Move script injection to right after <head> so it runs before
the page's own JS captures fetch references
- Strip Transfer-Encoding when rewriting response body
- Add CORS headers to all proxied responses in ModifyResponse

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <[email protected]>
This commit is contained in:
Agent
2026-05-24 23:01:36 +08:00
co-authored by HAPI
parent 31b5031f86
commit 1b8e24ddb1
2 changed files with 66 additions and 28 deletions
+28 -24
View File
@@ -51,36 +51,40 @@ func DynamicProxy(targetURL string) http.Handler {
}
}
if tokenPrefix == "" {
return nil
}
if tokenPrefix != "" {
host, proto := loadProxyInfo(resp.Request)
host, proto := loadProxyInfo(resp.Request)
// 重写 Location header
if loc := resp.Header.Get("Location"); loc != "" {
resp.Header.Set("Location", rewriteURL(loc, tokenPrefix))
}
// 对 HTML 响应做服务端 URL 重写 + 注入 JS 兜底
contentType := resp.Header.Get("Content-Type")
if strings.Contains(contentType, "text/html") && resp.Body != nil {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
// 重写 Location header
if loc := resp.Header.Get("Location"); loc != "" {
resp.Header.Set("Location", rewriteURL(loc, tokenPrefix))
}
resp.Body.Close()
// 1. 服务端重写所有已知 URL 属性(应对 CSP 禁止内联脚本的情况)
body = rewriteHTMLBody(body, tokenPrefix, host, proto)
// 2. 注入 JS 处理动态添加的内容(无 CSP 时生效)
body = injectTokenPrefixScript(body, tokenPrefix)
// 对 HTML 响应做服务端 URL 重写 + 注入 JS 兜底
contentType := resp.Header.Get("Content-Type")
if strings.Contains(contentType, "text/html") && resp.Body != nil {
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
resp.Body.Close()
resp.Body = io.NopCloser(bytes.NewReader(body))
resp.ContentLength = int64(len(body))
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
// 1. 服务端重写所有已知 URL 属性(应对 CSP 禁止内联脚本的情况)
body = rewriteHTMLBody(body, tokenPrefix, host, proto)
// 2. 注入 JS 处理动态添加的内容(无 CSP 时生效)
body = injectTokenPrefixScript(body, tokenPrefix)
resp.Body = io.NopCloser(bytes.NewReader(body))
resp.ContentLength = int64(len(body))
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
resp.Header.Del("Transfer-Encoding")
}
}
// 确保所有响应都带上 CORS 头
resp.Header.Set("Access-Control-Allow-Origin", "*")
resp.Header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH")
resp.Header.Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Origin, X-Requested-With")
return nil
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
+38 -4
View File
@@ -114,8 +114,14 @@ func NewGitHubProxy() http.Handler {
resp.Body = io.NopCloser(bytes.NewReader(body))
resp.ContentLength = int64(len(body))
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
resp.Header.Del("Transfer-Encoding")
}
// 确保所有响应都带上 CORS 头
resp.Header.Set("Access-Control-Allow-Origin", "*")
resp.Header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH")
resp.Header.Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Origin, X-Requested-With")
return nil
}
@@ -157,15 +163,31 @@ func rewriteURL(u string, prefix string) string {
}
// injectTokenPrefixScript 在 HTML 中注入 JS 脚本,重写所有链接使其包含 token 前缀
// 同时拦截 fetch / XMLHttpRequest,将直接访问 github.com 的请求也转回代理。
// 同时拦截 fetch / XMLHttpRequest / EventSource / WebSocket,将直接访问 github.com
// 的请求也转回代理,从而彻底避免 CORS 问题。
func injectTokenPrefixScript(body []byte, prefix string) []byte {
script := []byte(`<script>` +
`(function(){` +
`var p='` + prefix + `';` +
`function rw(v){return v&&v.startsWith('/')&&!v.startsWith(p+'/')&&v!==p?p+v:v};` +
`function rfw(u){if(typeof u!=='string')return u;if(u.startsWith(p+'/')||u===p)return u;var a=['https://github.com/','https://raw.githubusercontent.com/','https://api.github.com/'];for(var i=0;i<a.length;i++){if(u.startsWith(a[i]))return p+'/'+u;}if(u.startsWith('/'))return p+u;return u;}` +
`var of=window.fetch;window.fetch=function(u,o){if(typeof u==='string'){u=rfw(u);}return of.call(this,u,o);};` +
`function rfw(u){` +
`if(typeof u!=='string'){` +
`if(u&&typeof u==='object'){` +
`if(typeof u.url==='string'){var r=rfw(u.url);if(r!==u.url){try{var q={};['method','headers','body','mode','credentials','cache','redirect','referrer','referrerPolicy','integrity','keepalive','signal'].forEach(function(k){if(k in u)q[k]=u[k]});return new Request(r,q);}catch(e){return r;}}return u;}` +
`if(typeof u.href==='string'){var r=rfw(u.href);return r!==u.href?r:u;}` +
`}` +
`return u;` +
`}` +
`if(u.startsWith(p+'/')||u===p)return u;` +
`var a=['https://github.com/','https://raw.githubusercontent.com/','https://api.github.com/','wss://github.com/','wss://raw.githubusercontent.com/','wss://api.github.com/'];` +
`for(var i=0;i<a.length;i++){if(u.startsWith(a[i]))return p+'/'+u;}` +
`if(u.startsWith('/'))return p+u;` +
`return u;` +
`}` +
`var of=window.fetch;window.fetch=function(u,o){u=rfw(u);return of.call(this,u,o);};` +
`var oo=XMLHttpRequest.prototype.open;XMLHttpRequest.prototype.open=function(){var a=Array.prototype.slice.call(arguments);if(a.length>1){a[1]=rfw(a[1]);}return oo.apply(this,a);};` +
`if(typeof EventSource!=='undefined'){var oes=window.EventSource;window.EventSource=function(u,o){u=rfw(u);return new oes(u,o);};}` +
`if(typeof WebSocket!=='undefined'){var ows=window.WebSocket;window.WebSocket=function(u,p){if(typeof u==='string'){u=rfw(u);}return new ows(u,p);};}` +
`function fix(root){` +
`root.querySelectorAll&&root.querySelectorAll('a[href]').forEach(function(a){a.href=rw(a.getAttribute('href'))});` +
`root.querySelectorAll&&root.querySelectorAll('form[action]').forEach(function(f){f.action=rw(f.getAttribute('action'))});` +
@@ -195,7 +217,19 @@ func injectTokenPrefixScript(body []byte, prefix string) []byte {
`})();` +
`</script>`)
// 尝试在 </head> 前插入
// 优先在 <head> 标签后插入,确保比页面其他脚本先执行
if idx := bytes.Index(body, []byte("<head>")); idx != -1 {
pos := idx + len("<head>")
return append(body[:pos], append(script, body[pos:]...)...)
}
if idx := bytes.Index(body, []byte("<head ")); idx != -1 {
endIdx := bytes.Index(body[idx:], []byte(">"))
if endIdx != -1 {
pos := idx + endIdx + 1
return append(body[:pos], append(script, body[pos:]...)...)
}
}
// 回退到 </head> 前
if idx := bytes.Index(body, []byte("</head>")); idx != -1 {
return append(body[:idx], append(script, body[idx:]...)...)
}