Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c656aa41ca | ||
|
|
0b052f9c7f | ||
|
|
6fb7e1150e | ||
|
|
5e0f95dae3 | ||
|
|
c1c39a5a1f | ||
|
|
dd2f5b5a12 | ||
|
|
7e5b12dff8 | ||
|
|
26a42b6510 | ||
|
|
254c9a8bad | ||
|
|
060453f070 | ||
|
|
f110c96c1f | ||
|
|
73aac79c1b | ||
|
|
bed6c486dc | ||
|
|
ab77c5c7da | ||
|
|
bf21bd197a | ||
|
|
8af107c584 | ||
|
|
d6d54b222f | ||
|
|
005a4543d4 | ||
|
|
a85eb38de5 | ||
|
|
152fb8aa71 | ||
|
|
3e9e43cd44 | ||
|
|
8a50b311fc | ||
|
|
dcc50401c4 | ||
|
|
d62a1f9769 | ||
|
|
c522eba7ae | ||
|
|
3da54f0599 | ||
|
|
886c99f53d | ||
|
|
d3520a2133 | ||
|
|
1f0b43ec43 | ||
|
|
36646ebf7e | ||
|
|
d7ed4fc6ad | ||
|
|
7cbce12316 | ||
|
|
ff412f94ec | ||
|
|
b02aaeba8a | ||
|
|
395f641468 | ||
|
|
978ece6fa0 | ||
|
|
1adc3a3192 | ||
|
|
a66452cf10 | ||
|
|
d231fd839f | ||
|
|
4b37c6bb2b | ||
|
|
153b544024 | ||
|
|
635c22f9a7 | ||
|
|
f342312b40 | ||
|
|
acaf38b88d | ||
|
|
50cfd64db8 | ||
|
|
53e115242a | ||
|
|
cef0338d36 | ||
|
|
f8edb0e0bc | ||
|
|
c11f368a9c | ||
|
|
db38b2a402 | ||
|
|
accb52b952 | ||
|
|
70fb808acf | ||
|
|
b684227191 | ||
|
|
1498156f56 | ||
|
|
55158c0cb1 | ||
|
|
6c3280f850 | ||
|
|
866275aad3 | ||
|
|
f4cd7eecf1 | ||
|
|
5501cd3e3c | ||
|
|
f9f37262f0 | ||
|
|
026039e0bc | ||
|
|
8739027772 | ||
|
|
cafc713a65 | ||
|
|
8f2cc820aa | ||
|
|
139fc92abc | ||
|
|
e9d793c104 | ||
|
|
c931017f03 | ||
|
|
448e06d350 | ||
|
|
27cc30ab8b | ||
|
|
a65e44ac02 | ||
|
|
a0cfe826ea | ||
|
|
2e974ad7ae | ||
|
|
b7b9cd5db5 | ||
|
|
bcb73c18de | ||
|
|
ed839b828d | ||
|
|
801b8c6cda | ||
|
|
a92bbb7fb6 | ||
|
|
3e40146281 | ||
|
|
ac7e1e43b5 | ||
|
|
f134d22540 | ||
|
|
79153c0f7d | ||
|
|
4fd47812f7 | ||
|
|
17c49d534b | ||
|
|
284b38bab4 | ||
|
|
d73dfe7db5 | ||
|
|
dc286e002c | ||
|
|
5c54ae788c | ||
|
|
bfcb1c9901 | ||
|
|
9bfe8517cb | ||
|
|
50ba185aab | ||
|
|
6ee928b0c7 | ||
|
|
979f59545b | ||
|
|
da89b3f45e | ||
|
|
498266e08e | ||
|
|
e2faa497ab | ||
|
|
8def955151 | ||
|
|
a18660121a | ||
|
|
d26f6d1e1b |
2
.github/ISSUE_TEMPLATE/features_request.md
vendored
2
.github/ISSUE_TEMPLATE/features_request.md
vendored
@@ -2,7 +2,7 @@
|
|||||||
name: Features request
|
name: Features request
|
||||||
about: 提出新功能建议
|
about: 提出新功能建议
|
||||||
title: "[Features]"
|
title: "[Features]"
|
||||||
labels: enhancement
|
labels: 改进
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
2
.github/workflows/auto-assign.yml
vendored
2
.github/workflows/auto-assign.yml
vendored
@@ -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
|
||||||
5
.github/workflows/build-dev.yml
vendored
5
.github/workflows/build-dev.yml
vendored
@@ -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:
|
||||||
|
|||||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -2,4 +2,6 @@ demo
|
|||||||
demo.toml
|
demo.toml
|
||||||
*.log
|
*.log
|
||||||
*.bak
|
*.bak
|
||||||
list.json
|
list.json
|
||||||
|
repos
|
||||||
|
pages
|
||||||
298
CHANGELOG.md
298
CHANGELOG.md
@@ -1,5 +1,301 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
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
|
2.3.1
|
||||||
---
|
---
|
||||||
- CHANGE: 改进`Pages`在`External`模式下的路由
|
- CHANGE: 改进`Pages`在`External`模式下的路由
|
||||||
@@ -1023,4 +1319,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实现了缓存与速率限制
|
||||||
@@ -1 +1 @@
|
|||||||
25w15a
|
25w29b
|
||||||
179
README.md
179
README.md
@@ -1,60 +1,52 @@
|
|||||||
# GHProxy
|
# GHProxy
|
||||||
|
|
||||||

|
[](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
|
||||||

|
|
||||||
[](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
|
|
||||||
|
|
||||||
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
|
支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能
|
||||||
|
|
||||||
[DEMO](https://ghproxy.1888866.xyz)
|
|
||||||
|
|
||||||
[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 clone、raw、releases 等文件拉取**
|
||||||
- 支持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,105 +75,30 @@ wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghprox
|
|||||||
|
|
||||||
## 配置说明
|
## 配置说明
|
||||||
|
|
||||||
### 外部配置文件
|
参看[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md)
|
||||||
|
|
||||||
本项目采用`config.toml`作为外部配置,默认配置如下
|
|
||||||
使用Docker部署时,慎重修改`config.toml`,以免造成不必要的麻烦
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[server]
|
|
||||||
host = "0.0.0.0" # 监听地址
|
|
||||||
port = 8080 # 监听端口
|
|
||||||
sizeLimit = 125 # 125MB
|
|
||||||
H2C = true # 是否开启H2C传输
|
|
||||||
enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off (2.4.0弃用)
|
|
||||||
|
|
||||||
[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]
|
|
||||||
mode = "internal" # "internal" or "external" 内部/外部 前端 默认内部
|
|
||||||
enabled = false # 是否开启外置静态页面(Docker版本请关闭此项) (2.4.0弃用)
|
|
||||||
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-Frontend](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
|
||||||

|
|
||||||
|
## 项目简史
|
||||||
|
|
||||||
|
**本项目是[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
|
||||||
|
|
||||||
## 赞助
|
## 赞助
|
||||||
|
|
||||||
@@ -191,6 +108,10 @@ example.com {
|
|||||||
|
|
||||||
爱发电: https://afdian.com/a/wjqserver
|
爱发电: https://afdian.com/a/wjqserver
|
||||||
|
|
||||||
|
USDT(TRC20): `TNfSYG6F2vkiibd6J6mhhHNWDgWgNdF5hN`
|
||||||
|
|
||||||
### 捐赠列表
|
### 捐赠列表
|
||||||
|
|
||||||
虚位以待...
|
| 赞助人 |金额|
|
||||||
|
|--------|------|
|
||||||
|
| starry | 8 USDT (TRC20) |
|
||||||
|
|||||||
@@ -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) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。
|
||||||
|
|
||||||
## 报告漏洞
|
## 报告漏洞
|
||||||
|
|
||||||
|
|||||||
153
api/api.go
153
api/api.go
@@ -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",
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
18
auth/auth.go
18
auth/auth.go
@@ -1,15 +1,17 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"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 +36,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(ctx context.Context, c *app.RequestContext, cfg *config.Config) (isValid bool, err error) {
|
||||||
if cfg.Auth.AuthMethod == "parameters" {
|
if cfg.Auth.Method == "parameters" {
|
||||||
isValid, err = AuthParametersHandler(c, cfg)
|
isValid, err = AuthParametersHandler(c, cfg)
|
||||||
return isValid, err
|
return isValid, err
|
||||||
} else if cfg.Auth.AuthMethod == "header" {
|
} else if cfg.Auth.Method == "header" {
|
||||||
isValid, err = AuthHeaderHandler(c, cfg)
|
isValid, err = AuthHeaderHandler(c, cfg)
|
||||||
return isValid, err
|
return isValid, err
|
||||||
} else if cfg.Auth.AuthMethod == "" {
|
} else if cfg.Auth.Method == "" {
|
||||||
logError("Auth method not set")
|
logError("Auth method not set")
|
||||||
return true, ""
|
return true, nil
|
||||||
} else {
|
} else {
|
||||||
logError("Auth method not supported")
|
logError("Auth method not supported")
|
||||||
return false, "Auth method not supported"
|
return false, fmt.Errorf(fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
171
config/config.go
171
config/config.go
@@ -1,15 +1,18 @@
|
|||||||
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
|
||||||
@@ -19,19 +22,24 @@ type Config struct {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
[server]
|
[server]
|
||||||
host = "0.0.0.0" # 监听地址
|
host = "0.0.0.0"
|
||||||
port = 8080 # 监听端口
|
port = 8080
|
||||||
sizeLimit = 125 # 125MB
|
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||||
H2C = true # 是否开启H2C传输
|
sizeLimit = 125 # MB
|
||||||
enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off (2.4.0弃用)
|
memLimit = 0 # MB
|
||||||
|
H2C = true
|
||||||
|
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||||
|
debug = false
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
Port int `toml:"port"`
|
Port int `toml:"port"`
|
||||||
Host string `toml:"host"`
|
Host string `toml:"host"`
|
||||||
|
NetLib string `toml:"netlib"`
|
||||||
SizeLimit int `toml:"sizeLimit"`
|
SizeLimit int `toml:"sizeLimit"`
|
||||||
|
MemLimit int64 `toml:"memLimit"`
|
||||||
H2C bool `toml:"H2C"`
|
H2C bool `toml:"H2C"`
|
||||||
EnableH2C string `toml:"enableH2C"`
|
Cors string `toml:"cors"`
|
||||||
Debug bool `toml:"debug"`
|
Debug bool `toml:"debug"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,35 +57,63 @@ type HttpcConfig struct {
|
|||||||
MaxConnsPerHost int `toml:"maxConnsPerHost"`
|
MaxConnsPerHost int `toml:"maxConnsPerHost"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
[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]
|
||||||
mode = "internal" # "internal" or "external"
|
mode = "internal" # "internal" or "external"
|
||||||
enabled = false
|
|
||||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||||
staticDir = "/data/www"
|
staticDir = "/data/www"
|
||||||
*/
|
*/
|
||||||
type PagesConfig struct {
|
type PagesConfig struct {
|
||||||
Mode string `toml:"mode"`
|
Mode string `toml:"mode"`
|
||||||
Enabled bool `toml:"enabled"`
|
|
||||||
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 {
|
||||||
@@ -109,9 +145,104 @@ type OutboundConfig struct {
|
|||||||
|
|
||||||
// LoadConfig 从 TOML 配置文件加载配置
|
// LoadConfig 从 TOML 配置文件加载配置
|
||||||
func LoadConfig(filePath string) (*Config, error) {
|
func LoadConfig(filePath string) (*Config, error) {
|
||||||
|
if !FileExists(filePath) {
|
||||||
|
// 楔入配置文件
|
||||||
|
err := DefaultConfig().WriteConfig(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return DefaultConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
if _, err := toml.DecodeFile(filePath, &config); err != nil {
|
if _, err := toml.DecodeFile(filePath, &config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写入配置文件
|
||||||
|
func (c *Config) WriteConfig(filePath string) error {
|
||||||
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := toml.NewEncoder(file)
|
||||||
|
return encoder.Encode(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测文件是否存在
|
||||||
|
func FileExists(filename string) bool {
|
||||||
|
_, err := os.Stat(filename)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置结构体
|
||||||
|
func DefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Server: ServerConfig{
|
||||||
|
Port: 8080,
|
||||||
|
Host: "0.0.0.0",
|
||||||
|
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.txt",
|
||||||
|
},
|
||||||
|
Whitelist: WhitelistConfig{
|
||||||
|
Enabled: false,
|
||||||
|
WhitelistFile: "/data/ghproxy/config/whitelist.txt",
|
||||||
|
},
|
||||||
|
RateLimit: RateLimitConfig{
|
||||||
|
Enabled: false,
|
||||||
|
RateMethod: "total",
|
||||||
|
RatePerMinute: 100,
|
||||||
|
Burst: 10,
|
||||||
|
},
|
||||||
|
Outbound: OutboundConfig{
|
||||||
|
Enabled: false,
|
||||||
|
Url: "socks5://127.0.0.1:1080",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +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
|
||||||
|
memLimit = 0 # MB
|
||||||
H2C = true
|
H2C = true
|
||||||
enableH2C = "on" # "on" or "off"
|
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||||
debug = false
|
debug = false
|
||||||
|
|
||||||
[httpc]
|
[httpc]
|
||||||
@@ -12,9 +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]
|
||||||
mode = "internal" # "internal" or "external"
|
mode = "internal" # "internal" or "external"
|
||||||
enabled = false
|
|
||||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||||
staticDir = "/data/www"
|
staticDir = "/data/www"
|
||||||
|
|
||||||
@@ -22,15 +32,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"
|
||||||
|
|||||||
@@ -1,9 +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
|
||||||
|
memLimit = 0 # MB
|
||||||
H2C = true
|
H2C = true
|
||||||
enableH2C = "on"
|
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||||
debug = false
|
debug = false
|
||||||
|
|
||||||
[httpc]
|
[httpc]
|
||||||
@@ -12,9 +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]
|
||||||
mode = "internal" # "internal" or "external"
|
mode = "internal" # "internal" or "external"
|
||||||
enabled = false
|
|
||||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||||
staticDir = "/usr/local/ghproxy/pages"
|
staticDir = "/usr/local/ghproxy/pages"
|
||||||
|
|
||||||
@@ -22,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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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
|
|
||||||
@@ -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"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
343
docs/config.md
Normal file
343
docs/config.md
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
[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"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置项详细说明
|
||||||
|
|
||||||
|
* **`[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` 表示不限制。
|
||||||
|
|
||||||
|
* **`[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 代理协议。
|
||||||
|
|
||||||
|
## `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
24
docs/flag.md
Normal 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
17
docs/menu.md
Normal 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
|
||||||
56
go.mod
56
go.mod
@@ -1,40 +1,38 @@
|
|||||||
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.4.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.0
|
||||||
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.9 // 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/goccy/go-json v0.10.5 // indirect
|
|
||||||
github.com/google/go-cmp v0.6.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/nyaruka/phonenumbers v1.6.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/tidwall/pretty v1.2.1 // 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.35.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
|
|
||||||
)
|
)
|
||||||
|
|||||||
178
go.sum
178
go.sum
@@ -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.4.0 h1:pbtKxDHM3jUWZYfCT5BJqToAm5VQUp8mT2t6XTiYdRg=
|
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
|
||||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.4.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.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ=
|
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YYtrg1ixVSB/JvZM=
|
||||||
github.com/bytedance/sonic v1.12.9/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.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/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.0 h1:r9ax45fFg+YLUs2X4bNXm5RAxWl00hYjFgNlv32vtHk=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/nyaruka/phonenumbers v1.6.0/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.0 h1:cnOONdyJHJImMY8L64bvYF+7Ow/5CPf2Yr3RQRRMZOU=
|
||||||
github.com/satomitouka/touka-httpc v0.2.0/go.mod h1:ULB/0Ze0Apm46YKl35Jmj1hW5YLVVeOGqCqn+ijqGPM=
|
github.com/satomitouka/touka-httpc v0.4.0/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
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.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
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
18
init.sh
@@ -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
|
|
||||||
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
515
main.go
515
main.go
@@ -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) {
|
||||||
@@ -107,227 +185,300 @@ func InitReq(cfg *config.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadEmbeddedPages 加载嵌入式页面资源
|
// loadEmbeddedPages 加载嵌入式页面资源
|
||||||
func loadEmbeddedPages(cfg *config.Config) (fs.FS, error) {
|
func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
|
||||||
var pages fs.FS
|
var pages fs.FS
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
switch cfg.Pages.Theme {
|
switch cfg.Pages.Theme {
|
||||||
case "bootstrap":
|
case "bootstrap":
|
||||||
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
|
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
|
||||||
case "nebula":
|
case "nebula":
|
||||||
pages, err = fs.Sub(NebulaPagesFS, "pages/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:
|
default:
|
||||||
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
|
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
|
||||||
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
|
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load embedded pages: %w", err)
|
return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err)
|
||||||
}
|
}
|
||||||
return pages, nil
|
|
||||||
|
var assets fs.FS
|
||||||
|
assets, err = fs.Sub(pagesFS, "pages/assets")
|
||||||
|
return pages, assets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupPages 设置页面路由
|
// setupPages 设置页面路由
|
||||||
func setupPages(cfg *config.Config, router *gin.Engine) {
|
func setupPages(cfg *config.Config, r *server.Hertz) {
|
||||||
switch cfg.Pages.Mode {
|
switch cfg.Pages.Mode {
|
||||||
case "internal":
|
case "internal":
|
||||||
// 加载嵌入式资源
|
err := setInternalRoute(cfg, r)
|
||||||
pages, err := loadEmbeddedPages(cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logError("Failed when processing internal pages: %s", err)
|
logError("Failed when processing internal pages: %s", err)
|
||||||
|
fmt.Println(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置嵌入式资源路由
|
|
||||||
router.GET("/", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
router.GET("/favicon.ico", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
router.GET("/script.js", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
router.GET("/style.css", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
|
|
||||||
case "external":
|
case "external":
|
||||||
// 设置外部资源路径
|
// 设置外部资源路径
|
||||||
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
|
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
|
||||||
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
|
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
|
||||||
javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir)
|
javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir)
|
||||||
stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir)
|
stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir)
|
||||||
|
bootstrapPath := fmt.Sprintf("%s/bootstrap.min.css", cfg.Pages.StaticDir)
|
||||||
|
bootstrapBundlePath := fmt.Sprintf("%s/bootstrap.bundle.min.js", cfg.Pages.StaticDir)
|
||||||
|
|
||||||
// 设置外部资源路由
|
// 设置外部资源路由
|
||||||
router.GET("/", func(c *gin.Context) {
|
r.StaticFile("/", indexPagePath)
|
||||||
c.File(indexPagePath)
|
r.StaticFile("/favicon.ico", faviconPath)
|
||||||
logInfo("IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto)
|
r.StaticFile("/script.js", javascriptsPath)
|
||||||
})
|
r.StaticFile("/style.css", stylesheetsPath)
|
||||||
router.StaticFile("/favicon.ico", faviconPath)
|
r.StaticFile("/bootstrap.min.css", bootstrapPath)
|
||||||
router.StaticFile("/script.js", javascriptsPath)
|
r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath)
|
||||||
router.StaticFile("/style.css", stylesheetsPath)
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// 处理无效的Pages Mode
|
// 处理无效的Pages Mode
|
||||||
logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode)
|
logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode)
|
||||||
|
|
||||||
// 加载嵌入式资源
|
err := setInternalRoute(cfg, r)
|
||||||
pages, err := loadEmbeddedPages(cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logError("Failed when processing pages: %s", err)
|
logError("Failed when processing internal pages: %s", err)
|
||||||
|
fmt.Println(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 设置嵌入式资源路由
|
|
||||||
router.GET("/", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
router.GET("/favicon.ico", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
router.GET("/script.js", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
router.GET("/style.css", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.H2C {
|
|
||||||
router.UseH2C = true
|
|
||||||
} else {
|
|
||||||
logWarning("cfg.Server.EnableH2C 将于2.4.0弃用,请使用cfg.Server.H2C")
|
|
||||||
if cfg.Server.EnableH2C == "on" {
|
|
||||||
router.UseH2C = true
|
|
||||||
} else if cfg.Server.EnableH2C == "" {
|
|
||||||
router.UseH2C = true
|
|
||||||
} else {
|
} else {
|
||||||
router.UseH2C = false
|
runMode = "release"
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Server.Debug {
|
||||||
|
version = "Dev" // 如果是Debug模式,版本设置为"Dev"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||||
|
if cfg.Server.NetLib == "std" || cfg.Server.NetLib == "standard" || cfg.Server.NetLib == "net" || cfg.Server.NetLib == "net/http" {
|
||||||
|
if cfg.Server.H2C {
|
||||||
|
r = server.New(
|
||||||
|
server.WithH2C(true),
|
||||||
|
server.WithHostPorts(addr),
|
||||||
|
server.WithTransport(standard.NewTransporter),
|
||||||
|
)
|
||||||
|
r.AddProtocol("h2", factory.NewServerFactory())
|
||||||
|
} else {
|
||||||
|
r = server.New(
|
||||||
|
server.WithHostPorts(addr),
|
||||||
|
server.WithTransport(standard.NewTransporter),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" {
|
||||||
|
if cfg.Server.H2C {
|
||||||
|
r = server.New(
|
||||||
|
server.WithH2C(true),
|
||||||
|
server.WithHostPorts(addr),
|
||||||
|
)
|
||||||
|
r.AddProtocol("h2", factory.NewServerFactory())
|
||||||
|
} else {
|
||||||
|
r = server.New(
|
||||||
|
server.WithHostPorts(addr),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logError("Invalid NetLib: %s", cfg.Server.NetLib)
|
||||||
|
fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加Recovery中间件
|
||||||
|
r.Use(recovery.Recovery())
|
||||||
|
// 添加log中间件
|
||||||
|
r.Use(loggin.Middleware())
|
||||||
|
|
||||||
|
setupApi(cfg, r, version)
|
||||||
|
|
||||||
|
setupPages(cfg, r)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// (2.4.0启用)
|
// 1. GitHub Releases/Archive - Use distinct path segments for type
|
||||||
if cfg.Server.H2C {
|
r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for releases
|
||||||
router.UseH2C = true
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
}
|
})
|
||||||
|
|
||||||
|
r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for archive
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. GitHub Blob/Raw - Use distinct path segments for type
|
||||||
|
r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for blob
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for raw
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for info
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. Raw GitHubusercontent - Keep as is (assuming it's distinct enough)
|
||||||
|
r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 5. Gist GitHubusercontent - Keep as is (assuming it's distinct enough)
|
||||||
|
r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 6. GitHub API Repos - Keep as is (assuming it's distinct enough)
|
||||||
|
r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
|
})
|
||||||
*/
|
*/
|
||||||
|
|
||||||
setupApi(cfg, router, version)
|
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||||
// setupPages(cfg, router) // 2.4.0启用
|
|
||||||
|
|
||||||
logInfo("Pages Mode: %s", cfg.Pages.Mode)
|
|
||||||
if cfg.Pages.Mode == "internal" {
|
|
||||||
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 {
|
|
||||||
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
|
|
||||||
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))))
|
|
||||||
router.GET("/script.js", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
router.GET("/style.css", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
} else if cfg.Pages.Mode == "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)
|
|
||||||
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)
|
|
||||||
router.StaticFile("/script.js", javascriptsPath)
|
|
||||||
router.StaticFile("/style.css", stylesheetsPath)
|
|
||||||
} else {
|
|
||||||
logWarning("缺少 cfg.Pages.Mode 配置, cfg.Pages.Enable 将于2.4.0弃用,请使用cfg.Pages.Mode")
|
|
||||||
if cfg.Pages.Enabled {
|
|
||||||
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)
|
|
||||||
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)
|
|
||||||
router.StaticFile("/script.js", javascriptsPath)
|
|
||||||
router.StaticFile("/style.css", stylesheetsPath)
|
|
||||||
} 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 {
|
|
||||||
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
|
|
||||||
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))))
|
|
||||||
router.GET("/script.js", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
router.GET("/style.css", gin.WrapH(http.FileServer(http.FS(pages))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
router.NoRoute(func(c *gin.Context) {
|
|
||||||
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(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")
|
||||||
}
|
}
|
||||||
|
|||||||
32
middleware/loggin/loggin.go
Normal file
32
middleware/loggin/loggin.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
17
middleware/nocache/nocache.go
Normal file
17
middleware/nocache/nocache.go
Normal 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 |
@@ -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 © 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>
|
|
||||||
@@ -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);
|
|
||||||
@@ -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 |
@@ -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>
|
|
||||||
@@ -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();
|
|
||||||
})();
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -4,22 +4,22 @@ 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
|
// 500 Internal Server Error
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Conflict Auth Method"})
|
c.JSON(http.StatusInternalServerError, map[string]string{"error": "Conflict Auth Method"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case "header":
|
case "header":
|
||||||
@@ -27,9 +27,9 @@ 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
|
// 500 Internal Server Error
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid Auth Method / Auth Method is not be set"})
|
c.JSON(http.StatusInternalServerError, map[string]string{"error": "Invalid Auth Method / Auth Method is not be set"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,126 +2,162 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"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) {
|
var (
|
||||||
method := c.Request.Method
|
respHeadersToRemove = map[string]struct{}{
|
||||||
|
|
||||||
// 发送HEAD请求, 预获取Content-Length
|
|
||||||
headReq, err := client.NewRequest("HEAD", u, nil)
|
|
||||||
if err != nil {
|
|
||||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setRequestHeaders(c, headReq)
|
|
||||||
removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
|
|
||||||
AuthPassThrough(c, cfg, headReq)
|
|
||||||
|
|
||||||
headResp, err := client.Do(headReq)
|
|
||||||
if err != nil {
|
|
||||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//defer 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)
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 错误处理(404)
|
|
||||||
if resp.StatusCode == 404 {
|
|
||||||
c.String(http.StatusNotFound, "File Not Found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
contentLength = resp.Header.Get("Content-Length")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, values := range resp.Header {
|
|
||||||
for _, value := range values {
|
|
||||||
c.Header(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
headersToRemove := map[string]struct{}{
|
|
||||||
"Content-Security-Policy": {},
|
"Content-Security-Policy": {},
|
||||||
"Referrer-Policy": {},
|
"Referrer-Policy": {},
|
||||||
"Strict-Transport-Security": {},
|
"Strict-Transport-Security": {},
|
||||||
|
"X-Github-Request-Id": {},
|
||||||
|
"X-Timer": {},
|
||||||
|
"X-Served-By": {},
|
||||||
|
"X-Fastly-Request-Id": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
for header := range headersToRemove {
|
reqHeadersToRemove = map[string]struct{}{
|
||||||
resp.Header.Del(header)
|
"CF-IPCountry": {},
|
||||||
|
"CF-RAY": {},
|
||||||
|
"CF-Visitor": {},
|
||||||
|
"CF-Connecting-IP": {},
|
||||||
|
"CF-EW-Via": {},
|
||||||
|
"CDN-Loop": {},
|
||||||
|
"Upgrade": {},
|
||||||
|
"Connection": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
method []byte
|
||||||
|
bodyReader *bytes.Buffer
|
||||||
|
req *http.Request
|
||||||
|
resp *http.Response
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
method = c.Request.Method()
|
||||||
|
bodyReader = bytes.NewBuffer(c.Request.Body())
|
||||||
|
|
||||||
|
req, err = client.NewRequest(string(method), u, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.CORS.Enabled {
|
setRequestHeaders(c, req)
|
||||||
|
//removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
|
||||||
|
AuthPassThrough(c, cfg, req)
|
||||||
|
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理(404)
|
||||||
|
if resp.StatusCode == 404 {
|
||||||
|
//c.String(http.StatusNotFound, "File Not Found")
|
||||||
|
c.Status(http.StatusNotFound)
|
||||||
|
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(http.StatusMovedPermanently, []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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
for header := range headersToRemove {
|
||||||
|
resp.Header.Del(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
for key := range resp.Header {
|
||||||
|
var values []string = resp.Header.Values(key)
|
||||||
|
for _, value := range values {
|
||||||
|
c.Header(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 复制响应头,排除需要移除的 header
|
||||||
|
for key, values := range resp.Header {
|
||||||
|
if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove {
|
||||||
|
for _, value := range values {
|
||||||
|
c.Header(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
// 使用固定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)
|
logInfo("Is Shell: %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)
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
c.Writer.Flush() // 确保刷入
|
if contentLength != "" {
|
||||||
|
c.SetBodyStream(resp.Body, bodySize)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.SetBodyStream(resp.Body, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
23
proxy/error.go
Normal file
23
proxy/error.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"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) {
|
||||||
|
c.JSON(http.StatusInternalServerError, map[string]string{"error": message})
|
||||||
|
logError(message)
|
||||||
|
}
|
||||||
150
proxy/gitreq.go
150
proxy/gitreq.go
@@ -2,89 +2,83 @@ 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
|
logDump("Url Before FMT:%s", u)
|
||||||
headReq, err := client.NewRequest("HEAD", u, nil)
|
if cfg.GitClone.Mode == "cache" {
|
||||||
if err != nil {
|
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
|
||||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
if err != nil {
|
||||||
return
|
HandleError(c, fmt.Sprintf("Failed to extract parts from URL: %v", err))
|
||||||
}
|
return
|
||||||
setRequestHeaders(c, headReq)
|
|
||||||
AuthPassThrough(c, cfg, headReq)
|
|
||||||
|
|
||||||
headResp, err := client.Do(headReq)
|
|
||||||
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)
|
// 构建新url
|
||||||
|
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
|
||||||
|
logDump("New Url After FMT:%s", u)
|
||||||
|
}
|
||||||
|
|
||||||
contentLength := headResp.Header.Get("Content-Length")
|
var (
|
||||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
resp *http.Response
|
||||||
if contentLength != "" {
|
//err error
|
||||||
size, err := strconv.Atoi(contentLength)
|
)
|
||||||
if err == nil && size > sizelimit {
|
|
||||||
finalURL := headResp.Request.URL.String()
|
body := c.Request.Body()
|
||||||
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)
|
bodyReader := bytes.NewBuffer(body)
|
||||||
|
// 创建请求
|
||||||
|
|
||||||
|
if cfg.GitClone.Mode == "cache" {
|
||||||
|
req, err := gitclient.NewRequest(method, u, bodyReader)
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setRequestHeaders(c, req)
|
||||||
|
//removeWSHeader(req)
|
||||||
|
AuthPassThrough(c, cfg, req)
|
||||||
|
|
||||||
|
resp, err = gitclient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,23 +99,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)
|
||||||
}
|
}
|
||||||
|
|||||||
122
proxy/handler.go
122
proxy/handler.go
@@ -1,6 +1,8 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"ghproxy/auth"
|
"ghproxy/auth"
|
||||||
"ghproxy/config"
|
"ghproxy/config"
|
||||||
@@ -9,11 +11,13 @@ import (
|
|||||||
"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 {
|
||||||
|
return func(ctx context.Context, c *app.RequestContext) {
|
||||||
|
|
||||||
// 限制访问频率
|
// 限制访问频率
|
||||||
if cfg.RateLimit.Enabled {
|
if cfg.RateLimit.Enabled {
|
||||||
@@ -31,19 +35,25 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too Many Requests"})
|
c.JSON(http.StatusTooManyRequests, map[string]string{"error": "Too Many Requests"})
|
||||||
logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Request.Method, c.Request.URL.RequestURI(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/") // 去掉前缀/
|
var (
|
||||||
re := regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
|
rawPath string
|
||||||
matches := re.FindStringSubmatch(rawPath) // 匹配路径
|
matches []string
|
||||||
|
errMsg string
|
||||||
|
)
|
||||||
|
|
||||||
|
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
|
||||||
|
matches = re.FindStringSubmatch(rawPath) // 匹配路径
|
||||||
|
logInfo("URL: %v", matches)
|
||||||
|
|
||||||
// 匹配路径错误处理
|
// 匹配路径错误处理
|
||||||
if len(matches) < 3 {
|
if len(matches) < 3 {
|
||||||
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
errMsg = fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||||
logWarning(errMsg)
|
logWarning(errMsg)
|
||||||
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
|
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
|
||||||
return
|
return
|
||||||
@@ -52,76 +62,84 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
|
|||||||
// 制作url
|
// 制作url
|
||||||
rawPath = "https://" + matches[2]
|
rawPath = "https://" + matches[2]
|
||||||
|
|
||||||
username, repo := MatchUserRepo(rawPath, cfg, c, matches) // 匹配用户名和仓库名
|
var (
|
||||||
|
user string
|
||||||
|
repo string
|
||||||
|
matcher string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
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)
|
user, repo, matcher, err = Matcher(rawPath, cfg)
|
||||||
// dump log 记录详细信息 c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, full Header
|
if err != nil {
|
||||||
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 errors.Is(err, ErrInvalidURL) {
|
||||||
repouser := fmt.Sprintf("%s/%s", username, repo)
|
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
|
||||||
|
logWarning(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrAuthHeaderUnavailable) {
|
||||||
|
c.String(http.StatusForbidden, "AuthHeader Unavailable")
|
||||||
|
logWarning(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
|
||||||
|
// dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header
|
||||||
|
logDump("%s %s %s %s %s %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header())
|
||||||
|
var repouser string
|
||||||
|
repouser = fmt.Sprintf("%s/%s", user, repo)
|
||||||
|
|
||||||
// 白名单检查
|
// 白名单检查
|
||||||
if cfg.Whitelist.Enabled {
|
if cfg.Whitelist.Enabled {
|
||||||
whitelist := auth.CheckWhitelist(username, repo)
|
var whitelist bool
|
||||||
|
whitelist = auth.CheckWhitelist(user, repo)
|
||||||
if !whitelist {
|
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)
|
||||||
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser)
|
c.JSON(http.StatusForbidden, map[string]string{"error": errMsg})
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
|
logWarning("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser)
|
||||||
logWarning(logErrMsg)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 黑名单检查
|
// 黑名单检查
|
||||||
if cfg.Blacklist.Enabled {
|
if cfg.Blacklist.Enabled {
|
||||||
blacklist := auth.CheckBlacklist(username, repo)
|
var blacklist bool
|
||||||
|
blacklist = auth.CheckBlacklist(user, repo)
|
||||||
if blacklist {
|
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)
|
||||||
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
|
c.JSON(http.StatusForbidden, map[string]string{"error": errMsg})
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
|
logWarning("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser)
|
||||||
logWarning(logErrMsg)
|
|
||||||
return
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth
|
// 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth
|
||||||
if exps[5].MatchString(rawPath) {
|
|
||||||
if cfg.Auth.AuthMethod != "header" || !cfg.Auth.Enabled {
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "HeaderAuth is not enabled."})
|
|
||||||
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)
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鉴权
|
// 鉴权
|
||||||
authcheck, err := auth.AuthHandler(c, cfg)
|
if cfg.Auth.Enabled {
|
||||||
if !authcheck {
|
var authcheck bool
|
||||||
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
|
authcheck, err = auth.AuthHandler(ctx, c, cfg)
|
||||||
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)
|
if !authcheck {
|
||||||
return
|
//c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
|
||||||
|
c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"})
|
||||||
|
logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IP METHOD URL USERAGENT PROTO MATCHES
|
// IP METHOD URL USERAGENT PROTO MATCHES
|
||||||
logDebug("%s %s %s %s %s Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, matches)
|
logDebug("%s %s %s %s %s Matched: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), matcher)
|
||||||
|
|
||||||
switch {
|
switch matcher {
|
||||||
case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath):
|
case "releases", "blob", "raw", "gist", "api":
|
||||||
//ProxyRequest(c, rawPath, cfg, "chrome", runMode)
|
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
|
||||||
ChunkedProxyRequest(c, rawPath, cfg, "chrome", runMode) // dev test chunk
|
case "clone":
|
||||||
case exps[2].MatchString(rawPath):
|
GitReq(ctx, c, rawPath, cfg, "git")
|
||||||
//ProxyRequest(c, rawPath, cfg, "git", runMode)
|
|
||||||
GitReq(c, rawPath, cfg, "git", runMode)
|
|
||||||
default:
|
default:
|
||||||
c.String(http.StatusForbidden, "Invalid input.")
|
c.String(http.StatusForbidden, "Invalid input.")
|
||||||
fmt.Println("Invalid input.")
|
fmt.Println("Invalid input.")
|
||||||
|
|||||||
108
proxy/httpc.go
108
proxy/httpc.go
@@ -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,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
361
proxy/match.go
Normal file
361
proxy/match.go
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"ghproxy/config"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 定义错误类型, error承载描述, 便于处理
|
||||||
|
type MatcherErrors struct {
|
||||||
|
Code int
|
||||||
|
Msg string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidURL = &MatcherErrors{
|
||||||
|
Code: 403,
|
||||||
|
Msg: "Invalid URL Format",
|
||||||
|
}
|
||||||
|
ErrAuthHeaderUnavailable = &MatcherErrors{
|
||||||
|
Code: 403,
|
||||||
|
Msg: "AuthHeader Unavailable",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e *MatcherErrors) Error() string {
|
||||||
|
if e.Err != nil {
|
||||||
|
return fmt.Sprintf("Code: %d, Msg: %s, Err: %s", e.Code, e.Msg, e.Err.Error())
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Code: %d, Msg: %s", e.Code, e.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MatcherErrors) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) {
|
||||||
|
var (
|
||||||
|
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 {
|
||||||
|
return "", "", "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
user = parts[0]
|
||||||
|
repo = parts[1]
|
||||||
|
// 匹配 "https://github.com"开头的链接
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
switch parts[2] {
|
||||||
|
case "releases", "archive":
|
||||||
|
matcher = "releases"
|
||||||
|
case "blob", "raw":
|
||||||
|
matcher = "blob"
|
||||||
|
case "info", "git-upload-pack":
|
||||||
|
matcher = "clone"
|
||||||
|
default:
|
||||||
|
return "", "", "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return "", "", "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return "", "", "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user, repo, matcher, nil
|
||||||
|
}
|
||||||
|
return "", "", "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "", ErrInvalidURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 匹配文件扩展名是sh的rawPath
|
||||||
|
func MatcherShell(rawPath string) bool {
|
||||||
|
/*
|
||||||
|
if strings.HasSuffix(rawPath, ".sh") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
*/
|
||||||
|
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 和 written,error 由 Goroutine 通过 pipeWriter.CloseWithError 传递
|
||||||
|
}
|
||||||
@@ -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 "", ""
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
@@ -3,19 +3,35 @@ package proxy
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/cloudwego/hertz/pkg/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
// 设置请求头
|
// 设置请求头
|
||||||
func setRequestHeaders(c *gin.Context, req *http.Request) {
|
func setRequestHeaders(c *app.RequestContext, req *http.Request) {
|
||||||
for key, values := range c.Request.Header {
|
c.Request.Header.VisitAll(func(key, value []byte) {
|
||||||
for _, value := range values {
|
req.Header.Set(string(key), string(value))
|
||||||
req.Header.Set(key, value)
|
})
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func setRequestHeaders(c *app.RequestContext, req *http.Request) {
|
||||||
|
c.Request.Header.VisitAll(func(key, value []byte) {
|
||||||
|
headerKey := string(key)
|
||||||
|
headerValue := string(value)
|
||||||
|
if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove {
|
||||||
|
req.Header.Set(headerKey, headerValue)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// removeWSHeader removes the "Upgrade" and "Connection" headers from the given
|
||||||
|
// Request, which are added by the client when it wants to upgrade the
|
||||||
|
// connection to a WebSocket connection.
|
||||||
func removeWSHeader(req *http.Request) {
|
func removeWSHeader(req *http.Request) {
|
||||||
req.Header.Del("Upgrade")
|
req.Header.Del("Upgrade")
|
||||||
req.Header.Del("Connection")
|
req.Header.Del("Connection")
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// 日志输出
|
// 日志输出
|
||||||
var (
|
var (
|
||||||
logw = logger.Logw
|
logw = logger.Logw
|
||||||
LogDump = logger.LogDump
|
logDump = logger.LogDump
|
||||||
logDebug = logger.LogDebug
|
logDebug = logger.LogDebug
|
||||||
logInfo = logger.LogInfo
|
logInfo = logger.LogInfo
|
||||||
logWarning = logger.LogWarning
|
logWarning = logger.LogWarning
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user