Compare commits

...

79 Commits
2.0.2 ... 2.6.2

Author SHA1 Message Date
WJQSERVER
d65fe05336 Merge pull request #79 from WJQSERVER-STUDIO/dev
2.6.2 - 2025-03-29
---
- BACKPORT: 反向移植前端资源加载改进
2025-03-29 11:08:46 +08:00
wjqserver
8adc9c87f9 2.6.2 2025-03-29 11:02:54 +08:00
wjqserver
7e5092179e optimize frontend load 2025-03-28 10:41:02 +08:00
WJQSERVER
f112e77818 Merge pull request #76 from WJQSERVER-STUDIO/dev
2.6.1
2025-03-28 08:20:29 +08:00
wjqserver
8e5cfd1fac 2.6.1 2025-03-27 12:17:35 +08:00
wjqserver
1cc98ace75 update&sync changelog 2025-03-27 12:01:27 +08:00
wjqserver
b538ff2bd6 25w24a 2025-03-27 11:48:23 +08:00
wjqserver
f383518999 update&sync changelog 2025-03-27 11:47:44 +08:00
wjqserver
174bc14b97 update&sync changelog 2025-03-27 11:46:36 +08:00
wjqserver
15dfab722c update & fix matcher 2025-03-27 11:24:29 +08:00
wjqserver
470d1d58fa fix matcher issue 2025-03-27 11:20:39 +08:00
WJQSERVER
b80d25784a Merge pull request #74 from satomitouka/dev-fix-editor
Fix Match modifyURL
2025-03-25 23:49:15 +08:00
里見 灯花
1efd9e26b2 Update match.go 2025-03-25 21:44:07 +08:00
wjqserver
4fa91b67cf update match 2025-03-25 20:22:23 +08:00
wjqserver
ae234e64a8 update deps 2025-03-25 08:20:17 +08:00
wjqserver
87b9f47abc add graceful shutdown 2025-03-25 08:20:08 +08:00
wjqserver
5685240b41 fix changelog 2025-03-22 21:26:48 +08:00
wjqserver
40f0e3ad06 fix flags wrong 2025-03-22 21:25:48 +08:00
wjqserver
ef783f33c2 [backport] some change form v3 2025-03-22 21:20:47 +08:00
wjqserver
c478409bf8 25w23a 2025-03-22 21:20:07 +08:00
wjqserver
a53e18cb0b update repo info 2025-03-22 20:57:20 +08:00
wjqserver
0e7abf3411 [backport] add smart-git api 2025-03-22 20:56:57 +08:00
wjqserver
b5db6bcccc [backport] better cli flags 2025-03-22 20:50:43 +08:00
wjqserver
c1ba935ca4 [backport] add multi theme support 2025-03-22 20:46:08 +08:00
wjqserver
3c247665fc update 2025-03-22 20:18:30 +08:00
WJQSERVER
3e40146281 Merge pull request #67 from WJQSERVER-STUDIO/dev
2.5.0
2025-03-17 14:01:33 +08:00
wjqserver
ac7e1e43b5 update changelog 2025-03-17 13:53:37 +08:00
wjqserver
f134d22540 2.5.0 2025-03-17 13:48:53 +08:00
wjqserver
79153c0f7d update readme.md 2025-03-17 13:45:36 +08:00
wjqserver
4fd47812f7 25w19a 2025-03-16 21:03:28 +08:00
wjqserver
17c49d534b update readme.md 2025-03-16 12:28:00 +08:00
WJQSERVER
284b38bab4 Merge pull request #66 from WJQSERVER-STUDIO/dev
v2.4.2
2025-03-14 21:56:18 +08:00
wjqserver
d73dfe7db5 2.4.2 2025-03-14 21:48:25 +08:00
wjqserver
dc286e002c 25w18a 2025-03-14 21:40:21 +08:00
WJQSERVER
5c54ae788c Merge pull request #65 from WJQSERVER-STUDIO/dev
Rewrite path matcher (v2.4.1)
2025-03-13 22:48:27 +08:00
wjqserver
bfcb1c9901 2.4.1 2025-03-13 22:41:13 +08:00
wjqserver
9bfe8517cb rewrite path matcher 2025-03-13 18:16:17 +08:00
WJQSERVER
50ba185aab Merge pull request #63 from WJQSERVER-STUDIO/dev
v2.4.0
2025-03-13 00:34:24 +08:00
wjqserver
6ee928b0c7 update readme.md 2025-03-12 23:36:50 +08:00
wjqserver
979f59545b 2.4.0 2025-03-12 23:33:17 +08:00
wjqserver
da89b3f45e 25w16d 2025-03-12 23:01:52 +08:00
wjqserver
498266e08e 25w16c 2025-03-11 18:07:17 +08:00
wjqserver
e2faa497ab update frontend 2025-03-11 10:20:43 +08:00
wjqserver
8def955151 25w16b 2025-03-11 08:40:19 +08:00
wjqserver
a18660121a 25w16a 2025-03-10 18:53:12 +08:00
wjqserver
d26f6d1e1b update deps 2025-03-09 12:23:37 +08:00
WJQSERVER
60a1f6073d Merge pull request #54 from WJQSERVER-STUDIO/dev 2025-02-28 20:06:27 +08:00
wjqserver
2cc5409dd0 2.3.1 2025-02-28 19:57:25 +08:00
wjqserver
ad9cffe9e2 25w15a 2025-02-26 16:04:08 +08:00
里見 灯花
9af5010b79 Merge pull request #52 from WJQSERVER-STUDIO/dev
2.3.0
2025-02-19 16:10:00 +08:00
wjqserver
19cd77afd2 update readme.md 2025-02-19 16:07:21 +08:00
wjqserver
91cd76e541 2.3.0 2025-02-19 13:42:11 +08:00
WJQSERVER
0faddce474 change License to WJQserver Studio License 2.0 2025-02-18 11:59:43 +08:00
WJQSERVER
f7ba0c28b4 remove docker frontend & change to internel 2025-02-18 10:38:36 +08:00
WJQSERVER
146dedea21 25w14a 2025-02-18 10:34:24 +08:00
WJQSERVER
8336896979 feat(pages): add page theme configuration and static files 2025-02-18 10:19:11 +08:00
WJQSERVER
785a74dfeb 25w14t-2 2025-02-16 19:48:53 +08:00
WJQSERVER
bd666e08d1 25w14t-1 2025-02-14 15:50:42 +08:00
WJQSERVER
459aa24f89 Merge pull request #49 from liangshengmoran/frontend-refactoring
前端页面大型变动
2025-02-14 09:31:30 +08:00
WJQSERVER
a1e8e3e373 add 404 HTTP Error Handling 2025-02-14 07:20:34 +08:00
WJQSERVER
40c9ca5f38 optimize blacklist 2025-02-14 07:19:17 +08:00
WJQSERVER
97ae0044e7 Merge pull request #50 from WJQSERVER-STUDIO/dev
2.2.0
2025-02-12 19:30:46 +08:00
WJQSERVER
55afe7676c fix drak mode 2025-02-12 19:26:10 +08:00
里見 灯花
b95582ae1a fix wrong transport 2025-02-12 19:14:13 +08:00
WJQSERVER
9aaa3e64d3 2.2.0 2025-02-12 18:45:13 +08:00
WJQSERVER
9e0f222125 25w13b 2025-02-12 18:26:58 +08:00
清韵's
0d6c1d7e35 动态适配示例链接协议 2025-02-12 09:04:26 +08:00
清韵's
5c14aeb48d Refactor the front-end page. 2025-02-11 12:06:43 +08:00
WJQSERVER
21d30dee53 update deploy bash 2025-02-10 23:25:16 +08:00
WJQSERVER
a061b8d369 fix deploy/config.toml 2025-02-10 19:10:58 +08:00
WJQSERVER
68346717a5 25w13a 2025-02-10 18:54:20 +08:00
WJQSERVER
2b7fbd2a0d update outbound 2025-02-10 18:53:16 +08:00
三千
4c5d288f03 Support using proxy dial-up connection to GitHub. (#46) 2025-02-10 00:45:37 +08:00
WJQSERVER
09163ed4df 2.1.0 (#47)
2.1.0
---
- RELEASE: v2.1.0正式版发布;
- CHANGE: 加入`FreeBSD`与`Darwin`系统支持
- CHANGE: 更新安全政策, v1和24w版本序列生命周期正式结束
- ADD: 加入`timing`中间件记录响应时间
- ADD: 加入`loggin`中间件包装日志输出
- CHANGE: 更新logger版本至v1.3.0
- CHANGE: 改进日志相关
- ADD: 加入日志等级配置项
2025-02-09 23:13:57 +08:00
WJQSERVER
f5c32915b9 2.0.7 (#45)
2.0.7
---
- RELEASE: v2.0.7正式版发布;
- CHANGE: 更新Go版本至1.23.6
- CHANGE: 更新Logger版本至v1.2.0
2025-02-05 19:17:36 +08:00
WJQSERVER
286fa0f311 2.0.6 (#42)
2.0.6
---
- RELEASE: v2.0.6正式版发布;祝各位新春快乐!
- CHANGE: 优化前端的连接转换逻辑
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
- 优化`HTTP Client`参数
- CHANGE: 为api路由组增加no-cache标头
2025-01-28 23:54:51 +08:00
WJQSERVER
5d08993cbc 2.0.5 (#40)
- RELEASE: v2.0.5正式版发布;
- CHANGE: 优化响应体分块复制实现
- ADD: 加入缓存池
- CHANGE: 改进缓存实现
- CHANGE: 部分杂项改进
2025-01-27 15:00:19 +08:00
WJQSERVER
6e787ced6e 2.0.4 (#39)
- RELEASE: v2.0.4正式版发布;
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
- CHANGE: 优化Matches
- REMOVE: 移除Caddyfile残留
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
2025-01-26 16:24:37 +08:00
WJQSERVER
460b7514a9 2.0.3 (#37)
* Add support for reusing the Go net/http Client.

* Enhance HTTP Client parameters.

* 25w07a

* update deps

* Optimize HTTP Client parameters.

* 25w07b

* 2.0.3

* Update README.md

---------

Co-authored-by: 里見 灯花 <172008506+satomitouka@users.noreply.github.com>
2025-01-24 19:15:52 +08:00
51 changed files with 2549 additions and 1520 deletions

View File

@@ -9,15 +9,44 @@ on:
- 'DEV-VERSION'
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 加载版本号
run: |
if [ -f DEV-VERSION ]; then
echo "VERSION=$(cat DEV-VERSION)" >> $GITHUB_ENV
else
echo "DEV-VERSION file not found!" && exit 1
fi
- name: 输出版本号
run: |
echo "Version: ${{ env.VERSION }}"
- name: 预先创建Pre-release
id: create_release
uses: ncipollo/release-action@v1
with:
name: ${{ env.VERSION }}
artifacts: ./DEV-VERSION
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true
prerelease: true
body: ${{ env.VERSION }}
env:
export PATH: $PATH:/usr/local/go/bin
build:
runs-on: ubuntu-latest
needs: prepare
strategy:
matrix:
goos: [linux]
goos: [linux, darwin, freebsd]
goarch: [amd64, arm64]
env:
OUTPUT_BINARY: ghproxy
GO_VERSION: 1.23.5
GO_VERSION: 1.24
steps:
- uses: actions/checkout@v4
@@ -30,6 +59,13 @@ jobs:
else
echo "DEV-VERSION file not found!" && exit 1
fi
- name: 拉取前端
run: |
sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages
sudo rm -rf pages/.git/
- name: 安装 Go
uses: actions/setup-go@v3
with:
@@ -48,10 +84,10 @@ jobs:
cp LICENSE ./ghproxyd/
tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd .
ls
- name: Upload to GitHub Artifacts
uses: actions/upload-artifact@v3
- name: 上传Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_BINARY }}
name: ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
path: |
./${{ env.OUTPUT_BINARY }}*
- name: 上传至Release
@@ -64,6 +100,7 @@ jobs:
tag: ${{ env.VERSION }}
allowUpdates: true
prerelease: true
body: ${{ env.VERSION }}
env:
export PATH: $PATH:/usr/local/go/bin

View File

@@ -9,15 +9,43 @@ on:
- 'VERSION'
jobs:
prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 加载版本号
run: |
if [ -f VERSION ]; then
echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
else
echo "VERSION file not found!" && exit 1
fi
- name: 输出版本号
run: |
echo "Version: ${{ env.VERSION }}"
- name: 预先创建release
id: create_release
uses: ncipollo/release-action@v1
with:
name: ${{ env.VERSION }}
artifacts: ./VERSION
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true
body: ${{ env.VERSION }}
env:
export PATH: $PATH:/usr/local/go/bin
build:
runs-on: ubuntu-latest
needs: prepare # 确保这个作业在 prepare 作业完成后运行
strategy:
matrix:
goos: [linux]
goos: [linux, darwin, freebsd]
goarch: [amd64, arm64]
env:
OUTPUT_BINARY: ghproxy
GO_VERSION: 1.23.5
GO_VERSION: 1.24
steps:
- uses: actions/checkout@v3
@@ -28,6 +56,12 @@ jobs:
else
echo "VERSION file not found!" && exit 1
fi
- name: 拉取前端
run: |
sudo git clone https://github.com/WJQSERVER-STUDIO/GHPrxoy-Frontend.git pages
sudo rm -rf pages/.git/
- name: 安装 Go
uses: actions/setup-go@v3
with:
@@ -45,12 +79,12 @@ jobs:
mv ./ghproxyd/${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/${{ env.OUTPUT_BINARY }}
cp LICENSE ./ghproxyd/
tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd .
- name: Upload to GitHub Artifacts
uses: actions/upload-artifact@v3
- name: 上传Artifact
uses: actions/upload-artifact@v4
with:
name: ${{ env.OUTPUT_BINARY }}
name: ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
path: |
./${{ env.OUTPUT_BINARY }}*
./${{ env.OUTPUT_BINARY }}*
- name: 上传至Release
id: create_release
uses: ncipollo/release-action@v1
@@ -60,6 +94,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true
body: ${{ env.VERSION }}
env:
export PATH: $PATH:/usr/local/go/bin

5
.gitignore vendored
View File

@@ -1,4 +1,7 @@
demo
demo.toml
*.log
*.bak
*.bak
list.json
repos
pages

View File

@@ -1,5 +1,401 @@
# 更新日志
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; 下一个起点; v3会与v2.4.0及以上版本保证兼容关系, 可平顺升级;
- CHANGE: 使用HertZ框架重构, 提升性能
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
- CHANGE: 加入`Mino`主题对接选项
- FIX: 修正部分日志输出问题
- CHANGE: 移除gin残留
- CHANGE: 移除无用传入参数, 调整代码结构
25w20b - 2025-03-19
---
- PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用; v3.0.0会与v2.4.0及以上保证兼容关系, 可平顺升级;
- CHANGE: 加入`Mino`主题对接选项
- FIX: 修正部分日志输出问题
- CHANGE: 移除gin残留
- CHANGE: 移除无用传入参数, 调整代码结构
25w20a - 2025-03-18
---
- PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用; v3.0.0会与v2.4.0及以上保证兼容关系, 可平顺升级;
- CHANGE: 使用HertZ重构
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
2.5.0 - 2025-03-17
---
- ADD: 加入脚本嵌套加速功能
- CHANGE: 改进Auth模块
25w19a - 2025-03-16
---
- PRE-RELEASE: 此版本是v2.5.0的预发布版本,请勿在生产环境中使用;
- ADD: 加入脚本嵌套加速功能
- CHANGE: 改进Auth模块
- CHANGE: 将handler模块化改进
2.4.2 - 2025-03-14
---
- CHANGE: 在GitClone Cache模式下, 相关请求会使用独立httpc client
- CHANGE: 为GitClone Cache的独立httpc client增加ForceH2C选项
- FIX: 修正GitClone Cache模式下的Url生成问题
25w18a - 2025-03-14
---
- PRE-RELEASE: 此版本是v2.4.2的预发布版本,请勿在生产环境中使用;
- CHANGE: 在GitClone Cache模式下, 相关请求会使用独立httpc client
- CHANGE: 为GitClone Cache的独立httpc client增加ForceH2C选项
- FIX: 修正GitClone Cache模式下的Url生成问题
2.4.1 - 2025-03-13
---
- CHANGE: 重构路由匹配
- CHANGE: 更新相关依赖以修复错误
25w17a - 2025-03-13
---
- PRE-RELEASE: 此版本是v2.4.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 重构路由匹配
- CHANGE: 更新相关依赖以修复错误
2.4.0 - 2025-03-12
---
- ADD: 支持通过[Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git)实现Git Clone缓存
- CHANGE: 使用更高性能的Buffer Pool 实现, 调用 github.com/WJQSERVER-STUDIO/go-utils/copyb
- CHANGE: 改进路由匹配
- CHANGE: 更新依赖
- CHANGE: 改进前端
25w16d - 2025-03-12
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用更高性能的Buffer Pool 实现
25w16c
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用更高性能的Buffer Pool 实现
- CHANGE: 改进路由匹配
25w16b
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 修改路由
- CHANGE: 改进前端
25w16a
---
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 变更CORS配置
- ADD: 使用GO-GIT实现git smart http服务端和客户端
- CHANGE: 更新依赖
2.3.1
---
- CHANGE: 改进`Pages``External`模式下的路由
- CHANGE: 使用`H2C` bool 代替 `enableH2C` string (2.4.0 弃用 `enableH2C`)
- CHANGE: 使用`Mode` string 代替`Pages`内的 `enable` bool (2.4.0 弃用 `enable`)
25w15a
---
- PRE-RELEASE: 此版本是v2.3.1的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进`Pages``External`模式下的路由
- CHANGE: 使用`H2C` bool 代替 `enableH2C` string (2.4.0 弃用 `enableH2C`)
- CHANGE: 使用`Mode` string 代替`Pages`内的 `enable` bool (2.4.0 弃用 `enable`)
2.3.0
---
- CHANGE: 使用`touka-httpc`封装`HTTP Client`, 更新到`v0.2.0`版本, 参看`touka-httpc`
- CHANGE: 重构前端页面, 见[#49](https://github.com/WJQSERVER-STUDIO/ghproxy/pull/49)
- CHANGE: 重构`blacklist`实现
- CHANGE: 优化404处理
- CHANGE: 重构`whitelist`实现
- CHANGE: 对`proxy`进行结构性调整
- CHANGE: `chunckedreq``gitreq`共用`BufferPool``HTTP Client`
- CHANGE: 新增`HTTP Client`配置块
- CHANGE: 加入内置主题配置, 支持通过配置切换主题
- CHANGE: 将许可证转为WJQserver Studio License 2.0
25w14b
---
- PRE-RELEASE: 此版本是v2.3.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 将许可证转为WJQserver Studio License 2.0
25w14a
---
- PRE-RELEASE: 此版本是v2.3.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 使用`touka-httpc`封装`HTTP Client`, 更新到`v0.2.0`版本, 参看`touka-httpc`
- CHANGE: 重构前端页面, 见[#49](https://github.com/WJQSERVER-STUDIO/ghproxy/pull/49)
- CHANGE: 重构`blacklist`实现
- CHANGE: 优化404处理
- CHANGE: 重构`whitelist`实现
- CHANGE: 对`proxy`进行结构性调整
- CHANGE: `chunckedreq``gitreq`共用`BufferPool``HTTP Client`
- CHANGE: 新增`HTTP Client`配置块
- CHANGE: 加入内置主题配置, 支持通过配置切换主题
25w14t-2
---
- PRE-RELEASE: 此版本是测试验证版本,请勿在生产环境中使用;
- CHANGE: 使用`touka-httpc`封装`HTTP Client`,更新到`v0.1.0`版本, 参看`touka-httpc`
- CHANGE: 重构`whitelist`实现
- CHANGE: 对`proxy`进行结构性调整
- CHANGE: `chunckedreq``gitreq`共用`BufferPool``HTTP Client`
- CHANGE: 新增`HTTP Client`配置块
25w14t-1
---
- PRE-RELEASE: 此版本是测试验证版本,请勿在生产环境中使用;
- CHANGE: 使用`touka-httpc`封装`HTTP Client`
- CHANGE: 重构前端页面, 见[#49](https://github.com/WJQSERVER-STUDIO/ghproxy/pull/49)
- CHANGE: 重构`blacklist`实现
- CHANGE: 优化404处理
2.2.0
---
- RELEASE: v2.2.0正式版发布;
- CHANGE: 更新Go版本至1.24.0
- ADD: 加入`Socks5``HTTP(S)`出站支持
- CHANGE: 配置新增`Outbound`配置块
25w13b
---
- PRE-RELEASE: 此版本是v2.2.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 更新Go版本至1.24.0
25w13a
---
- PRE-RELEASE: 此版本是v2.2.0的预发布版本,请勿在生产环境中使用;
- ADD: 加入`Socks5``HTTP(S)`出站支持
- CHANGE: 配置新增`Outbound`配置块
2.1.0
---
- RELEASE: v2.1.0正式版发布;
- CHANGE: 加入`FreeBSD``Darwin`系统支持
- CHANGE: 更新安全政策, v1和24w版本序列生命周期正式结束
- ADD: 加入`timing`中间件记录响应时间
- ADD: 加入`loggin`中间件包装日志输出
- CHANGE: 更新logger版本至v1.3.0
- CHANGE: 改进日志相关
- ADD: 加入日志等级配置项
25w12d
---
- PRE-RELEASE: 此版本是v2.1.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 处理类型断言相关问题
25w12c
---
- PRE-RELEASE: 此版本是v2.1.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 加入`FreeBSD``Darwin`系统支持
25w12b
---
- PRE-RELEASE: 此版本是v2.0.8/v2.1.0的预发布版本,请勿在生产环境中使用;
- ADD: 加入`timing`中间件记录响应时间
- ADD: 加入`loggin`中间件包装日志输出
- CHANGE: 更新安全政策, v1和24w版本序列生命周期正式结束
25w12a
---
- PRE-RELEASE: 此版本是v2.0.8/v2.1.0的预发布版本,请勿在生产环境中使用;
- CHANGE: 更新logger版本至v1.3.0
- CHANGE: 改进日志相关
- ADD: 加入日志等级配置项
2.0.7
---
- RELEASE: v2.0.7正式版发布;
- CHANGE: 更新Go版本至1.23.6
- CHANGE: 更新Logger版本至v1.2.0
25w11a
---
- PRE-RELEASE: 此版本是v2.0.7的预发布版本,请勿在生产环境中使用;
- CHANGE: 更新Go版本至1.23.6
- CHANGE: 更新Logger版本至v1.2.0
2.0.6
---
- RELEASE: v2.0.6正式版发布;祝各位新春快乐!
- CHANGE: 优化前端的连接转换逻辑
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
- 优化`HTTP Client`参数
- CHANGE: 为api路由组增加no-cache标头
25w10b
---
- PRE-RELEASE: 此版本是v2.0.6的预发布版本,请勿在生产环境中使用;祝各位新春快乐!
- CHANGE: 为api路由组增加no-cache标头
25w10a
---
- PRE-RELEASE: 此版本是v2.0.6的预发布版本,请勿在生产环境中使用;祝各位新春快乐!
- CHANGE: 改进前端的连接转换逻辑
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
- 优化`HTTP Client`参数
2.0.5
---
- RELEASE: v2.0.5正式版发布;
- CHANGE: 优化响应体分块复制实现
- ADD: 加入缓存池
- CHANGE: 改进缓存实现
- CHANGE: 部分杂项改进
25w09b
---
- PRE-RELEASE: 此版本是v2.0.5的预发布版本,请勿在生产环境中使用;
- REMOVE: 移除残留配置
25w09a
---
- PRE-RELEASE: 此版本是v2.0.5的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进缓存实现
- ADD: 加入缓存池
2.0.4
---
- RELEASE: v2.0.4正式版发布;
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
- CHANGE: 优化Matches
- REMOVE: 移除Caddyfile残留
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
25w08b
---
- PRE-RELEASE: 此版本是v2.0.4的预发布版本,请勿在生产环境中使用;
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
25w08a
---
- PRE-RELEASE: 此版本是v2.0.4的预发布版本,请勿在生产环境中使用;
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
- CHANGE: 优化Matches
- REMOVE: 移除Caddyfile残留
2.0.3
---
- RELEASE: v2.0.3正式版发布;
- CHANGE: 优化`HTTP Client`参数, 使其更符合本项目使用场景
25w07b
---
- PRE-RELEASE: 此版本是v2.0.3的预发布版本,请勿在生产环境中使用;
- CHANGE: 改进`HTTP Client`参数
25w07a
---
- PRE-RELEASE: 此版本是v2.0.3的预发布版本,请勿在生产环境中使用;
- CHANGE: 为`HTTP Client`增加复用, 对性能有所优化
- CHANGE: 优化`HTTP Client`参数, 使其更符合本项目使用场景
2.0.2
---
- RELEASE: v2.0.2正式版发布; 此版本是v2.0.1改进
@@ -822,4 +1218,4 @@ v0.1.0
- ADD: 实现符合[RFC 7234](https://httpwg.org/specs/rfc7234.html)的HTTP缓存机制
- ADD: 实现action编译
- ADD: 实现Docker部署
- INFO: 使用Caddy作为Web服务器通过Caddy实现了缓存与速率限制
- INFO: 使用Caddy作为Web服务器通过Caddy实现了缓存与速率限制

View File

@@ -1 +1 @@
25w06b
25w24a

214
LICENSE
View File

@@ -1,107 +1,197 @@
WJQserver Studio 开源许可证
版本 1.2
版本 v2.0
版权所有 © WJQserver Studio 2024
定义
许可:指在本许可证内定义的使用、复制、分发与修改的条款与要求。
授权方:指拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体。
您:指行使本许可授予的权限的个人或法律实体。
开源与自由软件
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
本项目不等同于自由软件,使用权限受到本许可证条款的限制。
强调版权所有,所有权利均由 WJQserver Studio 保留。
许可证条款
1. 使用权限
1.1 您被授予在私人环境中自由使用本软件的权限。
1.2 您可以在不修改关键声明的前提下进行商用
* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求
* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。
* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。
* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。
* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。
* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于:
* 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。
* 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。
* 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。
* 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。
* 内部运营用途 (非营利组织) 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。
开源与自由软件
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。
强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。
许可证条款
1. 使用权限
* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
* 1.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
* 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
* 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。
* 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。
您必须选择以下两种方式之一:
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
2. 复制与分发
2.1 您可以复制和分发本软件的原始版本,前提是必须保留所有版权声明和本许可证。
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.2.2 条(开源继承与互惠共享)的约束。
3. 修改权限
3.1 您可以在非商业用途下修改本软件,前提是继承本许可证并保留原版权声明。
3.2 禁止在修改后进行商业用途。
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
4. 专利引用
4.1 若项目被专利相关引用,必须保留来源声明。
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.2.2 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
4.2 若为商业场景,需按照商用处理
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可
4. 专利权
* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。
* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。
5. 免责声明
5.1 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
5.2 在任何情况下,授权方均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害负责,即使已被告知可能发生此类损害
* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性
5.3 用户需根据当地法律对待本项目,确保遵守所有适用法规
* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害
6. 许可证期限
6.1 本许可证自2024年开始生效有效期暂为无限。
* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。
6.2 项目所有方有权修改许可证相关条例而不另行通知。
6. 许可证期限与终止
条款修订
7.1 授权方保留随时修改本许可证条款的权利,以便更好地适应法律和技术的发展。
* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。
7.2 修订后的条款将在发布时生效,继续使用本软件即表示接受修订后的条款
* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止
其他
8.1 本许可证不影响您作为最终用户的法定权利。
* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。
8.2 若本许可证的某些条款被认定为不可执行,其余条款仍然有效。
7. 条款修订
* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。
* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。
8. 其他
* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。
* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。
* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。
WJQserver Studio Open Source License
Version 1.2
Version v2.0
Copyright © WJQserver Studio 2024
Definitions
License: The terms and conditions defined within this license for use, copying, distribution, and modification.
Licensor: The individual or organization holding the copyright, or the entity designated by them.
You: The individual or legal entity exercising the permissions granted by this license.
Open Source vs. Free Software
This project is open source, allowing users to access and use the source code under the terms of this license.
This project is not equivalent to free software; usage rights are restricted by this license.
Copyright is emphasized, with all rights reserved by WJQserver Studio.
* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license.
* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license.
* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License.
* You: Refers to the individual or legal entity exercising permissions granted by this License.
* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof.
* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to:
* Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes.
* Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange.
* Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development.
* Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit.
* Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities.
Open Source and Free Software
This project is open-source software, allowing users to access and use the source code under the premise of complying with this License.
This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation.
Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors.
License Terms
1. Usage Rights
1.1 You are granted the right to use this software freely in a private environment.
1.2 You may use it commercially without modifying key statements.
1. Permissions for Use
2. Copying and Distribution
2.1 You may copy and distribute the original version of this software, provided all copyright notices and this license are retained.
* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section.
3. Modification Rights
3.1 You may modify this software for non-commercial purposes, provided you inherit this license and retain the original copyright notice.
* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms:
3.2 Modifications cannot be used commercially.
* 1.2.1 Maintain Statements: When conducting commercial use, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software.
* 1.2.2 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to:
4. Patent References
4.1 If the project is cited in patent-related contexts, the source statement must be retained.
* Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works.
* Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services.
* Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale.
* Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features.
4.2 For commercial scenarios, it must be treated as a commercial use.
You must choose one of the following two options:
5. Disclaimer
5.1 This software is provided "as is", without any express or implied warranties, including but not limited to merchantability, fitness for a particular purpose, and non-infringement.
* i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community.
* ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation.
5.2 In no event shall the licensor be liable for any direct, indirect, incidental, special, punitive, or consequential damages arising out of the use or inability to use this software, even if advised of the possibility of such damages.
2. Reproduction and Distribution
5.3 Users must comply with all applicable laws regarding this project.
* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met:
6. License Duration
6.1 This license is effective from 2024, with an indefinite duration.
* Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices.
* Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License.
6.2 The project owner reserves the right to modify the license terms without prior notice.
* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing).
Amendments
7.1 The licensor reserves the right to amend this license at any time to better adapt to legal and technological developments.
3. Modification Permissions
7.2 Revised terms become effective upon publication, and continued use of the software indicates acceptance of the revised terms.
* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use.
Miscellaneous
8.1 This license does not affect your statutory rights as an end user.
* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization.
8.2 If any provision of this license is held to be unenforceable, the remaining provisions shall remain in effect.
* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License.
4. Patent Rights
* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves.
* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom.
5. Disclaimer of Warranty
* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement.
* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations.
6. License Term and Termination
* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance.
* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License.
* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License.
7. Revision of Terms
* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs.
* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License.
8. Other
* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws.
* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect.
* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version.

View File

@@ -1,6 +1,7 @@
# GHProxy
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)
[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
@@ -15,12 +16,17 @@
### 项目特点
- 基于Go语言实现,使用[Gin框架](https://github.com/gin-gonic/gin)
- 基于Go语言实现,支持多平台
- 使用[Gin](https://github.com/gin-gonic/gin)作为Web框架
- 使用[Touka-HTTPC](https://github.com/satomitouka/touka-httpc)作为HTTP客户端
- 支持Git clone,raw,realeases等文件拉取
- 支持多个前端主题
- 支持自定义黑名单/白名单
- 支持Git Clone缓存(配合组件)
- 支持Docker部署
- 支持速率限制
- 支持用户鉴权
- 支持自定义黑名单/白名单
- 支持shell脚本嵌套加速
- 基于[WJQSERVER-STUDIO/golang-temp](https://github.com/WJQSERVER-STUDIO/golang-temp)模板构建,具有标准化的日志记录与构建流程
### 项目开发过程
@@ -28,13 +34,16 @@
**本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能**
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
- V2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
- V1.0.0 迁移至本仓库,并再次重构内容实现
- v2.4.1 对路径匹配进行优化
- v2.0.0 `proxy`核心模块进行了重构,大幅优化内存占用
- v1.0.0 迁移至本仓库,并再次重构内容实现
- v0.2.0 重构项目实现
### LICENSE
本项目使用WSL LICENSE Version1.2 (WJQSERVER STUDIO LICENSE Version1.2)
本项目使用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
@@ -43,9 +52,11 @@
```
# 下载文件
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/https://github.com/WJQSERVER-STUDIO/ghproxy.git
```
## 部署说明
@@ -70,6 +81,12 @@ docker run -p 7210:8080 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/
wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/install.sh && chmod +x install.sh &&./install.sh
```
Dev一键部署脚本:
```bash
wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/install-dev.sh && chmod +x install-dev.sh && ./install-dev.sh
```
## 配置说明
### 外部配置文件
@@ -82,24 +99,38 @@ wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/ma
host = "0.0.0.0" # 监听地址
port = 8080 # 监听端口
sizeLimit = 125 # 125MB
bufferSize = 4096 # Bytes 缓冲区大小
enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off
H2C = true # 是否开启H2C传输
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ; 除以上特殊情况, 会将值直接传入
[httpc]
mode = "auto" # "auto" or "advanced" HTTP客户端模式 自动/高级模式
maxIdleConns = 100 # only for advanced mode 仅用于高级模式
maxIdleConnsPerHost = 60 # only for advanced mode 仅用于高级模式
maxConnsPerHost = 0 # only for advanced mode 仅用于高级模式
[gitclone]
mode = "bypass" # bypass / cache 运行模式, cache模式依赖smart-git
smartGitAddr = "http://127.0.0.1:8080" # smart-git组件地址
ForceH2C = false # 强制使用H2C连接
[shell]
editor = false # 脚本嵌套加速
[pages]
enabled = false # 是否开启内置静态页面(Docker版本请关闭此项)
mode = "internal" # "internal" or "external" 内部/外部 前端 默认内部
theme = "bootstrap" # "bootstrap" or "nebula" 内置主题
staticPath = "/data/www" # 静态页面文件路径
[log]
logFilePath = "/data/ghproxy/log/ghproxy.log" # 日志文件路径
maxLogSize = 5 # MB 日志文件最大大小
[cors]
enabled = true # 是否开启跨域
level = "info" # 日志级别 dump, debug, info, warn, error, none
[auth]
authMethod = "parameters" # 鉴权方式,支持parameters,header
authToken = "token" # 用户鉴权Token
enabled = false # 是否开启用户鉴权
ForceAllowApi = false # 在不开启Header鉴权的情况下允许api代理
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json" # 黑名单文件路径
@@ -111,9 +142,13 @@ whitelistFile = "/data/ghproxy/config/whitelist.json" # 白名单文件路径
[rateLimit]
enabled = false # 是否开启速率限制
rateMrthod = "total" # "ip" or "total" 速率限制方式
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)出站传输
```
### 黑名单配置
@@ -126,6 +161,7 @@ burst = 5 # 突发请求数量
"test/test1",
"example/repo2",
"another/*"
"another"
]
}
```
@@ -140,22 +176,33 @@ burst = 5 # 突发请求数量
"test/test1",
"example/repo2",
"another/*"
"another"
]
}
```
### Caddy反代配置
```Caddyfile
example.com {
reverse_proxy {
to 127.0.0.1:7210
}
encode zstd gzip
}
```
### 前端页面
#### Bootstrap主题
![ghproxy-demo.png](https://webp.wjqserver.com/ghproxy/1.8.1-light.png)
![ghproxy-demo-dark.png](https://webp.wjqserver.com/ghproxy/1.8.1-dark.png)
#### Nebula主题
![nebula-dark-v2.3.0.png](https://webp.wjqserver.com/ghproxy/nebula-dark.png)
![nebula-light-v2.3.0.png](https://webp.wjqserver.com/ghproxy/nebula-light.png)
## 赞助
如果您觉得本项目对您有帮助,欢迎赞助支持,您的赞助将用于Demo服务器开支及开发者时间成本支出,感谢您的支持!
为爱发电,开源不易
爱发电: https://afdian.com/a/wjqserver
USDT(TRC20): `TNfSYG6F2vkiibd6J6mhhHNWDgWgNdF5hN`
### 捐赠列表
| 赞助人 |金额|
|--------|------|
| starry | 8 USDT (TRC20) |

View File

@@ -6,8 +6,11 @@
| 版本 | 是否支持 |
| --- | --- |
| v1.x.x | :white_check_mark: |
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
| v3.x.x | :white_check_mark: 接受issue, 实验性 |
| v2.x.x | :white_check_mark: 受支持, 正在维护 |
| v1.x.x | :x: 这些版本已结束生命周期,不再受支持 |
| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
| v0.x.x | :x: 这些版本不再受支持 |
### 用户须知

View File

@@ -1 +1 @@
2.0.2
2.6.2

View File

@@ -15,13 +15,25 @@ var (
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
)
func NoCacheMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 设置禁止缓存的响应头
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")
apiRouter := router.Group("api", NoCacheMiddleware())
{
apiRouter.GET("/size_limit", func(c *gin.Context) {
SizeLimitHandler(cfg, c)
@@ -47,6 +59,9 @@ func InitHandleRouter(cfg *config.Config, router *gin.Engine, version string) {
apiRouter.GET("/rate_limit/limit", func(c *gin.Context) {
RateLimitLimitHandler(c, cfg)
})
apiRouter.GET("/smartgit/status", func(c *gin.Context) {
SmartGitStatusHandler(c, cfg)
})
}
logInfo("API router Init success")
}
@@ -80,7 +95,7 @@ func CorsStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Cors": cfg.CORS.Enabled,
"Cors": cfg.Server.Cors,
})
}
@@ -115,3 +130,11 @@ func RateLimitLimitHandler(c *gin.Context, cfg *config.Config) {
"RatePerMinute": cfg.RateLimit.RatePerMinute,
})
}
func SmartGitStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"enabled": cfg.GitClone.Mode == "cache",
})
}

View File

@@ -7,24 +7,21 @@ import (
"github.com/gin-gonic/gin"
)
func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err error) {
if !cfg.Auth.Enabled {
return true, ""
return true, nil
}
// 获取"GH-Auth"的值
authToken := c.GetHeader("GH-Auth")
logInfo("%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)
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 authToken == "" {
err := "Auth Header == nil"
return false, err
return false, fmt.Errorf("Auth token not found")
}
isValid = authToken == cfg.Auth.AuthToken
if !isValid {
err := fmt.Sprintf("Auth token incorrect: %s", authToken)
return false, err
return false, fmt.Errorf("Auth token incorrect")
}
logInfo("auth SUCCESS: %t", isValid)
return isValid, ""
return isValid, nil
}

View File

@@ -7,25 +7,22 @@ import (
"github.com/gin-gonic/gin"
)
func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, err error) {
if !cfg.Auth.Enabled {
return true, ""
return true, nil
}
authToken := c.Query("auth_token")
logInfo("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto, authToken)
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 authToken == "" {
err := "Auth token == nil"
return false, err
return false, fmt.Errorf("Auth token not found")
}
isValid = authToken == cfg.Auth.AuthToken
if !isValid {
err := fmt.Sprintf("Auth token incorrect: %s", authToken)
return false, err
return false, fmt.Errorf("Auth token invalid")
}
logInfo("auth SUCCESS: %t", isValid)
return isValid, ""
return isValid, nil
}

View File

@@ -1,6 +1,7 @@
package auth
import (
"fmt"
"ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
@@ -9,6 +10,8 @@ import (
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
@@ -16,15 +19,23 @@ var (
func Init(cfg *config.Config) {
if cfg.Blacklist.Enabled {
LoadBlacklist(cfg)
err := InitBlacklist(cfg)
if err != nil {
logError(err.Error())
return
}
}
if cfg.Whitelist.Enabled {
LoadWhitelist(cfg)
err := InitWhitelist(cfg)
if err != nil {
logError(err.Error())
return
}
}
logInfo("Auth Init")
logDebug("Auth Init")
}
func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err error) {
if cfg.Auth.AuthMethod == "parameters" {
isValid, err = AuthParametersHandler(c, cfg)
return isValid, err
@@ -32,10 +43,10 @@ func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string)
isValid, err = AuthHeaderHandler(c, cfg)
return isValid, err
} else if cfg.Auth.AuthMethod == "" {
logWarning("Auth method not set")
return true, ""
logError("Auth method not set")
return true, nil
} else {
logWarning("Auth method not supported")
return false, "Auth method not supported"
logError("Auth method not supported")
return false, fmt.Errorf(fmt.Sprintf("Auth method %s not supported", cfg.Auth.AuthMethod))
}
}

View File

@@ -2,59 +2,89 @@ package auth
import (
"encoding/json"
"fmt"
"ghproxy/config"
"os"
"strings"
"sync"
)
type BlacklistConfig struct {
Blacklist []string `json:"blacklist"`
type Blacklist struct {
userSet map[string]struct{} // 用户级黑名单
repoSet map[string]map[string]struct{} // 仓库级黑名单
initOnce sync.Once // 确保初始化只执行一次
initialized bool // 初始化状态标识
}
var (
cfg *config.Config
blacklistfile = "/data/ghproxy/config/blacklist.json"
blacklist *BlacklistConfig
instance *Blacklist
initErr error
)
func LoadBlacklist(cfg *config.Config) {
blacklistfile = cfg.Blacklist.BlacklistFile
blacklist = &BlacklistConfig{}
// InitBlacklist 初始化黑名单(线程安全,仅执行一次)
func InitBlacklist(cfg *config.Config) error {
instance = &Blacklist{
userSet: make(map[string]struct{}),
repoSet: make(map[string]map[string]struct{}),
}
data, err := os.ReadFile(blacklistfile)
data, err := os.ReadFile(cfg.Blacklist.BlacklistFile)
if err != nil {
logError("Failed to read blacklist file: %v", err)
return fmt.Errorf("failed to read blacklist: %w", err)
}
err = json.Unmarshal(data, blacklist)
if err != nil {
logError("Failed to unmarshal blacklist JSON: %v", err)
var list struct {
Entries []string `json:"blacklist"`
}
}
func CheckBlacklist(repouser string, user string, repo string) bool {
return forRangeCheckBlacklist(blacklist.Blacklist, repouser, user)
}
func sliceRepoName_Blacklist(fullrepo string) (string, string) {
s := strings.Split(fullrepo, "/")
if len(s) != 2 {
return "", ""
if err := json.Unmarshal(data, &list); err != nil {
return fmt.Errorf("invalid blacklist format: %w", err)
}
return s[0], s[1]
}
func forRangeCheckBlacklist(blist []string, fullrepo string, user string) bool {
for _, blocked := range blist {
users, _ := sliceRepoName_Blacklist(blocked)
if user == users {
if strings.HasSuffix(blocked, "/*") {
return true
}
if fullrepo == blocked {
return true
for _, entry := range list.Entries {
user, repo := splitUserRepo(entry)
switch {
case repo == "" || repo == "*":
instance.userSet[user] = struct{}{}
default:
if _, exists := instance.repoSet[user]; !exists {
instance.repoSet[user] = make(map[string]struct{})
}
instance.repoSet[user][repo] = struct{}{}
}
}
instance.initialized = true
return nil
}
// CheckBlacklist 检查用户和仓库是否在黑名单中(无锁设计)
func CheckBlacklist(username, repo string) bool {
if instance == nil || !instance.initialized {
return false
}
// 先检查用户级黑名单
if _, exists := instance.userSet[username]; exists {
return true
}
// 再检查仓库级黑名单
if repos, userExists := instance.repoSet[username]; userExists {
// 允许仓库名为空时的全用户仓库匹配
if repo == "" {
return true
}
_, repoExists := repos[repo]
return repoExists
}
return false
}
// splitUserRepo 优化分割逻辑(仅初始化时使用)
func splitUserRepo(fullRepo string) (user, repo string) {
if idx := strings.Index(fullRepo, "/"); idx > 0 {
return fullRepo[:idx], fullRepo[idx+1:]
}
return fullRepo, ""
}

View File

@@ -2,58 +2,90 @@ package auth
import (
"encoding/json"
"fmt"
"ghproxy/config"
"os"
"strings"
"sync"
)
type WhitelistConfig struct {
Whitelist []string `json:"whitelist"`
// Whitelist 用于存储白名单信息
type Whitelist struct {
userSet map[string]struct{} // 用户级白名单
repoSet map[string]map[string]struct{} // 仓库级白名单
initOnce sync.Once // 确保初始化只执行一次
initialized bool // 初始化状态标识
}
var (
whitelistfile = "/data/ghproxy/config/whitelist.json"
whitelist *WhitelistConfig
whitelistInstance *Whitelist
whitelistInitErr error
)
func LoadWhitelist(cfg *config.Config) {
whitelistfile = cfg.Whitelist.WhitelistFile
whitelist = &WhitelistConfig{}
// InitWhitelist 初始化白名单(线程安全,仅执行一次)
func InitWhitelist(cfg *config.Config) error {
whitelistInstance = &Whitelist{
userSet: make(map[string]struct{}),
repoSet: make(map[string]map[string]struct{}),
}
data, err := os.ReadFile(whitelistfile)
data, err := os.ReadFile(cfg.Whitelist.WhitelistFile)
if err != nil {
logError("Failed to read whitelist file: %v", err)
return fmt.Errorf("failed to read whitelist: %w", err)
}
err = json.Unmarshal(data, whitelist)
if err != nil {
logError("Failed to unmarshal whitelist JSON: %v", err)
var list struct {
Entries []string `json:"whitelist"`
}
}
func CheckWhitelist(fullrepo string, user string, repo string) bool {
return forRangeCheckWhitelist(whitelist.Whitelist, fullrepo, user)
}
func sliceRepoName_Whitelist(fullrepo string) (string, string) {
s := strings.Split(fullrepo, "/")
if len(s) != 2 {
return "", ""
if err := json.Unmarshal(data, &list); err != nil {
return fmt.Errorf("invalid whitelist format: %w", err)
}
return s[0], s[1]
}
func forRangeCheckWhitelist(wlist []string, fullrepo string, user string) bool {
for _, passd := range wlist {
users, _ := sliceRepoName_Whitelist(passd)
if users == user {
if strings.HasSuffix(passd, "/*") {
return true
}
if fullrepo == passd {
return true
for _, entry := range list.Entries {
user, repo := splitUserRepoWhitelist(entry)
switch {
case repo == "" || repo == "*":
whitelistInstance.userSet[user] = struct{}{}
default:
if _, exists := whitelistInstance.repoSet[user]; !exists {
whitelistInstance.repoSet[user] = make(map[string]struct{})
}
whitelistInstance.repoSet[user][repo] = struct{}{}
}
}
whitelistInstance.initialized = true
return nil
}
// CheckWhitelist 检查用户和仓库是否在白名单中(无锁设计)
func CheckWhitelist(username, repo string) bool {
if whitelistInstance == nil || !whitelistInstance.initialized {
return false
}
// 先检查用户级白名单
if _, exists := whitelistInstance.userSet[username]; exists {
return true
}
// 再检查仓库级白名单
if repos, userExists := whitelistInstance.repoSet[username]; userExists {
// 允许仓库名为空时的全用户仓库匹配
if repo == "" {
return true
}
_, repoExists := repos[repo]
return repoExists
}
return false
}
// splitUserRepoWhitelist 分割用户和仓库信息(仅初始化时使用)
func splitUserRepoWhitelist(fullRepo string) (user, repo string) {
if idx := strings.Index(fullRepo, "/"); idx > 0 {
return fullRepo[:idx], fullRepo[idx+1:]
}
return fullRepo, ""
}

View File

@@ -1,102 +0,0 @@
{
debug
http_port 80
https_port 443
order cache before rewrite
cache {
cache_name GHProxyCache
}
log {
level INFO
output file /data/caddy/log/caddy.log {
roll_size 5MB
roll_keep 10
}
}
servers :80 {
protocols h1 h2c
}
}
(log) {
log {
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
time_format "02/Jan/2006:15:04:05 -0700"
}
output file /data/caddy/log/{args[0]}/access.log {
roll_size 5MB
roll_keep 10
roll_keep_for 24h
}
}
}
(error_page) {
handle_errors {
rewrite * /{err.status_code}.html
root * /data/caddy/pages/errors
file_server
}
}
(encode) {
encode {
zstd best
br 5 v2
gzip 5
minimum_length 512
}
}
(cache) {
cache {
allowed_http_verbs GET
stale {args[0]}
ttl {args[1]}
}
}
(header_realip) {
header_up X-Real-IP {remote_host}
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
}
(rate_limit) {
route /* {
rate_limit {remote.ip} {args[0]}r/m 10000 429
}
}
:80 {
reverse_proxy {
to 127.0.0.1:8080
import header_realip
transport http {
versions 1.1 h2c
}
}
import log ghproxy
import cache 0s 300s
import error_page
import encode
import rate_limit 60
route / {
root /data/www
file_server
import cache 0s 24h
}
route /favicon.ico {
root /data/www
file_server
import cache 0s 24h
}
route /api* {
rate_limit {remote.ip} 15r/m 10000 429
import cache 0s 6h
}
}
import /data/caddy/config.d/*

View File

@@ -1,99 +0,0 @@
{
debug
http_port 80
https_port 443
order cache before rewrite
cache {
cache_name GHProxyCache
}
log {
level INFO
output file /data/caddy/log/caddy.log {
roll_size 5MB
roll_keep 10
}
}
servers :80 {
protocols h1 h2c
}
}
(log) {
log {
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
time_format "02/Jan/2006:15:04:05 -0700"
}
output file /data/caddy/log/{args[0]}/access.log {
roll_size 5MB
roll_keep 10
roll_keep_for 24h
}
}
}
(error_page) {
handle_errors {
rewrite * /{err.status_code}.html
root * /data/caddy/pages/errors
file_server
}
}
(encode) {
encode {
zstd best
br 5 v2
gzip 5
minimum_length 256
}
}
(cache) {
cache {
allowed_http_verbs GET
stale {args[0]}
ttl {args[1]}
}
}
(header_realip) {
header_up X-Real-IP {remote_host}
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
}
(rate_limit) {
route /* {
rate_limit {remote.ip} {args[0]}r/m 10000 429
}
}
:80 {
reverse_proxy {
to h2c://127.0.0.1:8080
import header_realip
}
import log ghproxy
import error_page
import encode
import rate_limit 60
route / {
root /data/www
file_server
import cache 300s
}
route /favicon.ico {
root /data/www
file_server
import cache 300s
}
route /api* {
rate_limit {remote.ip} 15r/m 10000 429
import cache 300s
}
}
import /data/caddy/config.d/*

View File

@@ -1,103 +0,0 @@
{
debug
http_port 80
https_port 443
order cache before rewrite
cache {
cache_name GHProxyCache
}
log {
level INFO
output file /data/caddy/log/caddy.log {
roll_size 5MB
roll_keep 10
}
}
servers :80 {
protocols h1 h2c
}
}
(log) {
log {
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
time_format "02/Jan/2006:15:04:05 -0700"
}
output file /data/caddy/log/{args[0]}/access.log {
roll_size 5MB
roll_keep 10
roll_keep_for 24h
}
}
}
(error_page) {
handle_errors {
rewrite * /{err.status_code}.html
root * /data/caddy/pages/errors
file_server
}
}
(encode) {
encode {
zstd best
br 5 v2
gzip 5
minimum_length 512
}
}
(cache) {
cache {
allowed_http_verbs GET
stale {args[0]}
ttl {args[1]}
}
}
(header_realip) {
header_up X-Real-IP {remote_host}
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
}
(rate_limit) {
route /* {
rate_limit {remote.ip} {args[0]}r/m 10000 429
}
}
:80 {
reverse_proxy {
to 127.0.0.1:8080
import header_realip
transport http {
versions 1.1 h2c
}
}
import log ghproxy
import cache 0s 300s
import error_page
import encode
import rate_limit 60
route / {
root /data/www
file_server
import cache 0s 24h
}
route /favicon.ico {
root /data/www
file_server
import cache 0s 24h
}
route /api* {
rate_limit {remote.ip} 15r/m 10000 429
import cache 0s 6h
}
}
import /data/caddy/config.d/*

View File

@@ -1,7 +1,7 @@
{
"blacklist": [
"black/list",
"test/test1",
"example/*"
"eviluser",
"spamuser/bad-repo",
"malwareuser/*"
]
}

View File

@@ -6,43 +6,103 @@ import (
type Config struct {
Server ServerConfig
Httpc HttpcConfig
GitClone GitCloneConfig
Shell ShellConfig
Pages PagesConfig
Log LogConfig
CORS CORSConfig
Auth AuthConfig
Blacklist BlacklistConfig
Whitelist WhitelistConfig
RateLimit RateLimitConfig
Outbound OutboundConfig
}
/*
[server]
host = "0.0.0.0" # 监听地址
port = 8080 # 监听端口
sizeLimit = 125 # 125MB
H2C = true # 是否开启H2C传输
*/
type ServerConfig struct {
Port int `toml:"port"`
Host string `toml:"host"`
SizeLimit int `toml:"sizeLimit"`
EnableH2C string `toml:"enableH2C"`
BufferSize int `toml:"bufferSize"`
Debug bool `toml:"debug"`
Port int `toml:"port"`
Host string `toml:"host"`
SizeLimit int `toml:"sizeLimit"`
H2C bool `toml:"H2C"`
Cors string `toml:"cors"`
Debug bool `toml:"debug"`
}
/*
[httpc]
mode = "auto" # "auto" or "advanced"
maxIdleConns = 100 # only for advanced mode
maxIdleConnsPerHost = 60 # only for advanced mode
maxConnsPerHost = 0 # only for advanced mode
*/
type HttpcConfig struct {
Mode string `toml:"mode"`
MaxIdleConns int `toml:"maxIdleConns"`
MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"`
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]
mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www"
*/
type PagesConfig struct {
Enabled bool `toml:"enabled"`
Mode string `toml:"mode"`
Theme string `toml:"theme"`
StaticDir string `toml:"staticDir"`
}
type LogConfig struct {
LogFilePath string `toml:"logFilePath"`
MaxLogSize int `toml:"maxLogSize"`
Level string `toml:"level"`
}
type CORSConfig struct {
Enabled bool `toml:"enabled"`
}
/*
[auth]
authMethod = "parameters" # "header" or "parameters"
authToken = "token"
enabled = false
passThrough = false
ForceAllowApi = true
*/
type AuthConfig struct {
Enabled bool `toml:"enabled"`
AuthMethod string `toml:"authMethod"`
AuthToken string `toml:"authToken"`
PassThrough bool `toml:"passThrough"`
Enabled bool `toml:"enabled"`
AuthMethod string `toml:"authMethod"`
AuthToken string `toml:"authToken"`
PassThrough bool `toml:"passThrough"`
ForceAllowApi bool `toml:"ForceAllowApi"`
}
type BlacklistConfig struct {
@@ -62,6 +122,16 @@ type RateLimitConfig struct {
Burst int `toml:"burst"`
}
/*
[outbound]
enabled = false
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
*/
type OutboundConfig struct {
Enabled bool `toml:"enabled"`
Url string `toml:"url"`
}
// LoadConfig 从 TOML 配置文件加载配置
func LoadConfig(filePath string) (*Config, error) {
var config Config

View File

@@ -2,26 +2,41 @@
host = "0.0.0.0"
port = 8080
sizeLimit = 125 # MB
enableH2C = "on" # "on" or "off"
bufferSize = 4096 # Bytes
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]
enabled = false
mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/data/www"
[log]
logFilePath = "/data/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
[cors]
enabled = true
level = "info" # dump, debug, info, warn, error, none
[auth]
authMethod = "parameters" # "header" or "parameters"
authToken = "token"
enabled = false
passThrough = false
ForceAllowApi = false
[blacklist]
blacklistFile = "/data/ghproxy/config/blacklist.json"
@@ -36,3 +51,7 @@ 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"

View File

@@ -2,26 +2,40 @@
host = "127.0.0.1"
port = 8080
sizeLimit = 125 # MB
bufferSize = 4096 # Bytes
enableH2C = false
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
[pages]
enabled = false
mode = "internal" # "internal" or "external"
theme = "bootstrap" # "bootstrap" or "nebula"
staticDir = "/usr/local/ghproxy/pages"
[log]
logFilePath = "/usr/local/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
[cors]
enabled = true
level = "info" # dump, debug, info, warn, error, none
[auth]
authMethod = "parameters" # "header" or "parameters"
authToken = "token"
enabled = false
passThrough = false
ForceAllowApi = false
[blacklist]
blacklistFile = "/usr/local/ghproxy/config/blacklist.json"
@@ -36,3 +50,7 @@ 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"

View File

@@ -73,8 +73,51 @@ if [ -z "$ghproxy_dir" ]; then
ghproxy_dir="/usr/local/ghproxy"
fi
make_systemd_service() {
# 创建目录
mkdir -p ${ghproxy_dir}
mkdir -p ${ghproxy_dir}/config
mkdir -p ${ghproxy_dir}/log
mkdir -p ${ghproxy_dir}/pages
# 获取最新版本号
VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/DEV-VERSION)
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/DEV-VERSION
# 下载ghproxy
wget -q -O ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH.tar.gz
install tar
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
chmod +x ${ghproxy_dir}/ghproxy
# 下载pages
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/pages/bootstrap/index.html
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/pages/bootstrap/favicon.ico
# 下载配置文件
if [ -f ${ghproxy_dir}/config/config.toml ]; then
echo "配置文件已存在, 跳过下载"
echo "[WARNING] 请检查配置文件是否正确DEV版本升级时请注意配置文件兼容性"
sleep 2
else
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/config.toml
fi
# 替换 port = 8080
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
# 下载systemd服务文件
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/ghproxy.service
else
cat <<EOF > /etc/systemd/system/ghproxy.service
[Unit]
Description=Github Proxy Service
After=network.target
@@ -91,51 +134,6 @@ WantedBy=multi-user.target
EOF
}
# 创建目录
mkdir -p ${ghproxy_dir}
mkdir -p ${ghproxy_dir}/config
mkdir -p ${ghproxy_dir}/log
mkdir -p ${ghproxy_dir}/pages
# 获取最新版本号
VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/DEV-VERSION)
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/DEV-VERSION
# 下载ghproxy
wget -q -O ${ghproxy_dir}/ghproxy https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH.tar.gz
install tar
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
chmod +x ${ghproxy_dir}/ghproxy
# 下载pages
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/index.html
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/favicon.ico
# 下载配置文件
if [ -f ${ghproxy_dir}/config/config.toml ]; then
echo "配置文件已存在, 跳过下载"
echo "[WARNING] 请检查配置文件是否正确DEV版本升级时请注意配置文件兼容性"
sleep 2
else
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/config.toml
fi
# 替换 port = 8080
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
# 下载systemd服务文件
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
else
make_systemd_service()
fi
# 启动ghproxy

View File

@@ -73,26 +73,6 @@ if [ -z "$ghproxy_dir" ]; then
ghproxy_dir="/usr/local/ghproxy"
fi
make_systemd_service() {
cat <<EOF > /etc/systemd/system/ghproxy.service
[Unit]
Description=Github Proxy Service
After=network.target
[Service]
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
WorkingDirectory=$ghproxy_dir
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
EOF
}
# 创建目录
mkdir -p ${ghproxy_dir}
mkdir -p ${ghproxy_dir}/config
@@ -104,14 +84,14 @@ VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/mai
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/VERSION
# 下载ghproxy
wget -q -O ${ghproxy_dir}/ghproxy https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/${VERSION}/ghproxy-linux-${ARCH}.tar.gz
wget -q -O ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/${VERSION}/ghproxy-linux-${ARCH}.tar.gz
install tar
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
chmod +x ${ghproxy_dir}/ghproxy
# 下载pages
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/index.html
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/favicon.ico
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/bootstrap/index.html
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/bootstrap/favicon.ico
# 下载配置文件
@@ -135,7 +115,25 @@ sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelist
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
else
make_systemd_service()
cat <<EOF > /etc/systemd/system/ghproxy.service
[Unit]
Description=Github Proxy Service
After=network.target
[Service]
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
WorkingDirectory=$ghproxy_dir
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
EOF
fi
# 启动ghproxy

View File

@@ -15,10 +15,6 @@ RUN mkdir -p /data/${APPLICATION}/log
# 安装依赖
RUN apk add --no-cache curl wget tar
# 前端
RUN wget -O /data/www/index.html https://raw.githubusercontent.com/${USER}/${REPO}/dev/pages/index.html
RUN wget -O /data/www/favicon.ico https://raw.githubusercontent.com/${USER}/${REPO}/dev/pages/favicon.ico
# 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/dev/DEV-VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \

View File

@@ -14,12 +14,4 @@ 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 &
sleep 30
while [[ true ]]; do
# Failure Circuit Breaker
curl -f --max-time 5 -retry 3 http://127.0.0.1:8080/api/healthcheck || exit 1
sleep 120
done
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1

View File

@@ -15,10 +15,6 @@ RUN mkdir -p /data/${APPLICATION}/log
# 安装依赖
RUN apk add --no-cache curl wget tar
# 前端
RUN wget -O /data/www/index.html https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/index.html
RUN wget -O /data/www/favicon.ico https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/favicon.ico
# 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \

View File

@@ -14,12 +14,4 @@ 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 &
sleep 30
while [[ true ]]; do
# Failure Circuit Breaker
curl -f --max-time 5 -retry 3 http://127.0.0.1:8080/api/healthcheck || exit 1
sleep 120
done
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1

39
go.mod
View File

@@ -1,39 +1,46 @@
module ghproxy
go 1.23.5
go 1.24.1
require (
github.com/BurntSushi/toml v1.4.0
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.0
github.com/BurntSushi/toml v1.5.0
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0
github.com/gin-gonic/gin v1.10.0
golang.org/x/time v0.9.0
github.com/satomitouka/touka-httpc v0.3.3
golang.org/x/net v0.38.0
golang.org/x/time v0.11.0
)
require (
github.com/bytedance/sonic v1.12.7 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 // 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/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.24.0 // indirect
github.com/goccy/go-json v0.10.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.13.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/arch v0.15.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

83
go.sum
View File

@@ -1,15 +1,20 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.0 h1:OUrAOWb8xK0kxpWextJYUasmol+5KKqG2az52X2ae64=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.0/go.mod h1:sAqHVYSucoUnycyHMAZc1fMH5dS2bQwgwo8NUiAIcyk=
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YYtrg1ixVSB/JvZM=
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po=
github.com/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.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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=
@@ -25,19 +30,26 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
github.com/goccy/go-json v0.10.4/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/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
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/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -49,8 +61,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
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/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/satomitouka/touka-httpc v0.3.3 h1:Th0uJ5do3oqqZgdUDtqD1SH11x8TcJmrwHMJQlEIKCg=
github.com/satomitouka/touka-httpc v0.3.3/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA=
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.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -67,23 +85,26 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -14,12 +14,5 @@ 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 &
sleep 30
while [[ true ]]; do
curl -f http://localhost:8080/api/healthcheck || exit 1
sleep 120
done
/data/${APPLICATON}/${APPLICATON} > /data/${APPLICATON}/log/run.log 2>&1

34
loggin/loggin.go Normal file
View File

@@ -0,0 +1,34 @@
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)
}
}

314
main.go
View File

@@ -1,18 +1,23 @@
package main
import (
"context"
"embed"
"flag"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"ghproxy/api"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/middleware/loggin"
"ghproxy/middleware/timing"
"ghproxy/proxy"
"ghproxy/rate"
@@ -22,15 +27,17 @@ import (
)
var (
cfg *config.Config
router *gin.Engine
configfile = "/data/ghproxy/config/config.toml"
cfgfile string
version string
dev string
runMode string
limiter *rate.RateLimiter
iplimiter *rate.IPRateLimiter
cfg *config.Config
router *gin.Engine
configfile = "/data/ghproxy/config/config.toml"
cfgfile string
version string
dev string
runMode string
limiter *rate.RateLimiter
iplimiter *rate.IPRateLimiter
showVersion bool
showHelp bool
)
var (
@@ -40,6 +47,8 @@ var (
var (
logw = logger.Logw
logDump = logger.LogDump
logDebug = logger.LogDebug
logInfo = logger.LogInfo
logWarning = logger.LogWarning
logError = logger.LogError
@@ -47,26 +56,68 @@ var (
func readFlag() {
flag.StringVar(&cfgfile, "cfg", configfile, "config file path")
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() {
var err error
cfg, err = config.LoadConfig(cfgfile)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
fmt.Printf("Failed to load config: %v\n", err)
// 如果配置文件加载失败,也显示帮助信息并退出
flag.Usage()
os.Exit(1)
}
if cfg != nil && cfg.Server.Debug { // 确保 cfg 不为 nil
fmt.Println("Config File Path: ", cfgfile)
fmt.Printf("Loaded config: %v\n", cfg)
}
fmt.Println("Config File Path: ", cfgfile)
fmt.Printf("Loaded config: %v\n", cfg)
}
func setupLogger(cfg *config.Config) {
var err error
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
if err != nil {
log.Fatalf("Failed to initialize logger: %v", err)
fmt.Printf("Failed to initialize logger: %v\n", err)
}
logInfo("Config File Path: ", cfgfile)
logInfo("Loaded config: %v\n", cfg)
err = logger.SetLogLevel(cfg.Log.Level)
if err != nil {
fmt.Printf("Logger Level Error: %v\n", err)
}
fmt.Printf("Log Level: %s\n", cfg.Log.Level)
logDebug("Config File Path: ", cfgfile)
logDebug("Loaded config: %v\n", cfg)
logInfo("Init Completed")
}
@@ -87,20 +138,127 @@ func setupRateLimit(cfg *config.Config) {
} else {
logError("Invalid RateLimit Method: %s", cfg.RateLimit.RateMethod)
}
logInfo("Rate Limit Loaded")
}
}
func initBufferSize() {
proxy.InitChunkedBufferSize(cfg.Server.BufferSize)
func InitReq(cfg *config.Config) {
proxy.InitReq(cfg)
}
// loadEmbeddedPages 加载嵌入式页面资源
func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
var pages fs.FS
var err error
switch cfg.Pages.Theme {
case "bootstrap":
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
case "nebula":
pages, err = fs.Sub(pagesFS, "pages/nebula")
case "design":
pages, err = fs.Sub(pagesFS, "pages/design")
case "metro":
pages, err = fs.Sub(pagesFS, "pages/metro")
case "classic":
pages, err = fs.Sub(pagesFS, "pages/classic")
case "mino":
pages, err = fs.Sub(pagesFS, "pages/mino")
default:
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
}
if err != nil {
return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err)
}
var assets fs.FS
assets, err = fs.Sub(pagesFS, "pages/assets")
if err != nil {
return nil, nil, fmt.Errorf("failed to load embedded assets: %w", err)
}
return pages, assets, nil
}
// setupPages 设置页面路由
func setupPages(cfg *config.Config, router *gin.Engine) {
switch cfg.Pages.Mode {
case "internal":
// 加载嵌入式资源
pages, assets, err := loadEmbeddedPages(cfg)
if err != nil {
logError("Failed when processing internal pages: %s", err)
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))))
router.GET("/bootstrap.min.css", gin.WrapH(http.FileServer(http.FS(assets))))
router.GET("/bootstrap.bundle.min.js", gin.WrapH(http.FileServer(http.FS(assets))))
case "external":
// 设置外部资源路径
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir)
stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir)
bootstrapPath := fmt.Sprintf("%s/bootstrap.min.css", cfg.Pages.StaticDir)
bootstrapBundlePath := fmt.Sprintf("%s/bootstrap.bundle.min.js", cfg.Pages.StaticDir)
// 设置外部资源路由
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)
router.StaticFile("/bootstrap.min.css", bootstrapPath)
router.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath)
//router.StaticFile("/bootstrap.min.css", bootstrapPath)
default:
// 处理无效的Pages Mode
logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode)
// 加载嵌入式资源
pages, assets, err := loadEmbeddedPages(cfg)
if err != nil {
logError("Failed when processing pages: %s", err)
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))))
router.GET("/bootstrap.min.css", gin.WrapH(http.FileServer(http.FS(assets))))
router.GET("/bootstrap.bundle.min.js", gin.WrapH(http.FileServer(http.FS(assets))))
}
}
func init() {
readFlag()
flag.Parse()
// 如果设置了 -h则显示帮助信息并退出
if showHelp {
flag.Usage()
os.Exit(0)
}
// 如果设置了 -v则显示版本号并退出
if showVersion {
fmt.Printf("GHProxy Version: %s \n", version)
os.Exit(0)
}
loadConfig()
setupLogger(cfg)
initBufferSize()
InitReq(cfg)
loadlist(cfg)
setupRateLimit(cfg)
@@ -116,49 +274,113 @@ func init() {
runMode = "release"
}
logDebug("Run Mode: %s", runMode)
gin.LoggerWithWriter(io.Discard)
router = gin.New()
// 添加recovery中间件
router.Use(gin.Recovery())
//H2C默认值为true而后遵循cfg.Server.EnableH2C的设置
if cfg.Server.EnableH2C == "on" {
// 添加log中间件
router.Use(loggin.Middleware())
// 添加计时中间件
router.Use(timing.Middleware())
if cfg.Server.H2C {
router.UseH2C = true
} else if cfg.Server.EnableH2C == "" {
router.UseH2C = true
} else {
router.UseH2C = false
}
setupApi(cfg, router, version)
if cfg.Pages.Enabled {
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
router.GET("/", func(c *gin.Context) {
c.File(indexPagePath)
logInfo("IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto)
})
router.StaticFile("/favicon.ico", faviconPath)
} else if !cfg.Pages.Enabled {
pages, err := fs.Sub(pagesFS, "pages")
if err != nil {
log.Fatalf("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))))
}
setupPages(cfg, router)
// 1. GitHub Releases/Archive - Use distinct path segments for type
router.GET("/github.com/:username/:repo/releases/*filepath", func(c *gin.Context) { // Distinct path for releases
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.GET("/github.com/:username/:repo/archive/*filepath", func(c *gin.Context) { // Distinct path for archive
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 2. GitHub Blob/Raw - Use distinct path segments for type
router.GET("/github.com/:username/:repo/blob/*filepath", func(c *gin.Context) { // Distinct path for blob
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.GET("/github.com/:username/:repo/raw/*filepath", func(c *gin.Context) { // Distinct path for raw
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.GET("/github.com/:username/:repo/info/*filepath", func(c *gin.Context) { // Distinct path for info
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.GET("/github.com/:username/:repo/git-upload-pack", func(c *gin.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 4. Raw GitHubusercontent - Keep as is (assuming it's distinct enough)
router.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(c *gin.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 5. Gist GitHubusercontent - Keep as is (assuming it's distinct enough)
router.GET("/gist.githubusercontent.com/:username/*filepath", func(c *gin.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
// 6. GitHub API Repos - Keep as is (assuming it's distinct enough)
router.GET("/api.github.com/repos/:username/:repo/*filepath", func(c *gin.Context) {
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
router.NoRoute(func(c *gin.Context) {
logInfo(c.Request.URL.Path)
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
})
fmt.Printf("GHProxy Version: %s\n", version)
fmt.Printf("A Go Based High-Performance Github Proxy \n")
fmt.Printf("Made by WJQSERVER-STUDIO\n")
}
func main() {
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port))
if err != nil {
logError("Failed to start server: %v\n", err)
if showVersion || showHelp {
return
}
defer logger.Close()
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port),
Handler: router,
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
/*
go func() {
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port))
if err != nil {
logError("Failed to start server: %v\n", err)
}
}()
*/
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logError("Failed to start server: %v\n", err)
os.Exit(1)
}
}()
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if err := server.Shutdown(ctx); err != nil {
logError("Server forced to shutdown: %v\n", err)
}
defer cancel()
logger.Close()
fmt.Println("Program Exit")
}

View File

@@ -0,0 +1,34 @@
package loggin
import (
"ghproxy/middleware/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)
}
}

View File

@@ -0,0 +1,86 @@
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
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

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

View File

@@ -11,6 +11,7 @@ func AuthPassThrough(c *gin.Context, cfg *config.Config, req *http.Request) {
if cfg.Auth.PassThrough {
token := c.Query("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)
switch cfg.Auth.AuthMethod {
case "parameters":
if !cfg.Auth.Enabled {

View File

@@ -6,35 +6,24 @@ import (
"ghproxy/config"
"io"
"net/http"
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/copyb"
"github.com/gin-gonic/gin"
)
var chunkedBufferSize int
func InitChunkedBufferSize(cfgBufferSize int) {
if cfgBufferSize == 0 {
chunkedBufferSize = 4096 // 默认缓冲区大小
} else {
chunkedBufferSize = cfgBufferSize
}
}
func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, matcher string) {
method := c.Request.Method
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
// 创建HTTP客户端
client := &http.Client{}
// 发送HEAD请求, 预获取Content-Length
headReq, err := http.NewRequest("HEAD", u, nil)
headReq, err := client.NewRequest("HEAD", u, nil)
if err != nil {
HandleError(c, fmt.Sprintf("创建HEAD请求失败: %v", err))
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
setRequestHeaders(c, headReq)
removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
reWriteEncodeHeader(headReq)
AuthPassThrough(c, cfg, headReq)
headResp, err := client.Do(headReq)
@@ -42,11 +31,23 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
}
defer headResp.Body.Close()
//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)
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
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)
@@ -57,55 +58,93 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri
bodyReader := bytes.NewBuffer(body)
// 创建请求
req, err := http.NewRequest(method, u, bodyReader)
req, err := client.NewRequest(method, u, bodyReader)
if err != nil {
HandleError(c, fmt.Sprintf("创建请求失败: %v", err))
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
return
}
req.Header.Set("Transfer-Encoding", "chunked") // 确保设置分块传输编码
setRequestHeaders(c, req)
removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
reWriteEncodeHeader(req)
AuthPassThrough(c, cfg, req)
resp, err := client.Do(req)
if err != nil {
HandleError(c, fmt.Sprintf("发送请求失败: %v", err))
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)
// 错误处理(404)
if resp.StatusCode == 404 {
c.String(http.StatusNotFound, "File Not Found")
return
}
CopyResponseHeaders(resp, c, cfg)
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": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
}
for header := range headersToRemove {
resp.Header.Del(header)
}
switch cfg.Server.Cors {
case "*":
c.Header("Access-Control-Allow-Origin", "*")
case "":
c.Header("Access-Control-Allow-Origin", "*")
case "nil":
c.Header("Access-Control-Allow-Origin", "")
default:
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
}
c.Status(resp.StatusCode)
if err := chunkedCopyResponseBody(c, resp.Body); err != nil {
logError("%s %s %s %s %s 响应复制错误: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
}
}
// 复制响应体
func chunkedCopyResponseBody(c *gin.Context, respBody io.Reader) error {
buf := make([]byte, chunkedBufferSize)
for {
n, err := respBody.Read(buf)
if n > 0 {
if _, err := c.Writer.Write(buf[:n]); err != nil {
return err
}
c.Writer.Flush() // 确保每次写入后刷新
if MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor {
// 判断body是不是gzip
var compress string
if resp.Header.Get("Content-Encoding") == "gzip" {
compress = "gzip"
}
logInfo("Is Shell: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
c.Header("Content-Length", "")
_, err = processLinks(resp.Body, c.Writer, compress, c.Request.Host, cfg)
if err != nil {
if err == io.EOF {
break
}
return err
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
} else {
c.Writer.Flush() // 确保刷入
}
} else {
//_, err = io.CopyBuffer(c.Writer, resp.Body, nil)
_, err = copyb.Copy(c.Writer, resp.Body)
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() // 确保刷入
}
}
return nil
}

135
proxy/dial.go Normal file
View File

@@ -0,0 +1,135 @@
/*
made&PR by @lfhy
https://github.com/WJQSERVER-STUDIO/ghproxy/pull/46
*/
package proxy
import (
"ghproxy/config"
"net/http"
"net/url"
"strings"
"golang.org/x/net/proxy"
)
// initTransport 初始化 HTTP 传输层的代理设置
func initTransport(cfg *config.Config, transport *http.Transport) {
// 如果代理功能未启用,直接返回
if !cfg.Outbound.Enabled {
return
}
// 如果代理 URL 未设置,使用环境变量中的代理配置
if cfg.Outbound.Url == "" {
transport.Proxy = http.ProxyFromEnvironment
logWarning("Outbound proxy is not set, using environment variables")
return
}
// 尝试解析代理 URL
proxyInfo, err := url.Parse(cfg.Outbound.Url)
if err != nil {
// 如果解析失败,记录错误日志并使用环境变量中的代理配置
logError("Failed to parse outbound proxy URL %v", err)
transport.Proxy = http.ProxyFromEnvironment
return
}
// 根据代理 URL 的 scheme协议类型选择代理类型
switch strings.ToLower(proxyInfo.Scheme) {
case "http", "https": // 如果是 HTTP/HTTPS 代理
transport.Proxy = http.ProxyURL(proxyInfo) // 设置 HTTP(S) 代理
logInfo("Using HTTP(S) proxy: %s", proxyInfo.Redacted())
case "socks5": // 如果是 SOCKS5 代理
// 调用 newProxyDial 创建 SOCKS5 代理拨号器
proxyDialer := newProxyDial(cfg.Outbound.Url)
transport.Proxy = nil // 禁用 HTTP Proxy 设置,因为 SOCKS5 不需要 HTTP Proxy
// 尝试将 Dialer 转换为支持上下文的 ContextDialer
if contextDialer, ok := proxyDialer.(proxy.ContextDialer); ok {
transport.DialContext = contextDialer.DialContext
} else {
// 如果不支持 ContextDialer则回退到传统的 Dial 方法
transport.Dial = proxyDialer.Dial
logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
}
logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
default: // 如果代理协议不支持
logError("Unsupported proxy scheme: %s", proxyInfo.Scheme)
transport.Proxy = http.ProxyFromEnvironment // 回退到环境变量代理
}
}
// newProxyDial 创建一个 SOCKS5 代理拨号器
func newProxyDial(proxyUrls string) proxy.Dialer {
var proxyDialer proxy.Dialer = proxy.Direct // 初始为直接连接,不使用代理
// 支持多个代理 URL以逗号分隔
for _, proxyUrl := range strings.Split(proxyUrls, ",") {
proxyUrl = strings.TrimSpace(proxyUrl) // 去除首尾空格
if proxyUrl == "" { // 跳过空的代理 URL
continue
}
// 解析代理 URL
urlInfo, err := url.Parse(proxyUrl)
if err != nil {
// 如果 URL 解析失败,记录错误日志并跳过
logError("Failed to parse proxy URL %q: %v", proxyUrl, err)
continue
}
// 检查代理协议是否为 SOCKS5
if urlInfo.Scheme != "socks5" {
logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
continue
}
// 解析代理认证信息(用户名和密码)
auth := parseAuth(urlInfo)
// 创建 SOCKS5 代理拨号器
dialer, err := createSocksDialer(urlInfo.Host, auth, proxyDialer)
if err != nil {
// 如果创建失败,记录错误日志并跳过
logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err)
continue
}
// 更新代理拨号器,支持代理链
proxyDialer = dialer
}
return proxyDialer
}
// parseAuth 解析代理 URL 中的认证信息(用户名和密码)
func parseAuth(urlInfo *url.URL) *proxy.Auth {
// 如果 URL 中没有用户信息,返回 nil
if urlInfo.User == nil {
return nil
}
// 获取用户名
username := urlInfo.User.Username()
// 获取密码注意Password() 返回两个值,需要显式处理第二个值)
password, passwordSet := urlInfo.User.Password()
if !passwordSet {
password = "" // 如果密码未设置,使用空字符串
}
// 返回包含用户名和密码的认证信息
return &proxy.Auth{
User: username,
Password: password, // 允许空密码
}
}
// createSocksDialer 创建 SOCKS5 拨号器
func createSocksDialer(host string, auth *proxy.Auth, previous proxy.Dialer) (proxy.Dialer, error) {
// 调用 golang.org/x/net/proxy 提供的 SOCKS5 方法创建拨号器
return proxy.SOCKS5("tcp", host, auth, previous)
}

View File

@@ -6,7 +6,9 @@ import (
"ghproxy/config"
"io"
"net/http"
"strconv"
"github.com/WJQSERVER-STUDIO/go-utils/copyb"
"github.com/gin-gonic/gin"
)
@@ -14,29 +16,22 @@ func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode s
method := c.Request.Method
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
// 创建HTTP客户端
client := &http.Client{}
// 发送HEAD请求, 预获取Content-Length
headReq, err := http.NewRequest("HEAD", u, nil)
if err != nil {
HandleError(c, fmt.Sprintf("创建HEAD请求失败: %v", err))
return
logDump("Url Before FMT:%s", u)
if cfg.GitClone.Mode == "cache" {
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to extract parts from URL: %v", err))
return
}
// 构建新url
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
logDump("New Url After FMT:%s", u)
}
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()
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
}
var (
resp *http.Response
err error
)
body, err := readRequestBody(c)
if err != nil {
@@ -45,37 +40,96 @@ func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode s
}
bodyReader := bytes.NewBuffer(body)
// 创建请求
req, err := http.NewRequest(method, u, bodyReader)
if err != nil {
HandleError(c, fmt.Sprintf("创建请求失败: %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()
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)
reWriteEncodeHeader(req)
AuthPassThrough(c, cfg, req)
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
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)
reWriteEncodeHeader(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 != "" {
size, err := strconv.Atoi(contentLength)
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
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": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
}
for header := range headersToRemove {
resp.Header.Del(header)
}
switch cfg.Server.Cors {
case "*":
c.Header("Access-Control-Allow-Origin", "*")
case "":
c.Header("Access-Control-Allow-Origin", "*")
case "nil":
c.Header("Access-Control-Allow-Origin", "")
default:
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
}
CopyResponseHeaders(resp, c, cfg)
c.Status(resp.StatusCode)
if err := gitCopyResponseBody(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)
}
}
_, err = copyb.Copy(c.Writer, resp.Body)
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() // 确保刷入
}
// 复制响应体
func gitCopyResponseBody(c *gin.Context, respBody io.Reader) error {
_, err := io.Copy(c.Writer, respBody)
return err
}

View File

@@ -1,6 +1,7 @@
package proxy
import (
"errors"
"fmt"
"ghproxy/auth"
"ghproxy/config"
@@ -12,8 +13,11 @@ import (
"github.com/gin-gonic/gin"
)
var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter, runMode string) gin.HandlerFunc {
return func(c *gin.Context) {
// 限制访问频率
if cfg.RateLimit.Enabled {
@@ -36,9 +40,10 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
}
}
//rawPath := strings.TrimPrefix(c.Request.URL.Path, "/") // 去掉前缀/
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/") // 去掉前缀/
re := regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
matches := re.FindStringSubmatch(rawPath) // 匹配路径
logInfo("Matches: %v", matches)
// 匹配路径错误处理
if len(matches) < 3 {
@@ -51,14 +56,29 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
// 制作url
rawPath = "https://" + matches[2]
username, repo := MatchUserRepo(rawPath, cfg, c, matches) // 匹配用户名和仓库名
user, repo, matcher, err := Matcher(rawPath, cfg)
if err != nil {
if errors.Is(err, ErrInvalidURL) {
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
}
}
username := user
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)
// dump log 记录详细信息 c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, full Header
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)
repouser := fmt.Sprintf("%s/%s", username, repo)
// 白名单检查
if cfg.Whitelist.Enabled {
whitelist := auth.CheckWhitelist(repouser, username, repo)
whitelist := auth.CheckWhitelist(username, repo)
if !whitelist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser)
@@ -70,7 +90,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
// 黑名单检查
if cfg.Blacklist.Enabled {
blacklist := auth.CheckBlacklist(repouser, username, repo)
blacklist := auth.CheckBlacklist(username, repo)
if blacklist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
@@ -80,29 +100,16 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
}
}
matches = CheckURL(rawPath, c)
if matches == nil {
c.AbortWithStatus(http.StatusNotFound)
logError("%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
if exps[5].MatchString(rawPath) {
if cfg.Auth.AuthMethod != "header" || !cfg.Auth.Enabled {
c.JSON(http.StatusForbidden, gin.H{"error": "HeaderAuth is not enabled."})
logWarning("%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路径
if exps[1].MatchString(rawPath) {
if matcher == "blob" {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
}
// 鉴权
authcheck, err := auth.AuthHandler(c, cfg)
var authcheck bool
authcheck, err = auth.AuthHandler(c, cfg)
if !authcheck {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
@@ -110,13 +117,12 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
}
// IP METHOD URL USERAGENT PROTO MATCHES
logInfo("%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 Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, matches)
switch {
case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath):
//ProxyRequest(c, rawPath, cfg, "chrome", runMode)
ChunkedProxyRequest(c, rawPath, cfg, "chrome", runMode) // dev test chunk
case exps[2].MatchString(rawPath):
switch matcher {
case "releases", "blob", "raw", "gist", "api":
ChunkedProxyRequest(c, rawPath, cfg, matcher)
case "clone":
//ProxyRequest(c, rawPath, cfg, "git", runMode)
GitReq(c, rawPath, cfg, "git", runMode)
default:

143
proxy/httpc.go Normal file
View File

@@ -0,0 +1,143 @@
package proxy
import (
"fmt"
"ghproxy/config"
"net/http"
"sync"
"time"
httpc "github.com/satomitouka/touka-httpc"
)
var BufferSize int = 32 * 1024 // 32KB
var (
tr *http.Transport
gittr *http.Transport
BufferPool *sync.Pool
client *httpc.Client
gitclient *httpc.Client
)
func InitReq(cfg *config.Config) {
initHTTPClient(cfg)
if cfg.GitClone.Mode == "cache" {
initGitHTTPClient(cfg)
}
// 初始化固定大小的缓存池
BufferPool = &sync.Pool{
New: func() interface{} {
return make([]byte, BufferSize)
},
}
}
func initHTTPClient(cfg *config.Config) {
var proTolcols = new(http.Protocols)
proTolcols.SetHTTP1(true)
proTolcols.SetHTTP2(true)
proTolcols.SetUnencryptedHTTP2(true)
if cfg.Httpc.Mode == "auto" {
tr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
}
} else if cfg.Httpc.Mode == "advanced" {
tr = &http.Transport{
MaxIdleConns: cfg.Httpc.MaxIdleConns,
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
}
} 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")
tr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
}
}
if cfg.Outbound.Enabled {
initTransport(cfg, tr)
}
if cfg.Server.Debug {
client = httpc.New(
httpc.WithTransport(tr),
httpc.WithDumpLog(),
)
} else {
client = httpc.New(
httpc.WithTransport(tr),
)
}
}
func initGitHTTPClient(cfg *config.Config) {
var proTolcols = new(http.Protocols)
proTolcols.SetHTTP1(true)
proTolcols.SetHTTP2(true)
proTolcols.SetUnencryptedHTTP2(true)
if cfg.GitClone.ForceH2C {
proTolcols.SetHTTP1(false)
proTolcols.SetHTTP2(false)
proTolcols.SetUnencryptedHTTP2(true)
}
if cfg.Httpc.Mode == "auto" {
gittr = &http.Transport{
//MaxIdleConns: 160,
IdleConnTimeout: 30 * time.Second,
WriteBufferSize: 32 * 1024, // 32KB
ReadBufferSize: 32 * 1024, // 32KB
Protocols: proTolcols,
}
} 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
Protocols: proTolcols,
}
} 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 {
gitclient = httpc.New(
httpc.WithTransport(gittr),
httpc.WithDumpLog(),
)
} else {
gitclient = httpc.New(
httpc.WithTransport(gittr),
)
}
}

326
proxy/match.go Normal file
View File

@@ -0,0 +1,326 @@
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://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://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://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.AuthMethod != "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
}
// 匹配 "https://api.github.com/"开头的链接
if cfg.Shell.RewriteAPI {
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
}
// processLinks 处理链接并将结果写入输出流
func processLinks(input io.Reader, output io.Writer, compress string, host string, cfg *config.Config) (written int64, err error) {
var reader *bufio.Reader
if compress == "gzip" {
// 解压gzip
gzipReader, err := gzip.NewReader(input)
if err != nil {
return 0, fmt.Errorf("gzip解压错误: %v", err)
}
defer gzipReader.Close()
reader = bufio.NewReader(gzipReader)
} else {
reader = bufio.NewReader(input)
}
var writer *bufio.Writer
var gzipWriter *gzip.Writer
// 根据是否gzip确定 writer 的创建
if compress == "gzip" {
gzipWriter = gzip.NewWriter(output)
writer = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小
} else {
writer = bufio.NewWriterSize(output, 4096)
}
//确保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 := writer.Flush(); flushErr != nil {
logError("writer flush failed %v", flushErr)
// 如果已经存在错误,则保留。否则,记录此错误。
if err == nil {
err = flushErr
}
}
}()
// 使用正则表达式匹配 http 和 https 链接
urlPattern := regexp.MustCompile(`https?://[^\s'"]+`)
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break // 文件结束
}
return written, fmt.Errorf("读取行错误: %v", err) // 传递错误
}
// 替换所有匹配的 URL
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
return modifyURL(originalURL, host, cfg)
})
n, werr := writer.WriteString(modifiedLine)
written += int64(n) // 更新写入的字节数
if werr != nil {
return written, fmt.Errorf("写入文件错误: %v", werr) // 传递错误
}
}
// 在返回之前,再刷新一次
if fErr := writer.Flush(); fErr != nil {
return written, fErr
}
return written, nil
}
// 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
}

View File

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

View File

@@ -4,10 +4,6 @@ import (
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin"
@@ -16,78 +12,25 @@ import (
// 日志模块
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 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
}
func HandleError(c *gin.Context, message string) {
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
logWarning(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)
logWarning(errMsg)
return nil
logError(message)
}

View File

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

View File

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

View File

@@ -1,56 +0,0 @@
package proxy
import (
"ghproxy/config"
"net/http"
"github.com/gin-gonic/gin"
)
func CopyResponseHeaders(resp *http.Response, c *gin.Context, cfg *config.Config) {
copyHeaders(resp, c)
removeHeaders(resp)
setCORSHeaders(c, cfg)
setDefaultHeaders(c)
}
// 复制响应头
func copyHeaders(resp *http.Response, c *gin.Context) {
for key, values := range resp.Header {
for _, value := range values {
c.Header(key, value)
}
}
}
// 移除指定响应头
func removeHeaders(resp *http.Response) {
headersToRemove := map[string]struct{}{
"Content-Security-Policy": {},
"Referrer-Policy": {},
"Strict-Transport-Security": {},
}
for header := range headersToRemove {
resp.Header.Del(header)
}
}
// CORS配置
func setCORSHeaders(c *gin.Context, cfg *config.Config) {
if cfg.CORS.Enabled {
c.Header("Access-Control-Allow-Origin", "*")
} else {
c.Header("Access-Control-Allow-Origin", "")
}
}
// 默认响应
func setDefaultHeaders(c *gin.Context) {
c.Header("Age", "10")
c.Header("Cache-Control", "max-age=300")
}

View File

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

86
timing/timing.go Normal file
View File

@@ -0,0 +1,86 @@
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
}