Compare commits

...

116 Commits

Author SHA1 Message Date
WJQSERVER
b29940df21 Merge pull request #118 from WJQSERVER-STUDIO/dev
3.5.0
2025-06-05 22:55:42 +08:00
wjqserver
185522133b 3.5.0 2025-06-05 22:55:24 +08:00
wjqserver
6be6e1ba2c 25w44a 2025-06-05 20:14:58 +08:00
WJQSERVER
1ba100c28d Merge pull request #116 from WJQSERVER-STUDIO/dev
3.4.3
2025-06-05 17:30:01 +08:00
wjqserver
1370617f5b 3.4.3 2025-06-05 17:29:18 +08:00
wjqserver
e829c2baff 25w43a 2025-06-05 00:24:21 +08:00
WJQSERVER
75d909ef16 Merge pull request #113 from WJQSERVER-STUDIO/dev
revert github.com/nyaruka/phonenumbers to v1.6.1
2025-06-03 16:48:03 +08:00
wjqserver
2bab0a9774 3.4.2 2025-06-03 16:47:00 +08:00
wjqserver
171fe61342 25w42a 2025-06-02 21:00:49 +08:00
WJQSERVER
362ad96fbe Merge pull request #111 from WJQSERVER-STUDIO/dev
update docs link
2025-05-30 17:32:00 +08:00
wjqserver
5b17e1f0b6 update docs link 2025-05-30 17:31:35 +08:00
WJQSERVER
e40e1aadee Merge 3.4.1 #110 2025-05-29 15:26:44 +08:00
wjqserver
82943428d3 optimize cache & remove panic change to fallback error json 2025-05-29 15:13:28 +08:00
wjqserver
b7ce929db8 3.4.1 2025-05-29 15:01:44 +08:00
wjqserver
68bf51aaed 25w41b 2025-05-28 21:35:52 +08:00
wjqserver
16b6b05fb8 25w41a 2025-05-28 20:17:04 +08:00
WJQSERVER
d2b2d823b8 3.4.0
Add support for multi-target docker image(oci) proxy (3.4.0)
Add Hub theme
2025-05-21 16:24:08 +08:00
wjqserver
4598257faa update readme.md 2025-05-21 16:07:07 +08:00
wjqserver
1afb352194 3.4.0 2025-05-21 15:24:05 +08:00
wjqserver
430e313d47 avoid nil *ptr & fix path 2025-05-21 12:08:17 +08:00
wjqserver
31d435bfa0 add oci proxy & nest shell api 2025-05-21 11:55:04 +08:00
wjqserver
6ff23f639e add hub theme & add more check for wcache close 2025-05-21 11:54:43 +08:00
wjqserver
c7954ae91a 25w40a 2025-05-21 09:03:14 +08:00
wjqserver
11099176bf add support for multi-target docker image(oci) proxy 2025-05-21 09:03:00 +08:00
WJQSERVER
f3eb92ea51 Merge pull request #107 from WJQSERVER-STUDIO/dev
3.3.3
2025-05-20 10:10:19 +08:00
wjqserver
5ddbf1d2a0 3.3.3 2025-05-20 10:05:55 +08:00
wjqserver
d38ca3969f revert route handle for 3.3.x 2025-05-20 10:03:48 +08:00
wjqserver
146b0d7748 update nest 2025-05-19 12:16:20 +08:00
wjqserver
d92424cb94 25w39a 2025-05-19 12:00:36 +08:00
WJQSERVER
0f437dc891 Merge pull request #106 from WJQSERVER-STUDIO/dev
3.3.2
2025-05-18 07:05:19 +08:00
wjqserver
816b35654a update readme.md 2025-05-18 06:20:55 +08:00
wjqserver
a4fae95526 3.3.2 2025-05-18 06:13:00 +08:00
wjqserver
ea0e4e9801 change the default theme to design 2025-05-18 06:11:44 +08:00
wjqserver
5facc36947 update docs 2025-05-18 06:09:04 +08:00
WJQSERVER
5c25bc012f Merge pull request #105 from WJQSERVER-STUDIO/dev
3.3.1
2025-05-16 19:54:20 +08:00
wjqserver
b2712f8184 3.3.1 2025-05-16 19:53:48 +08:00
wjqserver
566a0ea26a 25w37a 2025-05-16 19:28:08 +08:00
wjqserver
7d4aae1668 merge customTarget into target 2025-05-16 00:24:57 +08:00
wjqserver
052243b095 add customTarget 2025-05-16 00:15:04 +08:00
wjqserver
4ded2186d8 update deps 2025-05-15 18:50:36 +08:00
WJQSERVER
aa95daf8c0 Merge pull request #103 from WJQSERVER-STUDIO/dev
3.3.0
2025-05-15 18:46:29 +08:00
wjqserver
89b850c1ec 3.3.0 2025-05-15 18:45:52 +08:00
wjqserver
ce814875e1 25w36d 2025-05-14 17:55:37 +08:00
wjqserver
47c03763a7 25w36c 2025-05-14 01:34:05 +08:00
wjqserver
71bc2aaed7 add bandwidth limiter 2025-05-14 01:33:54 +08:00
wjqserver
3f8d16511e 25w36b 2025-05-13 19:04:21 +08:00
wjqserver
43469532d4 25w36a 2025-05-13 14:51:34 +08:00
WJQSERVER
e32479b287 Merge pull request #102 from WJQSERVER-STUDIO/dev
3.2.4
2025-05-13 14:24:32 +08:00
wjqserver
ef6e0a78cd 3.2.4 2025-05-13 14:22:19 +08:00
wjqserver
c2e2b661a4 25w35a 2025-05-12 17:12:58 +08:00
wjqserver
791f668758 remove unused matcher return(fix #101) 2025-05-12 17:06:06 +08:00
WJQSERVER
92c4c62b46 Merge pull request #100 from WJQSERVER-STUDIO/dev
3.2.3
2025-05-07 19:09:18 +08:00
wjqserver
545144c7b5 3.2.3 2025-05-07 19:08:12 +08:00
wjqserver
866638ba8e 25w34b 2025-05-07 18:50:58 +08:00
wjqserver
e3fd604945 update Go to go1.24.3 2025-05-07 18:49:20 +08:00
wjqserver
90709539f4 update deps 2025-05-06 14:23:19 +08:00
wjqserver
1011a25d16 25w34a 2025-05-05 15:17:39 +08:00
wjqserver
bd63ed3070 change to new logger(enabled async log) 2025-05-05 15:15:18 +08:00
wjqserver
3c11e9826e change touka-httpc to httpc(touka/toka) 2025-05-05 10:41:41 +08:00
WJQSERVER
ef3b1bf1f0 Merge pull request #99 from WJQSERVER-STUDIO/dev
3.2.2
2025-04-29 22:29:13 +08:00
wjqserver
ad4d8eb670 remove unuse code 2025-04-29 22:25:20 +08:00
wjqserver
030f0d12a9 update docs 2025-04-29 22:24:11 +08:00
wjqserver
e57432a01c 3.2.2 2025-04-29 22:21:08 +08:00
wjqserver
ace795fe9d revert gitreq body stream 2025-04-29 22:13:11 +08:00
wjqserver
3f51e5319a 25w33a 2025-04-29 20:39:41 +08:00
wjqserver
55769d9a40 use custom headers for raw 2025-04-29 19:51:23 +08:00
WJQSERVER
7eb312243c Merge pull request #97 from WJQSERVER-STUDIO/dev
3.2.1
2025-04-29 07:44:45 +08:00
wjqserver
6ca31bc252 3.2.1 2025-04-29 07:43:19 +08:00
wjqserver
dfc49ae28b 25w32a 2025-04-29 07:24:28 +08:00
wjqserver
a0cca13deb fix matcher key issue 2025-04-29 07:22:03 +08:00
里見 灯花
1498aaed14 Merge pull request #95 from WJQSERVER-STUDIO/dev
3.2.0
2025-04-27 17:41:32 +08:00
wjqserver
086aa999e1 3.2.0 2025-04-27 17:38:30 +08:00
wjqserver
bf92cc8429 add req body 2025-04-27 17:33:17 +08:00
wjqserver
d94f6c0f5d 25w31a 2025-04-27 16:39:47 +08:00
wjqserver
f540b2edcd fix user name match issue 2025-04-27 15:57:06 +08:00
wjqserver
8aef197fde 25w31t-2 2025-04-25 22:14:23 +08:00
wjqserver
52d6f8e759 update readme 2025-04-25 17:56:22 +08:00
wjqserver
a7be65a111 25w31t-1 2025-04-25 17:14:33 +08:00
WJQSERVER
9977eb1437 3.1.0
- 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匹配的返回码处理
2025-04-24 18:46:28 +08:00
wjqserver
47de48bcce 3.1.0 2025-04-24 18:27:15 +08:00
wjqserver
8ccf48a6fe fix && update 2025-04-24 18:11:13 +08:00
wjqserver
7a6544c6c9 25w30e 2025-04-24 17:50:18 +08:00
wjqserver
b955c915ff fix callback issue 2025-04-24 01:09:53 +08:00
wjqserver
e42ea358bb remove debug output 2025-04-22 20:58:44 +08:00
wjqserver
4936a93788 25w30d 2025-04-22 20:56:34 +08:00
wjqserver
493ac28b59 add html/tmpl for status err page 2025-04-22 20:56:27 +08:00
wjqserver
d79aeaaacd 25w30c 2025-04-21 18:52:45 +08:00
wjqserver
558d3fbb0b 25w30b 2025-04-21 17:27:38 +08:00
wjqserver
3d7559bd66 change context.Context to hertz *app.RequestContext 2025-04-21 13:57:52 +08:00
wjqserver
809032a970 change to c.Request.BodyStream() 2025-04-21 13:47:45 +08:00
wjqserver
2eb6a9810b 25w30a 2025-04-19 23:02:13 +08:00
wjqserver
26a5148c6f use gertz route for std url 2025-04-19 22:59:59 +08:00
WJQSERVER
c656aa41ca Merge pull request #93 from WJQSERVER-STUDIO/dev
3.0.3
2025-04-19 21:26:39 +08:00
wjqserver
0b052f9c7f add debug output 2025-04-19 21:23:31 +08:00
wjqserver
6fb7e1150e 25w29b 2025-04-19 21:14:09 +08:00
wjqserver
5e0f95dae3 3.0.3 2025-04-19 20:44:43 +08:00
wjqserver
c1c39a5a1f remove unused bufferpool 2025-04-17 22:30:00 +08:00
wjqserver
dd2f5b5a12 25w29a 2025-04-17 22:20:06 +08:00
wjqserver
7e5b12dff8 Fix: Optimize header forwarding by excluding headers in a single pass 2025-04-16 15:50:04 +08:00
wjqserver
26a42b6510 add pprof for debug 2025-04-16 15:47:46 +08:00
wjqserver
254c9a8bad 25w29t-1 2025-04-15 15:05:36 +08:00
WJQSERVER
060453f070 Merge pull request #88 from WJQSERVER-STUDIO/dev
3.0.2
2025-04-15 13:26:46 +08:00
wjqserver
f110c96c1f update readme 2025-04-15 13:22:51 +08:00
wjqserver
73aac79c1b 3.0.2 2025-04-15 13:14:53 +08:00
wjqserver
bed6c486dc 25w28b 2025-04-15 10:23:29 +08:00
wjqserver
ab77c5c7da 25w28a 2025-04-14 12:27:12 +08:00
wjqserver
bf21bd197a 25w28t-2 2025-04-11 07:29:03 +08:00
wjqserver
8af107c584 update for touka-httpc 0.4.0 2025-04-11 07:24:50 +08:00
wjqserver
d6d54b222f dix auth checker 2025-04-10 23:07:48 +08:00
wjqserver
005a4543d4 update deps 2025-04-10 23:07:13 +08:00
wjqserver
a85eb38de5 update deps 2025-04-08 20:50:55 +08:00
里見 灯花
152fb8aa71 Merge pull request #84 from WJQSERVER-STUDIO/dev
3.0.1
2025-04-08 20:49:27 +08:00
wjqserver
3e9e43cd44 3.0.1 2025-04-08 20:48:56 +08:00
wjqserver
8a50b311fc 25w27a 2025-04-07 18:51:22 +08:00
wjqserver
dcc50401c4 update deps 2025-04-07 18:34:00 +08:00
wjqserver
d62a1f9769 [docs] update config docs 2025-04-07 18:33:47 +08:00
39 changed files with 2641 additions and 688 deletions

View File

@@ -2,7 +2,7 @@
name: Features request
about: 提出新功能建议
title: "[Features]"
labels: enhancement
labels: 改进
assignees: ''
---

View File

@@ -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

View File

@@ -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

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ demo.toml
*.bak
list.json
repos
pages
pages
*_test

View File

@@ -1,5 +1,304 @@
# 更新日志
3.5.0 - 2025-06-05
---
- CHANGE: 更新许可证 v2.0 => v2.1
- CHANGE: 修正工作流的一些问题
- ADD: 增加`ForceAllowApiPassList`, 实现 #114
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`
- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified`
- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate`
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观测到了一些反射造成的内存占用异常
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缓存, 避免重复渲染
- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic)
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: 替换到实验性的`encoding/json/v2`
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预发布版本,请勿在生产环境中使用;
- ADD: 初步实现多`target` Docker代理
- ADD: 加入`weakcache`用于处理短期令牌
3.3.3 - 2025-05-20
---
- CHANGE: 加入`senseClientDisconnection``async`配置项
25w39a - 2025-05-19
---
- PRE-RELEASE: 此版本是v3.3.3预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`senseClientDisconnection``async`配置项
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`
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`选项
- ADD: 加入带宽限制功能
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
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
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- CHANGE: `httpc`切换到`dev`, 测试在retry前检查ctx状态
25w36a - 2025-05-13
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- CHANGE: 为`httpc`加入`request builder``withcontext`选项
3.2.4 - 2025-05-13
---
- CHANGE: 移除未使用的变量与相关计算
25w35a - 2025-05-12
---
- PRE-RELEASE: 此版本是v3.2.4预发布版本,请勿在生产环境中使用;
- CHANGE: 移除未使用的变量与相关计算
3.2.3 - 2025-05-07
---
- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录
- CHANGE: 更新Go版本到go1.24.3
- CHANGE: 迁移httpc到新的仓库
25w34b - 2025-05-07
---
- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用;
- CHANGE: 更新Go版本到go1.24.3
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的限制
- FIX: 修正Header部分的一些处理问题
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
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部分的一些处理问题
3.2.1 - 2025-04-29
---
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
25w32a - 2025-04-29
---
- PRE-RELEASE: 此版本是v3.2.1预发布版本,请勿在生产环境中使用;
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
3.2.0 - 2025-04-27
---
- CHANGE: 加入`ghcr``dockerhub`反代功能
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
25w31a - 2025-04-27
---
- PRE-RELEASE: 此版本是v3.2.0预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`ghcr``dockerhub`反代功能
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
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预发布版本,请勿在生产环境中使用;
- CHANGE: 改进`rate`模块, 避免并发竞争问题
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
- CHANGE: 修改部分log等级
- FIX: 修正默认配置的填充错误
25w30d - 2025-04-22
---
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
25w30c - 2025-04-21
---
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
- CHANGE: 改进handle, 复用共同部分
- CHANGE: 细化url匹配的返回码处理
- CHANGE: 增加404界面
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预发布版本,请勿在生产环境中使用;
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器
3.0.3 - 2025-04-19
---
- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息
- FIX: 修正非预期的header操作行为
- CHANGE: 合并header相关逻辑, 避免多次操作
- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理
- CHANGE: 增加`netlib`配置项
25w29b - 2025-04-19
---
- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用;
- CHANGE: 增加`netlib`配置项
25w29a - 2025-04-17
---
- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用;
- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息
- FIX: 修正非预期的header操作行为
- CHANGE: 合并header相关逻辑, 避免多次操作
- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理
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预发布版本,请勿在生产环境中使用;
- CHANGE: 改进resp关闭
- CHANGE: 避免重复的re编译操作
25w28a - 2025-04-14
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用;
- CHANGE: 去除不必要的请求
- CHANGE: 改进`httpc`相关配置
- CHANGE: 合入test版本修改
25w28t-2 - 2025-04-11
---
- TEST: 测试验证版本
- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进
25w28t-1 - 2025-04-11
---
- TEST: 测试验证版本
- CHANGE: 更新httpc 0.4.0
3.0.1 - 2025-04-08
---
- CHANGE: 加入`memLimit`指示gc
- CHANGE: 加入`hlog`输出路径配置
- CHANGE: 修正H2C配置问题
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; 下一个起点;

View File

@@ -1 +1 @@
25w26a
25w44a

20
LICENSE
View File

@@ -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.

View File

@@ -1,8 +1,13 @@
# 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)
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能
## 项目说明
@@ -12,13 +17,15 @@
- 🌐 **使用字节旗下的 [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 脚本嵌套加速**
- 🐚 **支持 shell 脚本多层嵌套加速**
### 项目相关
@@ -28,11 +35,11 @@
[相关文章](https://blog.wjqserver.com/categories/my-program/)
[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md)
[GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/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
@@ -40,6 +47,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
```
## 部署说明

View File

@@ -1 +1 @@
3.0.0
3.5.0

View File

@@ -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"
)
@@ -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,
}))
}

View File

@@ -1,11 +1,10 @@
package auth
import (
"context"
"fmt"
"ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
)
@@ -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
@@ -47,7 +46,7 @@ func AuthHandler(ctx context.Context, c *app.RequestContext, cfg *config.Config)
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))
}
}

View File

@@ -1,12 +1,13 @@
package auth
import (
"encoding/json"
"fmt"
"ghproxy/config"
"os"
"strings"
"sync"
json "github.com/bytedance/sonic"
)
type Blacklist struct {

View File

@@ -1,12 +1,13 @@
package auth
import (
"encoding/json"
"fmt"
"ghproxy/config"
"os"
"strings"
"sync"
json "github.com/bytedance/sonic"
)
// Whitelist 用于存储白名单信息

View File

@@ -18,23 +18,31 @@ type Config struct {
Whitelist WhitelistConfig
RateLimit RateLimitConfig
Outbound OutboundConfig
Docker DockerConfig
}
/*
[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"`
SizeLimit int `toml:"sizeLimit"`
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"`
}
/*
@@ -43,12 +51,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"`
}
/*
@@ -86,9 +96,11 @@ 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"`
Async bool `toml:"async"`
HertZLogPath string `toml:"hertzLogPath"`
}
/*
@@ -98,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 {
@@ -119,11 +133,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"`
}
/*
@@ -136,6 +174,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) {
@@ -178,7 +226,9 @@ func DefaultConfig() *Config {
Server: ServerConfig{
Port: 8080,
Host: "0.0.0.0",
NetLib: "netpoll",
SizeLimit: 125,
MemLimit: 0,
H2C: true,
Cors: "*",
Debug: false,
@@ -204,35 +254,48 @@ 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,
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,
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,
RateMethod: "total",
RatePerMinute: 100,
Burst: 10,
BandwidthLimit: BandwidthLimitConfig{
Enabled: false,
TotalLimit: "100mbps",
TotalBurst: "100mbps",
SingleLimit: "10mbps",
SingleBurst: "10mbps",
},
},
Outbound: OutboundConfig{
Enabled: false,
Url: "socks5://127.0.0.1:1080",
},
Docker: DockerConfig{
Enabled: false,
Target: "ghcr",
},
}
}

View File

@@ -1,7 +1,10 @@
[server]
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
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false
@@ -11,6 +14,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
@@ -30,6 +34,8 @@ 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]
method = "parameters" # "header" or "parameters"
@@ -38,6 +44,7 @@ key = ""
enabled = false
passThrough = false
ForceAllowApi = false
ForceAllowApiPassList = false
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json"
@@ -53,6 +60,17 @@ 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"
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub

View File

@@ -1,7 +1,9 @@
[server]
host = "127.0.0.1"
port = 8080
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
sizeLimit = 125 # MB
memLimit = 0 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false
@@ -30,6 +32,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"
@@ -55,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

View File

@@ -1,5 +1,7 @@
# ghproxy 用户配置文档
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
`ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml``blacklist.json``whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。
## `config.toml` - 主配置文件
@@ -12,7 +14,9 @@
[server]
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
@@ -22,6 +26,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
@@ -41,6 +46,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"
@@ -64,9 +70,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 or "xx.example.com"
```
### 配置项详细说明
@@ -81,10 +98,18 @@ 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)
* 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。
* `memLimit`: `runtime`内存限制
* 类型: 整数 (`int64`)
* 默认值: `0` (不传入)
* 说明: 给`runtime`的指标, 让gc行为更高效
* `H2C`: 是否启用 H2C (HTTP/2 Cleartext) 传输。
* 类型: 布尔值 (`bool`)
* 默认值: `true` (启用)
@@ -123,6 +148,10 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
* 类型: 整数 (`int`)
* 默认值: `0` (不限制)
* 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。
* `useCustomRawHeaders`: 使用预定义header避免github waf对应zh-CN的封锁
* 类型: 布尔值(`bool`)
* 默认值: `false`(停用)
* 说明: 启用后, 拉取raw文件会使用程序预定义的固定headers, 而不是原先的复制行为
* **`[gitclone]` - Git 克隆配置**
@@ -193,6 +222,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]` - 认证配置**
@@ -267,6 +300,27 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
* 类型: 整数 (`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]` - 出站代理配置**
@@ -280,6 +334,22 @@ 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)。
* 自定义, 支持传入自定义target, 例如`"docker.example.com"`
## `blacklist.json` - 黑名单配置
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。

View File

@@ -1,5 +1,7 @@
# Flag
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
GHProxy接受以下flag传入
```bash

View File

@@ -1,5 +1,7 @@
## GHProxy 文档
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
### 配置文件
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md

37
go.mod
View File

@@ -1,38 +1,49 @@
module ghproxy
go 1.24.2
go 1.24.3
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/WJQSERVER-STUDIO/httpc v0.5.1
github.com/WJQSERVER-STUDIO/logger v1.7.3
github.com/cloudwego/hertz v0.10.0
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.40.0
golang.org/x/time v0.11.0
)
require (
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.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 (
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.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
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
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
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/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/arch v0.17.0 // 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

54
go.sum
View File

@@ -2,17 +2,21 @@ 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/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.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.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=
@@ -20,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.6 h1:Kj5SSPlKBC32NIN7+B/tt8O1pdDz8brMai00rqqjULQ=
github.com/cloudwego/hertz v0.9.6/go.mod h1:X5Ez52XhtszU4t+CTBGIJI4PqmcI1oSf8ULBz0SWfLo=
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=
@@ -29,12 +33,14 @@ 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=
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=
@@ -46,12 +52,10 @@ 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.3.3 h1:Th0uJ5do3oqqZgdUDtqD1SH11x8TcJmrwHMJQlEIKCg=
github.com/satomitouka/touka-httpc v0.3.3/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=
@@ -80,15 +84,17 @@ 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.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.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-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-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=
@@ -98,8 +104,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.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 +119,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.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 +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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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=

383
main.go
View File

@@ -8,6 +8,7 @@ import (
"io/fs"
"net/http"
"os"
"runtime/debug"
"time"
"ghproxy/api"
@@ -16,21 +17,27 @@ import (
"ghproxy/middleware/loggin"
"ghproxy/proxy"
"ghproxy/rate"
"ghproxy/weakcache"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"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"
"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"
)
var (
cfg *config.Config
r *server.Hertz
configfile = "/data/ghproxy/config/config.toml"
hertZfile *os.File
cfgfile string
version string
runMode string
@@ -45,6 +52,10 @@ var (
pagesFS embed.FS
)
var (
wcache *weakcache.Cache[string] // docker token缓存
)
var (
logw = logger.Logw
logDump = logger.LogDump
@@ -116,6 +127,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)
@@ -126,10 +138,35 @@ 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)
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 {
hlog.SetOutput(os.Stdout)
logWarning("Failed to open hertz log file: %v", err)
} else {
hlog.SetOutput(hertZfile)
}
hlog.SetLevel(hlog.LevelInfo)
}
}
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) {
@@ -153,37 +190,53 @@ 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 加载嵌入式页面资源
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(pageFS, "pages/hub")
default:
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
pages, err = fs.Sub(pageFS, "pages/design") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme)
}
if err != nil {
return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err)
}
// 初始化errPagesFs
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
}
@@ -214,7 +267,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
@@ -230,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 {
// 加载嵌入式资源
@@ -238,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(pages))
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
}
@@ -315,9 +381,14 @@ func init() {
loadConfig()
if cfg != nil { // 在setupLogger前添加空值检查
setupLogger(cfg)
setupHertZLogger(cfg)
InitReq(cfg)
setMemLimit(cfg)
loadlist(cfg)
setupRateLimit(cfg)
if cfg.Docker.Enabled {
wcache = proxy.InitWeakCache()
}
if cfg.Server.Debug {
runMode = "dev"
@@ -332,75 +403,118 @@ func init() {
}
func main() {
// 如果 showVersion 为 true则在 init 阶段已退出,这里直接返回
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.")
return // 如果配置未加载,则不继续执行
return
}
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
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),
server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
server.WithSenseClientDisconnection(cfg.Server.SenseClientDisconnection),
)
}
} else {
logError("Invalid NetLib: %s", cfg.Server.NetLib)
fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib)
os.Exit(1)
}
r := server.New(
server.WithHostPorts(addr),
server.WithH2C(true),
)
r.AddProtocol("h2", factory.NewServerFactory())
// 添加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)
r.GET("/github.com/:user/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) {
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", "releases")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
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/: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/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) {
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", "clone")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
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/: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/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "api")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
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)))
c.Header("Docker-Distribution-API-Version", "registry/2.0")
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)
})
/*
// 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/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for archive
proxy.NoRouteHandler(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/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/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)
})
// 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)
})
// 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)
})
// 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.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) {
proxy.GhcrRouting(cfg)(ctx, c)
})
*/
@@ -412,7 +526,26 @@ func main() {
fmt.Printf("A Go Based High-Performance Github Proxy \n")
fmt.Printf("Made by WJQSERVER-STUDIO\n")
r.Spin()
if cfg.Server.Debug {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
}
if wcache != nil {
defer wcache.StopCleanup()
}
defer logger.Close()
defer func() {
if hertZfile != nil {
err := hertZfile.Close()
if err != nil {
logError("Failed to close hertz log file: %v", err)
}
}
}()
r.Spin()
fmt.Println("Program Exit")
}

View File

@@ -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"
)

62
proxy/authparse.go Normal file
View File

@@ -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
}

View File

@@ -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
}
}

64
proxy/bandwidth.go Normal file
View File

@@ -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
}

View File

@@ -1,7 +1,6 @@
package proxy
import (
"bytes"
"context"
"fmt"
"ghproxy/config"
@@ -9,59 +8,43 @@ import (
"net/http"
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
)
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)
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()
}
}()
rb := client.NewRequestBuilder(string(c.Request.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
}
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)
req, err := client.NewRequest(string(method()), u, bodyReader)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
setRequestHeaders(c, req, cfg, matcher)
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
@@ -69,37 +52,45 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
// 错误处理(404)
if resp.StatusCode == 404 {
c.String(http.StatusNotFound, "File Not Found")
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 != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > sizelimit {
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))
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)
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)
if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove {
for _, value := range values {
c.Header(key, value)
}
}
}
headersToRemove := map[string]struct{}{
"Content-Security-Policy": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
}
for header := range headersToRemove {
resp.Header.Del(header)
}
switch cfg.Server.Cors {
case "*":
c.Header("Access-Control-Allow-Origin", "*")
@@ -113,6 +104,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
@@ -120,18 +117,25 @@ 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(), c.Request.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)
c.SetBodyStream(reader, -1)
var reader io.Reader
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(), 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
}
} else {
c.SetBodyStream(resp.Body, -1)
if contentLength != "" {
c.SetBodyStream(bodyReader, bodySize)
return
}
c.SetBodyStream(bodyReader, -1)
}
}

328
proxy/docker.go Normal file
View File

@@ -0,0 +1,328 @@
package proxy
import (
"context"
"fmt"
json "github.com/bytedance/sonic"
"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 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://"+ghcrTarget+string(c.Request.RequestURI()), image, cfg, ghcrTarget)
} else if cfg.Docker.Target == "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()), 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
}
}
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, image *imageInfo, cfg *config.Config, target string) {
var (
method []byte
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()
}
}()
method = c.Request.Method()
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 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)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
// 处理状态码
if resp.StatusCode == 401 {
// 请求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
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
}
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()
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.Response.Header.Add(key, value)
}
}
c.Status(resp.StatusCode)
bodyReader := resp.Body
if cfg.RateLimit.BandwidthLimit.Enabled {
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
}
if contentLength != "" {
c.SetBodyStream(bodyReader, bodySize)
return
}
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
}

View File

@@ -1,10 +1,19 @@
package proxy
import (
"net/http"
"bytes"
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"sync"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"fmt"
"html/template"
"io/fs"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
lru "github.com/hashicorp/golang-lru/v2"
)
// 日志模块
@@ -18,6 +27,303 @@ var (
)
func HandleError(c *app.RequestContext, message string) {
c.JSON(http.StatusInternalServerError, map[string]string{"error": message})
logError(message)
ErrorPage(c, NewErrorWithStatusLookup(500, message))
logError("Error handled: %s", 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: "抱歉,您访问的页面不存在。",
}
ErrTooManyRequests = &GHProxyErrors{
StatusCode: 429,
StatusDesc: "Too Many Requests",
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,
ErrTooManyRequests.StatusCode: ErrTooManyRequests,
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,
}
}
}
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
}
// 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)
return ""
}
hasher := sha256.New()
hasher.Write(buf.Bytes())
return hex.EncodeToString(hasher.Sum(nil))
}
func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData {
return ErrorPageData{
StatusCode: errInfo.StatusCode,
StatusDesc: errInfo.StatusDesc,
StatusText: errInfo.StatusText,
HelpInfo: errInfo.HelpInfo,
ErrorMessage: errInfo.ErrorMessage,
}
}
// SizedLRUCache 实现了基于字节大小限制的 LRU 缓存。
// 它包装了 hashicorp/golang-lru/v2.Cache并额外管理缓存的总字节大小。
type SizedLRUCache struct {
cache *lru.Cache[string, []byte]
mu sync.Mutex // 保护 currentBytes 字段
maxBytes int64 // 缓存的最大字节容量
currentBytes int64 // 缓存当前占用的字节数
}
// 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, 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 after parsing")
}
// 创建一个 bytes.Buffer 用于存储渲染结果
var buf bytes.Buffer
err = tmpl.Execute(&buf, data)
if err != nil {
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()
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
// 尝试从缓存中获取页面数据
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})
logWarning("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)
}

View File

@@ -8,13 +8,33 @@ 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) {
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())
logDump("Url Before FMT:%s", u)
reqBodyReader := bytes.NewBuffer(c.Request.Body())
//bodyReader := c.Request.BodyStream() // 不可替换为此实现
if cfg.GitClone.Mode == "cache" {
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
if err != nil {
@@ -23,27 +43,21 @@ 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 (
resp *http.Response
//err error
)
body := c.Request.Body()
bodyReader := bytes.NewBuffer(body)
// 创建请求
if cfg.GitClone.Mode == "cache" {
req, err := gitclient.NewRequest(method, u, bodyReader)
rb := gitclient.NewRequestBuilder(method, u)
rb.NoDefaultHeaders()
rb.SetBody(reqBodyReader)
rb.WithContext(ctx)
req, err := rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
removeWSHeader(req)
setRequestHeaders(c, req, cfg, "clone")
AuthPassThrough(c, cfg, req)
resp, err = gitclient.Do(req)
@@ -52,13 +66,18 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
return
}
} else {
req, err := client.NewRequest(method, u, bodyReader)
rb := client.NewRequestBuilder(string(c.Request.Method()), u)
rb.NoDefaultHeaders()
rb.SetBody(reqBodyReader)
rb.WithContext(ctx)
req, err := rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
removeWSHeader(req)
setRequestHeaders(c, req, cfg, "clone")
AuthPassThrough(c, cfg, req)
resp, err = client.Do(req)
@@ -72,6 +91,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)
@@ -82,7 +104,7 @@ 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)
}
}
@@ -114,5 +136,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)
}

View File

@@ -2,12 +2,9 @@ package proxy
import (
"context"
"errors"
"fmt"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/rate"
"net/http"
"regexp"
"strings"
@@ -19,104 +16,62 @@ 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
}
var shoudBreak bool
shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
if shoudBreak {
return
}
rawPath := strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
matches := re.FindStringSubmatch(rawPath) // 匹配路径
logInfo("URL: %v", matches)
var (
rawPath string
matches []string
)
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
matches = re.FindStringSubmatch(rawPath) // 匹配路径
// 匹配路径错误处理
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(), c.Path(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.Path())))
return
}
// 制作url
rawPath = "https://" + matches[2]
user, repo, matcher, err := Matcher(rawPath, cfg)
if err != nil {
if errors.Is(err, ErrInvalidURL) {
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
logWarning(err.Error())
return
}
if errors.Is(err, ErrAuthHeaderUnavailable) {
c.String(http.StatusForbidden, "AuthHeader Unavailable")
logWarning(err.Error())
return
}
}
username := user
var (
user string
repo string
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(), username, 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)
// 白名单检查
if cfg.Whitelist.Enabled {
whitelist := auth.CheckWhitelist(username, 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
}
var matcherErr *GHProxyErrors
user, repo, matcher, matcherErr = Matcher(rawPath, cfg)
if matcherErr != nil {
ErrorPage(c, matcherErr)
return
}
// 黑名单检查
if cfg.Blacklist.Enabled {
blacklist := auth.CheckBlacklist(username, 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
}
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)
if shoudBreak {
return
}
// 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth
shoudBreak = authCheck(c, cfg, matcher, rawPath)
if shoudBreak {
return
}
// 处理blob/raw路径
if matcher == "blob" {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
}
// 鉴权
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":
@@ -124,8 +79,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.")
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher)
return
}
}

View File

@@ -4,10 +4,9 @@ import (
"fmt"
"ghproxy/config"
"net/http"
"sync"
"time"
httpc "github.com/satomitouka/touka-httpc"
"github.com/WJQSERVER-STUDIO/httpc"
)
var BufferSize int = 32 * 1024 // 32KB
@@ -15,23 +14,24 @@ var BufferSize int = 32 * 1024 // 32KB
var (
tr *http.Transport
gittr *http.Transport
BufferPool *sync.Pool
client *httpc.Client
gitclient *httpc.Client
ghcrtr *http.Transport
ghcrclient *httpc.Client
)
func InitReq(cfg *config.Config) {
func InitReq(cfg *config.Config) error {
initHTTPClient(cfg)
if cfg.GitClone.Mode == "cache" {
initGitHTTPClient(cfg)
}
// 初始化固定大小的缓存池
BufferPool = &sync.Pool{
New: func() interface{} {
return make([]byte, BufferSize)
},
initGhcrHTTPClient(cfg)
err := SetGlobalRateLimit(cfg)
if err != nil {
return err
}
return nil
}
func initHTTPClient(cfg *config.Config) {
@@ -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
@@ -83,27 +81,16 @@ func initHTTPClient(cfg *config.Config) {
httpc.WithTransport(tr),
)
}
}
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 +99,6 @@ func initGitHTTPClient(cfg *config.Config) {
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
}
} else {
// 错误的模式
@@ -130,14 +116,87 @@ 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{
ForceH2C: true,
}),
)
} else if !cfg.Server.Debug && cfg.GitClone.ForceH2C {
gitclient = httpc.New(
httpc.WithTransport(gittr),
httpc.WithProtocols(httpc.ProtocolsConfig{
ForceH2C: 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,
}),
)
}
}
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),
)
}
}

View File

@@ -1,46 +1,14 @@
package proxy
import (
"bufio"
"compress/gzip"
"fmt"
"ghproxy/config"
"io"
"net/url"
"regexp"
"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",
}
)
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
@@ -49,14 +17,18 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, 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, "/")
}
/*
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 {
return "", "", "", ErrInvalidURL
errMsg := "Not enough parts in path after matching 'https://github.com*'"
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
user = parts[0]
repo = parts[1]
@@ -65,12 +37,15 @@ 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:
return "", "", "", ErrInvalidURL
errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher"
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
}
return user, repo, matcher, nil
@@ -80,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]
@@ -93,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 = ""
@@ -115,82 +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 "", "", "", ErrInvalidURL
}
func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) {
var (
matcher string
)
// 匹配 "https://github.com"开头的链接
if strings.HasPrefix(rawPath, "https://github.com") {
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
if strings.HasPrefix(remainingPath, "/") {
remainingPath = strings.TrimPrefix(remainingPath, "/")
}
return true, "", nil
}
// 匹配 "https://raw.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
return true, matcher, nil
}
// 匹配 "https://raw.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.github.com") {
return true, matcher, nil
}
// 匹配 "https://gist.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
return true, matcher, nil
}
// 匹配 "https://gist.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.github.com") {
return true, matcher, nil
}
if cfg.Shell.RewriteAPI {
// 匹配 "https://api.github.com/"开头的链接
if strings.HasPrefix(rawPath, "https://api.github.com") {
matcher = "api"
return true, matcher, nil
}
}
return false, "", ErrInvalidURL
}
// 匹配文件扩展名是sh的rawPath
func MatcherShell(rawPath string) bool {
/*
if strings.HasSuffix(rawPath, ".sh") {
return true
}
return false
*/
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
//return "", "", "", ErrNotFound
errMsg := "Didn't match any matcher"
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
}
var (
@@ -243,111 +154,4 @@ func extractParts(rawURL string) (string, string, string, url.Values, error) {
return repoOwner, repoName, remainingPath, queryParams, nil
}
// 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
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
}
}
}
}
}()
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 链接
urlPattern := regexp.MustCompile(`https?://[^\s'"]+`)
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 和 writtenerror 由 Goroutine 通过 pipeWriter.CloseWithError 传递
}
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)

185
proxy/nest.go Normal file
View File

@@ -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 和 writtenerror 由 Goroutine 通过 pipeWriter.CloseWithError 传递
}

View File

@@ -1,19 +1,75 @@
package proxy
import (
"ghproxy/config"
"net/http"
"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))
})
}
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": {},
}
func removeWSHeader(req *http.Request) {
req.Header.Del("Upgrade")
req.Header.Del("Connection")
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{
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Transfer-Encoding": "chunked",
"User-Agent": "GHProxy/1.0",
}
)
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)
headerValue := string(value)
if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove {
req.Header.Set(headerKey, headerValue)
}
})
}
}

72
proxy/routing.go Normal file
View File

@@ -0,0 +1,72 @@
package proxy
import (
"context"
"ghproxy/config"
"ghproxy/rate"
"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) {
var shoudBreak bool
shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
if shoudBreak {
return
}
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")
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)
if shoudBreak {
return
}
shoudBreak = authCheck(c, cfg, matcher, rawPath)
if shoudBreak {
return
}
// 处理blob/raw路径
if matcher == "blob" {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
}
// 为rawpath加入https:// 头
rawPath = "https://" + rawPath
logDebug("Matched: %v", matcher)
switch matcher {
case "releases", "blob", "raw", "gist", "api":
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
case "clone":
GitReq(ctx, c, rawPath, cfg, "git")
default:
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher)
return
}
}
}

90
proxy/utils.go Normal file
View File

@@ -0,0 +1,90 @@
package proxy
import (
"fmt"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/rate"
"github.com/cloudwego/hertz/pkg/app"
)
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)
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)
return true
}
}
// 黑名单检查
if cfg.Blacklist.Enabled {
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)
return true
}
}
return false
}
// 鉴权
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 {
ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed"))
logInfo("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath)
return true
}
}
// 鉴权
if cfg.Auth.Enabled {
var authcheck bool
authcheck, err = auth.AuthHandler(c, cfg)
if !authcheck {
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
}
}
return false
}
func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) bool {
// 限制访问频率
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")
ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid RateLimit Method"))
return true
}
if !allowed {
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
}

View File

@@ -1,13 +1,14 @@
package rate
import (
"sync"
"time"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/WJQSERVER-STUDIO/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()
}

258
weakcache/weakcache.go Normal file
View File

@@ -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 退出
}