Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b29940df21 | ||
|
|
185522133b | ||
|
|
6be6e1ba2c | ||
|
|
1ba100c28d | ||
|
|
1370617f5b | ||
|
|
e829c2baff | ||
|
|
75d909ef16 | ||
|
|
2bab0a9774 | ||
|
|
171fe61342 | ||
|
|
362ad96fbe | ||
|
|
5b17e1f0b6 | ||
|
|
e40e1aadee | ||
|
|
82943428d3 | ||
|
|
b7ce929db8 | ||
|
|
68bf51aaed | ||
|
|
16b6b05fb8 | ||
|
|
d2b2d823b8 | ||
|
|
4598257faa | ||
|
|
1afb352194 | ||
|
|
430e313d47 | ||
|
|
31d435bfa0 | ||
|
|
6ff23f639e | ||
|
|
c7954ae91a | ||
|
|
11099176bf | ||
|
|
f3eb92ea51 | ||
|
|
5ddbf1d2a0 | ||
|
|
d38ca3969f | ||
|
|
146b0d7748 | ||
|
|
d92424cb94 | ||
|
|
0f437dc891 | ||
|
|
816b35654a | ||
|
|
a4fae95526 | ||
|
|
ea0e4e9801 | ||
|
|
5facc36947 | ||
|
|
5c25bc012f | ||
|
|
b2712f8184 | ||
|
|
566a0ea26a | ||
|
|
7d4aae1668 | ||
|
|
052243b095 | ||
|
|
4ded2186d8 | ||
|
|
aa95daf8c0 | ||
|
|
89b850c1ec | ||
|
|
ce814875e1 | ||
|
|
47c03763a7 | ||
|
|
71bc2aaed7 | ||
|
|
3f8d16511e | ||
|
|
43469532d4 | ||
|
|
e32479b287 | ||
|
|
ef6e0a78cd | ||
|
|
c2e2b661a4 | ||
|
|
791f668758 | ||
|
|
92c4c62b46 | ||
|
|
545144c7b5 | ||
|
|
866638ba8e | ||
|
|
e3fd604945 | ||
|
|
90709539f4 | ||
|
|
1011a25d16 | ||
|
|
bd63ed3070 | ||
|
|
3c11e9826e | ||
|
|
ef3b1bf1f0 | ||
|
|
ad4d8eb670 | ||
|
|
030f0d12a9 | ||
|
|
e57432a01c | ||
|
|
ace795fe9d | ||
|
|
3f51e5319a | ||
|
|
55769d9a40 | ||
|
|
7eb312243c | ||
|
|
6ca31bc252 | ||
|
|
dfc49ae28b | ||
|
|
a0cca13deb | ||
|
|
1498aaed14 | ||
|
|
086aa999e1 | ||
|
|
bf92cc8429 | ||
|
|
d94f6c0f5d | ||
|
|
f540b2edcd | ||
|
|
8aef197fde | ||
|
|
52d6f8e759 | ||
|
|
a7be65a111 | ||
|
|
9977eb1437 | ||
|
|
47de48bcce | ||
|
|
8ccf48a6fe | ||
|
|
7a6544c6c9 | ||
|
|
b955c915ff | ||
|
|
e42ea358bb | ||
|
|
4936a93788 | ||
|
|
493ac28b59 | ||
|
|
d79aeaaacd | ||
|
|
558d3fbb0b | ||
|
|
3d7559bd66 | ||
|
|
809032a970 | ||
|
|
2eb6a9810b | ||
|
|
26a5148c6f | ||
|
|
c656aa41ca | ||
|
|
0b052f9c7f | ||
|
|
6fb7e1150e | ||
|
|
5e0f95dae3 | ||
|
|
c1c39a5a1f | ||
|
|
dd2f5b5a12 | ||
|
|
7e5b12dff8 | ||
|
|
26a42b6510 | ||
|
|
254c9a8bad | ||
|
|
060453f070 | ||
|
|
f110c96c1f | ||
|
|
73aac79c1b | ||
|
|
bed6c486dc | ||
|
|
ab77c5c7da | ||
|
|
bf21bd197a | ||
|
|
8af107c584 | ||
|
|
d6d54b222f | ||
|
|
005a4543d4 | ||
|
|
a85eb38de5 |
2
.github/ISSUE_TEMPLATE/features_request.md
vendored
2
.github/ISSUE_TEMPLATE/features_request.md
vendored
@@ -2,7 +2,7 @@
|
|||||||
name: Features request
|
name: Features request
|
||||||
about: 提出新功能建议
|
about: 提出新功能建议
|
||||||
title: "[Features]"
|
title: "[Features]"
|
||||||
labels: enhancement
|
labels: 改进
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
2
.github/workflows/build-dev.yml
vendored
2
.github/workflows/build-dev.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: 拉取前端
|
- name: 拉取前端
|
||||||
run: |
|
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/
|
sudo rm -rf pages/.git/
|
||||||
|
|
||||||
- name: 安装 Go
|
- name: 安装 Go
|
||||||
|
|||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: 拉取前端
|
- name: 拉取前端
|
||||||
run: |
|
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/
|
sudo rm -rf pages/.git/
|
||||||
|
|
||||||
- name: 安装 Go
|
- name: 安装 Go
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ demo.toml
|
|||||||
*.bak
|
*.bak
|
||||||
list.json
|
list.json
|
||||||
repos
|
repos
|
||||||
pages
|
pages
|
||||||
|
*_test
|
||||||
288
CHANGELOG.md
288
CHANGELOG.md
@@ -1,6 +1,292 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
3.0.1 -2025-04-08
|
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: 加入`memLimit`指示gc
|
||||||
- CHANGE: 加入`hlog`输出路径配置
|
- CHANGE: 加入`hlog`输出路径配置
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
25w27a
|
25w44a
|
||||||
20
LICENSE
20
LICENSE
@@ -1,5 +1,5 @@
|
|||||||
WJQserver Studio 开源许可证
|
WJQserver Studio 开源许可证
|
||||||
版本 v2.0
|
版本 v2.1
|
||||||
|
|
||||||
版权所有 © WJQserver Studio 2024
|
版权所有 © WJQserver Studio 2024
|
||||||
|
|
||||||
@@ -31,8 +31,7 @@ WJQserver Studio 开源许可证
|
|||||||
|
|
||||||
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
|
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
|
||||||
|
|
||||||
* 1.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
|
* 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
|
||||||
* 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
|
|
||||||
|
|
||||||
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
|
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
|
||||||
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
|
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
|
||||||
@@ -44,6 +43,8 @@ WJQserver Studio 开源许可证
|
|||||||
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
|
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
|
||||||
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
|
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
|
||||||
|
|
||||||
|
* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
|
||||||
|
|
||||||
2. 复制与分发
|
2. 复制与分发
|
||||||
|
|
||||||
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
|
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
|
||||||
@@ -51,13 +52,13 @@ WJQserver Studio 开源许可证
|
|||||||
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
|
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
|
||||||
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
|
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
|
||||||
|
|
||||||
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.2.2 条(开源继承与互惠共享)的约束。
|
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。
|
||||||
|
|
||||||
3. 修改权限
|
3. 修改权限
|
||||||
|
|
||||||
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
|
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
|
||||||
|
|
||||||
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.2.2 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
|
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
|
||||||
|
|
||||||
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。
|
* 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 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.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:
|
||||||
* 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:
|
|
||||||
|
|
||||||
* Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works.
|
* 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.
|
* 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.
|
* 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.
|
* 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. 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:
|
* 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.
|
* 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.
|
* 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. 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.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.
|
* 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.
|
||||||
|
|
||||||
|
|||||||
28
README.md
28
README.md
@@ -1,8 +1,13 @@
|
|||||||
# GHProxy
|
# GHProxy
|
||||||
|
|
||||||
[](https://goreportcard.com/report/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 框架**
|
- 🌐 **使用字节旗下的 [HertZ](https://github.com/cloudwego/hertz) 作为 Web 框架**
|
||||||
- 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端**
|
- 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端**
|
||||||
- 📥 **支持 Git clone、raw、releases 等文件拉取**
|
- 📥 **支持 Git clone、raw、releases 等文件拉取**
|
||||||
|
- 🐳 **支持反代Docker, GHCR等镜像仓库**
|
||||||
- 🎨 **支持多个前端主题**
|
- 🎨 **支持多个前端主题**
|
||||||
- 🚫 **支持自定义黑名单/白名单**
|
- 🚫 **支持自定义黑名单/白名单**
|
||||||
- 🗄️ **支持 Git Clone 缓存(配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git))**
|
- 🗄️ **支持 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://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/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
|
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/github.com/WJQSERVER-STUDIO/ghproxy.git
|
||||||
git clone https://ghproxy.1888866.xyz/https://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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 部署说明
|
## 部署说明
|
||||||
|
|||||||
33
api/api.go
33
api/api.go
@@ -5,7 +5,7 @@ import (
|
|||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
"ghproxy/middleware/nocache"
|
"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"
|
||||||
"github.com/cloudwego/hertz/pkg/app/server"
|
"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) {
|
apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) {
|
||||||
SmartGitStatusHandler(cfg, c, ctx)
|
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")
|
logInfo("API router Init success")
|
||||||
}
|
}
|
||||||
|
|
||||||
func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
||||||
sizeLimit := cfg.Server.SizeLimit
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"MaxResponseBodySize": sizeLimit,
|
"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) {
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"Whitelist": cfg.Whitelist.Enabled,
|
"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) {
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"Blacklist": cfg.Blacklist.Enabled,
|
"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) {
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"Cors": cfg.Server.Cors,
|
"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) {
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"Status": "OK",
|
"Status": "OK",
|
||||||
@@ -96,7 +96,6 @@ func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func VersionHandler(c *app.RequestContext, ctx context.Context, version string) {
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"Version": version,
|
"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) {
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"RateLimit": cfg.RateLimit.Enabled,
|
"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) {
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"RatePerMinute": cfg.RateLimit.RatePerMinute,
|
"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) {
|
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.Response.Header.Set("Content-Type", "application/json")
|
||||||
c.JSON(200, (map[string]interface{}{
|
c.JSON(200, (map[string]interface{}{
|
||||||
"enabled": cfg.GitClone.Mode == "cache",
|
"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,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
"github.com/WJQSERVER-STUDIO/logger"
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,7 +35,7 @@ func Init(cfg *config.Config) {
|
|||||||
logDebug("Auth Init")
|
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" {
|
if cfg.Auth.Method == "parameters" {
|
||||||
isValid, err = AuthParametersHandler(c, cfg)
|
isValid, err = AuthParametersHandler(c, cfg)
|
||||||
return isValid, err
|
return isValid, err
|
||||||
@@ -47,7 +46,7 @@ func AuthHandler(ctx context.Context, c *app.RequestContext, cfg *config.Config)
|
|||||||
logError("Auth method not set")
|
logError("Auth method not set")
|
||||||
return true, nil
|
return true, nil
|
||||||
} else {
|
} else {
|
||||||
logError("Auth method not supported")
|
logError("Auth method not supported %s", cfg.Auth.Method)
|
||||||
return false, fmt.Errorf(fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method))
|
return false, fmt.Errorf("%s", fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
json "github.com/bytedance/sonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Blacklist struct {
|
type Blacklist struct {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
json "github.com/bytedance/sonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Whitelist 用于存储白名单信息
|
// Whitelist 用于存储白名单信息
|
||||||
|
|||||||
119
config/config.go
119
config/config.go
@@ -18,24 +18,31 @@ type Config struct {
|
|||||||
Whitelist WhitelistConfig
|
Whitelist WhitelistConfig
|
||||||
RateLimit RateLimitConfig
|
RateLimit RateLimitConfig
|
||||||
Outbound OutboundConfig
|
Outbound OutboundConfig
|
||||||
|
Docker DockerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
[server]
|
[server]
|
||||||
host = "0.0.0.0" # 监听地址
|
host = "0.0.0.0"
|
||||||
port = 8080 # 监听端口
|
port = 8080
|
||||||
sizeLimit = 125 # 125MB
|
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||||
H2C = true # 是否开启H2C传输
|
sizeLimit = 125 # MB
|
||||||
|
memLimit = 0 # MB
|
||||||
|
H2C = true
|
||||||
|
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||||
|
debug = false
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
Host string `toml:"host"`
|
Host string `toml:"host"`
|
||||||
SizeLimit int `toml:"sizeLimit"`
|
NetLib string `toml:"netlib"`
|
||||||
MemLimit int64 `toml:"memLimit"`
|
SenseClientDisconnection bool `toml:"senseClientDisconnection"`
|
||||||
H2C bool `toml:"H2C"`
|
SizeLimit int `toml:"sizeLimit"`
|
||||||
Cors string `toml:"cors"`
|
MemLimit int64 `toml:"memLimit"`
|
||||||
Debug bool `toml:"debug"`
|
H2C bool `toml:"H2C"`
|
||||||
|
Cors string `toml:"cors"`
|
||||||
|
Debug bool `toml:"debug"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -44,12 +51,14 @@ mode = "auto" # "auto" or "advanced"
|
|||||||
maxIdleConns = 100 # only for advanced mode
|
maxIdleConns = 100 # only for advanced mode
|
||||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||||
maxConnsPerHost = 0 # only for advanced mode
|
maxConnsPerHost = 0 # only for advanced mode
|
||||||
|
useCustomRawHeaders = false
|
||||||
*/
|
*/
|
||||||
type HttpcConfig struct {
|
type HttpcConfig struct {
|
||||||
Mode string `toml:"mode"`
|
Mode string `toml:"mode"`
|
||||||
MaxIdleConns int `toml:"maxIdleConns"`
|
MaxIdleConns int `toml:"maxIdleConns"`
|
||||||
MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"`
|
MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"`
|
||||||
MaxConnsPerHost int `toml:"maxConnsPerHost"`
|
MaxConnsPerHost int `toml:"maxConnsPerHost"`
|
||||||
|
UseCustomRawHeaders bool `toml:"useCustomRawHeaders"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -90,6 +99,7 @@ type LogConfig struct {
|
|||||||
LogFilePath string `toml:"logFilePath"`
|
LogFilePath string `toml:"logFilePath"`
|
||||||
MaxLogSize int `toml:"maxLogSize"`
|
MaxLogSize int `toml:"maxLogSize"`
|
||||||
Level string `toml:"level"`
|
Level string `toml:"level"`
|
||||||
|
Async bool `toml:"async"`
|
||||||
HertZLogPath string `toml:"hertzLogPath"`
|
HertZLogPath string `toml:"hertzLogPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,15 +110,17 @@ Key = ""
|
|||||||
Token = "token"
|
Token = "token"
|
||||||
enabled = false
|
enabled = false
|
||||||
passThrough = false
|
passThrough = false
|
||||||
ForceAllowApi = true
|
ForceAllowApi = false
|
||||||
|
ForceAllowApiPassList = false
|
||||||
*/
|
*/
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
Enabled bool `toml:"enabled"`
|
Enabled bool `toml:"enabled"`
|
||||||
Method string `toml:"method"`
|
Method string `toml:"method"`
|
||||||
Key string `toml:"key"`
|
Key string `toml:"key"`
|
||||||
Token string `toml:"token"`
|
Token string `toml:"token"`
|
||||||
PassThrough bool `toml:"passThrough"`
|
PassThrough bool `toml:"passThrough"`
|
||||||
ForceAllowApi bool `toml:"ForceAllowApi"`
|
ForceAllowApi bool `toml:"ForceAllowApi"`
|
||||||
|
ForceAllowApiPassList bool `toml:"ForceAllowApiPassList"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlacklistConfig struct {
|
type BlacklistConfig struct {
|
||||||
@@ -121,11 +133,35 @@ type WhitelistConfig struct {
|
|||||||
WhitelistFile string `toml:"whitelistFile"`
|
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 {
|
type RateLimitConfig struct {
|
||||||
Enabled bool `toml:"enabled"`
|
Enabled bool `toml:"enabled"`
|
||||||
RateMethod string `toml:"rateMethod"`
|
RateMethod string `toml:"rateMethod"`
|
||||||
RatePerMinute int `toml:"ratePerMinute"`
|
RatePerMinute int `toml:"ratePerMinute"`
|
||||||
Burst int `toml:"burst"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -138,6 +174,16 @@ type OutboundConfig struct {
|
|||||||
Url string `toml:"url"`
|
Url string `toml:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
[docker]
|
||||||
|
enabled = false
|
||||||
|
target = "ghcr" # ghcr/dockerhub
|
||||||
|
*/
|
||||||
|
type DockerConfig struct {
|
||||||
|
Enabled bool `toml:"enabled"`
|
||||||
|
Target string `toml:"target"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig 从 TOML 配置文件加载配置
|
// LoadConfig 从 TOML 配置文件加载配置
|
||||||
func LoadConfig(filePath string) (*Config, error) {
|
func LoadConfig(filePath string) (*Config, error) {
|
||||||
if !FileExists(filePath) {
|
if !FileExists(filePath) {
|
||||||
@@ -180,6 +226,7 @@ func DefaultConfig() *Config {
|
|||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
|
NetLib: "netpoll",
|
||||||
SizeLimit: 125,
|
SizeLimit: 125,
|
||||||
MemLimit: 0,
|
MemLimit: 0,
|
||||||
H2C: true,
|
H2C: true,
|
||||||
@@ -213,30 +260,42 @@ func DefaultConfig() *Config {
|
|||||||
HertZLogPath: "/data/ghproxy/log/hertz.log",
|
HertZLogPath: "/data/ghproxy/log/hertz.log",
|
||||||
},
|
},
|
||||||
Auth: AuthConfig{
|
Auth: AuthConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
Method: "parameters",
|
Method: "parameters",
|
||||||
Key: "",
|
Key: "",
|
||||||
Token: "token",
|
Token: "token",
|
||||||
PassThrough: false,
|
PassThrough: false,
|
||||||
ForceAllowApi: false,
|
ForceAllowApi: false,
|
||||||
|
ForceAllowApiPassList: false,
|
||||||
},
|
},
|
||||||
Blacklist: BlacklistConfig{
|
Blacklist: BlacklistConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
BlacklistFile: "/data/ghproxy/config/blacklist.txt",
|
BlacklistFile: "/data/ghproxy/config/blacklist.json",
|
||||||
},
|
},
|
||||||
Whitelist: WhitelistConfig{
|
Whitelist: WhitelistConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
WhitelistFile: "/data/ghproxy/config/whitelist.txt",
|
WhitelistFile: "/data/ghproxy/config/whitelist.json",
|
||||||
},
|
},
|
||||||
RateLimit: RateLimitConfig{
|
RateLimit: RateLimitConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
RateMethod: "total",
|
RateMethod: "total",
|
||||||
RatePerMinute: 100,
|
RatePerMinute: 100,
|
||||||
Burst: 10,
|
Burst: 10,
|
||||||
|
BandwidthLimit: BandwidthLimitConfig{
|
||||||
|
Enabled: false,
|
||||||
|
TotalLimit: "100mbps",
|
||||||
|
TotalBurst: "100mbps",
|
||||||
|
SingleLimit: "10mbps",
|
||||||
|
SingleBurst: "10mbps",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Outbound: OutboundConfig{
|
Outbound: OutboundConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
Url: "socks5://127.0.0.1:1080",
|
Url: "socks5://127.0.0.1:1080",
|
||||||
},
|
},
|
||||||
|
Docker: DockerConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Target: "ghcr",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
[server]
|
[server]
|
||||||
host = "0.0.0.0"
|
host = "0.0.0.0"
|
||||||
port = 8080
|
port = 8080
|
||||||
|
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||||
|
senseClientDisconnection = false
|
||||||
sizeLimit = 125 # MB
|
sizeLimit = 125 # MB
|
||||||
memLimit = 0 # MB
|
memLimit = 0 # MB
|
||||||
H2C = true
|
H2C = true
|
||||||
@@ -12,6 +14,7 @@ mode = "auto" # "auto" or "advanced"
|
|||||||
maxIdleConns = 100 # only for advanced mode
|
maxIdleConns = 100 # only for advanced mode
|
||||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||||
maxConnsPerHost = 0 # only for advanced mode
|
maxConnsPerHost = 0 # only for advanced mode
|
||||||
|
useCustomRawHeaders = false
|
||||||
|
|
||||||
[gitclone]
|
[gitclone]
|
||||||
mode = "bypass" # bypass / cache
|
mode = "bypass" # bypass / cache
|
||||||
@@ -31,6 +34,7 @@ staticDir = "/data/www"
|
|||||||
logFilePath = "/data/ghproxy/log/ghproxy.log"
|
logFilePath = "/data/ghproxy/log/ghproxy.log"
|
||||||
maxLogSize = 5 # MB
|
maxLogSize = 5 # MB
|
||||||
level = "info" # dump, debug, info, warn, error, none
|
level = "info" # dump, debug, info, warn, error, none
|
||||||
|
async = false
|
||||||
hertzLogPath = "/data/ghproxy/log/hertz.log"
|
hertzLogPath = "/data/ghproxy/log/hertz.log"
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
@@ -40,6 +44,7 @@ key = ""
|
|||||||
enabled = false
|
enabled = false
|
||||||
passThrough = false
|
passThrough = false
|
||||||
ForceAllowApi = false
|
ForceAllowApi = false
|
||||||
|
ForceAllowApiPassList = false
|
||||||
|
|
||||||
[blacklist]
|
[blacklist]
|
||||||
blacklistFile = "/data/ghproxy/config/blacklist.json"
|
blacklistFile = "/data/ghproxy/config/blacklist.json"
|
||||||
@@ -55,6 +60,17 @@ rateMethod = "total" # "ip" or "total"
|
|||||||
ratePerMinute = 180
|
ratePerMinute = 180
|
||||||
burst = 5
|
burst = 5
|
||||||
|
|
||||||
|
[rateLimit.bandwidthLimit]
|
||||||
|
enabled = false
|
||||||
|
totalLimit = "100mbps"
|
||||||
|
totalBurst = "100mbps"
|
||||||
|
singleLimit = "10mbps"
|
||||||
|
singleBurst = "10mbps"
|
||||||
|
|
||||||
[outbound]
|
[outbound]
|
||||||
enabled = false
|
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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
[server]
|
[server]
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 8080
|
port = 8080
|
||||||
|
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||||
sizeLimit = 125 # MB
|
sizeLimit = 125 # MB
|
||||||
memLimit = 0 # MB
|
memLimit = 0 # MB
|
||||||
H2C = true
|
H2C = true
|
||||||
@@ -57,3 +58,7 @@ burst = 5
|
|||||||
[outbound]
|
[outbound]
|
||||||
enabled = false
|
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
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# ghproxy 用户配置文档
|
# ghproxy 用户配置文档
|
||||||
|
|
||||||
|
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
|
||||||
|
|
||||||
`ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml`、`blacklist.json` 和 `whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。
|
`ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml`、`blacklist.json` 和 `whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。
|
||||||
|
|
||||||
## `config.toml` - 主配置文件
|
## `config.toml` - 主配置文件
|
||||||
@@ -12,6 +14,7 @@
|
|||||||
[server]
|
[server]
|
||||||
host = "0.0.0.0"
|
host = "0.0.0.0"
|
||||||
port = 8080
|
port = 8080
|
||||||
|
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||||
sizeLimit = 125 # MB
|
sizeLimit = 125 # MB
|
||||||
memLimit = 0 # MB
|
memLimit = 0 # MB
|
||||||
H2C = true
|
H2C = true
|
||||||
@@ -23,6 +26,7 @@ mode = "auto" # "auto" or "advanced"
|
|||||||
maxIdleConns = 100 # only for advanced mode
|
maxIdleConns = 100 # only for advanced mode
|
||||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||||
maxConnsPerHost = 0 # only for advanced mode
|
maxConnsPerHost = 0 # only for advanced mode
|
||||||
|
useCustomRawHeaders = false
|
||||||
|
|
||||||
[gitclone]
|
[gitclone]
|
||||||
mode = "bypass" # bypass / cache
|
mode = "bypass" # bypass / cache
|
||||||
@@ -66,9 +70,20 @@ rateMethod = "total" # "ip" or "total"
|
|||||||
ratePerMinute = 180
|
ratePerMinute = 180
|
||||||
burst = 5
|
burst = 5
|
||||||
|
|
||||||
|
[rateLimit.bandwidthLimit]
|
||||||
|
enabled = false
|
||||||
|
totalLimit = "100mbps"
|
||||||
|
totalBurst = "100mbps"
|
||||||
|
singleLimit = "10mbps"
|
||||||
|
singleBurst = "10mbps"
|
||||||
|
|
||||||
[outbound]
|
[outbound]
|
||||||
enabled = false
|
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 or "xx.example.com"
|
||||||
```
|
```
|
||||||
|
|
||||||
### 配置项详细说明
|
### 配置项详细说明
|
||||||
@@ -83,6 +98,10 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
|||||||
* 类型: 整数 (`int`)
|
* 类型: 整数 (`int`)
|
||||||
* 默认值: `8080`
|
* 默认值: `8080`
|
||||||
* 说明: 设置 `ghproxy` 监听的端口号。
|
* 说明: 设置 `ghproxy` 监听的端口号。
|
||||||
|
* `netlib`: 底层网络库。
|
||||||
|
* 类型: 字符串 (`string`)
|
||||||
|
* 默认值: `""` (HertZ默认处置)
|
||||||
|
* 说明: `"std"` `"standard"` `"net/http"` `"net"` 均会被设置为go标准库`net/http`, 设置为`"netpoll"`或`""`会由`HertZ`默认逻辑处理
|
||||||
* `sizeLimit`: 请求体大小限制。
|
* `sizeLimit`: 请求体大小限制。
|
||||||
* 类型: 整数 (`int`)
|
* 类型: 整数 (`int`)
|
||||||
* 默认值: `125` (MB)
|
* 默认值: `125` (MB)
|
||||||
@@ -129,6 +148,10 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
|||||||
* 类型: 整数 (`int`)
|
* 类型: 整数 (`int`)
|
||||||
* 默认值: `0` (不限制)
|
* 默认值: `0` (不限制)
|
||||||
* 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。
|
* 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。
|
||||||
|
* `useCustomRawHeaders`: 使用预定义header避免github waf对应zh-CN的封锁
|
||||||
|
* 类型: 布尔值(`bool`)
|
||||||
|
* 默认值: `false`(停用)
|
||||||
|
* 说明: 启用后, 拉取raw文件会使用程序预定义的固定headers, 而不是原先的复制行为
|
||||||
|
|
||||||
* **`[gitclone]` - Git 克隆配置**
|
* **`[gitclone]` - Git 克隆配置**
|
||||||
|
|
||||||
@@ -277,6 +300,27 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
|||||||
* 类型: 整数 (`int`)
|
* 类型: 整数 (`int`)
|
||||||
* 默认值: `5`
|
* 默认值: `5`
|
||||||
* 说明: 允许在短时间内超过 `ratePerMinute` 的突发请求数。
|
* 说明: 允许在短时间内超过 `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]` - 出站代理配置**
|
* **`[outbound]` - 出站代理配置**
|
||||||
|
|
||||||
@@ -290,6 +334,22 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
|||||||
* 支持协议: `socks5://` 和 `http://`
|
* 支持协议: `socks5://` 和 `http://`
|
||||||
* 说明: 设置出站代理服务器的 URL。支持 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` - 黑名单配置
|
||||||
|
|
||||||
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。
|
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Flag
|
# Flag
|
||||||
|
|
||||||
|
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
|
||||||
|
|
||||||
GHProxy接受以下flag传入
|
GHProxy接受以下flag传入
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
## GHProxy 文档
|
## GHProxy 文档
|
||||||
|
|
||||||
|
> 弃用, 请转到 [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/)
|
||||||
|
|
||||||
### 配置文件
|
### 配置文件
|
||||||
|
|
||||||
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md
|
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md
|
||||||
|
|||||||
35
go.mod
35
go.mod
@@ -1,22 +1,28 @@
|
|||||||
module ghproxy
|
module ghproxy
|
||||||
|
|
||||||
go 1.24.2
|
go 1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.5.0
|
github.com/BurntSushi/toml v1.5.0
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0
|
github.com/WJQSERVER-STUDIO/httpc v0.5.1
|
||||||
github.com/cloudwego/hertz v0.9.6
|
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/hertz-contrib/http2 v0.1.8
|
||||||
github.com/satomitouka/touka-httpc v0.3.3
|
golang.org/x/net v0.40.0
|
||||||
golang.org/x/net v0.38.0
|
|
||||||
golang.org/x/time v0.11.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 (
|
require (
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect
|
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/gopkg v0.1.2 // indirect
|
||||||
github.com/bytedance/sonic v1.13.2 // indirect
|
|
||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/cloudwego/gopkg v0.1.4 // indirect
|
github.com/cloudwego/gopkg v0.1.4 // indirect
|
||||||
@@ -24,15 +30,20 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // 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/gjson v1.18.0 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
golang.org/x/arch v0.16.0 // indirect
|
golang.org/x/arch v0.17.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect
|
||||||
golang.org/x/sys v0.32.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.6 // 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
|
||||||
|
|||||||
50
go.sum
50
go.sum
@@ -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/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 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/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/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc5f0DK8N9agIJCpM1Cqw2NE=
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
|
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg=
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c=
|
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.3 h1:t6nyLhmo9pSfVHm1Wu1WyLsTpXFSjSpQtVKqEDpiZ5Q=
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po=
|
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.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 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ=
|
||||||
github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
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 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4=
|
||||||
github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk=
|
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.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
|
||||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
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.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 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
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/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 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50=
|
||||||
github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI=
|
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.10.0 h1:V0vmBaLdQPlgL6w2TA6PZL1g6SGgQznFx6vqxWdCcKw=
|
||||||
github.com/cloudwego/hertz v0.9.6/go.mod h1:X5Ez52XhtszU4t+CTBGIJI4PqmcI1oSf8ULBz0SWfLo=
|
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/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 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4=
|
||||||
github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU=
|
github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU=
|
||||||
@@ -35,6 +39,8 @@ 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/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 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
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 h1:kjfCGkUxJZHgfPsnRjx1FLJBG55KvtvSQD214guBQLw=
|
||||||
github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0=
|
github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0=
|
||||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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/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.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940=
|
||||||
github.com/nyaruka/phonenumbers v1.6.0/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
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/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 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
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=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
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-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.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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
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-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
|
||||||
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/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.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/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=
|
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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
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.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/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.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.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.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
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.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.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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
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 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
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=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|||||||
347
main.go
347
main.go
@@ -17,16 +17,20 @@ import (
|
|||||||
"ghproxy/middleware/loggin"
|
"ghproxy/middleware/loggin"
|
||||||
"ghproxy/proxy"
|
"ghproxy/proxy"
|
||||||
"ghproxy/rate"
|
"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"
|
||||||
"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
|
"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
|
||||||
"github.com/cloudwego/hertz/pkg/app/server"
|
"github.com/cloudwego/hertz/pkg/app/server"
|
||||||
"github.com/cloudwego/hertz/pkg/common/adaptor"
|
"github.com/cloudwego/hertz/pkg/common/adaptor"
|
||||||
"github.com/cloudwego/hertz/pkg/common/hlog"
|
"github.com/cloudwego/hertz/pkg/common/hlog"
|
||||||
|
"github.com/cloudwego/hertz/pkg/network/standard"
|
||||||
|
|
||||||
"github.com/hertz-contrib/http2/factory"
|
_ "net/http/pprof"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -48,6 +52,10 @@ var (
|
|||||||
pagesFS embed.FS
|
pagesFS embed.FS
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
wcache *weakcache.Cache[string] // docker token缓存
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logw = logger.Logw
|
logw = logger.Logw
|
||||||
logDump = logger.LogDump
|
logDump = logger.LogDump
|
||||||
@@ -119,6 +127,7 @@ func loadConfig() {
|
|||||||
|
|
||||||
func setupLogger(cfg *config.Config) {
|
func setupLogger(cfg *config.Config) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
|
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||||
@@ -129,6 +138,8 @@ func setupLogger(cfg *config.Config) {
|
|||||||
fmt.Printf("Logger Level Error: %v\n", err)
|
fmt.Printf("Logger Level Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
logger.SetAsync(cfg.Log.Async)
|
||||||
|
|
||||||
fmt.Printf("Log Level: %s\n", cfg.Log.Level)
|
fmt.Printf("Log Level: %s\n", cfg.Log.Level)
|
||||||
logDebug("Config File Path: ", cfgfile)
|
logDebug("Config File Path: ", cfgfile)
|
||||||
logDebug("Loaded config: %v\n", cfg)
|
logDebug("Loaded config: %v\n", cfg)
|
||||||
@@ -146,6 +157,7 @@ func setupHertZLogger(cfg *config.Config) {
|
|||||||
} else {
|
} else {
|
||||||
hlog.SetOutput(hertZfile)
|
hlog.SetOutput(hertZfile)
|
||||||
}
|
}
|
||||||
|
hlog.SetLevel(hlog.LevelInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -178,37 +190,53 @@ func setupRateLimit(cfg *config.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func InitReq(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 加载嵌入式页面资源
|
// loadEmbeddedPages 加载嵌入式页面资源
|
||||||
func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
|
func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
|
||||||
|
pageFS := modembed.NewModTimeFS(pagesFS, time.Now())
|
||||||
var pages fs.FS
|
var pages fs.FS
|
||||||
var err error
|
var err error
|
||||||
switch cfg.Pages.Theme {
|
switch cfg.Pages.Theme {
|
||||||
case "bootstrap":
|
case "bootstrap":
|
||||||
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
|
pages, err = fs.Sub(pageFS, "pages/bootstrap")
|
||||||
case "nebula":
|
case "nebula":
|
||||||
pages, err = fs.Sub(pagesFS, "pages/nebula")
|
pages, err = fs.Sub(pageFS, "pages/nebula")
|
||||||
case "design":
|
case "design":
|
||||||
pages, err = fs.Sub(pagesFS, "pages/design")
|
pages, err = fs.Sub(pageFS, "pages/design")
|
||||||
case "metro":
|
case "metro":
|
||||||
pages, err = fs.Sub(pagesFS, "pages/metro")
|
pages, err = fs.Sub(pageFS, "pages/metro")
|
||||||
case "classic":
|
case "classic":
|
||||||
pages, err = fs.Sub(pagesFS, "pages/classic")
|
pages, err = fs.Sub(pageFS, "pages/classic")
|
||||||
case "mino":
|
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:
|
default:
|
||||||
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
|
pages, err = fs.Sub(pageFS, "pages/design") // 默认主题
|
||||||
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
|
logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err)
|
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
|
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
|
return pages, assets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +267,6 @@ func setupPages(cfg *config.Config, r *server.Hertz) {
|
|||||||
r.StaticFile("/style.css", stylesheetsPath)
|
r.StaticFile("/style.css", stylesheetsPath)
|
||||||
r.StaticFile("/bootstrap.min.css", bootstrapPath)
|
r.StaticFile("/bootstrap.min.css", bootstrapPath)
|
||||||
r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath)
|
r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath)
|
||||||
//router.StaticFile("/bootstrap.min.css", bootstrapPath)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 处理无效的Pages Mode
|
// 处理无效的Pages Mode
|
||||||
@@ -255,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 {
|
func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
|
||||||
|
|
||||||
// 加载嵌入式资源
|
// 加载嵌入式资源
|
||||||
@@ -263,61 +296,69 @@ func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
|
|||||||
logError("Failed when processing pages: %s", err)
|
logError("Failed when processing pages: %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 设置嵌入式资源路由
|
/*
|
||||||
r.GET("/", func(ctx context.Context, c *app.RequestContext) {
|
// 设置嵌入式资源路由
|
||||||
staticServer := http.FileServer(http.FS(pages))
|
r.GET("/", func(ctx context.Context, c *app.RequestContext) {
|
||||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
staticServer := http.FileServer(http.FS(pages))
|
||||||
if err != nil {
|
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||||
logError("%s", err)
|
if err != nil {
|
||||||
return
|
logError("%s", err)
|
||||||
}
|
return
|
||||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
}
|
||||||
})
|
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||||
r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
|
})
|
||||||
staticServer := http.FileServer(http.FS(pages))
|
r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
|
||||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
staticServer := http.FileServer(http.FS(assets))
|
||||||
if err != nil {
|
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||||
logError("%s", err)
|
if err != nil {
|
||||||
return
|
logError("%s", err)
|
||||||
}
|
return
|
||||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
}
|
||||||
})
|
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||||
r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) {
|
})
|
||||||
staticServer := http.FileServer(http.FS(pages))
|
r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) {
|
||||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
staticServer := http.FileServer(http.FS(pages))
|
||||||
if err != nil {
|
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||||
logError("%s", err)
|
if err != nil {
|
||||||
return
|
logError("%s", err)
|
||||||
}
|
return
|
||||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
}
|
||||||
})
|
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||||
r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) {
|
})
|
||||||
staticServer := http.FileServer(http.FS(pages))
|
r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) {
|
||||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
staticServer := http.FileServer(http.FS(pages))
|
||||||
if err != nil {
|
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||||
logError("%s", err)
|
if err != nil {
|
||||||
return
|
logError("%s", err)
|
||||||
}
|
return
|
||||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
}
|
||||||
})
|
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))
|
r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) {
|
||||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
staticServer := http.FileServer(http.FS(assets))
|
||||||
if err != nil {
|
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||||
logError("%s", err)
|
if err != nil {
|
||||||
return
|
logError("%s", err)
|
||||||
}
|
return
|
||||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
}
|
||||||
})
|
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))
|
r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) {
|
||||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
staticServer := http.FileServer(http.FS(assets))
|
||||||
if err != nil {
|
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||||
logError("%s", err)
|
if err != nil {
|
||||||
return
|
logError("%s", err)
|
||||||
}
|
return
|
||||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
}
|
||||||
})
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +386,9 @@ func init() {
|
|||||||
setMemLimit(cfg)
|
setMemLimit(cfg)
|
||||||
loadlist(cfg)
|
loadlist(cfg)
|
||||||
setupRateLimit(cfg)
|
setupRateLimit(cfg)
|
||||||
|
if cfg.Docker.Enabled {
|
||||||
|
wcache = proxy.InitWeakCache()
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Server.Debug {
|
if cfg.Server.Debug {
|
||||||
runMode = "dev"
|
runMode = "dev"
|
||||||
@@ -359,80 +403,118 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 如果 showVersion 为 true,则在 init 阶段已退出,这里直接返回
|
|
||||||
if showVersion || showHelp {
|
if showVersion || showHelp {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logDebug("Run Mode: %s", runMode)
|
logDebug("Run Mode: %s Netlib: %s", runMode, cfg.Server.NetLib)
|
||||||
|
|
||||||
// 确保在程序配置加载且非版本显示模式下执行
|
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
fmt.Println("Config not loaded, exiting.")
|
fmt.Println("Config not loaded, exiting.")
|
||||||
return // 如果配置未加载,则不继续执行
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
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 {
|
if cfg.Server.H2C {
|
||||||
r = server.New(
|
r = server.New(
|
||||||
server.WithHostPorts(addr),
|
server.WithH2C(true),
|
||||||
server.WithH2C(true),
|
server.WithHostPorts(addr),
|
||||||
)
|
server.WithTransport(standard.NewTransporter),
|
||||||
r.AddProtocol("h2", factory.NewServerFactory())
|
)
|
||||||
|
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 {
|
} else {
|
||||||
r = server.New(
|
logError("Invalid NetLib: %s", cfg.Server.NetLib)
|
||||||
server.WithHostPorts(addr),
|
fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib)
|
||||||
)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加Recovery中间件
|
r.Use(recovery.Recovery()) // Recovery中间件
|
||||||
r.Use(recovery.Recovery())
|
r.Use(loggin.Middleware()) // log中间件
|
||||||
// 添加log中间件
|
|
||||||
r.Use(loggin.Middleware())
|
|
||||||
|
|
||||||
setupApi(cfg, r, version)
|
setupApi(cfg, r, version)
|
||||||
|
|
||||||
setupPages(cfg, r)
|
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.Any("/v2/:target/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for releases
|
proxy.GhcrRouting(cfg)(ctx, c)
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -444,7 +526,15 @@ func main() {
|
|||||||
fmt.Printf("A Go Based High-Performance Github Proxy \n")
|
fmt.Printf("A Go Based High-Performance Github Proxy \n")
|
||||||
fmt.Printf("Made by WJQSERVER-STUDIO\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 logger.Close()
|
||||||
defer func() {
|
defer func() {
|
||||||
if hertZfile != nil {
|
if hertZfile != nil {
|
||||||
@@ -454,5 +544,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
r.Spin()
|
||||||
|
|
||||||
fmt.Println("Program Exit")
|
fmt.Println("Program Exit")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
"github.com/WJQSERVER-STUDIO/logger"
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
62
proxy/authparse.go
Normal file
62
proxy/authparse.go
Normal 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
|
||||||
|
}
|
||||||
@@ -18,8 +18,7 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques
|
|||||||
req.Header.Set("Authorization", "token "+token)
|
req.Header.Set("Authorization", "token "+token)
|
||||||
} else {
|
} 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())
|
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
|
ErrorPage(c, NewErrorWithStatusLookup(500, "Conflict Auth Method"))
|
||||||
c.JSON(http.StatusInternalServerError, map[string]string{"error": "Conflict Auth Method"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "header":
|
case "header":
|
||||||
@@ -28,8 +27,7 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques
|
|||||||
}
|
}
|
||||||
default:
|
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())
|
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
|
ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid Auth Method / Auth Method is not be set"))
|
||||||
c.JSON(http.StatusInternalServerError, map[string]string{"error": "Invalid Auth Method / Auth Method is not be set"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
proxy/bandwidth.go
Normal file
64
proxy/bandwidth.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
@@ -9,59 +8,43 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
|
func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
|
||||||
method := c.Request.Method
|
|
||||||
|
|
||||||
// 发送HEAD请求, 预获取Content-Length
|
var (
|
||||||
headReq, err := client.NewRequest("HEAD", u, nil)
|
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 {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setRequestHeaders(c, headReq)
|
|
||||||
removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
|
|
||||||
AuthPassThrough(c, cfg, headReq)
|
|
||||||
|
|
||||||
headResp, err := client.Do(headReq)
|
setRequestHeaders(c, req, cfg, matcher)
|
||||||
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头)
|
|
||||||
AuthPassThrough(c, cfg, req)
|
AuthPassThrough(c, cfg, req)
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err = client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
return
|
return
|
||||||
@@ -69,38 +52,45 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
|
|||||||
|
|
||||||
// 错误处理(404)
|
// 错误处理(404)
|
||||||
if resp.StatusCode == 404 {
|
if resp.StatusCode == 404 {
|
||||||
c.String(http.StatusNotFound, "File Not Found")
|
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
||||||
//c.Status(http.StatusNotFound)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bodySize int
|
||||||
|
contentLength string
|
||||||
|
sizelimit int
|
||||||
|
)
|
||||||
|
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
|
||||||
contentLength = resp.Header.Get("Content-Length")
|
contentLength = resp.Header.Get("Content-Length")
|
||||||
if contentLength != "" {
|
if contentLength != "" {
|
||||||
size, err := strconv.Atoi(contentLength)
|
var err error
|
||||||
if err == nil && size > sizelimit {
|
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()
|
finalURL := resp.Request.URL.String()
|
||||||
c.Redirect(http.StatusMovedPermanently, []byte(finalURL))
|
err = resp.Body.Close()
|
||||||
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)
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复制响应头,排除需要移除的 header
|
||||||
for key, values := range resp.Header {
|
for key, values := range resp.Header {
|
||||||
for _, value := range values {
|
if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove {
|
||||||
c.Header(key, value)
|
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 {
|
switch cfg.Server.Cors {
|
||||||
case "*":
|
case "*":
|
||||||
c.Header("Access-Control-Allow-Origin", "*")
|
c.Header("Access-Control-Allow-Origin", "*")
|
||||||
@@ -114,6 +104,12 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
|
|||||||
|
|
||||||
c.Status(resp.StatusCode)
|
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 {
|
if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor {
|
||||||
// 判断body是不是gzip
|
// 判断body是不是gzip
|
||||||
var compress string
|
var compress string
|
||||||
@@ -121,18 +117,25 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
|
|||||||
compress = "gzip"
|
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", "")
|
c.Header("Content-Length", "")
|
||||||
|
|
||||||
reader, _, err := processLinks(resp.Body, compress, string(c.Request.Host()), cfg)
|
var reader io.Reader
|
||||||
c.SetBodyStream(reader, -1)
|
|
||||||
|
|
||||||
|
reader, _, err = processLinks(bodyReader, compress, string(c.Request.Host()), cfg)
|
||||||
|
c.SetBodyStream(reader, -1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.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
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.SetBodyStream(resp.Body, -1)
|
|
||||||
|
if contentLength != "" {
|
||||||
|
c.SetBodyStream(bodyReader, bodySize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetBodyStream(bodyReader, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
328
proxy/docker.go
Normal file
328
proxy/docker.go
Normal 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
|
||||||
|
|
||||||
|
}
|
||||||
314
proxy/error.go
314
proxy/error.go
@@ -1,10 +1,19 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
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"
|
"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) {
|
func HandleError(c *app.RequestContext, message string) {
|
||||||
c.JSON(http.StatusInternalServerError, map[string]string{"error": message})
|
ErrorPage(c, NewErrorWithStatusLookup(500, message))
|
||||||
logError(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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,13 +8,33 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) {
|
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())
|
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" {
|
if cfg.GitClone.Mode == "cache" {
|
||||||
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
|
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -23,27 +43,21 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
|
|||||||
}
|
}
|
||||||
// 构建新url
|
// 构建新url
|
||||||
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
|
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" {
|
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 {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setRequestHeaders(c, req)
|
|
||||||
removeWSHeader(req)
|
setRequestHeaders(c, req, cfg, "clone")
|
||||||
AuthPassThrough(c, cfg, req)
|
AuthPassThrough(c, cfg, req)
|
||||||
|
|
||||||
resp, err = gitclient.Do(req)
|
resp, err = gitclient.Do(req)
|
||||||
@@ -52,13 +66,18 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setRequestHeaders(c, req)
|
|
||||||
removeWSHeader(req)
|
setRequestHeaders(c, req, cfg, "clone")
|
||||||
AuthPassThrough(c, cfg, req)
|
AuthPassThrough(c, cfg, req)
|
||||||
|
|
||||||
resp, err = client.Do(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 != "" {
|
if contentLength != "" {
|
||||||
size, err := strconv.Atoi(contentLength)
|
size, err := strconv.Atoi(contentLength)
|
||||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
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 {
|
if err == nil && size > sizelimit {
|
||||||
finalURL := []byte(resp.Request.URL.String())
|
finalURL := []byte(resp.Request.URL.String())
|
||||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
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 key, values := range resp.Header {
|
||||||
for _, value := range values {
|
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.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)
|
||||||
}
|
}
|
||||||
|
|||||||
117
proxy/handler.go
117
proxy/handler.go
@@ -2,12 +2,9 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"ghproxy/auth"
|
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
"ghproxy/rate"
|
"ghproxy/rate"
|
||||||
"net/http"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"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 {
|
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc {
|
||||||
return func(ctx context.Context, c *app.RequestContext) {
|
return func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
|
||||||
// 限制访问频率
|
var shoudBreak bool
|
||||||
if cfg.RateLimit.Enabled {
|
shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
|
||||||
|
if shoudBreak {
|
||||||
var allowed bool
|
return
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rawPath := strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
|
var (
|
||||||
matches := re.FindStringSubmatch(rawPath) // 匹配路径
|
rawPath string
|
||||||
logInfo("URL: %v", matches)
|
matches []string
|
||||||
|
)
|
||||||
|
|
||||||
|
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
|
||||||
|
matches = re.FindStringSubmatch(rawPath) // 匹配路径
|
||||||
|
|
||||||
// 匹配路径错误处理
|
// 匹配路径错误处理
|
||||||
if len(matches) < 3 {
|
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("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||||
logWarning(errMsg)
|
ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.Path())))
|
||||||
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 制作url
|
// 制作url
|
||||||
rawPath = "https://" + matches[2]
|
rawPath = "https://" + matches[2]
|
||||||
|
|
||||||
user, repo, matcher, err := Matcher(rawPath, cfg)
|
var (
|
||||||
if err != nil {
|
user string
|
||||||
if errors.Is(err, ErrInvalidURL) {
|
repo string
|
||||||
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
|
matcher string
|
||||||
logWarning(err.Error())
|
)
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.Is(err, ErrAuthHeaderUnavailable) {
|
|
||||||
c.String(http.StatusForbidden, "AuthHeader Unavailable")
|
|
||||||
logWarning(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
username := user
|
|
||||||
|
|
||||||
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), username, repo)
|
var matcherErr *GHProxyErrors
|
||||||
// dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header
|
user, repo, matcher, matcherErr = Matcher(rawPath, cfg)
|
||||||
logDump("%s %s %s %s %s %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header())
|
if matcherErr != nil {
|
||||||
repouser := fmt.Sprintf("%s/%s", username, repo)
|
ErrorPage(c, matcherErr)
|
||||||
|
return
|
||||||
// 白名单检查
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 黑名单检查
|
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)
|
||||||
if cfg.Blacklist.Enabled {
|
logDump("%s", c.Request.Header.Header())
|
||||||
blacklist := auth.CheckBlacklist(username, repo)
|
|
||||||
if blacklist {
|
shoudBreak = listCheck(cfg, c, user, repo, rawPath)
|
||||||
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
|
if shoudBreak {
|
||||||
c.JSON(http.StatusForbidden, map[string]string{"error": errMsg})
|
return
|
||||||
logWarning("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth
|
shoudBreak = authCheck(c, cfg, matcher, rawPath)
|
||||||
|
if shoudBreak {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 处理blob/raw路径
|
// 处理blob/raw路径
|
||||||
if matcher == "blob" {
|
if matcher == "blob" {
|
||||||
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鉴权
|
logDebug("Matched: %v", matcher)
|
||||||
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)
|
|
||||||
|
|
||||||
switch matcher {
|
switch matcher {
|
||||||
case "releases", "blob", "raw", "gist", "api":
|
case "releases", "blob", "raw", "gist", "api":
|
||||||
@@ -124,8 +79,8 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
|
|||||||
case "clone":
|
case "clone":
|
||||||
GitReq(ctx, c, rawPath, cfg, "git")
|
GitReq(ctx, c, rawPath, cfg, "git")
|
||||||
default:
|
default:
|
||||||
c.String(http.StatusForbidden, "Invalid input.")
|
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
|
||||||
fmt.Println("Invalid input.")
|
logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
111
proxy/httpc.go
111
proxy/httpc.go
@@ -4,10 +4,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
httpc "github.com/satomitouka/touka-httpc"
|
"github.com/WJQSERVER-STUDIO/httpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var BufferSize int = 32 * 1024 // 32KB
|
var BufferSize int = 32 * 1024 // 32KB
|
||||||
@@ -15,23 +14,24 @@ var BufferSize int = 32 * 1024 // 32KB
|
|||||||
var (
|
var (
|
||||||
tr *http.Transport
|
tr *http.Transport
|
||||||
gittr *http.Transport
|
gittr *http.Transport
|
||||||
BufferPool *sync.Pool
|
|
||||||
client *httpc.Client
|
client *httpc.Client
|
||||||
gitclient *httpc.Client
|
gitclient *httpc.Client
|
||||||
|
ghcrtr *http.Transport
|
||||||
|
ghcrclient *httpc.Client
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitReq(cfg *config.Config) {
|
func InitReq(cfg *config.Config) error {
|
||||||
initHTTPClient(cfg)
|
initHTTPClient(cfg)
|
||||||
if cfg.GitClone.Mode == "cache" {
|
if cfg.GitClone.Mode == "cache" {
|
||||||
initGitHTTPClient(cfg)
|
initGitHTTPClient(cfg)
|
||||||
}
|
}
|
||||||
|
initGhcrHTTPClient(cfg)
|
||||||
// 初始化固定大小的缓存池
|
err := SetGlobalRateLimit(cfg)
|
||||||
BufferPool = &sync.Pool{
|
if err != nil {
|
||||||
New: func() interface{} {
|
return err
|
||||||
return make([]byte, BufferSize)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initHTTPClient(cfg *config.Config) {
|
func initHTTPClient(cfg *config.Config) {
|
||||||
@@ -42,7 +42,6 @@ func initHTTPClient(cfg *config.Config) {
|
|||||||
if cfg.Httpc.Mode == "auto" {
|
if cfg.Httpc.Mode == "auto" {
|
||||||
|
|
||||||
tr = &http.Transport{
|
tr = &http.Transport{
|
||||||
//MaxIdleConns: 160,
|
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
WriteBufferSize: 32 * 1024, // 32KB
|
WriteBufferSize: 32 * 1024, // 32KB
|
||||||
ReadBufferSize: 32 * 1024, // 32KB
|
ReadBufferSize: 32 * 1024, // 32KB
|
||||||
@@ -64,7 +63,6 @@ func initHTTPClient(cfg *config.Config) {
|
|||||||
logWarning("use Auto to Run HTTP Client")
|
logWarning("use Auto to Run HTTP Client")
|
||||||
fmt.Println("use Auto to Run HTTP Client")
|
fmt.Println("use Auto to Run HTTP Client")
|
||||||
tr = &http.Transport{
|
tr = &http.Transport{
|
||||||
//MaxIdleConns: 160,
|
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
WriteBufferSize: 32 * 1024, // 32KB
|
WriteBufferSize: 32 * 1024, // 32KB
|
||||||
ReadBufferSize: 32 * 1024, // 32KB
|
ReadBufferSize: 32 * 1024, // 32KB
|
||||||
@@ -83,27 +81,16 @@ func initHTTPClient(cfg *config.Config) {
|
|||||||
httpc.WithTransport(tr),
|
httpc.WithTransport(tr),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initGitHTTPClient(cfg *config.Config) {
|
func initGitHTTPClient(cfg *config.Config) {
|
||||||
|
|
||||||
var proTolcols = new(http.Protocols)
|
|
||||||
proTolcols.SetHTTP1(true)
|
|
||||||
proTolcols.SetHTTP2(true)
|
|
||||||
proTolcols.SetUnencryptedHTTP2(true)
|
|
||||||
if cfg.GitClone.ForceH2C {
|
|
||||||
proTolcols.SetHTTP1(false)
|
|
||||||
proTolcols.SetHTTP2(false)
|
|
||||||
proTolcols.SetUnencryptedHTTP2(true)
|
|
||||||
}
|
|
||||||
if cfg.Httpc.Mode == "auto" {
|
if cfg.Httpc.Mode == "auto" {
|
||||||
|
|
||||||
gittr = &http.Transport{
|
gittr = &http.Transport{
|
||||||
//MaxIdleConns: 160,
|
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
WriteBufferSize: 32 * 1024, // 32KB
|
WriteBufferSize: 32 * 1024, // 32KB
|
||||||
ReadBufferSize: 32 * 1024, // 32KB
|
ReadBufferSize: 32 * 1024, // 32KB
|
||||||
Protocols: proTolcols,
|
|
||||||
}
|
}
|
||||||
} else if cfg.Httpc.Mode == "advanced" {
|
} else if cfg.Httpc.Mode == "advanced" {
|
||||||
gittr = &http.Transport{
|
gittr = &http.Transport{
|
||||||
@@ -112,7 +99,6 @@ func initGitHTTPClient(cfg *config.Config) {
|
|||||||
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||||
WriteBufferSize: 32 * 1024, // 32KB
|
WriteBufferSize: 32 * 1024, // 32KB
|
||||||
ReadBufferSize: 32 * 1024, // 32KB
|
ReadBufferSize: 32 * 1024, // 32KB
|
||||||
Protocols: proTolcols,
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 错误的模式
|
// 错误的模式
|
||||||
@@ -130,14 +116,87 @@ func initGitHTTPClient(cfg *config.Config) {
|
|||||||
if cfg.Outbound.Enabled {
|
if cfg.Outbound.Enabled {
|
||||||
initTransport(cfg, gittr)
|
initTransport(cfg, gittr)
|
||||||
}
|
}
|
||||||
if cfg.Server.Debug {
|
if cfg.Server.Debug && cfg.GitClone.ForceH2C {
|
||||||
gitclient = httpc.New(
|
gitclient = httpc.New(
|
||||||
httpc.WithTransport(gittr),
|
httpc.WithTransport(gittr),
|
||||||
httpc.WithDumpLog(),
|
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 {
|
} else {
|
||||||
gitclient = httpc.New(
|
gitclient = httpc.New(
|
||||||
httpc.WithTransport(gittr),
|
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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
246
proxy/match.go
246
proxy/match.go
@@ -1,46 +1,14 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"compress/gzip"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
"io"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 定义错误类型, error承载描述, 便于处理
|
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
|
||||||
type MatcherErrors struct {
|
|
||||||
Code int
|
|
||||||
Msg string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidURL = &MatcherErrors{
|
|
||||||
Code: 403,
|
|
||||||
Msg: "Invalid URL Format",
|
|
||||||
}
|
|
||||||
ErrAuthHeaderUnavailable = &MatcherErrors{
|
|
||||||
Code: 403,
|
|
||||||
Msg: "AuthHeader Unavailable",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func (e *MatcherErrors) Error() string {
|
|
||||||
if e.Err != nil {
|
|
||||||
return fmt.Sprintf("Code: %d, Msg: %s, Err: %s", e.Code, e.Msg, e.Err.Error())
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("Code: %d, Msg: %s", e.Code, e.Msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *MatcherErrors) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) {
|
|
||||||
var (
|
var (
|
||||||
user string
|
user string
|
||||||
repo string
|
repo string
|
||||||
@@ -49,14 +17,18 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error)
|
|||||||
// 匹配 "https://github.com"开头的链接
|
// 匹配 "https://github.com"开头的链接
|
||||||
if strings.HasPrefix(rawPath, "https://github.com") {
|
if strings.HasPrefix(rawPath, "https://github.com") {
|
||||||
remainingPath := strings.TrimPrefix(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/more...
|
||||||
// 取出user和repo和最后部分
|
// 取出user和repo和最后部分
|
||||||
parts := strings.Split(remainingPath, "/")
|
parts := strings.Split(remainingPath, "/")
|
||||||
if len(parts) <= 2 {
|
if len(parts) <= 2 {
|
||||||
return "", "", "", ErrInvalidURL
|
errMsg := "Not enough parts in path after matching 'https://github.com*'"
|
||||||
|
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
|
||||||
}
|
}
|
||||||
user = parts[0]
|
user = parts[0]
|
||||||
repo = parts[1]
|
repo = parts[1]
|
||||||
@@ -65,12 +37,15 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error)
|
|||||||
switch parts[2] {
|
switch parts[2] {
|
||||||
case "releases", "archive":
|
case "releases", "archive":
|
||||||
matcher = "releases"
|
matcher = "releases"
|
||||||
case "blob", "raw":
|
case "blob":
|
||||||
matcher = "blob"
|
matcher = "blob"
|
||||||
|
case "raw":
|
||||||
|
matcher = "raw"
|
||||||
case "info", "git-upload-pack":
|
case "info", "git-upload-pack":
|
||||||
matcher = "clone"
|
matcher = "clone"
|
||||||
default:
|
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
|
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://")
|
remainingPath := strings.TrimPrefix(rawPath, "https://")
|
||||||
parts := strings.Split(remainingPath, "/")
|
parts := strings.Split(remainingPath, "/")
|
||||||
if len(parts) <= 3 {
|
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]
|
user = parts[1]
|
||||||
repo = parts[2]
|
repo = parts[2]
|
||||||
@@ -93,7 +69,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error)
|
|||||||
remainingPath := strings.TrimPrefix(rawPath, "https://")
|
remainingPath := strings.TrimPrefix(rawPath, "https://")
|
||||||
parts := strings.Split(remainingPath, "/")
|
parts := strings.Split(remainingPath, "/")
|
||||||
if len(parts) <= 3 {
|
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]
|
user = parts[1]
|
||||||
repo = ""
|
repo = ""
|
||||||
@@ -115,82 +92,16 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error)
|
|||||||
}
|
}
|
||||||
if !cfg.Auth.ForceAllowApi {
|
if !cfg.Auth.ForceAllowApi {
|
||||||
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
|
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 user, repo, matcher, nil
|
||||||
}
|
}
|
||||||
return "", "", "", ErrInvalidURL
|
//return "", "", "", ErrNotFound
|
||||||
}
|
errMsg := "Didn't match any matcher"
|
||||||
|
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -243,111 +154,4 @@ func extractParts(rawURL string) (string, string, string, url.Values, error) {
|
|||||||
return repoOwner, repoName, remainingPath, queryParams, nil
|
return repoOwner, repoName, remainingPath, queryParams, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processLinks 处理链接,返回包含处理后数据的 io.Reader
|
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)
|
||||||
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 和 written,error 由 Goroutine 通过 pipeWriter.CloseWithError 传递
|
|
||||||
}
|
|
||||||
|
|||||||
185
proxy/nest.go
Normal file
185
proxy/nest.go
Normal 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 和 written,error 由 Goroutine 通过 pipeWriter.CloseWithError 传递
|
||||||
|
}
|
||||||
@@ -1,19 +1,75 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"ghproxy/config"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 设置请求头
|
var (
|
||||||
func setRequestHeaders(c *app.RequestContext, req *http.Request) {
|
respHeadersToRemove = map[string]struct{}{
|
||||||
c.Request.Header.VisitAll(func(key, value []byte) {
|
"Content-Security-Policy": {},
|
||||||
req.Header.Set(string(key), string(value))
|
"Referrer-Policy": {},
|
||||||
})
|
"Strict-Transport-Security": {},
|
||||||
}
|
"X-Github-Request-Id": {},
|
||||||
|
"X-Timer": {},
|
||||||
|
"X-Served-By": {},
|
||||||
|
"X-Fastly-Request-Id": {},
|
||||||
|
}
|
||||||
|
|
||||||
func removeWSHeader(req *http.Request) {
|
reqHeadersToRemove = map[string]struct{}{
|
||||||
req.Header.Del("Upgrade")
|
"CF-IPCountry": {},
|
||||||
req.Header.Del("Connection")
|
"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
72
proxy/routing.go
Normal 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
90
proxy/utils.go
Normal 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
|
||||||
|
}
|
||||||
84
rate/rate.go
84
rate/rate.go
@@ -1,13 +1,14 @@
|
|||||||
package rate
|
package rate
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
"github.com/WJQSERVER-STUDIO/logger"
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 日志输出
|
// 日志模块
|
||||||
var (
|
var (
|
||||||
logw = logger.Logw
|
logw = logger.Logw
|
||||||
logDump = logger.LogDump
|
logDump = logger.LogDump
|
||||||
@@ -17,49 +18,90 @@ var (
|
|||||||
logError = logger.LogError
|
logError = logger.LogError
|
||||||
)
|
)
|
||||||
|
|
||||||
// 总体限流器
|
// RateLimiter 总体限流器
|
||||||
type RateLimiter struct {
|
type RateLimiter struct {
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
}
|
}
|
||||||
|
|
||||||
// 基于IP的限流器
|
// New 创建一个总体限流器
|
||||||
type IPRateLimiter struct {
|
|
||||||
limiters map[string]*RateLimiter
|
|
||||||
limit int
|
|
||||||
burst int
|
|
||||||
duration time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(limit int, burst int, duration time.Duration) *RateLimiter {
|
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{
|
return &RateLimiter{
|
||||||
limiter: rate.NewLimiter(rate.Limit(float64(limit)/duration.Seconds()), burst),
|
limiter: rate.NewLimiter(rateLimit, burst),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow 检查是否允许请求通过
|
||||||
func (rl *RateLimiter) Allow() bool {
|
func (rl *RateLimiter) Allow() bool {
|
||||||
return rl.limiter.Allow()
|
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{
|
return &IPRateLimiter{
|
||||||
limiters: make(map[string]*RateLimiter),
|
limiters: make(map[string]*RateLimiter),
|
||||||
limit: limit,
|
limit: ipLimit,
|
||||||
burst: burst,
|
burst: ipBurst,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow 检查给定IP的请求是否允许通过
|
||||||
func (rl *IPRateLimiter) Allow(ip string) bool {
|
func (rl *IPRateLimiter) Allow(ip string) bool {
|
||||||
if ip == "" {
|
if ip == "" {
|
||||||
logWarning("empty ip")
|
logWarning("empty ip for rate limiting")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
limiter, ok := rl.limiters[ip]
|
// 使用读锁快速查找
|
||||||
if !ok {
|
rl.mu.RLock()
|
||||||
// 创建新的 RateLimiter 并存储
|
limiter, found := rl.limiters[ip]
|
||||||
limiter = New(rl.limit, rl.burst, rl.duration)
|
rl.mu.RUnlock()
|
||||||
rl.limiters[ip] = limiter
|
|
||||||
|
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()
|
return limiter.Allow()
|
||||||
}
|
}
|
||||||
|
|||||||
258
weakcache/weakcache.go
Normal file
258
weakcache/weakcache.go
Normal 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 退出
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user