Compare commits

..

49 Commits

Author SHA1 Message Date
WJQSERVER
0f437dc891 Merge pull request #106 from WJQSERVER-STUDIO/dev
3.3.2
2025-05-18 07:05:19 +08:00
wjqserver
816b35654a update readme.md 2025-05-18 06:20:55 +08:00
wjqserver
a4fae95526 3.3.2 2025-05-18 06:13:00 +08:00
wjqserver
ea0e4e9801 change the default theme to design 2025-05-18 06:11:44 +08:00
wjqserver
5facc36947 update docs 2025-05-18 06:09:04 +08:00
WJQSERVER
5c25bc012f Merge pull request #105 from WJQSERVER-STUDIO/dev
3.3.1
2025-05-16 19:54:20 +08:00
wjqserver
b2712f8184 3.3.1 2025-05-16 19:53:48 +08:00
wjqserver
566a0ea26a 25w37a 2025-05-16 19:28:08 +08:00
wjqserver
7d4aae1668 merge customTarget into target 2025-05-16 00:24:57 +08:00
wjqserver
052243b095 add customTarget 2025-05-16 00:15:04 +08:00
wjqserver
4ded2186d8 update deps 2025-05-15 18:50:36 +08:00
WJQSERVER
aa95daf8c0 Merge pull request #103 from WJQSERVER-STUDIO/dev
3.3.0
2025-05-15 18:46:29 +08:00
wjqserver
89b850c1ec 3.3.0 2025-05-15 18:45:52 +08:00
wjqserver
ce814875e1 25w36d 2025-05-14 17:55:37 +08:00
wjqserver
47c03763a7 25w36c 2025-05-14 01:34:05 +08:00
wjqserver
71bc2aaed7 add bandwidth limiter 2025-05-14 01:33:54 +08:00
wjqserver
3f8d16511e 25w36b 2025-05-13 19:04:21 +08:00
wjqserver
43469532d4 25w36a 2025-05-13 14:51:34 +08:00
WJQSERVER
e32479b287 Merge pull request #102 from WJQSERVER-STUDIO/dev
3.2.4
2025-05-13 14:24:32 +08:00
wjqserver
ef6e0a78cd 3.2.4 2025-05-13 14:22:19 +08:00
wjqserver
c2e2b661a4 25w35a 2025-05-12 17:12:58 +08:00
wjqserver
791f668758 remove unused matcher return(fix #101) 2025-05-12 17:06:06 +08:00
WJQSERVER
92c4c62b46 Merge pull request #100 from WJQSERVER-STUDIO/dev
3.2.3
2025-05-07 19:09:18 +08:00
wjqserver
545144c7b5 3.2.3 2025-05-07 19:08:12 +08:00
wjqserver
866638ba8e 25w34b 2025-05-07 18:50:58 +08:00
wjqserver
e3fd604945 update Go to go1.24.3 2025-05-07 18:49:20 +08:00
wjqserver
90709539f4 update deps 2025-05-06 14:23:19 +08:00
wjqserver
1011a25d16 25w34a 2025-05-05 15:17:39 +08:00
wjqserver
bd63ed3070 change to new logger(enabled async log) 2025-05-05 15:15:18 +08:00
wjqserver
3c11e9826e change touka-httpc to httpc(touka/toka) 2025-05-05 10:41:41 +08:00
WJQSERVER
ef3b1bf1f0 Merge pull request #99 from WJQSERVER-STUDIO/dev
3.2.2
2025-04-29 22:29:13 +08:00
wjqserver
ad4d8eb670 remove unuse code 2025-04-29 22:25:20 +08:00
wjqserver
030f0d12a9 update docs 2025-04-29 22:24:11 +08:00
wjqserver
e57432a01c 3.2.2 2025-04-29 22:21:08 +08:00
wjqserver
ace795fe9d revert gitreq body stream 2025-04-29 22:13:11 +08:00
wjqserver
3f51e5319a 25w33a 2025-04-29 20:39:41 +08:00
wjqserver
55769d9a40 use custom headers for raw 2025-04-29 19:51:23 +08:00
WJQSERVER
7eb312243c Merge pull request #97 from WJQSERVER-STUDIO/dev
3.2.1
2025-04-29 07:44:45 +08:00
wjqserver
6ca31bc252 3.2.1 2025-04-29 07:43:19 +08:00
wjqserver
dfc49ae28b 25w32a 2025-04-29 07:24:28 +08:00
wjqserver
a0cca13deb fix matcher key issue 2025-04-29 07:22:03 +08:00
里見 灯花
1498aaed14 Merge pull request #95 from WJQSERVER-STUDIO/dev
3.2.0
2025-04-27 17:41:32 +08:00
wjqserver
086aa999e1 3.2.0 2025-04-27 17:38:30 +08:00
wjqserver
bf92cc8429 add req body 2025-04-27 17:33:17 +08:00
wjqserver
d94f6c0f5d 25w31a 2025-04-27 16:39:47 +08:00
wjqserver
f540b2edcd fix user name match issue 2025-04-27 15:57:06 +08:00
wjqserver
8aef197fde 25w31t-2 2025-04-25 22:14:23 +08:00
wjqserver
52d6f8e759 update readme 2025-04-25 17:56:22 +08:00
wjqserver
a7be65a111 25w31t-1 2025-04-25 17:14:33 +08:00
24 changed files with 676 additions and 142 deletions

3
.gitignore vendored
View File

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

View File

@@ -1,5 +1,114 @@
# 更新日志
3.3.2 - 2025-05-18
---
- CHANGE: 默认主题改为`design`
25w38a - 2025-05-18
---
- PRE-RELEASE: 此版本是v3.3.2预发布版本,请勿在生产环境中使用;
- CHANGE: 默认主题改为`design`
3.3.1 - 2025-05-16
- CHANGE: 为`target`放宽限制, 支持自定义
- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0`
25w37a - 2025-05-16
---
- PRE-RELEASE: 此版本是v3.3.1预发布版本,请勿在生产环境中使用;
- CHANGE: 为`target`放宽限制, 支持自定义
- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0`
3.3.0 - 2025-05-15
---
- CHANGE: 为`httpc`加入`request builder``withcontext`选项
- ADD: 加入带宽限制功能
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
25w36d - 2025-05-14
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
25w36c - 2025-05-14
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- ADD: 加入带宽限制功能
- CHANGE: 将`httpc`切换回主分支, `25w36b`测试的部分已被合入`httpc`主线
25w36b - 2025-05-13
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- CHANGE: `httpc`切换到`dev`, 测试在retry前检查ctx状态
25w36a - 2025-05-13
---
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
- CHANGE: 为`httpc`加入`request builder``withcontext`选项
3.2.4 - 2025-05-13
---
- CHANGE: 移除未使用的变量与相关计算
25w35a - 2025-05-12
---
- PRE-RELEASE: 此版本是v3.2.4预发布版本,请勿在生产环境中使用;
- CHANGE: 移除未使用的变量与相关计算
3.2.3 - 2025-05-07
---
- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录
- CHANGE: 更新Go版本到go1.24.3
- CHANGE: 迁移httpc到新的仓库
25w34b - 2025-05-07
---
- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用;
- CHANGE: 更新Go版本到go1.24.3
25w34a - 2025-05-05
---
- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用;
- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录
- CHANGE: 迁移httpc到新的仓库
3.2.2 - 2025-04-29
---
- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制
- FIX: 修正Header部分的一些处理问题
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
25w33b - 2025-04-29
---
- PRE-RELEASE: 此版本是v3.2.2预发布版本,请勿在生产环境中使用;
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
25w33a - 2025-04-29
---
- PRE-RELEASE: 此版本是v3.2.2预发布版本,请勿在生产环境中使用;
- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制
- FIX: 修正Header部分的一些处理问题
3.2.1 - 2025-04-29
---
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
25w32a - 2025-04-29
---
- PRE-RELEASE: 此版本是v3.2.1预发布版本,请勿在生产环境中使用;
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
3.2.0 - 2025-04-27
---
- CHANGE: 加入`ghcr``dockerhub`反代功能
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
25w31a - 2025-04-27
---
- PRE-RELEASE: 此版本是v3.2.0预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`ghcr``dockerhub`反代功能
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
3.1.0 - 2025-04-24
---
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率

View File

@@ -1 +1 @@
25w30e
25w38a

View File

@@ -1,6 +1,11 @@
# GHProxy
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
![GitHub Release](https://img.shields.io/github/v/release/WJQSERVER-STUDIO/ghproxy?display_name=tag&style=flat)
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/WJQSERVER-STUDIO/ghproxy)
[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能
@@ -12,12 +17,13 @@
- 🌐 **使用字节旗下的 [HertZ](https://github.com/cloudwego/hertz) 作为 Web 框架**
- 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端**
- 📥 **支持 Git clone、raw、releases 等文件拉取**
- 🐳 **支持反代Docker, GHCR等镜像仓库**
- 🎨 **支持多个前端主题**
- 🚫 **支持自定义黑名单/白名单**
- 🗄️ **支持 Git Clone 缓存(配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git)**
- 🐳 **支持 Docker 部署**
- 🐳 **支持自托管**
- 🐳 **支持自托管与Docker容器化部署**
-**支持速率限制**
-**支持带宽速率限制**
- 🔒 **支持用户鉴权**
- 🐚 **支持 shell 脚本多层嵌套加速**
@@ -29,7 +35,9 @@
[相关文章](https://blog.wjqserver.com/categories/my-program/)
[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md)
[GHProxy项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md)
[GHProxy项目文档Next(仍在建设中)](https://ghproxy-docs.pages.dev/)
### 使用示例

View File

@@ -1 +1 @@
3.1.0
3.3.2

View File

@@ -5,7 +5,7 @@ import (
"ghproxy/config"
"ghproxy/middleware/nocache"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
)

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
)

View File

@@ -18,6 +18,7 @@ type Config struct {
Whitelist WhitelistConfig
RateLimit RateLimitConfig
Outbound OutboundConfig
Docker DockerConfig
}
/*
@@ -49,12 +50,14 @@ mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
useCustomRawHeaders = false
*/
type HttpcConfig struct {
Mode string `toml:"mode"`
MaxIdleConns int `toml:"maxIdleConns"`
MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"`
MaxConnsPerHost int `toml:"maxConnsPerHost"`
UseCustomRawHeaders bool `toml:"useCustomRawHeaders"`
}
/*
@@ -126,11 +129,35 @@ type WhitelistConfig struct {
WhitelistFile string `toml:"whitelistFile"`
}
/*
[rateLimit]
enabled = false
rateMethod = "total" # "total" or "ip"
ratePerMinute = 100
burst = 10
[rateLimit.bandwidthLimit]
enabled = false
totalLimit = "100mbps"
totalBurst = "100mbps"
singleLimit = "10mbps"
singleBurst = "10mbps"
*/
type RateLimitConfig struct {
Enabled bool `toml:"enabled"`
RateMethod string `toml:"rateMethod"`
RatePerMinute int `toml:"ratePerMinute"`
Burst int `toml:"burst"`
Enabled bool `toml:"enabled"`
RateMethod string `toml:"rateMethod"`
RatePerMinute int `toml:"ratePerMinute"`
Burst int `toml:"burst"`
BandwidthLimit BandwidthLimitConfig
}
type BandwidthLimitConfig struct {
Enabled bool `toml:"enabled"`
TotalLimit string `toml:"totalLimit"`
TotalBurst string `toml:"totalBurst"`
SingleLimit string `toml:"singleLimit"`
SingleBurst string `toml:"singleBurst"`
}
/*
@@ -143,6 +170,16 @@ type OutboundConfig struct {
Url string `toml:"url"`
}
/*
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub
*/
type DockerConfig struct {
Enabled bool `toml:"enabled"`
Target string `toml:"target"`
}
// LoadConfig 从 TOML 配置文件加载配置
func LoadConfig(filePath string) (*Config, error) {
if !FileExists(filePath) {
@@ -239,10 +276,21 @@ func DefaultConfig() *Config {
RateMethod: "total",
RatePerMinute: 100,
Burst: 10,
BandwidthLimit: BandwidthLimitConfig{
Enabled: false,
TotalLimit: "100mbps",
TotalBurst: "100mbps",
SingleLimit: "10mbps",
SingleBurst: "10mbps",
},
},
Outbound: OutboundConfig{
Enabled: false,
Url: "socks5://127.0.0.1:1080",
},
Docker: DockerConfig{
Enabled: false,
Target: "ghcr",
},
}
}

View File

@@ -13,6 +13,7 @@ mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
useCustomRawHeaders = false
[gitclone]
mode = "bypass" # bypass / cache
@@ -56,6 +57,17 @@ rateMethod = "total" # "ip" or "total"
ratePerMinute = 180
burst = 5
[rateLimit.bandwidthLimit]
enabled = false
totalLimit = "100mbps"
totalBurst = "100mbps"
singleLimit = "10mbps"
singleBurst = "10mbps"
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub

View File

@@ -58,3 +58,7 @@ burst = 5
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub

View File

@@ -24,6 +24,7 @@ mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
useCustomRawHeaders = false
[gitclone]
mode = "bypass" # bypass / cache
@@ -67,9 +68,20 @@ rateMethod = "total" # "ip" or "total"
ratePerMinute = 180
burst = 5
[rateLimit.bandwidthLimit]
enabled = false
totalLimit = "100mbps"
totalBurst = "100mbps"
singleLimit = "10mbps"
singleBurst = "10mbps"
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub or "xx.example.com"
```
### 配置项详细说明
@@ -134,6 +146,10 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
* 类型: 整数 (`int`)
* 默认值: `0` (不限制)
* 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。
* `useCustomRawHeaders`: 使用预定义header避免github waf对应zh-CN的封锁
* 类型: 布尔值(`bool`)
* 默认值: `false`(停用)
* 说明: 启用后, 拉取raw文件会使用程序预定义的固定headers, 而不是原先的复制行为
* **`[gitclone]` - Git 克隆配置**
@@ -282,6 +298,27 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
* 类型: 整数 (`int`)
* 默认值: `5`
* 说明: 允许在短时间内超过 `ratePerMinute` 的突发请求数。
* **`[rateLimit.bandwidthLimit]` 带宽速率限制**
* `enabled`: 是否启用带宽速率限制。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将根据配置的策略限制带宽使用,防止服务被滥用。
* `totalLimit`: 全局带宽限制。
* 类型: 字符串 (`string`)
* 默认值: `"100mbps"`
* 说明: 设置全局最大带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。
* `totalBurst`: 全局突发带宽。
* 类型: 字符串 (`string`)
* 默认值: `"100mbps"`
* 说明: 设置全局突发带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。
* `singleLimit`: 单个连接带宽限制。
* 类型: 字符串 (`string`)
* 默认值: `"10mbps"`
* 说明: 设置单个连接的最大带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。
* `singleBurst`: 单个连接突发带宽。
* 类型: 字符串 (`string`)
* 默认值: `"10mbps"`
* 说明: 设置单个连接的突发带宽使用量。支持的单位有 "kbps", "mbps", "gbps"。
* **`[outbound]` - 出站代理配置**
@@ -295,6 +332,22 @@ url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
* 支持协议: `socks5://` 和 `http://`
* 说明: 设置出站代理服务器的 URL。支持 SOCKS5 和 HTTP 代理协议。
* **`[docker]` - Docker 镜像代理配置**
* `enabled`: 是否启用 Docker 镜像代理功能。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 当设置为 `true` 时,`ghproxy` 将尝试代理 Docker 镜像的下载请求,以加速从 GitHub Container Registry (GHCR) 或 Docker Hub 下载镜像。
* `target`: 代理的目标 Docker 注册表。
* 类型: 字符串 (`string`)
* 默认值: `"ghcr"` (代理 GHCR)
* 可选值: `"ghcr"` 或 `"dockerhub"`
* 说明: 指定要代理的 Docker 注册表。
* `"ghcr"`: 代理 GitHub Container Registry (ghcr.io)。
* `"dockerhub"`: 代理 Docker Hub (docker.io)。
* 自定义, 支持传入自定义target, 例如`"docker.example.com"`
## `blacklist.json` - 黑名单配置
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。

24
go.mod
View File

@@ -1,20 +1,22 @@
module ghproxy
go 1.24.2
go 1.24.3
require (
github.com/BurntSushi/toml v1.5.0
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0
github.com/cloudwego/hertz v0.9.7
github.com/WJQSERVER-STUDIO/httpc v0.5.1
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/satomitouka/touka-httpc v0.4.0
golang.org/x/net v0.39.0
golang.org/x/net v0.40.0
golang.org/x/time v0.11.0
)
require github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2
require (
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 // indirect
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 // indirect
github.com/bytedance/gopkg v0.1.2 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
@@ -30,9 +32,11 @@ require (
github.com/tidwall/pretty v1.2.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
//replace github.com/WJQSERVER-STUDIO/httpc v0.5.1 => /data/github/WJQSERVER-STUDIO/httpc

38
go.sum
View File

@@ -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/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YYtrg1ixVSB/JvZM=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po=
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc5f0DK8N9agIJCpM1Cqw2NE=
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2 h1:9CSf+V0ZQPl2ijC/g6v/ObemmhpKcikKVIodsaLExTA=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.2/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
github.com/WJQSERVER-STUDIO/httpc v0.5.1 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.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ=
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/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50=
github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI=
github.com/cloudwego/hertz v0.9.7 h1:tAVaiO+vTf+ZkQhvNhKbDJ0hmC4oJ7bzwDi1KhvhHy4=
github.com/cloudwego/hertz v0.9.7/go.mod h1:t6d7NcoQxPmETvzPMMIVPHMn5C5QzpqIiFsaavoLJYQ=
github.com/cloudwego/hertz v0.10.0 h1:V0vmBaLdQPlgL6w2TA6PZL1g6SGgQznFx6vqxWdCcKw=
github.com/cloudwego/hertz v0.10.0/go.mod h1:lRBohmcDkGx5TLK6QKFGdzJ6n3IXqGueHsOiXcYgXA4=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4=
github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU=
@@ -50,8 +54,6 @@ github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX
github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/satomitouka/touka-httpc v0.4.0 h1:cnOONdyJHJImMY8L64bvYF+7Ow/5CPf2Yr3RQRRMZOU=
github.com/satomitouka/touka-httpc v0.4.0/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
@@ -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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -98,8 +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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -113,8 +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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -127,8 +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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

47
main.go
View File

@@ -18,7 +18,7 @@ import (
"ghproxy/proxy"
"ghproxy/rate"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/hertz-contrib/http2/factory"
"github.com/cloudwego/hertz/pkg/app"
@@ -181,7 +181,11 @@ func setupRateLimit(cfg *config.Config) {
}
func InitReq(cfg *config.Config) {
proxy.InitReq(cfg)
err := proxy.InitReq(cfg)
if err != nil {
fmt.Printf("Failed to initialize request: %v\n", err)
os.Exit(1)
}
}
// loadEmbeddedPages 加载嵌入式页面资源
@@ -202,8 +206,8 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
case "mino":
pages, err = fs.Sub(pagesFS, "pages/mino")
default:
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
pages, err = fs.Sub(pagesFS, "pages/design") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme)
}
if err != nil {
@@ -397,11 +401,13 @@ func main() {
r = server.New(
server.WithH2C(true),
server.WithHostPorts(addr),
server.WithSenseClientDisconnection(true),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
server.WithSenseClientDisconnection(true),
)
}
} else {
@@ -415,50 +421,54 @@ func main() {
setupApi(cfg, r, version)
setupPages(cfg, r)
r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "release")
r.GET("/github.com/:user/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "releases")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "release")
r.GET("/github.com/:user/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "releases")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) {
r.GET("/github.com/:user/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "blob")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) {
r.GET("/github.com/:user/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "raw")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "gitclone")
r.GET("/github.com/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "clone")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "gitclone")
r.GET("/github.com/:user/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "clone")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "raw")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) {
r.GET("/gist.githubusercontent.com/:user/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "gist")
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
r.GET("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "api")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) {
proxy.GhcrRouting(cfg)(ctx, c)
})
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
})
@@ -477,8 +487,7 @@ func main() {
defer logger.Close()
defer func() {
if hertZfile != nil {
var err error
err = hertZfile.Close()
err := hertZfile.Close()
if err != nil {
logError("Failed to close hertz log file: %v", err)
}

View File

@@ -4,7 +4,7 @@ import (
"context"
"time"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
)

64
proxy/bandwidth.go Normal file
View File

@@ -0,0 +1,64 @@
package proxy
import (
"errors"
"ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"golang.org/x/time/rate"
)
var (
bandwidthLimit rate.Limit
bandwidthBurst rate.Limit
)
func UnDefiendRateStringErrHandle(err error) error {
if errors.Is(err, &limitreader.UnDefiendRateStringErr{}) {
logWarning("UnDefiendRateStringErr: %s", err)
return nil
}
return err
}
func SetGlobalRateLimit(cfg *config.Config) error {
if cfg.RateLimit.BandwidthLimit.Enabled {
var err error
var totalLimit rate.Limit
var totalBurst rate.Limit
totalLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalLimit)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse total bandwidth limit: %v", err)
return err
}
totalBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalBurst)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse total bandwidth burst: %v", err)
return err
}
limitreader.SetGlobalRateLimit(totalLimit, int(totalBurst))
err = SetBandwidthLimit(cfg)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to set bandwidth limit: %v", err)
return err
}
} else {
limitreader.SetGlobalRateLimit(rate.Inf, 0)
}
return nil
}
func SetBandwidthLimit(cfg *config.Config) error {
var err error
bandwidthLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleLimit)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse bandwidth limit: %v", err)
return err
}
bandwidthBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleBurst)
if UnDefiendRateStringErrHandle(err) != nil {
logError("Failed to parse bandwidth burst: %v", err)
return err
}
return nil
}

View File

@@ -8,51 +8,40 @@ import (
"net/http"
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
)
var (
respHeadersToRemove = map[string]struct{}{
"Content-Security-Policy": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
"X-Github-Request-Id": {},
"X-Timer": {},
"X-Served-By": {},
"X-Fastly-Request-Id": {},
}
reqHeadersToRemove = map[string]struct{}{
"CF-IPCountry": {},
"CF-RAY": {},
"CF-Visitor": {},
"CF-Connecting-IP": {},
"CF-EW-Via": {},
"CDN-Loop": {},
"Upgrade": {},
"Connection": {},
}
)
func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
var (
method []byte
req *http.Request
resp *http.Response
err error
req *http.Request
resp *http.Response
err error
)
method = c.Request.Method()
go func() {
<-ctx.Done()
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
if req != nil {
req.Body.Close()
}
}()
req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
rb := client.NewRequestBuilder(string(c.Request.Method()), u)
rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream())
rb.WithContext(ctx)
req, err = rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
//removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
setRequestHeaders(c, req, cfg, matcher)
AuthPassThrough(c, cfg, req)
resp, err = client.Do(req)
@@ -82,8 +71,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
bodySize = -1
}
if err == nil && bodySize > sizelimit {
var finalURL string
finalURL = resp.Request.URL.String()
finalURL := resp.Request.URL.String()
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
@@ -116,6 +104,12 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
c.Status(resp.StatusCode)
bodyReader := resp.Body
if cfg.RateLimit.BandwidthLimit.Enabled {
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
}
if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor {
// 判断body是不是gzip
var compress string
@@ -123,24 +117,25 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
compress = "gzip"
}
logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol())
logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol())
c.Header("Content-Length", "")
var reader io.Reader
reader, _, err = processLinks(resp.Body, compress, string(c.Request.Host()), cfg)
reader, _, err = processLinks(bodyReader, compress, string(c.Request.Host()), cfg)
c.SetBodyStream(reader, -1)
if err != nil {
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err)
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method(), u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err)
ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err)))
return
}
} else {
if contentLength != "" {
c.SetBodyStream(resp.Body, bodySize)
c.SetBodyStream(bodyReader, bodySize)
return
}
c.SetBodyStream(resp.Body, -1)
c.SetBodyStream(bodyReader, -1)
}
}

138
proxy/docker.go Normal file
View 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)
}

View File

@@ -6,7 +6,7 @@ import (
"html/template"
"io/fs"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/WJQSERVER-STUDIO/logger"
"github.com/cloudwego/hertz/pkg/app"
)

View File

@@ -1,19 +1,40 @@
package proxy
import (
"bytes"
"context"
"fmt"
"ghproxy/config"
"net/http"
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
"github.com/cloudwego/hertz/pkg/app"
)
func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) {
var (
req *http.Request
resp *http.Response
)
go func() {
<-ctx.Done()
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
if req != nil {
req.Body.Close()
}
}()
method := string(c.Request.Method())
logDump("Url Before FMT:%s", u)
reqBodyReader := bytes.NewBuffer(c.Request.Body())
//bodyReader := c.Request.BodyStream() // 不可替换为此实现
if cfg.GitClone.Mode == "cache" {
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
if err != nil {
@@ -22,21 +43,21 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
}
// 构建新url
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
logDump("New Url After FMT:%s", u)
}
var (
resp *http.Response
)
if cfg.GitClone.Mode == "cache" {
req, err := gitclient.NewRequest(method, u, c.Request.BodyStream())
rb := gitclient.NewRequestBuilder(method, u)
rb.NoDefaultHeaders()
rb.SetBody(reqBodyReader)
rb.WithContext(ctx)
req, err := rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
//removeWSHeader(req)
setRequestHeaders(c, req, cfg, "clone")
AuthPassThrough(c, cfg, req)
resp, err = gitclient.Do(req)
@@ -45,13 +66,18 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
return
}
} else {
req, err := client.NewRequest(method, u, c.Request.BodyStream())
rb := client.NewRequestBuilder(string(c.Request.Method()), u)
rb.NoDefaultHeaders()
rb.SetBody(reqBodyReader)
rb.WithContext(ctx)
req, err := rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
//removeWSHeader(req)
setRequestHeaders(c, req, cfg, "clone")
AuthPassThrough(c, cfg, req)
resp, err = client.Do(req)
@@ -78,7 +104,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
c.Response.Header.Add(key, value)
}
}
@@ -110,5 +136,11 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
c.Response.Header.Set("Expires", "0")
}
c.SetBodyStream(resp.Body, -1)
bodyReader := resp.Body
if cfg.RateLimit.BandwidthLimit.Enabled {
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
}
c.SetBodyStream(bodyReader, -1)
}

View File

@@ -6,7 +6,7 @@ import (
"net/http"
"time"
httpc "github.com/satomitouka/touka-httpc"
"github.com/WJQSERVER-STUDIO/httpc"
)
var BufferSize int = 32 * 1024 // 32KB
@@ -18,11 +18,16 @@ var (
gitclient *httpc.Client
)
func InitReq(cfg *config.Config) {
func InitReq(cfg *config.Config) error {
initHTTPClient(cfg)
if cfg.GitClone.Mode == "cache" {
initGitHTTPClient(cfg)
}
err := SetGlobalRateLimit(cfg)
if err != nil {
return err
}
return nil
}
func initHTTPClient(cfg *config.Config) {

View File

@@ -104,42 +104,34 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHPro
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
}
func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) {
var (
matcher string
)
func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) {
// 匹配 "https://github.com"开头的链接
if strings.HasPrefix(rawPath, "https://github.com") {
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
if strings.HasPrefix(remainingPath, "/") {
remainingPath = strings.TrimPrefix(remainingPath, "/")
}
return true, "", nil
return true, nil
}
// 匹配 "https://raw.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
return true, matcher, nil
return true, nil
}
// 匹配 "https://raw.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.github.com") {
return true, matcher, nil
return true, nil
}
// 匹配 "https://gist.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
return true, matcher, nil
return true, nil
}
// 匹配 "https://gist.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.github.com") {
return true, matcher, nil
return true, nil
}
if cfg.Shell.RewriteAPI {
// 匹配 "https://api.github.com/"开头的链接
if strings.HasPrefix(rawPath, "https://api.github.com") {
matcher = "api"
return true, matcher, nil
return true, nil
}
}
return false, "", nil
return false, nil
}
// 匹配文件扩展名是sh的rawPath
@@ -153,7 +145,7 @@ type LinkProcessor func(string) string
// 自定义 URL 修改函数
func modifyURL(url string, host string, cfg *config.Config) string {
// 去除url内的https://或http://
matched, _, err := EditorMatcher(url, cfg)
matched, err := EditorMatcher(url, cfg)
if err != nil {
logDump("Invalid URL: %s", url)
return url

View File

@@ -1,17 +1,75 @@
package proxy
import (
"ghproxy/config"
"net/http"
"github.com/cloudwego/hertz/pkg/app"
)
func setRequestHeaders(c *app.RequestContext, req *http.Request) {
c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key)
headerValue := string(value)
if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove {
req.Header.Set(headerKey, headerValue)
var (
respHeadersToRemove = map[string]struct{}{
"Content-Security-Policy": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
"X-Github-Request-Id": {},
"X-Timer": {},
"X-Served-By": {},
"X-Fastly-Request-Id": {},
}
reqHeadersToRemove = map[string]struct{}{
"CF-IPCountry": {},
"CF-RAY": {},
"CF-Visitor": {},
"CF-Connecting-IP": {},
"CF-EW-Via": {},
"CDN-Loop": {},
"Upgrade": {},
"Connection": {},
}
cloneHeadersToRemove = map[string]struct{}{
"CF-IPCountry": {},
"CF-RAY": {},
"CF-Visitor": {},
"CF-Connecting-IP": {},
"CF-EW-Via": {},
"CDN-Loop": {},
}
)
// 预定义headers
var (
defaultHeaders = map[string]string{
"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)
}
})
}
}

View File

@@ -4,7 +4,7 @@ import (
"sync"
"time"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/WJQSERVER-STUDIO/logger"
"golang.org/x/time/rate"
)