Compare commits

...

43 Commits

Author SHA1 Message Date
wjqserver
c522eba7ae update 2025-04-04 16:40:34 +08:00
wjqserver
3da54f0599 update changelog 2025-04-04 11:28:00 +08:00
wjqserver
886c99f53d update 2025-04-03 18:23:16 +08:00
wjqserver
d3520a2133 update 2025-04-03 18:20:41 +08:00
wjqserver
1f0b43ec43 fix docker build issue 2025-04-03 18:20:28 +08:00
wjqserver
36646ebf7e 25w26a 2025-04-03 18:17:08 +08:00
wjqserver
d7ed4fc6ad update changelog 2025-04-03 18:13:09 +08:00
wjqserver
7cbce12316 update reademe.md 2025-04-03 17:59:01 +08:00
wjqserver
ff412f94ec add docs 2025-04-03 16:39:56 +08:00
wjqserver
b02aaeba8a update for merge 2025-04-01 22:00:10 +08:00
wjqserver
395f641468 [break] change auth config & add auth key 2025-04-01 18:32:45 +08:00
wjqserver
978ece6fa0 remove reWriteEncodeHeader 2025-03-30 17:25:36 +08:00
wjqserver
1adc3a3192 update 2025-03-30 17:02:45 +08:00
wjqserver
a66452cf10 e3.0.7 2025-03-29 12:26:26 +08:00
wjqserver
d231fd839f add no-cache for smart-git 2025-03-29 12:23:19 +08:00
wjqserver
4b37c6bb2b depr -cfg flag & change to -c 2025-03-29 12:01:46 +08:00
wjqserver
153b544024 e3.0.6 2025-03-28 12:05:38 +08:00
wjqserver
635c22f9a7 fix status code 2025-03-28 12:05:19 +08:00
wjqserver
f342312b40 update dockerfile 2025-03-28 11:26:46 +08:00
wjqserver
acaf38b88d e3.0.5 2025-03-28 11:06:18 +08:00
wjqserver
50cfd64db8 update readme.md 2025-03-28 10:37:07 +08:00
wjqserver
53e115242a add default config 2025-03-28 05:58:51 +08:00
wjqserver
cef0338d36 e3.0.3 2025-03-27 12:00:59 +08:00
wjqserver
f8edb0e0bc update&sync changelog 2025-03-27 11:48:02 +08:00
wjqserver
c11f368a9c update&sync changelog 2025-03-27 11:46:08 +08:00
wjqserver
db38b2a402 update v3 workflow 2025-03-27 11:38:12 +08:00
wjqserver
accb52b952 e3.0.3rc2 2025-03-27 11:36:57 +08:00
wjqserver
70fb808acf [port] update matcher 2025-03-27 11:25:08 +08:00
wjqserver
b684227191 [port] config add rewriteAPI 2025-03-27 11:19:42 +08:00
wjqserver
1498156f56 e3.0.3rc1 2025-03-25 23:38:51 +08:00
wjqserver
55158c0cb1 update 2025-03-25 23:35:40 +08:00
WJQSERVER
6c3280f850 3.0.2 (fix 3.0.1)
3.0.2 (fix 3.0.1)
2025-03-21 20:00:12 +08:00
wjqserver
866275aad3 update deps 2025-03-21 19:55:25 +08:00
wjqserver
f4cd7eecf1 3.0.2 2025-03-21 19:53:55 +08:00
wjqserver
5501cd3e3c 25w22a 2025-03-21 18:53:08 +08:00
WJQSERVER
f9f37262f0 v3.0.1 Next Step (Fix & Optimize)
v3.0.1 Next Step
Fix & Optimize
2025-03-21 02:14:00 +08:00
wjqserver
026039e0bc 25w21e 2025-03-21 02:03:29 +08:00
wjqserver
8739027772 3.0.1 2025-03-21 01:28:32 +08:00
wjqserver
cafc713a65 25w21c 2025-03-20 23:01:44 +08:00
wjqserver
8f2cc820aa 25w21b 2025-03-20 15:02:27 +08:00
wjqserver
139fc92abc fix log output 2025-03-20 15:01:15 +08:00
wjqserver
e9d793c104 fix log output 2025-03-20 14:57:20 +08:00
wjqserver
c931017f03 25w21a 2025-03-20 14:47:02 +08:00
38 changed files with 1203 additions and 628 deletions

View File

@@ -15,5 +15,5 @@ jobs:
uses: pozil/auto-assign-issue@v1 uses: pozil/auto-assign-issue@v1
with: with:
repo-token: ${{ secrets.AUTO_ASSIGN }} repo-token: ${{ secrets.AUTO_ASSIGN }}
assignees: WJQSERVER, satomitoka assignees: WJQSERVER, satomitouka
numOfAssignee: 2 numOfAssignee: 2

View File

@@ -13,6 +13,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with:
ref: main
- name: 加载版本号 - name: 加载版本号
run: | run: |
if [ -f VERSION ]; then if [ -f VERSION ]; then
@@ -49,6 +51,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with:
ref: main
- name: 加载版本号 - name: 加载版本号
run: | run: |
if [ -f VERSION ]; then if [ -f VERSION ]; then
@@ -107,6 +111,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: main
- name: Load VERSION - name: Load VERSION
run: | run: |
if [ -f VERSION ]; then if [ -f VERSION ]; then
@@ -135,4 +141,4 @@ jobs:
push: true push: true
tags: | tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }} ${{ env.IMAGE_NAME }}:${{ env.VERSION }}
${{ env.IMAGE_NAME }}:latest ${{ env.IMAGE_NAME }}:latest

View File

@@ -1,8 +1,147 @@
# 更新日志 # 更新日志
3.0.0 - 2025-03-19 3.0.0 - 2025-04-04
--- ---
- RELEASE: Next Gen; 下一个起点; v3会与v2.4.0及以上版本保证兼容关系, 可平顺升级; - RELEASE: Next Gen; 下一个起点;
- CHANGE: 使用HertZ框架重构, 提升性能
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
- CHANGE: 加入`Mino`主题对接选项
- FIX: 修正部分日志输出问题
- CHANGE: 移除gin残留
- CHANGE: 移除无用传入参数, 调整代码结构
- CHANGE: 改进cli
- CHANGE: 改进`脚本嵌套加速处理器`
- CHANGE&FIX: 使用`c.SetBodyStream`方式, 修正此前`chunked`传输中存在的诸多问题, 参看[HertZ Issues #1309](https://github.com/cloudwego/hertz/issues/1309)
- PORT: 从v2移植`matcher`相关改进
- CHANGE: 增加默认配置生成
- CHANGE: 优化前端资源加载
- CHANGE: 将`cfg`flag改为`c`以符合`POSIX`规范
- CHANGE: 为`smart-git`添加`no-cache`标头
25w26a - 2025-04-03
---
- PRE-RELEASE: 此版本是v3的预发布版本,请勿在生产环境中使用;
2.6.3 - 2025-03-30
---
- FIX: 修正一些`git clone`行为异常
25w25a - 2025-03-30
---
- PRE-RELEASE: 此版本是v2.6.3的预发布版本,请勿在生产环境中使用;
- FIX: 修正一些`git clone`行为异常
e3.0.7 -2025-03-29
---
- CHANGE: 将`cfg`flag改为`c`以符合`POSIX`规范
- CHANGE: 为`smart-git`添加`no-cache`标头
2.6.2 - 2025-03-29
---
- BACKPORT: 反向移植前端资源加载改进
e3.0.6 - 2025-03-28
---
- ATTENTION: 此版本是实验性的, 请确保了解这一点
- FIX: 修正状态码相关问题(开发遗留所致)
e3.0.5 - 2025-03-28
---
- ATTENTION: 此版本是实验性的, 请确保了解这一点
- CHANGE: 增加默认配置生成
- CHANGE: 优化前端资源加载
2.6.1 - 2025-03-27
---
- CHANGE: 改进`matcher`组件
- CHANGE: 加入优雅关闭
e3.0.3 - 2025-03.27
---
- ATTENTION: 此版本是实验性的, 请确保了解这一点
- E-RELEASE: 修正过往问题, 还请各位多多测试反馈
- PORT: 从v2移植`matcher`相关改进
- CHANGE&FIX: 使用`c.SetBodyStream`方式, 修正此前`chunked`传输中存在的诸多问题, 参看[HertZ Issues #1309](https://github.com/cloudwego/hertz/issues/1309)
25w24a - 2025-03-27
---
- PRE-RELEASE: 此版本是v2.6.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进`matcher`组件
- CHANGE: 加入优雅关闭
e3.0.3rc2 - 2025-03-27
---
- ATTENTION: 此版本是实验性的, 请确保了解这一点
- PRE-RELEASE: 此版本是v3.0.3的候选版本,请勿在生产环境中使用;
- PORT: 从v2移植`matcher`相关改进
e3.0.3rc1 - 2025-03-26
---
- ATTENTION: 此版本是实验性的, 请确保了解这一点
- PRE-RELEASE: 此版本是v3.0.3的候选版本,请勿在生产环境中使用;
- CHANGE&FIX: 使用`c.SetBodyStream`方式, 修正此前`chunked`传输中存在的诸多问题, 参看[HertZ Issues #1309](https://github.com/cloudwego/hertz/issues/1309)
2.6.0 - 2025-03-22
---
- BACKPORT: 将v3的功能性改进反向移植
25w23a - 2025-03-22
---
- PRE-RELEASE: 此版本是v2.6.0的预发布版本,请勿在生产环境中使用;
- BACKPORT: 将v3的功能性改进反向移植
e3.0.2 - 2025-03-21
---
- ATTENTION: 此版本是实验性的, 请确保了解这一点
- RELEASE: 在此表达对各位的歉意, v3迁移到HertZ带来了许多问题; 此版本完善v3的同时, 修正已知问题;
- FIX: 使用等效`c.Writer()`, 回归v2.5.0 func以修正问题
- CHANGE: 更新相关依赖
25w22a - 2025-03-21
---
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
- FIX: 使用等效`c.Writer()`, 回归v2.5.0 func以修正问题
e3.0.1 - 2025-03-21
---
- ATTENTION: 此版本是实验性的, 请确保了解这一点
- RELEASE: Next Step; 下一步; 完善v3的同时, 修正已知问题;
- CHANGE: 改进cli
- CHANGE: 重写`ProcessLinksAndWriteChunked`(脚本嵌套加速处理器), 修正已知问题的同时提高性能与效率
- CHANGE: 完善`gitreq`部分
- FIX: 修正日志输出格式问题
- FIX: 使用更新的`hwriter`以修正相关问题
25w21e - 2025-03-21
---
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 重写`ProcessLinksAndWriteChunked`(脚本嵌套加速处理器), 修正已知问题的同时提高性能与效率
25w21d - 2025-03-21
---
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
- FIX: 使用更新的`hwriter`以修正相关问题
25w21c - 2025-03-20
---
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
- TEST: 测试新的`hwriter`
25w21b - 2025-03-20
---
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
- FIX: 修正日志输出格式问题
25w21a - 2025-03-20
---
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进cli
- CHANGE: 完善`gitreq`部分
e3.0.0 - 2025-03-19
---
- ATTENTION: 此版本是实验性的, 请确保了解这一点
- RELEASE: Next Gen; 下一个起点;
- CHANGE: 使用HertZ框架重构, 提升性能 - CHANGE: 使用HertZ框架重构, 提升性能
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题 - CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
- CHANGE: 加入`Mino`主题对接选项 - CHANGE: 加入`Mino`主题对接选项
@@ -12,7 +151,7 @@
25w20b - 2025-03-19 25w20b - 2025-03-19
--- ---
- PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用; v3.0.0会与v2.4.0及以上保证兼容关系, 可平顺升级; - PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`Mino`主题对接选项 - CHANGE: 加入`Mino`主题对接选项
- FIX: 修正部分日志输出问题 - FIX: 修正部分日志输出问题
- CHANGE: 移除gin残留 - CHANGE: 移除gin残留
@@ -20,7 +159,7 @@
25w20a - 2025-03-18 25w20a - 2025-03-18
--- ---
- PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用; v3.0.0会与v2.4.0及以上保证兼容关系, 可平顺升级; - PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用HertZ重构 - CHANGE: 使用HertZ重构
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题 - CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
@@ -1115,4 +1254,4 @@ v0.1.0
- ADD: 实现符合[RFC 7234](https://httpwg.org/specs/rfc7234.html)的HTTP缓存机制 - ADD: 实现符合[RFC 7234](https://httpwg.org/specs/rfc7234.html)的HTTP缓存机制
- ADD: 实现action编译 - ADD: 实现action编译
- ADD: 实现Docker部署 - ADD: 实现Docker部署
- INFO: 使用Caddy作为Web服务器通过Caddy实现了缓存与速率限制 - INFO: 使用Caddy作为Web服务器通过Caddy实现了缓存与速率限制

View File

@@ -1 +1 @@
25w20b 25w26a

173
README.md
View File

@@ -1,54 +1,36 @@
# GHProxy # GHProxy
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg) ![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)
![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)
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署 使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
[DEMO](https://ghproxy.1888866.xyz)
[TG讨论群组](https://t.me/ghproxy_go)
[版本更新介绍](https://blog.wjqserver.com/categories/my-program/)
## 项目说明 ## 项目说明
### 项目特点 ### 项目特点
- 基于Go语言实现,支持多平台 - **基于 Go 语言实现,跨平台的同时提供高并发性能**
- 使用字节旗下的[HertZ](https://github.com/cloudwego/hertz)作为Web框架 - 🌐 **使用字节旗下的 [HertZ](https://github.com/cloudwego/hertz) 作为 Web 框架**
- 使用[Touka-HTTPC](https://github.com/satomitouka/touka-httpc)作为HTTP客户端 - 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端**
- 支持Git clone,raw,realeases等文件拉取 - 📥 **支持 Git clonerawreleases 等文件拉取**
- 支持多个前端主题 - 🎨 **支持多个前端主题**
- 支持自定义黑名单/白名单 - 🚫 **支持自定义黑名单/白名单**
- 支持Git Clone缓存(配合组件) - 🗄️ **支持 Git Clone 缓存配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git)**
- 支持Docker部署 - 🐳 **支持 Docker 部署**
- 支持速率限制 - **支持速率限制**
- 支持用户鉴权 - 🔒 **支持用户鉴权**
- 支持shell脚本嵌套加速 - 🐚 **支持 shell 脚本嵌套加速**
- 基于[WJQSERVER-STUDIO/golang-temp](https://github.com/WJQSERVER-STUDIO/golang-temp)模板构建,具有标准化的日志记录与构建流程
### 项目开发过程 ### 项目相关
**本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能** [DEMO](https://ghproxy.1888866.xyz)
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
- v3.0.0 迁移到HertZ框架, 进一步提升效率, 同时v3.0.0与v2.4.0及以上版本兼容, 可直接平顺升级 [TG讨论群组](https://t.me/ghproxy_go)
- v2.4.1 对路径匹配进行优化
- v2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
- v1.0.0 迁移至本仓库,并再次重构内容实现
- v0.2.0 重构项目实现
### LICENSE [相关文章](https://blog.wjqserver.com/categories/my-program/)
本项目使用WJQserver Studio License 2.0 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) [项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md)
在v2.3.0之前, 本项目使用WJQserver Studio License 1.2 ### 使用示例
在v1.0.0版本之前,本项目继承于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的APACHE2.0 LICENSE VERSION
## 使用示例
``` ```
# 下载文件 # 下载文件
@@ -62,6 +44,8 @@ git clone https://ghproxy.1888866.xyz/https://github.com/WJQSERVER-STUDIO/ghprox
## 部署说明 ## 部署说明
可参考文章: https://blog.wjqserver.com/post/ghproxy-deploy-with-smart-git/
### Docker部署 ### Docker部署
- Docker-cli - Docker-cli
@@ -90,107 +74,30 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox
## 配置说明 ## 配置说明
### 外部配置文件 参看[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md)
本项目采用`config.toml`作为外部配置,默认配置如下
使用Docker部署时,慎重修改`config.toml`,以免造成不必要的麻烦
```toml
[server]
host = "0.0.0.0" # 监听地址
port = 8080 # 监听端口
sizeLimit = 125 # 125MB
H2C = true # 是否开启H2C传输
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; 除以上特殊情况, 会将值直接传入
[httpc]
mode = "auto" # "auto" or "advanced" HTTP客户端模式 自动/高级模式
maxIdleConns = 100 # only for advanced mode 仅用于高级模式
maxIdleConnsPerHost = 60 # only for advanced mode 仅用于高级模式
maxConnsPerHost = 0 # only for advanced mode 仅用于高级模式
[gitclone]
mode = "bypass" # bypass / cache 运行模式, cache模式依赖smart-git
smartGitAddr = "http://127.0.0.1:8080" # smart-git组件地址
ForceH2C = false # 强制使用H2C连接
[shell]
editor = false # 脚本嵌套加速
[pages]
mode = "internal" # "internal" or "external" 内部/外部 前端 默认内部
theme = "bootstrap" # "bootstrap" or "nebula" 内置主题
staticPath = "/data/www" # 静态页面文件路径
[log]
logFilePath = "/data/ghproxy/log/ghproxy.log" # 日志文件路径
maxLogSize = 5 # MB 日志文件最大大小
level = "info" # 日志级别 dump, debug, info, warn, error, none
[auth]
authMethod = "parameters" # 鉴权方式,支持parameters,header
authToken = "token" # 用户鉴权Token
enabled = false # 是否开启用户鉴权
ForceAllowApi = false # 在不开启Header鉴权的情况下允许api代理
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json" # 黑名单文件路径
enabled = false # 是否开启黑名单
[whitelist]
enabled = false # 是否开启白名单
whitelistFile = "/data/ghproxy/config/whitelist.json" # 白名单文件路径
[rateLimit]
enabled = false # 是否开启速率限制
rateMethod = "total" # "ip" or "total" 速率限制方式
ratePerMinute = 180 # 每分钟限制请求数量
burst = 5 # 突发请求数量
[outbound]
enabled = false # 是否使用自定义代理出站
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" 支持Socks5/HTTP(S)出站传输
```
### 黑名单配置
黑名单配置位于config/blacklist.json,格式如下:
```json
{
"blacklist": [
"test/test1",
"example/repo2",
"another/*"
"another"
]
}
```
### 白名单配置
白名单配置位于config/whitelist.json,格式如下:
```json
{
"whitelist": [
"test/test1",
"example/repo2",
"another/*"
"another"
]
}
```
### 前端页面 ### 前端页面
#### Bootstrap主题 参看[GHProxy-Frontend](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
![ghproxy-demo.png](https://webp.wjqserver.com/ghproxy/1.8.1-light.png)
![ghproxy-demo-dark.png](https://webp.wjqserver.com/ghproxy/1.8.1-dark.png)
#### Nebula主题 ## 项目简史
![nebula-dark-v2.3.0.png](https://webp.wjqserver.com/ghproxy/nebula-dark.png)
![nebula-light-v2.3.0.png](https://webp.wjqserver.com/ghproxy/nebula-light.png) **本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能**
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
- v3.0.0 迁移到HertZ框架, 进一步提升效率
- v2.4.1 对路径匹配进行优化
- v2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
- v1.0.0 迁移至本仓库,并再次重构内容实现
- v0.2.0 重构项目实现
## LICENSE
本项目使用WJQserver Studio License 2.0 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)
在v2.3.0之前, 本项目使用WJQserver Studio License 1.2
在v1.0.0版本之前,本项目继承于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的APACHE2.0 LICENSE VERSION
## 赞助 ## 赞助

View File

@@ -6,8 +6,9 @@
| 版本 | 是否支持 | | 版本 | 是否支持 |
| --- | --- | | --- | --- |
| v2.x.x | :white_check_mark: 当前最新版本序列, 受支持 | | v3.x.x | :white_check_mark: 当前最新版本序列 |
| v1.x.x | :x: 这些版本已结束生命周期,不受支持 | | v2.x.x | :x: 这些版本已结束生命周期,不受支持 |
| v1.x.x | :x: 这些版本已结束生命周期,不受支持 |
| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 | | 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 | | 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
| v0.x.x | :x: 这些版本不再受支持 | | v0.x.x | :x: 这些版本不再受支持 |
@@ -16,9 +17,9 @@
本项目为开源项目,开发者不对使用本项目造成的任何损失或问题承担责任。用户需自行评估并承担使用本项目的风险。 本项目为开源项目,开发者不对使用本项目造成的任何损失或问题承担责任。用户需自行评估并承担使用本项目的风险。
使用本项目,请遵循 **[WSL (WJQSERVER-STUDIO LICENSE)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。 使用本项目,请遵循 **[WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。
本项目所有文件均受到 WSL (WJQSERVER-STUDIO LICENSE) 协议保护,任何人不得在任何情况下以非 WSL (WJQSERVER-STUDIO LICENSE) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。 本项目所有文件均受到 WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0) 协议保护,任何人不得在任何情况下以非 WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。
## 报告漏洞 ## 报告漏洞

View File

@@ -3,6 +3,7 @@ package api
import ( import (
"context" "context"
"ghproxy/config" "ghproxy/config"
"ghproxy/middleware/nocache"
"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,18 +19,8 @@ var (
logError = logger.LogError logError = logger.LogError
) )
func NoCacheMiddleware() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
// 设置禁止缓存的响应头
c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate")
c.Response.Header.Set("Pragma", "no-cache")
c.Response.Header.Set("Expires", "0")
c.Next(ctx) // 继续处理请求
}
}
func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) { func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
apiRouter := r.Group("/api", NoCacheMiddleware()) apiRouter := r.Group("/api", nocache.NoCacheMiddleware())
{ {
apiRouter.GET("/size_limit", func(ctx context.Context, c *app.RequestContext) { apiRouter.GET("/size_limit", func(ctx context.Context, c *app.RequestContext) {
SizeLimitHandler(cfg, c, ctx) SizeLimitHandler(cfg, c, ctx)
@@ -65,7 +56,7 @@ func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
sizeLimit := cfg.Server.SizeLimit sizeLimit := cfg.Server.SizeLimit
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"MaxResponseBodySize": sizeLimit, "MaxResponseBodySize": sizeLimit,
@@ -73,7 +64,7 @@ func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Con
} }
func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Whitelist": cfg.Whitelist.Enabled, "Whitelist": cfg.Whitelist.Enabled,
@@ -81,7 +72,7 @@ func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
} }
func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Blacklist": cfg.Blacklist.Enabled, "Blacklist": cfg.Blacklist.Enabled,
@@ -89,7 +80,7 @@ func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
} }
func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Cors": cfg.Server.Cors, "Cors": cfg.Server.Cors,
@@ -97,7 +88,7 @@ func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Co
} }
func HealthcheckHandler(c *app.RequestContext, ctx context.Context) { func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Status": "OK", "Status": "OK",
@@ -105,7 +96,7 @@ func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
} }
func VersionHandler(c *app.RequestContext, ctx context.Context, version string) { func VersionHandler(c *app.RequestContext, ctx context.Context, version string) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Version": version, "Version": version,
@@ -113,7 +104,7 @@ func VersionHandler(c *app.RequestContext, ctx context.Context, version string)
} }
func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"RateLimit": cfg.RateLimit.Enabled, "RateLimit": cfg.RateLimit.Enabled,
@@ -121,7 +112,7 @@ func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx conte
} }
func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"RatePerMinute": cfg.RateLimit.RatePerMinute, "RatePerMinute": cfg.RateLimit.RatePerMinute,
@@ -129,7 +120,7 @@ func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx contex
} }
func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) { func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"enabled": cfg.GitClone.Mode == "cache", "enabled": cfg.GitClone.Mode == "cache",

View File

@@ -12,13 +12,19 @@ func AuthHeaderHandler(c *app.RequestContext, cfg *config.Config) (isValid bool,
return true, nil return true, nil
} }
// 获取"GH-Auth"的值 // 获取"GH-Auth"的值
authToken := string(c.GetHeader("GH-Auth")) var authToken string
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken) if cfg.Auth.Key != "" {
authToken = string(c.GetHeader(cfg.Auth.Key))
} else {
authToken = string(c.GetHeader("GH-Auth"))
}
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken)
if authToken == "" { if authToken == "" {
return false, fmt.Errorf("Auth token not found") return false, fmt.Errorf("Auth token not found")
} }
isValid = authToken == cfg.Auth.AuthToken isValid = authToken == cfg.Auth.Token
if !isValid { if !isValid {
return false, fmt.Errorf("Auth token incorrect") return false, fmt.Errorf("Auth token incorrect")
} }

View File

@@ -12,14 +12,20 @@ func AuthParametersHandler(c *app.RequestContext, cfg *config.Config) (isValid b
return true, nil return true, nil
} }
authToken := c.Query("auth_token") var authToken string
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken) if cfg.Auth.Key != "" {
authToken = c.Query(cfg.Auth.Key)
} else {
authToken = c.Query("auth_token")
}
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken)
if authToken == "" { if authToken == "" {
return false, fmt.Errorf("Auth token not found") return false, fmt.Errorf("Auth token not found")
} }
isValid = authToken == cfg.Auth.AuthToken isValid = authToken == cfg.Auth.Token
if !isValid { if !isValid {
return false, fmt.Errorf("Auth token invalid") return false, fmt.Errorf("Auth token invalid")
} }

View File

@@ -37,17 +37,17 @@ func Init(cfg *config.Config) {
} }
func AuthHandler(ctx context.Context, c *app.RequestContext, cfg *config.Config) (isValid bool, err error) { func AuthHandler(ctx context.Context, c *app.RequestContext, cfg *config.Config) (isValid bool, err error) {
if cfg.Auth.AuthMethod == "parameters" { if cfg.Auth.Method == "parameters" {
isValid, err = AuthParametersHandler(c, cfg) isValid, err = AuthParametersHandler(c, cfg)
return isValid, err return isValid, err
} else if cfg.Auth.AuthMethod == "header" { } else if cfg.Auth.Method == "header" {
isValid, err = AuthHeaderHandler(c, cfg) isValid, err = AuthHeaderHandler(c, cfg)
return isValid, err return isValid, err
} else if cfg.Auth.AuthMethod == "" { } else if cfg.Auth.Method == "" {
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")
return false, fmt.Errorf(fmt.Sprintf("Auth method %s not supported", cfg.Auth.AuthMethod)) return false, fmt.Errorf(fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method))
} }
} }

View File

@@ -1,6 +1,8 @@
package config package config
import ( import (
"os"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
@@ -64,15 +66,17 @@ type GitCloneConfig struct {
/* /*
[shell] [shell]
editor = true editor = true
rewriteAPI = false
*/ */
type ShellConfig struct { type ShellConfig struct {
Editor bool `toml:"editor"` Editor bool `toml:"editor"`
RewriteAPI bool `toml:"rewriteAPI"`
} }
/* /*
[pages] [pages]
mode = "internal" # "internal" or "external" mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula" or "design" or "classic" theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www" staticDir = "/data/www"
*/ */
type PagesConfig struct { type PagesConfig struct {
@@ -89,16 +93,18 @@ type LogConfig struct {
/* /*
[auth] [auth]
authMethod = "parameters" # "header" or "parameters" Method = "parameters" # "header" or "parameters"
authToken = "token" Key = ""
Token = "token"
enabled = false enabled = false
passThrough = false passThrough = false
ForceAllowApi = true ForceAllowApi = true
*/ */
type AuthConfig struct { type AuthConfig struct {
Enabled bool `toml:"enabled"` Enabled bool `toml:"enabled"`
AuthMethod string `toml:"authMethod"` Method string `toml:"method"`
AuthToken string `toml:"authToken"` Key string `toml:"key"`
Token string `toml:"token"`
PassThrough bool `toml:"passThrough"` PassThrough bool `toml:"passThrough"`
ForceAllowApi bool `toml:"ForceAllowApi"` ForceAllowApi bool `toml:"ForceAllowApi"`
} }
@@ -132,9 +138,101 @@ type OutboundConfig struct {
// LoadConfig 从 TOML 配置文件加载配置 // LoadConfig 从 TOML 配置文件加载配置
func LoadConfig(filePath string) (*Config, error) { func LoadConfig(filePath string) (*Config, error) {
if !FileExists(filePath) {
// 楔入配置文件
err := DefaultConfig().WriteConfig(filePath)
if err != nil {
return nil, err
}
return DefaultConfig(), nil
}
var config Config var config Config
if _, err := toml.DecodeFile(filePath, &config); err != nil { if _, err := toml.DecodeFile(filePath, &config); err != nil {
return nil, err return nil, err
} }
return &config, nil return &config, nil
} }
// 写入配置文件
func (c *Config) WriteConfig(filePath string) error {
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
encoder := toml.NewEncoder(file)
return encoder.Encode(c)
}
// 检测文件是否存在
func FileExists(filename string) bool {
_, err := os.Stat(filename)
return !os.IsNotExist(err)
}
// 默认配置结构体
func DefaultConfig() *Config {
return &Config{
Server: ServerConfig{
Port: 8080,
Host: "0.0.0.0",
SizeLimit: 125,
H2C: true,
Cors: "*",
Debug: false,
},
Httpc: HttpcConfig{
Mode: "auto",
MaxIdleConns: 100,
MaxIdleConnsPerHost: 60,
MaxConnsPerHost: 0,
},
GitClone: GitCloneConfig{
Mode: "bypass",
SmartGitAddr: "http://127.0.0.1:8080",
ForceH2C: false,
},
Shell: ShellConfig{
Editor: false,
RewriteAPI: false,
},
Pages: PagesConfig{
Mode: "internal",
Theme: "bootstrap",
StaticDir: "/data/www",
},
Log: LogConfig{
LogFilePath: "/data/ghproxy/log/ghproxy.log",
MaxLogSize: 10,
Level: "info",
},
Auth: AuthConfig{
Enabled: false,
Method: "parameters",
Key: "",
Token: "token",
PassThrough: false,
ForceAllowApi: false,
},
Blacklist: BlacklistConfig{
Enabled: false,
BlacklistFile: "/data/ghproxy/config/blacklist.txt",
},
Whitelist: WhitelistConfig{
Enabled: false,
WhitelistFile: "/data/ghproxy/config/whitelist.txt",
},
RateLimit: RateLimitConfig{
Enabled: false,
RateMethod: "total",
RatePerMinute: 100,
Burst: 10,
},
Outbound: OutboundConfig{
Enabled: false,
Url: "socks5://127.0.0.1:1080",
},
}
}

View File

@@ -19,6 +19,7 @@ ForceH2C = false
[shell] [shell]
editor = false editor = false
rewriteAPI = false
[pages] [pages]
mode = "internal" # "internal" or "external" mode = "internal" # "internal" or "external"
@@ -31,8 +32,9 @@ maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none level = "info" # dump, debug, info, warn, error, none
[auth] [auth]
authMethod = "parameters" # "header" or "parameters" method = "parameters" # "header" or "parameters"
authToken = "token" token = "token"
key = ""
enabled = false enabled = false
passThrough = false passThrough = false
ForceAllowApi = false ForceAllowApi = false

View File

@@ -19,6 +19,7 @@ ForceH2C = false
[shell] [shell]
editor = false editor = false
rewriteAPI = false
[pages] [pages]
mode = "internal" # "internal" or "external" mode = "internal" # "internal" or "external"

View File

@@ -3,7 +3,7 @@ Description=Github Proxy Service
After=network.target After=network.target
[Service] [Service]
ExecStart=/bin/bash -c '/usr/local/ghproxy/ghproxy -cfg /usr/local/ghproxy/config/config.toml > /usr/local/ghproxy/log/run.log 2>&1' ExecStart=/bin/bash -c '/usr/local/ghproxy/ghproxy -c /usr/local/ghproxy/config/config.toml > /usr/local/ghproxy/log/run.log 2>&1'
WorkingDirectory=/usr/local/ghproxy WorkingDirectory=/usr/local/ghproxy
Restart=always Restart=always
User=root User=root

View File

@@ -123,7 +123,7 @@ Description=Github Proxy Service
After=network.target After=network.target
[Service] [Service]
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1' ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -c $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
WorkingDirectory=$ghproxy_dir WorkingDirectory=$ghproxy_dir
Restart=always Restart=always
User=root User=root

View File

@@ -123,7 +123,7 @@ Description=Github Proxy Service
After=network.target After=network.target
[Service] [Service]
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1' ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -c $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
WorkingDirectory=$ghproxy_dir WorkingDirectory=$ghproxy_dir
Restart=always Restart=always
User=root User=root

View File

@@ -4,8 +4,7 @@ services:
image: 'wjqserver/ghproxy:latest' image: 'wjqserver/ghproxy:latest'
restart: always restart: always
volumes: volumes:
- './ghproxy/log/run:/data/ghproxy/log' - './ghproxy/log:/data/ghproxy/log'
- './ghproxy/log/caddy:/data/caddy/log'
- './ghproxy/config:/data/ghproxy/config' - './ghproxy/config:/data/ghproxy/config'
ports: ports:
- '7210:8080' - '7210:8080'

View File

@@ -3,6 +3,7 @@ FROM alpine:latest AS builder
ARG USER=WJQSERVER-STUDIO ARG USER=WJQSERVER-STUDIO
ARG REPO=ghproxy ARG REPO=ghproxy
ARG APPLICATION=ghproxy ARG APPLICATION=ghproxy
ARG BRANCH=dev
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
ARG TARGETPLATFORM ARG TARGETPLATFORM
@@ -16,32 +17,35 @@ RUN mkdir -p /data/${APPLICATION}/log
RUN apk add --no-cache curl wget tar RUN apk add --no-cache curl wget tar
# 后端 # 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/dev/DEV-VERSION) && \ RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/DEV-VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \ wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \ tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/dev/docker/dockerfile/dev/init.sh
# 拉取配置 # 拉取配置
#RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/dev/caddyfile/dev/Caddyfile RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/config.toml
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/config.toml RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/blacklist.json
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/blacklist.json RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/whitelist.json
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/whitelist.json
# 权限 # 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION} RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache curl RUN apk add --no-cache curl
ARG USER=WJQSERVER-STUDIO
ARG REPO=ghproxy
ARG BRANCH=v3
ARG APPLICATION=ghproxy
ARG TARGETOS
ARG TARGETARCH
ARG TARGETPLATFORM
COPY --from=builder /data/www /data/www COPY --from=builder /data/www /data/www
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION} COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
COPY --from=builder /usr/local/bin/init.sh /usr/local/bin/init.sh
# 权限 # 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION} RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
CMD ["/usr/local/bin/init.sh"] CMD ["/data/ghproxy/ghproxy"]

View File

@@ -1,17 +0,0 @@
#!/bin/sh
APPLICATION=ghproxy
if [ ! -f /data/${APPLICATION}/config/blacklist.json ]; then
cp /data/${APPLICATION}/blacklist.json /data/${APPLICATION}/config/blacklist.json
fi
if [ ! -f /data/${APPLICATION}/config/whitelist.json ]; then
cp /data/${APPLICATION}/whitelist.json /data/${APPLICATION}/config/whitelist.json
fi
if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
fi
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1

View File

@@ -2,6 +2,7 @@ FROM alpine:latest AS builder
ARG USER=WJQSERVER-STUDIO ARG USER=WJQSERVER-STUDIO
ARG REPO=ghproxy ARG REPO=ghproxy
ARG BRANCH=main
ARG APPLICATION=ghproxy ARG APPLICATION=ghproxy
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
@@ -16,34 +17,37 @@ RUN mkdir -p /data/${APPLICATION}/log
RUN apk add --no-cache curl wget tar RUN apk add --no-cache curl wget tar
# 后端 # 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/VERSION) && \ RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \ wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \ tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/release/init.sh
# 拉取配置 # 拉取配置
#RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/release/Caddyfile RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/config.toml
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.toml RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/blacklist.json
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/blacklist.json RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/whitelist.json
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/whitelist.json
# 权限 # 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION} RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
FROM alpine:latest FROM alpine:latest
RUN apk add --no-cache curl RUN apk add --no-cache curl
ARG USER=WJQSERVER-STUDIO
ARG REPO=ghproxy
ARG BRANCH=v3
ARG APPLICATION=ghproxy
ARG TARGETOS
ARG TARGETARCH
ARG TARGETPLATFORM
COPY --from=builder /data/www /data/www COPY --from=builder /data/www /data/www
#COPY --from=builder /data/caddy /data/caddy
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION} COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
COPY --from=builder /usr/local/bin/init.sh /usr/local/bin/init.sh
# 权限 # 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION} RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
CMD ["/usr/local/bin/init.sh"] CMD ["/data/ghproxy/ghproxy"]

View File

@@ -1,17 +0,0 @@
#!/bin/sh
APPLICATION=ghproxy
if [ ! -f /data/${APPLICATION}/config/blacklist.json ]; then
cp /data/${APPLICATION}/blacklist.json /data/${APPLICATION}/config/blacklist.json
fi
if [ ! -f /data/${APPLICATION}/config/whitelist.json ]; then
cp /data/${APPLICATION}/whitelist.json /data/${APPLICATION}/config/whitelist.json
fi
if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
fi
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1

328
docs/config.md Normal file
View File

@@ -0,0 +1,328 @@
# ghproxy 用户配置文档
`ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml``blacklist.json``whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。
## `config.toml` - 主配置文件
`config.toml``ghproxy` 的主配置文件,采用 TOML 格式。您可以通过修改此文件来定制 `ghproxy` 的各项功能例如服务器端口、连接设置、Git 克隆模式、日志级别、认证方式、黑白名单以及限速策略等。
以下是 `config.toml` 文件的详细配置项说明:
```toml name=config/config.toml
[server]
host = "0.0.0.0"
port = 8080
sizeLimit = 125 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false
[httpc]
mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
[gitclone]
mode = "bypass" # bypass / cache
smartGitAddr = "http://127.0.0.1:8080"
ForceH2C = false
[shell]
editor = false
rewriteAPI = false
[pages]
mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www"
[log]
logFilePath = "/data/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none
[auth]
method = "parameters" # "header" or "parameters"
token = "token"
key = ""
enabled = false
passThrough = false
ForceAllowApi = false
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json"
enabled = false
[whitelist]
enabled = false
whitelistFile = "/data/ghproxy/config/whitelist.json"
[rateLimit]
enabled = false
rateMethod = "total" # "ip" or "total"
ratePerMinute = 180
burst = 5
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
```
### 配置项详细说明
* **`[server]` - 服务器配置**
* `host`: 监听地址。
* 类型: 字符串 (`string`)
* 默认值: `"0.0.0.0"` (监听所有)
* 说明: 设置 `ghproxy` 监听的网络地址。通常设置为 `"0.0.0.0"` 以监听所有可用的网络接口。
* `port`: 监听端口。
* 类型: 整数 (`int`)
* 默认值: `8080`
* 说明: 设置 `ghproxy` 监听的端口号。
* `sizeLimit`: 请求体大小限制。
* 类型: 整数 (`int`)
* 默认值: `125` (MB)
* 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。
* `H2C`: 是否启用 H2C (HTTP/2 Cleartext) 传输。
* 类型: 布尔值 (`bool`)
* 默认值: `true` (启用)
* 说明: 启用后,允许客户端使用 HTTP/2 协议进行无加密传输,提升性能。
* `cors`: CORS (跨域资源共享) 设置。
* 类型: 字符串 (`string`)
* 默认值: `"*"` (允许所有来源)
* 可选值:
* `""` 或`"*"`: 允许所有来源跨域访问。
* `"nil"`: 禁用 CORS。
* 具体的域名: 例如 `"https://example.com"`,只允许来自指定域名的跨域请求。
* 说明: 配置 CORS 策略,用于控制哪些域名可以跨域访问 `ghproxy` 服务。
* `debug`: 是否启用调试模式。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 会输出更详细的日志信息,用于开发和调试。
* **`[httpc]` - HTTP 客户端配置**
* `mode`: HTTP 客户端模式。
* 类型: 字符串 (`string`)
* 默认值: `"auto"` (自动模式)
* 可选值:
* `"auto"`: 自动模式,使用默认的 HTTP 客户端配置,适用于大多数场景。
* `"advanced"`: 高级模式,允许自定义连接池参数,可以更精细地控制 HTTP 客户端的行为。
* 说明: 选择 HTTP 客户端的运行模式。
* `maxIdleConns`: 最大空闲连接数 (仅在高级模式下生效)。
* 类型: 整数 (`int`)
* 默认值: `100`
* 说明: 设置 HTTP 客户端连接池中保持的最大空闲连接数。
* `maxIdleConnsPerHost`: 每个主机最大空闲连接数 (仅在高级模式下生效)。
* 类型: 整数 (`int`)
* 默认值: `60`
* 说明: 设置 HTTP 客户端连接池中,每个主机允许保持的最大空闲连接数。
* `maxConnsPerHost`: 每个主机最大连接数 (仅在高级模式下生效)。
* 类型: 整数 (`int`)
* 默认值: `0` (不限制)
* 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。
* **`[gitclone]` - Git 克隆配置**
* `mode`: Git 克隆模式。
* 类型: 字符串 (`string`)
* 默认值: `"bypass"` (绕过模式)
* 可选值:
* `"bypass"`: 绕过模式,直接克隆 GitHub 仓库,不使用任何缓存加速。
* `"cache"`: 缓存模式,使用智能 Git 服务加速克隆,需要配置 `smartGitAddr`。
* 说明: 选择 Git 克隆的模式。
* `smartGitAddr`: 智能 Git 服务地址 (仅在缓存模式下生效)。
* 类型: 字符串 (`string`)
* 默认值: `"http://127.0.0.1:8080"`
* 说明: 当 `mode` 设置为 `"cache"` 时,需要配置智能 Git 服务的地址,用于加速 Git 克隆。
* `ForceH2C`: 是否强制使用 H2C 连接到智能 Git 服务。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (不强制)
* 说明: 如果智能 Git 服务支持 H2C可以设置为 `true` 以强制使用 H2C 连接,提升性能。
* **`[shell]` - Shell 嵌套加速功能配置**
* `editor`: 是否启用编辑(嵌套加速)功能。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后, 会修改`.sh`文件内容以实现嵌套加速
* `rewriteAPI`: 是否重写 API 地址。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 会重写脚本内的Github API地址。
* **`[pages]` - Pages 服务配置**
* `mode`: Pages 服务模式。
* 类型: 字符串 (`string`)
* 默认值: `"internal"` (内置 Pages 服务)
* 可选值:
* `"internal"`: 使用 `ghproxy` 内置的 Pages 服务。
* `"external"`: 使用外部 Pages 位置。
* 说明: 选择 Pages 服务的运行模式。
* `theme`: Pages 主题。
* 类型: 字符串 (`string`)
* 默认值: `"bootstrap"`
* 可选值: 参看[GHProxy项目前端仓库](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
* 说明: 设置内置 Pages 服务使用的主题。
* `staticDir`: 静态文件目录。
* 类型: 字符串 (`string`)
* 默认值: `"/data/www"`
* 说明: 指定外置 Pages 服务使用的静态文件目录。
* **`[log]` - 日志配置**
* `logFilePath`: 日志文件路径。
* 类型: 字符串 (`string`)
* 默认值: `"/data/ghproxy/log/ghproxy.log"`
* 说明: 设置 `ghproxy` 日志文件的存储路径。
* `maxLogSize`: 最大日志文件大小。
* 类型: 整数 (`int`)
* 默认值: `5` (MB)
* 说明: 设置单个日志文件的最大大小,单位为 MB。当日志文件大小超过此限制时会进行日志轮转。
* `level`: 日志级别。
* 类型: 字符串 (`string`)
* 默认值: `"info"`
* 可选值: `"dump"`, `"debug"`, `"info"`, `"warn"`, `"error"`, `"none"`
* 说明: 设置日志输出的级别。级别越高,输出的日志信息越少。
* `"dump"`: 输出所有日志,包括最详细的调试信息。
* `"debug"`: 输出调试信息、信息、警告和错误日志。
* `"info"`: 输出信息、警告和错误日志。
* `"warn"`: 输出警告和错误日志。
* `"error"`: 仅输出错误日志。
* `"none"`: 禁用所有日志输出。
* **`[auth]` - 认证配置**
* `enabled`: 是否启用认证。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,需要提供正确的认证信息才能访问 `ghproxy` 服务。
* `method`: 认证方法。
* 类型: 字符串 (`string`)
* 默认值: `"parameters"` (URL 参数)
* 可选值: `"header"` 或 `"parameters"`
* `"header"`: 通过请求头 `GH-Auth` 或自定义请求头 (通过 `key` 配置) 传递认证 Token。
* `"parameters"`: 通过 URL 参数 `auth_token` 或自定义 URL 参数名 (通过 `Key` 配置) 传递认证 Token。
* 说明: 选择认证信息传递的方式。
* `key`: 自定义认证 Key。
* 类型: 字符串 (`string`)
* 默认值: `""` (空字符串,使用默认的 `GH-Auth` 请求头或 `auth_token` URL 参数名)
* 说明: 可以自定义认证时使用的请求头名称或 URL 参数名。如果为空,则使用默认的 `GH-Auth` 请求头或 `auth_token` URL 参数名。
* `token`: 认证 Token。
* 类型: 字符串 (`string`)
* 默认值: `"token"`
* 说明: 设置认证时需要提供的 Token 值。
* `passThrough`: 是否认证参数透穿到Github。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (不允许)
* 说明: 如果设置为 `true`相关参数会被透穿到Github。
* `ForceAllowApi`: 是否强制允许 API 访问。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (不强制允许)
* 说明: 如果设置为 `true`,则强制允许对 GitHub API 的访问,即使未启用认证或认证失败。
* **`[blacklist]` - 黑名单配置**
* `enabled`: 是否启用黑名单。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将根据 `blacklist.json` 文件中的规则阻止对特定用户或仓库的访问。
* `blacklistFile`: 黑名单文件路径。
* 类型: 字符串 (`string`)
* 默认值: `"/data/ghproxy/config/blacklist.json"`
* 说明: 指定黑名单配置文件的路径。
* **`[whitelist]` - 白名单配置**
* `enabled`: 是否启用白名单。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将只允许访问 `whitelist.json` 文件中规则指定的用户或仓库。白名单的优先级高于黑名单。
* `whitelistFile`: 白名单文件路径。
* 类型: 字符串 (`string`)
* 默认值: `"/data/ghproxy/config/whitelist.json"`
* 说明: 指定白名单配置文件的路径。
* **`[rateLimit]` - 限速配置**
* `enabled`: 是否启用限速。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将根据配置的策略限制请求速率,防止服务被滥用。
* `rateMethod`: 限速方法。
* 类型: 字符串 (`string`)
* 默认值: `"total"` (全局限速)
* 可选值: `"ip"` 或 `"total"`
* `"ip"`: 基于客户端 IP 地址进行限速,每个 IP 地址都有独立的速率限制。
* `"total"`: 全局限速,所有客户端共享同一个速率限制。
* 说明: 选择限速的策略。
* `ratePerMinute`: 每分钟允许的请求数。
* 类型: 整数 (`int`)
* 默认值: `180`
* 说明: 设置每分钟允许通过的最大请求数。
* `burst`: 突发请求数。
* 类型: 整数 (`int`)
* 默认值: `5`
* 说明: 允许在短时间内超过 `ratePerMinute` 的突发请求数。
* **`[outbound]` - 出站代理配置**
* `enabled`: 是否启用出站代理。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 启用后,`ghproxy` 将通过配置的代理服务器转发所有出站请求。
* `url`: 出站代理 URL。
* 类型: 字符串 (`string`)
* 默认值: `"socks5://127.0.0.1:1080"`
* 支持协议: `socks5://` 和 `http://`
* 说明: 设置出站代理服务器的 URL。支持 SOCKS5 和 HTTP 代理协议。
## `blacklist.json` - 黑名单配置
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。
```json name=config/blacklist.json
{
"blacklist": [
"eviluser",
"spamuser/bad-repo",
"malwareuser/*"
]
}
```
### 黑名单规则说明
* `blacklist`: 一个 JSON 数组,包含黑名单规则,每条规则为一个字符串。
* **用户名**: 例如 `"eviluser"`,阻止所有名为 `eviluser` 的用户的访问。
* **仓库名**: 例如 `"spamuser/bad-repo"`,阻止访问 `spamuser` 用户下的 `bad-repo` 仓库。
* **通配符**: 例如 `"malwareuser/*"`,使用 `*` 通配符,阻止访问 `malwareuser` 用户下的所有仓库。
* **缩略写法**: 例如 `"example"`, 等同于 `"example/*"` 允许访问 `example` 用户下的所有仓库。
## `whitelist.json` - 白名单配置
`whitelist.json` 文件用于配置白名单规则,只允许访问白名单中指定的用户或仓库。白名单的优先级高于黑名单,如果一个请求同时匹配黑名单和白名单,则白名单生效,请求将被允许。
```json name=config/whitelist.json
{
"whitelist": [
"white/list",
"white/test1",
"example/*",
"example"
]
}
```
### 白名单规则说明
* `whitelist`: 一个 JSON 数组,包含白名单规则,每条规则为一个字符串。
* **仓库名**: 例如 `"white/list"`,允许访问 `white` 用户下的 `list` 仓库。
* **仓库名**: 例如 `"white/test1"`,允许访问 `white` 用户下的 `test1` 仓库。
* **通配符**: 例如 `"example/*"`,使用 `*` 通配符,允许访问 `example` 用户下的所有仓库。
* **缩略写法**: 例如 `"example"`, 等同于 `"example/*"` 允许访问 `example` 用户下的所有仓库。
---

24
docs/flag.md Normal file
View File

@@ -0,0 +1,24 @@
# Flag
GHProxy接受以下flag传入
```bash
root@root:/data/ghproxy$ ghproxy -h
-c string
config file path (default "/data/ghproxy/config/config.toml")
-cfg value
exit
-h show help message and exit
-v show version and exit
```
- `-c`
类型: `string`
默认值: `/data/ghproxy/config/config.toml`
示例: `ghproxy -c /data/ghproxy/demo.toml`
- `-cfg`
已弃用, 被`-c`替代
- `-h`
显示帮助信息
- `-v`
显示版本号

17
docs/menu.md Normal file
View File

@@ -0,0 +1,17 @@
## GHProxy 文档
### 配置文件
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md
### Flag
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/flag.md
### 部署
参看 https://blog.wjqserver.com/post/ghproxy-deploy-with-smart-git/
### 前端
https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend

19
go.mod
View File

@@ -1,39 +1,38 @@
module ghproxy module ghproxy
go 1.24.1 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/hwriter v0.0.2
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.6
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.3.3
github.com/valyala/bytebufferpool v1.0.0 golang.org/x/net v0.38.0
golang.org/x/net v0.37.0
golang.org/x/time v0.11.0 golang.org/x/time v0.11.0
) )
require ( require (
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 // indirect github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 // indirect
github.com/bytedance/gopkg v0.1.1 // indirect github.com/bytedance/gopkg v0.1.2 // indirect
github.com/bytedance/sonic v1.13.1 // indirect github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/netpoll v0.6.5 // indirect github.com/cloudwego/gopkg v0.1.4 // indirect
github.com/cloudwego/netpoll v0.7.0 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/fsnotify/fsnotify v1.8.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.5.0 // indirect github.com/nyaruka/phonenumbers v1.6.0 // indirect
github.com/stretchr/testify v1.10.0 // 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
golang.org/x/arch v0.15.0 // indirect golang.org/x/arch v0.15.0 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sys v0.31.0 // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.6 // indirect
) )

80
go.sum
View File

@@ -2,32 +2,33 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
github.com/WJQSERVER-STUDIO/go-utils/hwriter v0.0.2 h1:z9xSC3qkt8Qjjb+KRV0Az5klUBJ/gE3berBbjVSFVzY=
github.com/WJQSERVER-STUDIO/go-utils/hwriter v0.0.2/go.mod h1:U3dVP2MzKJfK6dPiobxmSdynibqCOn1mxQEVLylESWA=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YYtrg1ixVSB/JvZM= 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/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 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po= github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po=
github.com/bytedance/gopkg v0.1.0/go.mod h1:FtQG3YbQG9L/91pbKSw787yBQPutC+457AvDW77fgUQ=
github.com/bytedance/gopkg v0.1.1 h1:3azzgSkiaw79u24a+w9arfH8OfnQQ4MHUt9lJFREEaE=
github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ=
github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4= github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4=
github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk= github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk=
github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
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/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI=
github.com/cloudwego/hertz v0.9.6 h1:Kj5SSPlKBC32NIN7+B/tt8O1pdDz8brMai00rqqjULQ= github.com/cloudwego/hertz v0.9.6 h1:Kj5SSPlKBC32NIN7+B/tt8O1pdDz8brMai00rqqjULQ=
github.com/cloudwego/hertz v0.9.6/go.mod h1:X5Ez52XhtszU4t+CTBGIJI4PqmcI1oSf8ULBz0SWfLo= github.com/cloudwego/hertz v0.9.6/go.mod h1:X5Ez52XhtszU4t+CTBGIJI4PqmcI1oSf8ULBz0SWfLo=
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.6.5 h1:6E/BWhSzQoyLg9Kx/4xiMdIIpovzwBtXvuqSqaTUzDQ= github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4=
github.com/cloudwego/netpoll v0.6.5/go.mod h1:BtM+GjKTdwKoC8IOzD08/+8eEn2gYoiNLipFca6BVXQ= github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.8.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=
@@ -42,8 +43,11 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/nyaruka/phonenumbers v1.5.0 h1:0M+Gd9zl53QC4Nl5z1Yj1O/zPk2XXBUwR/vlzdXSJv4= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/nyaruka/phonenumbers v1.5.0/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/nyaruka/phonenumbers v1.6.0 h1:r9ax45fFg+YLUs2X4bNXm5RAxWl00hYjFgNlv32vtHk=
github.com/nyaruka/phonenumbers v1.6.0/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/satomitouka/touka-httpc v0.3.3 h1:Th0uJ5do3oqqZgdUDtqD1SH11x8TcJmrwHMJQlEIKCg= github.com/satomitouka/touka-httpc v0.3.3 h1:Th0uJ5do3oqqZgdUDtqD1SH11x8TcJmrwHMJQlEIKCg=
@@ -61,6 +65,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
@@ -74,28 +80,66 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= golang.org/x/arch v0.15.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/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=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.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.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

18
init.sh
View File

@@ -1,18 +0,0 @@
#!/bin/bash
APPLICATON=ghproxy
if [ ! -f /data/${APPLICATON}/config/blacklist.json ]; then
cp /data/${APPLICATON}/blacklist.json /data/${APPLICATON}/config/blacklist.json
fi
if [ ! -f /data/${APPLICATON}/config/whitelist.json ]; then
cp /data/${APPLICATON}/whitelist.json /data/${APPLICATON}/config/whitelist.json
fi
if [ ! -f /data/${APPLICATON}/config/config.yaml ]; then
cp /data/${APPLICATON}/config.yaml /data/${APPLICATON}/config/config.yaml
fi
/data/${APPLICATON}/${APPLICATON} > /data/${APPLICATON}/log/run.log 2>&1

359
main.go
View File

@@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io/fs" "io/fs"
"net/http" "net/http"
"os"
"time" "time"
"ghproxy/api" "ghproxy/api"
@@ -27,27 +28,21 @@ import (
) )
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"
cfgfile string cfgfile string
version string version string
runMode string runMode string
limiter *rate.RateLimiter limiter *rate.RateLimiter
iplimiter *rate.IPRateLimiter iplimiter *rate.IPRateLimiter
showVersion bool
showHelp bool
) )
var ( var (
//go:embed pages/* //go:embed pages/*
pagesFS embed.FS pagesFS embed.FS
/*
//go:embed pages/bootstrap/*
BootstrapPagesFS embed.FS
//go:embed pages/nebula/*
NebulaPagesFS embed.FS
//go:embed pages/design/*
DesignPagesFS embed.FS
*/
) )
var ( var (
@@ -60,7 +55,48 @@ var (
) )
func readFlag() { func readFlag() {
flag.StringVar(&cfgfile, "cfg", configfile, "config file path") flag.StringVar(&cfgfile, "c", configfile, "config file path")
flag.Func("cfg", "exit", func(s string) error {
// 被弃用的flag, fail退出
fmt.Printf("\n")
fmt.Println("[ERROR] cfg flag is deprecated, please use -c instead")
fmt.Printf("\n")
flag.Usage()
os.Exit(2)
return nil
})
flag.BoolVar(&showVersion, "v", false, "show version and exit") // 添加-v标志
flag.BoolVar(&showHelp, "h", false, "show help message and exit") // 添加-h标志
// 捕获未定义的 flag
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, "\nInvalid flags:")
// 检查未定义的flags
invalidFlags := []string{}
for _, arg := range os.Args[1:] {
if arg[0] == '-' && arg != "-h" && arg != "-v" { // 检查是否是flag, 排除 -h 和 -v
defined := false
flag.VisitAll(func(f *flag.Flag) {
if "-"+f.Name == arg {
defined = true
}
})
if !defined {
invalidFlags = append(invalidFlags, arg)
}
}
}
for _, flag := range invalidFlags {
fmt.Fprintf(os.Stderr, " %s\n", flag)
}
if len(invalidFlags) > 0 {
os.Exit(2)
}
}
} }
func loadConfig() { func loadConfig() {
@@ -68,8 +104,11 @@ func loadConfig() {
cfg, err = config.LoadConfig(cfgfile) cfg, err = config.LoadConfig(cfgfile)
if err != nil { if err != nil {
fmt.Printf("Failed to load config: %v\n", err) fmt.Printf("Failed to load config: %v\n", err)
// 如果配置文件加载失败,也显示帮助信息并退出
flag.Usage()
os.Exit(1)
} }
if cfg.Server.Debug { if cfg != nil && cfg.Server.Debug { // 确保 cfg 不为 nil
fmt.Println("Config File Path: ", cfgfile) fmt.Println("Config File Path: ", cfgfile)
fmt.Printf("Loaded config: %v\n", cfg) fmt.Printf("Loaded config: %v\n", cfg)
} }
@@ -80,10 +119,12 @@ func setupLogger(cfg *config.Config) {
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize) err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
if err != nil { if err != nil {
fmt.Printf("Failed to initialize logger: %v\n", err) fmt.Printf("Failed to initialize logger: %v\n", err)
os.Exit(1)
} }
err = logger.SetLogLevel(cfg.Log.Level) err = logger.SetLogLevel(cfg.Log.Level)
if err != nil { if err != nil {
fmt.Printf("Logger Level Error: %v\n", err) fmt.Printf("Logger Level Error: %v\n", err)
os.Exit(1)
} }
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)
@@ -116,7 +157,7 @@ func InitReq(cfg *config.Config) {
} }
// loadEmbeddedPages 加载嵌入式页面资源 // loadEmbeddedPages 加载嵌入式页面资源
func loadEmbeddedPages(cfg *config.Config) (fs.FS, error) { func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
var pages fs.FS var pages fs.FS
var err error var err error
switch cfg.Pages.Theme { switch cfg.Pages.Theme {
@@ -138,149 +179,171 @@ func loadEmbeddedPages(cfg *config.Config) (fs.FS, error) {
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load embedded pages: %w", err) return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err)
} }
return pages, nil
var assets fs.FS
assets, err = fs.Sub(pagesFS, "pages/assets")
return pages, assets, nil
} }
// setupPages 设置页面路由 // setupPages 设置页面路由
func setupPages(cfg *config.Config, r *server.Hertz) { func setupPages(cfg *config.Config, r *server.Hertz) {
switch cfg.Pages.Mode { switch cfg.Pages.Mode {
case "internal": case "internal":
// 加载嵌入式资源 err := setInternalRoute(cfg, r)
pages, err := loadEmbeddedPages(cfg)
if err != nil { if err != nil {
logError("Failed when processing internal pages: %s", err) logError("Failed when processing internal pages: %s", err)
fmt.Println(err.Error())
return return
} }
// 设置嵌入式资源路由
r.GET("/", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
case "external": case "external":
// 设置外部资源路径 // 设置外部资源路径
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir) indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir) faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir) javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir)
stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir) stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir)
//bootstrapPath := fmt.Sprintf("%s/bootstrap.min.css", cfg.Pages.StaticDir) bootstrapPath := fmt.Sprintf("%s/bootstrap.min.css", cfg.Pages.StaticDir)
bootstrapBundlePath := fmt.Sprintf("%s/bootstrap.bundle.min.js", cfg.Pages.StaticDir)
// 设置外部资源路由 // 设置外部资源路由
r.StaticFile("/", indexPagePath) r.StaticFile("/", indexPagePath)
r.StaticFile("/favicon.ico", faviconPath) r.StaticFile("/favicon.ico", faviconPath)
r.StaticFile("/script.js", javascriptsPath) r.StaticFile("/script.js", javascriptsPath)
r.StaticFile("/style.css", stylesheetsPath) r.StaticFile("/style.css", stylesheetsPath)
r.StaticFile("/bootstrap.min.css", bootstrapPath)
r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath)
//router.StaticFile("/bootstrap.min.css", bootstrapPath) //router.StaticFile("/bootstrap.min.css", bootstrapPath)
default: default:
// 处理无效的Pages Mode // 处理无效的Pages Mode
logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode) logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode)
// 加载嵌入式资源 err := setInternalRoute(cfg, r)
pages, err := loadEmbeddedPages(cfg)
if err != nil { if err != nil {
logError("Failed when processing pages: %s", err) logError("Failed when processing internal pages: %s", err)
fmt.Println(err.Error())
return return
} }
// 设置嵌入式资源路由
r.GET("/", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
} }
} }
func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
// 加载嵌入式资源
pages, assets, err := loadEmbeddedPages(cfg)
if err != nil {
logError("Failed when processing pages: %s", err)
return err
}
// 设置嵌入式资源路由
r.GET("/", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(pages))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(assets))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) {
staticServer := http.FileServer(http.FS(assets))
req, err := adaptor.GetCompatRequest(&c.Request)
if err != nil {
logError("%s", err)
return
}
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
})
return nil
}
func init() { func init() {
readFlag() readFlag()
flag.Parse() flag.Parse()
// 如果设置了 -h则显示帮助信息并退出
if showHelp {
flag.Usage()
os.Exit(0)
}
// 如果设置了 -v则显示版本号并退出
if showVersion {
fmt.Printf("GHProxy Version: %s \n", version)
os.Exit(0)
}
loadConfig() loadConfig()
setupLogger(cfg) if cfg != nil { // 在setupLogger前添加空值检查
InitReq(cfg) setupLogger(cfg)
loadlist(cfg) InitReq(cfg)
setupRateLimit(cfg) loadlist(cfg)
setupRateLimit(cfg)
if cfg.Server.Debug { if cfg.Server.Debug {
runMode = "dev" runMode = "dev"
} else { } else {
runMode = "release" runMode = "release"
}
if cfg.Server.Debug {
version = "Dev" // 如果是Debug模式版本设置为"Dev"
}
} }
if cfg.Server.Debug {
version = "Dev"
}
} }
func main() { func main() {
// 如果 showVersion 为 true则在 init 阶段已退出,这里直接返回
if showVersion || showHelp {
return
}
logDebug("Run Mode: %s", runMode) logDebug("Run Mode: %s", runMode)
// 确保在程序配置加载且非版本显示模式下执行
if cfg == nil {
fmt.Println("Config not loaded, exiting.")
return // 如果配置未加载,则不继续执行
}
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
r := server.New( r := server.New(
@@ -299,45 +362,47 @@ func main() {
setupPages(cfg, r) setupPages(cfg, r)
// 1. GitHub Releases/Archive - Use distinct path segments for type /*
r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for releases // 1. GitHub Releases/Archive - Use distinct path segments for type
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for releases
}) proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for archive r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for archive
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
}) })
// 2. GitHub Blob/Raw - Use distinct path segments for type // 2. GitHub Blob/Raw - Use distinct path segments for type
r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for blob r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for blob
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
}) })
r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for raw r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for raw
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
}) })
r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for info r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for info
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
}) })
r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) { r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) {
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
}) })
// 4. Raw GitHubusercontent - Keep as is (assuming it's distinct enough) // 4. Raw GitHubusercontent - Keep as is (assuming it's distinct enough)
r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
}) })
// 5. Gist GitHubusercontent - Keep as is (assuming it's distinct enough) // 5. Gist GitHubusercontent - Keep as is (assuming it's distinct enough)
r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) {
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) // 6. GitHub API Repos - Keep as is (assuming it's distinct enough)
r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) { r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c) proxy.NoRouteHandler(cfg, limiter, iplimiter)(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)

View File

@@ -20,18 +20,13 @@ var (
// 日志中间件 // 日志中间件
func Middleware() app.HandlerFunc { func Middleware() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) { return func(ctx context.Context, c *app.RequestContext) {
startTime := time.Now() // 请求开始处理前记录当前时间作为开始时间 startTime := time.Now()
c.Next(ctx) // 调用 Next() 执行后续的 Handler c.Next(ctx)
endTime := time.Now() // 请求处理完成后记录当前时间作为结束时间 endTime := time.Now()
timingResults := endTime.Sub(startTime) // 计算时间差,得到请求处理耗时 (Duration 类型) timingResults := endTime.Sub(startTime)
// 记录日志 IP METHOD URL USERAGENT PROTOCOL STATUS TIMING
// %s %s %s %s %s %d %s 分别对应: ClientIP, Method, Protolcol, Path, UserAgent, StatusCode, timingResults (需要格式化)
// %v 可以通用地格式化 time.Duration 类型
logInfo("%s %s %s %s %s %d %v ", c.ClientIP(), c.Method(), c.Request.Header.GetProtocol(), string(c.Path()), c.Request.Header.UserAgent(), c.Response.StatusCode(), timingResults) logInfo("%s %s %s %s %s %d %v ", c.ClientIP(), c.Method(), c.Request.Header.GetProtocol(), string(c.Path()), c.Request.Header.UserAgent(), c.Response.StatusCode(), timingResults)
//logInfo("%s %s %s %s %d %v ", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.UserAgent(), c.Response.StatusCode(), timingResults)
} }
} }

View File

@@ -0,0 +1,17 @@
package nocache
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
)
func NoCacheMiddleware() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
// 设置禁止缓存的响应头
c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate")
c.Response.Header.Set("Pragma", "no-cache")
c.Response.Header.Set("Expires", "0")
c.Next(ctx) // 继续处理请求
}
}

View File

@@ -11,13 +11,13 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques
if cfg.Auth.PassThrough { if cfg.Auth.PassThrough {
token := c.Query("token") token := c.Query("token")
if token != "" { if token != "" {
logDebug("%s %s %s %s %s Auth-PassThrough: token %s", c.ClientIP(), c.Request.Method, string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol(), token) logDebug("%s %s %s %s %s Auth-PassThrough: token %s", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol(), token)
switch cfg.Auth.AuthMethod { switch cfg.Auth.Method {
case "parameters": case "parameters":
if !cfg.Auth.Enabled { if !cfg.Auth.Enabled {
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.Request.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 // 500 Internal Server Error
c.JSON(http.StatusInternalServerError, map[string]string{"error": "Conflict Auth Method"}) c.JSON(http.StatusInternalServerError, map[string]string{"error": "Conflict Auth Method"})
return return
@@ -27,7 +27,7 @@ func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Reques
req.Header.Set("Authorization", "token "+token) req.Header.Set("Authorization", "token "+token)
} }
default: default:
logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Request.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 // 500 Internal Server Error
c.JSON(http.StatusInternalServerError, map[string]string{"error": "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

@@ -9,9 +9,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/WJQSERVER-STUDIO/go-utils/hwriter"
"github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app"
hresp "github.com/cloudwego/hertz/pkg/protocol/http1/resp"
) )
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) {
@@ -25,7 +23,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
} }
setRequestHeaders(c, headReq) setRequestHeaders(c, headReq)
removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
reWriteEncodeHeader(headReq)
AuthPassThrough(c, cfg, headReq) AuthPassThrough(c, cfg, headReq)
headResp, err := client.Do(headReq) headResp, err := client.Do(headReq)
@@ -33,7 +30,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return return
} }
//defer headResp.Body.Close()
defer func(Body io.ReadCloser) { defer func(Body io.ReadCloser) {
if err := Body.Close(); err != nil { if err := Body.Close(); err != nil {
logError("Failed to close response body: %v", err) logError("Failed to close response body: %v", err)
@@ -47,7 +43,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
if err == nil && size > sizelimit { if err == nil && size > sizelimit {
finalURL := headResp.Request.URL.String() finalURL := headResp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) c.Redirect(http.StatusMovedPermanently, []byte(finalURL))
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Path(), c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), finalURL, size) 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 return
} }
} }
@@ -63,7 +59,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
} }
setRequestHeaders(c, req) setRequestHeaders(c, req)
removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
reWriteEncodeHeader(req)
AuthPassThrough(c, cfg, req) AuthPassThrough(c, cfg, req)
resp, err := client.Do(req) resp, err := client.Do(req)
@@ -71,7 +66,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return return
} }
defer resp.Body.Close()
// 错误处理(404) // 错误处理(404)
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
@@ -85,7 +79,7 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
if err == nil && size > sizelimit { if err == nil && size > sizelimit {
finalURL := resp.Request.URL.String() finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, []byte(finalURL)) c.Redirect(http.StatusMovedPermanently, []byte(finalURL))
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, size) 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)
return return
} }
} }
@@ -118,7 +112,6 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
} }
c.Status(resp.StatusCode) c.Status(resp.StatusCode)
c.Response.HijackWriter(hresp.NewChunkedBodyWriter(&c.Response, c.GetWriter()))
if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor { if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor {
// 判断body是不是gzip // 判断body是不是gzip
@@ -130,21 +123,15 @@ func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, c
logInfo("Is Shell: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol()) logInfo("Is Shell: %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", "")
err := ProcessLinksAndWriteChunked(resp.Body, compress, string(c.Request.Host()), cfg, c) 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)
return return
} else {
c.Flush() // 确保刷入
} }
} else { } else {
err = hwriter.Writer(resp.Body, c) c.SetBodyStream(resp.Body, -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)
return
} else {
c.Flush() // 确保刷入
}
} }
} }

View File

@@ -1,7 +1,6 @@
package proxy package proxy
import ( import (
"fmt"
"net/http" "net/http"
"github.com/WJQSERVER-STUDIO/go-utils/logger" "github.com/WJQSERVER-STUDIO/go-utils/logger"
@@ -19,6 +18,6 @@ var (
) )
func HandleError(c *app.RequestContext, message string) { func HandleError(c *app.RequestContext, message string) {
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message)) c.JSON(http.StatusInternalServerError, map[string]string{"error": message})
logError(message) logError(message)
} }

View File

@@ -5,11 +5,9 @@ import (
"context" "context"
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"io"
"net/http" "net/http"
"strconv" "strconv"
"github.com/WJQSERVER-STUDIO/go-utils/hwriter"
"github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app"
) )
@@ -30,7 +28,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
var ( var (
resp *http.Response resp *http.Response
err error //err error
) )
body := c.Request.Body() body := c.Request.Body()
@@ -45,6 +43,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
return return
} }
setRequestHeaders(c, req) setRequestHeaders(c, req)
removeWSHeader(req)
AuthPassThrough(c, cfg, req) AuthPassThrough(c, cfg, req)
resp, err = gitclient.Do(req) resp, err = gitclient.Do(req)
@@ -59,6 +58,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
return return
} }
setRequestHeaders(c, req) setRequestHeaders(c, req)
removeWSHeader(req)
AuthPassThrough(c, cfg, req) AuthPassThrough(c, cfg, req)
resp, err = client.Do(req) resp, err = client.Do(req)
@@ -67,12 +67,6 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
return return
} }
} }
//defer resp.Body.Close()
defer func(Body io.ReadCloser) {
if err := Body.Close(); err != nil {
logError("Failed to close response body: %v", err)
}
}(resp.Body)
contentLength := resp.Header.Get("Content-Length") contentLength := resp.Header.Get("Content-Length")
if contentLength != "" { if contentLength != "" {
@@ -81,7 +75,7 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
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)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Path(), c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), finalURL, size) 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 return
} }
} }
@@ -114,14 +108,11 @@ func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Co
} }
c.Status(resp.StatusCode) c.Status(resp.StatusCode)
err = hwriter.Writer(resp.Body, c) if cfg.GitClone.Mode == "cache" {
c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate")
if err != nil { c.Response.Header.Set("Pragma", "no-cache")
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) c.Response.Header.Set("Expires", "0")
return
} else {
c.Flush() // 确保刷入
} }
c.SetBodyStream(resp.Body, -1)
} }

View File

@@ -36,18 +36,18 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
if !allowed { if !allowed {
c.JSON(http.StatusTooManyRequests, map[string]string{"error": "Too Many Requests"}) c.JSON(http.StatusTooManyRequests, map[string]string{"error": "Too Many Requests"})
logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Request.Method, c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
return return
} }
} }
rawPath := strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/ rawPath := strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
matches := re.FindStringSubmatch(rawPath) // 匹配路径 matches := re.FindStringSubmatch(rawPath) // 匹配路径
logInfo("Matches: %v", matches) logInfo("URL: %v", matches)
// 匹配路径错误处理 // 匹配路径错误处理
if len(matches) < 3 { if len(matches) < 3 {
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol()) errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
logWarning(errMsg) logWarning(errMsg)
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath) c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
return return
@@ -71,9 +71,9 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
} }
username := user username := user
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), username, repo) logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), username, repo)
// dump log 记录详细信息 c.ClientIP(), c.Request.Method, rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header // dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header
logDump("%s %s %s %s %s %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header()) logDump("%s %s %s %s %s %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header())
repouser := fmt.Sprintf("%s/%s", username, repo) repouser := fmt.Sprintf("%s/%s", username, repo)
// 白名单检查 // 白名单检查
@@ -82,7 +82,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
if !whitelist { if !whitelist {
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser) errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser)
c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) c.JSON(http.StatusForbidden, map[string]string{"error": errMsg})
logWarning("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser) 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 return
} }
} }
@@ -93,7 +93,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
if blacklist { if blacklist {
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser) errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
c.JSON(http.StatusForbidden, map[string]string{"error": errMsg}) c.JSON(http.StatusForbidden, map[string]string{"error": errMsg})
logWarning("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser) 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 return
} }
} }
@@ -111,12 +111,12 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
if !authcheck { if !authcheck {
//c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"}) //c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"}) c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"})
logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err) logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err)
return return
} }
// IP METHOD URL USERAGENT PROTO MATCHES // IP METHOD URL USERAGENT PROTO MATCHES
logDebug("%s %s %s %s %s Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), 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":

View File

@@ -6,14 +6,9 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"io" "io"
"net/http"
"net/url" "net/url"
"regexp" "regexp"
"strings" "strings"
"github.com/cloudwego/hertz/pkg/app"
hresp "github.com/cloudwego/hertz/pkg/protocol/http1/resp"
"github.com/valyala/bytebufferpool"
) )
// 定义错误类型, error承载描述, 便于处理 // 定义错误类型, error承载描述, 便于处理
@@ -119,7 +114,7 @@ func Matcher(rawPath string, cfg *config.Config) (string, string, string, error)
user = parts[1] user = parts[1]
} }
if !cfg.Auth.ForceAllowApi { if !cfg.Auth.ForceAllowApi {
if cfg.Auth.AuthMethod != "header" || !cfg.Auth.Enabled { if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
return "", "", "", ErrAuthHeaderUnavailable return "", "", "", ErrAuthHeaderUnavailable
} }
} }
@@ -156,20 +151,25 @@ func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) {
if strings.HasPrefix(rawPath, "https://gist.github.com") { if strings.HasPrefix(rawPath, "https://gist.github.com") {
return true, matcher, nil return true, matcher, nil
} }
// 匹配 "https://api.github.com/"开头的链接 if cfg.Shell.RewriteAPI {
if strings.HasPrefix(rawPath, "https://api.github.com") { // 匹配 "https://api.github.com/"开头的链接
matcher = "api" if strings.HasPrefix(rawPath, "https://api.github.com") {
return true, matcher, nil matcher = "api"
return true, matcher, nil
}
} }
return false, "", ErrInvalidURL return false, "", ErrInvalidURL
} }
// 匹配文件扩展名是sh的rawPath // 匹配文件扩展名是sh的rawPath
func MatcherShell(rawPath string) bool { func MatcherShell(rawPath string) bool {
if strings.HasSuffix(rawPath, ".sh") { /*
return true if strings.HasSuffix(rawPath, ".sh") {
} return true
return false }
return false
*/
return strings.HasSuffix(rawPath, ".sh")
} }
// LinkProcessor 是一个函数类型,用于处理提取到的链接。 // LinkProcessor 是一个函数类型,用于处理提取到的链接。
@@ -184,9 +184,9 @@ func modifyURL(url string, host string, cfg *config.Config) string {
return url return url
} }
if matched { if matched {
var u = url
u := strings.TrimPrefix(url, "https://") u = strings.TrimPrefix(u, "https://")
u = strings.TrimPrefix(url, "http://") u = strings.TrimPrefix(u, "http://")
logDump("Modified URL: %s", "https://"+host+"/"+u) logDump("Modified URL: %s", "https://"+host+"/"+u)
return "https://" + host + "/" + u return "https://" + host + "/" + u
} }
@@ -211,89 +211,6 @@ func matchString(target string, stringsToMatch []string) bool {
return exists return exists
} }
// processLinksAndWriteChunked 处理链接并将结果以 chunked 方式写入响应
func ProcessLinksAndWriteChunked(input io.Reader, compress string, host string, cfg *config.Config, c *app.RequestContext) error {
var reader *bufio.Reader
if compress == "gzip" {
// 解压 gzip
gzipReader, err := gzip.NewReader(input)
if err != nil {
c.String(http.StatusInternalServerError, fmt.Sprintf("gzip 解压错误: %v", err))
return fmt.Errorf("gzip 解压错误: %w", err)
}
defer gzipReader.Close()
reader = bufio.NewReader(gzipReader)
} else {
reader = bufio.NewReader(input)
}
// 获取 chunked body writer
chunkedWriter := hresp.NewChunkedBodyWriter(&c.Response, c.GetWriter())
var writer io.Writer = chunkedWriter
var gzipWriter *gzip.Writer
if compress == "gzip" {
gzipWriter = gzip.NewWriter(writer)
writer = gzipWriter
defer func() {
if err := gzipWriter.Close(); err != nil {
logError("gzipWriter close failed: %v", err)
}
}()
}
bufWrapper := bytebufferpool.Get()
buf := bufWrapper.B
size := 32768 // 32KB
buf = buf[:cap(buf)]
if len(buf) < size {
buf = append(buf, make([]byte, size-len(buf))...)
}
buf = buf[:size] // 将缓冲区限制为 'size'
defer bytebufferpool.Put(bufWrapper)
urlPattern := regexp.MustCompile(`https?://[^\s'"]+`)
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
return modifyURL(originalURL, host, cfg)
})
modifiedLineWithNewline := modifiedLine + "\n"
_, err := writer.Write([]byte(modifiedLineWithNewline))
if err != nil {
logError("写入 chunk 错误: %v", err)
return fmt.Errorf("写入 chunk 错误: %w", err)
}
if compress != "gzip" {
if fErr := chunkedWriter.Flush(); fErr != nil {
logError("chunkedWriter flush failed: %v", fErr)
return fmt.Errorf("chunkedWriter flush failed: %w", fErr)
}
}
}
if err := scanner.Err(); err != nil {
logError("读取输入错误: %v", err)
c.String(http.StatusInternalServerError, fmt.Sprintf("读取输入错误: %v", err))
return fmt.Errorf("读取输入错误: %w", err)
}
// 对于 gzipchunkedWriter 的关闭会触发最后的 chunk
if compress != "gzip" {
if fErr := chunkedWriter.Flush(); fErr != nil {
logError("final chunkedWriter flush failed: %v", fErr)
return fmt.Errorf("final chunkedWriter flush failed: %w", fErr)
}
}
return nil // 成功完成处理
}
// extractParts 从给定的 URL 中提取所需的部分 // extractParts 从给定的 URL 中提取所需的部分
func extractParts(rawURL string) (string, string, string, url.Values, error) { func extractParts(rawURL string) (string, string, string, url.Values, error) {
// 解析 URL // 解析 URL
@@ -325,3 +242,112 @@ func extractParts(rawURL string) (string, string, string, url.Values, error) {
return repoOwner, repoName, remainingPath, queryParams, nil return repoOwner, repoName, remainingPath, queryParams, nil
} }
// processLinks 处理链接,返回包含处理后数据的 io.Reader
func processLinks(input io.Reader, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
readerOut = pipeReader
go func() { // 在 Goroutine 中执行写入操作
defer func() {
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
if err != nil {
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
}
} else {
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
logError("pipeWriter close failed: %v", closeErr)
if err == nil { // 如果之前没有错误,记录关闭错误
err = closeErr
}
}
}
}
}()
var bufReader *bufio.Reader
if compress == "gzip" {
// 解压gzip
gzipReader, gzipErr := gzip.NewReader(input)
if gzipErr != nil {
err = fmt.Errorf("gzip解压错误: %v", gzipErr)
return // Goroutine 中使用 return 返回错误
}
defer gzipReader.Close()
bufReader = bufio.NewReader(gzipReader)
} else {
bufReader = bufio.NewReader(input)
}
var bufWriter *bufio.Writer
var gzipWriter *gzip.Writer
// 根据是否gzip确定 writer 的创建
if compress == "gzip" {
gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter
bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小
} else {
bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter
}
//确保writer关闭
defer func() {
var closeErr error // 局部变量用于保存defer中可能发生的错误
if gzipWriter != nil {
if closeErr = gzipWriter.Close(); closeErr != nil {
logError("gzipWriter close failed %v", closeErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = closeErr
}
}
}
if flushErr := bufWriter.Flush(); flushErr != nil {
logError("writer flush failed %v", flushErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = flushErr
}
}
}()
// 使用正则表达式匹配 http 和 https 链接
urlPattern := regexp.MustCompile(`https?://[^\s'"]+`)
for {
line, readErr := bufReader.ReadString('\n')
if readErr != nil {
if readErr == io.EOF {
break // 文件结束
}
err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误
return // Goroutine 中使用 return 返回错误
}
// 替换所有匹配的 URL
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
logDump("originalURL: %s", originalURL)
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
})
n, writeErr := bufWriter.WriteString(modifiedLine)
written += int64(n) // 更新写入的字节数
if writeErr != nil {
err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误
return // Goroutine 中使用 return 返回错误
}
}
// 在返回之前,再刷新一次 (虽然 defer 中已经有 flush但这里再加一次确保及时刷新)
if flushErr := bufWriter.Flush(); flushErr != nil {
if err == nil { // 避免覆盖之前的错误
err = flushErr
}
return // Goroutine 中使用 return 返回错误
}
}()
return readerOut, written, nil // 返回 reader 和 writtenerror 由 Goroutine 通过 pipeWriter.CloseWithError 传递
}

View File

@@ -2,7 +2,6 @@ package proxy
import ( import (
"net/http" "net/http"
"strings"
"github.com/cloudwego/hertz/pkg/app" "github.com/cloudwego/hertz/pkg/app"
) )
@@ -18,31 +17,3 @@ func removeWSHeader(req *http.Request) {
req.Header.Del("Upgrade") req.Header.Del("Upgrade")
req.Header.Del("Connection") req.Header.Del("Connection")
} }
func reWriteEncodeHeader(req *http.Request) {
if isGzipAccepted(req.Header) {
req.Header.Set("Content-Encoding", "gzip")
req.Header.Set("Accept-Encoding", "gzip")
} else {
req.Header.Del("Content-Encoding")
req.Header.Del("Accept-Encoding")
}
}
// isGzipAccepted 检查 Accept-Encoding 头部中是否包含 gzip
func isGzipAccepted(header http.Header) bool {
// 获取 Accept-Encoding 的值
encodings := header["Accept-Encoding"]
for _, encoding := range encodings {
// 将 encoding 字符串拆分为多个编码
for _, enc := range strings.Split(encoding, ",") {
// 去除空格并检查是否为 gzip
if strings.TrimSpace(enc) == "gzip" {
return true
}
}
}
return false
}

View File

@@ -10,7 +10,7 @@ import (
// 日志输出 // 日志输出
var ( var (
logw = logger.Logw logw = logger.Logw
LogDump = logger.LogDump logDump = logger.LogDump
logDebug = logger.LogDebug logDebug = logger.LogDebug
logInfo = logger.LogInfo logInfo = logger.LogInfo
logWarning = logger.LogWarning logWarning = logger.LogWarning