From 4fd47812f740059a35c61d336b659180babbce9c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 16 Mar 2025 21:03:28 +0800 Subject: [PATCH] 25w19a --- CHANGELOG.md | 6 + DEV-VERSION | 2 +- README.md | 4 + auth/auth-header.go | 12 +- auth/auth-parameters.go | 12 +- auth/auth.go | 7 +- config/config.go | 26 +++- config/config.toml | 4 + proxy/chunkreq.go | 37 ++++- proxy/handler.go | 90 ++---------- proxy/httpc.go | 14 -- proxy/matchrepo.go | 302 ++++++++++++++++++++++++++++++++++++---- proxy/reqheader.go | 29 ++++ 13 files changed, 398 insertions(+), 147 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fd3f06..85f1761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w19a - 2025-03-16 +--- +- PRE-RELEASE: 此版本是v2.5.0的预发布版本,请勿在生产环境中使用; +- ADD: 加入脚本嵌套加速功能 +- CHANGE: 修正Auth模块一些代码风格问题 + 2.4.2 - 2025-03-14 --- - CHANGE: 在GitClone Cache模式下, 相关请求会使用独立httpc client diff --git a/DEV-VERSION b/DEV-VERSION index 08b9602..e33ec6f 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w18a \ No newline at end of file +25w19a \ No newline at end of file diff --git a/README.md b/README.md index f245786..c6c7eee 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,9 @@ mode = "bypass" # bypass / cache 运行模式, cache模式依赖smart-git smartGitAddr = "http://127.0.0.1:8080" # smart-git组件地址 ForceH2C = false # 强制使用H2C连接 +[shell] +editor = false # 脚本嵌套加速 + [pages] mode = "internal" # "internal" or "external" 内部/外部 前端 默认内部 theme = "bootstrap" # "bootstrap" or "nebula" 内置主题 @@ -125,6 +128,7 @@ level = "info" # 日志级别 dump, debug, info, warn, error, none authMethod = "parameters" # 鉴权方式,支持parameters,header authToken = "token" # 用户鉴权Token enabled = false # 是否开启用户鉴权 +ForceAllowApi = false # 在不开启Header鉴权的情况下允许api代理 [blacklist] blacklistFile = "/data/ghproxy/config/blacklist.json" # 黑名单文件路径 diff --git a/auth/auth-header.go b/auth/auth-header.go index 4cdff18..c29f66b 100644 --- a/auth/auth-header.go +++ b/auth/auth-header.go @@ -7,23 +7,21 @@ import ( "github.com/gin-gonic/gin" ) -func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) { +func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err error) { if !cfg.Auth.Enabled { - return true, "" + return true, nil } // 获取"GH-Auth"的值 authToken := c.GetHeader("GH-Auth") logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Request.Method, c.Request.Host, c.Request.URL.Path, c.Request.Proto, c.Request.RemoteAddr, authToken) if authToken == "" { - err := "Auth Header == nil" - return false, err + return false, fmt.Errorf("Auth token not found") } isValid = authToken == cfg.Auth.AuthToken if !isValid { - err := fmt.Sprintf("Auth token incorrect: %s", authToken) - return false, err + return false, fmt.Errorf("Auth token incorrect") } - return isValid, "" + return isValid, nil } diff --git a/auth/auth-parameters.go b/auth/auth-parameters.go index 20d1ac2..e43a1a7 100644 --- a/auth/auth-parameters.go +++ b/auth/auth-parameters.go @@ -7,24 +7,22 @@ import ( "github.com/gin-gonic/gin" ) -func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) { +func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, err error) { if !cfg.Auth.Enabled { - return true, "" + return true, nil } authToken := c.Query("auth_token") logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto, authToken) if authToken == "" { - err := "Auth token == nil" - return false, err + return false, fmt.Errorf("Auth token not found") } isValid = authToken == cfg.Auth.AuthToken if !isValid { - err := fmt.Sprintf("Auth token incorrect: %s", authToken) - return false, err + return false, fmt.Errorf("Auth token invalid") } - return isValid, "" + return isValid, nil } diff --git a/auth/auth.go b/auth/auth.go index 99cb6de..d7cad1a 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,6 +1,7 @@ package auth import ( + "fmt" "ghproxy/config" "github.com/WJQSERVER-STUDIO/go-utils/logger" @@ -34,7 +35,7 @@ func Init(cfg *config.Config) { logDebug("Auth Init") } -func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) { +func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err error) { if cfg.Auth.AuthMethod == "parameters" { isValid, err = AuthParametersHandler(c, cfg) return isValid, err @@ -43,9 +44,9 @@ func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) return isValid, err } else if cfg.Auth.AuthMethod == "" { logError("Auth method not set") - return true, "" + return true, nil } else { logError("Auth method not supported") - return false, "Auth method not supported" + return false, fmt.Errorf(fmt.Sprintf("Auth method %s not supported", cfg.Auth.AuthMethod)) } } diff --git a/config/config.go b/config/config.go index 9724212..ee2ab4a 100644 --- a/config/config.go +++ b/config/config.go @@ -8,6 +8,7 @@ type Config struct { Server ServerConfig Httpc HttpcConfig GitClone GitCloneConfig + Shell ShellConfig Pages PagesConfig Log LogConfig Auth AuthConfig @@ -62,6 +63,14 @@ type GitCloneConfig struct { ForceH2C bool `toml:"ForceH2C"` } +/* +[shell] +editor = true +*/ +type ShellConfig struct { + Editor bool `toml:"editor"` +} + /* [pages] mode = "internal" # "internal" or "external" @@ -82,11 +91,20 @@ type LogConfig struct { Level string `toml:"level"` } +/* +[auth] +authMethod = "parameters" # "header" or "parameters" +authToken = "token" +enabled = false +passThrough = false +ForceAllowApi = true +*/ type AuthConfig struct { - Enabled bool `toml:"enabled"` - AuthMethod string `toml:"authMethod"` - AuthToken string `toml:"authToken"` - PassThrough bool `toml:"passThrough"` + Enabled bool `toml:"enabled"` + AuthMethod string `toml:"authMethod"` + AuthToken string `toml:"authToken"` + PassThrough bool `toml:"passThrough"` + ForceAllowApi bool `toml:"ForceAllowApi"` } type BlacklistConfig struct { diff --git a/config/config.toml b/config/config.toml index b539ce0..64ad1da 100644 --- a/config/config.toml +++ b/config/config.toml @@ -17,6 +17,9 @@ mode = "bypass" # bypass / cache smartGitAddr = "http://127.0.0.1:8080" ForceH2C = false +[shell] +editor = false + [pages] mode = "internal" # "internal" or "external" theme = "bootstrap" # "bootstrap" or "nebula" @@ -32,6 +35,7 @@ authMethod = "parameters" # "header" or "parameters" authToken = "token" enabled = false passThrough = false +ForceAllowApi = false [blacklist] blacklistFile = "/data/ghproxy/config/blacklist.json" diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 862743a..70af6ed 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -12,7 +12,7 @@ import ( "github.com/gin-gonic/gin" ) -func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) { +func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, matcher string) { method := c.Request.Method // 发送HEAD请求, 预获取Content-Length @@ -23,6 +23,7 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri } setRequestHeaders(c, headReq) removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) + reWriteEncodeHeader(headReq) AuthPassThrough(c, cfg, headReq) headResp, err := client.Do(headReq) @@ -64,6 +65,7 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri } setRequestHeaders(c, req) removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) + reWriteEncodeHeader(req) AuthPassThrough(c, cfg, req) resp, err := client.Do(req) @@ -106,6 +108,9 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri resp.Header.Del(header) } + //c.Header("Accept-Encoding", "gzip") + //c.Header("Content-Encoding", "gzip") + /* if cfg.CORS.Enabled { c.Header("Access-Control-Allow-Origin", "*") @@ -127,12 +132,30 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri c.Status(resp.StatusCode) - //_, err = io.CopyBuffer(c.Writer, resp.Body, nil) - _, err = copyb.CopyBuffer(c.Writer, resp.Body, nil) - if err != nil { - logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err) - return + if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor { + // 判断body是不是gzip + var compress string + if resp.Header.Get("Content-Encoding") == "gzip" { + compress = "gzip" + } + + logInfo("Is Shell: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto) + c.Header("Content-Length", "") + _, err = processLinks(resp.Body, c.Writer, compress, c.Request.Host, cfg) + if err != nil { + logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err) + return + } else { + c.Writer.Flush() // 确保刷入 + } } else { - c.Writer.Flush() // 确保刷入 + //_, err = io.CopyBuffer(c.Writer, resp.Body, nil) + _, err = copyb.CopyBuffer(c.Writer, resp.Body, nil) + if err != nil { + logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err) + return + } else { + c.Writer.Flush() // 确保刷入 + } } } diff --git a/proxy/handler.go b/proxy/handler.go index 2730226..6c8af38 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -1,6 +1,7 @@ package proxy import ( + "errors" "fmt" "ghproxy/auth" "ghproxy/config" @@ -65,89 +66,20 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 制作url rawPath = "https://" + matches[2] - var ( - user string - repo string - matcher string - ) - - // 匹配 "https://github.com"开头的链接 - if strings.HasPrefix(rawPath, "https://github.com") { - remainingPath := strings.TrimPrefix(rawPath, "https://github.com") - if strings.HasPrefix(remainingPath, "/") { - remainingPath = strings.TrimPrefix(remainingPath, "/") - } - // 预期格式/user/repo/more... - // 取出user和repo和最后部分 - parts := strings.Split(remainingPath, "/") - if len(parts) <= 2 { - logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto) + user, repo, matcher, err := Matcher(rawPath, cfg) + if err != nil { + if errors.Is(err, ErrInvalidURL) { c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath) + logWarning(err.Error()) return } - user = parts[0] - repo = parts[1] - // 匹配 "https://github.com"开头的链接 - if len(parts) >= 3 { - switch parts[2] { - case "releases", "archive": - matcher = "releases" - case "blob", "raw": - matcher = "blob" - case "info", "git-upload-pack": - matcher = "clone" - default: - fmt.Println("Invalid URL: Unknown type") - } - } - } - // 匹配 "https://raw"开头的链接 - if strings.HasPrefix(rawPath, "https://raw") { - remainingPath := strings.TrimPrefix(rawPath, "https://") - parts := strings.Split(remainingPath, "/") - if len(parts) <= 3 { - logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto) - c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath) - return - } - user = parts[1] - repo = parts[2] - matcher = "raw" - } - // 匹配 "https://gist"开头的链接 - if strings.HasPrefix(rawPath, "https://gist") { - remainingPath := strings.TrimPrefix(rawPath, "https://") - parts := strings.Split(remainingPath, "/") - if len(parts) <= 3 { - logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto) - c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath) - return - } - user = parts[1] - matcher = "gist" - } - // 匹配 "https://api.github.com/"开头的链接 - if strings.HasPrefix(rawPath, "https://api.github.com/") { - matcher = "api" - remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/") - - parts := strings.Split(remainingPath, "/") - if parts[0] == "repos" { - user = parts[1] - repo = parts[2] - } - if parts[0] == "users" { - user = parts[1] - } - if cfg.Auth.AuthMethod != "header" || !cfg.Auth.Enabled { - c.JSON(http.StatusForbidden, gin.H{"error": "HeaderAuth is not enabled."}) - logError("%s %s %s %s %s HeaderAuth-Error: HeaderAuth is not enabled.", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto) + if errors.Is(err, ErrAuthHeaderUnavailable) { + c.String(http.StatusForbidden, "AuthHeader Unavailable") + logWarning(err.Error()) return } } - username := user - //username, repo := MatchUserRepo(rawPath, cfg, c, matches) // 匹配用户名和仓库名 logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, username, repo) // dump log 记录详细信息 c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, full Header @@ -195,7 +127,8 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra } // 鉴权 - authcheck, err := auth.AuthHandler(c, cfg) + var authcheck bool + authcheck, err = auth.AuthHandler(c, cfg) if !authcheck { c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, err) @@ -207,8 +140,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra switch matcher { case "releases", "blob", "raw", "gist", "api": - //ProxyRequest(c, rawPath, cfg, "chrome", runMode) - ChunkedProxyRequest(c, rawPath, cfg, "chrome", runMode) // dev test chunk + ChunkedProxyRequest(c, rawPath, cfg, matcher) case "clone": //ProxyRequest(c, rawPath, cfg, "git", runMode) GitReq(c, rawPath, cfg, "git", runMode) diff --git a/proxy/httpc.go b/proxy/httpc.go index 4b78fc8..117f26b 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -35,20 +35,6 @@ func InitReq(cfg *config.Config) { } func initHTTPClient(cfg *config.Config) { - /* - ctr = &http.Transport{ - MaxIdleConns: 100, - MaxConnsPerHost: 60, - IdleConnTimeout: 20 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - ResponseHeaderTimeout: 10 * time.Second, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - } - */ var proTolcols = new(http.Protocols) proTolcols.SetHTTP1(true) proTolcols.SetHTTP2(true) diff --git a/proxy/matchrepo.go b/proxy/matchrepo.go index b69627e..7030ebe 100644 --- a/proxy/matchrepo.go +++ b/proxy/matchrepo.go @@ -1,34 +1,286 @@ package proxy import ( + "bufio" + "compress/gzip" "fmt" "ghproxy/config" - "net/http" + "io" "regexp" - - "github.com/gin-gonic/gin" + "strings" ) -// 预定义regex -var ( - pathRegex = regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`) // 匹配路径 - gistRegex = regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.*`) // 匹配gist路径 -) - -// 提取用户名和仓库名 -func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches []string) (string, string) { - if gistMatches := gistRegex.FindStringSubmatch(rawPath); len(gistMatches) == 3 { - logDump("%s %s %s %s %s Matched-Username: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, gistMatches[1]) - return gistMatches[1], "" - } - // 定义路径 - if pathMatches := pathRegex.FindStringSubmatch(matches[2]); len(pathMatches) >= 4 { - return pathMatches[2], pathMatches[3] - } - - // 返回错误信息 - errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto) - logWarning(errMsg) - c.String(http.StatusForbidden, "Invalid path; expected username/repo, Path: %s", rawPath) - return "", "" +// 定义错误类型, error承载描述, 便于处理 +type MatcherErrors struct { + Code int + Msg string + Err error +} + +var ( + ErrInvalidURL = &MatcherErrors{ + Code: 403, + Msg: "Invalid URL Format", + } + ErrAuthHeaderUnavailable = &MatcherErrors{ + Code: 403, + Msg: "AuthHeader Unavailable", + } +) + +func (e *MatcherErrors) Error() string { + if e.Err != nil { + return fmt.Sprintf("Code: %d, Msg: %s, Err: %s", e.Code, e.Msg, e.Err.Error()) + } + return fmt.Sprintf("Code: %d, Msg: %s", e.Code, e.Msg) +} + +func (e *MatcherErrors) Unwrap() error { + return e.Err +} + +func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) { + var ( + user string + repo string + matcher string + ) + // 匹配 "https://github.com"开头的链接 + if strings.HasPrefix(rawPath, "https://github.com") { + remainingPath := strings.TrimPrefix(rawPath, "https://github.com") + if strings.HasPrefix(remainingPath, "/") { + remainingPath = strings.TrimPrefix(remainingPath, "/") + } + // 预期格式/user/repo/more... + // 取出user和repo和最后部分 + parts := strings.Split(remainingPath, "/") + if len(parts) <= 2 { + return "", "", "", ErrInvalidURL + } + user = parts[0] + repo = parts[1] + // 匹配 "https://github.com"开头的链接 + if len(parts) >= 3 { + switch parts[2] { + case "releases", "archive": + matcher = "releases" + case "blob", "raw": + matcher = "blob" + case "info", "git-upload-pack": + matcher = "clone" + default: + return "", "", "", ErrInvalidURL + } + } + return user, repo, matcher, nil + } + // 匹配 "https://raw"开头的链接 + if strings.HasPrefix(rawPath, "https://raw") { + remainingPath := strings.TrimPrefix(rawPath, "https://") + parts := strings.Split(remainingPath, "/") + if len(parts) <= 3 { + return "", "", "", ErrInvalidURL + } + user = parts[1] + repo = parts[2] + matcher = "raw" + + return user, repo, matcher, nil + } + // 匹配 "https://gist"开头的链接 + if strings.HasPrefix(rawPath, "https://gist") { + remainingPath := strings.TrimPrefix(rawPath, "https://") + parts := strings.Split(remainingPath, "/") + if len(parts) <= 3 { + return "", "", "", ErrInvalidURL + } + user = parts[1] + repo = "" + matcher = "gist" + return user, repo, matcher, nil + } + // 匹配 "https://api.github.com/"开头的链接 + if strings.HasPrefix(rawPath, "https://api.github.com/") { + matcher = "api" + remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/") + + parts := strings.Split(remainingPath, "/") + if parts[0] == "repos" { + user = parts[1] + repo = parts[2] + } + if parts[0] == "users" { + user = parts[1] + } + if !cfg.Auth.ForceAllowApi { + if cfg.Auth.AuthMethod != "header" || !cfg.Auth.Enabled { + return "", "", "", ErrAuthHeaderUnavailable + } + } + return user, repo, matcher, nil + } + return "", "", "", ErrInvalidURL +} + +func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) { + var ( + matcher string + ) + // 匹配 "https://github.com"开头的链接 + if strings.HasPrefix(rawPath, "https://github.com") { + remainingPath := strings.TrimPrefix(rawPath, "https://github.com") + if strings.HasPrefix(remainingPath, "/") { + remainingPath = strings.TrimPrefix(remainingPath, "/") + } + return true, "", nil + } + // 匹配 "https://raw.githubusercontent.com"开头的链接 + if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") { + return true, matcher, nil + } + // 匹配 "https://raw.github.com"开头的链接 + if strings.HasPrefix(rawPath, "https://raw.github.com") { + return true, matcher, nil + } + // 匹配 "https://gist.githubusercontent.com"开头的链接 + if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") { + return true, matcher, nil + } + // 匹配 "https://gist.github.com"开头的链接 + if strings.HasPrefix(rawPath, "https://gist.github.com") { + return true, matcher, nil + } + // 匹配 "https://api.github.com/"开头的链接 + if strings.HasPrefix(rawPath, "https://api.github.com") { + matcher = "api" + return true, matcher, nil + } + return false, "", ErrInvalidURL +} + +// 匹配文件扩展名是sh的rawPath +func MatcherShell(rawPath string) bool { + if strings.HasSuffix(rawPath, ".sh") { + return true + } + return false +} + +// LinkProcessor 是一个函数类型,用于处理提取到的链接。 +type LinkProcessor func(string) string + +// 自定义 URL 修改函数 +func modifyURL(url string, host string, cfg *config.Config) string { + // 去除url内的https://或http:// + matched, _, err := EditorMatcher(url, cfg) + if err != nil { + logDump("Invalid URL: %s", url) + return url + } + if matched { + + u := strings.TrimPrefix(url, "https://") + u = strings.TrimPrefix(url, "http://") + logDump("Modified URL: %s", "https://"+host+"/"+u) + return "https://" + host + "/" + u + } + return url +} + +var ( + matchedMatchers = []string{ + "blob", + "raw", + "gist", + } +) + +// matchString 检查目标字符串是否在给定的字符串集合中 +func matchString(target string, stringsToMatch []string) bool { + matchMap := make(map[string]struct{}, len(stringsToMatch)) + for _, str := range stringsToMatch { + matchMap[str] = struct{}{} + } + _, exists := matchMap[target] + return exists +} + +// processLinks 处理链接并将结果写入输出流 +func processLinks(input io.Reader, output io.Writer, compress string, host string, cfg *config.Config) (written int64, err error) { + var reader *bufio.Reader + + if compress == "gzip" { + // 解压gzip + gzipReader, err := gzip.NewReader(input) + if err != nil { + return 0, fmt.Errorf("gzip解压错误: %v", err) + } + defer gzipReader.Close() + reader = bufio.NewReader(gzipReader) + } else { + reader = bufio.NewReader(input) + } + + var writer *bufio.Writer + var gzipWriter *gzip.Writer + + // 根据是否gzip确定 writer 的创建 + if compress == "gzip" { + gzipWriter = gzip.NewWriter(output) + writer = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小 + } else { + writer = bufio.NewWriterSize(output, 4096) + } + + //确保writer关闭 + defer func() { + var closeErr error // 局部变量,用于保存defer中可能发生的错误 + + if gzipWriter != nil { + if closeErr = gzipWriter.Close(); closeErr != nil { + logError("gzipWriter close failed %v", closeErr) + // 如果已经存在错误,则保留。否则,记录此错误。 + if err == nil { + err = closeErr + } + } + } + if flushErr := writer.Flush(); flushErr != nil { + logError("writer flush failed %v", flushErr) + // 如果已经存在错误,则保留。否则,记录此错误。 + if err == nil { + err = flushErr + } + } + }() + + // 使用正则表达式匹配 http 和 https 链接 + urlPattern := regexp.MustCompile(`https?://[^\s'"]+`) + for { + line, err := reader.ReadString('\n') + if err != nil { + if err == io.EOF { + break // 文件结束 + } + return written, fmt.Errorf("读取行错误: %v", err) // 传递错误 + } + + // 替换所有匹配的 URL + modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string { + return modifyURL(originalURL, host, cfg) + }) + + n, werr := writer.WriteString(modifiedLine) + written += int64(n) // 更新写入的字节数 + if werr != nil { + return written, fmt.Errorf("写入文件错误: %v", werr) // 传递错误 + } + } + + // 在返回之前,再刷新一次 + if fErr := writer.Flush(); fErr != nil { + return written, fErr + } + + return written, nil } diff --git a/proxy/reqheader.go b/proxy/reqheader.go index 07525d0..ce62b91 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -2,6 +2,7 @@ package proxy import ( "net/http" + "strings" "github.com/gin-gonic/gin" ) @@ -19,3 +20,31 @@ func removeWSHeader(req *http.Request) { req.Header.Del("Upgrade") req.Header.Del("Connection") } + +func reWriteEncodeHeader(req *http.Request) { + + if isGzipAccepted(req.Header) { + req.Header.Set("Content-Encoding", "gzip") + req.Header.Set("Accept-Encoding", "gzip") + } else { + req.Header.Del("Content-Encoding") + req.Header.Del("Accept-Encoding") + } + +} + +// isGzipAccepted 检查 Accept-Encoding 头部中是否包含 gzip +func isGzipAccepted(header http.Header) bool { + // 获取 Accept-Encoding 的值 + encodings := header["Accept-Encoding"] + for _, encoding := range encodings { + // 将 encoding 字符串拆分为多个编码 + for _, enc := range strings.Split(encoding, ",") { + // 去除空格并检查是否为 gzip + if strings.TrimSpace(enc) == "gzip" { + return true + } + } + } + return false +}