This commit is contained in:
WJQSERVER 2024-10-28 18:02:58 +08:00
parent 19f753a515
commit 92b3e19954
8 changed files with 70 additions and 50 deletions

View file

@ -1,5 +1,12 @@
# 更新日志 # 更新日志
24w20a
---
- PRE-RELEASE: 此版本是v1.6.2的预发布版本,请勿在生产环境中使用
- CHANGE: 大幅修改日志记录,对各个部分的日志记录进行统一格式,并对部分重复日志进行合并
- CHANGE: 大幅优化一键部署脚本,使其更加易用,并增加更多的功能(已于早些时候hotfix)
- CHANGE: 优化部分代码结构,提升性能
v1.6.1 v1.6.1
--- ---
- CHANGE: 根据社区建议,将`sizeLimit`由过去的以`byte`为单位,改为以`MB`为单位,以便于直观理解 - CHANGE: 根据社区建议,将`sizeLimit`由过去的以`byte`为单位,改为以`MB`为单位,以便于直观理解

View file

@ -1 +1 @@
24w19d 24w20a

View file

@ -57,7 +57,7 @@ git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
docker run -p 7210:80 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log -v ./ghproxy/config:/data/ghproxy/config --restart always wjqserver/ghproxy docker run -p 7210:80 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log -v ./ghproxy/config:/data/ghproxy/config --restart always wjqserver/ghproxy
``` ```
- Docker-Compose - Docker-Compose (建议使用)
参看[docker-compose.yml](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docker/compose/docker-compose.yml) 参看[docker-compose.yml](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docker/compose/docker-compose.yml)

View file

@ -7,7 +7,7 @@
| 版本 | 是否支持 | | 版本 | 是否支持 |
| --- | --- | | --- | --- |
| v1.x.x | :white_check_mark: | | v1.x.x | :white_check_mark: |
| **w**a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 | | 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
| v0.x.x | :x: 这些版本不再受支持 | | v0.x.x | :x: 这些版本不再受支持 |
### 用户须知 ### 用户须知

View file

@ -46,6 +46,7 @@ func InitHandleRouter(cfg *config.Config, router *gin.Engine) {
func SizeLimitHandler(cfg *config.Config, c *gin.Context) { func SizeLimitHandler(cfg *config.Config, c *gin.Context) {
sizeLimit := cfg.Server.SizeLimit sizeLimit := cfg.Server.SizeLimit
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json") c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"MaxResponseBodySize": sizeLimit, "MaxResponseBodySize": sizeLimit,
@ -53,6 +54,7 @@ func SizeLimitHandler(cfg *config.Config, c *gin.Context) {
} }
func WhiteListStatusHandler(c *gin.Context, cfg *config.Config) { func WhiteListStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json") c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Whitelist": cfg.Whitelist.Enabled, "Whitelist": cfg.Whitelist.Enabled,
@ -60,6 +62,7 @@ func WhiteListStatusHandler(c *gin.Context, cfg *config.Config) {
} }
func BlackListStatusHandler(c *gin.Context, cfg *config.Config) { func BlackListStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json") c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Blacklist": cfg.Blacklist.Enabled, "Blacklist": cfg.Blacklist.Enabled,
@ -67,6 +70,7 @@ func BlackListStatusHandler(c *gin.Context, cfg *config.Config) {
} }
func CorsStatusHandler(c *gin.Context, cfg *config.Config) { func CorsStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json") c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Cors": cfg.CORS.Enabled, "Cors": cfg.CORS.Enabled,
@ -74,6 +78,7 @@ func CorsStatusHandler(c *gin.Context, cfg *config.Config) {
} }
func HealthcheckHandler(c *gin.Context) { func HealthcheckHandler(c *gin.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json") c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Status": "OK", "Status": "OK",

View file

@ -1,6 +1,7 @@
package auth package auth
import ( import (
"fmt"
"ghproxy/config" "ghproxy/config"
"ghproxy/logger" "ghproxy/logger"
@ -26,27 +27,29 @@ func Init(cfg *config.Config) {
logInfo("Auth Init") logInfo("Auth Init")
} }
func AuthHandler(c *gin.Context, cfg *config.Config) bool { func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
// 如果身份验证未启用,直接返回 true // 如果身份验证未启用,直接返回 true
if !cfg.Auth.Enabled { if !cfg.Auth.Enabled {
return true return true, ""
} }
// 获取 auth_token 参数 // 获取 auth_token 参数
authToken := c.Query("auth_token") authToken := c.Query("auth_token")
logInfo("auth_token received: %s", authToken) // IP METHOD URL USERAGENT PROTO TOKEN
logInfo("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto, authToken)
// 验证 token // 验证 token
if authToken == "" { if authToken == "" {
logWarning("auth FAILED: no auth_token provided") err := "Auth token == nil"
return false return false, err
} }
isValid := authToken == cfg.Auth.AuthToken isValid = authToken == cfg.Auth.AuthToken
if !isValid { if !isValid {
logWarning("auth FAILED: invalid auth_token: %s", authToken) err := fmt.Sprintf("Auth token incorrect: %s", authToken)
return false, err
} }
logInfo("auth SUCCESS: %t", isValid) logInfo("auth SUCCESS: %t", isValid)
return isValid return isValid, ""
} }

14
main.go
View file

@ -22,7 +22,6 @@ var (
cfgfile string cfgfile string
) )
// 日志模块
var ( var (
logw = logger.Logw logw = logger.Logw
logInfo = logger.LogInfo logInfo = logger.LogInfo
@ -36,7 +35,6 @@ func readFlag() {
func loadConfig() { func loadConfig() {
var err error var err error
// 初始化配置
cfg, err = config.LoadConfig(cfgfile) cfg, err = config.LoadConfig(cfgfile)
if err != nil { if err != nil {
log.Fatalf("Failed to load config: %v", err) log.Fatalf("Failed to load config: %v", err)
@ -46,9 +44,8 @@ func loadConfig() {
} }
func setupLogger(cfg *config.Config) { func setupLogger(cfg *config.Config) {
// 初始化日志模块
var err error var err error
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize) // 传递日志文件路径 err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
if err != nil { if err != nil {
log.Fatalf("Failed to initialize logger: %v", err) log.Fatalf("Failed to initialize logger: %v", err)
} }
@ -89,8 +86,8 @@ func init() {
router.StaticFile("/favicon.ico", faviconPath) router.StaticFile("/favicon.ico", faviconPath)
} else if !cfg.Pages.Enabled { } else if !cfg.Pages.Enabled {
router.GET("/", func(c *gin.Context) { router.GET("/", func(c *gin.Context) {
c.String(http.StatusForbidden, "403 Forbidden This route is not allowed to access.") c.String(http.StatusForbidden, "403 Forbidden Access")
logWarning("Forbidden: IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto) logWarning("403 > Path:/ IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto)
}) })
} }
@ -100,11 +97,10 @@ func init() {
} }
func main() { func main() {
// 启动服务器
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)) err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port))
if err != nil { if err != nil {
logError("Error starting server: %v\n", err) logError("Failed to start server: %v\n", err)
} }
fmt.Println("Program finished") fmt.Println("Program Exit")
} }

View file

@ -1,4 +1,3 @@
// proxy/proxy.go 实验性
package proxy package proxy
import ( import (
@ -40,8 +39,9 @@ func NoRouteHandler(cfg *config.Config) gin.HandlerFunc {
matches := re.FindStringSubmatch(rawPath) matches := re.FindStringSubmatch(rawPath)
if len(matches) < 3 { if len(matches) < 3 {
logWarning("Invalid URL: %s", rawPath) 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)
c.String(http.StatusForbidden, "Invalid URL.") logWarning(errMsg)
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
return return
} }
@ -49,34 +49,37 @@ func NoRouteHandler(cfg *config.Config) gin.HandlerFunc {
username, repo := MatchUserRepo(rawPath, cfg, c, matches) username, repo := MatchUserRepo(rawPath, cfg, c, matches)
logWarning("Blacklist Check > Username: %s, Repo: %s", username, repo) 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)
fullrepo := fmt.Sprintf("%s/%s", username, repo) fullrepo := fmt.Sprintf("%s/%s", username, repo)
// 白名单检查 // 白名单检查
if cfg.Whitelist.Enabled { if cfg.Whitelist.Enabled {
whitelistpass := auth.CheckWhitelist(fullrepo) whitelist := auth.CheckWhitelist(fullrepo)
if !whitelistpass { if !whitelist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, fullrepo)
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", fullrepo) errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", fullrepo)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg}) c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
logWarning(errMsg) logWarning(logErrMsg)
return return
} }
} }
// 黑名单检查 // 黑名单检查
if cfg.Blacklist.Enabled { if cfg.Blacklist.Enabled {
blacklistpass := auth.CheckBlacklist(fullrepo) blacklist := auth.CheckBlacklist(fullrepo)
if blacklistpass { if blacklist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, fullrepo)
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", fullrepo) errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", fullrepo)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg}) c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
logWarning(errMsg) logWarning(logErrMsg)
return return
} }
} }
matches = CheckURL(rawPath) matches = CheckURL(rawPath, c)
if matches == nil { if matches == nil {
c.AbortWithStatus(http.StatusNotFound) c.AbortWithStatus(http.StatusNotFound)
logError("%s %s %s %s %s 404-NOMATCH", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
return return
} }
@ -84,13 +87,16 @@ func NoRouteHandler(cfg *config.Config) gin.HandlerFunc {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
} }
if !auth.AuthHandler(c, cfg) { // 鉴权
authcheck, err := auth.AuthHandler(c, cfg)
if !authcheck {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
logWarning("Unauthorized request: %s", rawPath) 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)
return return
} }
logInfo("Matches: %v", matches) // IP METHOD URL USERAGENT PROTO MATCHES
logInfo("%s %s %s %s %s Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, matches)
switch { switch {
case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath): case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath):
@ -99,6 +105,7 @@ func NoRouteHandler(cfg *config.Config) gin.HandlerFunc {
ProxyRequest(c, rawPath, cfg, "git") ProxyRequest(c, rawPath, cfg, "git")
default: default:
c.String(http.StatusForbidden, "Invalid input.") c.String(http.StatusForbidden, "Invalid input.")
fmt.Println("Invalid input.")
return return
} }
} }
@ -110,24 +117,24 @@ func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches [
var gistmatches []string var gistmatches []string
if gistregex.MatchString(rawPath) { if gistregex.MatchString(rawPath) {
gistmatches = gistregex.FindStringSubmatch(rawPath) gistmatches = gistregex.FindStringSubmatch(rawPath)
logInfo("Gist Matched > Username: %s, URL: %s", gistmatches[1], rawPath) logInfo("%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], "" return gistmatches[1], ""
} }
// 定义路径匹配的正则表达式 // 定义路径
pathRegex := regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`) pathRegex := regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`)
if pathMatches := pathRegex.FindStringSubmatch(matches[2]); len(pathMatches) >= 4 { if pathMatches := pathRegex.FindStringSubmatch(matches[2]); len(pathMatches) >= 4 {
return pathMatches[2], pathMatches[3] return pathMatches[2], pathMatches[3]
} }
// 返回错误信息 // 返回错误信息
logWarning("Invalid path: %s", rawPath) 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)
c.String(http.StatusForbidden, "Invalid path; expected username/repo.") logWarning(errMsg)
c.String(http.StatusForbidden, "Invalid path; expected username/repo, Path: %s", rawPath)
return "", "" return "", ""
} }
func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) { func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) {
method := c.Request.Method method := c.Request.Method
// 记录日志 IP 地址、请求方法、请求 URL、请求头 User-Agent 、HTTP版本
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
client := createHTTPClient(mode) client := createHTTPClient(mode)
@ -141,7 +148,7 @@ func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) {
req := client.R().SetBody(body) req := client.R().SetBody(body)
setRequestHeaders(c, req) setRequestHeaders(c, req)
resp, err := SendRequest(req, method, u) resp, err := SendRequest(c, req, method, u)
if err != nil { if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return return
@ -149,17 +156,18 @@ func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) {
defer resp.Body.Close() defer resp.Body.Close()
if err := HandleResponseSize(resp, cfg, c); err != nil { if err := HandleResponseSize(resp, cfg, c); err != nil {
logWarning("Error handling response size: %v", err) logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return return
} }
CopyResponseHeaders(resp, c, cfg) CopyResponseHeaders(resp, c, cfg)
c.Status(resp.StatusCode) c.Status(resp.StatusCode)
if err := copyResponseBody(c, resp.Body); err != nil { if err := copyResponseBody(c, resp.Body); err != nil {
logError("Failed to copy response body: %v", err) logError("%s %s %s %s %s Response-Copy-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
} }
} }
// 判断并选择TLS指纹
func createHTTPClient(mode string) *req.Client { func createHTTPClient(mode string) *req.Client {
client := req.C() client := req.C()
switch mode { switch mode {
@ -198,7 +206,7 @@ func copyResponseBody(c *gin.Context, respBody io.Reader) error {
return err return err
} }
func SendRequest(req *req.Request, method, url string) (*req.Response, error) { func SendRequest(c *gin.Context, req *req.Request, method, url string) (*req.Response, error) {
switch method { switch method {
case "GET": case "GET":
return req.Get(url) return req.Get(url)
@ -209,8 +217,10 @@ func SendRequest(req *req.Request, method, url string) (*req.Response, error) {
case "DELETE": case "DELETE":
return req.Delete(url) return req.Delete(url)
default: default:
logInfo("Unsupported method: %s", method) // IP METHOD URL USERAGENT PROTO UNSUPPORTED-METHOD
return nil, fmt.Errorf("unsupported method: %s", method) errmsg := fmt.Sprintf("%s %s %s %s %s Unsupported method", c.ClientIP(), method, url, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errmsg)
return nil, fmt.Errorf(errmsg)
} }
} }
@ -222,8 +232,8 @@ func HandleResponseSize(resp *req.Response, cfg *config.Config, c *gin.Context)
if err == nil && size > sizelimit { if err == nil && size > sizelimit {
finalURL := resp.Request.URL.String() finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL) c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("Size limit exceeded: %s, Size: %d", finalURL, size) logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return fmt.Errorf("size limit exceeded: %d", size) return fmt.Errorf("Path: %s size limit exceeded: %d", finalURL, size)
} }
} }
return nil return nil
@ -282,14 +292,13 @@ func HandleError(c *gin.Context, message string) {
logWarning(message) logWarning(message)
} }
func CheckURL(u string) []string { func CheckURL(u string, c *gin.Context) []string {
for _, exp := range exps { for _, exp := range exps {
if matches := exp.FindStringSubmatch(u); matches != nil { if matches := exp.FindStringSubmatch(u); matches != nil {
logInfo("URL matched: %s, Matches: %v", u, matches[1:])
return matches[1:] return matches[1:]
} }
} }
errMsg := fmt.Sprintf("Invalid URL: %s", u) errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errMsg) logWarning(errMsg)
return nil return nil
} }