Compare commits

...

46 Commits

Author SHA1 Message Date
里見 灯花
1498aaed14 Merge pull request #95 from WJQSERVER-STUDIO/dev
3.2.0
2025-04-27 17:41:32 +08:00
wjqserver
086aa999e1 3.2.0 2025-04-27 17:38:30 +08:00
wjqserver
bf92cc8429 add req body 2025-04-27 17:33:17 +08:00
wjqserver
d94f6c0f5d 25w31a 2025-04-27 16:39:47 +08:00
wjqserver
f540b2edcd fix user name match issue 2025-04-27 15:57:06 +08:00
wjqserver
8aef197fde 25w31t-2 2025-04-25 22:14:23 +08:00
wjqserver
52d6f8e759 update readme 2025-04-25 17:56:22 +08:00
wjqserver
a7be65a111 25w31t-1 2025-04-25 17:14:33 +08:00
WJQSERVER
9977eb1437 3.1.0
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率
- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`
- CHANGE: 使用`HertZ`的`requestContext`传递matcher参数, 而不是`25w30a`中的ctx
- CHANGE: 改进`rate`模块, 避免并发竞争问题
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
- CHANGE: 修改部分log等级
- FIX:    修正默认配置的填充错误
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
- CHANGE: 改进handle, 复用共同部分
- CHANGE: 细化url匹配的返回码处理
2025-04-24 18:46:28 +08:00
wjqserver
47de48bcce 3.1.0 2025-04-24 18:27:15 +08:00
wjqserver
8ccf48a6fe fix && update 2025-04-24 18:11:13 +08:00
wjqserver
7a6544c6c9 25w30e 2025-04-24 17:50:18 +08:00
wjqserver
b955c915ff fix callback issue 2025-04-24 01:09:53 +08:00
wjqserver
e42ea358bb remove debug output 2025-04-22 20:58:44 +08:00
wjqserver
4936a93788 25w30d 2025-04-22 20:56:34 +08:00
wjqserver
493ac28b59 add html/tmpl for status err page 2025-04-22 20:56:27 +08:00
wjqserver
d79aeaaacd 25w30c 2025-04-21 18:52:45 +08:00
wjqserver
558d3fbb0b 25w30b 2025-04-21 17:27:38 +08:00
wjqserver
3d7559bd66 change context.Context to hertz *app.RequestContext 2025-04-21 13:57:52 +08:00
wjqserver
809032a970 change to c.Request.BodyStream() 2025-04-21 13:47:45 +08:00
wjqserver
2eb6a9810b 25w30a 2025-04-19 23:02:13 +08:00
wjqserver
26a5148c6f use gertz route for std url 2025-04-19 22:59:59 +08:00
WJQSERVER
c656aa41ca Merge pull request #93 from WJQSERVER-STUDIO/dev
3.0.3
2025-04-19 21:26:39 +08:00
wjqserver
0b052f9c7f add debug output 2025-04-19 21:23:31 +08:00
wjqserver
6fb7e1150e 25w29b 2025-04-19 21:14:09 +08:00
wjqserver
5e0f95dae3 3.0.3 2025-04-19 20:44:43 +08:00
wjqserver
c1c39a5a1f remove unused bufferpool 2025-04-17 22:30:00 +08:00
wjqserver
dd2f5b5a12 25w29a 2025-04-17 22:20:06 +08:00
wjqserver
7e5b12dff8 Fix: Optimize header forwarding by excluding headers in a single pass 2025-04-16 15:50:04 +08:00
wjqserver
26a42b6510 add pprof for debug 2025-04-16 15:47:46 +08:00
wjqserver
254c9a8bad 25w29t-1 2025-04-15 15:05:36 +08:00
WJQSERVER
060453f070 Merge pull request #88 from WJQSERVER-STUDIO/dev
3.0.2
2025-04-15 13:26:46 +08:00
wjqserver
f110c96c1f update readme 2025-04-15 13:22:51 +08:00
wjqserver
73aac79c1b 3.0.2 2025-04-15 13:14:53 +08:00
wjqserver
bed6c486dc 25w28b 2025-04-15 10:23:29 +08:00
wjqserver
ab77c5c7da 25w28a 2025-04-14 12:27:12 +08:00
wjqserver
bf21bd197a 25w28t-2 2025-04-11 07:29:03 +08:00
wjqserver
8af107c584 update for touka-httpc 0.4.0 2025-04-11 07:24:50 +08:00
wjqserver
d6d54b222f dix auth checker 2025-04-10 23:07:48 +08:00
wjqserver
005a4543d4 update deps 2025-04-10 23:07:13 +08:00
wjqserver
a85eb38de5 update deps 2025-04-08 20:50:55 +08:00
里見 灯花
152fb8aa71 Merge pull request #84 from WJQSERVER-STUDIO/dev
3.0.1
2025-04-08 20:49:27 +08:00
wjqserver
3e9e43cd44 3.0.1 2025-04-08 20:48:56 +08:00
wjqserver
8a50b311fc 25w27a 2025-04-07 18:51:22 +08:00
wjqserver
dcc50401c4 update deps 2025-04-07 18:34:00 +08:00
wjqserver
d62a1f9769 [docs] update config docs 2025-04-07 18:33:47 +08:00
25 changed files with 1043 additions and 366 deletions

View File

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

View File

@@ -1,5 +1,126 @@
# 更新日志 # 更新日志
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; 下一个起点;

View File

@@ -1 +1 @@
25w26a 25w31a

View File

@@ -1,8 +1,13 @@
# GHProxy # GHProxy
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) ![GitHub Release](https://img.shields.io/github/v/release/WJQSERVER-STUDIO/ghproxy?display_name=tag&style=flat)
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/WJQSERVER-STUDIO/ghproxy)
[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能
## 项目说明 ## 项目说明
@@ -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 脚本多层嵌套加速**
### 项目相关 ### 项目相关

View File

@@ -1 +1 @@
3.0.0 3.2.0

View File

@@ -1,7 +1,6 @@
package auth package auth
import ( import (
"context"
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
@@ -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))
} }
} }

View File

@@ -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"`
@@ -86,9 +93,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"`
} }
/* /*
@@ -136,6 +144,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 +196,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 +224,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,11 +239,11 @@ 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,
@@ -234,5 +255,9 @@ func DefaultConfig() *Config {
Enabled: false, Enabled: false,
Url: "socks5://127.0.0.1:1080", Url: "socks5://127.0.0.1:1080",
}, },
Docker: DockerConfig{
Enabled: false,
Target: "ghcr",
},
} }
} }

View File

@@ -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
@@ -30,6 +32,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"
@@ -55,4 +58,8 @@ 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

View File

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

View File

@@ -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
@@ -41,6 +43,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 +70,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 +88,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` (启用)
@@ -193,6 +208,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 +299,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` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。

20
go.mod
View File

@@ -5,10 +5,10 @@ go 1.24.2
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/go-utils/logger v1.5.0
github.com/cloudwego/hertz v0.9.6 github.com/cloudwego/hertz v0.9.7
github.com/hertz-contrib/http2 v0.1.8 github.com/hertz-contrib/http2 v0.1.8
github.com/satomitouka/touka-httpc v0.3.3 github.com/satomitouka/touka-httpc v0.4.1
golang.org/x/net v0.38.0 golang.org/x/net v0.39.0
golang.org/x/time v0.11.0 golang.org/x/time v0.11.0
) )
@@ -21,18 +21,20 @@ require (
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.16.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.6 // indirect
) )
//replace github.com/satomitouka/touka-httpc v0.4.1 => /data/github/satomitoka/touka-httpc

36
go.sum
View File

@@ -20,8 +20,8 @@ github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCy
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/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.9.7 h1:tAVaiO+vTf+ZkQhvNhKbDJ0hmC4oJ7bzwDi1KhvhHy4=
github.com/cloudwego/hertz v0.9.6/go.mod h1:X5Ez52XhtszU4t+CTBGIJI4PqmcI1oSf8ULBz0SWfLo= github.com/cloudwego/hertz v0.9.7/go.mod h1:t6d7NcoQxPmETvzPMMIVPHMn5C5QzpqIiFsaavoLJYQ=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/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 +29,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.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 +46,12 @@ 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.4.1 h1:K1LJwSJJKRPkol6MPOEzc8bReAIUqxVuzdFfTAi/2AI=
github.com/satomitouka/touka-httpc v0.3.3/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA= github.com/satomitouka/touka-httpc v0.4.1/go.mod h1:E1JeXw81XclzvlqVvSio/GcDmvN8wWLPpbNRN42Uwfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d 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 +81,14 @@ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 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.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-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-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.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 +98,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-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 +113,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-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 +127,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0 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=

182
main.go
View File

@@ -8,6 +8,7 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"os" "os"
"runtime/debug"
"time" "time"
"ghproxy/api" "ghproxy/api"
@@ -18,19 +19,23 @@ import (
"ghproxy/rate" "ghproxy/rate"
"github.com/WJQSERVER-STUDIO/go-utils/logger" "github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/hertz-contrib/http2/factory"
"github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app"
"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) {
@@ -182,6 +210,12 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err) 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 +248,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 +348,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 +367,101 @@ 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),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
)
}
} 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", "release")
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", "release")
}) 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", "gitclone")
}) 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", "gitclone")
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 +471,22 @@ 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 {
var err error
err = hertZfile.Close()
if err != nil {
logError("Failed to close hertz log file: %v", err)
}
}
}()
fmt.Println("Program Exit") fmt.Println("Program Exit")
} }

View File

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

View File

@@ -1,7 +1,6 @@
package proxy package proxy
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
@@ -12,56 +11,50 @@ import (
"github.com/cloudwego/hertz/pkg/app" "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) { 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) method []byte
req *http.Request
resp *http.Response
err error
)
method = c.Request.Method()
req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
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)
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) 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 +62,46 @@ 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)
finalURL := resp.Request.URL.String() if err != nil {
c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) 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)
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) 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 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", "*")
@@ -120,17 +122,23 @@ 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(), 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(resp.Body, 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(), 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 {
if contentLength != "" {
c.SetBodyStream(resp.Body, bodySize)
return
}
c.SetBodyStream(resp.Body, -1) c.SetBodyStream(resp.Body, -1)
} }

115
proxy/docker.go Normal file
View File

@@ -0,0 +1,115 @@
package proxy
import (
"context"
"fmt"
"ghproxy/config"
"net/http"
"strconv"
"github.com/cloudwego/hertz/pkg/app"
)
func GhcrRouting(cfg *config.Config) app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
if cfg.Docker.Enabled {
if cfg.Docker.Target == "ghcr" {
GhcrRequest(ctx, c, "https://ghcr.io"+string(c.Request.RequestURI()), cfg, "ghcr")
} else if cfg.Docker.Target == "dockerhub" {
GhcrRequest(ctx, c, "https://registry-1.docker.io"+string(c.Request.RequestURI()), cfg, "dockerhub")
} else {
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not Allowed"))
return
}
} else {
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
return
}
}
}
func GhcrRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
var (
method []byte
req *http.Request
resp *http.Response
err error
)
method = c.Request.Method()
rb := client.NewRequestBuilder(string(method), u)
rb.NoDefaultHeaders()
rb.SetBody(c.Request.BodyStream())
//req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
req, err = rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key)
headerValue := string(value)
req.Header.Add(headerKey, headerValue)
})
resp, err = client.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
// 错误处理(404)
if resp.StatusCode == 404 {
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
return
}
var (
bodySize int
contentLength string
sizelimit int
)
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
contentLength = resp.Header.Get("Content-Length")
if contentLength != "" {
var err error
bodySize, err = strconv.Atoi(contentLength)
if err != nil {
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
bodySize = -1
}
if err == nil && bodySize > sizelimit {
var finalURL string
finalURL = resp.Request.URL.String()
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
}
c.Redirect(301, []byte(finalURL))
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize)
return
}
}
// 复制响应头,排除需要移除的 header
for key, values := range resp.Header {
for _, value := range values {
//c.Header(key, value)
c.Response.Header.Add(key, value)
}
}
c.Status(resp.StatusCode)
if contentLength != "" {
c.SetBodyStream(resp.Body, bodySize)
return
}
c.SetBodyStream(resp.Body, -1)
}

View File

@@ -1,7 +1,10 @@
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/go-utils/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
}

View File

@@ -1,7 +1,6 @@
package proxy package proxy
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
@@ -28,22 +27,16 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
var ( var (
resp *http.Response 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) req, err := gitclient.NewRequest(method, u, c.Request.BodyStream())
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) setRequestHeaders(c, req)
removeWSHeader(req) //removeWSHeader(req)
AuthPassThrough(c, cfg, req) AuthPassThrough(c, cfg, req)
resp, err = gitclient.Do(req) resp, err = gitclient.Do(req)
@@ -52,13 +45,13 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
return return
} }
} else { } else {
req, err := client.NewRequest(method, u, bodyReader) req, err := client.NewRequest(method, u, c.Request.BodyStream())
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) setRequestHeaders(c, req)
removeWSHeader(req) //removeWSHeader(req)
AuthPassThrough(c, cfg, req) AuthPassThrough(c, cfg, req)
resp, err = client.Do(req) resp, err = client.Do(req)
@@ -72,6 +65,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)

View File

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

View File

@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"net/http" "net/http"
"sync"
"time" "time"
httpc "github.com/satomitouka/touka-httpc" httpc "github.com/satomitouka/touka-httpc"
@@ -13,11 +12,10 @@ import (
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) {
@@ -25,13 +23,6 @@ func InitReq(cfg *config.Config) {
if cfg.GitClone.Mode == "cache" { if cfg.GitClone.Mode == "cache" {
initGitHTTPClient(cfg) initGitHTTPClient(cfg)
} }
// 初始化固定大小的缓存池
BufferPool = &sync.Pool{
New: func() interface{} {
return make([]byte, BufferSize)
},
}
} }
func initHTTPClient(cfg *config.Config) { func initHTTPClient(cfg *config.Config) {
@@ -42,7 +33,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 +54,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 +76,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 +89,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 +106,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,
}),
) )
} }
} }

View File

@@ -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,12 +92,16 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error)
} }
if !cfg.Auth.ForceAllowApi { if !cfg.Auth.ForceAllowApi {
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled { if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
return "", "", "", ErrAuthHeaderUnavailable //return "", "", "", ErrAuthHeaderUnavailable
errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy"
return "", "", "", NewErrorWithStatusLookup(403, errMsg)
} }
} }
return user, repo, matcher, nil return user, repo, matcher, nil
} }
return "", "", "", ErrInvalidURL //return "", "", "", ErrNotFound
errMsg := "Didn't match any matcher"
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
} }
func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) { func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) {
@@ -158,17 +139,11 @@ func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) {
return true, matcher, 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")
} }
@@ -243,8 +218,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 +243,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 +299,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 {

View File

@@ -6,14 +6,12 @@ import (
"github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app"
) )
// 设置请求头
func setRequestHeaders(c *app.RequestContext, req *http.Request) { func setRequestHeaders(c *app.RequestContext, req *http.Request) {
c.Request.Header.VisitAll(func(key, value []byte) { c.Request.Header.VisitAll(func(key, value []byte) {
req.Header.Set(string(key), string(value)) headerKey := string(key)
headerValue := string(value)
if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove {
req.Header.Set(headerKey, headerValue)
}
}) })
} }
func removeWSHeader(req *http.Request) {
req.Header.Del("Upgrade")
req.Header.Del("Connection")
}

72
proxy/routing.go Normal file
View File

@@ -0,0 +1,72 @@
package proxy
import (
"context"
"ghproxy/config"
"ghproxy/rate"
"strings"
"github.com/cloudwego/hertz/pkg/app"
)
func RoutingHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
var shoudBreak bool
shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
if shoudBreak {
return
}
var (
rawPath string
)
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
var (
user string
repo string
matcher string
)
user = c.Param("user")
repo = c.Param("repo")
matcher = c.GetString("matcher")
logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
logDump("%s", c.Request.Header.Header())
shoudBreak = listCheck(cfg, c, user, repo, rawPath)
if shoudBreak {
return
}
shoudBreak = authCheck(c, cfg, matcher, rawPath)
if shoudBreak {
return
}
// 处理blob/raw路径
if matcher == "blob" {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
}
// 为rawpath加入https:// 头
rawPath = "https://" + rawPath
logDebug("Matched: %v", matcher)
switch matcher {
case "releases", "blob", "raw", "gist", "api":
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
case "clone":
GitReq(ctx, c, rawPath, cfg, "git")
default:
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher)
return
}
}
}

90
proxy/utils.go Normal file
View File

@@ -0,0 +1,90 @@
package proxy
import (
"fmt"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/rate"
"github.com/cloudwego/hertz/pkg/app"
)
func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool {
// 白名单检查
if cfg.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
}

View File

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