Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
152fb8aa71 | ||
|
|
3e9e43cd44 | ||
|
|
8a50b311fc | ||
|
|
dcc50401c4 | ||
|
|
d62a1f9769 |
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: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ demo.toml
|
|||||||
*.bak
|
*.bak
|
||||||
list.json
|
list.json
|
||||||
repos
|
repos
|
||||||
pages
|
pages
|
||||||
|
*_test
|
||||||
210
CHANGELOG.md
210
CHANGELOG.md
@@ -1,5 +1,215 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
3.3.1 - 2025-05-16
|
||||||
|
- CHANGE: 为`target`放宽限制, 支持自定义
|
||||||
|
- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0`
|
||||||
|
|
||||||
|
25w37a - 2025-05-16
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.3.1预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 为`target`放宽限制, 支持自定义
|
||||||
|
- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0`
|
||||||
|
|
||||||
|
3.3.0 - 2025-05-15
|
||||||
|
---
|
||||||
|
- CHANGE: 为`httpc`加入`request builder`的`withcontext`选项
|
||||||
|
- ADD: 加入带宽限制功能
|
||||||
|
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
|
||||||
|
|
||||||
|
25w36d - 2025-05-14
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
|
||||||
|
|
||||||
|
25w36c - 2025-05-14
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- ADD: 加入带宽限制功能
|
||||||
|
- CHANGE: 将`httpc`切换回主分支, `25w36b`测试的部分已被合入`httpc`主线
|
||||||
|
|
||||||
|
25w36b - 2025-05-13
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: `httpc`切换到`dev`, 测试在retry前检查ctx状态
|
||||||
|
|
||||||
|
25w36a - 2025-05-13
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 为`httpc`加入`request builder`的`withcontext`选项
|
||||||
|
|
||||||
|
3.2.4 - 2025-05-13
|
||||||
|
---
|
||||||
|
- CHANGE: 移除未使用的变量与相关计算
|
||||||
|
|
||||||
|
25w35a - 2025-05-12
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.2.4预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 移除未使用的变量与相关计算
|
||||||
|
|
||||||
|
3.2.3 - 2025-05-07
|
||||||
|
---
|
||||||
|
- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录
|
||||||
|
- CHANGE: 更新Go版本到go1.24.3
|
||||||
|
- CHANGE: 迁移httpc到新的仓库
|
||||||
|
|
||||||
|
25w34b - 2025-05-07
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 更新Go版本到go1.24.3
|
||||||
|
|
||||||
|
25w34a - 2025-05-05
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录
|
||||||
|
- CHANGE: 迁移httpc到新的仓库
|
||||||
|
|
||||||
|
3.2.2 - 2025-04-29
|
||||||
|
---
|
||||||
|
- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制
|
||||||
|
- FIX: 修正Header部分的一些处理问题
|
||||||
|
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
|
||||||
|
|
||||||
|
25w33b - 2025-04-29
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.2.2预发布版本,请勿在生产环境中使用;
|
||||||
|
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
|
||||||
|
|
||||||
|
25w33a - 2025-04-29
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.2.2预发布版本,请勿在生产环境中使用;
|
||||||
|
- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制
|
||||||
|
- FIX: 修正Header部分的一些处理问题
|
||||||
|
|
||||||
|
3.2.1 - 2025-04-29
|
||||||
|
---
|
||||||
|
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
|
||||||
|
|
||||||
|
25w32a - 2025-04-29
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.2.1预发布版本,请勿在生产环境中使用;
|
||||||
|
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
|
||||||
|
|
||||||
|
3.2.0 - 2025-04-27
|
||||||
|
---
|
||||||
|
- CHANGE: 加入`ghcr`和`dockerhub`反代功能
|
||||||
|
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
|
||||||
|
|
||||||
|
25w31a - 2025-04-27
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.2.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 加入`ghcr`和`dockerhub`反代功能
|
||||||
|
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
|
||||||
|
|
||||||
|
3.1.0 - 2025-04-24
|
||||||
|
---
|
||||||
|
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率
|
||||||
|
- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`
|
||||||
|
- CHANGE: 使用`HertZ`的`requestContext`传递matcher参数, 而不是`25w30a`中的ctx
|
||||||
|
- CHANGE: 改进`rate`模块, 避免并发竞争问题
|
||||||
|
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
|
||||||
|
- CHANGE: 修改部分log等级
|
||||||
|
- FIX: 修正默认配置的填充错误
|
||||||
|
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
|
||||||
|
- CHANGE: 改进handle, 复用共同部分
|
||||||
|
- CHANGE: 细化url匹配的返回码处理
|
||||||
|
- CHANGE: 增加404界面
|
||||||
|
|
||||||
|
25w30e - 2025-04-24
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 改进`rate`模块, 避免并发竞争问题
|
||||||
|
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
|
||||||
|
- CHANGE: 修改部分log等级
|
||||||
|
- FIX: 修正默认配置的填充错误
|
||||||
|
|
||||||
|
25w30d - 2025-04-22
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
|
||||||
|
|
||||||
|
25w30c - 2025-04-21
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 改进handle, 复用共同部分
|
||||||
|
- CHANGE: 细化url匹配的返回码处理
|
||||||
|
- CHANGE: 增加404界面
|
||||||
|
|
||||||
|
25w30b - 2025-04-21
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`
|
||||||
|
- CHANGE: 使用`HertZ`的`requestContext`传递matcher参数, 而不是`25w30a`中的标准ctx
|
||||||
|
|
||||||
|
25w30a - 2025-04-19
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器
|
||||||
|
|
||||||
|
3.0.3 - 2025-04-19
|
||||||
|
---
|
||||||
|
- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息
|
||||||
|
- FIX: 修正非预期的header操作行为
|
||||||
|
- CHANGE: 合并header相关逻辑, 避免多次操作
|
||||||
|
- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理
|
||||||
|
- CHANGE: 增加`netlib`配置项
|
||||||
|
|
||||||
|
25w29b - 2025-04-19
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 增加`netlib`配置项
|
||||||
|
|
||||||
|
25w29a - 2025-04-17
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息
|
||||||
|
- FIX: 修正非预期的header操作行为
|
||||||
|
- CHANGE: 合并header相关逻辑, 避免多次操作
|
||||||
|
- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理
|
||||||
|
|
||||||
|
3.0.2 - 2025-04-15
|
||||||
|
---
|
||||||
|
- CHANGE: 避免重复的re编译操作
|
||||||
|
- CHANGE: 去除不必要的请求
|
||||||
|
- CHANGE: 改进`httpc`相关配置
|
||||||
|
- CHANGE: 更新`httpc` 0.4.0
|
||||||
|
- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进
|
||||||
|
|
||||||
|
25w28b - 2025-04-15
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.0.2预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 改进resp关闭
|
||||||
|
- CHANGE: 避免重复的re编译操作
|
||||||
|
|
||||||
|
25w28a - 2025-04-14
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 去除不必要的请求
|
||||||
|
- CHANGE: 改进`httpc`相关配置
|
||||||
|
- CHANGE: 合入test版本修改
|
||||||
|
|
||||||
|
25w28t-2 - 2025-04-11
|
||||||
|
---
|
||||||
|
- TEST: 测试验证版本
|
||||||
|
- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进
|
||||||
|
|
||||||
|
25w28t-1 - 2025-04-11
|
||||||
|
---
|
||||||
|
- TEST: 测试验证版本
|
||||||
|
- CHANGE: 更新httpc 0.4.0
|
||||||
|
|
||||||
|
3.0.1 - 2025-04-08
|
||||||
|
---
|
||||||
|
- CHANGE: 加入`memLimit`指示gc
|
||||||
|
- CHANGE: 加入`hlog`输出路径配置
|
||||||
|
- CHANGE: 修正H2C配置问题
|
||||||
|
|
||||||
|
25w27a - 2025-04-07
|
||||||
|
---
|
||||||
|
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
|
||||||
|
- CHANGE: 加入`memLimit`指示gc
|
||||||
|
- CHANGE: 加入`hlog`输出路径配置
|
||||||
|
- CHANGE: 修正H2C配置问题
|
||||||
|
|
||||||
3.0.0 - 2025-04-04
|
3.0.0 - 2025-04-04
|
||||||
---
|
---
|
||||||
- RELEASE: Next Gen; 下一个起点;
|
- RELEASE: Next Gen; 下一个起点;
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
25w26a
|
25w37a
|
||||||
12
README.md
12
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带来的优势), 同时支持多种额外功能
|
||||||
|
|
||||||
## 项目说明
|
## 项目说明
|
||||||
|
|
||||||
@@ -16,9 +21,10 @@
|
|||||||
- 🚫 **支持自定义黑名单/白名单**
|
- 🚫 **支持自定义黑名单/白名单**
|
||||||
- 🗄️ **支持 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 脚本多层嵌套加速**
|
||||||
|
|
||||||
### 项目相关
|
### 项目相关
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,20 +18,27 @@ 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"`
|
||||||
|
NetLib string `toml:"netlib"`
|
||||||
SizeLimit int `toml:"sizeLimit"`
|
SizeLimit int `toml:"sizeLimit"`
|
||||||
|
MemLimit int64 `toml:"memLimit"`
|
||||||
H2C bool `toml:"H2C"`
|
H2C bool `toml:"H2C"`
|
||||||
Cors string `toml:"cors"`
|
Cors string `toml:"cors"`
|
||||||
Debug bool `toml:"debug"`
|
Debug bool `toml:"debug"`
|
||||||
@@ -43,12 +50,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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -86,9 +95,10 @@ type PagesConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LogConfig struct {
|
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"`
|
||||||
|
HertZLogPath string `toml:"hertzLogPath"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -119,11 +129,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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -136,6 +170,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) {
|
||||||
@@ -178,7 +222,9 @@ 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,
|
||||||
H2C: true,
|
H2C: true,
|
||||||
Cors: "*",
|
Cors: "*",
|
||||||
Debug: false,
|
Debug: false,
|
||||||
@@ -204,9 +250,10 @@ func DefaultConfig() *Config {
|
|||||||
StaticDir: "/data/www",
|
StaticDir: "/data/www",
|
||||||
},
|
},
|
||||||
Log: LogConfig{
|
Log: LogConfig{
|
||||||
LogFilePath: "/data/ghproxy/log/ghproxy.log",
|
LogFilePath: "/data/ghproxy/log/ghproxy.log",
|
||||||
MaxLogSize: 10,
|
MaxLogSize: 10,
|
||||||
Level: "info",
|
Level: "info",
|
||||||
|
HertZLogPath: "/data/ghproxy/log/hertz.log",
|
||||||
},
|
},
|
||||||
Auth: AuthConfig{
|
Auth: AuthConfig{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@@ -218,21 +265,32 @@ func DefaultConfig() *Config {
|
|||||||
},
|
},
|
||||||
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,7 +1,9 @@
|
|||||||
[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
|
||||||
H2C = true
|
H2C = true
|
||||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||||
debug = false
|
debug = false
|
||||||
@@ -11,6 +13,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
|
||||||
@@ -30,6 +33,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
|
||||||
|
hertzLogPath = "/data/ghproxy/log/hertz.log"
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
method = "parameters" # "header" or "parameters"
|
method = "parameters" # "header" or "parameters"
|
||||||
@@ -53,6 +57,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,7 +1,9 @@
|
|||||||
[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
|
||||||
H2C = true
|
H2C = true
|
||||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||||
debug = false
|
debug = false
|
||||||
@@ -30,6 +32,7 @@ staticDir = "/usr/local/ghproxy/pages"
|
|||||||
logFilePath = "/usr/local/ghproxy/log/ghproxy.log"
|
logFilePath = "/usr/local/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
|
||||||
|
hertzLogPath = "/usr/local/ghproxy/log/hertz.log"
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
authMethod = "parameters" # "header" or "parameters"
|
authMethod = "parameters" # "header" or "parameters"
|
||||||
@@ -55,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
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
[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
|
||||||
H2C = true
|
H2C = true
|
||||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||||
debug = false
|
debug = false
|
||||||
@@ -22,6 +24,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
|
||||||
@@ -41,6 +44,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
|
||||||
|
hertzLogPath = "/data/ghproxy/log/hertz.log"
|
||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
method = "parameters" # "header" or "parameters"
|
method = "parameters" # "header" or "parameters"
|
||||||
@@ -67,6 +71,10 @@ 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
|
||||||
```
|
```
|
||||||
|
|
||||||
### 配置项详细说明
|
### 配置项详细说明
|
||||||
@@ -81,10 +89,18 @@ 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)
|
||||||
* 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。
|
* 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。
|
||||||
|
* `memLimit`: `runtime`内存限制
|
||||||
|
* 类型: 整数 (`int64`)
|
||||||
|
* 默认值: `0` (不传入)
|
||||||
|
* 说明: 给`runtime`的指标, 让gc行为更高效
|
||||||
* `H2C`: 是否启用 H2C (HTTP/2 Cleartext) 传输。
|
* `H2C`: 是否启用 H2C (HTTP/2 Cleartext) 传输。
|
||||||
* 类型: 布尔值 (`bool`)
|
* 类型: 布尔值 (`bool`)
|
||||||
* 默认值: `true` (启用)
|
* 默认值: `true` (启用)
|
||||||
@@ -123,6 +139,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 克隆配置**
|
||||||
|
|
||||||
@@ -193,6 +213,10 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
|||||||
* `"warn"`: 输出警告和错误日志。
|
* `"warn"`: 输出警告和错误日志。
|
||||||
* `"error"`: 仅输出错误日志。
|
* `"error"`: 仅输出错误日志。
|
||||||
* `"none"`: 禁用所有日志输出。
|
* `"none"`: 禁用所有日志输出。
|
||||||
|
* `hertzLogPath`: `HertZ`日志文件路径。
|
||||||
|
* 类型: 字符串 (`string`)
|
||||||
|
* 默认值: `"/data/ghproxy/log/hertz.log"`
|
||||||
|
* 说明: 设置 `HertZ` 日志文件的存储路径。
|
||||||
|
|
||||||
* **`[auth]` - 认证配置**
|
* **`[auth]` - 认证配置**
|
||||||
|
|
||||||
@@ -280,6 +304,21 @@ 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)。
|
||||||
|
|
||||||
## `blacklist.json` - 黑名单配置
|
## `blacklist.json` - 黑名单配置
|
||||||
|
|
||||||
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。
|
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。
|
||||||
|
|||||||
28
go.mod
28
go.mod
@@ -1,38 +1,42 @@
|
|||||||
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.6.0
|
||||||
|
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
|
||||||
|
|
||||||
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.2 // 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 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
|
||||||
github.com/cloudwego/netpoll v0.7.0 // indirect
|
github.com/cloudwego/netpoll v0.7.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/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.1 // 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.15.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-20250506013437-ce4c2cf36ca6 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.23.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/WJQSERVER-STUDIO/httpc v0.5.1 => /data/github/WJQSERVER-STUDIO/httpc
|
||||||
|
|||||||
46
go.sum
46
go.sum
@@ -2,10 +2,14 @@ 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.2 h1:9CSf+V0ZQPl2ijC/g6v/ObemmhpKcikKVIodsaLExTA=
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po=
|
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
|
||||||
|
github.com/WJQSERVER-STUDIO/httpc v0.5.1 h1:+TKCPYBuj7PAHuiduGCGAqsHAa4QtsUfoVwRN777q64=
|
||||||
|
github.com/WJQSERVER-STUDIO/httpc v0.5.1/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE=
|
||||||
|
github.com/WJQSERVER-STUDIO/logger v1.6.0 h1:xK2xV7hlkMXaWzvj4+cNoNWA+JfnJaHX6VU+RrPnr7Q=
|
||||||
|
github.com/WJQSERVER-STUDIO/logger v1.6.0/go.mod h1:TICMsR7geROHBg6rxwkqUNGydo34XVsX93yeoxyfuyY=
|
||||||
github.com/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=
|
||||||
@@ -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=
|
||||||
@@ -29,8 +33,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 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=
|
||||||
@@ -46,12 +50,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=
|
||||||
@@ -81,14 +83,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 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/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.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
|
||||||
golang.org/x/arch v0.15.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-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
|
||||||
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-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.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 +100,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 +115,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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.31.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 +129,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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
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=
|
||||||
|
|||||||
191
main.go
191
main.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"ghproxy/api"
|
"ghproxy/api"
|
||||||
@@ -17,20 +18,24 @@ import (
|
|||||||
"ghproxy/proxy"
|
"ghproxy/proxy"
|
||||||
"ghproxy/rate"
|
"ghproxy/rate"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
"github.com/WJQSERVER-STUDIO/logger"
|
||||||
|
"github.com/hertz-contrib/http2/factory"
|
||||||
|
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"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/network/standard"
|
||||||
|
|
||||||
"github.com/hertz-contrib/http2/factory"
|
_ "net/http/pprof"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
r *server.Hertz
|
r *server.Hertz
|
||||||
configfile = "/data/ghproxy/config/config.toml"
|
configfile = "/data/ghproxy/config/config.toml"
|
||||||
|
hertZfile *os.File
|
||||||
cfgfile string
|
cfgfile string
|
||||||
version string
|
version string
|
||||||
runMode string
|
runMode string
|
||||||
@@ -129,7 +134,30 @@ func setupLogger(cfg *config.Config) {
|
|||||||
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)
|
||||||
logInfo("Init Completed")
|
logInfo("Logger Initialized Successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupHertZLogger(cfg *config.Config) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if cfg.Log.HertZLogPath != "" {
|
||||||
|
hertZfile, err = os.OpenFile(cfg.Log.HertZLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||||
|
if err != nil {
|
||||||
|
hlog.SetOutput(os.Stdout)
|
||||||
|
logWarning("Failed to open hertz log file: %v", err)
|
||||||
|
} else {
|
||||||
|
hlog.SetOutput(hertZfile)
|
||||||
|
}
|
||||||
|
hlog.SetLevel(hlog.LevelInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMemLimit(cfg *config.Config) {
|
||||||
|
if cfg.Server.MemLimit > 0 {
|
||||||
|
debug.SetMemoryLimit((cfg.Server.MemLimit) * 1024 * 1024)
|
||||||
|
logInfo("Set Memory Limit to %d MB", cfg.Server.MemLimit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadlist(cfg *config.Config) {
|
func loadlist(cfg *config.Config) {
|
||||||
@@ -153,7 +181,11 @@ 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 加载嵌入式页面资源
|
||||||
@@ -182,6 +214,12 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
|
|||||||
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(pagesFS)
|
||||||
|
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(pagesFS, "pages/assets")
|
||||||
return pages, assets, nil
|
return pages, assets, nil
|
||||||
@@ -214,7 +252,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
|
||||||
@@ -315,7 +352,9 @@ func init() {
|
|||||||
loadConfig()
|
loadConfig()
|
||||||
if cfg != nil { // 在setupLogger前添加空值检查
|
if cfg != nil { // 在setupLogger前添加空值检查
|
||||||
setupLogger(cfg)
|
setupLogger(cfg)
|
||||||
|
setupHertZLogger(cfg)
|
||||||
InitReq(cfg)
|
InitReq(cfg)
|
||||||
|
setMemLimit(cfg)
|
||||||
loadlist(cfg)
|
loadlist(cfg)
|
||||||
setupRateLimit(cfg)
|
setupRateLimit(cfg)
|
||||||
|
|
||||||
@@ -332,77 +371,103 @@ 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 {
|
||||||
|
r = server.New(
|
||||||
|
server.WithH2C(true),
|
||||||
|
server.WithHostPorts(addr),
|
||||||
|
server.WithTransport(standard.NewTransporter),
|
||||||
|
)
|
||||||
|
r.AddProtocol("h2", factory.NewServerFactory())
|
||||||
|
} else {
|
||||||
|
r = server.New(
|
||||||
|
server.WithHostPorts(addr),
|
||||||
|
server.WithTransport(standard.NewTransporter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" {
|
||||||
|
if cfg.Server.H2C {
|
||||||
|
r = server.New(
|
||||||
|
server.WithH2C(true),
|
||||||
|
server.WithHostPorts(addr),
|
||||||
|
server.WithSenseClientDisconnection(true),
|
||||||
|
)
|
||||||
|
r.AddProtocol("h2", factory.NewServerFactory())
|
||||||
|
} else {
|
||||||
|
r = server.New(
|
||||||
|
server.WithHostPorts(addr),
|
||||||
|
server.WithSenseClientDisconnection(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logError("Invalid NetLib: %s", cfg.Server.NetLib)
|
||||||
|
fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
r := server.New(
|
r.Use(recovery.Recovery()) // Recovery中间件
|
||||||
server.WithHostPorts(addr),
|
r.Use(loggin.Middleware()) // log中间件
|
||||||
server.WithH2C(true),
|
|
||||||
)
|
|
||||||
|
|
||||||
r.AddProtocol("h2", factory.NewServerFactory())
|
|
||||||
|
|
||||||
// 添加Recovery中间件
|
|
||||||
r.Use(recovery.Recovery())
|
|
||||||
// 添加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) {
|
||||||
// 1. GitHub Releases/Archive - Use distinct path segments for type
|
c.Set("matcher", "releases")
|
||||||
r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for releases
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(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
|
r.GET("/github.com/:user/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
c.Set("matcher", "releases")
|
||||||
})
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
// 2. GitHub Blob/Raw - Use distinct path segments for type
|
r.GET("/github.com/:user/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for blob
|
c.Set("matcher", "blob")
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for raw
|
r.GET("/github.com/:user/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
c.Set("matcher", "raw")
|
||||||
})
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for info
|
r.GET("/github.com/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
c.Set("matcher", "clone")
|
||||||
})
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) {
|
})
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
r.GET("/github.com/:user/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) {
|
||||||
})
|
c.Set("matcher", "clone")
|
||||||
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
// 4. Raw GitHubusercontent - Keep as is (assuming it's distinct enough)
|
r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
c.Set("matcher", "raw")
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 5. Gist GitHubusercontent - Keep as is (assuming it's distinct enough)
|
r.GET("/gist.githubusercontent.com/:user/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
c.Set("matcher", "gist")
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
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/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
c.Set("matcher", "api")
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
})
|
})
|
||||||
*/
|
|
||||||
|
r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
proxy.GhcrRouting(cfg)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
|
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
@@ -412,7 +477,21 @@ 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")
|
||||||
|
|
||||||
|
if cfg.Server.Debug {
|
||||||
|
go func() {
|
||||||
|
http.ListenAndServe("localhost:6060", nil)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
r.Spin()
|
r.Spin()
|
||||||
defer logger.Close()
|
defer logger.Close()
|
||||||
|
defer func() {
|
||||||
|
if hertZfile != nil {
|
||||||
|
err := hertZfile.Close()
|
||||||
|
if err != nil {
|
||||||
|
logError("Failed to close hertz log file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
fmt.Println("Program Exit")
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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,37 +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)"))
|
||||||
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", "*")
|
||||||
@@ -113,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
|
||||||
@@ -120,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
138
proxy/docker.go
Normal file
138
proxy/docker.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"ghproxy/config"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||||
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GhcrRouting(cfg *config.Config) app.HandlerFunc {
|
||||||
|
return func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
if cfg.Docker.Enabled {
|
||||||
|
if cfg.Docker.Target == "ghcr" {
|
||||||
|
GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr")
|
||||||
|
} else if cfg.Docker.Target == "dockerhub" {
|
||||||
|
GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub")
|
||||||
|
} else if cfg.Docker.Target != "" {
|
||||||
|
// 自定义taget
|
||||||
|
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+string(c.Request.RequestURI()), cfg, "custom")
|
||||||
|
} 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, cfg *config.Config, matcher 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 := client.NewRequestBuilder(string(method), u)
|
||||||
|
rb.NoDefaultHeaders()
|
||||||
|
rb.SetBody(c.Request.BodyStream())
|
||||||
|
rb.WithContext(ctx)
|
||||||
|
|
||||||
|
//req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
|
||||||
|
req, err = rb.Build()
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Request.Header.VisitAll(func(key, value []byte) {
|
||||||
|
headerKey := string(key)
|
||||||
|
headerValue := string(value)
|
||||||
|
req.Header.Add(headerKey, headerValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理(404)
|
||||||
|
if resp.StatusCode == 404 {
|
||||||
|
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
bodySize int
|
||||||
|
contentLength string
|
||||||
|
sizelimit int
|
||||||
|
)
|
||||||
|
|
||||||
|
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
|
||||||
|
contentLength = resp.Header.Get("Content-Length")
|
||||||
|
if contentLength != "" {
|
||||||
|
var err error
|
||||||
|
bodySize, err = strconv.Atoi(contentLength)
|
||||||
|
if err != nil {
|
||||||
|
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
|
||||||
|
bodySize = -1
|
||||||
|
}
|
||||||
|
if err == nil && bodySize > sizelimit {
|
||||||
|
var finalURL string
|
||||||
|
finalURL = resp.Request.URL.String()
|
||||||
|
err = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
logError("Failed to close response body: %v", err)
|
||||||
|
}
|
||||||
|
c.Redirect(301, []byte(finalURL))
|
||||||
|
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制响应头,排除需要移除的 header
|
||||||
|
for key, values := range resp.Header {
|
||||||
|
for _, value := range values {
|
||||||
|
//c.Header(key, value)
|
||||||
|
c.Response.Header.Add(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Status(resp.StatusCode)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
150
proxy/error.go
150
proxy/error.go
@@ -1,9 +1,12 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io/fs"
|
||||||
|
|
||||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
"github.com/WJQSERVER-STUDIO/logger"
|
||||||
"github.com/cloudwego/hertz/pkg/app"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +21,147 @@ 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(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
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData {
|
||||||
|
return ErrorPageData{
|
||||||
|
StatusCode: errInfo.StatusCode,
|
||||||
|
StatusDesc: errInfo.StatusDesc,
|
||||||
|
StatusText: errInfo.StatusText,
|
||||||
|
HelpInfo: errInfo.HelpInfo,
|
||||||
|
ErrorMessage: errInfo.ErrorMessage,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) {
|
||||||
|
pageData, err := htmlTemplateRender(errPagesFs, ErrPageUnwarper(errInfo))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
|
||||||
|
logDebug("Error reading page.tmpl: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) {
|
||||||
|
tmplPath := "page.tmpl"
|
||||||
|
tmpl, err := template.ParseFS(fsys, tmplPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing template: %w", err)
|
||||||
|
}
|
||||||
|
if tmpl == nil {
|
||||||
|
return nil, fmt.Errorf("template is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个 bytes.Buffer 用于存储渲染结果
|
||||||
|
var buf bytes.Buffer
|
||||||
|
|
||||||
|
err = tmpl.Execute(&buf, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error executing template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回 buffer 的内容作为 []byte
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,34 +4,30 @@ 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
|
||||||
|
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
err := SetGlobalRateLimit(cfg)
|
||||||
// 初始化固定大小的缓存池
|
if err != nil {
|
||||||
BufferPool = &sync.Pool{
|
return err
|
||||||
New: func() interface{} {
|
|
||||||
return make([]byte, BufferSize)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initHTTPClient(cfg *config.Config) {
|
func initHTTPClient(cfg *config.Config) {
|
||||||
@@ -42,7 +38,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 +59,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
|
||||||
@@ -87,23 +81,11 @@ func initHTTPClient(cfg *config.Config) {
|
|||||||
|
|
||||||
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 +94,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 +111,39 @@ 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,
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,36 +11,7 @@ import (
|
|||||||
"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
|
||||||
@@ -56,7 +27,8 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error)
|
|||||||
// 取出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,60 +92,50 @@ 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) {
|
func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) {
|
||||||
var (
|
|
||||||
matcher string
|
|
||||||
)
|
|
||||||
// 匹配 "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")
|
return true, nil
|
||||||
if strings.HasPrefix(remainingPath, "/") {
|
|
||||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
|
||||||
}
|
|
||||||
return true, "", nil
|
|
||||||
}
|
}
|
||||||
// 匹配 "https://raw.githubusercontent.com"开头的链接
|
// 匹配 "https://raw.githubusercontent.com"开头的链接
|
||||||
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
|
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
|
||||||
return true, matcher, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
// 匹配 "https://raw.github.com"开头的链接
|
// 匹配 "https://raw.github.com"开头的链接
|
||||||
if strings.HasPrefix(rawPath, "https://raw.github.com") {
|
if strings.HasPrefix(rawPath, "https://raw.github.com") {
|
||||||
return true, matcher, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
// 匹配 "https://gist.githubusercontent.com"开头的链接
|
// 匹配 "https://gist.githubusercontent.com"开头的链接
|
||||||
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
|
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
|
||||||
return true, matcher, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
// 匹配 "https://gist.github.com"开头的链接
|
// 匹配 "https://gist.github.com"开头的链接
|
||||||
if strings.HasPrefix(rawPath, "https://gist.github.com") {
|
if strings.HasPrefix(rawPath, "https://gist.github.com") {
|
||||||
return true, matcher, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
if cfg.Shell.RewriteAPI {
|
if cfg.Shell.RewriteAPI {
|
||||||
// 匹配 "https://api.github.com/"开头的链接
|
// 匹配 "https://api.github.com/"开头的链接
|
||||||
if strings.HasPrefix(rawPath, "https://api.github.com") {
|
if strings.HasPrefix(rawPath, "https://api.github.com") {
|
||||||
matcher = "api"
|
return true, nil
|
||||||
return true, matcher, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false, "", ErrInvalidURL
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 匹配文件扩展名是sh的rawPath
|
// 匹配文件扩展名是sh的rawPath
|
||||||
func MatcherShell(rawPath string) bool {
|
func MatcherShell(rawPath string) bool {
|
||||||
/*
|
|
||||||
if strings.HasSuffix(rawPath, ".sh") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
*/
|
|
||||||
return strings.HasSuffix(rawPath, ".sh")
|
return strings.HasSuffix(rawPath, ".sh")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +145,7 @@ type LinkProcessor func(string) string
|
|||||||
// 自定义 URL 修改函数
|
// 自定义 URL 修改函数
|
||||||
func modifyURL(url string, host string, cfg *config.Config) string {
|
func modifyURL(url string, host string, cfg *config.Config) string {
|
||||||
// 去除url内的https://或http://
|
// 去除url内的https://或http://
|
||||||
matched, _, err := EditorMatcher(url, cfg)
|
matched, err := EditorMatcher(url, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logDump("Invalid URL: %s", url)
|
logDump("Invalid URL: %s", url)
|
||||||
return url
|
return url
|
||||||
@@ -243,8 +210,10 @@ func extractParts(rawURL string) (string, string, string, url.Values, error) {
|
|||||||
return repoOwner, repoName, remainingPath, queryParams, nil
|
return repoOwner, repoName, remainingPath, queryParams, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)
|
||||||
|
|
||||||
// processLinks 处理链接,返回包含处理后数据的 io.Reader
|
// processLinks 处理链接,返回包含处理后数据的 io.Reader
|
||||||
func processLinks(input io.Reader, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) {
|
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) {
|
||||||
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
|
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
|
||||||
readerOut = pipeReader
|
readerOut = pipeReader
|
||||||
|
|
||||||
@@ -266,6 +235,13 @@ func processLinks(input io.Reader, compress string, host string, cfg *config.Con
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if err := input.Close(); err != nil {
|
||||||
|
logError("input close failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
var bufReader *bufio.Reader
|
var bufReader *bufio.Reader
|
||||||
|
|
||||||
if compress == "gzip" {
|
if compress == "gzip" {
|
||||||
@@ -315,7 +291,6 @@ func processLinks(input io.Reader, compress string, host string, cfg *config.Con
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// 使用正则表达式匹配 http 和 https 链接
|
// 使用正则表达式匹配 http 和 https 链接
|
||||||
urlPattern := regexp.MustCompile(`https?://[^\s'"]+`)
|
|
||||||
for {
|
for {
|
||||||
line, readErr := bufReader.ReadString('\n')
|
line, readErr := bufReader.ReadString('\n')
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
|
|||||||
@@ -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.Whitelist.Enabled {
|
||||||
|
var whitelist bool
|
||||||
|
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 {
|
||||||
|
var blacklist bool
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user