fix: patch fetch Request objects, EventSource, WebSocket; inject script earlier
Build and Push to GHCR / build-and-push (push) Has been cancelled
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:
+28
-24
@@ -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) {
|
||||
|
||||
@@ -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:]...)...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user