From 36646ebf7e36c0fb673ed9788e4a58ac1c80f580 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:17:08 +0800 Subject: [PATCH 001/118] 25w26a --- DEV-VERSION | 2 +- VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index 1ca6842..ae7572d 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -e3.0.3 \ No newline at end of file +25w26a \ No newline at end of file diff --git a/VERSION b/VERSION index e4887ac..56fea8a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -e3.0.7 \ No newline at end of file +3.0.0 \ No newline at end of file From 1f0b43ec43bdbd5c268200107fbaf3e5f2b61744 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:20:28 +0800 Subject: [PATCH 002/118] fix docker build issue --- docker/dockerfile/dev/Dockerfile | 1 - docker/dockerfile/release/Dockerfile | 1 - 2 files changed, 2 deletions(-) diff --git a/docker/dockerfile/dev/Dockerfile b/docker/dockerfile/dev/Dockerfile index d146220..6821a3d 100644 --- a/docker/dockerfile/dev/Dockerfile +++ b/docker/dockerfile/dev/Dockerfile @@ -21,7 +21,6 @@ RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \ tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \ rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/docker/dockerfile//init.sh # 拉取配置 RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/config.toml diff --git a/docker/dockerfile/release/Dockerfile b/docker/dockerfile/release/Dockerfile index 7bfdd9f..c2c5689 100644 --- a/docker/dockerfile/release/Dockerfile +++ b/docker/dockerfile/release/Dockerfile @@ -21,7 +21,6 @@ RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \ tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \ rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/docker/dockerfile/release/init.sh # 拉取配置 RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/config.toml From d3520a2133ebbb762b5e919ef787991eedea504b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:20:41 +0800 Subject: [PATCH 003/118] update --- DEV-VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEV-VERSION b/DEV-VERSION index ae7572d..d2f78c4 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w26a \ No newline at end of file +25w26a From 886c99f53ddd082c40de6ec05f89e27d87132e0f Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:23:16 +0800 Subject: [PATCH 004/118] update --- DEV-VERSION | 2 +- docker/dockerfile/dev/Dockerfile | 1 - docker/dockerfile/release/Dockerfile | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index d2f78c4..ae7572d 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w26a +25w26a \ No newline at end of file diff --git a/docker/dockerfile/dev/Dockerfile b/docker/dockerfile/dev/Dockerfile index 6821a3d..5248916 100644 --- a/docker/dockerfile/dev/Dockerfile +++ b/docker/dockerfile/dev/Dockerfile @@ -29,7 +29,6 @@ RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.co # 权限 RUN chmod +x /data/${APPLICATION}/${APPLICATION} -RUN chmod +x /usr/local/bin/init.sh FROM alpine:latest diff --git a/docker/dockerfile/release/Dockerfile b/docker/dockerfile/release/Dockerfile index c2c5689..0e8c630 100644 --- a/docker/dockerfile/release/Dockerfile +++ b/docker/dockerfile/release/Dockerfile @@ -29,7 +29,6 @@ RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.co # 权限 RUN chmod +x /data/${APPLICATION}/${APPLICATION} -RUN chmod +x /usr/local/bin/init.sh FROM alpine:latest From 3da54f05998d1566e8a2159e95e18eaf929dea43 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:28:00 +0800 Subject: [PATCH 005/118] update changelog --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844af8c..f7d0c6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # 更新日志 +3.0.0 - 2025-04-04 +--- +- RELEASE: Next Gen; 下一个起点; +- CHANGE: 使用HertZ框架重构, 提升性能 +- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题 +- CHANGE: 加入`Mino`主题对接选项 +- FIX: 修正部分日志输出问题 +- CHANGE: 移除gin残留 +- CHANGE: 移除无用传入参数, 调整代码结构 +- CHANGE: 改进cli +- CHANGE: 改进`脚本嵌套加速处理器` +- CHANGE&FIX: 使用`c.SetBodyStream`方式, 修正此前`chunked`传输中存在的诸多问题, 参看[HertZ Issues #1309](https://github.com/cloudwego/hertz/issues/1309) +- PORT: 从v2移植`matcher`相关改进 +- CHANGE: 增加默认配置生成 +- CHANGE: 优化前端资源加载 +- CHANGE: 将`cfg`flag改为`c`以符合`POSIX`规范 +- CHANGE: 为`smart-git`添加`no-cache`标头 + 25w26a - 2025-04-03 --- - PRE-RELEASE: 此版本是v3的预发布版本,请勿在生产环境中使用; From c522eba7ae9640e4877096beba08541a2441f160 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 4 Apr 2025 16:40:34 +0800 Subject: [PATCH 006/118] update --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a02dfb..2200ead 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -141,4 +141,4 @@ jobs: push: true tags: | ${{ env.IMAGE_NAME }}:${{ env.VERSION }} - ${{ env.IMAGE_NAME }}:v3 + ${{ env.IMAGE_NAME }}:latest From d62a1f976934de827b1d2f07fc3e32ae6e8b1fd3 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:33:47 +0800 Subject: [PATCH 007/118] [docs] update config docs --- docs/config.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/config.md b/docs/config.md index a9cb18f..ccd1ce2 100644 --- a/docs/config.md +++ b/docs/config.md @@ -13,6 +13,7 @@ host = "0.0.0.0" port = 8080 sizeLimit = 125 # MB +memLimit = 0 # MB H2C = true cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; debug = false @@ -41,6 +42,7 @@ staticDir = "/data/www" logFilePath = "/data/ghproxy/log/ghproxy.log" maxLogSize = 5 # MB level = "info" # dump, debug, info, warn, error, none +hertzLogPath = "/data/ghproxy/log/hertz.log" [auth] method = "parameters" # "header" or "parameters" @@ -85,6 +87,10 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" * 类型: 整数 (`int`) * 默认值: `125` (MB) * 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。 + * `memLimit`: `runtime`内存限制 + * 类型: 整数 (`int64`) + * 默认值: `0` (不传入) + * 说明: 给`runtime`的指标, 让gc行为更高效 * `H2C`: 是否启用 H2C (HTTP/2 Cleartext) 传输。 * 类型: 布尔值 (`bool`) * 默认值: `true` (启用) @@ -193,6 +199,10 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" * `"warn"`: 输出警告和错误日志。 * `"error"`: 仅输出错误日志。 * `"none"`: 禁用所有日志输出。 + * `hertzLogPath`: `HertZ`日志文件路径。 + * 类型: 字符串 (`string`) + * 默认值: `"/data/ghproxy/log/hertz.log"` + * 说明: 设置 `HertZ` 日志文件的存储路径。 * **`[auth]` - 认证配置** From dcc50401c4806dee4830dce5b9b257435c1fcf7d Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:34:00 +0800 Subject: [PATCH 008/118] update deps --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index fe29791..9b03803 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/gopkg v0.1.4 // indirect github.com/cloudwego/netpoll v0.7.0 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/nyaruka/phonenumbers v1.6.0 // indirect @@ -30,9 +30,9 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/arch v0.15.0 // indirect + golang.org/x/arch v0.16.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index f8d62b2..730be7f 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= @@ -81,8 +81,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= -golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= +golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= @@ -113,8 +113,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -127,8 +127,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 8a50b311fcae046be5f4eb80f1aa4b54c720934a Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 7 Apr 2025 18:51:22 +0800 Subject: [PATCH 009/118] 25w27a --- CHANGELOG.md | 7 +++++++ DEV-VERSION | 2 +- config/config.go | 16 ++++++++++------ config/config.toml | 2 ++ deploy/config.toml | 2 ++ main.go | 47 +++++++++++++++++++++++++++++++++++++++------- proxy/chunkreq.go | 1 + 7 files changed, 63 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d0c6f..8b7bbb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +25w27a - 2025-04-07 +--- +- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用; +- CHANGE: 加入`memLimit`指示gc +- CHANGE: 加入`hlog`输出路径配置 +- CHANGE: 修正H2C配置问题 + 3.0.0 - 2025-04-04 --- - RELEASE: Next Gen; 下一个起点; diff --git a/DEV-VERSION b/DEV-VERSION index ae7572d..03ab3b3 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w26a \ No newline at end of file +25w27a \ No newline at end of file diff --git a/config/config.go b/config/config.go index 1ec7707..1bf1bdc 100644 --- a/config/config.go +++ b/config/config.go @@ -32,6 +32,7 @@ type ServerConfig struct { Port int `toml:"port"` Host string `toml:"host"` SizeLimit int `toml:"sizeLimit"` + MemLimit int64 `toml:"memLimit"` H2C bool `toml:"H2C"` Cors string `toml:"cors"` Debug bool `toml:"debug"` @@ -86,9 +87,10 @@ type PagesConfig struct { } type LogConfig struct { - LogFilePath string `toml:"logFilePath"` - MaxLogSize int `toml:"maxLogSize"` - Level string `toml:"level"` + LogFilePath string `toml:"logFilePath"` + MaxLogSize int `toml:"maxLogSize"` + Level string `toml:"level"` + HertZLogPath string `toml:"hertzLogPath"` } /* @@ -179,6 +181,7 @@ func DefaultConfig() *Config { Port: 8080, Host: "0.0.0.0", SizeLimit: 125, + MemLimit: 0, H2C: true, Cors: "*", Debug: false, @@ -204,9 +207,10 @@ func DefaultConfig() *Config { StaticDir: "/data/www", }, Log: LogConfig{ - LogFilePath: "/data/ghproxy/log/ghproxy.log", - MaxLogSize: 10, - Level: "info", + LogFilePath: "/data/ghproxy/log/ghproxy.log", + MaxLogSize: 10, + Level: "info", + HertZLogPath: "/data/ghproxy/log/hertz.log", }, Auth: AuthConfig{ Enabled: false, diff --git a/config/config.toml b/config/config.toml index ef27c6d..92b81f8 100644 --- a/config/config.toml +++ b/config/config.toml @@ -2,6 +2,7 @@ host = "0.0.0.0" port = 8080 sizeLimit = 125 # MB +memLimit = 0 # MB H2C = true cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; debug = false @@ -30,6 +31,7 @@ staticDir = "/data/www" logFilePath = "/data/ghproxy/log/ghproxy.log" maxLogSize = 5 # MB level = "info" # dump, debug, info, warn, error, none +hertzLogPath = "/data/ghproxy/log/hertz.log" [auth] method = "parameters" # "header" or "parameters" diff --git a/deploy/config.toml b/deploy/config.toml index 1c7e69a..a25acf1 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -2,6 +2,7 @@ host = "127.0.0.1" port = 8080 sizeLimit = 125 # MB +memLimit = 0 # MB H2C = true cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; debug = false @@ -30,6 +31,7 @@ staticDir = "/usr/local/ghproxy/pages" logFilePath = "/usr/local/ghproxy/log/ghproxy.log" maxLogSize = 5 # MB level = "info" # dump, debug, info, warn, error, none +hertzLogPath = "/usr/local/ghproxy/log/hertz.log" [auth] authMethod = "parameters" # "header" or "parameters" diff --git a/main.go b/main.go index aabb054..c59293b 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "io/fs" "net/http" "os" + "runtime/debug" "time" "ghproxy/api" @@ -23,6 +24,7 @@ import ( "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery" "github.com/cloudwego/hertz/pkg/app/server" "github.com/cloudwego/hertz/pkg/common/adaptor" + "github.com/cloudwego/hertz/pkg/common/hlog" "github.com/hertz-contrib/http2/factory" ) @@ -31,6 +33,7 @@ var ( cfg *config.Config r *server.Hertz configfile = "/data/ghproxy/config/config.toml" + hertZfile *os.File cfgfile string version string runMode string @@ -129,7 +132,29 @@ func setupLogger(cfg *config.Config) { fmt.Printf("Log Level: %s\n", cfg.Log.Level) logDebug("Config File Path: ", cfgfile) logDebug("Loaded config: %v\n", cfg) - logInfo("Init Completed") + logInfo("Logger Initialized Successfully") +} + +func setupHertZLogger(cfg *config.Config) { + var err error + + if cfg.Log.HertZLogPath != "" { + hertZfile, err = os.OpenFile(cfg.Log.HertZLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + fmt.Printf("Failed to open log file: %v\n", err) + os.Exit(1) + } + + hlog.SetOutput(hertZfile) + } + +} + +func setMemLimit(cfg *config.Config) { + if cfg.Server.MemLimit > 0 { + debug.SetMemoryLimit((cfg.Server.MemLimit) * 1024 * 1024) + logInfo("Set Memory Limit to %d MB", cfg.Server.MemLimit) + } } func loadlist(cfg *config.Config) { @@ -315,7 +340,9 @@ func init() { loadConfig() if cfg != nil { // 在setupLogger前添加空值检查 setupLogger(cfg) + setupHertZLogger(cfg) InitReq(cfg) + setMemLimit(cfg) loadlist(cfg) setupRateLimit(cfg) @@ -346,12 +373,17 @@ func main() { addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) - r := server.New( - server.WithHostPorts(addr), - server.WithH2C(true), - ) - - r.AddProtocol("h2", factory.NewServerFactory()) + if cfg.Server.H2C { + r = server.New( + server.WithHostPorts(addr), + server.WithH2C(true), + ) + r.AddProtocol("h2", factory.NewServerFactory()) + } else { + r = server.New( + server.WithHostPorts(addr), + ) + } // 添加Recovery中间件 r.Use(recovery.Recovery()) @@ -414,5 +446,6 @@ func main() { r.Spin() defer logger.Close() + defer hertZfile.Close() fmt.Println("Program Exit") } diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index dd2b1ee..0212e0d 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -70,6 +70,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c // 错误处理(404) if resp.StatusCode == 404 { c.String(http.StatusNotFound, "File Not Found") + //c.Status(http.StatusNotFound) return } From 3e9e43cd44260e57895d1b0a3fbfbacf0a7abe38 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:48:56 +0800 Subject: [PATCH 010/118] 3.0.1 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- main.go | 17 ++++++++++++----- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b7bbb4..979cb00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +3.0.1 -2025-04-08 +--- +- CHANGE: 加入`memLimit`指示gc +- CHANGE: 加入`hlog`输出路径配置 +- CHANGE: 修正H2C配置问题 + 25w27a - 2025-04-07 --- - PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 56fea8a..13d683c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.0 \ No newline at end of file +3.0.1 \ No newline at end of file diff --git a/main.go b/main.go index c59293b..cd21df2 100644 --- a/main.go +++ b/main.go @@ -141,11 +141,11 @@ func setupHertZLogger(cfg *config.Config) { if cfg.Log.HertZLogPath != "" { hertZfile, err = os.OpenFile(cfg.Log.HertZLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { - fmt.Printf("Failed to open log file: %v\n", err) - os.Exit(1) + hlog.SetOutput(os.Stdout) + logWarning("Failed to open hertz log file: %v", err) + } else { + hlog.SetOutput(hertZfile) } - - hlog.SetOutput(hertZfile) } } @@ -446,6 +446,13 @@ func main() { r.Spin() defer logger.Close() - defer hertZfile.Close() + defer func() { + if hertZfile != nil { + err := hertZfile.Close() + if err != nil { + logError("Failed to close hertz log file: %v", err) + } + } + }() fmt.Println("Program Exit") } From a85eb38de5bb3a387ebb61c6524d445015fd0ee9 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 8 Apr 2025 20:50:55 +0800 Subject: [PATCH 011/118] update deps --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9b03803..5fa7ac0 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/cloudwego/hertz v0.9.6 github.com/hertz-contrib/http2 v0.1.8 github.com/satomitouka/touka-httpc v0.3.3 - golang.org/x/net v0.38.0 + golang.org/x/net v0.39.0 golang.org/x/time v0.11.0 ) diff --git a/go.sum b/go.sum index 730be7f..d8e9b6a 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 005a4543d4a2947f198c51414b85eaf89d8cbd04 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 10 Apr 2025 23:07:13 +0800 Subject: [PATCH 012/118] update deps --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 5fa7ac0..2c6514f 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.24.2 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 - github.com/cloudwego/hertz v0.9.6 + github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 - github.com/satomitouka/touka-httpc v0.3.3 + github.com/satomitouka/touka-httpc v0.4.0 golang.org/x/net v0.39.0 golang.org/x/time v0.11.0 ) @@ -31,7 +31,7 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/arch v0.16.0 // indirect - golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect diff --git a/go.sum b/go.sum index d8e9b6a..b036c8b 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50= github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= -github.com/cloudwego/hertz v0.9.6 h1:Kj5SSPlKBC32NIN7+B/tt8O1pdDz8brMai00rqqjULQ= -github.com/cloudwego/hertz v0.9.6/go.mod h1:X5Ez52XhtszU4t+CTBGIJI4PqmcI1oSf8ULBz0SWfLo= +github.com/cloudwego/hertz v0.9.7 h1:tAVaiO+vTf+ZkQhvNhKbDJ0hmC4oJ7bzwDi1KhvhHy4= +github.com/cloudwego/hertz v0.9.7/go.mod h1:t6d7NcoQxPmETvzPMMIVPHMn5C5QzpqIiFsaavoLJYQ= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4= github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= @@ -50,8 +50,8 @@ github.com/nyaruka/phonenumbers v1.6.0 h1:r9ax45fFg+YLUs2X4bNXm5RAxWl00hYjFgNlv3 github.com/nyaruka/phonenumbers v1.6.0/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/satomitouka/touka-httpc v0.3.3 h1:Th0uJ5do3oqqZgdUDtqD1SH11x8TcJmrwHMJQlEIKCg= -github.com/satomitouka/touka-httpc v0.3.3/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA= +github.com/satomitouka/touka-httpc v0.4.0 h1:cnOONdyJHJImMY8L64bvYF+7Ow/5CPf2Yr3RQRRMZOU= +github.com/satomitouka/touka-httpc v0.4.0/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -87,8 +87,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= -golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= From d6d54b222f1f6879b9a2c97fdd376dfc888c5509 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 10 Apr 2025 23:07:48 +0800 Subject: [PATCH 013/118] dix auth checker --- proxy/handler.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/proxy/handler.go b/proxy/handler.go index 072fd26..f8b998c 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -106,13 +106,15 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra } // 鉴权 - var authcheck bool - authcheck, err = auth.AuthHandler(ctx, c, cfg) - if !authcheck { - //c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) - c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"}) - logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) - return + if cfg.Auth.Enabled { + var authcheck bool + authcheck, err = auth.AuthHandler(ctx, c, cfg) + if !authcheck { + //c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) + c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"}) + logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) + return + } } // IP METHOD URL USERAGENT PROTO MATCHES From 8af107c5843fe9c30ee33446637cc33f6557a34e Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 11 Apr 2025 07:24:50 +0800 Subject: [PATCH 014/118] update for touka-httpc 0.4.0 --- CHANGELOG.md | 7 ++++++- DEV-VERSION | 2 +- main.go | 5 +++++ proxy/httpc.go | 43 ++++++++++++++++++++++++++++++------------- 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 979cb00..bc0931e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # 更新日志 -3.0.1 -2025-04-08 +25w28t-1 - 2025-04-11 +--- +- TEST: 测试验证版本 +- CHANGE: 更新httpc 0.4.0 + +3.0.1 - 2025-04-08 --- - CHANGE: 加入`memLimit`指示gc - CHANGE: 加入`hlog`输出路径配置 diff --git a/DEV-VERSION b/DEV-VERSION index 03ab3b3..42eb3c3 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w27a \ No newline at end of file +25w28t-1 \ No newline at end of file diff --git a/main.go b/main.go index cd21df2..d766a9f 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,8 @@ import ( "github.com/cloudwego/hertz/pkg/common/adaptor" "github.com/cloudwego/hertz/pkg/common/hlog" + //"github.com/cloudwego/hertz/pkg/network/standard" + "github.com/hertz-contrib/http2/factory" ) @@ -146,6 +148,7 @@ func setupHertZLogger(cfg *config.Config) { } else { hlog.SetOutput(hertZfile) } + hlog.SetLevel(hlog.LevelInfo) } } @@ -377,6 +380,8 @@ func main() { r = server.New( server.WithHostPorts(addr), server.WithH2C(true), + // server.WithALPN(true), + // server.WithTransport(standard.NewTransporter), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { diff --git a/proxy/httpc.go b/proxy/httpc.go index 117f26b..80e4510 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -87,23 +87,12 @@ func initHTTPClient(cfg *config.Config) { func initGitHTTPClient(cfg *config.Config) { - var proTolcols = new(http.Protocols) - proTolcols.SetHTTP1(true) - proTolcols.SetHTTP2(true) - proTolcols.SetUnencryptedHTTP2(true) - if cfg.GitClone.ForceH2C { - proTolcols.SetHTTP1(false) - proTolcols.SetHTTP2(false) - proTolcols.SetUnencryptedHTTP2(true) - } if cfg.Httpc.Mode == "auto" { - gittr = &http.Transport{ //MaxIdleConns: 160, IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB - Protocols: proTolcols, } } else if cfg.Httpc.Mode == "advanced" { gittr = &http.Transport{ @@ -112,7 +101,6 @@ func initGitHTTPClient(cfg *config.Config) { MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB - Protocols: proTolcols, } } else { // 错误的模式 @@ -130,14 +118,43 @@ func initGitHTTPClient(cfg *config.Config) { if cfg.Outbound.Enabled { initTransport(cfg, gittr) } - if cfg.Server.Debug { + if cfg.Server.Debug && cfg.GitClone.ForceH2C { gitclient = httpc.New( httpc.WithTransport(gittr), httpc.WithDumpLog(), + httpc.WithProtocols(httpc.ProtocolsConfig{ + Http1: false, + Http2: false, + Http2_Cleartext: true, + }), + ) + } else if !cfg.Server.Debug && cfg.GitClone.ForceH2C { + gitclient = httpc.New( + httpc.WithTransport(gittr), + httpc.WithProtocols(httpc.ProtocolsConfig{ + Http1: false, + Http2: false, + Http2_Cleartext: true, + }), + ) + } else if cfg.Server.Debug && !cfg.GitClone.ForceH2C { + gitclient = httpc.New( + httpc.WithTransport(gittr), + httpc.WithDumpLog(), + httpc.WithProtocols(httpc.ProtocolsConfig{ + Http1: true, + Http2: true, + Http2_Cleartext: true, + }), ) } else { gitclient = httpc.New( httpc.WithTransport(gittr), + httpc.WithProtocols(httpc.ProtocolsConfig{ + Http1: true, + Http2: true, + Http2_Cleartext: true, + }), ) } } From bf21bd197ae12c0711647345c579fef37775068a Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 11 Apr 2025 07:29:03 +0800 Subject: [PATCH 015/118] 25w28t-2 --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- proxy/chunkreq.go | 12 +++++++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc0931e..74a19fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w28t-2 - 2025-04-11 +--- +- TEST: 测试验证版本 +- CHANGE: 为老旧的油猴xhr提供兼容性支持, break RFC 2616, RFC 9112 + 25w28t-1 - 2025-04-11 --- - TEST: 测试验证版本 diff --git a/DEV-VERSION b/DEV-VERSION index 42eb3c3..c774704 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w28t-1 \ No newline at end of file +25w28t-2 \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 0212e0d..dffb6ef 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -74,13 +74,15 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } + var bodySize int contentLength = resp.Header.Get("Content-Length") if contentLength != "" { - size, err := strconv.Atoi(contentLength) - if err == nil && size > sizelimit { + var err error + bodySize, err = strconv.Atoi(contentLength) + if err == nil && bodySize > sizelimit { finalURL := resp.Request.URL.String() c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) - logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, size) + logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize) return } } @@ -132,6 +134,10 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } } else { + if contentLength != "" { + c.SetBodyStream(resp.Body, bodySize) + return + } c.SetBodyStream(resp.Body, -1) } From ab77c5c7da250ee2e88539362902a8af5912d1b5 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:27:12 +0800 Subject: [PATCH 016/118] 25w28a --- CHANGELOG.md | 9 ++++++++- DEV-VERSION | 2 +- main.go | 1 - proxy/chunkreq.go | 49 ++++++++++++----------------------------------- proxy/gitreq.go | 3 +++ proxy/httpc.go | 11 ++--------- 6 files changed, 26 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74a19fc..becca9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,16 @@ # 更新日志 +25w28a - 2025-04-14 +--- +- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用; +- CHANGE: 去除不必要的请求 +- CHANGE: 改进`httpc`相关配置 +- CHANGE: 合入test版本修改 + 25w28t-2 - 2025-04-11 --- - TEST: 测试验证版本 -- CHANGE: 为老旧的油猴xhr提供兼容性支持, break RFC 2616, RFC 9112 +- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进 25w28t-1 - 2025-04-11 --- diff --git a/DEV-VERSION b/DEV-VERSION index c774704..d219ec2 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w28t-2 \ No newline at end of file +25w28a \ No newline at end of file diff --git a/main.go b/main.go index d766a9f..0c6947e 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,6 @@ import ( "github.com/cloudwego/hertz/pkg/common/hlog" //"github.com/cloudwego/hertz/pkg/network/standard" - "github.com/hertz-contrib/http2/factory" ) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index dffb6ef..a0a6c6b 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "ghproxy/config" - "io" "net/http" "strconv" @@ -15,39 +14,6 @@ import ( func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { method := c.Request.Method - // 发送HEAD请求, 预获取Content-Length - headReq, err := client.NewRequest("HEAD", u, nil) - if err != nil { - HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) - return - } - setRequestHeaders(c, headReq) - removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) - AuthPassThrough(c, cfg, headReq) - - headResp, err := client.Do(headReq) - if err != nil { - HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) - return - } - defer func(Body io.ReadCloser) { - if err := Body.Close(); err != nil { - logError("Failed to close response body: %v", err) - } - }(headResp.Body) - - contentLength := headResp.Header.Get("Content-Length") - sizelimit := cfg.Server.SizeLimit * 1024 * 1024 - if contentLength != "" { - size, err := strconv.Atoi(contentLength) - if err == nil && size > sizelimit { - finalURL := headResp.Request.URL.String() - c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) - logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), finalURL, size) - return - } - } - body := c.Request.Body() bodyReader := bytes.NewBuffer(body) @@ -69,16 +35,25 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c // 错误处理(404) if resp.StatusCode == 404 { - c.String(http.StatusNotFound, "File Not Found") - //c.Status(http.StatusNotFound) + //c.String(http.StatusNotFound, "File Not Found") + c.Status(http.StatusNotFound) return } - var bodySize int + var ( + bodySize int + contentLength string + sizelimit int + ) + sizelimit = cfg.Server.SizeLimit * 1024 * 1024 contentLength = resp.Header.Get("Content-Length") if contentLength != "" { var err error bodySize, err = strconv.Atoi(contentLength) + if err != nil { + logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err) + bodySize = -1 + } if err == nil && bodySize > sizelimit { finalURL := resp.Request.URL.String() c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 0826a58..c4f0cfd 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -72,6 +72,9 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co if contentLength != "" { size, err := strconv.Atoi(contentLength) sizelimit := cfg.Server.SizeLimit * 1024 * 1024 + if err != nil { + logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err) + } if err == nil && size > sizelimit { finalURL := []byte(resp.Request.URL.String()) c.Redirect(http.StatusMovedPermanently, finalURL) diff --git a/proxy/httpc.go b/proxy/httpc.go index 80e4510..1f0c6e9 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -42,7 +42,6 @@ func initHTTPClient(cfg *config.Config) { if cfg.Httpc.Mode == "auto" { tr = &http.Transport{ - //MaxIdleConns: 160, IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB @@ -64,7 +63,6 @@ func initHTTPClient(cfg *config.Config) { logWarning("use Auto to Run HTTP Client") fmt.Println("use Auto to Run HTTP Client") tr = &http.Transport{ - //MaxIdleConns: 160, IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB @@ -89,7 +87,6 @@ func initGitHTTPClient(cfg *config.Config) { if cfg.Httpc.Mode == "auto" { gittr = &http.Transport{ - //MaxIdleConns: 160, IdleConnTimeout: 30 * time.Second, WriteBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB @@ -123,18 +120,14 @@ func initGitHTTPClient(cfg *config.Config) { httpc.WithTransport(gittr), httpc.WithDumpLog(), httpc.WithProtocols(httpc.ProtocolsConfig{ - Http1: false, - Http2: false, - Http2_Cleartext: true, + ForceH2C: true, }), ) } else if !cfg.Server.Debug && cfg.GitClone.ForceH2C { gitclient = httpc.New( httpc.WithTransport(gittr), httpc.WithProtocols(httpc.ProtocolsConfig{ - Http1: false, - Http2: false, - Http2_Cleartext: true, + ForceH2C: true, }), ) } else if cfg.Server.Debug && !cfg.GitClone.ForceH2C { From bed6c486dc55341ea0262800e7c0893e5a446ffa Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 15 Apr 2025 10:23:29 +0800 Subject: [PATCH 017/118] 25w28b --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- main.go | 1 - proxy/chunkreq.go | 4 ++++ proxy/match.go | 3 ++- proxy/reqheader.go | 3 +++ 6 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index becca9b..42b9a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w28b - 2025-04-15 +--- +- PRE-RELEASE: 此版本是v3.0.2预发布版本,请勿在生产环境中使用; +- CHANGE: 改进resp关闭 +- CHANGE: 避免重复的re编译操作 + 25w28a - 2025-04-14 --- - PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index d219ec2..0d83d4d 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w28a \ No newline at end of file +25w28b \ No newline at end of file diff --git a/main.go b/main.go index 0c6947e..3cbd127 100644 --- a/main.go +++ b/main.go @@ -241,7 +241,6 @@ func setupPages(cfg *config.Config, r *server.Hertz) { r.StaticFile("/style.css", stylesheetsPath) r.StaticFile("/bootstrap.min.css", bootstrapPath) r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath) - //router.StaticFile("/bootstrap.min.css", bootstrapPath) default: // 处理无效的Pages Mode diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index a0a6c6b..47f0e59 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -56,6 +56,10 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c } if err == nil && bodySize > sizelimit { finalURL := resp.Request.URL.String() + err := resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize) return diff --git a/proxy/match.go b/proxy/match.go index 71f779a..8e6c0d9 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -243,6 +243,8 @@ func extractParts(rawURL string) (string, string, string, url.Values, error) { return repoOwner, repoName, remainingPath, queryParams, nil } +var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`) + // processLinks 处理链接,返回包含处理后数据的 io.Reader func processLinks(input io.Reader, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) { pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe @@ -315,7 +317,6 @@ func processLinks(input io.Reader, compress string, host string, cfg *config.Con }() // 使用正则表达式匹配 http 和 https 链接 - urlPattern := regexp.MustCompile(`https?://[^\s'"]+`) for { line, readErr := bufReader.ReadString('\n') if readErr != nil { diff --git a/proxy/reqheader.go b/proxy/reqheader.go index 3421722..01eab46 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -13,6 +13,9 @@ func setRequestHeaders(c *app.RequestContext, req *http.Request) { }) } +// removeWSHeader removes the "Upgrade" and "Connection" headers from the given +// Request, which are added by the client when it wants to upgrade the +// connection to a WebSocket connection. func removeWSHeader(req *http.Request) { req.Header.Del("Upgrade") req.Header.Del("Connection") From 73aac79c1bebdb1f55738c9d72ac14568fbe6fea Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:14:53 +0800 Subject: [PATCH 018/118] 3.0.2 --- CHANGELOG.md | 8 ++++++++ VERSION | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42b9a5a..639dc60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 更新日志 +3.0.2 - 2025-04-15 +--- +- CHANGE: 避免重复的re编译操作 +- CHANGE: 去除不必要的请求 +- CHANGE: 改进`httpc`相关配置 +- CHANGE: 更新`httpc` 0.4.0 +- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进 + 25w28b - 2025-04-15 --- - PRE-RELEASE: 此版本是v3.0.2预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 13d683c..d9c62ed 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.1 \ No newline at end of file +3.0.2 \ No newline at end of file From f110c96c1ff5de902e5e4810a47efe7a7dfbd79f Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 15 Apr 2025 13:22:51 +0800 Subject: [PATCH 019/118] update readme --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a45ced2..0209413 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) -使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署 +支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能 ## 项目说明 @@ -16,9 +16,10 @@ - 🚫 **支持自定义黑名单/白名单** - 🗄️ **支持 Git Clone 缓存(配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git))** - 🐳 **支持 Docker 部署** +- 🐳 **支持自托管** - ⚡ **支持速率限制** - 🔒 **支持用户鉴权** -- 🐚 **支持 shell 脚本嵌套加速** +- 🐚 **支持 shell 脚本多层嵌套加速** ### 项目相关 From 254c9a8bad4b4d9f77baca30e3f796532947cf6b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:05:36 +0800 Subject: [PATCH 020/118] 25w29t-1 --- DEV-VERSION | 2 +- main.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index 0d83d4d..f103177 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w28b \ No newline at end of file +25w29t-1 \ No newline at end of file diff --git a/main.go b/main.go index 3cbd127..e9b29a9 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ import ( "github.com/cloudwego/hertz/pkg/common/adaptor" "github.com/cloudwego/hertz/pkg/common/hlog" - //"github.com/cloudwego/hertz/pkg/network/standard" + "github.com/cloudwego/hertz/pkg/network/standard" "github.com/hertz-contrib/http2/factory" ) @@ -378,8 +378,8 @@ func main() { r = server.New( server.WithHostPorts(addr), server.WithH2C(true), - // server.WithALPN(true), - // server.WithTransport(standard.NewTransporter), + // server.WithALPN(true), + server.WithTransport(standard.NewTransporter), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { From 26a42b6510c399c7000725ed05785ddc62b5a717 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:47:46 +0800 Subject: [PATCH 021/118] add pprof for debug --- main.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index e9b29a9..6baac20 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,8 @@ import ( "github.com/cloudwego/hertz/pkg/network/standard" "github.com/hertz-contrib/http2/factory" + + _ "net/http/pprof" ) var ( @@ -385,6 +387,7 @@ func main() { } else { r = server.New( server.WithHostPorts(addr), + server.WithTransport(standard.NewTransporter), ) } @@ -447,11 +450,18 @@ func main() { fmt.Printf("A Go Based High-Performance Github Proxy \n") fmt.Printf("Made by WJQSERVER-STUDIO\n") + if cfg.Server.Debug { + go func() { + http.ListenAndServe("localhost:6060", nil) + }() + } + r.Spin() defer logger.Close() defer func() { if hertZfile != nil { - err := hertZfile.Close() + var err error + err = hertZfile.Close() if err != nil { logError("Failed to close hertz log file: %v", err) } From 7e5b12dff8a35f029719030c190a4dd043e89865 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:50:04 +0800 Subject: [PATCH 022/118] Fix: Optimize header forwarding by excluding headers in a single pass --- proxy/chunkreq.go | 66 +++++++++++++++++++++++++++++++++-------------- proxy/handler.go | 37 ++++++++++++++++++-------- 2 files changed, 73 insertions(+), 30 deletions(-) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 47f0e59..7187e1f 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -5,20 +5,39 @@ import ( "context" "fmt" "ghproxy/config" + "io" "net/http" "strconv" "github.com/cloudwego/hertz/pkg/app" ) +var ( + headersToRemove = map[string]struct{}{ + "Content-Security-Policy": {}, + "Referrer-Policy": {}, + "Strict-Transport-Security": {}, + "X-Github-Request-Id": {}, + "X-Timer": {}, + "X-Served-By": {}, + "X-Fastly-Request-Id": {}, + } +) + func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { - method := c.Request.Method - body := c.Request.Body() + var ( + method []byte + bodyReader *bytes.Buffer + req *http.Request + resp *http.Response + err error + ) - bodyReader := bytes.NewBuffer(body) + method = c.Request.Method() + bodyReader = bytes.NewBuffer(c.Request.Body()) - req, err := client.NewRequest(string(method()), u, bodyReader) + req, err = client.NewRequest(string(method), u, bodyReader) if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return @@ -27,7 +46,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) AuthPassThrough(c, cfg, req) - resp, err := client.Do(req) + resp, err = client.Do(req) if err != nil { HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) return @@ -55,8 +74,9 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodySize = -1 } if err == nil && bodySize > sizelimit { - finalURL := resp.Request.URL.String() - err := resp.Body.Close() + var finalURL string + finalURL = resp.Request.URL.String() + err = resp.Body.Close() if err != nil { logError("Failed to close response body: %v", err) } @@ -66,20 +86,26 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c } } - for key, values := range resp.Header { - for _, value := range values { - c.Header(key, value) + /* + for header := range headersToRemove { + resp.Header.Del(header) } - } - headersToRemove := map[string]struct{}{ - "Content-Security-Policy": {}, - "Referrer-Policy": {}, - "Strict-Transport-Security": {}, - } + for key := range resp.Header { + var values []string = resp.Header.Values(key) + for _, value := range values { + c.Header(key, value) + } + } + */ - for header := range headersToRemove { - resp.Header.Del(header) + // 复制响应头,排除需要移除的 header + for key, values := range resp.Header { + if _, shouldRemove := headersToRemove[key]; !shouldRemove { + for _, value := range values { + c.Header(key, value) + } + } } switch cfg.Server.Cors { @@ -105,7 +131,9 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c logInfo("Is Shell: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol()) c.Header("Content-Length", "") - reader, _, err := processLinks(resp.Body, compress, string(c.Request.Host()), cfg) + var reader io.Reader + + reader, _, err = processLinks(resp.Body, compress, string(c.Request.Host()), cfg) c.SetBodyStream(reader, -1) if err != nil { diff --git a/proxy/handler.go b/proxy/handler.go index f8b998c..9d214d3 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -41,13 +41,19 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra } } - rawPath := strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ - matches := re.FindStringSubmatch(rawPath) // 匹配路径 + var ( + rawPath string + matches []string + errMsg string + ) + + rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ + matches = re.FindStringSubmatch(rawPath) // 匹配路径 logInfo("URL: %v", matches) // 匹配路径错误处理 if len(matches) < 3 { - errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) + errMsg = fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logWarning(errMsg) c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath) return @@ -56,7 +62,14 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 制作url rawPath = "https://" + matches[2] - user, repo, matcher, err := Matcher(rawPath, cfg) + var ( + user string + repo string + matcher string + err error + ) + + 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) @@ -69,18 +82,19 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra return } } - username := user - logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), username, repo) + logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) // dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header logDump("%s %s %s %s %s %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header()) - repouser := fmt.Sprintf("%s/%s", username, repo) + var repouser string + repouser = fmt.Sprintf("%s/%s", user, repo) // 白名单检查 if cfg.Whitelist.Enabled { - whitelist := auth.CheckWhitelist(username, repo) + var whitelist bool + whitelist = auth.CheckWhitelist(user, repo) if !whitelist { - errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser) + errMsg = fmt.Sprintf("Whitelist Blocked repo: %s", repouser) c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) logWarning("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser) return @@ -89,9 +103,10 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 黑名单检查 if cfg.Blacklist.Enabled { - blacklist := auth.CheckBlacklist(username, repo) + var blacklist bool + blacklist = auth.CheckBlacklist(user, repo) if blacklist { - errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser) + errMsg = fmt.Sprintf("Blacklist Blocked repo: %s", repouser) c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) logWarning("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser) return From dd2f5b5a12d7ebc56c396ec290433ab16ede981b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 17 Apr 2025 22:20:06 +0800 Subject: [PATCH 023/118] 25w29a --- .github/ISSUE_TEMPLATE/features_request.md | 2 +- CHANGELOG.md | 8 ++++++++ DEV-VERSION | 2 +- proxy/chunkreq.go | 18 +++++++++++++++--- proxy/gitreq.go | 4 ++-- proxy/match.go | 9 ++++++++- proxy/reqheader.go | 15 +++++++++++++++ 7 files changed, 50 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/features_request.md b/.github/ISSUE_TEMPLATE/features_request.md index 603c02f..38732d9 100644 --- a/.github/ISSUE_TEMPLATE/features_request.md +++ b/.github/ISSUE_TEMPLATE/features_request.md @@ -2,7 +2,7 @@ name: Features request about: 提出新功能建议 title: "[Features]" -labels: enhancement +labels: 改进 assignees: '' --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 639dc60..fe627e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 更新日志 +25w29a - 2025-04-27 +--- +- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用; +- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息 +- FIX: 修正非预期的header操作行为 +- CHANGE: 合并header相关逻辑, 避免多次操作 +- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理 + 3.0.2 - 2025-04-15 --- - CHANGE: 避免重复的re编译操作 diff --git a/DEV-VERSION b/DEV-VERSION index f103177..f4abfe2 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w29t-1 \ No newline at end of file +25w29a \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 7187e1f..b02ec4c 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -13,7 +13,7 @@ import ( ) var ( - headersToRemove = map[string]struct{}{ + respHeadersToRemove = map[string]struct{}{ "Content-Security-Policy": {}, "Referrer-Policy": {}, "Strict-Transport-Security": {}, @@ -22,6 +22,17 @@ var ( "X-Served-By": {}, "X-Fastly-Request-Id": {}, } + + reqHeadersToRemove = map[string]struct{}{ + "CF-IPCountry": {}, + "CF-RAY": {}, + "CF-Visitor": {}, + "CF-Connecting-IP": {}, + "CF-EW-Via": {}, + "CDN-Loop": {}, + "Upgrade": {}, + "Connection": {}, + } ) func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { @@ -42,8 +53,9 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return } + setRequestHeaders(c, req) - removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) + //removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) AuthPassThrough(c, cfg, req) resp, err = client.Do(req) @@ -101,7 +113,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c // 复制响应头,排除需要移除的 header for key, values := range resp.Header { - if _, shouldRemove := headersToRemove[key]; !shouldRemove { + if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove { for _, value := range values { c.Header(key, value) } diff --git a/proxy/gitreq.go b/proxy/gitreq.go index c4f0cfd..55ede9a 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -43,7 +43,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co return } setRequestHeaders(c, req) - removeWSHeader(req) + //removeWSHeader(req) AuthPassThrough(c, cfg, req) resp, err = gitclient.Do(req) @@ -58,7 +58,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co return } setRequestHeaders(c, req) - removeWSHeader(req) + //removeWSHeader(req) AuthPassThrough(c, cfg, req) resp, err = client.Do(req) diff --git a/proxy/match.go b/proxy/match.go index 8e6c0d9..1cae46c 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -246,7 +246,7 @@ func extractParts(rawURL string) (string, string, string, url.Values, error) { var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`) // processLinks 处理链接,返回包含处理后数据的 io.Reader -func processLinks(input io.Reader, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) { +func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) { pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe readerOut = pipeReader @@ -268,6 +268,13 @@ func processLinks(input io.Reader, compress string, host string, cfg *config.Con } }() + defer func() { + if err := input.Close(); err != nil { + logError("input close failed: %v", err) + } + + }() + var bufReader *bufio.Reader if compress == "gzip" { diff --git a/proxy/reqheader.go b/proxy/reqheader.go index 01eab46..c338706 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -6,13 +6,27 @@ import ( "github.com/cloudwego/hertz/pkg/app" ) +/* // 设置请求头 func setRequestHeaders(c *app.RequestContext, req *http.Request) { c.Request.Header.VisitAll(func(key, value []byte) { req.Header.Set(string(key), string(value)) }) } +*/ +func setRequestHeaders(c *app.RequestContext, req *http.Request) { + c.Request.Header.VisitAll(func(key, value []byte) { + headerKey := string(key) + headerValue := string(value) + if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove { + req.Header.Set(headerKey, headerValue) + } + + }) +} + +/* // removeWSHeader removes the "Upgrade" and "Connection" headers from the given // Request, which are added by the client when it wants to upgrade the // connection to a WebSocket connection. @@ -20,3 +34,4 @@ func removeWSHeader(req *http.Request) { req.Header.Del("Upgrade") req.Header.Del("Connection") } +*/ From c1c39a5a1f17dcea5ad0f4b8ed91b7b8d667ca6d Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 17 Apr 2025 22:30:00 +0800 Subject: [PATCH 024/118] remove unused bufferpool --- proxy/httpc.go | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/proxy/httpc.go b/proxy/httpc.go index 1f0c6e9..dae57b6 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -4,7 +4,6 @@ import ( "fmt" "ghproxy/config" "net/http" - "sync" "time" httpc "github.com/satomitouka/touka-httpc" @@ -13,11 +12,10 @@ import ( var BufferSize int = 32 * 1024 // 32KB var ( - tr *http.Transport - gittr *http.Transport - BufferPool *sync.Pool - client *httpc.Client - gitclient *httpc.Client + tr *http.Transport + gittr *http.Transport + client *httpc.Client + gitclient *httpc.Client ) func InitReq(cfg *config.Config) { @@ -25,13 +23,6 @@ func InitReq(cfg *config.Config) { if cfg.GitClone.Mode == "cache" { initGitHTTPClient(cfg) } - - // 初始化固定大小的缓存池 - BufferPool = &sync.Pool{ - New: func() interface{} { - return make([]byte, BufferSize) - }, - } } func initHTTPClient(cfg *config.Config) { From 5e0f95dae3fa777a16cf9a8b2208754fb88676ef Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 19 Apr 2025 20:44:43 +0800 Subject: [PATCH 025/118] 3.0.3 --- CHANGELOG.md | 9 ++++++++- VERSION | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe627e5..b20e93a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # 更新日志 -25w29a - 2025-04-27 +3.0.3 - 2025-04-19 +--- +- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息 +- FIX: 修正非预期的header操作行为 +- CHANGE: 合并header相关逻辑, 避免多次操作 +- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理 + +25w29a - 2025-04-17 --- - PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用; - CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息 diff --git a/VERSION b/VERSION index d9c62ed..282895a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.2 \ No newline at end of file +3.0.3 \ No newline at end of file From 6fb7e1150e37835fda6fd6fabc23723590ced413 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 19 Apr 2025 21:14:09 +0800 Subject: [PATCH 026/118] 25w29b --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- config/config.go | 14 +++++++++---- config/config.toml | 1 + deploy/config.toml | 1 + docs/config.md | 5 +++++ main.go | 49 +++++++++++++++++++++++++++++----------------- 7 files changed, 55 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b20e93a..8361a09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ - FIX: 修正非预期的header操作行为 - CHANGE: 合并header相关逻辑, 避免多次操作 - CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理 +- CHANGE: 增加`netlib`配置项 + +25w29b - 2025-04-19 +--- +- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用; +- CHANGE: 增加`netlib`配置项 25w29a - 2025-04-17 --- diff --git a/DEV-VERSION b/DEV-VERSION index f4abfe2..c307029 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w29a \ No newline at end of file +25w29b \ No newline at end of file diff --git a/config/config.go b/config/config.go index 1bf1bdc..e737a63 100644 --- a/config/config.go +++ b/config/config.go @@ -22,15 +22,20 @@ type Config struct { /* [server] -host = "0.0.0.0" # 监听地址 -port = 8080 # 监听端口 -sizeLimit = 125 # 125MB -H2C = true # 是否开启H2C传输 +host = "0.0.0.0" +port = 8080 +netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" +sizeLimit = 125 # MB +memLimit = 0 # MB +H2C = true +cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; +debug = false */ type ServerConfig struct { Port int `toml:"port"` Host string `toml:"host"` + NetLib string `toml:"netlib"` SizeLimit int `toml:"sizeLimit"` MemLimit int64 `toml:"memLimit"` H2C bool `toml:"H2C"` @@ -180,6 +185,7 @@ func DefaultConfig() *Config { Server: ServerConfig{ Port: 8080, Host: "0.0.0.0", + NetLib: "netpoll", SizeLimit: 125, MemLimit: 0, H2C: true, diff --git a/config/config.toml b/config/config.toml index 92b81f8..ac1ee59 100644 --- a/config/config.toml +++ b/config/config.toml @@ -1,6 +1,7 @@ [server] host = "0.0.0.0" port = 8080 +netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" sizeLimit = 125 # MB memLimit = 0 # MB H2C = true diff --git a/deploy/config.toml b/deploy/config.toml index a25acf1..b88e30b 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -1,6 +1,7 @@ [server] host = "127.0.0.1" port = 8080 +netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" sizeLimit = 125 # MB memLimit = 0 # MB H2C = true diff --git a/docs/config.md b/docs/config.md index ccd1ce2..bad8c65 100644 --- a/docs/config.md +++ b/docs/config.md @@ -12,6 +12,7 @@ [server] host = "0.0.0.0" port = 8080 +netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" sizeLimit = 125 # MB memLimit = 0 # MB H2C = true @@ -83,6 +84,10 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" * 类型: 整数 (`int`) * 默认值: `8080` * 说明: 设置 `ghproxy` 监听的端口号。 + * `netlib`: 底层网络库。 + * 类型: 字符串 (`string`) + * 默认值: `""` (HertZ默认处置) + * 说明: `"std"` `"standard"` `"net/http"` `"net"` 均会被设置为go标准库`net/http`, 设置为`"netpoll"`或`""`会由`HertZ`默认逻辑处理 * `sizeLimit`: 请求体大小限制。 * 类型: 整数 (`int`) * 默认值: `125` (MB) diff --git a/main.go b/main.go index 6baac20..f915fcc 100644 --- a/main.go +++ b/main.go @@ -19,15 +19,14 @@ import ( "ghproxy/rate" "github.com/WJQSERVER-STUDIO/go-utils/logger" + "github.com/hertz-contrib/http2/factory" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery" "github.com/cloudwego/hertz/pkg/app/server" "github.com/cloudwego/hertz/pkg/common/adaptor" "github.com/cloudwego/hertz/pkg/common/hlog" - "github.com/cloudwego/hertz/pkg/network/standard" - "github.com/hertz-contrib/http2/factory" _ "net/http/pprof" ) @@ -362,33 +361,47 @@ func init() { } func main() { - // 如果 showVersion 为 true,则在 init 阶段已退出,这里直接返回 if showVersion || showHelp { return } logDebug("Run Mode: %s", runMode) - // 确保在程序配置加载且非版本显示模式下执行 if cfg == nil { fmt.Println("Config not loaded, exiting.") - return // 如果配置未加载,则不继续执行 + return } addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) - - if cfg.Server.H2C { - r = server.New( - server.WithHostPorts(addr), - server.WithH2C(true), - // server.WithALPN(true), - server.WithTransport(standard.NewTransporter), - ) - r.AddProtocol("h2", factory.NewServerFactory()) + if cfg.Server.NetLib == "std" || cfg.Server.NetLib == "standard" || cfg.Server.NetLib == "net" || cfg.Server.NetLib == "net/http" { + if cfg.Server.H2C { + r = server.New( + server.WithH2C(true), + server.WithHostPorts(addr), + server.WithTransport(standard.NewTransporter), + ) + r.AddProtocol("h2", factory.NewServerFactory()) + } else { + r = server.New( + server.WithHostPorts(addr), + server.WithTransport(standard.NewTransporter), + ) + } + } else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" { + if cfg.Server.H2C { + r = server.New( + server.WithH2C(true), + server.WithHostPorts(addr), + ) + r.AddProtocol("h2", factory.NewServerFactory()) + } else { + r = server.New( + server.WithHostPorts(addr), + ) + } } else { - r = server.New( - server.WithHostPorts(addr), - server.WithTransport(standard.NewTransporter), - ) + logError("Invalid NetLib: %s", cfg.Server.NetLib) + fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib) + os.Exit(1) } // 添加Recovery中间件 From 0b052f9c7fb30bb9e69ba5e4f633939b6398e815 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 19 Apr 2025 21:23:31 +0800 Subject: [PATCH 027/118] add debug output --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index f915fcc..50df92e 100644 --- a/main.go +++ b/main.go @@ -364,7 +364,7 @@ func main() { if showVersion || showHelp { return } - logDebug("Run Mode: %s", runMode) + logDebug("Run Mode: %s Netlib: %s", runMode, cfg.Server.NetLib) if cfg == nil { fmt.Println("Config not loaded, exiting.") From 26a5148c6f91c874a94a5eec0937879ee9be9cfc Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 19 Apr 2025 22:59:59 +0800 Subject: [PATCH 028/118] use gertz route for std url --- main.go | 78 +++++++++++++++--------------- proxy/handler.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++ proxy/match.go | 4 +- 3 files changed, 161 insertions(+), 41 deletions(-) diff --git a/main.go b/main.go index 50df92e..6e557c2 100644 --- a/main.go +++ b/main.go @@ -404,56 +404,54 @@ func main() { os.Exit(1) } - // 添加Recovery中间件 - r.Use(recovery.Recovery()) - // 添加log中间件 - r.Use(loggin.Middleware()) - + r.Use(recovery.Recovery()) // Recovery中间件 + r.Use(loggin.Middleware()) // log中间件 setupApi(cfg, r, version) - setupPages(cfg, r) - /* - // 1. GitHub Releases/Archive - Use distinct path segments for type - r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for releases - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) + r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "release") + proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + }) - r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for archive - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) + r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "release") + proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + }) - // 2. GitHub Blob/Raw - Use distinct path segments for type - r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for blob - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) + r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "blob") + proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + }) - r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for raw - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) + r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "raw") + proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + }) - r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for info - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) - r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) + r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "gitclone") + proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + }) + r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "gitclone") + proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + }) - // 4. Raw GitHubusercontent - Keep as is (assuming it's distinct enough) - r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) + r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "raw") + proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + }) - // 5. Gist GitHubusercontent - Keep as is (assuming it's distinct enough) - r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) { - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) + r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "gist") + proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) + }) - // 6. GitHub API Repos - Keep as is (assuming it's distinct enough) - r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { - proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) - }) - */ + r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + ctx = context.WithValue(ctx, "matcher", "api") + proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) + }) r.NoRoute(func(ctx context.Context, c *app.RequestContext) { proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) diff --git a/proxy/handler.go b/proxy/handler.go index 9d214d3..9e1ced0 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -147,3 +147,123 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra } } } + +func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + // 输出所有传入参数 + logDebug("All Request Params: %v", c.Params) + logDebug("Context Params(Matcher): %v", ctx.Value("matcher")) + + // 限制访问频率 + if cfg.RateLimit.Enabled { + + var allowed bool + + switch cfg.RateLimit.RateMethod { + case "ip": + allowed = iplimiter.Allow(c.ClientIP()) + case "total": + allowed = limiter.Allow() + default: + logWarning("Invalid RateLimit Method") + return + } + + if !allowed { + c.JSON(http.StatusTooManyRequests, map[string]string{"error": "Too Many Requests"}) + logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) + return + } + } + + var ( + rawPath string + errMsg string + ) + + rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ + + var ( + user string + repo string + matcher string + err error + ) + + user = c.Param("user") + repo = c.Param("repo") + matcher = ctx.Value("matcher").(string) + + logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + // dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header + logDump("%s %s %s %s %s %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header()) + + // 白名单检查 + if cfg.Whitelist.Enabled { + var whitelist bool + whitelist = auth.CheckWhitelist(user, repo) + if !whitelist { + errMsg = fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo) + c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) + logWarning("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + return + } + } + + // 黑名单检查 + if cfg.Blacklist.Enabled { + var blacklist bool + blacklist = auth.CheckBlacklist(user, repo) + if blacklist { + errMsg = fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo) + c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) + logWarning("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + return + } + } + + if matcher == "api" && !cfg.Auth.ForceAllowApi { + if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { + c.JSON(http.StatusForbidden, map[string]string{"error": "Github API Req without AuthHeader is Not Allowed"}) + logWarning("%s %s %s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) + return + } + } + + // 鉴权 + if cfg.Auth.Enabled { + var authcheck bool + authcheck, err = auth.AuthHandler(ctx, c, cfg) + if !authcheck { + //c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) + c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"}) + logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) + return + } + } + + // 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth + + // 处理blob/raw路径 + if matcher == "blob" { + rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) + } + + // 为rawpath加入https:// 头 + rawPath = "https://" + rawPath + + // IP METHOD URL USERAGENT PROTO MATCHES + logDebug("%s %s %s %s %s Matched: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), matcher) + + switch matcher { + case "releases", "blob", "raw", "gist", "api": + ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher) + case "clone": + GitReq(ctx, c, rawPath, cfg, "git") + default: + c.String(http.StatusForbidden, "Invalid input.") + fmt.Println("Invalid input.") + return + } + } +} diff --git a/proxy/match.go b/proxy/match.go index 1cae46c..fa42494 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -65,8 +65,10 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) switch parts[2] { case "releases", "archive": matcher = "releases" - case "blob", "raw": + case "blob": matcher = "blob" + case "raw": + matcher = "raw" case "info", "git-upload-pack": matcher = "clone" default: From 2eb6a9810bcdce61b0f9613d275b4b1ac75d2887 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:02:13 +0800 Subject: [PATCH 029/118] 25w30a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8361a09..79e0f49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w30a - 2025-04-19 +--- +- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; +- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器 + 3.0.3 - 2025-04-19 --- - CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息 diff --git a/DEV-VERSION b/DEV-VERSION index c307029..e1a38b6 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w29b \ No newline at end of file +25w30a \ No newline at end of file From 809032a9704b5c7a8c5e4454abd818078cbfd90f Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:47:45 +0800 Subject: [PATCH 030/118] change to c.Request.BodyStream() --- proxy/chunkreq.go | 15 +++++++-------- proxy/gitreq.go | 9 ++++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index b02ec4c..18cc0cb 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -1,7 +1,6 @@ package proxy import ( - "bytes" "context" "fmt" "ghproxy/config" @@ -38,17 +37,17 @@ var ( func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { var ( - method []byte - bodyReader *bytes.Buffer - req *http.Request - resp *http.Response - err error + method []byte + //bodyReader *bytes.Buffer + req *http.Request + resp *http.Response + err error ) method = c.Request.Method() - bodyReader = bytes.NewBuffer(c.Request.Body()) + //bodyReader = bytes.NewBuffer(c.Request.Body()) - req, err = client.NewRequest(string(method), u, bodyReader) + req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 55ede9a..ad53f94 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -1,7 +1,6 @@ package proxy import ( - "bytes" "context" "fmt" "ghproxy/config" @@ -31,13 +30,13 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co //err error ) - body := c.Request.Body() + //body := c.Request.Body() - bodyReader := bytes.NewBuffer(body) + //bodyReader := bytes.NewBuffer(body) // 创建请求 if cfg.GitClone.Mode == "cache" { - req, err := gitclient.NewRequest(method, u, bodyReader) + req, err := gitclient.NewRequest(method, u, c.Request.BodyStream()) if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return @@ -52,7 +51,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co return } } else { - req, err := client.NewRequest(method, u, bodyReader) + req, err := client.NewRequest(method, u, c.Request.BodyStream()) if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return From 3d7559bd66e8764d64b7fc6bfc0194d0d4b03a9a Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 21 Apr 2025 13:57:52 +0800 Subject: [PATCH 031/118] change context.Context to hertz *app.RequestContext --- main.go | 18 +++++++++--------- proxy/handler.go | 5 ++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index 6e557c2..2fd7bfa 100644 --- a/main.go +++ b/main.go @@ -410,46 +410,46 @@ func main() { setupPages(cfg, r) r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "release") + c.Set("matcher", "release") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "release") + c.Set("matcher", "release") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "blob") + c.Set("matcher", "blob") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "raw") + c.Set("matcher", "raw") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "gitclone") + c.Set("matcher", "gitclone") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "gitclone") + c.Set("matcher", "gitclone") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "raw") + c.Set("matcher", "raw") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "gist") + c.Set("matcher", "gist") proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { - ctx = context.WithValue(ctx, "matcher", "api") + c.Set("matcher", "api") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) diff --git a/proxy/handler.go b/proxy/handler.go index 9e1ced0..4b9b977 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -151,8 +151,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { // 输出所有传入参数 - logDebug("All Request Params: %v", c.Params) - logDebug("Context Params(Matcher): %v", ctx.Value("matcher")) + logDebug("Context Params(Matcher): %v", c.GetString("matcher")) // 限制访问频率 if cfg.RateLimit.Enabled { @@ -192,7 +191,7 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra user = c.Param("user") repo = c.Param("repo") - matcher = ctx.Value("matcher").(string) + matcher = c.GetString("matcher") logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) // dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header From 558d3fbb0be6a734460d2e8d1d5761f412bc8f87 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:27:38 +0800 Subject: [PATCH 032/118] 25w30b --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e0f49..c60bba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w30b - 2025-04-21 +--- +- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; +- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader` +- CHANGE: 使用`HertZ`的`requestContext`传递matcher参数, 而不是`25w30a`中的标准ctx + 25w30a - 2025-04-19 --- - PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index e1a38b6..087f399 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w30a \ No newline at end of file +25w30b \ No newline at end of file From d79aeaaacd9f6faf5c43351a04126c7701228085 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 21 Apr 2025 18:52:45 +0800 Subject: [PATCH 033/118] 25w30c --- CHANGELOG.md | 7 ++ DEV-VERSION | 2 +- auth/auth.go | 3 +- main.go | 6 ++ proxy/chunkreq.go | 8 +- proxy/gitreq.go | 6 -- proxy/handler.go | 213 +++++----------------------------------------- proxy/match.go | 12 ++- proxy/routing.go | 62 ++++++++++++++ proxy/utils.go | 110 ++++++++++++++++++++++++ 10 files changed, 214 insertions(+), 215 deletions(-) create mode 100644 proxy/routing.go create mode 100644 proxy/utils.go diff --git a/CHANGELOG.md b/CHANGELOG.md index c60bba0..e59acad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +25w30c - 2025-04-21 +--- +- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; +- CHANGE: 改进handle, 复用共同部分 +- CHANGE: 细化url匹配的返回码处理 +- CHANGE: 增加404界面 + 25w30b - 2025-04-21 --- - PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 087f399..a71ee32 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w30b \ No newline at end of file +25w30c \ No newline at end of file diff --git a/auth/auth.go b/auth/auth.go index 1a7f1a2..eacfbf6 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,7 +1,6 @@ package auth import ( - "context" "fmt" "ghproxy/config" @@ -36,7 +35,7 @@ func Init(cfg *config.Config) { logDebug("Auth Init") } -func AuthHandler(ctx context.Context, c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { +func AuthHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { if cfg.Auth.Method == "parameters" { isValid, err = AuthParametersHandler(c, cfg) return isValid, err diff --git a/main.go b/main.go index 2fd7bfa..5314f31 100644 --- a/main.go +++ b/main.go @@ -210,6 +210,12 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err) } + // 初始化errPagesFs + errPagesInitErr := proxy.InitErrPagesFS(pagesFS) + if errPagesInitErr != nil { + logWarning("errPagesInitErr: %s", errPagesInitErr) + } + var assets fs.FS assets, err = fs.Sub(pagesFS, "pages/assets") return pages, assets, nil diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 18cc0cb..1571b27 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -38,14 +38,12 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c var ( method []byte - //bodyReader *bytes.Buffer - req *http.Request - resp *http.Response - err error + req *http.Request + resp *http.Response + err error ) method = c.Request.Method() - //bodyReader = bytes.NewBuffer(c.Request.Body()) req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) if err != nil { diff --git a/proxy/gitreq.go b/proxy/gitreq.go index ad53f94..b8ba66d 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -27,14 +27,8 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co var ( resp *http.Response - //err error ) - //body := c.Request.Body() - - //bodyReader := bytes.NewBuffer(body) - // 创建请求 - if cfg.GitClone.Mode == "cache" { req, err := gitclient.NewRequest(method, u, c.Request.BodyStream()) if err != nil { diff --git a/proxy/handler.go b/proxy/handler.go index 4b9b977..444685a 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -3,8 +3,6 @@ package proxy import ( "context" "errors" - "fmt" - "ghproxy/auth" "ghproxy/config" "ghproxy/rate" "net/http" @@ -19,43 +17,22 @@ var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { - // 限制访问频率 - if cfg.RateLimit.Enabled { - - var allowed bool - - switch cfg.RateLimit.RateMethod { - case "ip": - allowed = iplimiter.Allow(c.ClientIP()) - case "total": - allowed = limiter.Allow() - default: - logWarning("Invalid RateLimit Method") - return - } - - if !allowed { - c.JSON(http.StatusTooManyRequests, map[string]string{"error": "Too Many Requests"}) - logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) - return - } - } + rateCheck(cfg, c, limiter, iplimiter) var ( rawPath string matches []string - errMsg string ) rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ matches = re.FindStringSubmatch(rawPath) // 匹配路径 - logInfo("URL: %v", matches) + logDebug("URL: %v", matches) // 匹配路径错误处理 if len(matches) < 3 { - errMsg = fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) - logWarning(errMsg) - c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath) + logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) + //c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath) + c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid URL Format"}) return } @@ -72,68 +49,35 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra 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) + c.JSON(ErrInvalidURL.Code, map[string]string{"error": "Invalid URL Format, Path: " + rawPath}) logWarning(err.Error()) return } if errors.Is(err, ErrAuthHeaderUnavailable) { - c.String(http.StatusForbidden, "AuthHeader Unavailable") + c.JSON(ErrAuthHeaderUnavailable.Code, map[string]string{"error": "AuthHeader Unavailable"}) + logWarning(err.Error()) + return + } + if errors.Is(err, ErrNotFound) { + //c.JSON(ErrNotFound.Code, map[string]string{"error": "Not Found"}) + NotFoundPage(c) logWarning(err.Error()) return } } logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) - // dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header - logDump("%s %s %s %s %s %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header()) - var repouser string - repouser = fmt.Sprintf("%s/%s", user, repo) + logDump("%s", c.Request.Header.Header()) - // 白名单检查 - if cfg.Whitelist.Enabled { - var whitelist bool - whitelist = auth.CheckWhitelist(user, repo) - if !whitelist { - errMsg = fmt.Sprintf("Whitelist Blocked repo: %s", repouser) - c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) - logWarning("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser) - return - } - } - - // 黑名单检查 - if cfg.Blacklist.Enabled { - var blacklist bool - blacklist = auth.CheckBlacklist(user, repo) - if blacklist { - errMsg = fmt.Sprintf("Blacklist Blocked repo: %s", repouser) - c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) - logWarning("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser) - return - } - } - - // 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth + listCheck(cfg, c, user, repo, rawPath) + authCheck(c, cfg, matcher, rawPath) // 处理blob/raw路径 if matcher == "blob" { rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) } - // 鉴权 - if cfg.Auth.Enabled { - var authcheck bool - authcheck, err = auth.AuthHandler(ctx, c, cfg) - if !authcheck { - //c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) - c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"}) - logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) - return - } - } - - // IP METHOD URL USERAGENT PROTO MATCHES - logDebug("%s %s %s %s %s Matched: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), matcher) + logDebug("Matched: %v", matcher) switch matcher { case "releases", "blob", "raw", "gist", "api": @@ -141,127 +85,8 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra case "clone": GitReq(ctx, c, rawPath, cfg, "git") default: - c.String(http.StatusForbidden, "Invalid input.") - fmt.Println("Invalid input.") - return - } - } -} - -func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { - // 输出所有传入参数 - logDebug("Context Params(Matcher): %v", c.GetString("matcher")) - - // 限制访问频率 - if cfg.RateLimit.Enabled { - - var allowed bool - - switch cfg.RateLimit.RateMethod { - case "ip": - allowed = iplimiter.Allow(c.ClientIP()) - case "total": - allowed = limiter.Allow() - default: - logWarning("Invalid RateLimit Method") - return - } - - if !allowed { - c.JSON(http.StatusTooManyRequests, map[string]string{"error": "Too Many Requests"}) - logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) - return - } - } - - var ( - rawPath string - errMsg string - ) - - rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ - - var ( - user string - repo string - matcher string - err error - ) - - user = c.Param("user") - repo = c.Param("repo") - matcher = c.GetString("matcher") - - logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) - // dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header - logDump("%s %s %s %s %s %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header()) - - // 白名单检查 - if cfg.Whitelist.Enabled { - var whitelist bool - whitelist = auth.CheckWhitelist(user, repo) - if !whitelist { - errMsg = fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo) - c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) - logWarning("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) - return - } - } - - // 黑名单检查 - if cfg.Blacklist.Enabled { - var blacklist bool - blacklist = auth.CheckBlacklist(user, repo) - if blacklist { - errMsg = fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo) - c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) - logWarning("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) - return - } - } - - if matcher == "api" && !cfg.Auth.ForceAllowApi { - if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { - c.JSON(http.StatusForbidden, map[string]string{"error": "Github API Req without AuthHeader is Not Allowed"}) - logWarning("%s %s %s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) - return - } - } - - // 鉴权 - if cfg.Auth.Enabled { - var authcheck bool - authcheck, err = auth.AuthHandler(ctx, c, cfg) - if !authcheck { - //c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) - c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"}) - logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) - return - } - } - - // 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth - - // 处理blob/raw路径 - if matcher == "blob" { - rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) - } - - // 为rawpath加入https:// 头 - rawPath = "https://" + rawPath - - // IP METHOD URL USERAGENT PROTO MATCHES - logDebug("%s %s %s %s %s Matched: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), matcher) - - switch matcher { - case "releases", "blob", "raw", "gist", "api": - ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher) - case "clone": - GitReq(ctx, c, rawPath, cfg, "git") - default: - c.String(http.StatusForbidden, "Invalid input.") - fmt.Println("Invalid input.") + c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid input."}) + logError("Invalid input") return } } diff --git a/proxy/match.go b/proxy/match.go index fa42494..5f0f6e2 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -27,6 +27,10 @@ var ( Code: 403, Msg: "AuthHeader Unavailable", } + ErrNotFound = &MatcherErrors{ + Code: 404, + Msg: "Not Found", + } ) func (e *MatcherErrors) Error() string { @@ -122,7 +126,7 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) } return user, repo, matcher, nil } - return "", "", "", ErrInvalidURL + return "", "", "", ErrNotFound } func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) { @@ -165,12 +169,6 @@ func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) { // 匹配文件扩展名是sh的rawPath func MatcherShell(rawPath string) bool { - /* - if strings.HasSuffix(rawPath, ".sh") { - return true - } - return false - */ return strings.HasSuffix(rawPath, ".sh") } diff --git a/proxy/routing.go b/proxy/routing.go new file mode 100644 index 0000000..c07824c --- /dev/null +++ b/proxy/routing.go @@ -0,0 +1,62 @@ +package proxy + +import ( + "context" + "ghproxy/config" + "ghproxy/rate" + "net/http" + "strings" + + "github.com/cloudwego/hertz/pkg/app" +) + +func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + + rateCheck(cfg, c, limiter, iplimiter) + + var ( + rawPath string + ) + + rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ + + var ( + user string + repo string + matcher string + ) + + user = c.Param("user") + repo = c.Param("repo") + matcher = c.GetString("matcher") + + logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + logDump("%s", c.Request.Header.Header()) + + listCheck(cfg, c, user, repo, rawPath) + authCheck(c, cfg, matcher, rawPath) + + // 处理blob/raw路径 + if matcher == "blob" { + rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) + } + + // 为rawpath加入https:// 头 + rawPath = "https://" + rawPath + + // IP METHOD URL USERAGENT PROTO MATCHES + logDebug("%s %s %s %s %s Matched: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), matcher) + + switch matcher { + case "releases", "blob", "raw", "gist", "api": + ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher) + case "clone": + GitReq(ctx, c, rawPath, cfg, "git") + default: + c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid input."}) + logError("Invalid input") + return + } + } +} diff --git a/proxy/utils.go b/proxy/utils.go new file mode 100644 index 0000000..615daa2 --- /dev/null +++ b/proxy/utils.go @@ -0,0 +1,110 @@ +package proxy + +import ( + "fmt" + "ghproxy/auth" + "ghproxy/config" + "ghproxy/rate" + "io/fs" + + "github.com/cloudwego/hertz/pkg/app" +) + +func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) { + var errMsg string + + // 白名单检查 + if cfg.Whitelist.Enabled { + var whitelist bool + whitelist = auth.CheckWhitelist(user, repo) + if !whitelist { + errMsg = fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo) + c.JSON(403, map[string]string{"error": errMsg}) + logWarning("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + return + } + } + + // 黑名单检查 + if cfg.Blacklist.Enabled { + var blacklist bool + blacklist = auth.CheckBlacklist(user, repo) + if blacklist { + errMsg = fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo) + c.JSON(403, map[string]string{"error": errMsg}) + logWarning("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + return + } + } +} + +// 鉴权 +func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPath string) { + var err error + + if matcher == "api" && !cfg.Auth.ForceAllowApi { + if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { + c.JSON(403, map[string]string{"error": "Github API Req without AuthHeader is Not Allowed"}) + logWarning("%s %s %s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) + return + } + } + + // 鉴权 + if cfg.Auth.Enabled { + var authcheck bool + authcheck, err = auth.AuthHandler(c, cfg) + if !authcheck { + c.JSON(401, map[string]string{"error": "Unauthorized"}) + logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) + return + } + } +} + +func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) { + // 限制访问频率 + if cfg.RateLimit.Enabled { + + var allowed bool + + switch cfg.RateLimit.RateMethod { + case "ip": + allowed = iplimiter.Allow(c.ClientIP()) + case "total": + allowed = limiter.Allow() + default: + logWarning("Invalid RateLimit Method") + c.JSON(500, map[string]string{"error": "Invalid RateLimit Method"}) + return + } + + if !allowed { + c.JSON(429, map[string]string{"error": "Too Many Requests"}) + logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) + return + } + } +} + +var errPagesFs fs.FS + +func InitErrPagesFS(pages fs.FS) error { + var err error + errPagesFs, err = fs.Sub(pages, "pages/err") + if err != nil { + return err + } + return nil +} + +func NotFoundPage(c *app.RequestContext) { + pageData, err := fs.ReadFile(errPagesFs, "404.html") + if err != nil { + c.JSON(404, map[string]string{"error": "Not Found"}) + logDebug("Error reading 404.html: %v", err) + return + } + c.Data(404, "text/html; charset=utf-8", pageData) + return +} From 493ac28b59d1010b5c548e3374d0fdf81a8506fa Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:56:27 +0800 Subject: [PATCH 034/118] add html/tmpl for status err page --- proxy/error.go | 72 ++++++++++++++++++++++++++++++++++++++++++++++++ proxy/handler.go | 44 +++++++++++++++-------------- proxy/match.go | 57 +++++++++++--------------------------- proxy/utils.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 183 insertions(+), 62 deletions(-) diff --git a/proxy/error.go b/proxy/error.go index d8b27c5..2b80593 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -21,3 +21,75 @@ func HandleError(c *app.RequestContext, message string) { c.JSON(http.StatusInternalServerError, map[string]string{"error": message}) logError(message) } + +type GHProxyErrors struct { + StatusCode int + StatusDesc string + StatusText string + HelpInfo string + ErrorMessage string +} + +var ( + ErrInvalidURL = &GHProxyErrors{ + StatusCode: 400, + StatusDesc: "Bad Request", + StatusText: "无效请求", + HelpInfo: "请求的URL格式不正确,请检查后重试。", + } + ErrAuthHeaderUnavailable = &GHProxyErrors{ + StatusCode: 401, + StatusDesc: "Unauthorized", + StatusText: "认证失败", + HelpInfo: "缺少或无效的鉴权信息。", + } + ErrForbidden = &GHProxyErrors{ + StatusCode: 403, + StatusDesc: "Forbidden", + StatusText: "权限不足", + HelpInfo: "您没有权限访问此资源。", + } + ErrNotFound = &GHProxyErrors{ + StatusCode: 404, + StatusDesc: "Not Found", + StatusText: "页面未找到", + HelpInfo: "抱歉,您访问的页面不存在。", + } + ErrInternalServerError = &GHProxyErrors{ + StatusCode: 500, + StatusDesc: "Internal Server Error", + StatusText: "服务器内部错误", + HelpInfo: "服务器处理您的请求时发生错误,请稍后重试或联系管理员。", + } +) + +var statusErrorMap map[int]*GHProxyErrors + +func init() { + statusErrorMap = map[int]*GHProxyErrors{ + ErrInvalidURL.StatusCode: ErrInvalidURL, + ErrAuthHeaderUnavailable.StatusCode: ErrAuthHeaderUnavailable, + ErrForbidden.StatusCode: ErrForbidden, + ErrNotFound.StatusCode: ErrNotFound, + ErrInternalServerError.StatusCode: ErrInternalServerError, + } +} + +func NewErrorWithStatusLookup(statusCode int, errMsg string) *GHProxyErrors { + baseErr, found := statusErrorMap[statusCode] + + if found { + return &GHProxyErrors{ + StatusCode: baseErr.StatusCode, + StatusDesc: baseErr.StatusDesc, + StatusText: baseErr.StatusText, + HelpInfo: baseErr.HelpInfo, + ErrorMessage: errMsg, + } + } else { + return &GHProxyErrors{ + StatusCode: statusCode, + ErrorMessage: errMsg, + } + } +} diff --git a/proxy/handler.go b/proxy/handler.go index 444685a..dae537b 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -2,7 +2,6 @@ package proxy import ( "context" - "errors" "ghproxy/config" "ghproxy/rate" "net/http" @@ -43,27 +42,32 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra user string repo string matcher string - err error + //err error ) - user, repo, matcher, err = Matcher(rawPath, cfg) - if err != nil { - if errors.Is(err, ErrInvalidURL) { - c.JSON(ErrInvalidURL.Code, map[string]string{"error": "Invalid URL Format, Path: " + rawPath}) - logWarning(err.Error()) - return - } - if errors.Is(err, ErrAuthHeaderUnavailable) { - c.JSON(ErrAuthHeaderUnavailable.Code, map[string]string{"error": "AuthHeader Unavailable"}) - logWarning(err.Error()) - return - } - if errors.Is(err, ErrNotFound) { - //c.JSON(ErrNotFound.Code, map[string]string{"error": "Not Found"}) - NotFoundPage(c) - logWarning(err.Error()) - return - } + var matcherErr *GHProxyErrors + user, repo, matcher, matcherErr = Matcher(rawPath, cfg) + if matcherErr != nil { + ErrorPage(c, matcherErr) + return + /* + if errors.Is(err, ErrInvalidURL) { + c.JSON(ErrInvalidURL.Code, map[string]string{"error": "Invalid URL Format, Path: " + rawPath}) + logWarning(err.Error()) + return + } + if errors.Is(err, ErrAuthHeaderUnavailable) { + c.JSON(ErrAuthHeaderUnavailable.Code, map[string]string{"error": "AuthHeader Unavailable"}) + logWarning(err.Error()) + return + } + if errors.Is(err, ErrNotFound) { + //c.JSON(ErrNotFound.Code, map[string]string{"error": "Not Found"}) + NotFoundPage(c) + logWarning(err.Error()) + return + } + */ } logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) diff --git a/proxy/match.go b/proxy/match.go index 5f0f6e2..9b55339 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -11,40 +11,7 @@ import ( "strings" ) -// 定义错误类型, 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", - } - ErrNotFound = &MatcherErrors{ - Code: 404, - Msg: "Not Found", - } -) - -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) { +func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { var ( user string repo string @@ -60,7 +27,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) // 取出user和repo和最后部分 parts := strings.Split(remainingPath, "/") if len(parts) <= 2 { - return "", "", "", ErrInvalidURL + errMsg := "Not enough parts in path after matching 'https://github.com*'" + return "", "", "", NewErrorWithStatusLookup(400, errMsg) } user = parts[0] repo = parts[1] @@ -76,7 +44,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) case "info", "git-upload-pack": matcher = "clone" default: - return "", "", "", ErrInvalidURL + errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher" + return "", "", "", NewErrorWithStatusLookup(400, errMsg) } } return user, repo, matcher, nil @@ -86,7 +55,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) remainingPath := strings.TrimPrefix(rawPath, "https://") parts := strings.Split(remainingPath, "/") if len(parts) <= 3 { - return "", "", "", ErrInvalidURL + errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)." + return "", "", "", NewErrorWithStatusLookup(400, errMsg) } user = parts[1] repo = parts[2] @@ -99,7 +69,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) remainingPath := strings.TrimPrefix(rawPath, "https://") parts := strings.Split(remainingPath, "/") if len(parts) <= 3 { - return "", "", "", ErrInvalidURL + errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)." + return "", "", "", NewErrorWithStatusLookup(400, errMsg) } user = parts[1] repo = "" @@ -121,12 +92,16 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) } if !cfg.Auth.ForceAllowApi { if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { - return "", "", "", ErrAuthHeaderUnavailable + //return "", "", "", ErrAuthHeaderUnavailable + errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy" + return "", "", "", NewErrorWithStatusLookup(403, errMsg) } } return user, repo, matcher, nil } - return "", "", "", ErrNotFound + //return "", "", "", ErrNotFound + errMsg := "Didn't match any matcher" + return "", "", "", NewErrorWithStatusLookup(404, errMsg) } func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) { @@ -164,7 +139,7 @@ func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) { return true, matcher, nil } } - return false, "", ErrInvalidURL + return false, "", nil } // 匹配文件扩展名是sh的rawPath diff --git a/proxy/utils.go b/proxy/utils.go index 615daa2..ba481d0 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -1,10 +1,12 @@ package proxy import ( + "bytes" "fmt" "ghproxy/auth" "ghproxy/config" "ghproxy/rate" + "html/template" "io/fs" "github.com/cloudwego/hertz/pkg/app" @@ -98,13 +100,81 @@ func InitErrPagesFS(pages fs.FS) error { return nil } +type ErrorPageData struct { + StatusCode int + StatusDesc string + StatusText string + HelpInfo string + ErrorMessage string +} + +func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { + return ErrorPageData{ + StatusCode: errInfo.StatusCode, + StatusDesc: errInfo.StatusDesc, + StatusText: errInfo.StatusText, + HelpInfo: errInfo.HelpInfo, + ErrorMessage: errInfo.ErrorMessage, + } +} + +func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { + pageData, _ := htmlTemplateRender(errPagesFs, ErrPageUnwarper(errInfo)) + /* + if err != nil { + c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) + logDebug("Error reading page.tmpl: %v", err) + return + } + */ + fmt.Printf("errInfo: %s\n", errInfo) + c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) + return +} + func NotFoundPage(c *app.RequestContext) { - pageData, err := fs.ReadFile(errPagesFs, "404.html") + /* + pageData, err := fs.ReadFile(errPagesFs, "404.html") + if err != nil { + c.JSON(404, map[string]string{"error": "Not Found"}) + logDebug("Error reading 404.html: %v", err) + return + } + */ + pageData, err := htmlTemplateRender(errPagesFs, ErrorPageData{ + StatusCode: 404, + StatusDesc: "Not Found", + StatusText: "The requested URL was not found on this server.", + ErrorMessage: "The requested URL was not found on this server.", + }) if err != nil { c.JSON(404, map[string]string{"error": "Not Found"}) logDebug("Error reading 404.html: %v", err) return } + c.Data(404, "text/html; charset=utf-8", pageData) return } + +func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) { + tmplPath := "page.tmpl" + tmpl, err := template.ParseFS(fsys, tmplPath) + if err != nil { + return nil, fmt.Errorf("error parsing template: %w", err) + } + if tmpl == nil { + return nil, fmt.Errorf("template is nil") + } + + // 创建一个 bytes.Buffer 用于存储渲染结果 + var buf bytes.Buffer + + err = tmpl.Execute(&buf, data) + if err != nil { + return nil, fmt.Errorf("error executing template: %w", err) + } + + // 返回 buffer 的内容作为 []byte + return buf.Bytes(), nil +} From 4936a93788994954ae1bd153beeb210019bf848e Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:56:34 +0800 Subject: [PATCH 035/118] 25w30d --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e59acad..6bac976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w30d - 2025-04-22 +--- +- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; +- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示 + 25w30c - 2025-04-21 --- - PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index a71ee32..3b627a1 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w30c \ No newline at end of file +25w30d \ No newline at end of file From e42ea358bb014f917eab4743e341cbfda41b6410 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 22 Apr 2025 20:58:44 +0800 Subject: [PATCH 036/118] remove debug output --- DEV-VERSION | 2 +- proxy/utils.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index 3b627a1..89ba98d 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w30d \ No newline at end of file +25w30d diff --git a/proxy/utils.go b/proxy/utils.go index ba481d0..25027b6 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -127,7 +127,6 @@ func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { return } */ - fmt.Printf("errInfo: %s\n", errInfo) c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) return } From b955c915ff2badf400bdbda51ceaceb925fa482c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 24 Apr 2025 01:09:53 +0800 Subject: [PATCH 037/118] fix callback issue --- proxy/handler.go | 37 ++++++++++++++----------------------- proxy/routing.go | 18 +++++++++++++++--- proxy/utils.go | 32 +++++++++++++++++++------------- 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/proxy/handler.go b/proxy/handler.go index dae537b..e7aa236 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -16,7 +16,11 @@ var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { - rateCheck(cfg, c, limiter, iplimiter) + var shoudBreak bool + shoudBreak = rateCheck(cfg, c, limiter, iplimiter) + if shoudBreak { + return + } var ( rawPath string @@ -30,7 +34,6 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 匹配路径错误处理 if len(matches) < 3 { logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) - //c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath) c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid URL Format"}) return } @@ -42,7 +45,6 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra user string repo string matcher string - //err error ) var matcherErr *GHProxyErrors @@ -50,31 +52,20 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra if matcherErr != nil { ErrorPage(c, matcherErr) return - /* - if errors.Is(err, ErrInvalidURL) { - c.JSON(ErrInvalidURL.Code, map[string]string{"error": "Invalid URL Format, Path: " + rawPath}) - logWarning(err.Error()) - return - } - if errors.Is(err, ErrAuthHeaderUnavailable) { - c.JSON(ErrAuthHeaderUnavailable.Code, map[string]string{"error": "AuthHeader Unavailable"}) - logWarning(err.Error()) - return - } - if errors.Is(err, ErrNotFound) { - //c.JSON(ErrNotFound.Code, map[string]string{"error": "Not Found"}) - NotFoundPage(c) - logWarning(err.Error()) - return - } - */ } logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) logDump("%s", c.Request.Header.Header()) - listCheck(cfg, c, user, repo, rawPath) - authCheck(c, cfg, matcher, rawPath) + shoudBreak = listCheck(cfg, c, user, repo, rawPath) + if shoudBreak { + return + } + + shoudBreak = authCheck(c, cfg, matcher, rawPath) + if shoudBreak { + return + } // 处理blob/raw路径 if matcher == "blob" { diff --git a/proxy/routing.go b/proxy/routing.go index c07824c..140c5c5 100644 --- a/proxy/routing.go +++ b/proxy/routing.go @@ -13,7 +13,12 @@ import ( func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { - rateCheck(cfg, c, limiter, iplimiter) + var shoudBreak bool + + shoudBreak = rateCheck(cfg, c, limiter, iplimiter) + if shoudBreak { + return + } var ( rawPath string @@ -34,8 +39,15 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) logDump("%s", c.Request.Header.Header()) - listCheck(cfg, c, user, repo, rawPath) - authCheck(c, cfg, matcher, rawPath) + shoudBreak = listCheck(cfg, c, user, repo, rawPath) + if shoudBreak { + return + } + + shoudBreak = authCheck(c, cfg, matcher, rawPath) + if shoudBreak { + return + } // 处理blob/raw路径 if matcher == "blob" { diff --git a/proxy/utils.go b/proxy/utils.go index 25027b6..cb6dedf 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -12,7 +12,7 @@ import ( "github.com/cloudwego/hertz/pkg/app" ) -func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) { +func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool { var errMsg string // 白名单检查 @@ -22,8 +22,8 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri if !whitelist { errMsg = fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo) c.JSON(403, map[string]string{"error": errMsg}) - logWarning("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) - return + logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + return true } } @@ -34,21 +34,23 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri if blacklist { errMsg = fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo) c.JSON(403, map[string]string{"error": errMsg}) - logWarning("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) - return + logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + return true } } + + return false } // 鉴权 -func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPath string) { +func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPath string) bool { var err error if matcher == "api" && !cfg.Auth.ForceAllowApi { if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { c.JSON(403, map[string]string{"error": "Github API Req without AuthHeader is Not Allowed"}) - logWarning("%s %s %s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) - return + logInfo("%s %s %s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) + return true } } @@ -58,13 +60,15 @@ func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPat authcheck, err = auth.AuthHandler(c, cfg) if !authcheck { c.JSON(401, map[string]string{"error": "Unauthorized"}) - logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) - return + logInfo("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) + return true } } + + return false } -func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) { +func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) bool { // 限制访问频率 if cfg.RateLimit.Enabled { @@ -78,15 +82,17 @@ func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimi default: logWarning("Invalid RateLimit Method") c.JSON(500, map[string]string{"error": "Invalid RateLimit Method"}) - return + return true } if !allowed { c.JSON(429, map[string]string{"error": "Too Many Requests"}) logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) - return + return true } } + + return false } var errPagesFs fs.FS From 7a6544c6c917d2cd28f67de80585bb6c8712fe0a Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 24 Apr 2025 17:50:18 +0800 Subject: [PATCH 038/118] 25w30e --- CHANGELOG.md | 8 ++++ DEV-VERSION | 2 +- auth/auth.go | 4 +- config/config.go | 4 +- proxy/authpass.go | 6 +-- proxy/chunkreq.go | 22 ++------- proxy/error.go | 76 ++++++++++++++++++++++++++++++- proxy/handler.go | 11 +++-- proxy/reqheader.go | 20 --------- proxy/routing.go | 8 ++-- proxy/utils.go | 109 +++------------------------------------------ rate/rate.go | 82 +++++++++++++++++++++++++--------- 12 files changed, 170 insertions(+), 182 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bac976..ab57d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 更新日志 +25w30e - 2025-04-24 +--- +- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; +- CHANGE: 改进`rate`模块, 避免并发竞争问题 +- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理 +- CHANGE: 修改部分log等级 +- FIX: 修正默认配置的填充错误 + 25w30d - 2025-04-22 --- - PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 89ba98d..1fecf83 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w30d +25w30e \ No newline at end of file diff --git a/auth/auth.go b/auth/auth.go index eacfbf6..7ebff30 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -46,7 +46,7 @@ func AuthHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err e logError("Auth method not set") return true, nil } else { - logError("Auth method not supported") - return false, fmt.Errorf(fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method)) + logError("Auth method not supported %s", cfg.Auth.Method) + return false, fmt.Errorf("%s", fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method)) } } diff --git a/config/config.go b/config/config.go index e737a63..984dc9e 100644 --- a/config/config.go +++ b/config/config.go @@ -228,11 +228,11 @@ func DefaultConfig() *Config { }, Blacklist: BlacklistConfig{ Enabled: false, - BlacklistFile: "/data/ghproxy/config/blacklist.txt", + BlacklistFile: "/data/ghproxy/config/blacklist.json", }, Whitelist: WhitelistConfig{ Enabled: false, - WhitelistFile: "/data/ghproxy/config/whitelist.txt", + WhitelistFile: "/data/ghproxy/config/whitelist.json", }, RateLimit: RateLimitConfig{ Enabled: false, diff --git a/proxy/authpass.go b/proxy/authpass.go index e506a9e..16887c7 100644 --- a/proxy/authpass.go +++ b/proxy/authpass.go @@ -18,8 +18,7 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques req.Header.Set("Authorization", "token "+token) } else { logWarning("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol()) - // 500 Internal Server Error - c.JSON(http.StatusInternalServerError, map[string]string{"error": "Conflict Auth Method"}) + ErrorPage(c, NewErrorWithStatusLookup(500, "Conflict Auth Method")) return } case "header": @@ -28,8 +27,7 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques } default: logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol()) - // 500 Internal Server Error - c.JSON(http.StatusInternalServerError, map[string]string{"error": "Invalid Auth Method / Auth Method is not be set"}) + ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid Auth Method / Auth Method is not be set")) return } } diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 1571b27..f68b07f 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -63,8 +63,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c // 错误处理(404) if resp.StatusCode == 404 { - //c.String(http.StatusNotFound, "File Not Found") - c.Status(http.StatusNotFound) + ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)")) return } @@ -89,25 +88,12 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c if err != nil { logError("Failed to close response body: %v", err) } - c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) + c.Redirect(301, []byte(finalURL)) logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize) return } } - /* - for header := range headersToRemove { - resp.Header.Del(header) - } - - for key := range resp.Header { - var values []string = resp.Header.Values(key) - for _, value := range values { - c.Header(key, value) - } - } - */ - // 复制响应头,排除需要移除的 header for key, values := range resp.Header { if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove { @@ -137,16 +123,16 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c compress = "gzip" } - logInfo("Is Shell: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol()) + logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol()) c.Header("Content-Length", "") var reader io.Reader reader, _, err = processLinks(resp.Body, compress, string(c.Request.Host()), cfg) c.SetBodyStream(reader, -1) - 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.Header.GetProtocol(), err) + ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err))) return } } else { diff --git a/proxy/error.go b/proxy/error.go index 2b80593..b5ff39d 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -1,7 +1,10 @@ package proxy import ( - "net/http" + "bytes" + "fmt" + "html/template" + "io/fs" "github.com/WJQSERVER-STUDIO/go-utils/logger" "github.com/cloudwego/hertz/pkg/app" @@ -18,7 +21,7 @@ var ( ) func HandleError(c *app.RequestContext, message string) { - c.JSON(http.StatusInternalServerError, map[string]string{"error": message}) + ErrorPage(c, NewErrorWithStatusLookup(500, message)) logError(message) } @@ -55,6 +58,12 @@ var ( StatusText: "页面未找到", HelpInfo: "抱歉,您访问的页面不存在。", } + ErrTooManyRequests = &GHProxyErrors{ + StatusCode: 429, + StatusDesc: "Too Many Requests", + StatusText: "请求过于频繁", + HelpInfo: "您的请求过于频繁,请稍后再试。", + } ErrInternalServerError = &GHProxyErrors{ StatusCode: 500, StatusDesc: "Internal Server Error", @@ -71,6 +80,7 @@ func init() { ErrAuthHeaderUnavailable.StatusCode: ErrAuthHeaderUnavailable, ErrForbidden.StatusCode: ErrForbidden, ErrNotFound.StatusCode: ErrNotFound, + ErrTooManyRequests.StatusCode: ErrTooManyRequests, ErrInternalServerError.StatusCode: ErrInternalServerError, } } @@ -93,3 +103,65 @@ func NewErrorWithStatusLookup(statusCode int, errMsg string) *GHProxyErrors { } } } + +var errPagesFs fs.FS + +func InitErrPagesFS(pages fs.FS) error { + var err error + errPagesFs, err = fs.Sub(pages, "pages/err") + if err != nil { + return err + } + return nil +} + +type ErrorPageData struct { + StatusCode int + StatusDesc string + StatusText string + HelpInfo string + ErrorMessage string +} + +func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { + return ErrorPageData{ + StatusCode: errInfo.StatusCode, + StatusDesc: errInfo.StatusDesc, + StatusText: errInfo.StatusText, + HelpInfo: errInfo.HelpInfo, + ErrorMessage: errInfo.ErrorMessage, + } +} + +func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { + pageData, err := htmlTemplateRender(errPagesFs, ErrPageUnwarper(errInfo)) + if err != nil { + c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) + logDebug("Error reading page.tmpl: %v", err) + return + } + c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) + return +} + +func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) { + tmplPath := "page.tmpl" + tmpl, err := template.ParseFS(fsys, tmplPath) + if err != nil { + return nil, fmt.Errorf("error parsing template: %w", err) + } + if tmpl == nil { + return nil, fmt.Errorf("template is nil") + } + + // 创建一个 bytes.Buffer 用于存储渲染结果 + var buf bytes.Buffer + + err = tmpl.Execute(&buf, data) + if err != nil { + return nil, fmt.Errorf("error executing template: %w", err) + } + + // 返回 buffer 的内容作为 []byte + return buf.Bytes(), nil +} diff --git a/proxy/handler.go b/proxy/handler.go index e7aa236..e3b51e9 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -2,9 +2,9 @@ package proxy import ( "context" + "fmt" "ghproxy/config" "ghproxy/rate" - "net/http" "regexp" "strings" @@ -29,12 +29,11 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ matches = re.FindStringSubmatch(rawPath) // 匹配路径 - logDebug("URL: %v", matches) // 匹配路径错误处理 if len(matches) < 3 { - logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) - c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid URL Format"}) + logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) + ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.Path()))) return } @@ -80,8 +79,8 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra case "clone": GitReq(ctx, c, rawPath, cfg, "git") default: - c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid input."}) - logError("Invalid input") + ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched")) + logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher) return } } diff --git a/proxy/reqheader.go b/proxy/reqheader.go index c338706..99bef3a 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -6,15 +6,6 @@ import ( "github.com/cloudwego/hertz/pkg/app" ) -/* -// 设置请求头 -func setRequestHeaders(c *app.RequestContext, req *http.Request) { - c.Request.Header.VisitAll(func(key, value []byte) { - req.Header.Set(string(key), string(value)) - }) -} -*/ - func setRequestHeaders(c *app.RequestContext, req *http.Request) { c.Request.Header.VisitAll(func(key, value []byte) { headerKey := string(key) @@ -22,16 +13,5 @@ func setRequestHeaders(c *app.RequestContext, req *http.Request) { if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove { req.Header.Set(headerKey, headerValue) } - }) } - -/* -// removeWSHeader removes the "Upgrade" and "Connection" headers from the given -// Request, which are added by the client when it wants to upgrade the -// connection to a WebSocket connection. -func removeWSHeader(req *http.Request) { - req.Header.Del("Upgrade") - req.Header.Del("Connection") -} -*/ diff --git a/proxy/routing.go b/proxy/routing.go index 140c5c5..893c994 100644 --- a/proxy/routing.go +++ b/proxy/routing.go @@ -4,7 +4,6 @@ import ( "context" "ghproxy/config" "ghproxy/rate" - "net/http" "strings" "github.com/cloudwego/hertz/pkg/app" @@ -57,8 +56,7 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 为rawpath加入https:// 头 rawPath = "https://" + rawPath - // IP METHOD URL USERAGENT PROTO MATCHES - logDebug("%s %s %s %s %s Matched: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), matcher) + logDebug("Matched: %v", matcher) switch matcher { case "releases", "blob", "raw", "gist", "api": @@ -66,8 +64,8 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra case "clone": GitReq(ctx, c, rawPath, cfg, "git") default: - c.JSON(http.StatusForbidden, map[string]string{"error": "Invalid input."}) - logError("Invalid input") + ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched")) + logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher) return } } diff --git a/proxy/utils.go b/proxy/utils.go index cb6dedf..bf51dce 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -1,27 +1,22 @@ package proxy import ( - "bytes" "fmt" "ghproxy/auth" "ghproxy/config" "ghproxy/rate" - "html/template" - "io/fs" "github.com/cloudwego/hertz/pkg/app" ) func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool { - var errMsg string // 白名单检查 if cfg.Whitelist.Enabled { var whitelist bool whitelist = auth.CheckWhitelist(user, repo) if !whitelist { - errMsg = fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo) - c.JSON(403, map[string]string{"error": errMsg}) + ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo))) logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) return true } @@ -32,8 +27,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri var blacklist bool blacklist = auth.CheckBlacklist(user, repo) if blacklist { - errMsg = fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo) - c.JSON(403, map[string]string{"error": errMsg}) + ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo))) logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) return true } @@ -48,7 +42,7 @@ func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPat if matcher == "api" && !cfg.Auth.ForceAllowApi { if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { - c.JSON(403, map[string]string{"error": "Github API Req without AuthHeader is Not Allowed"}) + ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed")) logInfo("%s %s %s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) return true } @@ -59,7 +53,7 @@ func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPat var authcheck bool authcheck, err = auth.AuthHandler(c, cfg) if !authcheck { - c.JSON(401, map[string]string{"error": "Unauthorized"}) + ErrorPage(c, NewErrorWithStatusLookup(401, fmt.Sprintf("Unauthorized: %v", err))) logInfo("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) return true } @@ -81,105 +75,16 @@ func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimi allowed = limiter.Allow() default: logWarning("Invalid RateLimit Method") - c.JSON(500, map[string]string{"error": "Invalid RateLimit Method"}) + ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid RateLimit Method")) return true } if !allowed { - c.JSON(429, map[string]string{"error": "Too Many Requests"}) - logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) + ErrorPage(c, NewErrorWithStatusLookup(429, fmt.Sprintf("Too Many Requests; Rate Limit is %d per minute", cfg.RateLimit.RatePerMinute))) + logInfo("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) return true } } return false } - -var errPagesFs fs.FS - -func InitErrPagesFS(pages fs.FS) error { - var err error - errPagesFs, err = fs.Sub(pages, "pages/err") - if err != nil { - return err - } - return nil -} - -type ErrorPageData struct { - StatusCode int - StatusDesc string - StatusText string - HelpInfo string - ErrorMessage string -} - -func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { - return ErrorPageData{ - StatusCode: errInfo.StatusCode, - StatusDesc: errInfo.StatusDesc, - StatusText: errInfo.StatusText, - HelpInfo: errInfo.HelpInfo, - ErrorMessage: errInfo.ErrorMessage, - } -} - -func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { - pageData, _ := htmlTemplateRender(errPagesFs, ErrPageUnwarper(errInfo)) - /* - if err != nil { - c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) - logDebug("Error reading page.tmpl: %v", err) - return - } - */ - c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) - return -} - -func NotFoundPage(c *app.RequestContext) { - /* - pageData, err := fs.ReadFile(errPagesFs, "404.html") - if err != nil { - c.JSON(404, map[string]string{"error": "Not Found"}) - logDebug("Error reading 404.html: %v", err) - return - } - */ - pageData, err := htmlTemplateRender(errPagesFs, ErrorPageData{ - StatusCode: 404, - StatusDesc: "Not Found", - StatusText: "The requested URL was not found on this server.", - ErrorMessage: "The requested URL was not found on this server.", - }) - if err != nil { - c.JSON(404, map[string]string{"error": "Not Found"}) - logDebug("Error reading 404.html: %v", err) - return - } - - c.Data(404, "text/html; charset=utf-8", pageData) - return -} - -func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) { - tmplPath := "page.tmpl" - tmpl, err := template.ParseFS(fsys, tmplPath) - if err != nil { - return nil, fmt.Errorf("error parsing template: %w", err) - } - if tmpl == nil { - return nil, fmt.Errorf("template is nil") - } - - // 创建一个 bytes.Buffer 用于存储渲染结果 - var buf bytes.Buffer - - err = tmpl.Execute(&buf, data) - if err != nil { - return nil, fmt.Errorf("error executing template: %w", err) - } - - // 返回 buffer 的内容作为 []byte - return buf.Bytes(), nil -} diff --git a/rate/rate.go b/rate/rate.go index a1237b2..8305801 100644 --- a/rate/rate.go +++ b/rate/rate.go @@ -1,13 +1,14 @@ package rate import ( + "sync" "time" "github.com/WJQSERVER-STUDIO/go-utils/logger" "golang.org/x/time/rate" ) -// 日志输出 +// 日志模块 var ( logw = logger.Logw logDump = logger.LogDump @@ -17,49 +18,90 @@ var ( logError = logger.LogError ) -// 总体限流器 +// RateLimiter 总体限流器 type RateLimiter struct { limiter *rate.Limiter } -// 基于IP的限流器 -type IPRateLimiter struct { - limiters map[string]*RateLimiter - limit int - burst int - duration time.Duration -} - +// New 创建一个总体限流器 func New(limit int, burst int, duration time.Duration) *RateLimiter { + if limit <= 0 { + limit = 1 + logWarning("rate limit per minute must be positive, setting to 1") + } + if burst <= 0 { + burst = 1 + logWarning("rate limit burst must be positive, setting to 1") + } + + rateLimit := rate.Limit(float64(limit) / duration.Seconds()) + return &RateLimiter{ - limiter: rate.NewLimiter(rate.Limit(float64(limit)/duration.Seconds()), burst), + limiter: rate.NewLimiter(rateLimit, burst), } } +// Allow 检查是否允许请求通过 func (rl *RateLimiter) Allow() bool { return rl.limiter.Allow() } -func NewIPRateLimiter(limit int, burst int, duration time.Duration) *IPRateLimiter { +// IPRateLimiter 基于IP的限流器 +type IPRateLimiter struct { + limiters map[string]*RateLimiter // 用户级限流器 map + mu sync.RWMutex // 保护 limiters map + limit int // 每 duration 时间段内允许的请求数 + burst int // 突发请求数 + duration time.Duration // 限流周期 +} + +// NewIPRateLimiter 创建一个基于IP的限流器 +func NewIPRateLimiter(ipLimit int, ipBurst int, duration time.Duration) *IPRateLimiter { + if ipLimit <= 0 { + ipLimit = 1 + logWarning("IP rate limit per minute must be positive, setting to 1") + } + if ipBurst <= 0 { + ipBurst = 1 + logWarning("IP rate limit burst must be positive, setting to 1") + } + + logInfo("IP Rate Limiter initialized with limit: %d, burst: %d, duration: %v", ipLimit, ipBurst, duration) + return &IPRateLimiter{ limiters: make(map[string]*RateLimiter), - limit: limit, - burst: burst, + limit: ipLimit, + burst: ipBurst, duration: duration, } } +// Allow 检查给定IP的请求是否允许通过 func (rl *IPRateLimiter) Allow(ip string) bool { if ip == "" { - logWarning("empty ip") + logWarning("empty ip for rate limiting") return false } - limiter, ok := rl.limiters[ip] - if !ok { - // 创建新的 RateLimiter 并存储 - limiter = New(rl.limit, rl.burst, rl.duration) - rl.limiters[ip] = limiter + // 使用读锁快速查找 + rl.mu.RLock() + limiter, found := rl.limiters[ip] + rl.mu.RUnlock() + + if found { + return limiter.Allow() } + + // 未找到,获取写锁来创建和添加 + rl.mu.Lock() + // 双重检查 + limiter, found = rl.limiters[ip] + if !found { + newL := New(rl.limit, rl.burst, rl.duration) + rl.limiters[ip] = newL + limiter = newL + } + rl.mu.Unlock() + return limiter.Allow() } From 8ccf48a6fef1698a02e8a225f9abbfe85c26d7c6 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:11:13 +0800 Subject: [PATCH 039/118] fix && update --- go.mod | 2 +- go.sum | 4 ++-- proxy/utils.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 2c6514f..1820f16 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/nyaruka/phonenumbers v1.6.0 // indirect + github.com/nyaruka/phonenumbers v1.6.1 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/go.sum b/go.sum index b036c8b..b6da4cc 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,8 @@ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgSh github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/nyaruka/phonenumbers v1.6.0 h1:r9ax45fFg+YLUs2X4bNXm5RAxWl00hYjFgNlv32vtHk= -github.com/nyaruka/phonenumbers v1.6.0/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940= +github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/satomitouka/touka-httpc v0.4.0 h1:cnOONdyJHJImMY8L64bvYF+7Ow/5CPf2Yr3RQRRMZOU= diff --git a/proxy/utils.go b/proxy/utils.go index bf51dce..21e7886 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -43,7 +43,7 @@ func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPat if matcher == "api" && !cfg.Auth.ForceAllowApi { if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed")) - logInfo("%s %s %s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) + logInfo("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath) return true } } From 47de48bccee4b68ac5de153d14625e81f46b185c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 24 Apr 2025 18:27:15 +0800 Subject: [PATCH 040/118] 3.1.0 --- CHANGELOG.md | 14 ++++++++++++++ VERSION | 2 +- proxy/handler.go | 2 +- proxy/routing.go | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab57d36..9f96690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # 更新日志 +3.1.0 - 2025-04-24 +--- +- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率 +- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader` +- CHANGE: 使用`HertZ`的`requestContext`传递matcher参数, 而不是`25w30a`中的ctx +- CHANGE: 改进`rate`模块, 避免并发竞争问题 +- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理 +- CHANGE: 修改部分log等级 +- FIX: 修正默认配置的填充错误 +- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示 +- CHANGE: 改进handle, 复用共同部分 +- CHANGE: 细化url匹配的返回码处理 +- CHANGE: 增加404界面 + 25w30e - 2025-04-24 --- - PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 282895a..a0cd9f0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.3 \ No newline at end of file +3.1.0 \ No newline at end of file diff --git a/proxy/handler.go b/proxy/handler.go index e3b51e9..bdd7ecb 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -53,7 +53,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra return } - logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) logDump("%s", c.Request.Header.Header()) shoudBreak = listCheck(cfg, c, user, repo, rawPath) diff --git a/proxy/routing.go b/proxy/routing.go index 893c994..a3135ec 100644 --- a/proxy/routing.go +++ b/proxy/routing.go @@ -35,7 +35,7 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra repo = c.Param("repo") matcher = c.GetString("matcher") - logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) + logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) logDump("%s", c.Request.Header.Header()) shoudBreak = listCheck(cfg, c, user, repo, rawPath) From a7be65a111bce5b7cb72a4c71eb9f6c31953a71c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:14:33 +0800 Subject: [PATCH 041/118] 25w31t-1 --- DEV-VERSION | 2 +- main.go | 4 ++++ proxy/chunkreq.go | 1 - proxy/ghcr.go | 14 ++++++++++++++ 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 proxy/ghcr.go diff --git a/DEV-VERSION b/DEV-VERSION index 1fecf83..8dfa506 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w30e \ No newline at end of file +25w31t-1 \ No newline at end of file diff --git a/main.go b/main.go index 5314f31..8dce53f 100644 --- a/main.go +++ b/main.go @@ -459,6 +459,10 @@ func main() { proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) + r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) { + proxy.GhcrRouting(cfg)(ctx, c) + }) + r.NoRoute(func(ctx context.Context, c *app.RequestContext) { proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) }) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index f68b07f..13be45a 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -52,7 +52,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c } setRequestHeaders(c, req) - //removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) AuthPassThrough(c, cfg, req) resp, err = client.Do(req) diff --git a/proxy/ghcr.go b/proxy/ghcr.go new file mode 100644 index 0000000..71d764d --- /dev/null +++ b/proxy/ghcr.go @@ -0,0 +1,14 @@ +package proxy + +import ( + "context" + "ghproxy/config" + + "github.com/cloudwego/hertz/pkg/app" +) + +func GhcrRouting(cfg *config.Config) app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + ChunkedProxyRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + } +} From 52d6f8e759013c78b74fc0a147c2575c2342a373 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Apr 2025 17:56:22 +0800 Subject: [PATCH 042/118] update readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0209413..871c644 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # GHProxy -![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) +![GitHub Release](https://img.shields.io/github/v/release/WJQSERVER-STUDIO/ghproxy?display_name=tag&style=flat) +![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg) +![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest) +![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/WJQSERVER-STUDIO/ghproxy) +[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) + 支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能 From 8aef197fde0e8389b20f3409fcf20f4db4f290e7 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:14:23 +0800 Subject: [PATCH 043/118] 25w31t-2 --- DEV-VERSION | 2 +- go.mod | 4 ++- go.sum | 4 +-- proxy/ghcr.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 5 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index 8dfa506..98260d3 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w31t-1 \ No newline at end of file +25w31t-2 \ No newline at end of file diff --git a/go.mod b/go.mod index 1820f16..4144ee8 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 - github.com/satomitouka/touka-httpc v0.4.0 + github.com/satomitouka/touka-httpc v0.4.1 golang.org/x/net v0.39.0 golang.org/x/time v0.11.0 ) @@ -36,3 +36,5 @@ require ( golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) + +//replace github.com/satomitouka/touka-httpc v0.4.1 => /data/github/satomitoka/touka-httpc diff --git a/go.sum b/go.sum index b6da4cc..47257b7 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,8 @@ github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/satomitouka/touka-httpc v0.4.0 h1:cnOONdyJHJImMY8L64bvYF+7Ow/5CPf2Yr3RQRRMZOU= -github.com/satomitouka/touka-httpc v0.4.0/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA= +github.com/satomitouka/touka-httpc v0.4.1 h1:K1LJwSJJKRPkol6MPOEzc8bReAIUqxVuzdFfTAi/2AI= +github.com/satomitouka/touka-httpc v0.4.1/go.mod h1:E1JeXw81XclzvlqVvSio/GcDmvN8wWLPpbNRN42Uwfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= diff --git a/proxy/ghcr.go b/proxy/ghcr.go index 71d764d..c3e9ca3 100644 --- a/proxy/ghcr.go +++ b/proxy/ghcr.go @@ -2,13 +2,101 @@ package proxy import ( "context" + "fmt" "ghproxy/config" + "net/http" + "strconv" "github.com/cloudwego/hertz/pkg/app" ) func GhcrRouting(cfg *config.Config) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { - ChunkedProxyRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") } } + +func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { + + var ( + method []byte + req *http.Request + resp *http.Response + err error + ) + + method = c.Request.Method() + + rb := client.NewRequestBuilder(string(method), u) + rb.NoDefaultHeaders() + + //req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) + req, err = rb.Build() + if err != nil { + HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) + return + } + + c.Request.Header.VisitAll(func(key, value []byte) { + headerKey := string(key) + headerValue := string(value) + req.Header.Add(headerKey, headerValue) + }) + + resp, err = client.Do(req) + if err != nil { + HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) + return + } + + // 错误处理(404) + if resp.StatusCode == 404 { + ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)")) + return + } + + var ( + bodySize int + contentLength string + sizelimit int + ) + + sizelimit = cfg.Server.SizeLimit * 1024 * 1024 + contentLength = resp.Header.Get("Content-Length") + if contentLength != "" { + var err error + bodySize, err = strconv.Atoi(contentLength) + if err != nil { + logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err) + bodySize = -1 + } + if err == nil && bodySize > sizelimit { + var finalURL string + finalURL = resp.Request.URL.String() + err = resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } + c.Redirect(301, []byte(finalURL)) + logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize) + return + } + } + + // 复制响应头,排除需要移除的 header + for key, values := range resp.Header { + for _, value := range values { + //c.Header(key, value) + c.Response.Header.Add(key, value) + } + } + + c.Status(resp.StatusCode) + + if contentLength != "" { + c.SetBodyStream(resp.Body, bodySize) + return + } + c.SetBodyStream(resp.Body, -1) + +} From f540b2edcd4725d0c836b7d66e416bb08ec8fe1b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 27 Apr 2025 15:57:06 +0800 Subject: [PATCH 044/118] fix user name match issue --- main.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/main.go b/main.go index 8dce53f..fe6039d 100644 --- a/main.go +++ b/main.go @@ -415,46 +415,46 @@ func main() { setupApi(cfg, r, version) setupPages(cfg, r) - r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "release") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "release") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "blob") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "raw") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "gitclone") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { + r.GET("/github.com/:user/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "gitclone") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "raw") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/gist.githubusercontent.com/:user/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "gist") proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "api") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) From d94f6c0f5db1cbc595ad478749ec351ac8b276b4 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 27 Apr 2025 16:39:47 +0800 Subject: [PATCH 045/118] 25w31a --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- config/config.go | 15 +++++++++++++++ config/config.toml | 6 +++++- deploy/config.toml | 4 ++++ docs/config.md | 19 +++++++++++++++++++ proxy/{ghcr.go => docker.go} | 14 +++++++++++++- 7 files changed, 63 insertions(+), 3 deletions(-) rename proxy/{ghcr.go => docker.go} (82%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f96690..befb5e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w31a - 2025-04-27 +--- +- PRE-RELEASE: 此版本是v3.2.0预发布版本,请勿在生产环境中使用; +- CHANGE: 加入`ghcr`和`dockerhub`反代功能 +- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题 + 3.1.0 - 2025-04-24 --- - CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率 diff --git a/DEV-VERSION b/DEV-VERSION index 98260d3..b7b50a1 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w31t-2 \ No newline at end of file +25w31a \ No newline at end of file diff --git a/config/config.go b/config/config.go index 984dc9e..ed62121 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,7 @@ type Config struct { Whitelist WhitelistConfig RateLimit RateLimitConfig Outbound OutboundConfig + Docker DockerConfig } /* @@ -143,6 +144,16 @@ type OutboundConfig struct { Url string `toml:"url"` } +/* +[docker] +enabled = false +target = "ghcr" # ghcr/dockerhub +*/ +type DockerConfig struct { + Enabled bool `toml:"enabled"` + Target string `toml:"target"` +} + // LoadConfig 从 TOML 配置文件加载配置 func LoadConfig(filePath string) (*Config, error) { if !FileExists(filePath) { @@ -244,5 +255,9 @@ func DefaultConfig() *Config { Enabled: false, Url: "socks5://127.0.0.1:1080", }, + Docker: DockerConfig{ + Enabled: false, + Target: "ghcr", + }, } } diff --git a/config/config.toml b/config/config.toml index ac1ee59..fc47ef1 100644 --- a/config/config.toml +++ b/config/config.toml @@ -58,4 +58,8 @@ burst = 5 [outbound] enabled = false -url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" \ No newline at end of file +url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" + +[docker] +enabled = false +target = "ghcr" # ghcr/dockerhub \ No newline at end of file diff --git a/deploy/config.toml b/deploy/config.toml index b88e30b..eb6d7b8 100644 --- a/deploy/config.toml +++ b/deploy/config.toml @@ -58,3 +58,7 @@ burst = 5 [outbound] enabled = false url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" + +[docker] +enabled = false +target = "ghcr" # ghcr/dockerhub \ No newline at end of file diff --git a/docs/config.md b/docs/config.md index bad8c65..1a91225 100644 --- a/docs/config.md +++ b/docs/config.md @@ -70,6 +70,10 @@ burst = 5 [outbound] enabled = false url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" + +[docker] +enabled = false +target = "ghcr" # ghcr/dockerhub ``` ### 配置项详细说明 @@ -295,6 +299,21 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" * 支持协议: `socks5://` 和 `http://` * 说明: 设置出站代理服务器的 URL。支持 SOCKS5 和 HTTP 代理协议。 +* **`[docker]` - Docker 镜像代理配置** + + * `enabled`: 是否启用 Docker 镜像代理功能。 + * 类型: 布尔值 (`bool`) + * 默认值: `false` (禁用) + * 说明: 当设置为 `true` 时,`ghproxy` 将尝试代理 Docker 镜像的下载请求,以加速从 GitHub Container Registry (GHCR) 或 Docker Hub 下载镜像。 + + * `target`: 代理的目标 Docker 注册表。 + * 类型: 字符串 (`string`) + * 默认值: `"ghcr"` (代理 GHCR) + * 可选值: `"ghcr"` 或 `"dockerhub"` + * 说明: 指定要代理的 Docker 注册表。 + * `"ghcr"`: 代理 GitHub Container Registry (ghcr.io)。 + * `"dockerhub"`: 代理 Docker Hub (docker.io)。 + ## `blacklist.json` - 黑名单配置 `blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。 diff --git a/proxy/ghcr.go b/proxy/docker.go similarity index 82% rename from proxy/ghcr.go rename to proxy/docker.go index c3e9ca3..8fc5f57 100644 --- a/proxy/ghcr.go +++ b/proxy/docker.go @@ -12,7 +12,19 @@ import ( func GhcrRouting(cfg *config.Config) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { - GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + if cfg.Docker.Enabled { + if cfg.Docker.Target == "ghcr" { + GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + } else if cfg.Docker.Target == "dockerhub" { + GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub") + } else { + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not Allowed")) + return + } + } else { + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed")) + return + } } } From bf92cc8429193e552e003065ed3085ee16fe8458 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 27 Apr 2025 17:33:17 +0800 Subject: [PATCH 046/118] add req body --- proxy/docker.go | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/docker.go b/proxy/docker.go index 8fc5f57..6347f24 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -41,6 +41,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf rb := client.NewRequestBuilder(string(method), u) rb.NoDefaultHeaders() + rb.SetBody(c.Request.BodyStream()) //req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) req, err = rb.Build() From 086aa999e1877aa7f4bbbeade2c8ab4b0c76dd81 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 27 Apr 2025 17:38:30 +0800 Subject: [PATCH 047/118] 3.2.0 --- CHANGELOG.md | 5 +++++ VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index befb5e8..05800b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +3.2.0 - 2025-04-27 +--- +- CHANGE: 加入`ghcr`和`dockerhub`反代功能 +- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题 + 25w31a - 2025-04-27 --- - PRE-RELEASE: 此版本是v3.2.0预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index a0cd9f0..a4f52a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.0 \ No newline at end of file +3.2.0 \ No newline at end of file From a0cca13debb411a29553bc09545f81ac63fecdad Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 07:22:03 +0800 Subject: [PATCH 048/118] fix matcher key issue --- main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index fe6039d..1bfc76f 100644 --- a/main.go +++ b/main.go @@ -416,12 +416,12 @@ func main() { setupPages(cfg, r) r.GET("/github.com/:user/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { - c.Set("matcher", "release") + c.Set("matcher", "releases") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/github.com/:user/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { - c.Set("matcher", "release") + c.Set("matcher", "releases") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) @@ -436,11 +436,11 @@ func main() { }) r.GET("/github.com/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { - c.Set("matcher", "gitclone") + c.Set("matcher", "clone") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) r.GET("/github.com/:user/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { - c.Set("matcher", "gitclone") + c.Set("matcher", "clone") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) From dfc49ae28b12d5d068a9ecd1e7a336ca25fbb15c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 07:24:28 +0800 Subject: [PATCH 049/118] 25w32a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05800b3..ef7201a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w32a - 2025-04-29 +--- +- PRE-RELEASE: 此版本是v3.2.1预发布版本,请勿在生产环境中使用; +- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题 + 3.2.0 - 2025-04-27 --- - CHANGE: 加入`ghcr`和`dockerhub`反代功能 diff --git a/DEV-VERSION b/DEV-VERSION index b7b50a1..ca629c1 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w31a \ No newline at end of file +25w32a \ No newline at end of file From 6ca31bc25200ce8e4d8feaf3bbbc3a317b36a901 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 07:43:19 +0800 Subject: [PATCH 050/118] 3.2.1 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7201a..1f1aeac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.2.1 - 2025-04-29 +--- +- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题 + 25w32a - 2025-04-29 --- - PRE-RELEASE: 此版本是v3.2.1预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index a4f52a5..0444f32 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.0 \ No newline at end of file +3.2.1 \ No newline at end of file From 55769d9a400bf06ebd369ed4c686667aa14ca19d Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 19:51:23 +0800 Subject: [PATCH 051/118] use custom headers for raw --- DEV-VERSION | 2 +- proxy/chunkreq.go | 2 +- proxy/gitreq.go | 4 ++-- proxy/reqheader.go | 31 ++++++++++++++++++++++++------- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index ca629c1..f9c9056 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w32a \ No newline at end of file +25w33t-1 \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 13be45a..e766a06 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -51,7 +51,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } - setRequestHeaders(c, req) + setRequestHeaders(c, req, matcher) AuthPassThrough(c, cfg, req) resp, err = client.Do(req) diff --git a/proxy/gitreq.go b/proxy/gitreq.go index b8ba66d..2a4b57b 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -35,7 +35,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return } - setRequestHeaders(c, req) + setRequestHeaders(c, req, "clone") //removeWSHeader(req) AuthPassThrough(c, cfg, req) @@ -50,7 +50,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return } - setRequestHeaders(c, req) + setRequestHeaders(c, req, "clone") //removeWSHeader(req) AuthPassThrough(c, cfg, req) diff --git a/proxy/reqheader.go b/proxy/reqheader.go index 99bef3a..24baaae 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -6,12 +6,29 @@ import ( "github.com/cloudwego/hertz/pkg/app" ) -func setRequestHeaders(c *app.RequestContext, req *http.Request) { - c.Request.Header.VisitAll(func(key, value []byte) { - headerKey := string(key) - headerValue := string(value) - if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove { - req.Header.Set(headerKey, headerValue) +// 预定义headers +var ( + defaultHeaders = map[string]string{ + "Accept": "*/*", + "Accept-Encoding": "gzip", + "Transfer-Encoding": "chunked", + "User-Agent": "GHProxy/1.0", + } +) + +func setRequestHeaders(c *app.RequestContext, req *http.Request, matcher string) { + if matcher == "raw" { + // 使用预定义Header + for key, value := range defaultHeaders { + req.Header.Set(key, value) } - }) + } else { + c.Request.Header.VisitAll(func(key, value []byte) { + headerKey := string(key) + headerValue := string(value) + if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove { + req.Header.Set(headerKey, headerValue) + } + }) + } } From 3f51e5319ad1cf10aa5beb7d9ad4884122b1ea16 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 20:39:41 +0800 Subject: [PATCH 052/118] 25w33a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- config/config.go | 2 ++ config/config.toml | 1 + proxy/chunkreq.go | 25 +------------------------ proxy/gitreq.go | 8 ++------ proxy/reqheader.go | 45 +++++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 55 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1aeac..5a8c609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w33a - 2025-04-29 +--- +- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制 +- FIX: 修正Header部分的一些处理问题 + 3.2.1 - 2025-04-29 --- - FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题 diff --git a/DEV-VERSION b/DEV-VERSION index f9c9056..23f75d9 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w33t-1 \ No newline at end of file +25w33a \ No newline at end of file diff --git a/config/config.go b/config/config.go index ed62121..14c9301 100644 --- a/config/config.go +++ b/config/config.go @@ -50,12 +50,14 @@ mode = "auto" # "auto" or "advanced" maxIdleConns = 100 # only for advanced mode maxIdleConnsPerHost = 60 # only for advanced mode maxConnsPerHost = 0 # only for advanced mode +useCustomRawHeaders = false */ type HttpcConfig struct { Mode string `toml:"mode"` MaxIdleConns int `toml:"maxIdleConns"` MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"` MaxConnsPerHost int `toml:"maxConnsPerHost"` + UseCustomRawHeaders bool `toml:"useCustomRawHeaders"` } /* diff --git a/config/config.toml b/config/config.toml index fc47ef1..b43ebf3 100644 --- a/config/config.toml +++ b/config/config.toml @@ -13,6 +13,7 @@ mode = "auto" # "auto" or "advanced" maxIdleConns = 100 # only for advanced mode maxIdleConnsPerHost = 60 # only for advanced mode maxConnsPerHost = 0 # only for advanced mode +useCustomRawHeaders = false [gitclone] mode = "bypass" # bypass / cache diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index e766a06..22c7b61 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -11,29 +11,6 @@ import ( "github.com/cloudwego/hertz/pkg/app" ) -var ( - respHeadersToRemove = map[string]struct{}{ - "Content-Security-Policy": {}, - "Referrer-Policy": {}, - "Strict-Transport-Security": {}, - "X-Github-Request-Id": {}, - "X-Timer": {}, - "X-Served-By": {}, - "X-Fastly-Request-Id": {}, - } - - reqHeadersToRemove = map[string]struct{}{ - "CF-IPCountry": {}, - "CF-RAY": {}, - "CF-Visitor": {}, - "CF-Connecting-IP": {}, - "CF-EW-Via": {}, - "CDN-Loop": {}, - "Upgrade": {}, - "Connection": {}, - } -) - func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { var ( @@ -51,7 +28,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } - setRequestHeaders(c, req, matcher) + setRequestHeaders(c, req, cfg, matcher) AuthPassThrough(c, cfg, req) resp, err = client.Do(req) diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 2a4b57b..e48598f 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -13,7 +13,6 @@ import ( func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) { method := string(c.Request.Method()) - logDump("Url Before FMT:%s", u) if cfg.GitClone.Mode == "cache" { userPath, repoPath, remainingPath, queryParams, err := extractParts(u) if err != nil { @@ -22,7 +21,6 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co } // 构建新url u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode() - logDump("New Url After FMT:%s", u) } var ( @@ -35,8 +33,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return } - setRequestHeaders(c, req, "clone") - //removeWSHeader(req) + setRequestHeaders(c, req, cfg, "clone") AuthPassThrough(c, cfg, req) resp, err = gitclient.Do(req) @@ -50,8 +47,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return } - setRequestHeaders(c, req, "clone") - //removeWSHeader(req) + setRequestHeaders(c, req, cfg, "clone") AuthPassThrough(c, cfg, req) resp, err = client.Do(req) diff --git a/proxy/reqheader.go b/proxy/reqheader.go index 24baaae..8400821 100644 --- a/proxy/reqheader.go +++ b/proxy/reqheader.go @@ -1,11 +1,44 @@ package proxy import ( + "ghproxy/config" "net/http" "github.com/cloudwego/hertz/pkg/app" ) +var ( + respHeadersToRemove = map[string]struct{}{ + "Content-Security-Policy": {}, + "Referrer-Policy": {}, + "Strict-Transport-Security": {}, + "X-Github-Request-Id": {}, + "X-Timer": {}, + "X-Served-By": {}, + "X-Fastly-Request-Id": {}, + } + + reqHeadersToRemove = map[string]struct{}{ + "CF-IPCountry": {}, + "CF-RAY": {}, + "CF-Visitor": {}, + "CF-Connecting-IP": {}, + "CF-EW-Via": {}, + "CDN-Loop": {}, + "Upgrade": {}, + "Connection": {}, + } + + cloneHeadersToRemove = map[string]struct{}{ + "CF-IPCountry": {}, + "CF-RAY": {}, + "CF-Visitor": {}, + "CF-Connecting-IP": {}, + "CF-EW-Via": {}, + "CDN-Loop": {}, + } +) + // 预定义headers var ( defaultHeaders = map[string]string{ @@ -16,12 +49,20 @@ var ( } ) -func setRequestHeaders(c *app.RequestContext, req *http.Request, matcher string) { - if matcher == "raw" { +func setRequestHeaders(c *app.RequestContext, req *http.Request, cfg *config.Config, matcher string) { + if matcher == "raw" && cfg.Httpc.UseCustomRawHeaders { // 使用预定义Header for key, value := range defaultHeaders { req.Header.Set(key, value) } + } else if matcher == "clone" { + c.Request.Header.VisitAll(func(key, value []byte) { + headerKey := string(key) + headerValue := string(value) + if _, shouldRemove := cloneHeadersToRemove[headerKey]; !shouldRemove { + req.Header.Set(headerKey, headerValue) + } + }) } else { c.Request.Header.VisitAll(func(key, value []byte) { headerKey := string(key) From ace795fe9df55689ed529696dd9e9d127d1308bb Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:13:11 +0800 Subject: [PATCH 053/118] revert gitreq body stream --- CHANGELOG.md | 4 ++++ DEV-VERSION | 2 +- proxy/gitreq.go | 28 +++++++++++++++++++++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8c609..f6bdb26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +25w33b - 2025-04-29 +--- +- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改 + 25w33a - 2025-04-29 --- - ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制 diff --git a/DEV-VERSION b/DEV-VERSION index 23f75d9..58d1f65 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w33a \ No newline at end of file +25w33b \ No newline at end of file diff --git a/proxy/gitreq.go b/proxy/gitreq.go index e48598f..a050e29 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -1,6 +1,7 @@ package proxy import ( + "bytes" "context" "fmt" "ghproxy/config" @@ -13,6 +14,10 @@ import ( func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) { method := string(c.Request.Method()) + bodyReader := bytes.NewBuffer(c.Request.Body()) + + //bodyReader := c.Request.BodyStream() + if cfg.GitClone.Mode == "cache" { userPath, repoPath, remainingPath, queryParams, err := extractParts(u) if err != nil { @@ -28,11 +33,16 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co ) if cfg.GitClone.Mode == "cache" { - req, err := gitclient.NewRequest(method, u, c.Request.BodyStream()) + rb := gitclient.NewRequestBuilder(method, u) + rb.NoDefaultHeaders() + rb.SetBody(bodyReader) + + req, err := rb.Build() if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return } + setRequestHeaders(c, req, cfg, "clone") AuthPassThrough(c, cfg, req) @@ -42,11 +52,16 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co return } } else { - req, err := client.NewRequest(method, u, c.Request.BodyStream()) + rb := client.NewRequestBuilder(string(c.Request.Method()), u) + rb.NoDefaultHeaders() + rb.SetBody(bodyReader) + + req, err := rb.Build() if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return } + setRequestHeaders(c, req, cfg, "clone") AuthPassThrough(c, cfg, req) @@ -74,7 +89,8 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co for key, values := range resp.Header { for _, value := range values { - c.Header(key, value) + //c.Header(key, value) + c.Response.Header.Add(key, value) } } @@ -106,5 +122,11 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co c.Response.Header.Set("Expires", "0") } + bodySize, _ := strconv.Atoi(contentLength) + + if contentLength != "" { + c.SetBodyStream(resp.Body, bodySize) + return + } c.SetBodyStream(resp.Body, -1) } From e57432a01c32b1a0e2cfcb3f006f670dddd8d429 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:21:08 +0800 Subject: [PATCH 054/118] 3.2.2 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6bdb26..09baf1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +3.2.2 - 2025-04-29 +--- +- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制 +- FIX: 修正Header部分的一些处理问题 +- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改 + 25w33b - 2025-04-29 --- - REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改 diff --git a/VERSION b/VERSION index 0444f32..acf9bf0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.1 \ No newline at end of file +3.2.2 \ No newline at end of file From 030f0d12a923fc8b44d1173e8eba7dca0f32beaf Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:24:11 +0800 Subject: [PATCH 055/118] update docs --- docs/config.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/config.md b/docs/config.md index 1a91225..c146ca4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -24,6 +24,7 @@ mode = "auto" # "auto" or "advanced" maxIdleConns = 100 # only for advanced mode maxIdleConnsPerHost = 60 # only for advanced mode maxConnsPerHost = 0 # only for advanced mode +useCustomRawHeaders = false [gitclone] mode = "bypass" # bypass / cache @@ -138,6 +139,10 @@ target = "ghcr" # ghcr/dockerhub * 类型: 整数 (`int`) * 默认值: `0` (不限制) * 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。 + * `useCustomRawHeaders`: 使用预定义header避免github waf对应zh-CN的封锁 + * 类型: 布尔值(`bool`) + * 默认值: `false`(停用) + * 说明: 启用后, 拉取raw文件会使用程序预定义的固定headers, 而不是原先的复制行为 * **`[gitclone]` - Git 克隆配置** From ad4d8eb670b6ed93d34c117cfcc14fe36202df1a Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:25:20 +0800 Subject: [PATCH 056/118] remove unuse code --- proxy/gitreq.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/proxy/gitreq.go b/proxy/gitreq.go index a050e29..81293cd 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -122,11 +122,5 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co c.Response.Header.Set("Expires", "0") } - bodySize, _ := strconv.Atoi(contentLength) - - if contentLength != "" { - c.SetBodyStream(resp.Body, bodySize) - return - } c.SetBodyStream(resp.Body, -1) } From 3c11e9826e6543237d8a7cc6c011af26cd354c87 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 5 May 2025 10:41:41 +0800 Subject: [PATCH 057/118] change touka-httpc to httpc(touka/toka) --- go.mod | 4 +--- go.sum | 4 ++-- proxy/httpc.go | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 4144ee8..ecf151a 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.24.2 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 + github.com/WJQSERVER-STUDIO/httpc v0.5.0 github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 - github.com/satomitouka/touka-httpc v0.4.1 golang.org/x/net v0.39.0 golang.org/x/time v0.11.0 ) @@ -36,5 +36,3 @@ require ( golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) - -//replace github.com/satomitouka/touka-httpc v0.4.1 => /data/github/satomitoka/touka-httpc diff --git a/go.sum b/go.sum index 47257b7..09cb196 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YY github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c= github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po= +github.com/WJQSERVER-STUDIO/httpc v0.5.0 h1:0yJA+dOgbnO3R/mAWPjlbUq5lIqaxRV38XfiX3jt6pg= +github.com/WJQSERVER-STUDIO/httpc v0.5.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= @@ -50,8 +52,6 @@ github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/satomitouka/touka-httpc v0.4.1 h1:K1LJwSJJKRPkol6MPOEzc8bReAIUqxVuzdFfTAi/2AI= -github.com/satomitouka/touka-httpc v0.4.1/go.mod h1:E1JeXw81XclzvlqVvSio/GcDmvN8wWLPpbNRN42Uwfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= diff --git a/proxy/httpc.go b/proxy/httpc.go index dae57b6..120d8a7 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - httpc "github.com/satomitouka/touka-httpc" + "github.com/WJQSERVER-STUDIO/httpc" ) var BufferSize int = 32 * 1024 // 32KB From bd63ed3070ca97afe2af51cb08beb0be6856a928 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 5 May 2025 15:15:18 +0800 Subject: [PATCH 058/118] change to new logger(enabled async log) --- api/api.go | 2 +- auth/auth.go | 2 +- go.mod | 4 ++-- go.sum | 8 ++++---- main.go | 2 +- middleware/loggin/loggin.go | 2 +- proxy/error.go | 2 +- rate/rate.go | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/api/api.go b/api/api.go index 3f9adc7..0c34ac0 100644 --- a/api/api.go +++ b/api/api.go @@ -5,7 +5,7 @@ import ( "ghproxy/config" "ghproxy/middleware/nocache" - "github.com/WJQSERVER-STUDIO/go-utils/logger" + "github.com/WJQSERVER-STUDIO/logger" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app/server" ) diff --git a/auth/auth.go b/auth/auth.go index 7ebff30..d817dc4 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -4,7 +4,7 @@ import ( "fmt" "ghproxy/config" - "github.com/WJQSERVER-STUDIO/go-utils/logger" + "github.com/WJQSERVER-STUDIO/logger" "github.com/cloudwego/hertz/pkg/app" ) diff --git a/go.mod b/go.mod index ecf151a..45aad0b 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,8 @@ go 1.24.2 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.5.0 + github.com/WJQSERVER-STUDIO/logger v1.6.0 github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.39.0 @@ -14,7 +14,7 @@ require ( require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect - github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 // indirect + github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 // indirect github.com/bytedance/gopkg v0.1.2 // indirect github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect diff --git a/go.sum b/go.sum index 09cb196..6f4d285 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= -github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YYtrg1ixVSB/JvZM= -github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= -github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c= -github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po= +github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 h1:9CSf+V0ZQPl2ijC/g6v/ObemmhpKcikKVIodsaLExTA= +github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.5.0 h1:0yJA+dOgbnO3R/mAWPjlbUq5lIqaxRV38XfiX3jt6pg= github.com/WJQSERVER-STUDIO/httpc v0.5.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/logger v1.6.0 h1:xK2xV7hlkMXaWzvj4+cNoNWA+JfnJaHX6VU+RrPnr7Q= +github.com/WJQSERVER-STUDIO/logger v1.6.0/go.mod h1:TICMsR7geROHBg6rxwkqUNGydo34XVsX93yeoxyfuyY= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= diff --git a/main.go b/main.go index 1bfc76f..8d61589 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( "ghproxy/proxy" "ghproxy/rate" - "github.com/WJQSERVER-STUDIO/go-utils/logger" + "github.com/WJQSERVER-STUDIO/logger" "github.com/hertz-contrib/http2/factory" "github.com/cloudwego/hertz/pkg/app" diff --git a/middleware/loggin/loggin.go b/middleware/loggin/loggin.go index 15f17ee..62dccc0 100644 --- a/middleware/loggin/loggin.go +++ b/middleware/loggin/loggin.go @@ -4,7 +4,7 @@ import ( "context" "time" - "github.com/WJQSERVER-STUDIO/go-utils/logger" + "github.com/WJQSERVER-STUDIO/logger" "github.com/cloudwego/hertz/pkg/app" ) diff --git a/proxy/error.go b/proxy/error.go index b5ff39d..756c881 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -6,7 +6,7 @@ import ( "html/template" "io/fs" - "github.com/WJQSERVER-STUDIO/go-utils/logger" + "github.com/WJQSERVER-STUDIO/logger" "github.com/cloudwego/hertz/pkg/app" ) diff --git a/rate/rate.go b/rate/rate.go index 8305801..390b8da 100644 --- a/rate/rate.go +++ b/rate/rate.go @@ -4,7 +4,7 @@ import ( "sync" "time" - "github.com/WJQSERVER-STUDIO/go-utils/logger" + "github.com/WJQSERVER-STUDIO/logger" "golang.org/x/time/rate" ) From 1011a25d164934aa2b9a3273215e700544814786 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 5 May 2025 15:17:39 +0800 Subject: [PATCH 059/118] 25w34a --- CHANGELOG.md | 8 ++++++++ DEV-VERSION | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09baf1d..c995916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w34a - 2025-05-05 +--- +- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用; +- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录 +- CHANGE: 迁移httpc到新的仓库 + 3.2.2 - 2025-04-29 --- - ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制 @@ -8,10 +14,12 @@ 25w33b - 2025-04-29 --- +- PRE-RELEASE: 此版本是v3.2.2预发布版本,请勿在生产环境中使用; - REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改 25w33a - 2025-04-29 --- +- PRE-RELEASE: 此版本是v3.2.2预发布版本,请勿在生产环境中使用; - ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制 - FIX: 修正Header部分的一些处理问题 diff --git a/DEV-VERSION b/DEV-VERSION index 58d1f65..daad4c5 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w33b \ No newline at end of file +25w34a \ No newline at end of file From 90709539f4339226acf9759421352c6298675610 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 6 May 2025 14:23:19 +0800 Subject: [PATCH 060/118] update deps --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 45aad0b..5bdc528 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/WJQSERVER-STUDIO/logger v1.6.0 github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 - golang.org/x/net v0.39.0 + golang.org/x/net v0.40.0 golang.org/x/time v0.11.0 ) @@ -30,9 +30,9 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/arch v0.16.0 // indirect - golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/text v0.24.0 // indirect + golang.org/x/arch v0.17.0 // indirect + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 6f4d285..5b8252e 100644 --- a/go.sum +++ b/go.sum @@ -81,14 +81,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= -golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= +golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= -golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -98,8 +98,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -113,8 +113,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -127,8 +127,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From e3fd604945a3d902a7f6601c7d5f359d0c1aa958 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 7 May 2025 18:49:20 +0800 Subject: [PATCH 061/118] update Go to go1.24.3 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5bdc528..4f8bbbd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module ghproxy -go 1.24.2 +go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 From 866638ba8ef7e0ec660577433b396f77c35f883f Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 7 May 2025 18:50:58 +0800 Subject: [PATCH 062/118] 25w34b --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c995916..1adac57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w34b - 2025-05-07 +--- +- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用; +- CHANGE: 更新Go版本到go1.24.3 + 25w34a - 2025-05-05 --- - PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index daad4c5..4c76d8f 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w34a \ No newline at end of file +25w34b \ No newline at end of file From 545144c7b5f6c319229efd67b6f7f4c3e6e35e55 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 7 May 2025 19:08:12 +0800 Subject: [PATCH 063/118] 3.2.3 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1adac57..70daffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +3.2.3 - 2025-05-07 +--- +- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录 +- CHANGE: 更新Go版本到go1.24.3 +- CHANGE: 迁移httpc到新的仓库 + 25w34b - 2025-05-07 --- - PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index acf9bf0..06eda28 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.2 \ No newline at end of file +3.2.3 \ No newline at end of file From 791f668758f296542ce1dac829795becdaa62080 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 12 May 2025 17:06:06 +0800 Subject: [PATCH 064/118] remove unused matcher return(fix #101) --- proxy/match.go | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/proxy/match.go b/proxy/match.go index 9b55339..8c64623 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -104,42 +104,34 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro return "", "", "", NewErrorWithStatusLookup(404, errMsg) } -func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) { - var ( - matcher string - ) +func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) { // 匹配 "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 + return true, nil } // 匹配 "https://raw.githubusercontent.com"开头的链接 if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") { - return true, matcher, nil + return true, nil } // 匹配 "https://raw.github.com"开头的链接 if strings.HasPrefix(rawPath, "https://raw.github.com") { - return true, matcher, nil + return true, nil } // 匹配 "https://gist.githubusercontent.com"开头的链接 if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") { - return true, matcher, nil + return true, nil } // 匹配 "https://gist.github.com"开头的链接 if strings.HasPrefix(rawPath, "https://gist.github.com") { - return true, matcher, nil + return true, nil } if cfg.Shell.RewriteAPI { // 匹配 "https://api.github.com/"开头的链接 if strings.HasPrefix(rawPath, "https://api.github.com") { - matcher = "api" - return true, matcher, nil + return true, nil } } - return false, "", nil + return false, nil } // 匹配文件扩展名是sh的rawPath @@ -153,7 +145,7 @@ type LinkProcessor func(string) string // 自定义 URL 修改函数 func modifyURL(url string, host string, cfg *config.Config) string { // 去除url内的https://或http:// - matched, _, err := EditorMatcher(url, cfg) + matched, err := EditorMatcher(url, cfg) if err != nil { logDump("Invalid URL: %s", url) return url From c2e2b661a448b0d4a588b088f2c5e9db35401cf8 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 12 May 2025 17:12:58 +0800 Subject: [PATCH 065/118] 25w35a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70daffb..fe5f172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w35a - 2025-05-12 +--- +- PRE-RELEASE: 此版本是v3.2.4预发布版本,请勿在生产环境中使用; +- CHANGE: 移除未使用的变量与相关计算 + 3.2.3 - 2025-05-07 --- - CHANGE: 迁移logger库到新的仓库, 开启异步日志记录 diff --git a/DEV-VERSION b/DEV-VERSION index 4c76d8f..ef0053d 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w34b \ No newline at end of file +25w35a \ No newline at end of file From ef6e0a78cdc450edc7db883678ff0b06bd706392 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 13 May 2025 14:22:19 +0800 Subject: [PATCH 066/118] 3.2.4 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe5f172..2b9103c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.2.4 - 2025-05-13 +--- +- CHANGE: 移除未使用的变量与相关计算 + 25w35a - 2025-05-12 --- - PRE-RELEASE: 此版本是v3.2.4预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 06eda28..9b7a431 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.3 \ No newline at end of file +3.2.4 \ No newline at end of file From 43469532d4ea7fa308386ff29c0fa557cdc794a2 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 13 May 2025 14:51:34 +0800 Subject: [PATCH 067/118] 25w36a --- .gitignore | 3 ++- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- go.mod | 2 ++ proxy/chunkreq.go | 22 ++++++++++++---------- proxy/docker.go | 1 + proxy/gitreq.go | 2 ++ 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 02838fb..0ad54a9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ demo.toml *.bak list.json repos -pages \ No newline at end of file +pages +*_test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b9103c..ad477b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w36a - 2025-05-13 +--- +- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用; +- CHANGE: 为`httpc`加入`request builder`的`withcontext`选项 + 3.2.4 - 2025-05-13 --- - CHANGE: 移除未使用的变量与相关计算 diff --git a/DEV-VERSION b/DEV-VERSION index ef0053d..e5cf07e 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w35a \ No newline at end of file +25w36a \ No newline at end of file diff --git a/go.mod b/go.mod index 4f8bbbd..60eb1a8 100644 --- a/go.mod +++ b/go.mod @@ -36,3 +36,5 @@ require ( golang.org/x/text v0.25.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) + +//replace github.com/WJQSERVER-STUDIO/httpc v0.5.0 => /data/github/WJQSERVER-STUDIO/httpc diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 22c7b61..35b615e 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -14,15 +14,18 @@ import ( func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { var ( - method []byte - req *http.Request - resp *http.Response - err error + req *http.Request + resp *http.Response + err error ) - method = c.Request.Method() + rb := client.NewRequestBuilder(string(c.Request.Method()), u) + rb.NoDefaultHeaders() + rb.SetBody(c.Request.BodyStream()) + rb.WithContext(ctx) - req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) + req, err = rb.Build() + //req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return @@ -58,8 +61,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodySize = -1 } if err == nil && bodySize > sizelimit { - var finalURL string - finalURL = resp.Request.URL.String() + finalURL := resp.Request.URL.String() err = resp.Body.Close() if err != nil { logError("Failed to close response body: %v", err) @@ -99,7 +101,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c compress = "gzip" } - logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol()) + logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol()) c.Header("Content-Length", "") var reader io.Reader @@ -107,7 +109,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c reader, _, err = processLinks(resp.Body, compress, string(c.Request.Host()), cfg) c.SetBodyStream(reader, -1) 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.Header.GetProtocol(), err) + logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err) ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err))) return } diff --git a/proxy/docker.go b/proxy/docker.go index 6347f24..7fbb039 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -42,6 +42,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf rb := client.NewRequestBuilder(string(method), u) rb.NoDefaultHeaders() rb.SetBody(c.Request.BodyStream()) + rb.WithContext(ctx) //req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) req, err = rb.Build() diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 81293cd..4398bd7 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -36,6 +36,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co rb := gitclient.NewRequestBuilder(method, u) rb.NoDefaultHeaders() rb.SetBody(bodyReader) + rb.WithContext(ctx) req, err := rb.Build() if err != nil { @@ -55,6 +56,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb.NoDefaultHeaders() rb.SetBody(bodyReader) + rb.WithContext(ctx) req, err := rb.Build() if err != nil { From 3f8d16511ea3a652c09aecf04f5b455aa0ee377e Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 13 May 2025 19:04:21 +0800 Subject: [PATCH 068/118] 25w36b --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- go.mod | 3 ++- go.sum | 4 ++-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad477b3..2c0585c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w36b - 2025-05-13 +--- +- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用; +- CHANGE: `httpc`切换到`dev`, 测试在retry前检查ctx状态 + 25w36a - 2025-05-13 --- - PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index e5cf07e..657925f 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w36a \ No newline at end of file +25w36b \ No newline at end of file diff --git a/go.mod b/go.mod index 60eb1a8..24d73f2 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,8 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.5.0 + //github.com/WJQSERVER-STUDIO/httpc v0.5.0 + github.com/WJQSERVER-STUDIO/httpc v0.5.1-0.20250513102952-d961182b2489 github.com/WJQSERVER-STUDIO/logger v1.6.0 github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 diff --git a/go.sum b/go.sum index 5b8252e..8676974 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKU github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 h1:9CSf+V0ZQPl2ijC/g6v/ObemmhpKcikKVIodsaLExTA= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= -github.com/WJQSERVER-STUDIO/httpc v0.5.0 h1:0yJA+dOgbnO3R/mAWPjlbUq5lIqaxRV38XfiX3jt6pg= -github.com/WJQSERVER-STUDIO/httpc v0.5.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.5.1-0.20250513102952-d961182b2489 h1:BScWEkOFYMDaSB4SNhBa6XeBoBjg1IHxmGE3NSNW6zw= +github.com/WJQSERVER-STUDIO/httpc v0.5.1-0.20250513102952-d961182b2489/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/WJQSERVER-STUDIO/logger v1.6.0 h1:xK2xV7hlkMXaWzvj4+cNoNWA+JfnJaHX6VU+RrPnr7Q= github.com/WJQSERVER-STUDIO/logger v1.6.0/go.mod h1:TICMsR7geROHBg6rxwkqUNGydo34XVsX93yeoxyfuyY= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= From 71bc2aaed7d67f84a876313702d1779cbe6154d1 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 14 May 2025 01:33:54 +0800 Subject: [PATCH 069/118] add bandwidth limiter --- config/config.go | 39 +++++++++++++++++++++++++--- config/config.toml | 7 +++++ go.mod | 7 ++--- go.sum | 6 +++-- main.go | 6 ++++- proxy/bandwidth.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++ proxy/chunkreq.go | 14 +++++++--- proxy/docker.go | 11 ++++++-- proxy/gitreq.go | 18 ++++++++----- proxy/httpc.go | 7 ++++- 10 files changed, 157 insertions(+), 22 deletions(-) create mode 100644 proxy/bandwidth.go diff --git a/config/config.go b/config/config.go index 14c9301..c5b8aa0 100644 --- a/config/config.go +++ b/config/config.go @@ -129,11 +129,35 @@ type WhitelistConfig struct { WhitelistFile string `toml:"whitelistFile"` } +/* +[rateLimit] +enabled = false +rateMethod = "total" # "total" or "ip" +ratePerMinute = 100 +burst = 10 + + [rateLimit.bandwidthLimit] + enabled = false + totalLimit = "100mbps" + totalBurst = "100mbps" + singleLimit = "10mbps" + singleBurst = "10mbps" +*/ + type RateLimitConfig struct { - Enabled bool `toml:"enabled"` - RateMethod string `toml:"rateMethod"` - RatePerMinute int `toml:"ratePerMinute"` - Burst int `toml:"burst"` + Enabled bool `toml:"enabled"` + RateMethod string `toml:"rateMethod"` + RatePerMinute int `toml:"ratePerMinute"` + Burst int `toml:"burst"` + BandwidthLimit BandwidthLimitConfig +} + +type BandwidthLimitConfig struct { + Enabled bool `toml:"enabled"` + TotalLimit string `toml:"totalLimit"` + TotalBurst string `toml:"totalBurst"` + SingleLimit string `toml:"singleLimit"` + SingleBurst string `toml:"singleBurst"` } /* @@ -252,6 +276,13 @@ func DefaultConfig() *Config { RateMethod: "total", RatePerMinute: 100, Burst: 10, + BandwidthLimit: BandwidthLimitConfig{ + Enabled: false, + TotalLimit: "100mbps", + TotalBurst: "100mbps", + SingleLimit: "10mbps", + SingleBurst: "10mbps", + }, }, Outbound: OutboundConfig{ Enabled: false, diff --git a/config/config.toml b/config/config.toml index b43ebf3..ca7b80a 100644 --- a/config/config.toml +++ b/config/config.toml @@ -57,6 +57,13 @@ rateMethod = "total" # "ip" or "total" ratePerMinute = 180 burst = 5 +[rateLimit.bandwidthLimit] + enabled = false + totalLimit = "100mbps" + totalBurst = "100mbps" + singleLimit = "10mbps" + singleBurst = "10mbps" + [outbound] enabled = false url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" diff --git a/go.mod b/go.mod index 24d73f2..d20e90a 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,7 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 - //github.com/WJQSERVER-STUDIO/httpc v0.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.5.1-0.20250513102952-d961182b2489 + github.com/WJQSERVER-STUDIO/httpc v0.5.1 github.com/WJQSERVER-STUDIO/logger v1.6.0 github.com/cloudwego/hertz v0.9.7 github.com/hertz-contrib/http2 v0.1.8 @@ -13,6 +12,8 @@ require ( golang.org/x/time v0.11.0 ) +require github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 + require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 // indirect @@ -38,4 +39,4 @@ require ( google.golang.org/protobuf v1.36.6 // indirect ) -//replace github.com/WJQSERVER-STUDIO/httpc v0.5.0 => /data/github/WJQSERVER-STUDIO/httpc +//replace github.com/WJQSERVER-STUDIO/httpc v0.5.1 => /data/github/WJQSERVER-STUDIO/httpc diff --git a/go.sum b/go.sum index 8676974..d906a16 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,12 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= +github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc5f0DK8N9agIJCpM1Cqw2NE= +github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 h1:9CSf+V0ZQPl2ijC/g6v/ObemmhpKcikKVIodsaLExTA= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= -github.com/WJQSERVER-STUDIO/httpc v0.5.1-0.20250513102952-d961182b2489 h1:BScWEkOFYMDaSB4SNhBa6XeBoBjg1IHxmGE3NSNW6zw= -github.com/WJQSERVER-STUDIO/httpc v0.5.1-0.20250513102952-d961182b2489/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= +github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/WJQSERVER-STUDIO/logger v1.6.0 h1:xK2xV7hlkMXaWzvj4+cNoNWA+JfnJaHX6VU+RrPnr7Q= github.com/WJQSERVER-STUDIO/logger v1.6.0/go.mod h1:TICMsR7geROHBg6rxwkqUNGydo34XVsX93yeoxyfuyY= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= diff --git a/main.go b/main.go index 8d61589..d0237d8 100644 --- a/main.go +++ b/main.go @@ -181,7 +181,11 @@ func setupRateLimit(cfg *config.Config) { } func InitReq(cfg *config.Config) { - proxy.InitReq(cfg) + err := proxy.InitReq(cfg) + if err != nil { + fmt.Printf("Failed to initialize request: %v\n", err) + os.Exit(1) + } } // loadEmbeddedPages 加载嵌入式页面资源 diff --git a/proxy/bandwidth.go b/proxy/bandwidth.go new file mode 100644 index 0000000..a7591c2 --- /dev/null +++ b/proxy/bandwidth.go @@ -0,0 +1,64 @@ +package proxy + +import ( + "errors" + "ghproxy/config" + + "github.com/WJQSERVER-STUDIO/go-utils/limitreader" + "golang.org/x/time/rate" +) + +var ( + bandwidthLimit rate.Limit + bandwidthBurst rate.Limit +) + +func UnDefiendRateStringErrHandle(err error) error { + if errors.Is(err, &limitreader.UnDefiendRateStringErr{}) { + logWarning("UnDefiendRateStringErr: %s", err) + return nil + } + return err +} + +func SetGlobalRateLimit(cfg *config.Config) error { + if cfg.RateLimit.BandwidthLimit.Enabled { + var err error + var totalLimit rate.Limit + var totalBurst rate.Limit + totalLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalLimit) + if UnDefiendRateStringErrHandle(err) != nil { + logError("Failed to parse total bandwidth limit: %v", err) + return err + } + totalBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalBurst) + if UnDefiendRateStringErrHandle(err) != nil { + logError("Failed to parse total bandwidth burst: %v", err) + return err + } + limitreader.SetGlobalRateLimit(totalLimit, int(totalBurst)) + err = SetBandwidthLimit(cfg) + if UnDefiendRateStringErrHandle(err) != nil { + logError("Failed to set bandwidth limit: %v", err) + return err + } + } else { + limitreader.SetGlobalRateLimit(rate.Inf, 0) + } + return nil +} + +func SetBandwidthLimit(cfg *config.Config) error { + var err error + bandwidthLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleLimit) + if UnDefiendRateStringErrHandle(err) != nil { + logError("Failed to parse bandwidth limit: %v", err) + return err + } + bandwidthBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleBurst) + if UnDefiendRateStringErrHandle(err) != nil { + logError("Failed to parse bandwidth burst: %v", err) + return err + } + return nil +} diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 35b615e..56e55a3 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -8,6 +8,7 @@ import ( "net/http" "strconv" + "github.com/WJQSERVER-STUDIO/go-utils/limitreader" "github.com/cloudwego/hertz/pkg/app" ) @@ -94,6 +95,12 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c c.Status(resp.StatusCode) + bodyReader := resp.Body + + if cfg.RateLimit.BandwidthLimit.Enabled { + bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) + } + if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor { // 判断body是不是gzip var compress string @@ -106,7 +113,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c var reader io.Reader - reader, _, err = processLinks(resp.Body, compress, string(c.Request.Host()), cfg) + reader, _, err = processLinks(bodyReader, compress, string(c.Request.Host()), cfg) c.SetBodyStream(reader, -1) if err != nil { logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err) @@ -114,11 +121,12 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } } else { + if contentLength != "" { - c.SetBodyStream(resp.Body, bodySize) + c.SetBodyStream(bodyReader, bodySize) return } - c.SetBodyStream(resp.Body, -1) + c.SetBodyStream(bodyReader, -1) } } diff --git a/proxy/docker.go b/proxy/docker.go index 7fbb039..f12a74e 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -7,6 +7,7 @@ import ( "net/http" "strconv" + "github.com/WJQSERVER-STUDIO/go-utils/limitreader" "github.com/cloudwego/hertz/pkg/app" ) @@ -107,10 +108,16 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf c.Status(resp.StatusCode) + bodyReader := resp.Body + + if cfg.RateLimit.BandwidthLimit.Enabled { + bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) + } + if contentLength != "" { - c.SetBodyStream(resp.Body, bodySize) + c.SetBodyStream(bodyReader, bodySize) return } - c.SetBodyStream(resp.Body, -1) + c.SetBodyStream(bodyReader, -1) } diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 4398bd7..1afd856 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -8,15 +8,16 @@ import ( "net/http" "strconv" + "github.com/WJQSERVER-STUDIO/go-utils/limitreader" "github.com/cloudwego/hertz/pkg/app" ) func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) { method := string(c.Request.Method()) - bodyReader := bytes.NewBuffer(c.Request.Body()) + reqBodyReader := bytes.NewBuffer(c.Request.Body()) - //bodyReader := c.Request.BodyStream() + //bodyReader := c.Request.BodyStream() // 不可替换为此实现 if cfg.GitClone.Mode == "cache" { userPath, repoPath, remainingPath, queryParams, err := extractParts(u) @@ -35,7 +36,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co if cfg.GitClone.Mode == "cache" { rb := gitclient.NewRequestBuilder(method, u) rb.NoDefaultHeaders() - rb.SetBody(bodyReader) + rb.SetBody(reqBodyReader) rb.WithContext(ctx) req, err := rb.Build() @@ -55,7 +56,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co } else { rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb.NoDefaultHeaders() - rb.SetBody(bodyReader) + rb.SetBody(reqBodyReader) rb.WithContext(ctx) req, err := rb.Build() @@ -91,7 +92,6 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co for key, values := range resp.Header { for _, value := range values { - //c.Header(key, value) c.Response.Header.Add(key, value) } } @@ -124,5 +124,11 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co c.Response.Header.Set("Expires", "0") } - c.SetBodyStream(resp.Body, -1) + bodyReader := resp.Body + + if cfg.RateLimit.BandwidthLimit.Enabled { + bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) + } + + c.SetBodyStream(bodyReader, -1) } diff --git a/proxy/httpc.go b/proxy/httpc.go index 120d8a7..83de29b 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -18,11 +18,16 @@ var ( gitclient *httpc.Client ) -func InitReq(cfg *config.Config) { +func InitReq(cfg *config.Config) error { initHTTPClient(cfg) if cfg.GitClone.Mode == "cache" { initGitHTTPClient(cfg) } + err := SetGlobalRateLimit(cfg) + if err != nil { + return err + } + return nil } func initHTTPClient(cfg *config.Config) { From 47c03763a7f742cb2e7b57f785c65873259731a3 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 14 May 2025 01:34:05 +0800 Subject: [PATCH 070/118] 25w36c --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0585c..7af6e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w36c - 2025-05-14 +--- +- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用; +- ADD: 加入带宽限制功能 + 25w36b - 2025-05-13 --- - PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 657925f..1420bf3 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w36b \ No newline at end of file +25w36c \ No newline at end of file From ce814875e142621ef5341fd1a317a04c98324dfb Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 14 May 2025 17:55:37 +0800 Subject: [PATCH 071/118] 25w36d --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- main.go | 5 +++-- proxy/chunkreq.go | 11 ++++++++++- proxy/docker.go | 10 ++++++++++ proxy/gitreq.go | 20 ++++++++++++++++---- 6 files changed, 46 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7af6e66..d64fb70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # 更新日志 +25w36d - 2025-05-14 +--- +- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用; +- ADD: 为`netpoll`模式开启探测客户端是否断开功能 + 25w36c - 2025-05-14 --- - PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用; - ADD: 加入带宽限制功能 +- CHANGE: 将`httpc`切换回主分支, `25w36b`测试的部分已被合入`httpc`主线 25w36b - 2025-05-13 --- diff --git a/DEV-VERSION b/DEV-VERSION index 1420bf3..c9f9483 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w36c \ No newline at end of file +25w36d \ No newline at end of file diff --git a/main.go b/main.go index d0237d8..2cb9ac4 100644 --- a/main.go +++ b/main.go @@ -401,11 +401,13 @@ func main() { r = server.New( server.WithH2C(true), server.WithHostPorts(addr), + server.WithSenseClientDisconnection(true), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { r = server.New( server.WithHostPorts(addr), + server.WithSenseClientDisconnection(true), ) } } else { @@ -485,8 +487,7 @@ func main() { defer logger.Close() defer func() { if hertZfile != nil { - var err error - err = hertZfile.Close() + err := hertZfile.Close() if err != nil { logError("Failed to close hertz log file: %v", err) } diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 56e55a3..c760b9a 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -20,13 +20,22 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c err error ) + go func() { + <-ctx.Done() + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + if req != nil { + req.Body.Close() + } + }() + rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb.NoDefaultHeaders() rb.SetBody(c.Request.BodyStream()) rb.WithContext(ctx) req, err = rb.Build() - //req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return diff --git a/proxy/docker.go b/proxy/docker.go index f12a74e..696de84 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -38,6 +38,16 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf err error ) + go func() { + <-ctx.Done() + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + if req != nil { + req.Body.Close() + } + }() + method = c.Request.Method() rb := client.NewRequestBuilder(string(method), u) diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 1afd856..5667d21 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -13,6 +13,22 @@ import ( ) func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) { + + var ( + req *http.Request + resp *http.Response + ) + + go func() { + <-ctx.Done() + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + if req != nil { + req.Body.Close() + } + }() + method := string(c.Request.Method()) reqBodyReader := bytes.NewBuffer(c.Request.Body()) @@ -29,10 +45,6 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode() } - var ( - resp *http.Response - ) - if cfg.GitClone.Mode == "cache" { rb := gitclient.NewRequestBuilder(method, u) rb.NoDefaultHeaders() From 89b850c1ec08f8297a8bc9cbf2eb970829774408 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 15 May 2025 18:45:52 +0800 Subject: [PATCH 072/118] 3.3.0 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d64fb70..5d07c91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +3.3.0 - 2025-05-15 +--- +- CHANGE: 为`httpc`加入`request builder`的`withcontext`选项 +- ADD: 加入带宽限制功能 +- ADD: 为`netpoll`模式开启探测客户端是否断开功能 + 25w36d - 2025-05-14 --- - PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 9b7a431..0fa4ae4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.4 \ No newline at end of file +3.3.0 \ No newline at end of file From 4ded2186d887269f412c80266ee3159393e5b6d5 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 15 May 2025 18:50:36 +0800 Subject: [PATCH 073/118] update deps --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d20e90a..e798fb1 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.5.1 github.com/WJQSERVER-STUDIO/logger v1.6.0 - github.com/cloudwego/hertz v0.9.7 + github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.40.0 golang.org/x/time v0.11.0 diff --git a/go.sum b/go.sum index d906a16..68f4222 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50= github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI= -github.com/cloudwego/hertz v0.9.7 h1:tAVaiO+vTf+ZkQhvNhKbDJ0hmC4oJ7bzwDi1KhvhHy4= -github.com/cloudwego/hertz v0.9.7/go.mod h1:t6d7NcoQxPmETvzPMMIVPHMn5C5QzpqIiFsaavoLJYQ= +github.com/cloudwego/hertz v0.10.0 h1:V0vmBaLdQPlgL6w2TA6PZL1g6SGgQznFx6vqxWdCcKw= +github.com/cloudwego/hertz v0.10.0/go.mod h1:lRBohmcDkGx5TLK6QKFGdzJ6n3IXqGueHsOiXcYgXA4= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4= github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU= From 052243b09507d039a4b84be76699362b4030800f Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 16 May 2025 00:15:04 +0800 Subject: [PATCH 074/118] add customTarget --- config/config.go | 6 ++++-- config/config.toml | 3 ++- proxy/docker.go | 17 +++++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/config/config.go b/config/config.go index c5b8aa0..008a5ef 100644 --- a/config/config.go +++ b/config/config.go @@ -174,10 +174,12 @@ type OutboundConfig struct { [docker] enabled = false target = "ghcr" # ghcr/dockerhub +customTarget = "" # 自定义host(不带协议头) */ type DockerConfig struct { - Enabled bool `toml:"enabled"` - Target string `toml:"target"` + Enabled bool `toml:"enabled"` + Target string `toml:"target"` + CustomTarget string `toml:"customTarget"` } // LoadConfig 从 TOML 配置文件加载配置 diff --git a/config/config.toml b/config/config.toml index ca7b80a..b72c538 100644 --- a/config/config.toml +++ b/config/config.toml @@ -70,4 +70,5 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" [docker] enabled = false -target = "ghcr" # ghcr/dockerhub \ No newline at end of file +target = "ghcr" # ghcr/dockerhub +customTarget = "" # 自定义host(不带协议头) \ No newline at end of file diff --git a/proxy/docker.go b/proxy/docker.go index 696de84..0c805f5 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -14,14 +14,19 @@ import ( func GhcrRouting(cfg *config.Config) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { if cfg.Docker.Enabled { - if cfg.Docker.Target == "ghcr" { - GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") - } else if cfg.Docker.Target == "dockerhub" { - GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub") + if cfg.Docker.CustomTarget == "" { + if cfg.Docker.Target == "ghcr" { + GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + } else if cfg.Docker.Target == "dockerhub" { + GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub") + } else { + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not Allowed")) + return + } } else { - ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not Allowed")) - return + GhcrRequest(ctx, c, "https://"+cfg.Docker.CustomTarget+string(c.Request.RequestURI()), cfg, "custom") } + } else { ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed")) return From 7d4aae166857555069e05d30e6655fbc12b7f61c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 16 May 2025 00:24:57 +0800 Subject: [PATCH 075/118] merge customTarget into target --- config/config.go | 6 ++---- config/config.toml | 3 +-- proxy/docker.go | 20 ++++++++++---------- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/config/config.go b/config/config.go index 008a5ef..c5b8aa0 100644 --- a/config/config.go +++ b/config/config.go @@ -174,12 +174,10 @@ type OutboundConfig struct { [docker] enabled = false target = "ghcr" # ghcr/dockerhub -customTarget = "" # 自定义host(不带协议头) */ type DockerConfig struct { - Enabled bool `toml:"enabled"` - Target string `toml:"target"` - CustomTarget string `toml:"customTarget"` + Enabled bool `toml:"enabled"` + Target string `toml:"target"` } // LoadConfig 从 TOML 配置文件加载配置 diff --git a/config/config.toml b/config/config.toml index b72c538..ca7b80a 100644 --- a/config/config.toml +++ b/config/config.toml @@ -70,5 +70,4 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" [docker] enabled = false -target = "ghcr" # ghcr/dockerhub -customTarget = "" # 自定义host(不带协议头) \ No newline at end of file +target = "ghcr" # ghcr/dockerhub \ No newline at end of file diff --git a/proxy/docker.go b/proxy/docker.go index 0c805f5..cdfc743 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -14,17 +14,17 @@ import ( func GhcrRouting(cfg *config.Config) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { if cfg.Docker.Enabled { - if cfg.Docker.CustomTarget == "" { - if cfg.Docker.Target == "ghcr" { - GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") - } else if cfg.Docker.Target == "dockerhub" { - GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub") - } else { - ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not Allowed")) - return - } + if cfg.Docker.Target == "ghcr" { + GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + } else if cfg.Docker.Target == "dockerhub" { + GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub") + } else if cfg.Docker.Target != "" { + // 自定义taget + GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), cfg, "custom") } else { - GhcrRequest(ctx, c, "https://"+cfg.Docker.CustomTarget+string(c.Request.RequestURI()), cfg, "custom") + // 配置为空 + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set")) + return } } else { From 566a0ea26ae2a871069db3b69052861af70ba977 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 16 May 2025 19:28:08 +0800 Subject: [PATCH 076/118] 25w37a --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d07c91..dbe9dc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w37a - 2025-05-16 +--- +- PRE-RELEASE: 此版本是v3.3.1预发布版本,请勿在生产环境中使用; +- CHANGE: 为`target`放宽限制, 支持自定义 +- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0` + 3.3.0 - 2025-05-15 --- - CHANGE: 为`httpc`加入`request builder`的`withcontext`选项 diff --git a/DEV-VERSION b/DEV-VERSION index c9f9483..cd31a95 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w36d \ No newline at end of file +25w37a \ No newline at end of file From b2712f8184e990c5a2f03db8d5228c8790e4cd9b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 16 May 2025 19:53:48 +0800 Subject: [PATCH 077/118] 3.3.1 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe9dc4..cb0a95f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.3.1 - 2025-05-16 +- CHANGE: 为`target`放宽限制, 支持自定义 +- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0` + 25w37a - 2025-05-16 --- - PRE-RELEASE: 此版本是v3.3.1预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 0fa4ae4..712bd5a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.0 \ No newline at end of file +3.3.1 \ No newline at end of file From 5facc3694720ca78b91e021daf07643e5ce30547 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 18 May 2025 06:09:04 +0800 Subject: [PATCH 078/118] update docs --- docs/config.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index c146ca4..e5de161 100644 --- a/docs/config.md +++ b/docs/config.md @@ -68,13 +68,20 @@ rateMethod = "total" # "ip" or "total" ratePerMinute = 180 burst = 5 +[rateLimit.bandwidthLimit] + enabled = false + totalLimit = "100mbps" + totalBurst = "100mbps" + singleLimit = "10mbps" + singleBurst = "10mbps" + [outbound] enabled = false url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" [docker] enabled = false -target = "ghcr" # ghcr/dockerhub +target = "ghcr" # ghcr/dockerhub or "xx.example.com" ``` ### 配置项详细说明 @@ -291,6 +298,27 @@ target = "ghcr" # ghcr/dockerhub * 类型: 整数 (`int`) * 默认值: `5` * 说明: 允许在短时间内超过 `ratePerMinute` 的突发请求数。 + * **`[rateLimit.bandwidthLimit]` 带宽速率限制** + * `enabled`: 是否启用带宽速率限制。 + * 类型: 布尔值 (`bool`) + * 默认值: `false` (禁用) + * 说明: 启用后,`ghproxy` 将根据配置的策略限制带宽使用,防止服务被滥用。 + * `totalLimit`: 全局带宽限制。 + * 类型: 字符串 (`string`) + * 默认值: `"100mbps"` + * 说明: 设置全局最大带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。 + * `totalBurst`: 全局突发带宽。 + * 类型: 字符串 (`string`) + * 默认值: `"100mbps"` + * 说明: 设置全局突发带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。 + * `singleLimit`: 单个连接带宽限制。 + * 类型: 字符串 (`string`) + * 默认值: `"10mbps"` + * 说明: 设置单个连接的最大带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。 + * `singleBurst`: 单个连接突发带宽。 + * 类型: 字符串 (`string`) + * 默认值: `"10mbps"` + * 说明: 设置单个连接的突发带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。 * **`[outbound]` - 出站代理配置** @@ -318,6 +346,7 @@ target = "ghcr" # ghcr/dockerhub * 说明: 指定要代理的 Docker 注册表。 * `"ghcr"`: 代理 GitHub Container Registry (ghcr.io)。 * `"dockerhub"`: 代理 Docker Hub (docker.io)。 + * 自定义, 支持传入自定义target, 例如`"docker.example.com"` ## `blacklist.json` - 黑名单配置 From ea0e4e980162ec1e72fdda5ddfaba578b44ec90d Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 18 May 2025 06:11:44 +0800 Subject: [PATCH 079/118] change the default theme to design --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 2cb9ac4..a9c37ae 100644 --- a/main.go +++ b/main.go @@ -206,8 +206,8 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { case "mino": pages, err = fs.Sub(pagesFS, "pages/mino") default: - pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题 - logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme) + pages, err = fs.Sub(pagesFS, "pages/design") // 默认主题 + logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme) } if err != nil { From a4fae955263f2665ca973f3a8a1f6808224b21d1 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 18 May 2025 06:13:00 +0800 Subject: [PATCH 080/118] 3.3.2 --- CHANGELOG.md | 9 +++++++++ DEV-VERSION | 2 +- VERSION | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb0a95f..a9a1114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 更新日志 +3.3.2 - 2025-05-18 +--- +- CHANGE: 默认主题改为`design` + +25w38a - 2025-05-18 +--- +- PRE-RELEASE: 此版本是v3.3.2预发布版本,请勿在生产环境中使用; +- CHANGE: 默认主题改为`design` + 3.3.1 - 2025-05-16 - CHANGE: 为`target`放宽限制, 支持自定义 - CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0` diff --git a/DEV-VERSION b/DEV-VERSION index cd31a95..5c77f30 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w37a \ No newline at end of file +25w38a \ No newline at end of file diff --git a/VERSION b/VERSION index 712bd5a..5436ea0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.1 \ No newline at end of file +3.3.2 \ No newline at end of file From 816b35654a9d7d5e4a485d025becca64ac903dc3 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sun, 18 May 2025 06:20:55 +0800 Subject: [PATCH 081/118] update readme.md --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 871c644..9df6f2b 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,13 @@ - 🌐 **使用字节旗下的 [HertZ](https://github.com/cloudwego/hertz) 作为 Web 框架** - 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端** - 📥 **支持 Git clone、raw、releases 等文件拉取** +- 🐳 **支持反代Docker, GHCR等镜像仓库** - 🎨 **支持多个前端主题** - 🚫 **支持自定义黑名单/白名单** - 🗄️ **支持 Git Clone 缓存(配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git))** -- 🐳 **支持 Docker 部署** -- 🐳 **支持自托管** +- 🐳 **支持自托管与Docker容器化部署** - ⚡ **支持速率限制** +- ⚡ **支持带宽速率限制** - 🔒 **支持用户鉴权** - 🐚 **支持 shell 脚本多层嵌套加速** @@ -34,7 +35,9 @@ [相关文章](https://blog.wjqserver.com/categories/my-program/) -[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) +[GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) + +[GHProxy项目文档Next(仍在建设中)](https://ghproxy-docs.pages.dev/) ### 使用示例 From d92424cb94bcdfd64dfe281e9fcb9aa882d9a99e Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 19 May 2025 12:00:36 +0800 Subject: [PATCH 082/118] 25w39a --- .gitignore | 3 ++- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- config/config.go | 18 ++++++++++-------- config/config.toml | 2 ++ go.mod | 5 +++-- go.sum | 8 ++++---- main.go | 46 +++++++++++++++++++++++++++++++++++++++++++--- 8 files changed, 70 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 0ad54a9..1834d15 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ demo.toml list.json repos pages -*_test \ No newline at end of file +*_test +.* \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a9a1114..80d54b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w39a - 2025-05-19 +--- +- PRE-RELEASE: 此版本是v3.3.3预发布版本,请勿在生产环境中使用; +- CHANGE: 加入`senseClientDisconnection`与`async`配置项 + 3.3.2 - 2025-05-18 --- - CHANGE: 默认主题改为`design` diff --git a/DEV-VERSION b/DEV-VERSION index 5c77f30..fdef6bc 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w38a \ No newline at end of file +25w39a \ No newline at end of file diff --git a/config/config.go b/config/config.go index c5b8aa0..c1300dc 100644 --- a/config/config.go +++ b/config/config.go @@ -34,14 +34,15 @@ debug = false */ type ServerConfig struct { - Port int `toml:"port"` - Host string `toml:"host"` - NetLib string `toml:"netlib"` - SizeLimit int `toml:"sizeLimit"` - MemLimit int64 `toml:"memLimit"` - H2C bool `toml:"H2C"` - Cors string `toml:"cors"` - Debug bool `toml:"debug"` + Port int `toml:"port"` + Host string `toml:"host"` + NetLib string `toml:"netlib"` + SenseClientDisconnection bool `toml:"senseClientDisconnection"` + SizeLimit int `toml:"sizeLimit"` + MemLimit int64 `toml:"memLimit"` + H2C bool `toml:"H2C"` + Cors string `toml:"cors"` + Debug bool `toml:"debug"` } /* @@ -98,6 +99,7 @@ type LogConfig struct { LogFilePath string `toml:"logFilePath"` MaxLogSize int `toml:"maxLogSize"` Level string `toml:"level"` + Async bool `toml:"async"` HertZLogPath string `toml:"hertzLogPath"` } diff --git a/config/config.toml b/config/config.toml index ca7b80a..c8ea1f5 100644 --- a/config/config.toml +++ b/config/config.toml @@ -2,6 +2,7 @@ host = "0.0.0.0" port = 8080 netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net" +senseClientDisconnection = false sizeLimit = 125 # MB memLimit = 0 # MB H2C = true @@ -33,6 +34,7 @@ staticDir = "/data/www" logFilePath = "/data/ghproxy/log/ghproxy.log" maxLogSize = 5 # MB level = "info" # dump, debug, info, warn, error, none +async = false hertzLogPath = "/data/ghproxy/log/hertz.log" [auth] diff --git a/go.mod b/go.mod index e798fb1..d4a930a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.5.1 - github.com/WJQSERVER-STUDIO/logger v1.6.0 + github.com/WJQSERVER-STUDIO/logger v1.7.1 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.40.0 @@ -16,7 +16,7 @@ require github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect - github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 // indirect + github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 // indirect github.com/bytedance/gopkg v0.1.2 // indirect github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect @@ -40,3 +40,4 @@ require ( ) //replace github.com/WJQSERVER-STUDIO/httpc v0.5.1 => /data/github/WJQSERVER-STUDIO/httpc +//replace github.com/WJQSERVER-STUDIO/logger v1.6.0 => /data/github/WJQSERVER-STUDIO/logger diff --git a/go.sum b/go.sum index 68f4222..a614fc3 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,12 @@ github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKU github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc5f0DK8N9agIJCpM1Cqw2NE= github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg= -github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 h1:9CSf+V0ZQPl2ijC/g6v/ObemmhpKcikKVIodsaLExTA= -github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= +github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSjSpQtVKqEDpiZ5Q= +github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/logger v1.6.0 h1:xK2xV7hlkMXaWzvj4+cNoNWA+JfnJaHX6VU+RrPnr7Q= -github.com/WJQSERVER-STUDIO/logger v1.6.0/go.mod h1:TICMsR7geROHBg6rxwkqUNGydo34XVsX93yeoxyfuyY= +github.com/WJQSERVER-STUDIO/logger v1.7.1 h1:sAFsF3umimY0Vmue5WnGf1Qxvm/vlhK2srZakWVtlFU= +github.com/WJQSERVER-STUDIO/logger v1.7.1/go.mod h1:cvP0XdFIMLtDWOZeKhklshzipkVU1zufsU4rKNfoM24= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= diff --git a/main.go b/main.go index a9c37ae..041acab 100644 --- a/main.go +++ b/main.go @@ -121,6 +121,7 @@ func loadConfig() { func setupLogger(cfg *config.Config) { var err error + err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize) if err != nil { fmt.Printf("Failed to initialize logger: %v\n", err) @@ -131,6 +132,8 @@ func setupLogger(cfg *config.Config) { fmt.Printf("Logger Level Error: %v\n", err) os.Exit(1) } + logger.SetAsync(cfg.Log.Async) + fmt.Printf("Log Level: %s\n", cfg.Log.Level) logDebug("Config File Path: ", cfgfile) logDebug("Loaded config: %v\n", cfg) @@ -401,13 +404,13 @@ func main() { r = server.New( server.WithH2C(true), server.WithHostPorts(addr), - server.WithSenseClientDisconnection(true), + server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { r = server.New( server.WithHostPorts(addr), - server.WithSenseClientDisconnection(true), + server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), ) } } else { @@ -465,7 +468,44 @@ func main() { proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { + proxy.GhcrRouting(cfg)(ctx, c) + + /* + //proxy.GhcrRouting(cfg)(ctx, c) + // 返回200与空json + //c.JSON(200, map[string]interface{}{}) + emptyJSON := "{}" + //emptyJSON := `{"name":"disable-list-tags","tags":[]}` + c.Header("Content-Type", "application/json") + c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) + c.String(200, emptyJSON) + */ + /* + emptyJSON := "{}" + c.Header("Content-Type", "application/json") + c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) + + c.Header("Docker-Distribution-API-Version", "registry/2.0") + + c.Status(200) + c.Write([]byte(emptyJSON)) + */ + + /* + w := adaptor.GetCompatResponseWriter(&c.Response) + + const emptyJSON = "{}" + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) + w.Header().Del("Server") + + fmt.Fprint(w, emptyJSON) + */ + + }) + + r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) { proxy.GhcrRouting(cfg)(ctx, c) }) From 146b0d77482e052f5aa8860d217a8d5d1ea91184 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 19 May 2025 12:16:20 +0800 Subject: [PATCH 083/118] update nest --- proxy/match.go | 174 ---------------------------------------------- proxy/nest.go | 185 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 174 deletions(-) create mode 100644 proxy/nest.go diff --git a/proxy/match.go b/proxy/match.go index 8c64623..9db0c23 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -1,11 +1,8 @@ package proxy import ( - "bufio" - "compress/gzip" "fmt" "ghproxy/config" - "io" "net/url" "regexp" "strings" @@ -104,62 +101,6 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro return "", "", "", NewErrorWithStatusLookup(404, errMsg) } -func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) { - // 匹配 "https://github.com"开头的链接 - if strings.HasPrefix(rawPath, "https://github.com") { - return true, nil - } - // 匹配 "https://raw.githubusercontent.com"开头的链接 - if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") { - return true, nil - } - // 匹配 "https://raw.github.com"开头的链接 - if strings.HasPrefix(rawPath, "https://raw.github.com") { - return true, nil - } - // 匹配 "https://gist.githubusercontent.com"开头的链接 - if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") { - return true, nil - } - // 匹配 "https://gist.github.com"开头的链接 - if strings.HasPrefix(rawPath, "https://gist.github.com") { - return true, nil - } - if cfg.Shell.RewriteAPI { - // 匹配 "https://api.github.com/"开头的链接 - if strings.HasPrefix(rawPath, "https://api.github.com") { - return true, nil - } - } - return false, nil -} - -// 匹配文件扩展名是sh的rawPath -func MatcherShell(rawPath string) bool { - return strings.HasSuffix(rawPath, ".sh") -} - -// 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 { - var u = url - u = strings.TrimPrefix(u, "https://") - u = strings.TrimPrefix(u, "http://") - logDump("Modified URL: %s", "https://"+host+"/"+u) - return "https://" + host + "/" + u - } - return url -} - var ( matchedMatchers = []string{ "blob", @@ -211,118 +152,3 @@ func extractParts(rawURL string) (string, string, string, url.Values, error) { } var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`) - -// processLinks 处理链接,返回包含处理后数据的 io.Reader -func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) { - pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe - readerOut = pipeReader - - go func() { // 在 Goroutine 中执行写入操作 - defer func() { - if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误 - if err != nil { - if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader - logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err) - } - } else { - if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭 - logError("pipeWriter close failed: %v", closeErr) - if err == nil { // 如果之前没有错误,记录关闭错误 - err = closeErr - } - } - } - } - }() - - defer func() { - if err := input.Close(); err != nil { - logError("input close failed: %v", err) - } - - }() - - var bufReader *bufio.Reader - - if compress == "gzip" { - // 解压gzip - gzipReader, gzipErr := gzip.NewReader(input) - if gzipErr != nil { - err = fmt.Errorf("gzip解压错误: %v", gzipErr) - return // Goroutine 中使用 return 返回错误 - } - defer gzipReader.Close() - bufReader = bufio.NewReader(gzipReader) - } else { - bufReader = bufio.NewReader(input) - } - - var bufWriter *bufio.Writer - var gzipWriter *gzip.Writer - - // 根据是否gzip确定 writer 的创建 - if compress == "gzip" { - gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter - bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小 - } else { - bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter - } - - //确保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 := bufWriter.Flush(); flushErr != nil { - logError("writer flush failed %v", flushErr) - // 如果已经存在错误,则保留。否则,记录此错误。 - if err == nil { - err = flushErr - } - } - }() - - // 使用正则表达式匹配 http 和 https 链接 - for { - line, readErr := bufReader.ReadString('\n') - if readErr != nil { - if readErr == io.EOF { - break // 文件结束 - } - err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误 - return // Goroutine 中使用 return 返回错误 - } - - // 替换所有匹配的 URL - modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string { - logDump("originalURL: %s", originalURL) - return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义 - }) - - n, writeErr := bufWriter.WriteString(modifiedLine) - written += int64(n) // 更新写入的字节数 - if writeErr != nil { - err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误 - return // Goroutine 中使用 return 返回错误 - } - } - - // 在返回之前,再刷新一次 (虽然 defer 中已经有 flush,但这里再加一次确保及时刷新) - if flushErr := bufWriter.Flush(); flushErr != nil { - if err == nil { // 避免覆盖之前的错误 - err = flushErr - } - return // Goroutine 中使用 return 返回错误 - } - }() - - return readerOut, written, nil // 返回 reader 和 written,error 由 Goroutine 通过 pipeWriter.CloseWithError 传递 -} diff --git a/proxy/nest.go b/proxy/nest.go new file mode 100644 index 0000000..84f55cd --- /dev/null +++ b/proxy/nest.go @@ -0,0 +1,185 @@ +// Copyright 2025 WJQSERVER, WJQSERVER-STUDIO. All rights reserved. +// 使用本源代码受 WSL 2.0(WJQserver Studio License v2.0)与MPL 2.0(Mozilla Public License v2.0)许可协议的约束 +// 此段代码使用双重授权许可, 允许用户选择其中一种许可证 + +package proxy + +import ( + "bufio" + "compress/gzip" + "fmt" + "ghproxy/config" + "io" + "strings" +) + +func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) { + // 匹配 "https://github.com"开头的链接 + if strings.HasPrefix(rawPath, "https://github.com") { + return true, nil + } + // 匹配 "https://raw.githubusercontent.com"开头的链接 + if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") { + return true, nil + } + // 匹配 "https://raw.github.com"开头的链接 + if strings.HasPrefix(rawPath, "https://raw.github.com") { + return true, nil + } + // 匹配 "https://gist.githubusercontent.com"开头的链接 + if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") { + return true, nil + } + // 匹配 "https://gist.github.com"开头的链接 + if strings.HasPrefix(rawPath, "https://gist.github.com") { + return true, nil + } + if cfg.Shell.RewriteAPI { + // 匹配 "https://api.github.com/"开头的链接 + if strings.HasPrefix(rawPath, "https://api.github.com") { + return true, nil + } + } + return false, nil +} + +// 匹配文件扩展名是sh的rawPath +func MatcherShell(rawPath string) bool { + return strings.HasSuffix(rawPath, ".sh") +} + +// 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 { + var u = url + u = strings.TrimPrefix(u, "https://") + u = strings.TrimPrefix(u, "http://") + logDump("Modified URL: %s", "https://"+host+"/"+u) + return "https://" + host + "/" + u + } + return url +} + +// processLinks 处理链接,返回包含处理后数据的 io.Reader +func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) { + pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe + readerOut = pipeReader + + go func() { // 在 Goroutine 中执行写入操作 + defer func() { + if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误 + if err != nil { + if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader + logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err) + } + } else { + if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭 + logError("pipeWriter close failed: %v", closeErr) + if err == nil { // 如果之前没有错误,记录关闭错误 + err = closeErr + } + } + } + } + }() + + defer func() { + if err := input.Close(); err != nil { + logError("input close failed: %v", err) + } + + }() + + var bufReader *bufio.Reader + + if compress == "gzip" { + // 解压gzip + gzipReader, gzipErr := gzip.NewReader(input) + if gzipErr != nil { + err = fmt.Errorf("gzip解压错误: %v", gzipErr) + return // Goroutine 中使用 return 返回错误 + } + defer gzipReader.Close() + bufReader = bufio.NewReader(gzipReader) + } else { + bufReader = bufio.NewReader(input) + } + + var bufWriter *bufio.Writer + var gzipWriter *gzip.Writer + + // 根据是否gzip确定 writer 的创建 + if compress == "gzip" { + gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter + bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小 + } else { + bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter + } + + //确保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 := bufWriter.Flush(); flushErr != nil { + logError("writer flush failed %v", flushErr) + // 如果已经存在错误,则保留。否则,记录此错误。 + if err == nil { + err = flushErr + } + } + }() + + // 使用正则表达式匹配 http 和 https 链接 + for { + line, readErr := bufReader.ReadString('\n') + if readErr != nil { + if readErr == io.EOF { + break // 文件结束 + } + err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误 + return // Goroutine 中使用 return 返回错误 + } + + // 替换所有匹配的 URL + modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string { + logDump("originalURL: %s", originalURL) + return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义 + }) + + n, writeErr := bufWriter.WriteString(modifiedLine) + written += int64(n) // 更新写入的字节数 + if writeErr != nil { + err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误 + return // Goroutine 中使用 return 返回错误 + } + } + + // 在返回之前,再刷新一次 (虽然 defer 中已经有 flush,但这里再加一次确保及时刷新) + if flushErr := bufWriter.Flush(); flushErr != nil { + if err == nil { // 避免覆盖之前的错误 + err = flushErr + } + return // Goroutine 中使用 return 返回错误 + } + }() + + return readerOut, written, nil // 返回 reader 和 written,error 由 Goroutine 通过 pipeWriter.CloseWithError 传递 +} From d38ca3969f482c3e11929b9d6fb7c9f8d078c504 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 20 May 2025 10:03:48 +0800 Subject: [PATCH 084/118] revert route handle for 3.3.x --- main.go | 68 ++++++++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/main.go b/main.go index 041acab..484d59f 100644 --- a/main.go +++ b/main.go @@ -468,44 +468,52 @@ func main() { proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { - proxy.GhcrRouting(cfg)(ctx, c) + // for 3.4.0 - /* - //proxy.GhcrRouting(cfg)(ctx, c) - // 返回200与空json - //c.JSON(200, map[string]interface{}{}) - emptyJSON := "{}" - //emptyJSON := `{"name":"disable-list-tags","tags":[]}` - c.Header("Content-Type", "application/json") - c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) - c.String(200, emptyJSON) - */ - /* - emptyJSON := "{}" - c.Header("Content-Type", "application/json") - c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) + /* + r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { + proxy.GhcrRouting(cfg)(ctx, c) - c.Header("Docker-Distribution-API-Version", "registry/2.0") + /* + //proxy.GhcrRouting(cfg)(ctx, c) + // 返回200与空json + //c.JSON(200, map[string]interface{}{}) + emptyJSON := "{}" + //emptyJSON := `{"name":"disable-list-tags","tags":[]}` + c.Header("Content-Type", "application/json") + c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) + c.String(200, emptyJSON) + */ + /* + emptyJSON := "{}" + c.Header("Content-Type", "application/json") + c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) - c.Status(200) - c.Write([]byte(emptyJSON)) - */ + c.Header("Docker-Distribution-API-Version", "registry/2.0") - /* - w := adaptor.GetCompatResponseWriter(&c.Response) + c.Status(200) + c.Write([]byte(emptyJSON)) + */ - const emptyJSON = "{}" - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) - w.Header().Del("Server") + /* + w := adaptor.GetCompatResponseWriter(&c.Response) - fmt.Fprint(w, emptyJSON) - */ + const emptyJSON = "{}" + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) + w.Header().Del("Server") - }) + fmt.Fprint(w, emptyJSON) + */ + /* + }) - r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) { + proxy.GhcrRouting(cfg)(ctx, c) + }) + */ + + r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) { proxy.GhcrRouting(cfg)(ctx, c) }) From 5ddbf1d2a0ba0aa14d578abaf67f73f2bbe8cf57 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 20 May 2025 10:05:55 +0800 Subject: [PATCH 085/118] 3.3.3 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d54b1..427ec1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.3.3 - 2025-05-20 +--- +- CHANGE: 加入`senseClientDisconnection`与`async`配置项 + 25w39a - 2025-05-19 --- - PRE-RELEASE: 此版本是v3.3.3预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 5436ea0..3f09e91 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.2 \ No newline at end of file +3.3.3 \ No newline at end of file From 11099176bfd226df48c6e608eeb0cff8f7076dcd Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 09:03:00 +0800 Subject: [PATCH 086/118] add support for multi-target docker image(oci) proxy --- main.go | 53 +++------ proxy/authparse.go | 62 ++++++++++ proxy/docker.go | 247 ++++++++++++++++++++++++++++++++++++--- proxy/httpc.go | 61 +++++++++- proxy/match.go | 9 +- proxy/utils.go | 6 +- weakcache/weakcache.go | 258 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 633 insertions(+), 63 deletions(-) create mode 100644 proxy/authparse.go create mode 100644 weakcache/weakcache.go diff --git a/main.go b/main.go index 484d59f..2236e2a 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "ghproxy/middleware/loggin" "ghproxy/proxy" "ghproxy/rate" + "ghproxy/weakcache" "github.com/WJQSERVER-STUDIO/logger" "github.com/hertz-contrib/http2/factory" @@ -50,6 +51,10 @@ var ( pagesFS embed.FS ) +var ( + wcache *weakcache.Cache[string] // docker token缓存 +) + var ( logw = logger.Logw logDump = logger.LogDump @@ -360,6 +365,9 @@ func init() { setMemLimit(cfg) loadlist(cfg) setupRateLimit(cfg) + if cfg.Docker.Enabled { + wcache = proxy.InitWeakCache() + } if cfg.Server.Debug { runMode = "dev" @@ -468,23 +476,7 @@ func main() { proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) - // for 3.4.0 - - /* - r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { - proxy.GhcrRouting(cfg)(ctx, c) - - /* - //proxy.GhcrRouting(cfg)(ctx, c) - // 返回200与空json - //c.JSON(200, map[string]interface{}{}) - emptyJSON := "{}" - //emptyJSON := `{"name":"disable-list-tags","tags":[]}` - c.Header("Content-Type", "application/json") - c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) - c.String(200, emptyJSON) - */ - /* + r.GET("/v2/", func(ctx context.Context, c *app.RequestContext) { emptyJSON := "{}" c.Header("Content-Type", "application/json") c.Header("Content-Length", fmt.Sprint(len(emptyJSON))) @@ -493,30 +485,18 @@ func main() { c.Status(200) c.Write([]byte(emptyJSON)) - */ + }) + + r.Any("/v2/:target/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + proxy.GhcrWithImageRouting(cfg)(ctx, c) + }) /* - w := adaptor.GetCompatResponseWriter(&c.Response) - - const emptyJSON = "{}" - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) - w.Header().Del("Server") - - fmt.Fprint(w, emptyJSON) - */ - /* - }) - r.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) { proxy.GhcrRouting(cfg)(ctx, c) }) */ - r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) { - proxy.GhcrRouting(cfg)(ctx, c) - }) - r.NoRoute(func(ctx context.Context, c *app.RequestContext) { proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) }) @@ -531,7 +511,7 @@ func main() { }() } - r.Spin() + defer wcache.StopCleanup() defer logger.Close() defer func() { if hertZfile != nil { @@ -541,5 +521,8 @@ func main() { } } }() + + r.Spin() + fmt.Println("Program Exit") } diff --git a/proxy/authparse.go b/proxy/authparse.go new file mode 100644 index 0000000..f353db9 --- /dev/null +++ b/proxy/authparse.go @@ -0,0 +1,62 @@ +package proxy + +import ( + "fmt" + "strings" +) + +// BearerAuthParams 用于存放解析出的 Bearer 认证参数 +type BearerAuthParams struct { + Realm string + Service string + Scope string +} + +// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。 +// 它期望格式为 'Bearer key1="value1",key2="value2",...' +// 并尝试将已知参数解析到 BearerAuthParams struct 中。 +func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) { + if headerValue == "" { + return nil, fmt.Errorf("header value is empty") + } + + // 检查 Scheme 是否是 "Bearer" + parts := strings.SplitN(headerValue, " ", 2) + if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { + return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue) + } + paramsStr := parts[1] + + paramPairs := strings.Split(paramsStr, ",") + tempMap := make(map[string]string) + + for _, pair := range paramPairs { + trimmedPair := strings.TrimSpace(pair) + keyValue := strings.SplitN(trimmedPair, "=", 2) + if len(keyValue) != 2 { + logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue) + continue + } + key := strings.TrimSpace(keyValue[0]) + value := strings.TrimSpace(keyValue[1]) + if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") { + value = value[1 : len(value)-1] + } + tempMap[key] = value + } + + //从 map 中提取值并填充到 struct + authParams := &BearerAuthParams{} + + if realm, ok := tempMap["realm"]; ok { + authParams.Realm = realm + } + if service, ok := tempMap["service"]; ok { + authParams.Service = service + } + if scope, ok := tempMap["scope"]; ok { + authParams.Scope = scope + } + + return authParams, nil +} diff --git a/proxy/docker.go b/proxy/docker.go index cdfc743..d4db75e 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -2,39 +2,132 @@ package proxy import ( "context" + "encoding/json" "fmt" + "ghproxy/config" + "ghproxy/weakcache" + "io" "net/http" "strconv" + "strings" "github.com/WJQSERVER-STUDIO/go-utils/limitreader" "github.com/cloudwego/hertz/pkg/app" ) +var ( + dockerhubTarget = "registry-1.docker.io" + ghcrTarget = "ghcr.io" +) + +var cache *weakcache.Cache[string] + +type imageInfo struct { + User string + Repo string + Image string +} + +func InitWeakCache() *weakcache.Cache[string] { + cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100) + return cache +} + +/* func GhcrRouting(cfg *config.Config) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { - if cfg.Docker.Enabled { + + charToFind := '.' + reqTarget := c.Param("target") + path := "" + target := "" + + if strings.ContainsRune(reqTarget, charToFind) { + + path = c.Param("filepath") + if reqTarget == "docker.io" { + target = dockerhubTarget + } else if reqTarget == "ghcr.io" { + target = ghcrTarget + } else { + target = reqTarget + } + } else { + path = string(c.Request.RequestURI()) + } + + GhcrToTarget(ctx, c, cfg, target, path, nil) + + } +} +*/ + +func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + + charToFind := '.' + reqTarget := c.Param("target") + reqImageUser := c.Param("user") + reqImageName := c.Param("repo") + reqFilePath := c.Param("filepath") + + path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath) + target := "" + + if strings.ContainsRune(reqTarget, charToFind) { + + if reqTarget == "docker.io" { + target = dockerhubTarget + } else if reqTarget == "ghcr.io" { + target = ghcrTarget + } else { + target = reqTarget + } + } else { + path = string(c.Request.RequestURI()) + reqImageUser = c.Param("target") + reqImageName = c.Param("user") + } + image := &imageInfo{ + User: reqImageUser, + Repo: reqImageName, + Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName), + } + + GhcrToTarget(ctx, c, cfg, target, path, image) + + } + +} + +func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) { + if cfg.Docker.Enabled { + if target != "" { + GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+string(c.Request.QueryString()), image, cfg, target) + + } else { if cfg.Docker.Target == "ghcr" { - GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr") + GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget) } else if cfg.Docker.Target == "dockerhub" { - GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub") + GhcrRequest(ctx, c, "https://"+dockerhubTarget+string(c.Request.RequestURI()), image, cfg, dockerhubTarget) } else if cfg.Docker.Target != "" { // 自定义taget - GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), cfg, "custom") + GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), image, cfg, cfg.Docker.Target) } else { // 配置为空 ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set")) return } - - } else { - ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed")) - return } + + } else { + ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed")) + return } } -func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) { +func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) { var ( method []byte @@ -55,12 +148,11 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf method = c.Request.Method() - rb := client.NewRequestBuilder(string(method), u) + rb := ghcrclient.NewRequestBuilder(string(method), u) rb.NoDefaultHeaders() rb.SetBody(c.Request.BodyStream()) rb.WithContext(ctx) - //req, err = client.NewRequest(string(method), u, c.Request.BodyStream()) req, err = rb.Build() if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) @@ -73,14 +165,62 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf req.Header.Add(headerKey, headerValue) }) - resp, err = client.Do(req) + req.Header.Set("Host", target) + token, exist := cache.Get(image.Image) + if exist { + logDebug("Use Cache Token: %s", token) + req.Header.Set("Authorization", "Bearer "+token) + } + + resp, err = ghcrclient.Do(req) if err != nil { HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) return } - // 错误处理(404) - if resp.StatusCode == 404 { + // 处理状态码 + if resp.StatusCode == 401 { + // 请求target /v2/路径 + if string(c.Request.URI().Path()) != "/v2/" { + resp.Body.Close() + token := ChallengeReq(target, image, ctx, c) + + // 更新kv + if token != "" { + logDump("Update Cache Token: %s", token) + cache.Put(image.Image, token) + } + + rb := ghcrclient.NewRequestBuilder(string(method), u) + rb.NoDefaultHeaders() + rb.SetBody(c.Request.BodyStream()) + rb.WithContext(ctx) + + req, err = rb.Build() + if err != nil { + HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) + return + } + + c.Request.Header.VisitAll(func(key, value []byte) { + headerKey := string(key) + headerValue := string(value) + req.Header.Add(headerKey, headerValue) + }) + + req.Header.Set("Host", target) + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + resp, err = ghcrclient.Do(req) + if err != nil { + HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) + return + } + } + + } else if resp.StatusCode == 404 { // 错误处理(404) ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)")) return } @@ -101,8 +241,7 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf bodySize = -1 } if err == nil && bodySize > sizelimit { - var finalURL string - finalURL = resp.Request.URL.String() + finalURL := resp.Request.URL.String() err = resp.Body.Close() if err != nil { logError("Failed to close response body: %v", err) @@ -116,7 +255,6 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf // 复制响应头,排除需要移除的 header for key, values := range resp.Header { for _, value := range values { - //c.Header(key, value) c.Response.Header.Add(key, value) } } @@ -136,3 +274,78 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *conf c.SetBodyStream(bodyReader, -1) } + +type AuthToken struct { + Token string `json:"token"` +} + +func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *app.RequestContext) (token string) { + var resp401 *http.Response + var req401 *http.Request + var err error + + rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/") + rb401.NoDefaultHeaders() + rb401.WithContext(ctx) + rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ") + req401, err = rb401.Build() + if err != nil { + HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) + return + } + req401.Header.Set("Host", target) + + resp401, err = ghcrclient.Do(req401) + if err != nil { + HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) + return + } + defer resp401.Body.Close() + bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate")) + if err != nil { + logError("Failed to parse Www-Authenticate header: %v", err) + return + } + + scope := fmt.Sprintf("repository:%s:pull", image.Image) + + getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm). + NoDefaultHeaders(). + WithContext(ctx). + AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 "). + SetHeader("Host", bearer.Service). + AddQueryParam("service", bearer.Service). + AddQueryParam("scope", scope) + + getAuthReq, err := getAuthRB.Build() + if err != nil { + logError("Failed to create request: %v", err) + return + } + + authResp, err := ghcrclient.Do(getAuthReq) + if err != nil { + logError("Failed to send request: %v", err) + return + } + + defer authResp.Body.Close() + + bodyBytes, err := io.ReadAll(authResp.Body) + if err != nil { + logError("Failed to read auth response body: %v", err) + return + } + + // 解码json + var authToken AuthToken + err = json.Unmarshal(bodyBytes, &authToken) + if err != nil { + logError("Failed to decode auth response body: %v", err) + return + } + token = authToken.Token + + return token + +} diff --git a/proxy/httpc.go b/proxy/httpc.go index 83de29b..4d8bc4c 100644 --- a/proxy/httpc.go +++ b/proxy/httpc.go @@ -12,10 +12,12 @@ import ( var BufferSize int = 32 * 1024 // 32KB var ( - tr *http.Transport - gittr *http.Transport - client *httpc.Client - gitclient *httpc.Client + tr *http.Transport + gittr *http.Transport + client *httpc.Client + gitclient *httpc.Client + ghcrtr *http.Transport + ghcrclient *httpc.Client ) func InitReq(cfg *config.Config) error { @@ -23,11 +25,13 @@ func InitReq(cfg *config.Config) error { if cfg.GitClone.Mode == "cache" { initGitHTTPClient(cfg) } + initGhcrHTTPClient(cfg) err := SetGlobalRateLimit(cfg) if err != nil { return err } return nil + } func initHTTPClient(cfg *config.Config) { @@ -77,6 +81,7 @@ func initHTTPClient(cfg *config.Config) { httpc.WithTransport(tr), ) } + } func initGitHTTPClient(cfg *config.Config) { @@ -147,3 +152,51 @@ func initGitHTTPClient(cfg *config.Config) { ) } } + +func initGhcrHTTPClient(cfg *config.Config) { + var proTolcols = new(http.Protocols) + proTolcols.SetHTTP1(true) + proTolcols.SetHTTP2(true) + if cfg.Httpc.Mode == "auto" { + + ghcrtr = &http.Transport{ + IdleConnTimeout: 30 * time.Second, + WriteBufferSize: 32 * 1024, // 32KB + ReadBufferSize: 32 * 1024, // 32KB + Protocols: proTolcols, + } + } else if cfg.Httpc.Mode == "advanced" { + ghcrtr = &http.Transport{ + MaxIdleConns: cfg.Httpc.MaxIdleConns, + MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost, + MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost, + WriteBufferSize: 32 * 1024, // 32KB + ReadBufferSize: 32 * 1024, // 32KB + Protocols: proTolcols, + } + } else { + // 错误的模式 + logError("unknown httpc mode: %s", cfg.Httpc.Mode) + fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode) + logWarning("use Auto to Run HTTP Client") + fmt.Println("use Auto to Run HTTP Client") + ghcrtr = &http.Transport{ + IdleConnTimeout: 30 * time.Second, + WriteBufferSize: 32 * 1024, // 32KB + ReadBufferSize: 32 * 1024, // 32KB + } + } + if cfg.Outbound.Enabled { + initTransport(cfg, ghcrtr) + } + if cfg.Server.Debug { + ghcrclient = httpc.New( + httpc.WithTransport(ghcrtr), + httpc.WithDumpLog(), + ) + } else { + ghcrclient = httpc.New( + httpc.WithTransport(ghcrtr), + ) + } +} diff --git a/proxy/match.go b/proxy/match.go index 9db0c23..cee4eaa 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -17,9 +17,12 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro // 匹配 "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, "/") - } + /* + if strings.HasPrefix(remainingPath, "/") { + remainingPath = strings.TrimPrefix(remainingPath, "/") + } + */ + remainingPath = strings.TrimPrefix(remainingPath, "/") // 预期格式/user/repo/more... // 取出user和repo和最后部分 parts := strings.Split(remainingPath, "/") diff --git a/proxy/utils.go b/proxy/utils.go index 21e7886..029dc26 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -13,8 +13,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri // 白名单检查 if cfg.Whitelist.Enabled { - var whitelist bool - whitelist = auth.CheckWhitelist(user, repo) + whitelist := auth.CheckWhitelist(user, repo) if !whitelist { ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo))) logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) @@ -24,8 +23,7 @@ func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo stri // 黑名单检查 if cfg.Blacklist.Enabled { - var blacklist bool - blacklist = auth.CheckBlacklist(user, repo) + blacklist := auth.CheckBlacklist(user, repo) if blacklist { ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo))) logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo) diff --git a/weakcache/weakcache.go b/weakcache/weakcache.go new file mode 100644 index 0000000..32d897c --- /dev/null +++ b/weakcache/weakcache.go @@ -0,0 +1,258 @@ +package weakcache + +import ( + "container/list" + "sync" + "time" + "weak" // Go 1.24 引入的 weak 包 +) + +// DefaultExpiration 默认过期时间,这里设置为 15 分钟。 +// 这是一个导出的常量,方便用户使用包时引用默认值。 +const DefaultExpiration = 5 * time.Minute + +// cleanupInterval 是后台清理 Go routine 的扫描间隔,这里设置为 5 分钟。 +// 这是一个内部常量,不导出。 +const cleanupInterval = 2 * time.Minute + +// cacheEntry 缓存项的内部结构。不导出。 +type cacheEntry[T any] struct { + Value T + Expiration time.Time + key string // 存储key,方便在list.Element中引用 +} + +// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。 +// 这是一个导出的类型。 +type Cache[T any] struct { + mu sync.RWMutex + + // 修正:缓存存储:key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针) + // weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]] + data map[string]weak.Pointer[cacheEntry[T]] + + // FIFO 链表:存储 key 的 list.Element + // 链表头部是最近放入的,尾部是最早放入的(最老的) + fifoList *list.List + // FIFO 元素的映射:key -> *list.Element + fifoMap map[string]*list.Element + + defaultExpiration time.Duration + maxSize int // 缓存最大容量,0 表示无限制 + + stopCleanup chan struct{} + wg sync.WaitGroup // 用于等待清理 Go routine 退出 +} + +// NewCache 创建一个新的缓存实例。 +// expiration: 新添加项的默认过期时间。如果为 0,则使用 DefaultExpiration。 +// maxSize: 缓存的最大容量,0 表示无限制。当达到上限时,采用 FIFO 策略淘汰。 +// 这是一个导出的构造函数。 +func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] { + if expiration <= 0 { + expiration = DefaultExpiration + } + + c := &Cache[T]{ + // 修正:初始化 map,值类型已修正 + data: make(map[string]weak.Pointer[cacheEntry[T]]), + fifoList: list.New(), + fifoMap: make(map[string]*list.Element), + defaultExpiration: expiration, + maxSize: maxSize, + stopCleanup: make(chan struct{}), + } + // 启动后台清理 Go routine + c.wg.Add(1) + go c.cleanupLoop() + return c +} + +// Put 将值放入缓存。如果 key 已存在,会更新其值和过期时间。 +// 这是导出的方法。 +func (c *Cache[T]) Put(key string, value T) { + c.mu.Lock() + defer c.mu.Unlock() + + now := time.Now() + expiration := now.Add(c.defaultExpiration) + + // 如果 key 已经存在,更新其值和过期时间。 + // 在 FIFO 策略中, Put 更新不改变其在链表中的位置,除非旧的 entry 已经被 GC。 + if elem, ok := c.fifoMap[key]; ok { + // 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]] + if wp, dataOk := c.data[key]; dataOk { + // wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T] + entry := wp.Value() + if entry != nil { + // 旧的 cacheEntry 仍在内存中,直接更新 + entry.Value = value + entry.Expiration = expiration + // 在严格 FIFO 中,更新不移动位置 + return + } + // 如果 weak.Pointer.Value() 为 nil,说明之前的 cacheEntry 已经被 GC 了 + // 此时需要创建一个新的 entry,并将其从旧位置移除,再重新添加 + c.fifoList.Remove(elem) + delete(c.fifoMap, key) + } else { + c.fifoList.Remove(elem) + delete(c.fifoMap, key) + } + } + + // 新建缓存项 (注意这里是结构体值,而不是指针) + // weak.Make 接收的是指针 *T + entry := &cacheEntry[T]{ // 创建结构体指针 + Value: value, + Expiration: expiration, + key: key, // 存储 key + } + + // 将新的 *cacheEntry[T] 包装成 weak.Pointer[cacheEntry[T]] 存入 data map + // weak.Make(entry) 现在返回 weak.Pointer[cacheEntry[T]],类型匹配 data map 的值类型 + c.data[key] = weak.Make(entry) + + // 添加到 FIFO 链表头部 (最近放入/更新的在头部) + // PushFront 返回新的 list.Element + c.fifoMap[key] = c.fifoList.PushFront(key) + + // 检查大小上限并进行淘汰 (淘汰尾部的最老项) + c.evictIfNeeded() +} + +// Get 从缓存中获取值。返回获取到的值和是否存在/是否有效。 +// 这是导出的方法。 +func (c *Cache[T]) Get(key string) (T, bool) { + c.mu.RLock() // 先读锁 + // 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]] + wp, ok := c.data[key] + c.mu.RUnlock() // 立即释放读锁,如果需要写操作(removeEntry)可以获得锁 + + var zero T // 零值 + + if !ok { + return zero, false + } + + // 尝试获取实际的 cacheEntry 指针 + // wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T] + entry := wp.Value() + + if entry == nil { + // 对象已被GC回收,需要清理此弱引用 + c.removeEntry(key) // 内部会加写锁 + return zero, false + } + + // 检查过期时间 (通过 entry 指针访问字段) + if time.Now().After(entry.Expiration) { + // 逻辑上已过期 + c.removeEntry(key) // 内部会加写锁 + return zero, false + } + + // 在 FIFO 缓存中,Get 操作不改变项在链表中的位置 + return entry.Value, true // 通过 entry 指针访问值字段 +} + +// removeEntry 从缓存中移除项。 +// 这个方法是内部使用的,不导出。需要被调用者确保持有写锁,或者内部自己加锁。 +// 考虑到 Get 和 cleanupLoop 可能会调用,让其内部自己加锁更安全。 +func (c *Cache[T]) removeEntry(key string) { + c.mu.Lock() + defer c.mu.Unlock() + + // 从 data map 中删除 + delete(c.data, key) + + // 从 FIFO 链表和 fifoMap 中删除 + if elem, ok := c.fifoMap[key]; ok { + c.fifoList.Remove(elem) + delete(c.fifoMap, key) + } +} + +// evictIfNeeded 检查是否需要淘汰最老(FIFO 链表尾部)的项。 +// 这个方法是内部使用的,不导出。必须在持有写锁的情况下调用。 +func (c *Cache[T]) evictIfNeeded() { + if c.maxSize > 0 && c.fifoList.Len() > c.maxSize { + // 淘汰 FIFO 链表尾部的元素 (最老的) + oldest := c.fifoList.Back() + if oldest != nil { + keyToEvict := oldest.Value.(string) // 链表元素存储的是 key + c.fifoList.Remove(oldest) + delete(c.fifoMap, keyToEvict) + delete(c.data, keyToEvict) // 移除弱引用 + } + } +} + +// Size 返回当前缓存中的弱引用项数量。 +// 注意:这个数量可能包含已被 GC 回收但尚未清理的项。 +// 这是一个导出的方法。 +func (c *Cache[T]) Size() int { + c.mu.RLock() + defer c.mu.RUnlock() + return len(c.data) +} + +// cleanupLoop 后台清理 Go routine。不导出。 +func (c *Cache[T]) cleanupLoop() { + defer c.wg.Done() + // 使用内部常量 cleanupInterval + ticker := time.NewTicker(cleanupInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + c.cleanupExpiredAndGCed() + case <-c.stopCleanup: + return + } + } +} + +// cleanupExpiredAndGCed 扫描并清理已过期或已被 GC 回收的项。不导出。 +func (c *Cache[T]) cleanupExpiredAndGCed() { + c.mu.Lock() // 清理时需要写锁 + defer c.mu.Unlock() + + now := time.Now() + keysToRemove := make([]string, 0, len(c.data)) // 预估容量 + + // 遍历 data map 查找需要清理的键 + for key, wp := range c.data { + // wp 的类型是 weak.Pointer[cacheEntry[T]] + // wp.Value() 返回 *cacheEntry[T], entry 的类型是 *cacheEntry[T] + entry := wp.Value() // 尝试获取强引用 + + if entry == nil { + // 已被 GC 回收 + keysToRemove = append(keysToRemove, key) + } else if now.After(entry.Expiration) { + // 逻辑过期 (通过 entry 指针访问字段) + keysToRemove = append(keysToRemove, key) + } + } + + // 执行删除操作 + for _, key := range keysToRemove { + // 从 data map 中删除 + delete(c.data, key) + // 从 FIFO 链表和 fifoMap 中删除 + // 需要再次检查 fifoMap,因为在持有锁期间,evictIfNeeded 可能已经移除了这个 key + if elem, ok := c.fifoMap[key]; ok { + c.fifoList.Remove(elem) + delete(c.fifoMap, key) + } + } +} + +// StopCleanup 停止后台清理 Go routine。 +// 这是一个导出的方法。 +func (c *Cache[T]) StopCleanup() { + close(c.stopCleanup) + c.wg.Wait() // 等待 Go routine 退出 +} From c7954ae91aba1eb947f0fd0b0df1f6c977637083 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 09:03:14 +0800 Subject: [PATCH 087/118] 25w40a --- CHANGELOG.md | 6 ++++++ DEV-VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 427ec1b..aa7ba43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w40a - 2025-05-21 +--- +- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用; +- ADD: 初步实现多`target` Docker代理 +- ADD: 加入`weakcache`用于处理短期令牌 + 3.3.3 - 2025-05-20 --- - CHANGE: 加入`senseClientDisconnection`与`async`配置项 diff --git a/DEV-VERSION b/DEV-VERSION index fdef6bc..2114cc7 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w39a \ No newline at end of file +25w40a \ No newline at end of file From 6ff23f639ebc9143e11b6a6bcea29657292ebabd Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 11:54:43 +0800 Subject: [PATCH 088/118] add hub theme & add more check for wcache close --- main.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/main.go b/main.go index 2236e2a..6b84afe 100644 --- a/main.go +++ b/main.go @@ -213,6 +213,8 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { pages, err = fs.Sub(pagesFS, "pages/classic") case "mino": pages, err = fs.Sub(pagesFS, "pages/mino") + case "hub": + pages, err = fs.Sub(pagesFS, "pages/hub") default: pages, err = fs.Sub(pagesFS, "pages/design") // 默认主题 logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme) @@ -294,7 +296,7 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error { staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) }) r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) + staticServer := http.FileServer(http.FS(assets)) req, err := adaptor.GetCompatRequest(&c.Request) if err != nil { logError("%s", err) @@ -510,8 +512,9 @@ func main() { http.ListenAndServe("localhost:6060", nil) }() } - - defer wcache.StopCleanup() + if wcache != nil { + defer wcache.StopCleanup() + } defer logger.Close() defer func() { if hertZfile != nil { From 31d435bfa0acafc21eb89ad6270535f35b290b84 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 11:55:04 +0800 Subject: [PATCH 089/118] add oci proxy & nest shell api --- api/api.go | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/api/api.go b/api/api.go index 0c34ac0..e8ca10f 100644 --- a/api/api.go +++ b/api/api.go @@ -49,14 +49,18 @@ func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) { apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) { SmartGitStatusHandler(cfg, c, ctx) }) - + apiRouter.GET("/shell_nest/status", func(ctx context.Context, c *app.RequestContext) { + shellNestStatusHandler(cfg, c, ctx) + }) + apiRouter.GET("/oci_proxy/status", func(ctx context.Context, c *app.RequestContext) { + ociProxyStatusHandler(cfg, c, ctx) + }) } logInfo("API router Init success") } func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { sizeLimit := cfg.Server.SizeLimit - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "MaxResponseBodySize": sizeLimit, @@ -64,7 +68,6 @@ func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Con } func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Whitelist": cfg.Whitelist.Enabled, @@ -72,7 +75,6 @@ func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte } func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Blacklist": cfg.Blacklist.Enabled, @@ -80,7 +82,6 @@ func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte } func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Cors": cfg.Server.Cors, @@ -88,7 +89,6 @@ func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Co } func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Status": "OK", @@ -96,7 +96,6 @@ func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { } func VersionHandler(c *app.RequestContext, ctx context.Context, version string) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Version": version, @@ -104,7 +103,6 @@ func VersionHandler(c *app.RequestContext, ctx context.Context, version string) } func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "RateLimit": cfg.RateLimit.Enabled, @@ -112,7 +110,6 @@ func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte } func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "RatePerMinute": cfg.RateLimit.RatePerMinute, @@ -120,9 +117,23 @@ func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx contex } func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { - logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "enabled": cfg.GitClone.Mode == "cache", })) } + +func shellNestStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { + c.Response.Header.Set("Content-Type", "application/json") + c.JSON(200, (map[string]interface{}{ + "enabled": cfg.Shell.Editor, + })) +} + +func ociProxyStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { + c.Response.Header.Set("Content-Type", "application/json") + c.JSON(200, (map[string]interface{}{ + "enabled": cfg.Docker.Enabled, + "target": cfg.Docker.Target, + })) +} From 430e313d4739b374ac03425bd47d47c44ead0508 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 12:08:17 +0800 Subject: [PATCH 090/118] avoid nil *ptr & fix path --- proxy/docker.go | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/proxy/docker.go b/proxy/docker.go index d4db75e..3f4c43a 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -104,8 +104,7 @@ func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { func GhcrToTarget(ctx context.Context, c *app.RequestContext, cfg *config.Config, target string, path string, image *imageInfo) { if cfg.Docker.Enabled { if target != "" { - GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+string(c.Request.QueryString()), image, cfg, target) - + GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+string(c.Request.QueryString()), image, cfg, target) } else { if cfg.Docker.Target == "ghcr" { GhcrRequest(ctx, c, "https://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget) @@ -166,10 +165,12 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im }) req.Header.Set("Host", target) - token, exist := cache.Get(image.Image) - if exist { - logDebug("Use Cache Token: %s", token) - req.Header.Set("Authorization", "Bearer "+token) + if image != nil { + token, exist := cache.Get(image.Image) + if exist { + logDebug("Use Cache Token: %s", token) + req.Header.Set("Authorization", "Bearer "+token) + } } resp, err = ghcrclient.Do(req) @@ -183,6 +184,10 @@ func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *im // 请求target /v2/路径 if string(c.Request.URI().Path()) != "/v2/" { resp.Body.Close() + if image == nil { + ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized")) + return + } token := ChallengeReq(target, image, ctx, c) // 更新kv From 1afb3521942a952b57291086889b2d0db2af4017 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 15:24:05 +0800 Subject: [PATCH 091/118] 3.4.0 --- CHANGELOG.md | 14 ++++++++++++++ DEV-VERSION | 2 +- VERSION | 2 +- main.go | 1 + 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7ba43..1666a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # 更新日志 +3.4.0 - 2025-05-21 +--- +- ADD: 初步实现多`target` Docker代理 +- ADD: 加入`weakcache`用于处理短期令牌 +- ADD: 新增`hub`主题 +- ADD: 新增`/api/shell_nest/status`与`/api/oci_proxy/status` API + +25w40b - 2025-05-21 +--- +- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用; +- ADD: 新增`hub`主题 +- ADD: 新增`/api/shell_nest/status`与`/api/oci_proxy/status` API +- CHANGE: 对细节进行优化 + 25w40a - 2025-05-21 --- - PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 2114cc7..9c87980 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w40a \ No newline at end of file +25w40b \ No newline at end of file diff --git a/VERSION b/VERSION index 3f09e91..fbcbf73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.3 \ No newline at end of file +3.4.0 \ No newline at end of file diff --git a/main.go b/main.go index 6b84afe..2cbc5db 100644 --- a/main.go +++ b/main.go @@ -515,6 +515,7 @@ func main() { if wcache != nil { defer wcache.StopCleanup() } + defer logger.Close() defer func() { if hertZfile != nil { From 4598257faa9ce5a0b043dc0626a00291585a9734 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 21 May 2025 16:07:07 +0800 Subject: [PATCH 092/118] update readme.md --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9df6f2b..42815a0 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,11 @@ [GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) -[GHProxy项目文档Next(仍在建设中)](https://ghproxy-docs.pages.dev/) +[GHProxy项目文档Next(仍在建设中)](https://wjqserver.pages.dev/docs/category/ghproxy) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 ### 使用示例 -``` +```bash # 下载文件 https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh @@ -49,6 +49,15 @@ https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/t # 克隆仓库 git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git git clone https://ghproxy.1888866.xyz/https://github.com/WJQSERVER-STUDIO/ghproxy.git + +# Docker(OCI) 代理 +docker pull gh.example.com/wjqserver/ghproxy +docker pull gh.example.com/adguard/adguardhome + +docker pull gh.example.com/docker.io/wjqserver/ghproxy +docker pull gh.example.com/docker.io/adguard/adguardhome + +docker pull gh.example.com/ghcr.io/openfaas/queue-worker ``` ## 部署说明 From 16b6b05fb83146d7218db0aad63b00e8e6373534 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 28 May 2025 20:17:04 +0800 Subject: [PATCH 093/118] 25w41a --- CHANGELOG.md | 6 ++ DEV-VERSION | 2 +- auth/blacklist.go | 3 +- auth/whitelist.go | 3 +- go.mod | 10 ++- go.sum | 12 ++- proxy/docker.go | 3 +- proxy/error.go | 189 ++++++++++++++++++++++++++++++++++++++++++---- 8 files changed, 201 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1666a28..75b1830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +25w41a - 2025-05-28 +--- +- PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用; +- ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染 +- CHANGE: 替换到实验性的`encode/json/v2` + 3.4.0 - 2025-05-21 --- - ADD: 初步实现多`target` Docker代理 diff --git a/DEV-VERSION b/DEV-VERSION index 9c87980..cd3e78b 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w40b \ No newline at end of file +25w41a \ No newline at end of file diff --git a/auth/blacklist.go b/auth/blacklist.go index ba2091b..014a41d 100644 --- a/auth/blacklist.go +++ b/auth/blacklist.go @@ -1,12 +1,13 @@ package auth import ( - "encoding/json" "fmt" "ghproxy/config" "os" "strings" "sync" + + "github.com/go-json-experiment/json" ) type Blacklist struct { diff --git a/auth/whitelist.go b/auth/whitelist.go index ee93c20..1218307 100644 --- a/auth/whitelist.go +++ b/auth/whitelist.go @@ -1,12 +1,13 @@ package auth import ( - "encoding/json" "fmt" "ghproxy/config" "os" "strings" "sync" + + "github.com/go-json-experiment/json" ) // Whitelist 用于存储白名单信息 diff --git a/go.mod b/go.mod index d4a930a..0c5cb9b 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,18 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.5.1 - github.com/WJQSERVER-STUDIO/logger v1.7.1 + github.com/WJQSERVER-STUDIO/logger v1.7.2 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.40.0 golang.org/x/time v0.11.0 ) -require github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 +require ( + github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 + github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 + github.com/hashicorp/golang-lru/v2 v2.0.7 +) require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect @@ -26,7 +30,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect - github.com/nyaruka/phonenumbers v1.6.1 // indirect + github.com/nyaruka/phonenumbers v1.6.3 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect diff --git a/go.sum b/go.sum index a614fc3..1d2dadf 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSj github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/logger v1.7.1 h1:sAFsF3umimY0Vmue5WnGf1Qxvm/vlhK2srZakWVtlFU= -github.com/WJQSERVER-STUDIO/logger v1.7.1/go.mod h1:cvP0XdFIMLtDWOZeKhklshzipkVU1zufsU4rKNfoM24= +github.com/WJQSERVER-STUDIO/logger v1.7.2 h1:Tu9WICwlrY+BMQmY7k4llDB1ziFtZ9VmK7/85VIPN+M= +github.com/WJQSERVER-STUDIO/logger v1.7.2/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= @@ -35,10 +35,14 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= +github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hertz-contrib/http2 v0.1.8 h1:kjfCGkUxJZHgfPsnRjx1FLJBG55KvtvSQD214guBQLw= github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -50,8 +54,8 @@ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgSh github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940= -github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/nyaruka/phonenumbers v1.6.3 h1:JU7Q30+UM/03/vto6Q4EiZfEuRpTVyXMqImIbI942Qw= +github.com/nyaruka/phonenumbers v1.6.3/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= diff --git a/proxy/docker.go b/proxy/docker.go index 3f4c43a..08b23c3 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -2,9 +2,10 @@ package proxy import ( "context" - "encoding/json" "fmt" + "github.com/go-json-experiment/json" + "ghproxy/config" "ghproxy/weakcache" "io" diff --git a/proxy/error.go b/proxy/error.go index 756c881..b684193 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -2,12 +2,18 @@ package proxy import ( "bytes" + "crypto/sha256" + "encoding/gob" + "encoding/hex" + "sync" + "fmt" "html/template" "io/fs" "github.com/WJQSERVER-STUDIO/logger" "github.com/cloudwego/hertz/pkg/app" + lru "github.com/hashicorp/golang-lru/v2" ) // 日志模块 @@ -22,7 +28,7 @@ var ( func HandleError(c *app.RequestContext, message string) { ErrorPage(c, NewErrorWithStatusLookup(500, message)) - logError(message) + logError("Error handled: %s", message) } type GHProxyErrors struct { @@ -123,6 +129,22 @@ type ErrorPageData struct { ErrorMessage string } +// ToCacheKey 为 ErrorPageData 生成一个唯一的 SHA256 字符串键。 +// 使用 gob 序列化来确保结构体内容到字节序列的顺序一致性,然后计算哈希。 +func (d ErrorPageData) ToCacheKey() string { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err := enc.Encode(d) + if err != nil { + logError("Failed to gob encode ErrorPageData for cache key: %v", err) + panic(fmt.Errorf("failed to gob encode ErrorPageData: %w", err)) + } + + hasher := sha256.New() + hasher.Write(buf.Bytes()) + return hex.EncodeToString(hasher.Sum(nil)) +} + func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { return ErrorPageData{ StatusCode: errInfo.StatusCode, @@ -133,25 +155,130 @@ func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData { } } -func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { - pageData, err := htmlTemplateRender(errPagesFs, ErrPageUnwarper(errInfo)) - if err != nil { - c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) - logDebug("Error reading page.tmpl: %v", err) - return - } - c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) - return +// SizedLRUCache 实现了基于字节大小限制的 LRU 缓存。 +// 它包装了 hashicorp/golang-lru/v2.Cache,并额外管理缓存的总字节大小。 +type SizedLRUCache struct { + cache *lru.Cache[string, []byte] + mu sync.Mutex // 保护 currentBytes 字段 + maxBytes int64 // 缓存的最大字节容量 + currentBytes int64 // 缓存当前占用的字节数 } -func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) { - tmplPath := "page.tmpl" - tmpl, err := template.ParseFS(fsys, tmplPath) +// NewSizedLRUCache 创建一个新的 SizedLRUCache 实例。 +// 内部的 lru.Cache 的条目容量被设置为一个较大的值 (例如 10000), +// 因为主要的逐出逻辑将由字节大小限制来控制。 +func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) { + if maxBytes <= 0 { + return nil, fmt.Errorf("maxBytes must be positive") + } + + c := &SizedLRUCache{ + maxBytes: maxBytes, + } + + // 创建内部 LRU 缓存,并提供一个 OnEvictedFunc 回调函数。 + // 当内部 LRU 缓存因其自身的条目容量限制或 RemoveOldest 方法被调用而逐出条目时, + // 此回调函数会被执行,从而更新 currentBytes。 + var err error + c.cache, err = lru.NewWithEvict[string, []byte](10000, func(key string, value []byte) { + c.mu.Lock() + defer c.mu.Unlock() + c.currentBytes -= int64(len(value)) + logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes) + }) if err != nil { - return nil, fmt.Errorf("error parsing template: %w", err) + return nil, err + } + return c, nil +} + +// Get 从缓存中检索值。 +func (c *SizedLRUCache) Get(key string) ([]byte, bool) { + return c.cache.Get(key) +} + +// Add 向缓存中添加或更新一个键值对,并在必要时执行逐出以满足字节限制。 +func (c *SizedLRUCache) Add(key string, value []byte) { + c.mu.Lock() // 保护 currentBytes 和逐出逻辑 + defer c.mu.Unlock() + + itemSize := int64(len(value)) + + // 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。 + if itemSize > c.maxBytes { + logWarning("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes) + return + } + + // 如果键已存在,则首先从 currentBytes 中减去旧值的大小,并从内部 LRU 中移除旧条目。 + if oldVal, ok := c.cache.Get(key); ok { + c.currentBytes -= int64(len(oldVal)) + c.cache.Remove(key) + logDebug("Key %s exists, removed old size %d. Current total: %d", key, len(oldVal), c.currentBytes) + } + + // 主动逐出最旧的条目,直到有足够的空间容纳新条目。 + for c.currentBytes+itemSize > c.maxBytes && c.cache.Len() > 0 { + _, oldVal, existed := c.cache.RemoveOldest() + if !existed { + logWarning("Attempted to remove oldest, but item not found.") + break + } + logDebug("Proactively evicted item (size %d) to free space. Current total: %d", len(oldVal), c.currentBytes) + } + + // 添加新条目到内部 LRU 缓存。 + c.cache.Add(key, value) + c.currentBytes += itemSize // 手动增加新条目的大小到 currentBytes。 + logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes) +} + +const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量:512KB + +var errorPageCache *SizedLRUCache + +func init() { + // 初始化 SizedLRUCache。 + var err error + errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes) + if err != nil { + logError("Failed to initialize error page LRU cache: %v", err) + panic(err) + } +} + +// parsedTemplateOnce 用于确保 HTML 模板只被解析一次。 +var ( + parsedTemplateOnce sync.Once + parsedTemplate *template.Template + parsedTemplateErr error +) + +// getParsedTemplate 用于获取缓存的解析后的 HTML 模板。 +func getParsedTemplate() (*template.Template, error) { + parsedTemplateOnce.Do(func() { + tmplPath := "page.tmpl" + // 确保 errPagesFs 已初始化。这要求在任何 ErrorPage 调用之前调用 InitErrPagesFS。 + if errPagesFs == nil { + parsedTemplateErr = fmt.Errorf("errPagesFs not initialized. Call InitErrPagesFS first") + return + } + parsedTemplate, parsedTemplateErr = template.ParseFS(errPagesFs, tmplPath) + if parsedTemplateErr != nil { + parsedTemplate = nil + } + }) + return parsedTemplate, parsedTemplateErr +} + +// htmlTemplateRender 修改为使用缓存的模板。 +func htmlTemplateRender(data interface{}) ([]byte, error) { + tmpl, err := getParsedTemplate() + if err != nil { + return nil, fmt.Errorf("failed to get parsed template: %w", err) } if tmpl == nil { - return nil, fmt.Errorf("template is nil") + return nil, fmt.Errorf("template is nil after parsing") } // 创建一个 bytes.Buffer 用于存储渲染结果 @@ -159,9 +286,39 @@ func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) { err = tmpl.Execute(&buf, data) if err != nil { - return nil, fmt.Errorf("error executing template: %w", err) + return nil, fmt.Errorf("failed to execute template: %w", err) } // 返回 buffer 的内容作为 []byte return buf.Bytes(), nil } + +func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { + // 将 errInfo 转换为 ErrorPageData 结构体 + pageDataStruct := ErrPageUnwarper(errInfo) + // 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键 + cacheKey := pageDataStruct.ToCacheKey() + + var pageData []byte + var err error + + // 尝试从缓存中获取页面数据 + if cachedPage, found := errorPageCache.Get(cacheKey); found { + pageData = cachedPage + logDebug("Serving error page from cache (Key: %s)", cacheKey) + } else { + // 如果不在缓存中,则渲染页面 + pageData, err = htmlTemplateRender(pageDataStruct) + if err != nil { + c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) + logDebug("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err) + return + } + + // 将渲染结果存入缓存 + errorPageCache.Add(cacheKey, pageData) + logDebug("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData)) + } + + c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData) +} From 68bf51aaedf7ba2f89957c13f03f316033594e0b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 28 May 2025 21:35:52 +0800 Subject: [PATCH 094/118] 25w41b --- CHANGELOG.md | 7 ++++++- DEV-VERSION | 2 +- auth/blacklist.go | 2 +- auth/whitelist.go | 2 +- go.mod | 3 +-- go.sum | 2 -- proxy/docker.go | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75b1830..b292d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # 更新日志 +25w41b - 2025-05-28 +--- +- PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用; +- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic) + 25w41a - 2025-05-28 --- - PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用; - ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染 -- CHANGE: 替换到实验性的`encode/json/v2` +- CHANGE: 替换到实验性的`encoding/json/v2` 3.4.0 - 2025-05-21 --- diff --git a/DEV-VERSION b/DEV-VERSION index cd3e78b..82272f8 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w41a \ No newline at end of file +25w41b \ No newline at end of file diff --git a/auth/blacklist.go b/auth/blacklist.go index 014a41d..fd6b276 100644 --- a/auth/blacklist.go +++ b/auth/blacklist.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/go-json-experiment/json" + json "github.com/bytedance/sonic" ) type Blacklist struct { diff --git a/auth/whitelist.go b/auth/whitelist.go index 1218307..b4071d1 100644 --- a/auth/whitelist.go +++ b/auth/whitelist.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/go-json-experiment/json" + json "github.com/bytedance/sonic" ) // Whitelist 用于存储白名单信息 diff --git a/go.mod b/go.mod index 0c5cb9b..9f79dc7 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( require ( github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 - github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 + github.com/bytedance/sonic v1.13.2 github.com/hashicorp/golang-lru/v2 v2.0.7 ) @@ -22,7 +22,6 @@ require ( github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 // indirect github.com/bytedance/gopkg v0.1.2 // indirect - github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/gopkg v0.1.4 // indirect diff --git a/go.sum b/go.sum index 1d2dadf..36131ff 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk= -github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= diff --git a/proxy/docker.go b/proxy/docker.go index 08b23c3..eb5fadf 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/go-json-experiment/json" + json "github.com/bytedance/sonic" "ghproxy/config" "ghproxy/weakcache" From b7ce929db8c3700826c3def70e290f7a94f6ca91 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 29 May 2025 15:01:44 +0800 Subject: [PATCH 095/118] 3.4.1 --- CHANGELOG.md | 5 +++++ VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b292d3e..68201d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +3.4.1 - 2025-05-29 +--- +- ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染 +- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic) + 25w41b - 2025-05-28 --- - PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index fbcbf73..8cf6caf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.0 \ No newline at end of file +3.4.1 \ No newline at end of file From 82943428d340e8b85d9171c6271f44d1a233d619 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 29 May 2025 15:13:28 +0800 Subject: [PATCH 096/118] optimize cache & remove panic change to fallback error json --- proxy/error.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/proxy/error.go b/proxy/error.go index b684193..1820259 100644 --- a/proxy/error.go +++ b/proxy/error.go @@ -137,7 +137,7 @@ func (d ErrorPageData) ToCacheKey() string { err := enc.Encode(d) if err != nil { logError("Failed to gob encode ErrorPageData for cache key: %v", err) - panic(fmt.Errorf("failed to gob encode ErrorPageData: %w", err)) + return "" } hasher := sha256.New() @@ -298,6 +298,11 @@ func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { pageDataStruct := ErrPageUnwarper(errInfo) // 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键 cacheKey := pageDataStruct.ToCacheKey() + if cacheKey == "" { + c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) + logWarning("Failed to generate cache key for error page: %v", errInfo) + return + } var pageData []byte var err error @@ -311,7 +316,7 @@ func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) { pageData, err = htmlTemplateRender(pageDataStruct) if err != nil { c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage}) - logDebug("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err) + logWarning("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err) return } From 5b17e1f0b6fba4daf77da8700da891c00a99462e Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 30 May 2025 17:31:35 +0800 Subject: [PATCH 097/118] update docs link --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 42815a0..829de33 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ [相关文章](https://blog.wjqserver.com/categories/my-program/) -[GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) +[GHProxy项目文档Next](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 -[GHProxy项目文档Next(仍在建设中)](https://wjqserver.pages.dev/docs/category/ghproxy) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 +[GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) ### 使用示例 From 171fe613426677bd5bcb032eec57657e006f1c13 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 2 Jun 2025 21:00:49 +0800 Subject: [PATCH 098/118] 25w42a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- go.mod | 4 +++- go.sum | 8 ++++---- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68201d4..9c2b605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w42a - 2025-06-02 +--- +- PRE-RELEASE: 此版本是v3.4.2预发布版本,请勿在生产环境中使用; +- DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常 + 3.4.1 - 2025-05-29 --- - ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染 diff --git a/DEV-VERSION b/DEV-VERSION index 82272f8..0b20fcc 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w41b \ No newline at end of file +25w42a \ No newline at end of file diff --git a/go.mod b/go.mod index 9f79dc7..437f381 100644 --- a/go.mod +++ b/go.mod @@ -36,11 +36,13 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect golang.org/x/arch v0.17.0 // indirect - golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) +replace github.com/nyaruka/phonenumbers => github.com/nyaruka/phonenumbers v1.6.1 // 1.6.3 has reflect leaking + //replace github.com/WJQSERVER-STUDIO/httpc v0.5.1 => /data/github/WJQSERVER-STUDIO/httpc //replace github.com/WJQSERVER-STUDIO/logger v1.6.0 => /data/github/WJQSERVER-STUDIO/logger diff --git a/go.sum b/go.sum index 36131ff..5e3a90e 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgSh github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/nyaruka/phonenumbers v1.6.3 h1:JU7Q30+UM/03/vto6Q4EiZfEuRpTVyXMqImIbI942Qw= -github.com/nyaruka/phonenumbers v1.6.3/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940= +github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -91,8 +91,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= From 2bab0a977423480df81cf4fc83b494b29a1f2f3b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 3 Jun 2025 16:47:00 +0800 Subject: [PATCH 099/118] 3.4.2 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2b605..c2820fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.4.2 - 2025-06-03 +--- +- DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常 + 25w42a - 2025-06-02 --- - PRE-RELEASE: 此版本是v3.4.2预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 8cf6caf..a423d42 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.1 \ No newline at end of file +3.4.2 \ No newline at end of file From e829c2baff44e927919a42161b4570aab3a22c53 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 5 Jun 2025 00:24:21 +0800 Subject: [PATCH 100/118] 25w43a --- CHANGELOG.md | 7 +++ DEV-VERSION | 2 +- go.mod | 5 +- go.sum | 10 ++-- main.go | 149 +++++++++++++++++++++++++++++---------------------- 5 files changed, 101 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2820fb..49187fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +25w43a - 2025-06-05 +--- +- PRE-RELEASE: 此版本是v3.4.3预发布版本,请勿在生产环境中使用; +- CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler` +- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified` +- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate` + 3.4.2 - 2025-06-03 --- - DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常 diff --git a/DEV-VERSION b/DEV-VERSION index 0b20fcc..e303d5b 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w42a \ No newline at end of file +25w43a \ No newline at end of file diff --git a/go.mod b/go.mod index 437f381..90b925e 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.5.1 - github.com/WJQSERVER-STUDIO/logger v1.7.2 + github.com/WJQSERVER-STUDIO/logger v1.7.3 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.40.0 @@ -14,8 +14,9 @@ require ( require ( github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 - github.com/bytedance/sonic v1.13.2 + github.com/bytedance/sonic v1.13.3 github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/wjqserver/modembed v0.0.1 ) require ( diff --git a/go.sum b/go.sum index 5e3a90e..834e6c7 100644 --- a/go.sum +++ b/go.sum @@ -8,15 +8,15 @@ github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSj github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/logger v1.7.2 h1:Tu9WICwlrY+BMQmY7k4llDB1ziFtZ9VmK7/85VIPN+M= -github.com/WJQSERVER-STUDIO/logger v1.7.2/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= +github.com/WJQSERVER-STUDIO/logger v1.7.3 h1:XoFJ1nBcZKyMvP4v0MZv5jL2q7IkAF7yfXgwyB3MLP4= +github.com/WJQSERVER-STUDIO/logger v1.7.3/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4= github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk= -github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= -github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= +github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= @@ -84,6 +84,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/wjqserver/modembed v0.0.1 h1:8ZDz7t9M5DLrUFlYgBUUmrMzxWsZPmHvOazkr/T2jEs= +github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g9yyCztTXOQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= diff --git a/main.go b/main.go index 2cbc5db..a441752 100644 --- a/main.go +++ b/main.go @@ -21,6 +21,7 @@ import ( "github.com/WJQSERVER-STUDIO/logger" "github.com/hertz-contrib/http2/factory" + "github.com/wjqserver/modembed" "github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery" @@ -198,25 +199,26 @@ func InitReq(cfg *config.Config) { // loadEmbeddedPages 加载嵌入式页面资源 func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { + pageFS := modembed.NewModTimeFS(pagesFS, time.Now()) var pages fs.FS var err error switch cfg.Pages.Theme { case "bootstrap": - pages, err = fs.Sub(pagesFS, "pages/bootstrap") + pages, err = fs.Sub(pageFS, "pages/bootstrap") case "nebula": - pages, err = fs.Sub(pagesFS, "pages/nebula") + pages, err = fs.Sub(pageFS, "pages/nebula") case "design": - pages, err = fs.Sub(pagesFS, "pages/design") + pages, err = fs.Sub(pageFS, "pages/design") case "metro": - pages, err = fs.Sub(pagesFS, "pages/metro") + pages, err = fs.Sub(pageFS, "pages/metro") case "classic": - pages, err = fs.Sub(pagesFS, "pages/classic") + pages, err = fs.Sub(pageFS, "pages/classic") case "mino": - pages, err = fs.Sub(pagesFS, "pages/mino") + pages, err = fs.Sub(pageFS, "pages/mino") case "hub": - pages, err = fs.Sub(pagesFS, "pages/hub") + pages, err = fs.Sub(pageFS, "pages/hub") default: - pages, err = fs.Sub(pagesFS, "pages/design") // 默认主题 + pages, err = fs.Sub(pageFS, "pages/design") // 默认主题 logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme) } @@ -225,13 +227,16 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) { } // 初始化errPagesFs - errPagesInitErr := proxy.InitErrPagesFS(pagesFS) + errPagesInitErr := proxy.InitErrPagesFS(pageFS) if errPagesInitErr != nil { logWarning("errPagesInitErr: %s", errPagesInitErr) } var assets fs.FS - assets, err = fs.Sub(pagesFS, "pages/assets") + assets, err = fs.Sub(pageFS, "pages/assets") + if err != nil { + return nil, nil, fmt.Errorf("failed to load embedded assets: %w", err) + } return pages, assets, nil } @@ -277,6 +282,12 @@ func setupPages(cfg *config.Config, r *server.Hertz) { } } +func pageCacheHeader() func(ctx context.Context, c *app.RequestContext) { + return func(ctx context.Context, c *app.RequestContext) { + c.Header("Cache-Control", "public, max-age=3600, must-revalidate") + } +} + func setInternalRoute(cfg *config.Config, r *server.Hertz) error { // 加载嵌入式资源 @@ -285,61 +296,69 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error { logError("Failed when processing pages: %s", err) return err } - // 设置嵌入式资源路由 - r.GET("/", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(pages)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) - r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) { - staticServer := http.FileServer(http.FS(assets)) - req, err := adaptor.GetCompatRequest(&c.Request) - if err != nil { - logError("%s", err) - return - } - staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) - }) + /* + // 设置嵌入式资源路由 + r.GET("/", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(pages)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(assets)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(pages)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(pages)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(assets)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) { + staticServer := http.FileServer(http.FS(assets)) + req, err := adaptor.GetCompatRequest(&c.Request) + if err != nil { + logError("%s", err) + return + } + staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req) + }) + */ + r.GET("/", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) + r.GET("/favicon.ico", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) + r.GET("/script.js", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) + r.GET("/style.css", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(pages)))) + r.GET("/bootstrap.min.css", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) + r.GET("/bootstrap.bundle.min.js", pageCacheHeader(), adaptor.HertzHandler(http.FileServer(http.FS(assets)))) return nil } From 1370617f5ba91dbba72e738abba8a826db8178a1 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:29:18 +0800 Subject: [PATCH 101/118] 3.4.3 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49187fe..92b291b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +3.4.3 - 2025-06-05 +--- +- CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler` +- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified` +- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate` + 25w43a - 2025-06-05 --- - PRE-RELEASE: 此版本是v3.4.3预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index a423d42..8a0feb9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.2 \ No newline at end of file +3.4.3 \ No newline at end of file From 6be6e1ba2cce564707c7349b30f784ab1f099233 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 5 Jun 2025 20:14:58 +0800 Subject: [PATCH 102/118] 25w44a --- .github/workflows/build-dev.yml | 2 +- .github/workflows/build.yml | 2 +- .gitignore | 3 +-- CHANGELOG.md | 7 +++++++ DEV-VERSION | 2 +- LICENSE | 20 +++++++++++--------- README.md | 4 +--- config/config.go | 29 ++++++++++++++++------------- config/config.toml | 1 + docs/config.md | 2 ++ docs/flag.md | 2 ++ docs/menu.md | 2 ++ proxy/docker.go | 29 ----------------------------- proxy/utils.go | 4 +++- 14 files changed, 49 insertions(+), 60 deletions(-) diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml index ecee468..c1cb9d1 100644 --- a/.github/workflows/build-dev.yml +++ b/.github/workflows/build-dev.yml @@ -61,7 +61,7 @@ jobs: fi - name: 拉取前端 run: | - sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages + sudo git clone https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend.git pages sudo rm -rf pages/.git/ - name: 安装 Go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a02dfb..e0306c0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,7 +62,7 @@ jobs: fi - name: 拉取前端 run: | - sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages + sudo git clone https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend.git pages sudo rm -rf pages/.git/ - name: 安装 Go diff --git a/.gitignore b/.gitignore index 1834d15..0ad54a9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,4 @@ demo.toml list.json repos pages -*_test -.* \ No newline at end of file +*_test \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b291b..fa06781 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 更新日志 +25w44a - 2025-06-05 +--- +- PRE-RELEASE: 此版本是v3.5.0预发布版本,请勿在生产环境中使用; +- CHANGE: 更新许可证 v2.0 => v2.1 +- CHANGE: 修正工作流的一些问题 +- ADD: 增加`ForceAllowApiPassList`, 实现 #114 + 3.4.3 - 2025-06-05 --- - CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler` diff --git a/DEV-VERSION b/DEV-VERSION index e303d5b..c4bec82 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w43a \ No newline at end of file +25w44a \ No newline at end of file diff --git a/LICENSE b/LICENSE index de09e5d..fbdd300 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ WJQserver Studio 开源许可证 -版本 v2.0 +版本 v2.1 版权所有 © WJQserver Studio 2024 @@ -31,8 +31,7 @@ WJQserver Studio 开源许可证 * 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款: - * 1.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 - * 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: + * 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: * 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。 * 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。 @@ -44,6 +43,8 @@ WJQserver Studio 开源许可证 * i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。 * ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。 +* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 + 2. 复制与分发 * 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件: @@ -51,13 +52,13 @@ WJQserver Studio 开源许可证 * 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。 * 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。 -* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.2.2 条(开源继承与互惠共享)的约束。 +* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。 3. 修改权限 * 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。 -* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.2.2 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 +* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 * 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。 @@ -130,8 +131,7 @@ License Terms * 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms: - * 1.2.1 Maintain Statements: When conducting commercial use, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. - * 1.2.2 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: + * 1.2.1 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: * Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works. * Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services. @@ -143,6 +143,8 @@ License Terms * i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community. * ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation. +* 1.3 Maintain Statements: When publish services to public, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. + 2. Reproduction and Distribution * 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met: @@ -150,13 +152,13 @@ License Terms * Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices. * Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License. -* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing). +* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing). 3. Modification Permissions * 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use. -* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. +* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. * 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License. diff --git a/README.md b/README.md index 829de33..b0d49f9 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,7 @@ [相关文章](https://blog.wjqserver.com/categories/my-program/) -[GHProxy项目文档Next](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 - -[GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md) +[GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护 ### 使用示例 diff --git a/config/config.go b/config/config.go index c1300dc..0060b63 100644 --- a/config/config.go +++ b/config/config.go @@ -110,15 +110,17 @@ Key = "" Token = "token" enabled = false passThrough = false -ForceAllowApi = true +ForceAllowApi = false +ForceAllowApiPassList = false */ type AuthConfig struct { - Enabled bool `toml:"enabled"` - Method string `toml:"method"` - Key string `toml:"key"` - Token string `toml:"token"` - PassThrough bool `toml:"passThrough"` - ForceAllowApi bool `toml:"ForceAllowApi"` + Enabled bool `toml:"enabled"` + Method string `toml:"method"` + Key string `toml:"key"` + Token string `toml:"token"` + PassThrough bool `toml:"passThrough"` + ForceAllowApi bool `toml:"ForceAllowApi"` + ForceAllowApiPassList bool `toml:"ForceAllowApiPassList"` } type BlacklistConfig struct { @@ -258,12 +260,13 @@ func DefaultConfig() *Config { HertZLogPath: "/data/ghproxy/log/hertz.log", }, Auth: AuthConfig{ - Enabled: false, - Method: "parameters", - Key: "", - Token: "token", - PassThrough: false, - ForceAllowApi: false, + Enabled: false, + Method: "parameters", + Key: "", + Token: "token", + PassThrough: false, + ForceAllowApi: false, + ForceAllowApiPassList: false, }, Blacklist: BlacklistConfig{ Enabled: false, diff --git a/config/config.toml b/config/config.toml index c8ea1f5..9ef2662 100644 --- a/config/config.toml +++ b/config/config.toml @@ -44,6 +44,7 @@ key = "" enabled = false passThrough = false ForceAllowApi = false +ForceAllowApiPassList = false [blacklist] blacklistFile = "/data/ghproxy/config/blacklist.json" diff --git a/docs/config.md b/docs/config.md index e5de161..a01f762 100644 --- a/docs/config.md +++ b/docs/config.md @@ -1,5 +1,7 @@ # ghproxy 用户配置文档 +> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) + `ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml`、`blacklist.json` 和 `whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。 ## `config.toml` - 主配置文件 diff --git a/docs/flag.md b/docs/flag.md index cc09c4c..c3aa0b7 100644 --- a/docs/flag.md +++ b/docs/flag.md @@ -1,5 +1,7 @@ # Flag +> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) + GHProxy接受以下flag传入 ```bash diff --git a/docs/menu.md b/docs/menu.md index 369e4b5..7e2c0ed 100644 --- a/docs/menu.md +++ b/docs/menu.md @@ -1,5 +1,7 @@ ## GHProxy 文档 +> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) + ### 配置文件 https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md diff --git a/proxy/docker.go b/proxy/docker.go index eb5fadf..615041e 100644 --- a/proxy/docker.go +++ b/proxy/docker.go @@ -35,35 +35,6 @@ func InitWeakCache() *weakcache.Cache[string] { return cache } -/* -func GhcrRouting(cfg *config.Config) app.HandlerFunc { - return func(ctx context.Context, c *app.RequestContext) { - - charToFind := '.' - reqTarget := c.Param("target") - path := "" - target := "" - - if strings.ContainsRune(reqTarget, charToFind) { - - path = c.Param("filepath") - if reqTarget == "docker.io" { - target = dockerhubTarget - } else if reqTarget == "ghcr.io" { - target = ghcrTarget - } else { - target = reqTarget - } - } else { - path = string(c.Request.RequestURI()) - } - - GhcrToTarget(ctx, c, cfg, target, path, nil) - - } -} -*/ - func GhcrWithImageRouting(cfg *config.Config) app.HandlerFunc { return func(ctx context.Context, c *app.RequestContext) { diff --git a/proxy/utils.go b/proxy/utils.go index 029dc26..e770c2b 100644 --- a/proxy/utils.go +++ b/proxy/utils.go @@ -10,7 +10,9 @@ import ( ) func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool { - + if cfg.Auth.ForceAllowApi && cfg.Auth.ForceAllowApiPassList { + return false + } // 白名单检查 if cfg.Whitelist.Enabled { whitelist := auth.CheckWhitelist(user, repo) From 185522133b55f23550fc8ec58d7a7a40ee04d266 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Thu, 5 Jun 2025 22:55:24 +0800 Subject: [PATCH 103/118] 3.5.0 --- CHANGELOG.md | 6 ++++++ VERSION | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa06781..7922247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # 更新日志 +3.5.0 - 2025-06-05 +--- +- CHANGE: 更新许可证 v2.0 => v2.1 +- CHANGE: 修正工作流的一些问题 +- ADD: 增加`ForceAllowApiPassList`, 实现 #114 + 25w44a - 2025-06-05 --- - PRE-RELEASE: 此版本是v3.5.0预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 8a0feb9..e5b8203 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.3 \ No newline at end of file +3.5.0 \ No newline at end of file From f706615d87b755acdfb7f9e47510b917b413c65c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 7 Jun 2025 23:44:47 +0800 Subject: [PATCH 104/118] update deps --- go.mod | 10 +++++----- go.sum | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 90b925e..1ccfe73 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/WJQSERVER-STUDIO/logger v1.7.3 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 - golang.org/x/net v0.40.0 - golang.org/x/time v0.11.0 + golang.org/x/net v0.41.0 + golang.org/x/time v0.12.0 ) require ( @@ -36,10 +36,10 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - golang.org/x/arch v0.17.0 // indirect - golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect + golang.org/x/arch v0.18.0 // indirect + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index 834e6c7..4881472 100644 --- a/go.sum +++ b/go.sum @@ -89,12 +89,16 @@ github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= +golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -106,6 +110,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -135,8 +141,12 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 5dde21a403320031b43cd4b38189542e66ac11f0 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 9 Jun 2025 06:58:21 +0800 Subject: [PATCH 105/118] optimize matcher performance --- CHANGELOG.md | 5 + DEV-VERSION | 2 +- proxy/chunkreq.go | 2 +- proxy/match.go | 339 ++++++++++++++++++++++++++++-------------- proxy/matcher_test.go | 303 +++++++++++++++++++++++++++++++++++++ 5 files changed, 536 insertions(+), 115 deletions(-) create mode 100644 proxy/matcher_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 7922247..4df6a10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w45a - 2025-06-09 +--- +- PRE-RELEASE: 此版本是v3.5.1预发布版本,请勿在生产环境中使用; +- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` + 3.5.0 - 2025-06-05 --- - CHANGE: 更新许可证 v2.0 => v2.1 diff --git a/DEV-VERSION b/DEV-VERSION index c4bec82..ebe0357 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w44a \ No newline at end of file +25w45a \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index c760b9a..c50b7fb 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -110,7 +110,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor { + if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip var compress string if resp.Header.Get("Content-Encoding") == "gzip" { diff --git a/proxy/match.go b/proxy/match.go index cee4eaa..a28520e 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -6,152 +6,265 @@ import ( "net/url" "regexp" "strings" + "sync" ) +// Matcher 从原始URL路径中高效地解析并匹配代理规则. func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { - 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, "/") - } - */ - remainingPath = strings.TrimPrefix(remainingPath, "/") - // 预期格式/user/repo/more... - // 取出user和repo和最后部分 - parts := strings.Split(remainingPath, "/") - if len(parts) <= 2 { - errMsg := "Not enough parts in path after matching 'https://github.com*'" - return "", "", "", NewErrorWithStatusLookup(400, errMsg) - } - user = parts[0] - repo = parts[1] - // 匹配 "https://github.com"开头的链接 - if len(parts) >= 3 { - switch parts[2] { - case "releases", "archive": - matcher = "releases" - case "blob": - matcher = "blob" - case "raw": - matcher = "raw" - case "info", "git-upload-pack": - matcher = "clone" - default: - errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher" - return "", "", "", NewErrorWithStatusLookup(400, errMsg) - } - } - return user, repo, matcher, nil + if len(rawPath) < 18 { + return "", "", "", NewErrorWithStatusLookup(404, "path too short") } - // 匹配 "https://raw"开头的链接 - if strings.HasPrefix(rawPath, "https://raw") { - remainingPath := strings.TrimPrefix(rawPath, "https://") - parts := strings.Split(remainingPath, "/") - if len(parts) <= 3 { - errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)." - return "", "", "", NewErrorWithStatusLookup(400, errMsg) - } - 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 { - errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)." - return "", "", "", NewErrorWithStatusLookup(400, errMsg) + // 匹配 "https://github.com/" + if strings.HasPrefix(rawPath, "https://github.com/") { + remaining := rawPath[19:] + i := strings.IndexByte(remaining, '/') + if i <= 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user") + } + user := remaining[:i] + remaining = remaining[i+1:] + i = strings.IndexByte(remaining, '/') + if i <= 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing repo") + } + repo := remaining[:i] + remaining = remaining[i+1:] + if len(remaining) == 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action") + } + i = strings.IndexByte(remaining, '/') + action := remaining + if i != -1 { + action = remaining[:i] + } + var matcher string + switch action { + case "releases", "archive": + matcher = "releases" + case "blob": + matcher = "blob" + case "raw": + matcher = "raw" + case "info", "git-upload-pack": + matcher = "clone" + default: + return "", "", "", NewErrorWithStatusLookup(400, fmt.Sprintf("unsupported github action: %s", action)) } - user = parts[1] - repo = "" - matcher = "gist" return user, repo, matcher, nil } - // 匹配 "https://api.github.com/"开头的链接 + + // 匹配 "https://raw.githubusercontent.com/" + if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com/") { + remaining := rawPath[34:] + // 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file... + // 我们只需要 user 和 repo + i := strings.IndexByte(remaining, '/') + if i <= 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing user") + } + user := remaining[:i] + remaining = remaining[i+1:] + i = strings.IndexByte(remaining, '/') + if i <= 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing repo") + } + repo := remaining[:i] + // raw 链接至少需要 user/repo/branch 三部分 + remaining = remaining[i+1:] + if len(remaining) == 0 { + return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing branch/commit") + } + return user, repo, "raw", nil + } + + // 匹配 "https://gist.github.com/" + if strings.HasPrefix(rawPath, "https://gist.github.com/") { + remaining := rawPath[24:] + i := strings.IndexByte(remaining, '/') + if i <= 0 { + // case: https://gist.github.com/user + // 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的 + if len(remaining) > 0 { + return remaining, "", "gist", nil + } + return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user") + } + // case: https://gist.github.com/user/gist_id... + user := remaining[:i] + return user, "", "gist", 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 !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) { + return "", "", "", NewErrorWithStatusLookup(403, "API proxy requires header authentication") } - if parts[0] == "users" { - user = parts[1] - } - if !cfg.Auth.ForceAllowApi { - if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { - //return "", "", "", ErrAuthHeaderUnavailable - errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy" - return "", "", "", NewErrorWithStatusLookup(403, errMsg) + remaining := rawPath[23:] + var user, repo string + if strings.HasPrefix(remaining, "repos/") { + parts := strings.SplitN(remaining[6:], "/", 3) + if len(parts) >= 2 { + user = parts[0] + repo = parts[1] + } + } else if strings.HasPrefix(remaining, "users/") { + parts := strings.SplitN(remaining[6:], "/", 2) + if len(parts) >= 1 { + user = parts[0] } } - return user, repo, matcher, nil + return user, repo, "api", nil } - //return "", "", "", ErrNotFound - errMsg := "Didn't match any matcher" - return "", "", "", NewErrorWithStatusLookup(404, errMsg) + + return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path") } -var ( - matchedMatchers = []string{ - "blob", - "raw", - "gist", +/* + func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { + 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, "/") + //} + + remainingPath = strings.TrimPrefix(remainingPath, "/") + // 预期格式/user/repo/more... + // 取出user和repo和最后部分 + parts := strings.Split(remainingPath, "/") + if len(parts) <= 2 { + errMsg := "Not enough parts in path after matching 'https://github.com*'" + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + user = parts[0] + repo = parts[1] + // 匹配 "https://github.com"开头的链接 + if len(parts) >= 3 { + switch parts[2] { + case "releases", "archive": + matcher = "releases" + case "blob": + matcher = "blob" + case "raw": + matcher = "raw" + case "info", "git-upload-pack": + matcher = "clone" + default: + errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher" + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + } + 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 { + errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)." + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + 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 { + errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)." + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + 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.Method != "header" || !cfg.Auth.Enabled { + //return "", "", "", ErrAuthHeaderUnavailable + errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy" + return "", "", "", NewErrorWithStatusLookup(403, errMsg) + } + } + return user, repo, matcher, nil + } + //return "", "", "", ErrNotFound + errMsg := "Didn't match any matcher" + return "", "", "", NewErrorWithStatusLookup(404, errMsg) } +*/ +var ( + proxyableMatchersMap map[string]struct{} + initMatchersOnce sync.Once ) -// 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] +func initMatchers() { + initMatchersOnce.Do(func() { + matchers := []string{"blob", "raw", "gist"} + proxyableMatchersMap = make(map[string]struct{}, len(matchers)) + for _, m := range matchers { + proxyableMatchersMap[m] = struct{}{} + } + }) +} + +// matchString 与原始版本签名兼容 +func matchString(target string) bool { + initMatchers() + _, exists := proxyableMatchersMap[target] return exists } -// extractParts 从给定的 URL 中提取所需的部分 +// extractParts 与原始版本签名兼容 func extractParts(rawURL string) (string, string, string, url.Values, error) { - // 解析 URL parsedURL, err := url.Parse(rawURL) if err != nil { return "", "", "", nil, err } - // 获取路径部分并分割 - pathParts := strings.Split(parsedURL.Path, "/") + path := parsedURL.Path + if len(path) > 0 && path[0] == '/' { + path = path[1:] + } - // 提取所需的部分 - if len(pathParts) < 3 { + parts := strings.SplitN(path, "/", 3) + + if len(parts) < 2 { return "", "", "", nil, fmt.Errorf("URL path is too short") } - // 提取 /WJQSERVER-STUDIO 和 /go-utils.git - repoOwner := "/" + pathParts[1] - repoName := "/" + pathParts[2] - - // 剩余部分 - remainingPath := strings.Join(pathParts[3:], "/") - if remainingPath != "" { - remainingPath = "/" + remainingPath + repoOwner := "/" + parts[0] + repoName := "/" + parts[1] + var remainingPath string + if len(parts) > 2 { + remainingPath = "/" + parts[2] } - // 查询参数 - queryParams := parsedURL.Query() - - return repoOwner, repoName, remainingPath, queryParams, nil + return repoOwner, repoName, remainingPath, parsedURL.Query(), nil } var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`) diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go new file mode 100644 index 0000000..21329c3 --- /dev/null +++ b/proxy/matcher_test.go @@ -0,0 +1,303 @@ +package proxy + +import ( + "ghproxy/config" + "net/url" + "reflect" + "testing" +) + +func TestMatcher_Compatibility(t *testing.T) { + // --- 准备各种配置用于测试 --- + cfgWithAuth := &config.Config{ + Auth: config.AuthConfig{Enabled: true, Method: "header", ForceAllowApi: false}, + } + cfgNoAuth := &config.Config{ + Auth: config.AuthConfig{Enabled: false}, + } + cfgApiForceAllowed := &config.Config{ + Auth: config.AuthConfig{ForceAllowApi: true}, + } + cfgWrongAuthMethod := &config.Config{ + Auth: config.AuthConfig{Enabled: true, Method: "none"}, + } + + testCases := []struct { + name string + rawPath string + config *config.Config + expectedUser string + expectedRepo string + expectedMatcher string + expectError bool + expectedErrCode int + }{ + { + name: "GH Releases Path", + rawPath: "https://github.com/owner/repo/releases/download/v1.0/asset.zip", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases", + }, + { + name: "GH Archive Path", + rawPath: "https://github.com/owner/repo.git/archive/main.zip", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "releases", + }, + { + name: "GH Blob Path", + rawPath: "https://github.com/owner/repo/blob/main/path/to/file.go", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "blob", + }, + { + name: "GH Raw Path", + rawPath: "https://github.com/owner/repo/raw/main/image.png", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw", + }, + { + name: "GH Clone Info Refs", + rawPath: "https://github.com/owner/repo.git/info/refs?service=git-upload-pack", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "clone", + }, + { + name: "GH Clone Git Upload Pack", + rawPath: "https://github.com/owner/repo/git-upload-pack", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "clone", + }, + + { + name: "RawGHUserContent Path", + rawPath: "https://raw.githubusercontent.com/owner/repo/branch/file.sh", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw", + }, + { + name: "Gist Path", + rawPath: "https://gist.github.com/user/abcdef1234567890", + config: cfgWithAuth, + expectedUser: "user", expectedRepo: "", expectedMatcher: "gist", + }, + { + name: "API Repos Path (with Auth)", + rawPath: "https://api.github.com/repos/owner/repo/pulls", + config: cfgWithAuth, + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api", + }, + { + name: "API Users Path (with Auth)", + rawPath: "https://api.github.com/users/someuser/repos", + config: cfgWithAuth, + expectedUser: "someuser", expectedRepo: "", expectedMatcher: "api", + }, + { + name: "API Other Path (with Auth)", + rawPath: "https://api.github.com/octocat", + config: cfgWithAuth, + expectedUser: "", expectedRepo: "", expectedMatcher: "api", + }, + { + name: "API Path (Force Allowed)", + rawPath: "https://api.github.com/repos/owner/repo", + config: cfgApiForceAllowed, // Auth disabled, but force allowed + expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api", + }, + { + name: "Malformed GH Path (no repo)", + rawPath: "https://github.com/owner/", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Malformed GH Path (no action)", + rawPath: "https://github.com/owner/repo", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Malformed GH Path (empty user)", + rawPath: "https://github.com//repo/blob/main/file.go", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Malformed Raw Path (no repo)", + rawPath: "https://raw.githubusercontent.com/owner/", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Malformed Gist Path (no user)", + rawPath: "https://gist.github.com/", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "Unsupported GH Action", + rawPath: "https://github.com/owner/repo/issues/123", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, + { + name: "API Path (No Auth)", + rawPath: "https://api.github.com/user", + config: cfgNoAuth, + expectError: true, expectedErrCode: 403, + }, + { + name: "API Path (Wrong Auth Method)", + rawPath: "https://api.github.com/user", + config: cfgWrongAuthMethod, + expectError: true, expectedErrCode: 403, + }, + { + name: "No Matcher Found (other domain)", + rawPath: "https://bitbucket.org/owner/repo", + config: cfgWithAuth, + expectError: true, expectedErrCode: 404, + }, + { + name: "No Matcher Found (path too short)", + rawPath: "https://a.co", + config: cfgWithAuth, + expectError: true, expectedErrCode: 404, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + user, repo, matcher, ghErr := Matcher(tc.rawPath, tc.config) + + if tc.expectError { + if ghErr == nil { + t.Fatalf("Expected a GHProxyErrors error, but got nil") + } + if ghErr.StatusCode != tc.expectedErrCode { + t.Errorf("Expected error code %d, but got %d (msg: %s)", + tc.expectedErrCode, ghErr.StatusCode, ghErr.ErrorMessage) + } + } else { + if ghErr != nil { + t.Fatalf("Expected no error, but got: %s", ghErr.ErrorMessage) + } + if user != tc.expectedUser { + t.Errorf("user: got %q, want %q", user, tc.expectedUser) + } + if repo != tc.expectedRepo { + t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo) + } + if matcher != tc.expectedMatcher { + t.Errorf("matcher: got %q, want %q", matcher, tc.expectedMatcher) + } + } + }) + } +} + +func TestExtractParts_Compatibility(t *testing.T) { + testCases := []struct { + name string + rawURL string + expectedOwner string + expectedRepo string + expectedRem string + expectedQuery url.Values + expectError bool + }{ + { + name: "Standard git clone URL", + rawURL: "https://github.com/WJQSERVER-STUDIO/go-utils.git/info/refs?service=git-upload-pack", + expectedOwner: "/WJQSERVER-STUDIO", + expectedRepo: "/go-utils.git", + expectedRem: "/info/refs", + expectedQuery: url.Values{"service": []string{"git-upload-pack"}}, + }, + { + name: "No remaining path", + rawURL: "https://example.com/owner/repo", + expectedOwner: "/owner", + expectedRepo: "/repo", + expectedRem: "", + expectedQuery: url.Values{}, + }, + { + name: "Root path only", + rawURL: "https://example.com/", + expectError: true, // Path is too short + }, + { + name: "One level path", + rawURL: "https://example.com/owner", + expectError: true, // Path is too short + }, + { + name: "Empty path segments", + rawURL: "https://example.com//repo/a", // Will be treated as /repo/a + expectedOwner: "", // First part is empty + expectedRepo: "/repo", + expectedRem: "/a", + }, + { + name: "Invalid URL format", + rawURL: "://invalid", + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + owner, repo, rem, query, err := extractParts(tc.rawURL) + + if (err != nil) != tc.expectError { + t.Fatalf("extractParts() error = %v, expectError %v", err, tc.expectError) + } + + if !tc.expectError { + if owner != tc.expectedOwner { + t.Errorf("owner: got %q, want %q", owner, tc.expectedOwner) + } + if repo != tc.expectedRepo { + t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo) + } + if rem != tc.expectedRem { + t.Errorf("remaining path: got %q, want %q", rem, tc.expectedRem) + } + if !reflect.DeepEqual(query, tc.expectedQuery) { + t.Errorf("query: got %v, want %v", query, tc.expectedQuery) + } + } + }) + } +} + +func TestMatchString_Compatibility(t *testing.T) { + testCases := []struct { + target string + expected bool + }{ + {"blob", true}, {"raw", true}, {"gist", true}, + {"clone", false}, {"releases", false}, + } + for _, tc := range testCases { + t.Run(tc.target, func(t *testing.T) { + if got := matchString(tc.target); got != tc.expected { + t.Errorf("matchString('%s') = %v; want %v", tc.target, got, tc.expected) + } + }) + } +} + +func BenchmarkMatcher(b *testing.B) { + cfg := &config.Config{} + path := "https://github.com/WJQSERVER/speedtest-ex/releases/download/v1.2.0/speedtest-linux-amd64.tar.gz" + + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, _, _, _ = Matcher(path, cfg) + } +} From 8d5b764ec742808456ea377e7075095cc6d961db Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:30:39 +0800 Subject: [PATCH 106/118] optimize matcher --- proxy/match.go | 201 +++++++++++++++++++++++------------------- proxy/matcher_test.go | 6 ++ 2 files changed, 117 insertions(+), 90 deletions(-) diff --git a/proxy/match.go b/proxy/match.go index a28520e..896a99b 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -9,6 +9,25 @@ import ( "sync" ) +var ( + githubPrefix = "https://github.com/" + rawPrefix = "https://raw.githubusercontent.com/" + gistPrefix = "https://gist.github.com/" + apiPrefix = "https://api.github.com/" + githubPrefixLen int + rawPrefixLen int + gistPrefixLen int + apiPrefixLen int +) + +func init() { + githubPrefixLen = len(githubPrefix) + rawPrefixLen = len(rawPrefix) + gistPrefixLen = len(gistPrefix) + apiPrefixLen = len(apiPrefix) + //log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen) +} + // Matcher 从原始URL路径中高效地解析并匹配代理规则. func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { if len(rawPath) < 18 { @@ -16,8 +35,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } // 匹配 "https://github.com/" - if strings.HasPrefix(rawPath, "https://github.com/") { - remaining := rawPath[19:] + if strings.HasPrefix(rawPath, githubPrefix) { + remaining := rawPath[githubPrefixLen:] i := strings.IndexByte(remaining, '/') if i <= 0 { return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user") @@ -55,8 +74,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } // 匹配 "https://raw.githubusercontent.com/" - if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com/") { - remaining := rawPath[34:] + if strings.HasPrefix(rawPath, rawPrefix) { + remaining := rawPath[rawPrefixLen:] // 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file... // 我们只需要 user 和 repo i := strings.IndexByte(remaining, '/') @@ -79,8 +98,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } // 匹配 "https://gist.github.com/" - if strings.HasPrefix(rawPath, "https://gist.github.com/") { - remaining := rawPath[24:] + if strings.HasPrefix(rawPath, gistPrefix) { + remaining := rawPath[gistPrefixLen:] i := strings.IndexByte(remaining, '/') if i <= 0 { // case: https://gist.github.com/user @@ -96,11 +115,11 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro } // 匹配 "https://api.github.com/" - if strings.HasPrefix(rawPath, "https://api.github.com/") { + if strings.HasPrefix(rawPath, apiPrefix) { if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) { return "", "", "", NewErrorWithStatusLookup(403, "API proxy requires header authentication") } - remaining := rawPath[23:] + remaining := rawPath[apiPrefixLen:] var user, repo string if strings.HasPrefix(remaining, "repos/") { parts := strings.SplitN(remaining[6:], "/", 3) @@ -120,103 +139,105 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path") } +// 原实现 /* - func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { - var ( - user string - repo string - matcher string - ) - // 匹配 "https://github.com"开头的链接 - if strings.HasPrefix(rawPath, "https://github.com") { - remainingPath := strings.TrimPrefix(rawPath, "https://github.com") +func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) { + 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, "/") - //} + //if strings.HasPrefix(remainingPath, "/") { + // remainingPath = strings.TrimPrefix(remainingPath, "/") + //} - remainingPath = strings.TrimPrefix(remainingPath, "/") - // 预期格式/user/repo/more... - // 取出user和repo和最后部分 - parts := strings.Split(remainingPath, "/") - if len(parts) <= 2 { - errMsg := "Not enough parts in path after matching 'https://github.com*'" - return "", "", "", NewErrorWithStatusLookup(400, errMsg) - } - user = parts[0] - repo = parts[1] - // 匹配 "https://github.com"开头的链接 - if len(parts) >= 3 { - switch parts[2] { - case "releases", "archive": - matcher = "releases" - case "blob": - matcher = "blob" - case "raw": - matcher = "raw" - case "info", "git-upload-pack": - matcher = "clone" - default: - errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher" - return "", "", "", NewErrorWithStatusLookup(400, errMsg) - } - } - return user, repo, matcher, nil + remainingPath = strings.TrimPrefix(remainingPath, "/") + // 预期格式/user/repo/more... + // 取出user和repo和最后部分 + parts := strings.Split(remainingPath, "/") + if len(parts) <= 2 { + errMsg := "Not enough parts in path after matching 'https://github.com*'" + return "", "", "", NewErrorWithStatusLookup(400, errMsg) } - // 匹配 "https://raw"开头的链接 - if strings.HasPrefix(rawPath, "https://raw") { - remainingPath := strings.TrimPrefix(rawPath, "https://") - parts := strings.Split(remainingPath, "/") - if len(parts) <= 3 { - errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)." + user = parts[0] + repo = parts[1] + // 匹配 "https://github.com"开头的链接 + if len(parts) >= 3 { + switch parts[2] { + case "releases", "archive": + matcher = "releases" + case "blob": + matcher = "blob" + case "raw": + matcher = "raw" + case "info", "git-upload-pack": + matcher = "clone" + default: + errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher" return "", "", "", NewErrorWithStatusLookup(400, errMsg) } + } + 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 { + errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)." + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + 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 { + errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)." + return "", "", "", NewErrorWithStatusLookup(400, errMsg) + } + 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] - 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 { - errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)." - return "", "", "", NewErrorWithStatusLookup(400, errMsg) - } + if parts[0] == "users" { 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 !cfg.Auth.ForceAllowApi { + if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { + //return "", "", "", ErrAuthHeaderUnavailable + errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy" + return "", "", "", NewErrorWithStatusLookup(403, errMsg) } - if parts[0] == "users" { - user = parts[1] - } - if !cfg.Auth.ForceAllowApi { - if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { - //return "", "", "", ErrAuthHeaderUnavailable - errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy" - return "", "", "", NewErrorWithStatusLookup(403, errMsg) - } - } - return user, repo, matcher, nil } - //return "", "", "", ErrNotFound - errMsg := "Didn't match any matcher" - return "", "", "", NewErrorWithStatusLookup(404, errMsg) + return user, repo, matcher, nil } + //return "", "", "", ErrNotFound + errMsg := "Didn't match any matcher" + return "", "", "", NewErrorWithStatusLookup(404, errMsg) +} */ + var ( proxyableMatchersMap map[string]struct{} initMatchersOnce sync.Once diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go index 21329c3..3293817 100644 --- a/proxy/matcher_test.go +++ b/proxy/matcher_test.go @@ -68,6 +68,12 @@ func TestMatcher_Compatibility(t *testing.T) { config: cfgWithAuth, expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "clone", }, + { + name: "Girhub Broken Path", + rawPath: "https://github.com/owner", + config: cfgWithAuth, + expectError: true, expectedErrCode: 400, + }, { name: "RawGHUserContent Path", From 5731418822fd37e3e6c81da6992797a2868b790b Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Mon, 9 Jun 2025 23:31:37 +0800 Subject: [PATCH 107/118] 3.5.1 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df6a10..a36f5a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.1 - 2025-06-09 +--- +- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` + 25w45a - 2025-06-09 --- - PRE-RELEASE: 此版本是v3.5.1预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index e5b8203..3c8ff8c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.0 \ No newline at end of file +3.5.1 \ No newline at end of file From 5b05588375dcd169e149b8840bce6ba79ad3be22 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Tue, 10 Jun 2025 22:11:52 +0800 Subject: [PATCH 108/118] update deps && LICENSE --- LICENSE | 516 +++++++++++++++++++++++++++++++++----------------- LICENSE-WSL | 199 +++++++++++++++++++ README.md | 4 +- go.mod | 2 +- go.sum | 4 + proxy/nest.go | 4 - 6 files changed, 552 insertions(+), 177 deletions(-) create mode 100644 LICENSE-WSL diff --git a/LICENSE b/LICENSE index fbdd300..dbdb0fa 100644 --- a/LICENSE +++ b/LICENSE @@ -1,199 +1,373 @@ -WJQserver Studio 开源许可证 -版本 v2.1 +Mozilla Public License Version 2.0 +================================== -版权所有 © WJQserver Studio 2024 +1. Definitions +-------------- -定义 +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. -* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。 -* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。 -* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。 -* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。 -* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。 -* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于: - * 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。 - * 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。 - * 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。 - * 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。 - * 内部运营用途 (非营利组织): 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。 +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. -开源与自由软件 +1.3. "Contribution" + means Covered Software of a particular Contributor. -本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。 -本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。 -强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。 +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. -许可证条款 +1.5. "Incompatible With Secondary Licenses" + means -1. 使用权限 + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or -* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。 + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. -* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款: +1.6. "Executable Form" + means any form of the work other than Source Code Form. - * 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. - * 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。 - * 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。 - * 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。 - * 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。 +1.8. "License" + means this document. - 您必须选择以下两种方式之一: +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. - * i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。 - * ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。 +1.10. "Modifications" + means any of the following: -* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or -2. 复制与分发 + (b) any new file in Source Code Form that contains any Covered + Software. -* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件: +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. - * 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。 - * 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。 +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. -* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。 +1.13. "Source Code Form" + means the form of the work preferred for making modifications. -3. 修改权限 +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. -* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。 +2. License Grants and Conditions +-------------------------------- -* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 +2.1. Grants -* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。 +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: -4. 专利权 +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and -* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。 +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. -* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。 +2.2. Effective Date -5. 免责声明 +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. -* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。 +2.3. Limitations on Grant Scope -* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。 - -* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。 - -6. 许可证期限与终止 - -* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。 - -* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。 - -* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。 - -7. 条款修订 - -* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。 - -* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。 - -8. 其他 - -* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。 - -* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。 - -* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。 - -WJQserver Studio Open Source License -Version v2.0 - -Copyright © WJQserver Studio 2024 - -Definitions - -* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license. -* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license. -* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License. -* You: Refers to the individual or legal entity exercising permissions granted by this License. -* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof. -* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to: - * Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes. - * Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange. - * Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development. - * Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit. - * Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities. - -Open Source and Free Software - -This project is open-source software, allowing users to access and use the source code under the premise of complying with this License. -This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation. -Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors. - -License Terms - -1. Permissions for Use - -* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section. - -* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms: - - * 1.2.1 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: - - * Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works. - * Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services. - * Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale. - * Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features. - - You must choose one of the following two options: - - * i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community. - * ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation. - -* 1.3 Maintain Statements: When publish services to public, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. - -2. Reproduction and Distribution - -* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met: - - * Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices. - * Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License. - -* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing). - -3. Modification Permissions - -* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use. - -* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. - -* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License. - -4. Patent Rights - -* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves. - -* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom. - -5. Disclaimer of Warranty - -* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. - -* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. - -* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations. - -6. License Term and Termination - -* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance. - -* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License. - -* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License. - -7. Revision of Terms - -* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs. - -* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License. - -8. Other - -* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws. - -* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect. - -* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version. +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/LICENSE-WSL b/LICENSE-WSL new file mode 100644 index 0000000..fbdd300 --- /dev/null +++ b/LICENSE-WSL @@ -0,0 +1,199 @@ +WJQserver Studio 开源许可证 +版本 v2.1 + +版权所有 © WJQserver Studio 2024 + +定义 + +* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。 +* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。 +* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。 +* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。 +* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。 +* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于: + * 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。 + * 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。 + * 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。 + * 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。 + * 内部运营用途 (非营利组织): 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。 + +开源与自由软件 + +本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。 +本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。 +强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。 + +许可证条款 + +1. 使用权限 + +* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。 + +* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款: + + * 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于: + + * 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。 + * 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。 + * 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。 + * 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。 + + 您必须选择以下两种方式之一: + + * i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。 + * ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。 + +* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。 + +2. 复制与分发 + +* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件: + + * 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。 + * 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。 + +* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。 + +3. 修改权限 + +* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。 + +* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。 + +* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。 + +4. 专利权 + +* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。 + +* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。 + +5. 免责声明 + +* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。 + +* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。 + +* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。 + +6. 许可证期限与终止 + +* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。 + +* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。 + +* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。 + +7. 条款修订 + +* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。 + +* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。 + +8. 其他 + +* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。 + +* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。 + +* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。 + +WJQserver Studio Open Source License +Version v2.0 + +Copyright © WJQserver Studio 2024 + +Definitions + +* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license. +* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license. +* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License. +* You: Refers to the individual or legal entity exercising permissions granted by this License. +* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof. +* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to: + * Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes. + * Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange. + * Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development. + * Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit. + * Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities. + +Open Source and Free Software + +This project is open-source software, allowing users to access and use the source code under the premise of complying with this License. +This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation. +Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors. + +License Terms + +1. Permissions for Use + +* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section. + +* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms: + + * 1.2.1 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to: + + * Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works. + * Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services. + * Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale. + * Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features. + + You must choose one of the following two options: + + * i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community. + * ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation. + +* 1.3 Maintain Statements: When publish services to public, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software. + +2. Reproduction and Distribution + +* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met: + + * Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices. + * Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License. + +* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing). + +3. Modification Permissions + +* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use. + +* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.3 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization. + +* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License. + +4. Patent Rights + +* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves. + +* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom. + +5. Disclaimer of Warranty + +* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. + +* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. + +* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations. + +6. License Term and Termination + +* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance. + +* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License. + +* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License. + +7. Revision of Terms + +* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs. + +* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License. + +8. Other + +* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws. + +* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect. + +* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version. diff --git a/README.md b/README.md index b0d49f9..c388d19 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,9 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox ## LICENSE -本项目使用WJQserver Studio License 2.0 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) +v3.5.2开始, 本项目使用 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) 和 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 双重许可, 您可从中选择一个使用 + +前端位于单独仓库中, 且各个主题均存在各自的许可证, 本项目许可证并不包括前端 在v2.3.0之前, 本项目使用WJQserver Studio License 1.2 diff --git a/go.mod b/go.mod index 1ccfe73..1941ecd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 - github.com/WJQSERVER-STUDIO/httpc v0.5.1 + github.com/WJQSERVER-STUDIO/httpc v0.7.0 github.com/WJQSERVER-STUDIO/logger v1.7.3 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 diff --git a/go.sum b/go.sum index 4881472..368b49a 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,10 @@ github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSj github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.6.0 h1:GXKc6BNGn5ALdDLkCsM+mP24jyi1S9QBLL2XQ1a2sPM= +github.com/WJQSERVER-STUDIO/httpc v0.6.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= +github.com/WJQSERVER-STUDIO/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc= +github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/WJQSERVER-STUDIO/logger v1.7.3 h1:XoFJ1nBcZKyMvP4v0MZv5jL2q7IkAF7yfXgwyB3MLP4= github.com/WJQSERVER-STUDIO/logger v1.7.3/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= diff --git a/proxy/nest.go b/proxy/nest.go index 84f55cd..2748ca6 100644 --- a/proxy/nest.go +++ b/proxy/nest.go @@ -1,7 +1,3 @@ -// Copyright 2025 WJQSERVER, WJQSERVER-STUDIO. All rights reserved. -// 使用本源代码受 WSL 2.0(WJQserver Studio License v2.0)与MPL 2.0(Mozilla Public License v2.0)许可协议的约束 -// 此段代码使用双重授权许可, 允许用户选择其中一种许可证 - package proxy import ( From 44673b9a3feb10a4937ca1771e6dcb70cd4a3cde Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:39:10 +0800 Subject: [PATCH 109/118] 3.5.2 --- LICENSE-WSL | 2 +- VERSION | 2 +- api/api.go | 4 ++++ main.go | 11 +++++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/LICENSE-WSL b/LICENSE-WSL index fbdd300..e3f452b 100644 --- a/LICENSE-WSL +++ b/LICENSE-WSL @@ -99,7 +99,7 @@ WJQserver Studio 开源许可证 * 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。 WJQserver Studio Open Source License -Version v2.0 +Version v2.1 Copyright © WJQserver Studio 2024 diff --git a/VERSION b/VERSION index 3c8ff8c..80d13b7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.1 \ No newline at end of file +3.5.2 \ No newline at end of file diff --git a/api/api.go b/api/api.go index e8ca10f..597a198 100644 --- a/api/api.go +++ b/api/api.go @@ -92,6 +92,8 @@ func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Status": "OK", + "Repo": "WJQSERVER-STUDIO/GHProxy", + "Author": "WJQSERVER-STUDIO", })) } @@ -99,6 +101,8 @@ func VersionHandler(c *app.RequestContext, ctx context.Context, version string) c.Response.Header.Set("Content-Type", "application/json") c.JSON(200, (map[string]interface{}{ "Version": version, + "Repo": "WJQSERVER-STUDIO/GHProxy", + "Author": "WJQSERVER-STUDIO", })) } diff --git a/main.go b/main.go index a441752..e2e898b 100644 --- a/main.go +++ b/main.go @@ -402,6 +402,16 @@ func init() { } } +var viaString string = "WJQSERVER-STUDIO/GHProxy" + +func viaHeader() app.HandlerFunc { + return func(ctx context.Context, c *app.RequestContext) { + protoVersion := "1.1" + c.Header("Via", protoVersion+" "+viaString) + c.Next(ctx) + } +} + func main() { if showVersion || showHelp { return @@ -450,6 +460,7 @@ func main() { r.Use(recovery.Recovery()) // Recovery中间件 r.Use(loggin.Middleware()) // log中间件 + r.Use(viaHeader()) setupApi(cfg, r, version) setupPages(cfg, r) From b86e58cddfbab15be1ae8e436cabad5fe1b77217 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:40:00 +0800 Subject: [PATCH 110/118] 3.5.2 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a36f5a3..fc0d51c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.2 - 2025-06-11 +--- +- CHANGE: 加入MPL 2.0许可证, 项目转为双重许可 + 3.5.1 - 2025-06-09 --- - CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op` From a5bf7686bd8d3dc31f0893a03d2510959acdb770 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Fri, 13 Jun 2025 15:23:46 +0800 Subject: [PATCH 111/118] 3.5.3 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- go.mod | 2 +- go.sum | 18 ++---------------- main.go | 4 ++++ proxy/chunkreq.go | 2 ++ 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0d51c..7c4cabe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.3 - 2025-06-13 +--- +- CHANGE: 显式配置`WithStreamBody(true)` + 3.5.2 - 2025-06-11 --- - CHANGE: 加入MPL 2.0许可证, 项目转为双重许可 diff --git a/VERSION b/VERSION index 80d13b7..678fd88 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.2 \ No newline at end of file +3.5.3 \ No newline at end of file diff --git a/go.mod b/go.mod index 1941ecd..ba6d974 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.3 require ( github.com/BurntSushi/toml v1.5.0 github.com/WJQSERVER-STUDIO/httpc v0.7.0 - github.com/WJQSERVER-STUDIO/logger v1.7.3 + github.com/WJQSERVER-STUDIO/logger v1.8.0 github.com/cloudwego/hertz v0.10.0 github.com/hertz-contrib/http2 v0.1.8 golang.org/x/net v0.41.0 diff --git a/go.sum b/go.sum index 368b49a..d8d7e39 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,10 @@ github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSjSpQtVKqEDpiZ5Q= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE= -github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64= -github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/httpc v0.6.0 h1:GXKc6BNGn5ALdDLkCsM+mP24jyi1S9QBLL2XQ1a2sPM= -github.com/WJQSERVER-STUDIO/httpc v0.6.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= github.com/WJQSERVER-STUDIO/httpc v0.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc= github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE= -github.com/WJQSERVER-STUDIO/logger v1.7.3 h1:XoFJ1nBcZKyMvP4v0MZv5jL2q7IkAF7yfXgwyB3MLP4= -github.com/WJQSERVER-STUDIO/logger v1.7.3/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= +github.com/WJQSERVER-STUDIO/logger v1.8.0 h1:AQ3Qe2kxiqpuOoDlRzseGP6u4LAaJc+ng4l8P+CK7Co= +github.com/WJQSERVER-STUDIO/logger v1.8.0/go.mod h1:yzXPtot0OvR1gzx4+rlFrv/sccUpz0gIXVBwUx3H7fM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ= github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= @@ -91,16 +87,12 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/wjqserver/modembed v0.0.1 h1:8ZDz7t9M5DLrUFlYgBUUmrMzxWsZPmHvOazkr/T2jEs= github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g9yyCztTXOQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= -golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc= golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= -golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -112,8 +104,6 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -143,12 +133,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/main.go b/main.go index e2e898b..afc923a 100644 --- a/main.go +++ b/main.go @@ -430,12 +430,14 @@ func main() { server.WithH2C(true), server.WithHostPorts(addr), server.WithTransport(standard.NewTransporter), + server.WithStreamBody(true), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { r = server.New( server.WithHostPorts(addr), server.WithTransport(standard.NewTransporter), + server.WithStreamBody(true), ) } } else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" { @@ -444,12 +446,14 @@ func main() { server.WithH2C(true), server.WithHostPorts(addr), server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), + server.WithStreamBody(true), ) r.AddProtocol("h2", factory.NewServerFactory()) } else { r = server.New( server.WithHostPorts(addr), server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection), + server.WithStreamBody(true), ) } } else { diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index c50b7fb..6e4c8f4 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -110,6 +110,8 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } + defer bodyReader.Close() + if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip var compress string From d2a0177015a17a4f1ac1f4045499b79c29583f28 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 06:47:47 +0800 Subject: [PATCH 112/118] 25w46a --- CHANGELOG.md | 10 ++++++++++ DEV-VERSION | 2 +- README.md | 2 +- main.go | 2 +- proxy/chunkreq.go | 19 +++++++++++++------ proxy/gitreq.go | 11 ++++++----- 6 files changed, 32 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c4cabe..74eeb11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # 更新日志 +25w46b - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; +- CHANGE: 修改关闭行为以测试问题 + +25w46a - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; +- CHANGE: 修改payload行为以测试问题 + 3.5.3 - 2025-06-13 --- - CHANGE: 显式配置`WithStreamBody(true)` diff --git a/DEV-VERSION b/DEV-VERSION index ebe0357..09188a8 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w45a \ No newline at end of file +25w46a \ No newline at end of file diff --git a/README.md b/README.md index c388d19..3f4123d 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox ## LICENSE -v3.5.2开始, 本项目使用 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) 和 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 双重许可, 您可从中选择一个使用 +v3.5.2开始, 本项目使用 [WJQserver Studio License 2.1](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) 和 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 双重许可, 您可从中选择一个使用 前端位于单独仓库中, 且各个主题均存在各自的许可证, 本项目许可证并不包括前端 diff --git a/main.go b/main.go index afc923a..7d6fb0e 100644 --- a/main.go +++ b/main.go @@ -507,7 +507,7 @@ func main() { proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) }) - r.GET("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { + r.Any("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { c.Set("matcher", "api") proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c) }) diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 6e4c8f4..9ec1cc2 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -1,6 +1,7 @@ package proxy import ( + "bytes" "context" "fmt" "ghproxy/config" @@ -23,16 +24,17 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c go func() { <-ctx.Done() if resp != nil && resp.Body != nil { - resp.Body.Close() - } - if req != nil { - req.Body.Close() + err := resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } } }() rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb.NoDefaultHeaders() - rb.SetBody(c.Request.BodyStream()) + rb.SetBody(bytes.NewBuffer(c.Request.Body())) + //rb.SetBody(c.RequestBodyStream()) rb.WithContext(ctx) req, err = rb.Build() @@ -110,7 +112,12 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - defer bodyReader.Close() + defer func() { + err := bodyReader.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } + }() if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip diff --git a/proxy/gitreq.go b/proxy/gitreq.go index 5667d21..de6bff7 100644 --- a/proxy/gitreq.go +++ b/proxy/gitreq.go @@ -17,15 +17,16 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co var ( req *http.Request resp *http.Response + err error ) go func() { <-ctx.Done() if resp != nil && resp.Body != nil { - resp.Body.Close() - } - if req != nil { - req.Body.Close() + err = resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } } }() @@ -51,7 +52,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co rb.SetBody(reqBodyReader) rb.WithContext(ctx) - req, err := rb.Build() + req, err = rb.Build() if err != nil { HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) return From bd8412f15767cdd02a2ff22ebf983187a578c102 Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 06:51:41 +0800 Subject: [PATCH 113/118] 25w46b --- DEV-VERSION | 2 +- proxy/chunkreq.go | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/DEV-VERSION b/DEV-VERSION index 09188a8..99cdc41 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w46a \ No newline at end of file +25w46b \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 9ec1cc2..5fa4728 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -1,7 +1,6 @@ package proxy import ( - "bytes" "context" "fmt" "ghproxy/config" @@ -33,8 +32,8 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c rb := client.NewRequestBuilder(string(c.Request.Method()), u) rb.NoDefaultHeaders() - rb.SetBody(bytes.NewBuffer(c.Request.Body())) - //rb.SetBody(c.RequestBodyStream()) + //rb.SetBody(bytes.NewBuffer(c.Request.Body())) + rb.SetBody(c.RequestBodyStream()) rb.WithContext(ctx) req, err = rb.Build() @@ -112,12 +111,14 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - defer func() { - err := bodyReader.Close() - if err != nil { - logError("Failed to close response body: %v", err) - } - }() + /* + defer func() { + err := bodyReader.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } + }() + */ if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip From 41395b1d724dd66af11d409b7591c9c22ed1570c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 07:16:43 +0800 Subject: [PATCH 114/118] 25w46c --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- proxy/chunkreq.go | 23 ++++++++++++++--------- proxy/handler.go | 5 ++++- proxy/routing.go | 6 +++++- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74eeb11..5c34c60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w46c - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; +- CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑 + 25w46b - 2025-06-14 --- - PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; diff --git a/DEV-VERSION b/DEV-VERSION index 99cdc41..7b8b745 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w46b \ No newline at end of file +25w46c \ No newline at end of file diff --git a/proxy/chunkreq.go b/proxy/chunkreq.go index 5fa4728..35461c9 100644 --- a/proxy/chunkreq.go +++ b/proxy/chunkreq.go @@ -57,6 +57,20 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c return } + // 处理302情况 + if resp.StatusCode == 302 { + finalURL := resp.Header.Get("Location") + if finalURL != "" { + err = resp.Body.Close() + if err != nil { + logError("Failed to close response body: %v", err) + } + c.Request.Header.Del("Referer") + logInfo("Internal Redirecting to %s", finalURL) + ChunkedProxyRequest(ctx, c, finalURL, cfg, matcher) + } + } + var ( bodySize int contentLength string @@ -111,15 +125,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx) } - /* - defer func() { - err := bodyReader.Close() - if err != nil { - logError("Failed to close response body: %v", err) - } - }() - */ - if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor { // 判断body是不是gzip var compress string diff --git a/proxy/handler.go b/proxy/handler.go index bdd7ecb..80a5892 100644 --- a/proxy/handler.go +++ b/proxy/handler.go @@ -68,7 +68,10 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra // 处理blob/raw路径 if matcher == "blob" { - rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) + rawPath = rawPath[10:] + rawPath = "raw.githubusercontent.com" + rawPath + rawPath = strings.Replace(rawPath, "/blob/", "/", 1) + matcher = "raw" } logDebug("Matched: %v", matcher) diff --git a/proxy/routing.go b/proxy/routing.go index a3135ec..9d68ca7 100644 --- a/proxy/routing.go +++ b/proxy/routing.go @@ -48,9 +48,13 @@ func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra return } + // 处理blob/raw路径 // 处理blob/raw路径 if matcher == "blob" { - rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) + rawPath = rawPath[10:] + rawPath = "raw.githubusercontent.com" + rawPath + rawPath = strings.Replace(rawPath, "/blob/", "/", 1) + matcher = "raw" } // 为rawpath加入https:// 头 From bbb108689a728ec0b21fe9d35a42443d2254ed3c Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 12:50:26 +0800 Subject: [PATCH 115/118] 3.5.4 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c34c60..d323c99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.4 - 2025-06-14 +--- +- CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑 + 25w46c - 2025-06-14 --- - PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index 678fd88..e5b8a84 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.3 \ No newline at end of file +3.5.4 \ No newline at end of file From 8ab622d149d0f79a07ec2f045221fe718ba5d78f Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:05:45 +0800 Subject: [PATCH 116/118] update matcher for gist usercontent --- proxy/match.go | 36 ++++++++++++++++++++++++++++-------- proxy/matcher_test.go | 6 ++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/proxy/match.go b/proxy/match.go index 896a99b..f526461 100644 --- a/proxy/match.go +++ b/proxy/match.go @@ -10,14 +10,16 @@ import ( ) var ( - githubPrefix = "https://github.com/" - rawPrefix = "https://raw.githubusercontent.com/" - gistPrefix = "https://gist.github.com/" - apiPrefix = "https://api.github.com/" - githubPrefixLen int - rawPrefixLen int - gistPrefixLen int - apiPrefixLen int + githubPrefix = "https://github.com/" + rawPrefix = "https://raw.githubusercontent.com/" + gistPrefix = "https://gist.github.com/" + gistContentPrefix = "https://gist.githubusercontent.com/" + apiPrefix = "https://api.github.com/" + githubPrefixLen int + rawPrefixLen int + gistPrefixLen int + gistContentPrefixLen int + apiPrefixLen int ) func init() { @@ -25,6 +27,7 @@ func init() { rawPrefixLen = len(rawPrefix) gistPrefixLen = len(gistPrefix) apiPrefixLen = len(apiPrefix) + gistContentPrefixLen = len(gistContentPrefix) //log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen) } @@ -114,6 +117,23 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro return user, "", "gist", nil } + // 匹配 "https://gist.githubusercontent.com/" + if strings.HasPrefix(rawPath, gistContentPrefix) { + remaining := rawPath[gistContentPrefixLen:] + i := strings.IndexByte(remaining, '/') + if i <= 0 { + // case: https://gist.githubusercontent.com/user + // 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的 + if len(remaining) > 0 { + return remaining, "", "gist", nil + } + return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user") + } + // case: https://gist.githubusercontent.com/user/gist_id... + user := remaining[:i] + return user, "", "gist", nil + } + // 匹配 "https://api.github.com/" if strings.HasPrefix(rawPath, apiPrefix) { if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) { diff --git a/proxy/matcher_test.go b/proxy/matcher_test.go index 3293817..0c35381 100644 --- a/proxy/matcher_test.go +++ b/proxy/matcher_test.go @@ -87,6 +87,12 @@ func TestMatcher_Compatibility(t *testing.T) { config: cfgWithAuth, expectedUser: "user", expectedRepo: "", expectedMatcher: "gist", }, + { + name: "Gist UserContent Path", + rawPath: "https://gist.githubusercontent.com/user/abcdef1234567890", + config: cfgWithAuth, + expectedUser: "user", expectedRepo: "", expectedMatcher: "gist", + }, { name: "API Repos Path (with Auth)", rawPath: "https://api.github.com/repos/owner/repo/pulls", From 1b06260a14f5c057ad3393aacb006a093c46a2ff Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:11:31 +0800 Subject: [PATCH 117/118] 25w47a --- CHANGELOG.md | 5 +++++ DEV-VERSION | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d323c99..e0064c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 更新日志 +25w47a - 2025-06-14 +--- +- PRE-RELEASE: 此版本是v3.5.5预发布版本,请勿在生产环境中使用; +- CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖 + 3.5.4 - 2025-06-14 --- - CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑 diff --git a/DEV-VERSION b/DEV-VERSION index 7b8b745..55564ab 100644 --- a/DEV-VERSION +++ b/DEV-VERSION @@ -1 +1 @@ -25w46c \ No newline at end of file +25w47a \ No newline at end of file From e0cbfed1e7fecdd0788c44416dc220b888a1a1aa Mon Sep 17 00:00:00 2001 From: wjqserver <114663932+WJQSERVER@users.noreply.github.com> Date: Sat, 14 Jun 2025 22:17:13 +0800 Subject: [PATCH 118/118] 3.5.5 --- CHANGELOG.md | 4 ++++ VERSION | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0064c0..006230b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 更新日志 +3.5.5 - 2025-06-14 +--- +- CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖 + 25w47a - 2025-06-14 --- - PRE-RELEASE: 此版本是v3.5.5预发布版本,请勿在生产环境中使用; diff --git a/VERSION b/VERSION index e5b8a84..1947319 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.5.4 \ No newline at end of file +3.5.5 \ No newline at end of file