Compare commits

...

134 Commits
2.3.0 ... 3.2.2

Author SHA1 Message Date
WJQSERVER
ef3b1bf1f0 Merge pull request #99 from WJQSERVER-STUDIO/dev
3.2.2
2025-04-29 22:29:13 +08:00
wjqserver
ad4d8eb670 remove unuse code 2025-04-29 22:25:20 +08:00
wjqserver
030f0d12a9 update docs 2025-04-29 22:24:11 +08:00
wjqserver
e57432a01c 3.2.2 2025-04-29 22:21:08 +08:00
wjqserver
ace795fe9d revert gitreq body stream 2025-04-29 22:13:11 +08:00
wjqserver
3f51e5319a 25w33a 2025-04-29 20:39:41 +08:00
wjqserver
55769d9a40 use custom headers for raw 2025-04-29 19:51:23 +08:00
WJQSERVER
7eb312243c Merge pull request #97 from WJQSERVER-STUDIO/dev
3.2.1
2025-04-29 07:44:45 +08:00
wjqserver
6ca31bc252 3.2.1 2025-04-29 07:43:19 +08:00
wjqserver
dfc49ae28b 25w32a 2025-04-29 07:24:28 +08:00
wjqserver
a0cca13deb fix matcher key issue 2025-04-29 07:22:03 +08:00
里見 灯花
1498aaed14 Merge pull request #95 from WJQSERVER-STUDIO/dev
3.2.0
2025-04-27 17:41:32 +08:00
wjqserver
086aa999e1 3.2.0 2025-04-27 17:38:30 +08:00
wjqserver
bf92cc8429 add req body 2025-04-27 17:33:17 +08:00
wjqserver
d94f6c0f5d 25w31a 2025-04-27 16:39:47 +08:00
wjqserver
f540b2edcd fix user name match issue 2025-04-27 15:57:06 +08:00
wjqserver
8aef197fde 25w31t-2 2025-04-25 22:14:23 +08:00
wjqserver
52d6f8e759 update readme 2025-04-25 17:56:22 +08:00
wjqserver
a7be65a111 25w31t-1 2025-04-25 17:14:33 +08:00
WJQSERVER
9977eb1437 3.1.0
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率
- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`
- CHANGE: 使用`HertZ`的`requestContext`传递matcher参数, 而不是`25w30a`中的ctx
- CHANGE: 改进`rate`模块, 避免并发竞争问题
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
- CHANGE: 修改部分log等级
- FIX:    修正默认配置的填充错误
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
- CHANGE: 改进handle, 复用共同部分
- CHANGE: 细化url匹配的返回码处理
2025-04-24 18:46:28 +08:00
wjqserver
47de48bcce 3.1.0 2025-04-24 18:27:15 +08:00
wjqserver
8ccf48a6fe fix && update 2025-04-24 18:11:13 +08:00
wjqserver
7a6544c6c9 25w30e 2025-04-24 17:50:18 +08:00
wjqserver
b955c915ff fix callback issue 2025-04-24 01:09:53 +08:00
wjqserver
e42ea358bb remove debug output 2025-04-22 20:58:44 +08:00
wjqserver
4936a93788 25w30d 2025-04-22 20:56:34 +08:00
wjqserver
493ac28b59 add html/tmpl for status err page 2025-04-22 20:56:27 +08:00
wjqserver
d79aeaaacd 25w30c 2025-04-21 18:52:45 +08:00
wjqserver
558d3fbb0b 25w30b 2025-04-21 17:27:38 +08:00
wjqserver
3d7559bd66 change context.Context to hertz *app.RequestContext 2025-04-21 13:57:52 +08:00
wjqserver
809032a970 change to c.Request.BodyStream() 2025-04-21 13:47:45 +08:00
wjqserver
2eb6a9810b 25w30a 2025-04-19 23:02:13 +08:00
wjqserver
26a5148c6f use gertz route for std url 2025-04-19 22:59:59 +08:00
WJQSERVER
c656aa41ca Merge pull request #93 from WJQSERVER-STUDIO/dev
3.0.3
2025-04-19 21:26:39 +08:00
wjqserver
0b052f9c7f add debug output 2025-04-19 21:23:31 +08:00
wjqserver
6fb7e1150e 25w29b 2025-04-19 21:14:09 +08:00
wjqserver
5e0f95dae3 3.0.3 2025-04-19 20:44:43 +08:00
wjqserver
c1c39a5a1f remove unused bufferpool 2025-04-17 22:30:00 +08:00
wjqserver
dd2f5b5a12 25w29a 2025-04-17 22:20:06 +08:00
wjqserver
7e5b12dff8 Fix: Optimize header forwarding by excluding headers in a single pass 2025-04-16 15:50:04 +08:00
wjqserver
26a42b6510 add pprof for debug 2025-04-16 15:47:46 +08:00
wjqserver
254c9a8bad 25w29t-1 2025-04-15 15:05:36 +08:00
WJQSERVER
060453f070 Merge pull request #88 from WJQSERVER-STUDIO/dev
3.0.2
2025-04-15 13:26:46 +08:00
wjqserver
f110c96c1f update readme 2025-04-15 13:22:51 +08:00
wjqserver
73aac79c1b 3.0.2 2025-04-15 13:14:53 +08:00
wjqserver
bed6c486dc 25w28b 2025-04-15 10:23:29 +08:00
wjqserver
ab77c5c7da 25w28a 2025-04-14 12:27:12 +08:00
wjqserver
bf21bd197a 25w28t-2 2025-04-11 07:29:03 +08:00
wjqserver
8af107c584 update for touka-httpc 0.4.0 2025-04-11 07:24:50 +08:00
wjqserver
d6d54b222f dix auth checker 2025-04-10 23:07:48 +08:00
wjqserver
005a4543d4 update deps 2025-04-10 23:07:13 +08:00
wjqserver
a85eb38de5 update deps 2025-04-08 20:50:55 +08:00
里見 灯花
152fb8aa71 Merge pull request #84 from WJQSERVER-STUDIO/dev
3.0.1
2025-04-08 20:49:27 +08:00
wjqserver
3e9e43cd44 3.0.1 2025-04-08 20:48:56 +08:00
wjqserver
8a50b311fc 25w27a 2025-04-07 18:51:22 +08:00
wjqserver
dcc50401c4 update deps 2025-04-07 18:34:00 +08:00
wjqserver
d62a1f9769 [docs] update config docs 2025-04-07 18:33:47 +08:00
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
里見 灯花
448e06d350 v3 ! ! ! Go to Next Gen ! ! !
v3 Next Gen
v3 下一个起点
2025-03-19 18:10:04 +08:00
wjqserver
27cc30ab8b Next Gen 2025-03-19 18:03:17 +08:00
wjqserver
a65e44ac02 update changelog 2025-03-19 17:33:43 +08:00
wjqserver
a0cfe826ea 25w20b 2025-03-19 17:28:01 +08:00
wjqserver
2e974ad7ae remove unuse things 2025-03-18 22:37:39 +08:00
wjqserver
b7b9cd5db5 fix log print issues 2025-03-18 22:26:25 +08:00
wjqserver
bcb73c18de add mino theme 2025-03-18 22:25:54 +08:00
wjqserver
ed839b828d update .gitignore 2025-03-18 21:59:38 +08:00
wjqserver
801b8c6cda remove pages 2025-03-18 21:56:13 +08:00
wjqserver
a92bbb7fb6 25w20a 2025-03-18 21:53:59 +08:00
WJQSERVER
3e40146281 Merge pull request #67 from WJQSERVER-STUDIO/dev
2.5.0
2025-03-17 14:01:33 +08:00
wjqserver
ac7e1e43b5 update changelog 2025-03-17 13:53:37 +08:00
wjqserver
f134d22540 2.5.0 2025-03-17 13:48:53 +08:00
wjqserver
79153c0f7d update readme.md 2025-03-17 13:45:36 +08:00
wjqserver
4fd47812f7 25w19a 2025-03-16 21:03:28 +08:00
wjqserver
17c49d534b update readme.md 2025-03-16 12:28:00 +08:00
WJQSERVER
284b38bab4 Merge pull request #66 from WJQSERVER-STUDIO/dev
v2.4.2
2025-03-14 21:56:18 +08:00
wjqserver
d73dfe7db5 2.4.2 2025-03-14 21:48:25 +08:00
wjqserver
dc286e002c 25w18a 2025-03-14 21:40:21 +08:00
WJQSERVER
5c54ae788c Merge pull request #65 from WJQSERVER-STUDIO/dev
Rewrite path matcher (v2.4.1)
2025-03-13 22:48:27 +08:00
wjqserver
bfcb1c9901 2.4.1 2025-03-13 22:41:13 +08:00
wjqserver
9bfe8517cb rewrite path matcher 2025-03-13 18:16:17 +08:00
WJQSERVER
50ba185aab Merge pull request #63 from WJQSERVER-STUDIO/dev
v2.4.0
2025-03-13 00:34:24 +08:00
wjqserver
6ee928b0c7 update readme.md 2025-03-12 23:36:50 +08:00
wjqserver
979f59545b 2.4.0 2025-03-12 23:33:17 +08:00
wjqserver
da89b3f45e 25w16d 2025-03-12 23:01:52 +08:00
wjqserver
498266e08e 25w16c 2025-03-11 18:07:17 +08:00
wjqserver
e2faa497ab update frontend 2025-03-11 10:20:43 +08:00
wjqserver
8def955151 25w16b 2025-03-11 08:40:19 +08:00
wjqserver
a18660121a 25w16a 2025-03-10 18:53:12 +08:00
wjqserver
d26f6d1e1b update deps 2025-03-09 12:23:37 +08:00
WJQSERVER
60a1f6073d Merge pull request #54 from WJQSERVER-STUDIO/dev 2025-02-28 20:06:27 +08:00
wjqserver
2cc5409dd0 2.3.1 2025-02-28 19:57:25 +08:00
wjqserver
ad9cffe9e2 25w15a 2025-02-26 16:04:08 +08:00
59 changed files with 3024 additions and 2084 deletions

View File

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

View File

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

@@ -59,6 +59,11 @@ jobs:
else else
echo "DEV-VERSION file not found!" && exit 1 echo "DEV-VERSION file not found!" && exit 1
fi fi
- name: 拉取前端
run: |
sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages
sudo rm -rf pages/.git/
- name: 安装 Go - name: 安装 Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:

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
@@ -56,6 +60,11 @@ jobs:
else else
echo "VERSION file not found!" && exit 1 echo "VERSION file not found!" && exit 1
fi fi
- name: 拉取前端
run: |
sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages
sudo rm -rf pages/.git/
- name: 安装 Go - name: 安装 Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
@@ -102,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
@@ -130,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

4
.gitignore vendored
View File

@@ -2,4 +2,6 @@ demo
demo.toml demo.toml
*.log *.log
*.bak *.bak
list.json list.json
repos
pages

View File

@@ -1,5 +1,394 @@
# 更新日志 # 更新日志
3.2.2 - 2025-04-29
---
- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制
- FIX: 修正Header部分的一些处理问题
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
25w33b - 2025-04-29
---
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
25w33a - 2025-04-29
---
- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制
- FIX: 修正Header部分的一些处理问题
3.2.1 - 2025-04-29
---
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
25w32a - 2025-04-29
---
- PRE-RELEASE: 此版本是v3.2.1预发布版本,请勿在生产环境中使用;
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
3.2.0 - 2025-04-27
---
- CHANGE: 加入`ghcr``dockerhub`反代功能
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
25w31a - 2025-04-27
---
- PRE-RELEASE: 此版本是v3.2.0预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`ghcr``dockerhub`反代功能
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
3.1.0 - 2025-04-24
---
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率
- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`
- CHANGE: 使用`HertZ``requestContext`传递matcher参数, 而不是`25w30a`中的ctx
- CHANGE: 改进`rate`模块, 避免并发竞争问题
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
- CHANGE: 修改部分log等级
- FIX: 修正默认配置的填充错误
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
- CHANGE: 改进handle, 复用共同部分
- CHANGE: 细化url匹配的返回码处理
- CHANGE: 增加404界面
25w30e - 2025-04-24
---
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
- CHANGE: 改进`rate`模块, 避免并发竞争问题
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
- CHANGE: 修改部分log等级
- FIX: 修正默认配置的填充错误
25w30d - 2025-04-22
---
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
25w30c - 2025-04-21
---
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
- CHANGE: 改进handle, 复用共同部分
- CHANGE: 细化url匹配的返回码处理
- CHANGE: 增加404界面
25w30b - 2025-04-21
---
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`
- CHANGE: 使用`HertZ``requestContext`传递matcher参数, 而不是`25w30a`中的标准ctx
25w30a - 2025-04-19
---
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器
3.0.3 - 2025-04-19
---
- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息
- FIX: 修正非预期的header操作行为
- CHANGE: 合并header相关逻辑, 避免多次操作
- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理
- CHANGE: 增加`netlib`配置项
25w29b - 2025-04-19
---
- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用;
- CHANGE: 增加`netlib`配置项
25w29a - 2025-04-17
---
- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用;
- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息
- FIX: 修正非预期的header操作行为
- CHANGE: 合并header相关逻辑, 避免多次操作
- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理
3.0.2 - 2025-04-15
---
- CHANGE: 避免重复的re编译操作
- CHANGE: 去除不必要的请求
- CHANGE: 改进`httpc`相关配置
- CHANGE: 更新`httpc` 0.4.0
- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进
25w28b - 2025-04-15
---
- PRE-RELEASE: 此版本是v3.0.2预发布版本,请勿在生产环境中使用;
- CHANGE: 改进resp关闭
- CHANGE: 避免重复的re编译操作
25w28a - 2025-04-14
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用;
- CHANGE: 去除不必要的请求
- CHANGE: 改进`httpc`相关配置
- CHANGE: 合入test版本修改
25w28t-2 - 2025-04-11
---
- TEST: 测试验证版本
- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进
25w28t-1 - 2025-04-11
---
- TEST: 测试验证版本
- CHANGE: 更新httpc 0.4.0
3.0.1 - 2025-04-08
---
- CHANGE: 加入`memLimit`指示gc
- CHANGE: 加入`hlog`输出路径配置
- CHANGE: 修正H2C配置问题
25w27a - 2025-04-07
---
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`memLimit`指示gc
- CHANGE: 加入`hlog`输出路径配置
- CHANGE: 修正H2C配置问题
3.0.0 - 2025-04-04
---
- 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: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
- CHANGE: 加入`Mino`主题对接选项
- FIX: 修正部分日志输出问题
- CHANGE: 移除gin残留
- CHANGE: 移除无用传入参数, 调整代码结构
25w20b - 2025-03-19
---
- PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`Mino`主题对接选项
- FIX: 修正部分日志输出问题
- CHANGE: 移除gin残留
- CHANGE: 移除无用传入参数, 调整代码结构
25w20a - 2025-03-18
---
- PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用HertZ重构
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
2.5.0 - 2025-03-17
---
- ADD: 加入脚本嵌套加速功能
- CHANGE: 改进Auth模块
25w19a - 2025-03-16
---
- PRE-RELEASE: 此版本是v2.5.0的预发布版本,请勿在生产环境中使用;
- ADD: 加入脚本嵌套加速功能
- CHANGE: 改进Auth模块
- CHANGE: 将handler模块化改进
2.4.2 - 2025-03-14
---
- CHANGE: 在GitClone Cache模式下, 相关请求会使用独立httpc client
- CHANGE: 为GitClone Cache的独立httpc client增加ForceH2C选项
- FIX: 修正GitClone Cache模式下的Url生成问题
25w18a - 2025-03-14
---
- PRE-RELEASE: 此版本是v2.4.2的预发布版本,请勿在生产环境中使用;
- CHANGE: 在GitClone Cache模式下, 相关请求会使用独立httpc client
- CHANGE: 为GitClone Cache的独立httpc client增加ForceH2C选项
- FIX: 修正GitClone Cache模式下的Url生成问题
2.4.1 - 2025-03-13
---
- CHANGE: 重构路由匹配
- CHANGE: 更新相关依赖以修复错误
25w17a - 2025-03-13
---
- PRE-RELEASE: 此版本是v2.4.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 重构路由匹配
- CHANGE: 更新相关依赖以修复错误
2.4.0 - 2025-03-12
---
- ADD: 支持通过[Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git)实现Git Clone缓存
- CHANGE: 使用更高性能的Buffer Pool 实现, 调用 github.com/WJQSERVER-STUDIO/go-utils/copyb
- CHANGE: 改进路由匹配
- CHANGE: 更新依赖
- CHANGE: 改进前端
25w16d - 2025-03-12
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用更高性能的Buffer Pool 实现
25w16c
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用更高性能的Buffer Pool 实现
- CHANGE: 改进路由匹配
25w16b
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 修改路由
- CHANGE: 改进前端
25w16a
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 变更CORS配置
- ADD: 使用GO-GIT实现git smart http服务端和客户端
- CHANGE: 更新依赖
2.3.1
---
- CHANGE: 改进`Pages``External`模式下的路由
- CHANGE: 使用`H2C` bool 代替 `enableH2C` string (2.4.0 弃用 `enableH2C`)
- CHANGE: 使用`Mode` string 代替`Pages`内的 `enable` bool (2.4.0 弃用 `enable`)
25w15a
---
- PRE-RELEASE: 此版本是v2.3.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进`Pages``External`模式下的路由
- CHANGE: 使用`H2C` bool 代替 `enableH2C` string (2.4.0 弃用 `enableH2C`)
- CHANGE: 使用`Mode` string 代替`Pages`内的 `enable` bool (2.4.0 弃用 `enable`)
2.3.0 2.3.0
--- ---
- CHANGE: 使用`touka-httpc`封装`HTTP Client`, 更新到`v0.2.0`版本, 参看`touka-httpc` - CHANGE: 使用`touka-httpc`封装`HTTP Client`, 更新到`v0.2.0`版本, 参看`touka-httpc`
@@ -1010,4 +1399,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 @@
25w14b 25w33b

174
README.md
View File

@@ -1,60 +1,57 @@
# GHProxy # GHProxy
![GitHub Release](https://img.shields.io/github/v/release/WJQSERVER-STUDIO/ghproxy?display_name=tag&style=flat)
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg) ![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/WJQSERVER-STUDIO/ghproxy)
[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) [![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
[DEMO](https://ghproxy.1888866.xyz) 支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能
[TG讨论群组](https://t.me/ghproxy_go)
[版本更新介绍](https://blog.wjqserver.com/categories/my-program/)
## 项目说明 ## 项目说明
### 项目特点 ### 项目特点
- 基于Go语言实现,支持多平台 - **基于 Go 语言实现,跨平台的同时提供高并发性能**
- 使用[Gin](https://github.com/gin-gonic/gin)作为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 等文件拉取**
- 支持Docker部署 - 🎨 **支持多个前端主题**
- 支持速率限制 - 🚫 **支持自定义黑名单/白名单**
- 支持用户鉴权 - 🗄️ **支持 Git Clone 缓存(配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git)**
- 支持自定义黑名单/白名单 - 🐳 **支持 Docker 部署**
- 基于[WJQSERVER-STUDIO/golang-temp](https://github.com/WJQSERVER-STUDIO/golang-temp)模板构建,具有标准化的日志记录与构建流程 - 🐳 **支持自托管**
-**支持速率限制**
- 🔒 **支持用户鉴权**
- 🐚 **支持 shell 脚本多层嵌套加速**
### 项目开发过程 ### 项目相关
**本项目是[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)
- V2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用 [TG讨论群组](https://t.me/ghproxy_go)
- 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
## 使用示例
``` ```
# 下载文件 # 下载文件
https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
https://ghproxy.1888866.xyz/https://raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
# 克隆仓库 # 克隆仓库
git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
git clone https://ghproxy.1888866.xyz/https://github.com/WJQSERVER-STUDIO/ghproxy.git
``` ```
## 部署说明 ## 部署说明
可参考文章: https://blog.wjqserver.com/post/ghproxy-deploy-with-smart-git/
### Docker部署 ### Docker部署
- Docker-cli - Docker-cli
@@ -83,103 +80,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
enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off
[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 仅用于高级模式
[pages]
enabled = false # 是否开启外置静态页面(Docker版本请关闭此项)
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
[cors]
enabled = true # 是否开启跨域
[auth]
authMethod = "parameters" # 鉴权方式,支持parameters,header
authToken = "token" # 用户鉴权Token
enabled = 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" 支持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"
]
}
```
### Caddy反代配置
```Caddyfile
example.com {
reverse_proxy * 127.0.0.1:7210
}
```
### 前端页面 ### 前端页面
![ghproxy-demo.png](https://webp.wjqserver.com/ghproxy/1.8.1-light.png) 参看[GHProxy-Frontend](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
![ghproxy-demo-dark.png](https://webp.wjqserver.com/ghproxy/1.8.1-dark.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
## 赞助 ## 赞助
@@ -189,6 +113,10 @@ example.com {
爱发电: https://afdian.com/a/wjqserver 爱发电: https://afdian.com/a/wjqserver
USDT(TRC20): `TNfSYG6F2vkiibd6J6mhhHNWDgWgNdF5hN`
### 捐赠列表 ### 捐赠列表
虚位以待... | 赞助人 |金额|
|--------|------|
| starry | 8 USDT (TRC20) |

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

@@ -1 +1 @@
2.3.0 3.2.2

View File

@@ -1,129 +1,128 @@
package api package api
import ( import (
"encoding/json" "context"
"ghproxy/config" "ghproxy/config"
"ghproxy/middleware/nocache"
"github.com/WJQSERVER-STUDIO/go-utils/logger" "github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) "github.com/cloudwego/hertz/pkg/app/server"
var (
router *gin.Engine
cfg *config.Config
) )
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
logError = logger.LogError logError = logger.LogError
) )
func NoCacheMiddleware() gin.HandlerFunc { func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
return func(c *gin.Context) { apiRouter := r.Group("/api", nocache.NoCacheMiddleware())
// 设置禁止缓存的响应头
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
c.Next() // 继续处理请求
}
}
func InitHandleRouter(cfg *config.Config, router *gin.Engine, version string) {
apiRouter := router.Group("api", NoCacheMiddleware())
{ {
apiRouter.GET("/size_limit", func(c *gin.Context) { apiRouter.GET("/size_limit", func(ctx context.Context, c *app.RequestContext) {
SizeLimitHandler(cfg, c) SizeLimitHandler(cfg, c, ctx)
}) })
apiRouter.GET("/whitelist/status", func(c *gin.Context) { apiRouter.GET("/whitelist/status", func(ctx context.Context, c *app.RequestContext) {
WhiteListStatusHandler(c, cfg) WhiteListStatusHandler(cfg, c, ctx)
}) })
apiRouter.GET("/blacklist/status", func(c *gin.Context) { apiRouter.GET("/blacklist/status", func(ctx context.Context, c *app.RequestContext) {
BlackListStatusHandler(c, cfg) BlackListStatusHandler(cfg, c, ctx)
}) })
apiRouter.GET("/cors/status", func(c *gin.Context) { apiRouter.GET("/cors/status", func(ctx context.Context, c *app.RequestContext) {
CorsStatusHandler(c, cfg) CorsStatusHandler(cfg, c, ctx)
}) })
apiRouter.GET("/healthcheck", func(c *gin.Context) { apiRouter.GET("/healthcheck", func(ctx context.Context, c *app.RequestContext) {
HealthcheckHandler(c) HealthcheckHandler(c, ctx)
}) })
apiRouter.GET("/version", func(c *gin.Context) { apiRouter.GET("/version", func(ctx context.Context, c *app.RequestContext) {
VersionHandler(c, version) VersionHandler(c, ctx, version)
}) })
apiRouter.GET("/rate_limit/status", func(c *gin.Context) { apiRouter.GET("/rate_limit/status", func(ctx context.Context, c *app.RequestContext) {
RateLimitStatusHandler(c, cfg) RateLimitStatusHandler(cfg, c, ctx)
}) })
apiRouter.GET("/rate_limit/limit", func(c *gin.Context) { apiRouter.GET("/rate_limit/limit", func(ctx context.Context, c *app.RequestContext) {
RateLimitLimitHandler(c, cfg) RateLimitLimitHandler(cfg, c, ctx)
}) })
apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) {
SmartGitStatusHandler(cfg, c, ctx)
})
} }
logInfo("API router Init success") logInfo("API router Init success")
} }
func SizeLimitHandler(cfg *config.Config, c *gin.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, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Writer.Header().Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"MaxResponseBodySize": sizeLimit, "MaxResponseBodySize": sizeLimit,
}) }))
} }
func WhiteListStatusHandler(c *gin.Context, cfg *config.Config) { func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Writer.Header().Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Whitelist": cfg.Whitelist.Enabled, "Whitelist": cfg.Whitelist.Enabled,
}) }))
} }
func BlackListStatusHandler(c *gin.Context, cfg *config.Config) { func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Writer.Header().Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Blacklist": cfg.Blacklist.Enabled, "Blacklist": cfg.Blacklist.Enabled,
}) }))
} }
func CorsStatusHandler(c *gin.Context, cfg *config.Config) { func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Writer.Header().Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Cors": cfg.CORS.Enabled, "Cors": cfg.Server.Cors,
}) }))
} }
func HealthcheckHandler(c *gin.Context) { func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Writer.Header().Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Status": "OK", "Status": "OK",
}) }))
} }
func VersionHandler(c *gin.Context, version string) { func VersionHandler(c *app.RequestContext, ctx context.Context, version string) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Writer.Header().Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"Version": version, "Version": version,
}) }))
} }
func RateLimitStatusHandler(c *gin.Context, cfg *config.Config) { func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Writer.Header().Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"RateLimit": cfg.RateLimit.Enabled, "RateLimit": cfg.RateLimit.Enabled,
}) }))
} }
func RateLimitLimitHandler(c *gin.Context, cfg *config.Config) { func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Writer.Header().Set("Content-Type", "application/json") c.Response.Header.Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{ c.JSON(200, (map[string]interface{}{
"RatePerMinute": cfg.RateLimit.RatePerMinute, "RatePerMinute": cfg.RateLimit.RatePerMinute,
}) }))
}
func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
c.Response.Header.Set("Content-Type", "application/json")
c.JSON(200, (map[string]interface{}{
"enabled": cfg.GitClone.Mode == "cache",
}))
} }

View File

@@ -4,26 +4,30 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) )
func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) { func AuthHeaderHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) {
if !cfg.Auth.Enabled { if !cfg.Auth.Enabled {
return true, "" return true, nil
} }
// 获取"GH-Auth"的值 // 获取"GH-Auth"的值
authToken := c.GetHeader("GH-Auth") var authToken string
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Request.Method, c.Request.Host, c.Request.URL.Path, c.Request.Proto, c.Request.RemoteAddr, 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 == "" {
err := "Auth Header == nil" return false, fmt.Errorf("Auth token not found")
return false, err
} }
isValid = authToken == cfg.Auth.AuthToken isValid = authToken == cfg.Auth.Token
if !isValid { if !isValid {
err := fmt.Sprintf("Auth token incorrect: %s", authToken) return false, fmt.Errorf("Auth token incorrect")
return false, err
} }
return isValid, "" return isValid, nil
} }

View File

@@ -4,27 +4,31 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) )
func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) { func AuthParametersHandler(c *app.RequestContext, cfg *config.Config) (isValid bool, err error) {
if !cfg.Auth.Enabled { if !cfg.Auth.Enabled {
return true, "" 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, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto, 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 == "" {
err := "Auth token == nil" return false, fmt.Errorf("Auth token not found")
return false, err
} }
isValid = authToken == cfg.Auth.AuthToken isValid = authToken == cfg.Auth.Token
if !isValid { if !isValid {
err := fmt.Sprintf("Auth token incorrect: %s", authToken) return false, fmt.Errorf("Auth token invalid")
return false, err
} }
return isValid, "" return isValid, nil
} }

View File

@@ -1,15 +1,16 @@
package auth package auth
import ( import (
"fmt"
"ghproxy/config" "ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/logger" "github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) )
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
@@ -34,18 +35,18 @@ func Init(cfg *config.Config) {
logDebug("Auth Init") logDebug("Auth Init")
} }
func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) { func AuthHandler(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, "" return true, nil
} else { } else {
logError("Auth method not supported") logError("Auth method not supported %s", cfg.Auth.Method)
return false, "Auth method not supported" return false, fmt.Errorf("%s", fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method))
} }
} }

View File

@@ -1,27 +1,46 @@
package config package config
import ( import (
"os"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
type Config struct { type Config struct {
Server ServerConfig Server ServerConfig
Httpc HttpcConfig Httpc HttpcConfig
GitClone GitCloneConfig
Shell ShellConfig
Pages PagesConfig Pages PagesConfig
Log LogConfig Log LogConfig
CORS CORSConfig
Auth AuthConfig Auth AuthConfig
Blacklist BlacklistConfig Blacklist BlacklistConfig
Whitelist WhitelistConfig Whitelist WhitelistConfig
RateLimit RateLimitConfig RateLimit RateLimitConfig
Outbound OutboundConfig Outbound OutboundConfig
Docker DockerConfig
} }
/*
[server]
host = "0.0.0.0"
port = 8080
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
sizeLimit = 125 # MB
memLimit = 0 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false
*/
type ServerConfig struct { type ServerConfig struct {
Port int `toml:"port"` Port int `toml:"port"`
Host string `toml:"host"` Host string `toml:"host"`
NetLib string `toml:"netlib"`
SizeLimit int `toml:"sizeLimit"` SizeLimit int `toml:"sizeLimit"`
EnableH2C string `toml:"enableH2C"` MemLimit int64 `toml:"memLimit"`
H2C bool `toml:"H2C"`
Cors string `toml:"cors"`
Debug bool `toml:"debug"` Debug bool `toml:"debug"`
} }
@@ -31,41 +50,73 @@ mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode maxConnsPerHost = 0 # only for advanced mode
useCustomRawHeaders = false
*/ */
type HttpcConfig struct { type HttpcConfig struct {
Mode string `toml:"mode"` Mode string `toml:"mode"`
MaxIdleConns int `toml:"maxIdleConns"` MaxIdleConns int `toml:"maxIdleConns"`
MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"` MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"`
MaxConnsPerHost int `toml:"maxConnsPerHost"` MaxConnsPerHost int `toml:"maxConnsPerHost"`
UseCustomRawHeaders bool `toml:"useCustomRawHeaders"`
}
/*
[gitclone]
mode = "bypass" # bypass / cache
smartGitAddr = "http://127.0.0.1:8080"
ForceH2C = true
*/
type GitCloneConfig struct {
Mode string `toml:"mode"`
SmartGitAddr string `toml:"smartGitAddr"`
ForceH2C bool `toml:"ForceH2C"`
}
/*
[shell]
editor = true
rewriteAPI = false
*/
type ShellConfig struct {
Editor bool `toml:"editor"`
RewriteAPI bool `toml:"rewriteAPI"`
} }
/* /*
[pages] [pages]
enabled = false mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula" theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www" staticDir = "/data/www"
*/ */
type PagesConfig struct { type PagesConfig struct {
Enabled bool `toml:"enabled"` Mode string `toml:"mode"`
Theme string `toml:"theme"` Theme string `toml:"theme"`
StaticDir string `toml:"staticDir"` StaticDir string `toml:"staticDir"`
} }
type LogConfig struct { type LogConfig struct {
LogFilePath string `toml:"logFilePath"` LogFilePath string `toml:"logFilePath"`
MaxLogSize int `toml:"maxLogSize"` MaxLogSize int `toml:"maxLogSize"`
Level string `toml:"level"` Level string `toml:"level"`
} HertZLogPath string `toml:"hertzLogPath"`
type CORSConfig struct {
Enabled bool `toml:"enabled"`
} }
/*
[auth]
Method = "parameters" # "header" or "parameters"
Key = ""
Token = "token"
enabled = false
passThrough = false
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"`
PassThrough bool `toml:"passThrough"` Token string `toml:"token"`
PassThrough bool `toml:"passThrough"`
ForceAllowApi bool `toml:"ForceAllowApi"`
} }
type BlacklistConfig struct { type BlacklistConfig struct {
@@ -95,11 +146,120 @@ type OutboundConfig struct {
Url string `toml:"url"` Url string `toml:"url"`
} }
/*
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub
*/
type DockerConfig struct {
Enabled bool `toml:"enabled"`
Target string `toml:"target"`
}
// LoadConfig 从 TOML 配置文件加载配置 // LoadConfig 从 TOML 配置文件加载配置
func LoadConfig(filePath string) (*Config, error) { func LoadConfig(filePath string) (*Config, error) {
if !FileExists(filePath) {
// 楔入配置文件
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",
NetLib: "netpoll",
SizeLimit: 125,
MemLimit: 0,
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",
HertZLogPath: "/data/ghproxy/log/hertz.log",
},
Auth: AuthConfig{
Enabled: false,
Method: "parameters",
Key: "",
Token: "token",
PassThrough: false,
ForceAllowApi: false,
},
Blacklist: BlacklistConfig{
Enabled: false,
BlacklistFile: "/data/ghproxy/config/blacklist.json",
},
Whitelist: WhitelistConfig{
Enabled: false,
WhitelistFile: "/data/ghproxy/config/whitelist.json",
},
RateLimit: RateLimitConfig{
Enabled: false,
RateMethod: "total",
RatePerMinute: 100,
Burst: 10,
},
Outbound: OutboundConfig{
Enabled: false,
Url: "socks5://127.0.0.1:1080",
},
Docker: DockerConfig{
Enabled: false,
Target: "ghcr",
},
}
}

View File

@@ -1,8 +1,11 @@
[server] [server]
host = "0.0.0.0" host = "0.0.0.0"
port = 8080 port = 8080
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
sizeLimit = 125 # MB sizeLimit = 125 # MB
enableH2C = "on" # "on" or "off" memLimit = 0 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false debug = false
[httpc] [httpc]
@@ -10,9 +13,19 @@ mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode maxConnsPerHost = 0 # only for advanced mode
useCustomRawHeaders = false
[gitclone]
mode = "bypass" # bypass / cache
smartGitAddr = "http://127.0.0.1:8080"
ForceH2C = false
[shell]
editor = false
rewriteAPI = false
[pages] [pages]
enabled = false mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula" theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www" staticDir = "/data/www"
@@ -20,15 +33,15 @@ staticDir = "/data/www"
logFilePath = "/data/ghproxy/log/ghproxy.log" logFilePath = "/data/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none level = "info" # dump, debug, info, warn, error, none
hertzLogPath = "/data/ghproxy/log/hertz.log"
[cors]
enabled = true
[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
[blacklist] [blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json" blacklistFile = "/data/ghproxy/config/blacklist.json"
@@ -46,4 +59,8 @@ burst = 5
[outbound] [outbound]
enabled = false enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub

View File

@@ -1,8 +1,11 @@
[server] [server]
host = "127.0.0.1" host = "127.0.0.1"
port = 8080 port = 8080
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
sizeLimit = 125 # MB sizeLimit = 125 # MB
enableH2C = "on" memLimit = 0 # MB
H2C = true
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
debug = false debug = false
[httpc] [httpc]
@@ -11,8 +14,17 @@ maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode maxConnsPerHost = 0 # only for advanced mode
[gitclone]
mode = "bypass" # bypass / cache
smartGitAddr = "http://127.0.0.1:8080"
ForceH2C = false
[shell]
editor = false
rewriteAPI = false
[pages] [pages]
enabled = false mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula" theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/usr/local/ghproxy/pages" staticDir = "/usr/local/ghproxy/pages"
@@ -20,15 +32,14 @@ staticDir = "/usr/local/ghproxy/pages"
logFilePath = "/usr/local/ghproxy/log/ghproxy.log" logFilePath = "/usr/local/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB maxLogSize = 5 # MB
level = "info" # dump, debug, info, warn, error, none level = "info" # dump, debug, info, warn, error, none
hertzLogPath = "/usr/local/ghproxy/log/hertz.log"
[cors]
enabled = true
[auth] [auth]
authMethod = "parameters" # "header" or "parameters" authMethod = "parameters" # "header" or "parameters"
authToken = "token" authToken = "token"
enabled = false enabled = false
passThrough = false passThrough = false
ForceAllowApi = false
[blacklist] [blacklist]
blacklistFile = "/usr/local/ghproxy/config/blacklist.json" blacklistFile = "/usr/local/ghproxy/config/blacklist.json"
@@ -47,3 +58,7 @@ burst = 5
[outbound] [outbound]
enabled = false enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub

View File

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

367
docs/config.md Normal file
View File

@@ -0,0 +1,367 @@
# 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
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
sizeLimit = 125 # MB
memLimit = 0 # 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
useCustomRawHeaders = false
[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
hertzLogPath = "/data/ghproxy/log/hertz.log"
[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"
[docker]
enabled = false
target = "ghcr" # ghcr/dockerhub
```
### 配置项详细说明
* **`[server]` - 服务器配置**
* `host`: 监听地址。
* 类型: 字符串 (`string`)
* 默认值: `"0.0.0.0"` (监听所有)
* 说明: 设置 `ghproxy` 监听的网络地址。通常设置为 `"0.0.0.0"` 以监听所有可用的网络接口。
* `port`: 监听端口。
* 类型: 整数 (`int`)
* 默认值: `8080`
* 说明: 设置 `ghproxy` 监听的端口号。
* `netlib`: 底层网络库。
* 类型: 字符串 (`string`)
* 默认值: `""` (HertZ默认处置)
* 说明: `"std"` `"standard"` `"net/http"` `"net"` 均会被设置为go标准库`net/http`, 设置为`"netpoll"`或`""`会由`HertZ`默认逻辑处理
* `sizeLimit`: 请求体大小限制。
* 类型: 整数 (`int`)
* 默认值: `125` (MB)
* 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。
* `memLimit`: `runtime`内存限制
* 类型: 整数 (`int64`)
* 默认值: `0` (不传入)
* 说明: 给`runtime`的指标, 让gc行为更高效
* `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` 表示不限制。
* `useCustomRawHeaders`: 使用预定义header避免github waf对应zh-CN的封锁
* 类型: 布尔值(`bool`)
* 默认值: `false`(停用)
* 说明: 启用后, 拉取raw文件会使用程序预定义的固定headers, 而不是原先的复制行为
* **`[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"`: 禁用所有日志输出。
* `hertzLogPath`: `HertZ`日志文件路径。
* 类型: 字符串 (`string`)
* 默认值: `"/data/ghproxy/log/hertz.log"`
* 说明: 设置 `HertZ` 日志文件的存储路径。
* **`[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 代理协议。
* **`[docker]` - Docker 镜像代理配置**
* `enabled`: 是否启用 Docker 镜像代理功能。
* 类型: 布尔值 (`bool`)
* 默认值: `false` (禁用)
* 说明: 当设置为 `true` 时,`ghproxy` 将尝试代理 Docker 镜像的下载请求,以加速从 GitHub Container Registry (GHCR) 或 Docker Hub 下载镜像。
* `target`: 代理的目标 Docker 注册表。
* 类型: 字符串 (`string`)
* 默认值: `"ghcr"` (代理 GHCR)
* 可选值: `"ghcr"` 或 `"dockerhub"`
* 说明: 指定要代理的 Docker 注册表。
* `"ghcr"`: 代理 GitHub Container Registry (ghcr.io)。
* `"dockerhub"`: 代理 Docker Hub (docker.io)。
## `blacklist.json` - 黑名单配置
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。
```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

60
go.mod
View File

@@ -1,40 +1,40 @@
module ghproxy module ghproxy
go 1.24.0 go 1.24.2
require ( require (
github.com/BurntSushi/toml v1.4.0 github.com/BurntSushi/toml v1.5.0
github.com/WJQSERVER-STUDIO/go-utils/logger v1.3.0 github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0
github.com/gin-gonic/gin v1.10.0 github.com/cloudwego/hertz v0.9.7
github.com/satomitouka/touka-httpc v0.2.0 github.com/hertz-contrib/http2 v0.1.8
golang.org/x/net v0.35.0 github.com/satomitouka/touka-httpc v0.4.1
golang.org/x/time v0.10.0 golang.org/x/net v0.39.0
golang.org/x/time v0.11.0
) )
require ( require (
github.com/bytedance/sonic v1.12.8 // indirect github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 // indirect
github.com/bytedance/gopkg v0.1.2 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/cloudwego/gopkg v0.1.4 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect github.com/cloudwego/netpoll v0.7.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/google/go-cmp v0.7.0 // indirect
github.com/go-playground/validator/v10 v10.25.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/nyaruka/phonenumbers v1.6.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/arch v0.14.0 // indirect golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.33.0 // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )
//replace github.com/satomitouka/touka-httpc v0.4.1 => /data/github/satomitoka/touka-httpc

182
go.sum
View File

@@ -1,91 +1,145 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.3.0 h1:rOvutC4zYfvtSGN2CNZrycjtq8dLpfu7ypy7tTEErPY= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.3.0/go.mod h1:oW884JCCPDU6c906LI0uKXndWLiRvjb9LkGYC2cqRO8= github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YYtrg1ixVSB/JvZM=
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po=
github.com/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/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk=
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
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.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.3/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.7 h1:tAVaiO+vTf+ZkQhvNhKbDJ0hmC4oJ7bzwDi1KhvhHy4=
github.com/cloudwego/hertz v0.9.7/go.mod h1:t6d7NcoQxPmETvzPMMIVPHMn5C5QzpqIiFsaavoLJYQ=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4=
github.com/cloudwego/netpoll v0.7.0/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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/hertz-contrib/http2 v0.1.8 h1:kjfCGkUxJZHgfPsnRjx1FLJBG55KvtvSQD214guBQLw=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/nyaruka/phonenumbers v1.6.1 h1:XAJcTdYow16VrVKfglznMpJZz8KMJoMjx/91sX+K940=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/nyaruka/phonenumbers v1.6.1/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
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.2.0 h1:JohnKH0T5KuVcouycqSI70oJIhMxY1nlNDhgZRxI73s= github.com/satomitouka/touka-httpc v0.4.1 h1:K1LJwSJJKRPkol6MPOEzc8bReAIUqxVuzdFfTAi/2AI=
github.com/satomitouka/touka-httpc v0.2.0/go.mod h1:ULB/0Ze0Apm46YKl35Jmj1hW5YLVVeOGqCqn+ijqGPM= github.com/satomitouka/touka-httpc v0.4.1/go.mod h1:E1JeXw81XclzvlqVvSio/GcDmvN8wWLPpbNRN42Uwfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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.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/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
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/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/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-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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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

View File

@@ -1,34 +0,0 @@
package loggin
import (
"ghproxy/timing"
"time"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
)
var (
logw = logger.Logw
LogDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
// 日志中间件
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 处理请求
c.Next()
var timingResults time.Duration
// 获取计时结果
timingResults, _ = timing.Get(c)
// 记录日志 IP METHOD URL USERAGENT PROTOCOL STATUS TIMING
logInfo("%s %s %s %s %d %s ", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Writer.Status(), timingResults)
}
}

486
main.go
View File

@@ -1,49 +1,58 @@
package main package main
import ( import (
"context"
"embed" "embed"
"flag" "flag"
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"net/http" "net/http"
"os"
"runtime/debug"
"time" "time"
"ghproxy/api" "ghproxy/api"
"ghproxy/auth" "ghproxy/auth"
"ghproxy/config" "ghproxy/config"
"ghproxy/loggin" "ghproxy/middleware/loggin"
"ghproxy/proxy" "ghproxy/proxy"
"ghproxy/rate" "ghproxy/rate"
"ghproxy/timing"
"github.com/WJQSERVER-STUDIO/go-utils/logger" "github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/hertz-contrib/http2/factory"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/adaptor"
"github.com/cloudwego/hertz/pkg/common/hlog"
"github.com/cloudwego/hertz/pkg/network/standard"
_ "net/http/pprof"
) )
var ( var (
cfg *config.Config cfg *config.Config
router *gin.Engine r *server.Hertz
configfile = "/data/ghproxy/config/config.toml" configfile = "/data/ghproxy/config/config.toml"
cfgfile string hertZfile *os.File
version string cfgfile string
dev 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/bootstrap/* //go:embed pages/*
pagesFS embed.FS pagesFS embed.FS
//go:embed pages/nebula/*
NebulaPagesFS embed.FS
) )
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
@@ -51,7 +60,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() {
@@ -59,8 +109,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)
} }
@@ -71,23 +124,48 @@ 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)
logDebug("Loaded config: %v\n", cfg) logDebug("Loaded config: %v\n", cfg)
logInfo("Init Completed") logInfo("Logger Initialized Successfully")
}
func setupHertZLogger(cfg *config.Config) {
var err error
if cfg.Log.HertZLogPath != "" {
hertZfile, err = os.OpenFile(cfg.Log.HertZLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
hlog.SetOutput(os.Stdout)
logWarning("Failed to open hertz log file: %v", err)
} else {
hlog.SetOutput(hertZfile)
}
hlog.SetLevel(hlog.LevelInfo)
}
}
func setMemLimit(cfg *config.Config) {
if cfg.Server.MemLimit > 0 {
debug.SetMemoryLimit((cfg.Server.MemLimit) * 1024 * 1024)
logInfo("Set Memory Limit to %d MB", cfg.Server.MemLimit)
}
} }
func loadlist(cfg *config.Config) { func loadlist(cfg *config.Config) {
auth.Init(cfg) auth.Init(cfg)
} }
func setupApi(cfg *config.Config, router *gin.Engine, version string) { func setupApi(cfg *config.Config, r *server.Hertz, version string) {
api.InitHandleRouter(cfg, router, version) api.InitHandleRouter(cfg, r, version)
} }
func setupRateLimit(cfg *config.Config) { func setupRateLimit(cfg *config.Config) {
@@ -106,99 +184,309 @@ func InitReq(cfg *config.Config) {
proxy.InitReq(cfg) proxy.InitReq(cfg)
} }
// loadEmbeddedPages 加载嵌入式页面资源
func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
var pages fs.FS
var err error
switch cfg.Pages.Theme {
case "bootstrap":
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
case "nebula":
pages, err = fs.Sub(pagesFS, "pages/nebula")
case "design":
pages, err = fs.Sub(pagesFS, "pages/design")
case "metro":
pages, err = fs.Sub(pagesFS, "pages/metro")
case "classic":
pages, err = fs.Sub(pagesFS, "pages/classic")
case "mino":
pages, err = fs.Sub(pagesFS, "pages/mino")
default:
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
}
if err != nil {
return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err)
}
// 初始化errPagesFs
errPagesInitErr := proxy.InitErrPagesFS(pagesFS)
if errPagesInitErr != nil {
logWarning("errPagesInitErr: %s", errPagesInitErr)
}
var assets fs.FS
assets, err = fs.Sub(pagesFS, "pages/assets")
return pages, assets, nil
}
// setupPages 设置页面路由
func setupPages(cfg *config.Config, r *server.Hertz) {
switch cfg.Pages.Mode {
case "internal":
err := setInternalRoute(cfg, r)
if err != nil {
logError("Failed when processing internal pages: %s", err)
fmt.Println(err.Error())
return
}
case "external":
// 设置外部资源路径
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir)
stylesheetsPath := fmt.Sprintf("%s/style.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("/favicon.ico", faviconPath)
r.StaticFile("/script.js", javascriptsPath)
r.StaticFile("/style.css", stylesheetsPath)
r.StaticFile("/bootstrap.min.css", bootstrapPath)
r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath)
default:
// 处理无效的Pages Mode
logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode)
err := setInternalRoute(cfg, r)
if err != nil {
logError("Failed when processing internal pages: %s", err)
fmt.Println(err.Error())
return
}
}
}
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) setupHertZLogger(cfg)
setupRateLimit(cfg) InitReq(cfg)
setMemLimit(cfg)
loadlist(cfg)
setupRateLimit(cfg)
if cfg.Server.Debug { if cfg.Server.Debug {
dev = "true" runMode = "dev"
version = "dev"
}
if dev == "true" {
gin.SetMode(gin.DebugMode)
runMode = "dev"
} else {
gin.SetMode(gin.ReleaseMode)
runMode = "release"
}
logDebug("Run Mode: %s", runMode)
gin.LoggerWithWriter(io.Discard)
router = gin.New()
// 添加recovery中间件
router.Use(gin.Recovery())
// 添加log中间件
router.Use(loggin.Middleware())
// 添加计时中间件
router.Use(timing.Middleware())
//H2C默认值为true而后遵循cfg.Server.EnableH2C的设置
if cfg.Server.EnableH2C == "on" {
router.UseH2C = true
} else if cfg.Server.EnableH2C == "" {
router.UseH2C = true
} else {
router.UseH2C = false
}
setupApi(cfg, router, version)
if cfg.Pages.Enabled {
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
router.GET("/", func(c *gin.Context) {
c.File(indexPagePath)
logInfo("IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto)
})
router.StaticFile("/favicon.ico", faviconPath)
} else if !cfg.Pages.Enabled {
var pages fs.FS
var err error
if cfg.Pages.Theme == "bootstrap" {
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
if err != nil {
logError("Failed when processing pages: %s", err)
}
} else if cfg.Pages.Theme == "nebula" {
pages, err = fs.Sub(NebulaPagesFS, "pages/nebula")
if err != nil {
logError("Failed when processing pages: %s", err)
}
} else { } else {
pages, err = fs.Sub(pagesFS, "pages/bootstrap") runMode = "release"
if err != nil {
logError("Failed when processing pages: %s", err)
}
} }
router.GET("/", gin.WrapH(http.FileServer(http.FS(pages))))
router.GET("/favicon.ico", gin.WrapH(http.FileServer(http.FS(pages)))) if cfg.Server.Debug {
router.GET("/script.js", gin.WrapH(http.FileServer(http.FS(pages)))) version = "Dev" // 如果是Debug模式版本设置为"Dev"
router.GET("/style.css", gin.WrapH(http.FileServer(http.FS(pages)))) }
}
}
func main() {
if showVersion || showHelp {
return
}
logDebug("Run Mode: %s Netlib: %s", runMode, cfg.Server.NetLib)
if cfg == nil {
fmt.Println("Config not loaded, exiting.")
return
} }
router.NoRoute(func(c *gin.Context) { addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c) if cfg.Server.NetLib == "std" || cfg.Server.NetLib == "standard" || cfg.Server.NetLib == "net" || cfg.Server.NetLib == "net/http" {
if cfg.Server.H2C {
r = server.New(
server.WithH2C(true),
server.WithHostPorts(addr),
server.WithTransport(standard.NewTransporter),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
server.WithTransport(standard.NewTransporter),
)
}
} else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" {
if cfg.Server.H2C {
r = server.New(
server.WithH2C(true),
server.WithHostPorts(addr),
)
r.AddProtocol("h2", factory.NewServerFactory())
} else {
r = server.New(
server.WithHostPorts(addr),
)
}
} else {
logError("Invalid NetLib: %s", cfg.Server.NetLib)
fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib)
os.Exit(1)
}
r.Use(recovery.Recovery()) // Recovery中间件
r.Use(loggin.Middleware()) // log中间件
setupApi(cfg, r, version)
setupPages(cfg, r)
r.GET("/github.com/:user/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "releases")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:user/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "releases")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:user/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "blob")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:user/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "raw")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:user/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "clone")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/github.com/:user/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "clone")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "raw")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/gist.githubusercontent.com/:user/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "gist")
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.GET("/api.github.com/repos/:user/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
c.Set("matcher", "api")
proxy.RoutingHandler(cfg, limiter, iplimiter)(ctx, c)
})
r.Any("/v2/*filepath", func(ctx context.Context, c *app.RequestContext) {
proxy.GhcrRouting(cfg)(ctx, c)
})
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
}) })
fmt.Printf("GHProxy Version: %s\n", version) fmt.Printf("GHProxy Version: %s\n", version)
fmt.Printf("A Go Based High-Performance Github Proxy \n") fmt.Printf("A Go Based High-Performance Github Proxy \n")
fmt.Printf("Made by WJQSERVER-STUDIO\n") fmt.Printf("Made by WJQSERVER-STUDIO\n")
}
func main() { if cfg.Server.Debug {
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)) go func() {
if err != nil { http.ListenAndServe("localhost:6060", nil)
logError("Failed to start server: %v\n", err) }()
} }
r.Spin()
defer logger.Close() defer logger.Close()
defer func() {
if hertZfile != nil {
var err error
err = hertZfile.Close()
if err != nil {
logError("Failed to close hertz log file: %v", err)
}
}
}()
fmt.Println("Program Exit") fmt.Println("Program Exit")
} }

View File

@@ -0,0 +1,32 @@
package loggin
import (
"context"
"time"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/cloudwego/hertz/pkg/app"
)
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
// 日志中间件
func Middleware() app.HandlerFunc {
return func(ctx context.Context, c *app.RequestContext) {
startTime := time.Now()
c.Next(ctx)
endTime := time.Now()
timingResults := endTime.Sub(startTime)
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)
}
}

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) // 继续处理请求
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,103 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Github文件加速</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container py-4 py-md-5">
<main>
<div class="card mb-4">
<div class="card-body">
<h1 class="text-center mb-4">Github文件加速</h1>
<p class="lead text-center mb-4">为访问Github文件进行加速</p>
<form id="github-form">
<div class="mb-3">
<input type="text" class="form-control form-control-lg" id="githubLinkInput"
placeholder="请键入需要代理的 Github 链接">
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">获取文件链接</button>
</form>
<div id="output" class="mt-3 bg-light p-3 rounded position-relative" style="display: none;">
<pre id="formattedLinkOutput" class="mb-0"></pre>
<button id="copyButton"
class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2" title="复制链接">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-clipboard" viewBox="0 0 16 16">
<path
d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z" />
<path
d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z" />
</svg>
</button>
<button id="openButton"
class="btn btn-outline-secondary btn-sm position-absolute top-0 end-0 m-2 me-5"
title="在新标签页中打开">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-box-arrow-up-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z" />
<path fill-rule="evenodd"
d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z" />
</svg>
</button>
</div>
<p class="text-muted small mt-3 mb-0">GitHub 链接带不带协议头均可,支持 release、archive 以及文件,转换后链接均可使用。</p>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">文件大小限制</h5>
<p class="card-text" id="sizeLimitDisplay">...</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">白名单状态</h5>
<p class="card-text" id="whiteListStatus">...</p>
</div>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">黑名单状态</h5>
<p class="card-text" id="blackListStatus">...</p>
</div>
</div>
</div>
</div>
</main>
<footer class="text-center mt-4">
<p class="text-muted">
Copyright &copy; 2024-2025 WJQSERVER-STUDIO<br>
<a href="https://github.com/WJQSERVER-STUDIO/ghproxy" class="text-decoration-none">GitHub 仓库</a> |
<a href="https://t.me/ghproxy_go" class="text-decoration-none">Telegram 交流群</a>
</p>
</footer>
</div>
<div class="toast-container position-fixed top-0 end-0 p-3">
<div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body"></div>
</div>
</div>
<div id="versionBadge" class="version-badge"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="script.js"></script>
</body>
</html>

View File

@@ -1,84 +0,0 @@
const githubForm = document.getElementById('github-form');
const githubLinkInput = document.getElementById('githubLinkInput');
const formattedLinkOutput = document.getElementById('formattedLinkOutput');
const output = document.getElementById('output');
const copyButton = document.getElementById('copyButton');
const openButton = document.getElementById('openButton');
const toast = new bootstrap.Toast(document.getElementById('toast'));
function showToast(message) {
const toastBody = document.querySelector('.toast-body');
toastBody.textContent = message;
toast.show();
}
function formatGithubLink(githubLink) {
const currentHost = window.location.host;
let formattedLink = "";
if (githubLink.startsWith("https://github.com/") || githubLink.startsWith("http://github.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/github.com" + githubLink.substring(githubLink.indexOf("/", 8));
} else if (githubLink.startsWith("github.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
} else if (githubLink.startsWith("https://raw.githubusercontent.com/") || githubLink.startsWith("http://raw.githubusercontent.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + githubLink.substring(githubLink.indexOf("/", 7));
} else if (githubLink.startsWith("raw.githubusercontent.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
} else if (githubLink.startsWith("https://gist.githubusercontent.com/") || githubLink.startsWith("http://gist.githubusercontent.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/gist.github.com" + githubLink.substring(githubLink.indexOf("/", 18));
} else if (githubLink.startsWith("gist.githubusercontent.com/")) {
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
} else {
showToast('请输入有效的GitHub链接');
return null;
}
return formattedLink;
}
githubForm.addEventListener('submit', function (e) {
e.preventDefault();
const formattedLink = formatGithubLink(githubLinkInput.value);
if (formattedLink) {
formattedLinkOutput.textContent = formattedLink;
output.style.display = 'block';
}
});
copyButton.addEventListener('click', function () {
navigator.clipboard.writeText(formattedLinkOutput.textContent).then(() => {
showToast('链接已复制到剪贴板');
});
});
openButton.addEventListener('click', function () {
window.open(formattedLinkOutput.textContent, '_blank');
});
function fetchAPI() {
fetch('/api/size_limit')
.then(response => response.json())
.then(data => {
document.getElementById('sizeLimitDisplay').textContent = `${data.MaxResponseBodySize} MB`;
});
fetch('/api/whitelist/status')
.then(response => response.json())
.then(data => {
document.getElementById('whiteListStatus').textContent = data.Whitelist ? '已开启' : '已关闭';
});
fetch('/api/blacklist/status')
.then(response => response.json())
.then(data => {
document.getElementById('blackListStatus').textContent = data.Blacklist ? '已开启' : '已关闭';
});
fetch('/api/version')
.then(response => response.json())
.then(data => {
document.getElementById('versionBadge').textContent = data.Version;
});
}
document.addEventListener('DOMContentLoaded', fetchAPI);

View File

@@ -1,259 +0,0 @@
/* 通用样式 */
:root {
--primary-color: #007aff;
/* 主要按钮颜色 */
--secondary-color: #34c759;
/* 次要按钮颜色 */
--background-color: #f9f9f9;
/* 亮色模式背景 */
--card-background: #ffffff;
/* 卡片背景 */
--text-color: #333333;
/* 亮色模式文本颜色 */
--border-color: #e0e0e0;
/* 边框颜色 */
--input-background: #ffffff;
/* 输入框背景 */
--input-border: #d1d1d6;
/* 输入框边框 */
--pre-background: #f1f3f4;
/* 代码块背景 */
--pre-text-color: #333333;
/* 代码块文本颜色 */
--version-badge-hover: #39c5bb;
/* 说明文字颜色 */
--muted-text-color: #6c757d;
}
body {
background-color: var(--background-color);
color: var(--text-color);
font-family: sans-serif;
line-height: 1.8;
font-size: 15px;
margin: 0;
padding: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--text-color);
font-weight: 800;
letter-spacing: 0.5px;
margin: 1rem 0;
}
p,
span,
a,
li {
color: var(--text-color);
margin-bottom: 1rem;
}
a {
text-decoration: none;
color: var(--primary-color);
}
a:hover {
color: #0056b3;
}
.card {
background-color: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
transform: translateY(-4px);
}
.btn-outline-secondary {
border-radius: 50%;
padding: 6px;
transition: #e9e9e9 0.3s ease-in-out, color 0.3s ease-in-out;
}
.btn-outline-secondary:hover {
background-color: var(--primary-color);
color: white;
}
.form-control {
background-color: var(--input-background);
border: 1px solid var(--input-border);
color: var(--text-color);
padding: 10px;
border-radius: 4px;
font-size: 14px;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3);
}
.text-muted {
color: var(--muted-text-color) !important;
}
.bg-light {
background-color: var(--card-background) !important;
}
pre {
background-color: var(--pre-background);
color: var(--pre-text-color);
padding: 16px;
border-radius: 8px;
overflow-x: auto;
font-size: 14px;
line-height: 1.6;
}
.version-badge {
position: fixed;
bottom: 20px;
right: 20px;
background-color: var(--secondary-color);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease-in-out;
}
.version-badge:hover {
background-color: var(--version-badge-hover);
}
footer {
padding: 16px;
text-align: center;
color: var(--text-color);
font-size: 0.9rem;
background-color: var(--card-background);
}
footer a {
color: var(--primary-color);
}
footer a:hover {
color: #0056b3;
}
/* 暗色模式 */
@media (prefers-color-scheme: dark) {
:root {
--background-color: #121212;
/* 深灰色背景 */
--card-background: #1e1e1e;
/* 卡片背景稍浅 */
--text-color: #ffffff;
/* 纯白文本 */
--primary-color: #0a84ff;
/* 按钮蓝色 */
--secondary-color: #30d158;
/* 次要按钮绿色 */
--border-color: #3a3a3a;
/* 边框颜色 */
--input-background: #2c2c2c;
/* 输入框背景 */
--input-border: #4a4a4a;
/* 输入框边框 */
--pre-background: #3b3636;
/* 代码块背景 */
--pre-text-color: #ffffff;
/* 代码块文本颜色 */
--version-badge-hover: #39c5bc9a;
/* 说明文字颜色 */
--muted-text-color: #a0a0a0;
}
body {
background-color: var(--background-color);
color: var(--text-color);
}
h1,
h2,
h3,
h4,
h5,
h6,
p,
span,
a,
li {
color: var(--text-color);
}
.card {
background-color: var(--card-background);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.btn-outline-secondary {
border-radius: 50%;
padding: 6px;
transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out;
}
.btn-outline-secondary:hover {
background-color: var(--primary-color);
color: white;
}
.toast {
background-color: var(--card-background);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.toast-body {
padding: 12px;
}
.form-control {
background-color: var(--input-background);
border: 1px solid var(--input-border);
color: var(--text-color);
}
.bg-light {
background-color: var(--card-background) !important;
}
pre {
background-color: var(--pre-background);
color: var(--pre-text-color);
}
footer {
background-color: var(--card-background);
color: var(--text-color);
}
footer a {
color: var(--primary-color);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,168 +0,0 @@
<!DOCTYPE html>
<html lang="zh" data-bs-theme="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub加速服务</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="style.css" rel="stylesheet">
</head>
<body>
<div class="container py-4 py-lg-5">
<header class="text-center mb-5">
<h1 class="display-5 fw-bold mb-3">GitHub加速服务</h1>
<p class="lead text-muted">高速稳定的 GitHub 资源访问解决方案</p>
</header>
<div class="main-card p-4 mb-4">
<form id="mainForm" class="mb-4">
<div class="mb-3">
<label for="inputUrl" class="form-label fw-semibold">GitHub 链接</label>
<input type="url" class="form-control form-control-lg" id="inputUrl" placeholder="输入 GitHub 文件/仓库链接"
required>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100 py-2">
🚀 生成加速链接
</button>
</form>
<!-- 结果输出 -->
<div id="output" class="mt-4" hidden>
<div class="code-block">
<code id="outputLink" class="d-block text-break"></code>
</div>
<div class="d-flex gap-2 mt-3">
<button id="copyBtn" class="btn btn-outline-secondary">
⎘ 复制链接
</button>
<button id="openBtn" class="btn btn-primary">
↗ 立即打开
</button>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-4">
<div class="main-card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">文件大小限制</h6>
<small class="text-muted">最大支持文件尺寸</small>
</div>
<span id="sizeLimit" class="status-badge bg-primary">...</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="main-card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">白名单状态</h6>
<small class="text-muted">访问控制列表</small>
</div>
<span id="whitelistStatus" class="status-badge bg-success">...</span>
</div>
</div>
</div>
<div class="col-md-4">
<div class="main-card p-3">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-0">黑名单状态</h6>
<small class="text-muted">屏蔽列表状态</small>
</div>
<span id="blacklistStatus" class="status-badge bg-danger">...</span>
</div>
</div>
</div>
</div>
<div class="main-card p-4">
<h2 class="h4 fw-bold mb-4">📚 详细使用指南</h2>
<div class="guide-section">
<h3 class="h5 fw-semibold mb-3">支持的工具</h3>
<ul class="list-unstyled">
<li class="mb-2">✅ 支持域名github.com</li>
<li class="mb-2">✅ 支持域名raw.githubusercontent.com</li>
<li class="mb-2">✅ 支持域名gist.githubusercontent.com</li>
<li class="mb-2">✅ 支持HTTPS Git Clone</li>
<li class="text-muted">❌ 不支持 SSH Git Clone</li>
</ul>
</div>
<div class="guide-section">
<h3 class="h5 fw-semibold mb-3">基础用法示例</h3>
<div class="command-example">
<h4 class="h6 fw-medium mb-2">Git 克隆</h4>
<code
class="d-block mb-3">git clone <span class="protocol">https</span>://<span class="host">example.com</span>/https://github.com/user/project.git</code>
<h4 class="h6 fw-medium mb-2">私有仓库克隆</h4>
<code
class="d-block">git clone <span class="protocol">https</span>://user:your_token@<span class="host">example.com</span>/https://github.com/user/project.git</code>
</div>
<div class="command-example">
<h4 class="h6 fw-medium mb-2">文件下载</h4>
<code
class="d-block mb-3">wget <span class="protocol">https</span>://<span class="host">example.com</span>/https://raw.githubusercontent.com/user/project/main/README.md</code>
<h4 class="h6 fw-medium mb-2">版本发布</h4>
<code
class="d-block">curl -LO <span class="protocol">https</span>://<span class="host">example.com</span>/https://github.com/user/project/releases/download/v1.0.0/project_1.0.0.amd64.tar.gz</code>
</div>
</div>
<div class="guide-section">
<h3 class="h5 fw-semibold mb-3">支持的文件类型</h3>
<div class="row g-3">
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">原始文件</h4>
<code
class="d-block text-muted fs-sm">https://raw.githubusercontent.com/user/repo/main/file.txt</code>
</div>
</div>
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">分支源码</h4>
<code class="d-block text-muted fs-sm">https://github.com/user/repo/archive/main.zip</code>
</div>
</div>
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">版本发布</h4>
<code
class="d-block text-muted fs-sm">https://github.com/user/repo/releases/download/v1.0.0/app.zip</code>
</div>
</div>
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">Gist 文件</h4>
<code
class="d-block text-muted fs-sm">https://gist.githubusercontent.com/user/gist_id/raw/file.txt</code>
</div>
</div>
<div class="col-md-6">
<div class="main-card p-3">
<h4 class="h6 fw-medium mb-2">HTTPS Git Clone</h4>
<code class="d-block text-muted fs-sm">git clone https://github.com/user/repo.git</code>
</div>
</div>
</div>
</div>
</div>
<footer class="text-center mt-5 text-muted">
<p class="mb-0 small" id="version">v </p>
<p class="mb-0 small">Copyright © 2024 - <span id="currentYear"></span> WJQSERVER-STUDIO<br>
<a href="https://github.com/WJQSERVER-STUDIO/ghproxy" class="text-decoration-none">GitHub 仓库</a> |
<a href="https://t.me/ghproxy_go" class="text-decoration-none">Telegram 交流群</a>
</p>
</footer>
</div>
<div class="toast-container position-fixed bottom-0 end-0 p-3">
<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="script.js"></script>
</body>
</html>

View File

@@ -1,131 +0,0 @@
(() => {
'use strict';
// 初始化基础配置
const currentYear = new Date().getFullYear();
document.getElementById('currentYear').textContent = currentYear;
const toast = new bootstrap.Toast('#liveToast');
// DOM 元素
const form = document.getElementById('mainForm');
const input = document.getElementById('inputUrl');
const output = document.getElementById('output');
const outputLink = document.getElementById('outputLink');
// 获取当前域名
const CURRENT_PROTOCOL = window.location.protocol.replace(':', '');
const CURRENT_HOST = window.location.host;
// 替换协议部分
document.querySelectorAll('code .protocol').forEach(span => {
span.textContent = CURRENT_PROTOCOL;
});
// 替换域名部分
document.querySelectorAll('code .host').forEach(span => {
span.textContent = CURRENT_HOST;
});
// URL 转换规则
const URL_RULES = [
{
regex: /^(?:https?:\/\/)?(?:www\.)?(github\.com\/.*)/i,
build: path => `${location.protocol}//${location.host}/${path}`
},
{
regex: /^(?:https?:\/\/)?(raw\.githubusercontent\.com\/.*)/i,
build: path => `${location.protocol}//${location.host}/${path}`
},
{
regex: /^(?:https?:\/\/)?(gist\.(?:githubusercontent|github)\.com\/.*)/i,
build: path => `${location.protocol}//${location.host}/${path}`
}
];
// 核心功能:链接转换
function transformGitHubURL(url) {
const cleanURL = url.trim().replace(/^https?:\/\//i, '');
for (const rule of URL_RULES) {
const match = cleanURL.match(rule.regex);
if (match) return rule.build(match[1]);
}
return null;
}
// 事件处理
form.addEventListener('submit', e => {
e.preventDefault();
if (!input.checkValidity()) {
input.classList.add('is-invalid');
showToast('⚠️ 请输入有效的 GitHub 链接');
return;
}
const result = transformGitHubURL(input.value);
if (!result) {
showToast('❌ 不支持的链接格式');
return;
}
outputLink.textContent = result;
output.hidden = false;
window.scrollTo({ top: output.offsetTop - 100, behavior: 'smooth' });
});
document.getElementById('copyBtn').addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(outputLink.textContent);
showToast('✅ 链接已复制');
} catch {
showToast('❌ 复制失败');
}
});
document.getElementById('openBtn').addEventListener('click', () => {
window.open(outputLink.textContent, '_blank', 'noopener,noreferrer');
});
// 服务状态监控
async function loadServiceStatus() {
try {
const [size, whitelist, blacklist, version] = await Promise.all([
fetchJSON('/api/size_limit'),
fetchJSON('/api/whitelist/status'),
fetchJSON('/api/blacklist/status'),
fetchJSON('/api/version')
]);
updateStatus('sizeLimit', `${size.MaxResponseBodySize}MB`);
updateStatus('whitelistStatus', whitelist.Whitelist ? '已开启' : '已关闭');
updateStatus('blacklistStatus', blacklist.Blacklist ? '已开启' : '已关闭');
updateStatus('version', `Version ${version.Version}`);
} catch {
showToast('⚠️ 服务状态获取失败');
}
}
async function fetchJSON(url) {
const response = await fetch(url);
if (!response.ok) throw new Error('API Error');
return response.json();
}
function updateStatus(elementId, text) {
const element = document.getElementById(elementId);
if (element) element.textContent = text;
}
// 工具函数
function showToast(message) {
const toastBody = document.querySelector('.toast-body');
toastBody.textContent = message;
toast.show();
}
// 初始化
input.addEventListener('input', () => {
input.classList.remove('is-invalid');
if (output.hidden === false) output.hidden = true;
});
loadServiceStatus();
})();

View File

@@ -1,157 +0,0 @@
:root {
--primary: #007aff;
--secondary: #007aff;
--background: #f9f9f9;
--card-bg: #ffffff;
--text: #333333;
--border: #d1d1d6;
--muted: #64748b;
--success: #00a83ed2;
--danger: #ce0000dd;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #121212;
--card-bg: #1e1e1e;
--text: #ffffff;
--border: #334155;
--muted: #94a3b8;
}
.form-control::placeholder {
color: var(--muted);
}
.text-muted {
color: var(--muted) !important;
}
.btn-outline-secondary {
color: var(--muted);
border-color: var(--border);
}
.status-badge {
color: var(--text) !important;
}
.code-block {
background: rgba(255, 255, 255, 0.05);
}
.command-example {
background: rgba(255, 255, 255, 0.03);
}
.btn-primary {
--bs-btn-bg: var(--primary);
--bs-btn-border-color: var(--primary);
--bs-btn-hover-bg: var(--secondary);
--bs-btn-hover-border-color: var(--secondary);
}
.form-control {
background-color: rgba(255, 255, 255, 0.05);
border-color: var(--border);
color: var(--text);
}
a {
color: var(--secondary);
}
}
body {
background: var(--background);
color: var(--text);
font-family: 'Inter', system-ui, sans-serif;
line-height: 1.6;
}
.main-card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
}
.main-card:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
.code-block {
background: rgba(37, 99, 235, 0.05);
border-left: 3px solid var(--primary);
border-radius: 6px;
padding: 1rem;
position: relative;
overflow-x: auto;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
}
.guide-section {
border-left: 3px solid var(--primary);
padding-left: 1rem;
margin: 2rem 0;
}
.command-example {
position: relative;
padding: 1.25rem;
background: rgba(37, 99, 235, 0.03);
border-radius: 8px;
margin: 1rem 0;
}
.command-example::before {
content: "➜";
position: absolute;
left: -1.5rem;
color: var(--muted);
}
.toast {
position: fixed;
top: 1%;
right: 1%;
background-color: var(--card-background);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.toast-body {
padding: 12px;
}
#version {
text-align: center;
font-size: 13px;
line-height: 26px;
color: #747474;
}
.bg-primary {
--bs-bg-opacity: 1;
background-color: #2c82de !important;
}
.bg-success {
--bs-bg-opacity: 1;
background-color: #2c82de !important;
}
.bg-danger {
--bs-bg-opacity: 1;
background-color: #2c82de !important;
}

View File

@@ -4,22 +4,21 @@ import (
"ghproxy/config" "ghproxy/config"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) )
func AuthPassThrough(c *gin.Context, cfg *config.Config, req *http.Request) { func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Request) {
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, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, 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, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto) logWarning("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol())
// 500 Internal Server Error ErrorPage(c, NewErrorWithStatusLookup(500, "Conflict Auth Method"))
c.JSON(http.StatusInternalServerError, gin.H{"error": "Conflict Auth Method"})
return return
} }
case "header": case "header":
@@ -27,9 +26,8 @@ func AuthPassThrough(c *gin.Context, cfg *config.Config, req *http.Request) {
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, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto) logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol())
// 500 Internal Server Error ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid Auth Method / Auth Method is not be set"))
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid Auth Method / Auth Method is not be set"})
return return
} }
} }

View File

@@ -1,127 +1,122 @@
package proxy package proxy
import ( import (
"bytes" "context"
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) )
func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) { func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
method := c.Request.Method
// 发送HEAD请求, 预获取Content-Length var (
headReq, err := client.NewRequest("HEAD", u, nil) method []byte
req *http.Request
resp *http.Response
err error
)
method = c.Request.Method()
req, err = client.NewRequest(string(method), u, c.Request.BodyStream())
if err != nil { if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err)) HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return return
} }
setRequestHeaders(c, headReq)
removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
AuthPassThrough(c, cfg, headReq)
headResp, err := client.Do(headReq) setRequestHeaders(c, req, cfg, matcher)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
//defer headResp.Body.Close()
defer func(Body io.ReadCloser) {
if err := Body.Close(); err != nil {
logError("Failed to close response body: %v", err)
}
}(headResp.Body)
contentLength := headResp.Header.Get("Content-Length")
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > sizelimit {
finalURL := headResp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return
}
}
body, err := readRequestBody(c)
if err != nil {
HandleError(c, err.Error())
return
}
bodyReader := bytes.NewBuffer(body)
req, err := client.NewRequest(method, u, bodyReader)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
AuthPassThrough(c, cfg, req) AuthPassThrough(c, cfg, req)
resp, err := client.Do(req) resp, err = client.Do(req)
if err != nil { if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return return
} }
defer resp.Body.Close()
// 错误处理(404) // 错误处理(404)
if resp.StatusCode == 404 { if resp.StatusCode == 404 {
c.String(http.StatusNotFound, "File Not Found") ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
return return
} }
var (
bodySize int
contentLength string
sizelimit int
)
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
contentLength = resp.Header.Get("Content-Length") contentLength = resp.Header.Get("Content-Length")
if contentLength != "" { if contentLength != "" {
size, err := strconv.Atoi(contentLength) var err error
if err == nil && size > sizelimit { bodySize, err = strconv.Atoi(contentLength)
finalURL := resp.Request.URL.String() if err != nil {
c.Redirect(http.StatusMovedPermanently, finalURL) logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size) bodySize = -1
}
if err == nil && bodySize > sizelimit {
var finalURL string
finalURL = resp.Request.URL.String()
err = resp.Body.Close()
if err != nil {
logError("Failed to close response body: %v", err)
}
c.Redirect(301, []byte(finalURL))
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize)
return return
} }
} }
// 复制响应头,排除需要移除的 header
for key, values := range resp.Header { for key, values := range resp.Header {
for _, value := range values { if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove {
c.Header(key, value) for _, value := range values {
c.Header(key, value)
}
} }
} }
headersToRemove := map[string]struct{}{ switch cfg.Server.Cors {
"Content-Security-Policy": {}, case "*":
"Referrer-Policy": {},
"Strict-Transport-Security": {},
}
for header := range headersToRemove {
resp.Header.Del(header)
}
if cfg.CORS.Enabled {
c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Origin", "*")
} else { case "":
c.Header("Access-Control-Allow-Origin", "*")
case "nil":
c.Header("Access-Control-Allow-Origin", "") c.Header("Access-Control-Allow-Origin", "")
default:
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
} }
c.Status(resp.StatusCode) c.Status(resp.StatusCode)
// 使用固定32KB缓冲池 if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor {
buffer := BufferPool.Get().([]byte) // 判断body是不是gzip
defer BufferPool.Put(buffer) var compress string
if resp.Header.Get("Content-Encoding") == "gzip" {
compress = "gzip"
}
_, err = io.CopyBuffer(c.Writer, resp.Body, buffer) logDebug("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol())
if err != nil { c.Header("Content-Length", "")
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return var reader io.Reader
reader, _, err = processLinks(resp.Body, compress, string(c.Request.Host()), cfg)
c.SetBodyStream(reader, -1)
if err != nil {
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err)
ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err)))
return
}
} else { } else {
c.Writer.Flush() // 确保刷入 if contentLength != "" {
c.SetBodyStream(resp.Body, bodySize)
return
}
c.SetBodyStream(resp.Body, -1)
} }
} }

115
proxy/docker.go Normal file
View File

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

167
proxy/error.go Normal file
View File

@@ -0,0 +1,167 @@
package proxy
import (
"bytes"
"fmt"
"html/template"
"io/fs"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/cloudwego/hertz/pkg/app"
)
// 日志模块
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func HandleError(c *app.RequestContext, message string) {
ErrorPage(c, NewErrorWithStatusLookup(500, message))
logError(message)
}
type GHProxyErrors struct {
StatusCode int
StatusDesc string
StatusText string
HelpInfo string
ErrorMessage string
}
var (
ErrInvalidURL = &GHProxyErrors{
StatusCode: 400,
StatusDesc: "Bad Request",
StatusText: "无效请求",
HelpInfo: "请求的URL格式不正确请检查后重试。",
}
ErrAuthHeaderUnavailable = &GHProxyErrors{
StatusCode: 401,
StatusDesc: "Unauthorized",
StatusText: "认证失败",
HelpInfo: "缺少或无效的鉴权信息。",
}
ErrForbidden = &GHProxyErrors{
StatusCode: 403,
StatusDesc: "Forbidden",
StatusText: "权限不足",
HelpInfo: "您没有权限访问此资源。",
}
ErrNotFound = &GHProxyErrors{
StatusCode: 404,
StatusDesc: "Not Found",
StatusText: "页面未找到",
HelpInfo: "抱歉,您访问的页面不存在。",
}
ErrTooManyRequests = &GHProxyErrors{
StatusCode: 429,
StatusDesc: "Too Many Requests",
StatusText: "请求过于频繁",
HelpInfo: "您的请求过于频繁,请稍后再试。",
}
ErrInternalServerError = &GHProxyErrors{
StatusCode: 500,
StatusDesc: "Internal Server Error",
StatusText: "服务器内部错误",
HelpInfo: "服务器处理您的请求时发生错误,请稍后重试或联系管理员。",
}
)
var statusErrorMap map[int]*GHProxyErrors
func init() {
statusErrorMap = map[int]*GHProxyErrors{
ErrInvalidURL.StatusCode: ErrInvalidURL,
ErrAuthHeaderUnavailable.StatusCode: ErrAuthHeaderUnavailable,
ErrForbidden.StatusCode: ErrForbidden,
ErrNotFound.StatusCode: ErrNotFound,
ErrTooManyRequests.StatusCode: ErrTooManyRequests,
ErrInternalServerError.StatusCode: ErrInternalServerError,
}
}
func NewErrorWithStatusLookup(statusCode int, errMsg string) *GHProxyErrors {
baseErr, found := statusErrorMap[statusCode]
if found {
return &GHProxyErrors{
StatusCode: baseErr.StatusCode,
StatusDesc: baseErr.StatusDesc,
StatusText: baseErr.StatusText,
HelpInfo: baseErr.HelpInfo,
ErrorMessage: errMsg,
}
} else {
return &GHProxyErrors{
StatusCode: statusCode,
ErrorMessage: errMsg,
}
}
}
var errPagesFs fs.FS
func InitErrPagesFS(pages fs.FS) error {
var err error
errPagesFs, err = fs.Sub(pages, "pages/err")
if err != nil {
return err
}
return nil
}
type ErrorPageData struct {
StatusCode int
StatusDesc string
StatusText string
HelpInfo string
ErrorMessage string
}
func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData {
return ErrorPageData{
StatusCode: errInfo.StatusCode,
StatusDesc: errInfo.StatusDesc,
StatusText: errInfo.StatusText,
HelpInfo: errInfo.HelpInfo,
ErrorMessage: errInfo.ErrorMessage,
}
}
func ErrorPage(c *app.RequestContext, errInfo *GHProxyErrors) {
pageData, err := htmlTemplateRender(errPagesFs, ErrPageUnwarper(errInfo))
if err != nil {
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
logDebug("Error reading page.tmpl: %v", err)
return
}
c.Data(errInfo.StatusCode, "text/html; charset=utf-8", pageData)
return
}
func htmlTemplateRender(fsys fs.FS, data interface{}) ([]byte, error) {
tmplPath := "page.tmpl"
tmpl, err := template.ParseFS(fsys, tmplPath)
if err != nil {
return nil, fmt.Errorf("error parsing template: %w", err)
}
if tmpl == nil {
return nil, fmt.Errorf("template is nil")
}
// 创建一个 bytes.Buffer 用于存储渲染结果
var buf bytes.Buffer
err = tmpl.Execute(&buf, data)
if err != nil {
return nil, fmt.Errorf("error executing template: %w", err)
}
// 返回 buffer 的内容作为 []byte
return buf.Bytes(), nil
}

View File

@@ -2,96 +2,95 @@ package proxy
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"io"
"net/http" "net/http"
"strconv" "strconv"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) )
func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) { func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) {
method := c.Request.Method method := string(c.Request.Method())
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
// 发送HEAD请求, 预获取Content-Length bodyReader := bytes.NewBuffer(c.Request.Body())
headReq, err := client.NewRequest("HEAD", u, nil)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, headReq)
AuthPassThrough(c, cfg, headReq)
headResp, err := client.Do(headReq) //bodyReader := c.Request.BodyStream()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
// defer headResp.Body.Close() if cfg.GitClone.Mode == "cache" {
defer func(Body io.ReadCloser) { userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
if err := Body.Close(); err != nil { if err != nil {
logError("Failed to close response body: %v", err) HandleError(c, fmt.Sprintf("Failed to extract parts from URL: %v", err))
return
} }
}(headResp.Body) // 构建新url
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
}
contentLength := headResp.Header.Get("Content-Length") var (
sizelimit := cfg.Server.SizeLimit * 1024 * 1024 resp *http.Response
if contentLength != "" { )
size, err := strconv.Atoi(contentLength)
if err == nil && size > sizelimit { if cfg.GitClone.Mode == "cache" {
finalURL := headResp.Request.URL.String() rb := gitclient.NewRequestBuilder(method, u)
c.Redirect(http.StatusMovedPermanently, finalURL) rb.NoDefaultHeaders()
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size) rb.SetBody(bodyReader)
req, err := rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req, cfg, "clone")
AuthPassThrough(c, cfg, req)
resp, err = gitclient.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
} else {
rb := client.NewRequestBuilder(string(c.Request.Method()), u)
rb.NoDefaultHeaders()
rb.SetBody(bodyReader)
req, err := rb.Build()
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req, cfg, "clone")
AuthPassThrough(c, cfg, req)
resp, err = client.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return return
} }
} }
body, err := readRequestBody(c) contentLength := resp.Header.Get("Content-Length")
if err != nil {
HandleError(c, err.Error())
return
}
bodyReader := bytes.NewBuffer(body)
// 创建请求
req, err := client.NewRequest(method, u, bodyReader)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, req)
AuthPassThrough(c, cfg, req)
resp, err := client.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
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")
if contentLength != "" { if contentLength != "" {
size, err := strconv.Atoi(contentLength) size, err := strconv.Atoi(contentLength)
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if err != nil {
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
}
if err == nil && size > sizelimit { if err == nil && size > sizelimit {
finalURL := 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.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, 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
} }
} }
for key, values := range resp.Header { for key, values := range resp.Header {
for _, value := range values { for _, value := range values {
c.Header(key, value) //c.Header(key, value)
c.Response.Header.Add(key, value)
} }
} }
@@ -105,23 +104,23 @@ func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode s
resp.Header.Del(header) resp.Header.Del(header)
} }
if cfg.CORS.Enabled { switch cfg.Server.Cors {
case "*":
c.Header("Access-Control-Allow-Origin", "*") c.Header("Access-Control-Allow-Origin", "*")
} else { case "":
c.Header("Access-Control-Allow-Origin", "*")
case "nil":
c.Header("Access-Control-Allow-Origin", "") c.Header("Access-Control-Allow-Origin", "")
default:
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
} }
c.Status(resp.StatusCode) c.Status(resp.StatusCode)
if cfg.GitClone.Mode == "cache" {
// 使用固定32KB缓冲池 c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate")
buffer := BufferPool.Get().([]byte) c.Response.Header.Set("Pragma", "no-cache")
defer BufferPool.Put(buffer) c.Response.Header.Set("Expires", "0")
_, err = io.CopyBuffer(c.Writer, resp.Body, buffer)
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.Proto, err)
return
} else {
c.Writer.Flush() // 确保刷入
} }
c.SetBodyStream(resp.Body, -1)
} }

View File

@@ -1,130 +1,86 @@
package proxy package proxy
import ( import (
"context"
"fmt" "fmt"
"ghproxy/auth"
"ghproxy/config" "ghproxy/config"
"ghproxy/rate" "ghproxy/rate"
"net/http"
"regexp" "regexp"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) )
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter, runMode string) gin.HandlerFunc { var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
return func(c *gin.Context) {
// 限制访问频率 func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc {
if cfg.RateLimit.Enabled { return func(ctx context.Context, c *app.RequestContext) {
var allowed bool var shoudBreak bool
shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
switch cfg.RateLimit.RateMethod { if shoudBreak {
case "ip": return
allowed = iplimiter.Allow(c.ClientIP())
case "total":
allowed = limiter.Allow()
default:
logWarning("Invalid RateLimit Method")
return
}
if !allowed {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too Many Requests"})
logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Request.Method, c.Request.URL.RequestURI(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
return
}
} }
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/") // 去掉前缀/ var (
re := regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径 rawPath string
matches := re.FindStringSubmatch(rawPath) // 匹配路径 matches []string
)
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
matches = re.FindStringSubmatch(rawPath) // 匹配路径
// 匹配路径错误处理 // 匹配路径错误处理
if len(matches) < 3 { if len(matches) < 3 {
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto) logWarning("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
logWarning(errMsg) ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.Path())))
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
return return
} }
// 制作url // 制作url
rawPath = "https://" + matches[2] rawPath = "https://" + matches[2]
username, repo := MatchUserRepo(rawPath, cfg, c, matches) // 匹配用户名和仓库名 var (
user string
repo string
matcher string
)
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, username, repo) var matcherErr *GHProxyErrors
// dump log 记录详细信息 c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, full Header user, repo, matcher, matcherErr = Matcher(rawPath, cfg)
LogDump("%s %s %s %s %s %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, c.Request.Header) if matcherErr != nil {
repouser := fmt.Sprintf("%s/%s", username, repo) ErrorPage(c, matcherErr)
// 白名单检查
if cfg.Whitelist.Enabled {
whitelist := auth.CheckWhitelist(username, repo)
if !whitelist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
logWarning(logErrMsg)
return
}
}
// 黑名单检查
if cfg.Blacklist.Enabled {
blacklist := auth.CheckBlacklist(username, repo)
if blacklist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
logWarning(logErrMsg)
return
}
}
matches = CheckURL(rawPath, c)
if matches == nil {
c.AbortWithStatus(http.StatusNotFound)
logWarning("%s %s %s %s %s 404-NOMATCH", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
return return
} }
// 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth logDump("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
if exps[5].MatchString(rawPath) { logDump("%s", c.Request.Header.Header())
if cfg.Auth.AuthMethod != "header" || !cfg.Auth.Enabled {
c.JSON(http.StatusForbidden, gin.H{"error": "HeaderAuth is not enabled."}) shoudBreak = listCheck(cfg, c, user, repo, rawPath)
logError("%s %s %s %s %s HeaderAuth-Error: HeaderAuth is not enabled.", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto) if shoudBreak {
return return
} }
shoudBreak = authCheck(c, cfg, matcher, rawPath)
if shoudBreak {
return
} }
// 处理blob/raw路径 // 处理blob/raw路径
if exps[1].MatchString(rawPath) { if matcher == "blob" {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1) rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
} }
// 鉴权 logDebug("Matched: %v", matcher)
authcheck, err := auth.AuthHandler(c, cfg)
if !authcheck {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
}
// IP METHOD URL USERAGENT PROTO MATCHES switch matcher {
logDebug("%s %s %s %s %s Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, matches) case "releases", "blob", "raw", "gist", "api":
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
switch { case "clone":
case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath): GitReq(ctx, c, rawPath, cfg, "git")
//ProxyRequest(c, rawPath, cfg, "chrome", runMode)
ChunkedProxyRequest(c, rawPath, cfg, "chrome", runMode) // dev test chunk
case exps[2].MatchString(rawPath):
//ProxyRequest(c, rawPath, cfg, "git", runMode)
GitReq(c, rawPath, cfg, "git", runMode)
default: default:
c.String(http.StatusForbidden, "Invalid input.") ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
fmt.Println("Invalid input.") logError("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.Path(), rawPath, matcher)
return return
} }
} }

View File

@@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"net/http" "net/http"
"sync"
"time" "time"
httpc "github.com/satomitouka/touka-httpc" httpc "github.com/satomitouka/touka-httpc"
@@ -13,43 +12,31 @@ import (
var BufferSize int = 32 * 1024 // 32KB var BufferSize int = 32 * 1024 // 32KB
var ( var (
tr *http.Transport tr *http.Transport
BufferPool *sync.Pool gittr *http.Transport
client *httpc.Client client *httpc.Client
gitclient *httpc.Client
) )
func InitReq(cfg *config.Config) { func InitReq(cfg *config.Config) {
initHTTPClient(cfg) initHTTPClient(cfg)
if cfg.GitClone.Mode == "cache" {
// 初始化固定大小的缓存池 initGitHTTPClient(cfg)
BufferPool = &sync.Pool{
New: func() interface{} {
return make([]byte, BufferSize)
},
} }
} }
func initHTTPClient(cfg *config.Config) { func initHTTPClient(cfg *config.Config) {
/* var proTolcols = new(http.Protocols)
ctr = &http.Transport{ proTolcols.SetHTTP1(true)
MaxIdleConns: 100, proTolcols.SetHTTP2(true)
MaxConnsPerHost: 60, proTolcols.SetUnencryptedHTTP2(true)
IdleConnTimeout: 20 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
}
*/
if cfg.Httpc.Mode == "auto" { if cfg.Httpc.Mode == "auto" {
tr = &http.Transport{ tr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second, IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
} }
} else if cfg.Httpc.Mode == "advanced" { } else if cfg.Httpc.Mode == "advanced" {
tr = &http.Transport{ tr = &http.Transport{
@@ -58,6 +45,7 @@ func initHTTPClient(cfg *config.Config) {
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost, MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
WriteBufferSize: 32 * 1024, // 32KB WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
} }
} else { } else {
// 错误的模式 // 错误的模式
@@ -66,7 +54,6 @@ func initHTTPClient(cfg *config.Config) {
logWarning("use Auto to Run HTTP Client") logWarning("use Auto to Run HTTP Client")
fmt.Println("use Auto to Run HTTP Client") fmt.Println("use Auto to Run HTTP Client")
tr = &http.Transport{ tr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second, IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB ReadBufferSize: 32 * 1024, // 32KB
@@ -86,3 +73,72 @@ func initHTTPClient(cfg *config.Config) {
) )
} }
} }
func initGitHTTPClient(cfg *config.Config) {
if cfg.Httpc.Mode == "auto" {
gittr = &http.Transport{
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
} else if cfg.Httpc.Mode == "advanced" {
gittr = &http.Transport{
MaxIdleConns: cfg.Httpc.MaxIdleConns,
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
} else {
// 错误的模式
logError("unknown httpc mode: %s", cfg.Httpc.Mode)
fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
logWarning("use Auto to Run HTTP Client")
fmt.Println("use Auto to Run HTTP Client")
gittr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
}
if cfg.Outbound.Enabled {
initTransport(cfg, gittr)
}
if cfg.Server.Debug && cfg.GitClone.ForceH2C {
gitclient = httpc.New(
httpc.WithTransport(gittr),
httpc.WithDumpLog(),
httpc.WithProtocols(httpc.ProtocolsConfig{
ForceH2C: true,
}),
)
} else if !cfg.Server.Debug && cfg.GitClone.ForceH2C {
gitclient = httpc.New(
httpc.WithTransport(gittr),
httpc.WithProtocols(httpc.ProtocolsConfig{
ForceH2C: true,
}),
)
} else if cfg.Server.Debug && !cfg.GitClone.ForceH2C {
gitclient = httpc.New(
httpc.WithTransport(gittr),
httpc.WithDumpLog(),
httpc.WithProtocols(httpc.ProtocolsConfig{
Http1: true,
Http2: true,
Http2_Cleartext: true,
}),
)
} else {
gitclient = httpc.New(
httpc.WithTransport(gittr),
httpc.WithProtocols(httpc.ProtocolsConfig{
Http1: true,
Http2: true,
Http2_Cleartext: true,
}),
)
}
}

336
proxy/match.go Normal file
View File

@@ -0,0 +1,336 @@
package proxy
import (
"bufio"
"compress/gzip"
"fmt"
"ghproxy/config"
"io"
"net/url"
"regexp"
"strings"
)
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
var (
user string
repo string
matcher string
)
// 匹配 "https://github.com"开头的链接
if strings.HasPrefix(rawPath, "https://github.com") {
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
if strings.HasPrefix(remainingPath, "/") {
remainingPath = strings.TrimPrefix(remainingPath, "/")
}
// 预期格式/user/repo/more...
// 取出user和repo和最后部分
parts := strings.Split(remainingPath, "/")
if len(parts) <= 2 {
errMsg := "Not enough parts in path after matching 'https://github.com*'"
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
user = parts[0]
repo = parts[1]
// 匹配 "https://github.com"开头的链接
if len(parts) >= 3 {
switch parts[2] {
case "releases", "archive":
matcher = "releases"
case "blob":
matcher = "blob"
case "raw":
matcher = "raw"
case "info", "git-upload-pack":
matcher = "clone"
default:
errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher"
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
}
return user, repo, matcher, nil
}
// 匹配 "https://raw"开头的链接
if strings.HasPrefix(rawPath, "https://raw") {
remainingPath := strings.TrimPrefix(rawPath, "https://")
parts := strings.Split(remainingPath, "/")
if len(parts) <= 3 {
errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)."
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
user = parts[1]
repo = parts[2]
matcher = "raw"
return user, repo, matcher, nil
}
// 匹配 "https://gist"开头的链接
if strings.HasPrefix(rawPath, "https://gist") {
remainingPath := strings.TrimPrefix(rawPath, "https://")
parts := strings.Split(remainingPath, "/")
if len(parts) <= 3 {
errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)."
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
}
user = parts[1]
repo = ""
matcher = "gist"
return user, repo, matcher, nil
}
// 匹配 "https://api.github.com/"开头的链接
if strings.HasPrefix(rawPath, "https://api.github.com/") {
matcher = "api"
remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/")
parts := strings.Split(remainingPath, "/")
if parts[0] == "repos" {
user = parts[1]
repo = parts[2]
}
if parts[0] == "users" {
user = parts[1]
}
if !cfg.Auth.ForceAllowApi {
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
//return "", "", "", ErrAuthHeaderUnavailable
errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy"
return "", "", "", NewErrorWithStatusLookup(403, errMsg)
}
}
return user, repo, matcher, nil
}
//return "", "", "", ErrNotFound
errMsg := "Didn't match any matcher"
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
}
func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) {
var (
matcher string
)
// 匹配 "https://github.com"开头的链接
if strings.HasPrefix(rawPath, "https://github.com") {
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
if strings.HasPrefix(remainingPath, "/") {
remainingPath = strings.TrimPrefix(remainingPath, "/")
}
return true, "", nil
}
// 匹配 "https://raw.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
return true, matcher, nil
}
// 匹配 "https://raw.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://raw.github.com") {
return true, matcher, nil
}
// 匹配 "https://gist.githubusercontent.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
return true, matcher, nil
}
// 匹配 "https://gist.github.com"开头的链接
if strings.HasPrefix(rawPath, "https://gist.github.com") {
return true, matcher, nil
}
if cfg.Shell.RewriteAPI {
// 匹配 "https://api.github.com/"开头的链接
if strings.HasPrefix(rawPath, "https://api.github.com") {
matcher = "api"
return true, matcher, nil
}
}
return false, "", nil
}
// 匹配文件扩展名是sh的rawPath
func MatcherShell(rawPath string) bool {
return strings.HasSuffix(rawPath, ".sh")
}
// LinkProcessor 是一个函数类型,用于处理提取到的链接。
type LinkProcessor func(string) string
// 自定义 URL 修改函数
func modifyURL(url string, host string, cfg *config.Config) string {
// 去除url内的https://或http://
matched, _, err := EditorMatcher(url, cfg)
if err != nil {
logDump("Invalid URL: %s", url)
return url
}
if matched {
var u = url
u = strings.TrimPrefix(u, "https://")
u = strings.TrimPrefix(u, "http://")
logDump("Modified URL: %s", "https://"+host+"/"+u)
return "https://" + host + "/" + u
}
return url
}
var (
matchedMatchers = []string{
"blob",
"raw",
"gist",
}
)
// matchString 检查目标字符串是否在给定的字符串集合中
func matchString(target string, stringsToMatch []string) bool {
matchMap := make(map[string]struct{}, len(stringsToMatch))
for _, str := range stringsToMatch {
matchMap[str] = struct{}{}
}
_, exists := matchMap[target]
return exists
}
// extractParts 从给定的 URL 中提取所需的部分
func extractParts(rawURL string) (string, string, string, url.Values, error) {
// 解析 URL
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "", "", "", nil, err
}
// 获取路径部分并分割
pathParts := strings.Split(parsedURL.Path, "/")
// 提取所需的部分
if len(pathParts) < 3 {
return "", "", "", nil, fmt.Errorf("URL path is too short")
}
// 提取 /WJQSERVER-STUDIO 和 /go-utils.git
repoOwner := "/" + pathParts[1]
repoName := "/" + pathParts[2]
// 剩余部分
remainingPath := strings.Join(pathParts[3:], "/")
if remainingPath != "" {
remainingPath = "/" + remainingPath
}
// 查询参数
queryParams := parsedURL.Query()
return repoOwner, repoName, remainingPath, queryParams, nil
}
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)
// processLinks 处理链接,返回包含处理后数据的 io.Reader
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) {
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
readerOut = pipeReader
go func() { // 在 Goroutine 中执行写入操作
defer func() {
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
if err != nil {
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
}
} else {
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
logError("pipeWriter close failed: %v", closeErr)
if err == nil { // 如果之前没有错误,记录关闭错误
err = closeErr
}
}
}
}
}()
defer func() {
if err := input.Close(); err != nil {
logError("input close failed: %v", err)
}
}()
var bufReader *bufio.Reader
if compress == "gzip" {
// 解压gzip
gzipReader, gzipErr := gzip.NewReader(input)
if gzipErr != nil {
err = fmt.Errorf("gzip解压错误: %v", gzipErr)
return // Goroutine 中使用 return 返回错误
}
defer gzipReader.Close()
bufReader = bufio.NewReader(gzipReader)
} else {
bufReader = bufio.NewReader(input)
}
var bufWriter *bufio.Writer
var gzipWriter *gzip.Writer
// 根据是否gzip确定 writer 的创建
if compress == "gzip" {
gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter
bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小
} else {
bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter
}
//确保writer关闭
defer func() {
var closeErr error // 局部变量用于保存defer中可能发生的错误
if gzipWriter != nil {
if closeErr = gzipWriter.Close(); closeErr != nil {
logError("gzipWriter close failed %v", closeErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = closeErr
}
}
}
if flushErr := bufWriter.Flush(); flushErr != nil {
logError("writer flush failed %v", flushErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = flushErr
}
}
}()
// 使用正则表达式匹配 http 和 https 链接
for {
line, readErr := bufReader.ReadString('\n')
if readErr != nil {
if readErr == io.EOF {
break // 文件结束
}
err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误
return // Goroutine 中使用 return 返回错误
}
// 替换所有匹配的 URL
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
logDump("originalURL: %s", originalURL)
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
})
n, writeErr := bufWriter.WriteString(modifiedLine)
written += int64(n) // 更新写入的字节数
if writeErr != nil {
err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误
return // Goroutine 中使用 return 返回错误
}
}
// 在返回之前,再刷新一次 (虽然 defer 中已经有 flush但这里再加一次确保及时刷新)
if flushErr := bufWriter.Flush(); flushErr != nil {
if err == nil { // 避免覆盖之前的错误
err = flushErr
}
return // Goroutine 中使用 return 返回错误
}
}()
return readerOut, written, nil // 返回 reader 和 writtenerror 由 Goroutine 通过 pipeWriter.CloseWithError 传递
}

View File

@@ -1,34 +0,0 @@
package proxy
import (
"fmt"
"ghproxy/config"
"net/http"
"regexp"
"github.com/gin-gonic/gin"
)
// 预定义regex
var (
pathRegex = regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`) // 匹配路径
gistRegex = regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.*`) // 匹配gist路径
)
// 提取用户名和仓库名
func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches []string) (string, string) {
if gistMatches := gistRegex.FindStringSubmatch(rawPath); len(gistMatches) == 3 {
LogDump("%s %s %s %s %s Matched-Username: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, gistMatches[1])
return gistMatches[1], ""
}
// 定义路径
if pathMatches := pathRegex.FindStringSubmatch(matches[2]); len(pathMatches) >= 4 {
return pathMatches[2], pathMatches[3]
}
// 返回错误信息
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errMsg)
c.String(http.StatusForbidden, "Invalid path; expected username/repo, Path: %s", rawPath)
return "", ""
}

View File

@@ -1,95 +0,0 @@
package proxy
import (
"fmt"
"io"
"net/http"
"regexp"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
)
// 日志模块
var (
logw = logger.Logw
LogDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
var exps = []*regexp.Regexp{
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*`),
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*`),
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*`),
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+`),
regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/.+?/.+`),
regexp.MustCompile(`^(?:https?://)?api\.github\.com/repos/([^/]+)/([^/]+)/.*`),
}
// 读取请求体
func readRequestBody(c *gin.Context) ([]byte, error) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
logError("failed to read request body: %v", err)
return nil, fmt.Errorf("failed to read request body: %v", err)
}
defer c.Request.Body.Close()
return body, nil
}
/*
func SendRequest(c *gin.Context, req *req.Request, method, url string) (*req.Response, error) {
switch method {
case "GET":
return req.Get(url)
case "POST":
return req.Post(url)
case "PUT":
return req.Put(url)
case "DELETE":
return req.Delete(url)
default:
// IP METHOD URL USERAGENT PROTO UNSUPPORTED-METHOD
errmsg := fmt.Sprintf("%s %s %s %s %s Unsupported method", c.ClientIP(), method, url, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errmsg)
return nil, fmt.Errorf(errmsg)
}
}
*/
func HandleError(c *gin.Context, message string) {
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
logError(message)
}
func CheckURL(u string, c *gin.Context) []string {
for _, exp := range exps {
if matches := exp.FindStringSubmatch(u); matches != nil {
return matches[1:]
}
}
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logError(errMsg)
return nil
}
/*
// 处理响应大小
func HandleResponseSize(resp *http.Response, cfg *config.Config, c *gin.Context) error {
contentLength := resp.Header.Get("Content-Length")
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > sizelimit {
finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return fmt.Errorf("Path: %s size limit exceeded: %d", finalURL, size)
}
}
return nil
}
*/

View File

@@ -1,79 +0,0 @@
package proxy
/*
func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
method := c.Request.Method
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
client := createHTTPClient(mode)
if runMode == "dev" {
client.DevMode()
}
// 发送HEAD请求, 预获取Content-Length
headReq := client.R()
setRequestHeaders(c, headReq)
AuthPassThrough(c, cfg, headReq)
headResp, err := headReq.Head(u)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer headResp.Body.Close()
if err := HandleResponseSize(headResp, cfg, c); err != nil {
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
}
body, err := readRequestBody(c)
if err != nil {
HandleError(c, err.Error())
return
}
req := client.R().SetBody(body)
setRequestHeaders(c, req)
AuthPassThrough(c, cfg, req)
resp, err := SendRequest(c, req, method, u)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer resp.Body.Close()
if err := HandleResponseSize(resp, cfg, c); err != nil {
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
}
CopyResponseHeaders(resp, c, cfg)
c.Status(resp.StatusCode)
if err := copyResponseBody(c, resp.Body); err != nil {
logError("%s %s %s %s %s Response-Copy-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
}
}
// 复制响应体
func copyResponseBody(c *gin.Context, respBody io.Reader) error {
_, err := io.Copy(c.Writer, respBody)
return err
}
// 判断并选择TLS指纹
func createHTTPClient(mode string) *req.Client {
client := req.C()
switch mode {
case "chrome":
client.SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36").
SetTLSFingerprintChrome().
ImpersonateChrome()
case "git":
client.SetUserAgent("git/2.33.1")
}
return client
}
*/

View File

@@ -1,21 +1,75 @@
package proxy package proxy
import ( import (
"ghproxy/config"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/cloudwego/hertz/pkg/app"
) )
// 设置请求头 var (
func setRequestHeaders(c *gin.Context, req *http.Request) { respHeadersToRemove = map[string]struct{}{
for key, values := range c.Request.Header { "Content-Security-Policy": {},
for _, value := range values { "Referrer-Policy": {},
"Strict-Transport-Security": {},
"X-Github-Request-Id": {},
"X-Timer": {},
"X-Served-By": {},
"X-Fastly-Request-Id": {},
}
reqHeadersToRemove = map[string]struct{}{
"CF-IPCountry": {},
"CF-RAY": {},
"CF-Visitor": {},
"CF-Connecting-IP": {},
"CF-EW-Via": {},
"CDN-Loop": {},
"Upgrade": {},
"Connection": {},
}
cloneHeadersToRemove = map[string]struct{}{
"CF-IPCountry": {},
"CF-RAY": {},
"CF-Visitor": {},
"CF-Connecting-IP": {},
"CF-EW-Via": {},
"CDN-Loop": {},
}
)
// 预定义headers
var (
defaultHeaders = map[string]string{
"Accept": "*/*",
"Accept-Encoding": "gzip",
"Transfer-Encoding": "chunked",
"User-Agent": "GHProxy/1.0",
}
)
func setRequestHeaders(c *app.RequestContext, req *http.Request, cfg *config.Config, matcher string) {
if matcher == "raw" && cfg.Httpc.UseCustomRawHeaders {
// 使用预定义Header
for key, value := range defaultHeaders {
req.Header.Set(key, value) req.Header.Set(key, value)
} }
} else if matcher == "clone" {
c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key)
headerValue := string(value)
if _, shouldRemove := cloneHeadersToRemove[headerKey]; !shouldRemove {
req.Header.Set(headerKey, headerValue)
}
})
} else {
c.Request.Header.VisitAll(func(key, value []byte) {
headerKey := string(key)
headerValue := string(value)
if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove {
req.Header.Set(headerKey, headerValue)
}
})
} }
} }
func removeWSHeader(req *http.Request) {
req.Header.Del("Upgrade")
req.Header.Del("Connection")
}

72
proxy/routing.go Normal file
View File

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

90
proxy/utils.go Normal file
View File

@@ -0,0 +1,90 @@
package proxy
import (
"fmt"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/rate"
"github.com/cloudwego/hertz/pkg/app"
)
func listCheck(cfg *config.Config, c *app.RequestContext, user string, repo string, rawPath string) bool {
// 白名单检查
if cfg.Whitelist.Enabled {
var whitelist bool
whitelist = auth.CheckWhitelist(user, repo)
if !whitelist {
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo)))
logInfo("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
return true
}
}
// 黑名单检查
if cfg.Blacklist.Enabled {
var blacklist bool
blacklist = auth.CheckBlacklist(user, repo)
if blacklist {
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo)))
logInfo("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
return true
}
}
return false
}
// 鉴权
func authCheck(c *app.RequestContext, cfg *config.Config, matcher string, rawPath string) bool {
var err error
if matcher == "api" && !cfg.Auth.ForceAllowApi {
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed"))
logInfo("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Method(), rawPath)
return true
}
}
// 鉴权
if cfg.Auth.Enabled {
var authcheck bool
authcheck, err = auth.AuthHandler(c, cfg)
if !authcheck {
ErrorPage(c, NewErrorWithStatusLookup(401, fmt.Sprintf("Unauthorized: %v", err)))
logInfo("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err)
return true
}
}
return false
}
func rateCheck(cfg *config.Config, c *app.RequestContext, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) bool {
// 限制访问频率
if cfg.RateLimit.Enabled {
var allowed bool
switch cfg.RateLimit.RateMethod {
case "ip":
allowed = iplimiter.Allow(c.ClientIP())
case "total":
allowed = limiter.Allow()
default:
logWarning("Invalid RateLimit Method")
ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid RateLimit Method"))
return true
}
if !allowed {
ErrorPage(c, NewErrorWithStatusLookup(429, fmt.Sprintf("Too Many Requests; Rate Limit is %d per minute", cfg.RateLimit.RatePerMinute)))
logInfo("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
return true
}
}
return false
}

View File

@@ -1,65 +1,107 @@
package rate package rate
import ( import (
"sync"
"time" "time"
"github.com/WJQSERVER-STUDIO/go-utils/logger" "github.com/WJQSERVER-STUDIO/go-utils/logger"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
// 日志输出 // 日志模块
var ( var (
logw = logger.Logw logw = logger.Logw
LogDump = logger.LogDump logDump = logger.LogDump
logDebug = logger.LogDebug logDebug = logger.LogDebug
logInfo = logger.LogInfo logInfo = logger.LogInfo
logWarning = logger.LogWarning logWarning = logger.LogWarning
logError = logger.LogError logError = logger.LogError
) )
// 总体限流器 // RateLimiter 总体限流器
type RateLimiter struct { type RateLimiter struct {
limiter *rate.Limiter limiter *rate.Limiter
} }
// 基于IP的限流器 // New 创建一个总体限流器
type IPRateLimiter struct {
limiters map[string]*RateLimiter
limit int
burst int
duration time.Duration
}
func New(limit int, burst int, duration time.Duration) *RateLimiter { func New(limit int, burst int, duration time.Duration) *RateLimiter {
if limit <= 0 {
limit = 1
logWarning("rate limit per minute must be positive, setting to 1")
}
if burst <= 0 {
burst = 1
logWarning("rate limit burst must be positive, setting to 1")
}
rateLimit := rate.Limit(float64(limit) / duration.Seconds())
return &RateLimiter{ return &RateLimiter{
limiter: rate.NewLimiter(rate.Limit(float64(limit)/duration.Seconds()), burst), limiter: rate.NewLimiter(rateLimit, burst),
} }
} }
// Allow 检查是否允许请求通过
func (rl *RateLimiter) Allow() bool { func (rl *RateLimiter) Allow() bool {
return rl.limiter.Allow() return rl.limiter.Allow()
} }
func NewIPRateLimiter(limit int, burst int, duration time.Duration) *IPRateLimiter { // IPRateLimiter 基于IP的限流器
type IPRateLimiter struct {
limiters map[string]*RateLimiter // 用户级限流器 map
mu sync.RWMutex // 保护 limiters map
limit int // 每 duration 时间段内允许的请求数
burst int // 突发请求数
duration time.Duration // 限流周期
}
// NewIPRateLimiter 创建一个基于IP的限流器
func NewIPRateLimiter(ipLimit int, ipBurst int, duration time.Duration) *IPRateLimiter {
if ipLimit <= 0 {
ipLimit = 1
logWarning("IP rate limit per minute must be positive, setting to 1")
}
if ipBurst <= 0 {
ipBurst = 1
logWarning("IP rate limit burst must be positive, setting to 1")
}
logInfo("IP Rate Limiter initialized with limit: %d, burst: %d, duration: %v", ipLimit, ipBurst, duration)
return &IPRateLimiter{ return &IPRateLimiter{
limiters: make(map[string]*RateLimiter), limiters: make(map[string]*RateLimiter),
limit: limit, limit: ipLimit,
burst: burst, burst: ipBurst,
duration: duration, duration: duration,
} }
} }
// Allow 检查给定IP的请求是否允许通过
func (rl *IPRateLimiter) Allow(ip string) bool { func (rl *IPRateLimiter) Allow(ip string) bool {
if ip == "" { if ip == "" {
logWarning("empty ip") logWarning("empty ip for rate limiting")
return false return false
} }
limiter, ok := rl.limiters[ip] // 使用读锁快速查找
if !ok { rl.mu.RLock()
// 创建新的 RateLimiter 并存储 limiter, found := rl.limiters[ip]
limiter = New(rl.limit, rl.burst, rl.duration) rl.mu.RUnlock()
rl.limiters[ip] = limiter
if found {
return limiter.Allow()
} }
// 未找到,获取写锁来创建和添加
rl.mu.Lock()
// 双重检查
limiter, found = rl.limiters[ip]
if !found {
newL := New(rl.limit, rl.burst, rl.duration)
rl.limiters[ip] = newL
limiter = newL
}
rl.mu.Unlock()
return limiter.Allow() return limiter.Allow()
} }

View File

@@ -1,86 +0,0 @@
package timing
import (
"sync"
"time"
"github.com/gin-gonic/gin"
)
// 阶段计时结构(固定数组优化)
type timingData struct {
phases [8]struct { // 预分配8个阶段存储
name string
dur time.Duration
}
count int
start time.Time
}
// 对象池(内存重用优化)
var pool = sync.Pool{
New: func() interface{} {
return new(timingData)
},
}
// 中间件入口
func Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 从池中获取计时器
td := pool.Get().(*timingData)
td.start = time.Now()
td.count = 0
// 存储到上下文
c.Set("timing", td)
// 请求完成后回收对象
defer func() {
pool.Put(td)
}()
c.Next()
}
}
// 记录阶段耗时
func Record(c *gin.Context, name string) {
if val, exists := c.Get("timing"); exists {
//td := val.(*timingData)
td, ok := val.(*timingData)
if !ok {
return
}
if td.count < len(td.phases) {
td.phases[td.count].name = name
td.phases[td.count].dur = time.Since(td.start) // 直接记录当前时间
td.count++
}
}
}
// 获取计时结果(日志输出用)
func Get(c *gin.Context) (total time.Duration, phases []struct {
Name string
Dur time.Duration
}) {
if val, exists := c.Get("timing"); exists {
//td := val.(*timingData)
td, ok := val.(*timingData)
if !ok {
return
}
for i := 0; i < td.count; i++ {
phases = append(phases, struct {
Name string
Dur time.Duration
}{
Name: td.phases[i].name,
Dur: td.phases[i].dur,
})
}
total = time.Since(td.start)
}
return
}