Compare commits
292 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f89fc53046 | ||
|
|
98fdd61673 | ||
|
|
3f802a0ed3 | ||
|
|
88d84d0703 | ||
|
|
33bb588c36 | ||
|
|
cc4b04ede2 | ||
|
|
3abe4419d6 | ||
|
|
d2d9ad1db7 | ||
|
|
80f52dda3c | ||
|
|
95dd34a456 | ||
|
|
1f3a036267 | ||
|
|
799a4895e5 | ||
|
|
b033079553 | ||
|
|
a391895e7f | ||
|
|
7e153d2b51 | ||
|
|
90eca70eb1 | ||
|
|
c19a0e9af9 | ||
|
|
4ee7f56ec5 | ||
|
|
4e469a4896 | ||
|
|
00513f689d | ||
|
|
ad4d55bc39 | ||
|
|
7adab36c68 | ||
|
|
4ea5a875fe | ||
|
|
904a800eea | ||
|
|
f0902c1da3 | ||
|
|
781e175721 | ||
|
|
79692965a6 | ||
|
|
ff5f77edc9 | ||
|
|
402308d620 | ||
|
|
d4237f0463 | ||
|
|
eb3bf16e06 | ||
|
|
b701a89b98 | ||
|
|
933aeee518 | ||
|
|
3812b029cf | ||
|
|
e629b5db47 | ||
|
|
e5bc171f25 | ||
|
|
6eae638256 | ||
|
|
3e03f47ef7 | ||
|
|
eb113b4191 | ||
|
|
8ea741aec8 | ||
|
|
37488db087 | ||
|
|
5b253998ce | ||
|
|
92432121e5 | ||
|
|
37fd1383ee | ||
|
|
ceda8220fd | ||
|
|
1636bf1548 | ||
|
|
a4d324a361 | ||
|
|
35b70fadca | ||
|
|
91c3ad7fd8 | ||
|
|
97b1f69f99 | ||
|
|
fd7e270db4 | ||
|
|
cf5ae0d184 | ||
|
|
0008366e07 | ||
|
|
c13dd9082b | ||
|
|
e0cbfed1e7 | ||
|
|
1b06260a14 | ||
|
|
8ab622d149 | ||
|
|
96c30889f4 | ||
|
|
bbb108689a | ||
|
|
41395b1d72 | ||
|
|
bd8412f157 | ||
|
|
d2a0177015 | ||
|
|
bf75e62eb2 | ||
|
|
a5bf7686bd | ||
|
|
a1991367c3 | ||
|
|
b86e58cddf | ||
|
|
44673b9a3f | ||
|
|
5b05588375 | ||
|
|
65769975b6 | ||
|
|
5731418822 | ||
|
|
8d5b764ec7 | ||
|
|
5dde21a403 | ||
|
|
f706615d87 | ||
|
|
b29940df21 | ||
|
|
185522133b | ||
|
|
6be6e1ba2c | ||
|
|
1ba100c28d | ||
|
|
1370617f5b | ||
|
|
e829c2baff | ||
|
|
75d909ef16 | ||
|
|
2bab0a9774 | ||
|
|
171fe61342 | ||
|
|
362ad96fbe | ||
|
|
5b17e1f0b6 | ||
|
|
e40e1aadee | ||
|
|
82943428d3 | ||
|
|
b7ce929db8 | ||
|
|
68bf51aaed | ||
|
|
16b6b05fb8 | ||
|
|
d2b2d823b8 | ||
|
|
4598257faa | ||
|
|
1afb352194 | ||
|
|
430e313d47 | ||
|
|
31d435bfa0 | ||
|
|
6ff23f639e | ||
|
|
c7954ae91a | ||
|
|
11099176bf | ||
|
|
f3eb92ea51 | ||
|
|
5ddbf1d2a0 | ||
|
|
d38ca3969f | ||
|
|
146b0d7748 | ||
|
|
d92424cb94 | ||
|
|
0f437dc891 | ||
|
|
816b35654a | ||
|
|
a4fae95526 | ||
|
|
ea0e4e9801 | ||
|
|
5facc36947 | ||
|
|
5c25bc012f | ||
|
|
b2712f8184 | ||
|
|
566a0ea26a | ||
|
|
7d4aae1668 | ||
|
|
052243b095 | ||
|
|
4ded2186d8 | ||
|
|
aa95daf8c0 | ||
|
|
89b850c1ec | ||
|
|
ce814875e1 | ||
|
|
47c03763a7 | ||
|
|
71bc2aaed7 | ||
|
|
3f8d16511e | ||
|
|
43469532d4 | ||
|
|
e32479b287 | ||
|
|
ef6e0a78cd | ||
|
|
c2e2b661a4 | ||
|
|
791f668758 | ||
|
|
92c4c62b46 | ||
|
|
545144c7b5 | ||
|
|
866638ba8e | ||
|
|
e3fd604945 | ||
|
|
90709539f4 | ||
|
|
1011a25d16 | ||
|
|
bd63ed3070 | ||
|
|
3c11e9826e | ||
|
|
ef3b1bf1f0 | ||
|
|
ad4d8eb670 | ||
|
|
030f0d12a9 | ||
|
|
e57432a01c | ||
|
|
ace795fe9d | ||
|
|
3f51e5319a | ||
|
|
55769d9a40 | ||
|
|
7eb312243c | ||
|
|
6ca31bc252 | ||
|
|
dfc49ae28b | ||
|
|
a0cca13deb | ||
|
|
1498aaed14 | ||
|
|
086aa999e1 | ||
|
|
bf92cc8429 | ||
|
|
d94f6c0f5d | ||
|
|
f540b2edcd | ||
|
|
8aef197fde | ||
|
|
52d6f8e759 | ||
|
|
a7be65a111 | ||
|
|
9977eb1437 | ||
|
|
47de48bcce | ||
|
|
8ccf48a6fe | ||
|
|
7a6544c6c9 | ||
|
|
b955c915ff | ||
|
|
e42ea358bb | ||
|
|
4936a93788 | ||
|
|
493ac28b59 | ||
|
|
d79aeaaacd | ||
|
|
558d3fbb0b | ||
|
|
3d7559bd66 | ||
|
|
809032a970 | ||
|
|
2eb6a9810b | ||
|
|
26a5148c6f | ||
|
|
c656aa41ca | ||
|
|
0b052f9c7f | ||
|
|
6fb7e1150e | ||
|
|
5e0f95dae3 | ||
|
|
c1c39a5a1f | ||
|
|
dd2f5b5a12 | ||
|
|
7e5b12dff8 | ||
|
|
26a42b6510 | ||
|
|
254c9a8bad | ||
|
|
060453f070 | ||
|
|
f110c96c1f | ||
|
|
73aac79c1b | ||
|
|
bed6c486dc | ||
|
|
ab77c5c7da | ||
|
|
bf21bd197a | ||
|
|
8af107c584 | ||
|
|
d6d54b222f | ||
|
|
005a4543d4 | ||
|
|
a85eb38de5 | ||
|
|
152fb8aa71 | ||
|
|
3e9e43cd44 | ||
|
|
8a50b311fc | ||
|
|
dcc50401c4 | ||
|
|
d62a1f9769 | ||
|
|
c522eba7ae | ||
|
|
3da54f0599 | ||
|
|
886c99f53d | ||
|
|
d3520a2133 | ||
|
|
1f0b43ec43 | ||
|
|
36646ebf7e | ||
|
|
d7ed4fc6ad | ||
|
|
7cbce12316 | ||
|
|
ff412f94ec | ||
|
|
b02aaeba8a | ||
|
|
395f641468 | ||
|
|
978ece6fa0 | ||
|
|
1adc3a3192 | ||
|
|
a66452cf10 | ||
|
|
d231fd839f | ||
|
|
4b37c6bb2b | ||
|
|
153b544024 | ||
|
|
635c22f9a7 | ||
|
|
f342312b40 | ||
|
|
acaf38b88d | ||
|
|
50cfd64db8 | ||
|
|
53e115242a | ||
|
|
cef0338d36 | ||
|
|
f8edb0e0bc | ||
|
|
c11f368a9c | ||
|
|
db38b2a402 | ||
|
|
accb52b952 | ||
|
|
70fb808acf | ||
|
|
b684227191 | ||
|
|
1498156f56 | ||
|
|
55158c0cb1 | ||
|
|
6c3280f850 | ||
|
|
866275aad3 | ||
|
|
f4cd7eecf1 | ||
|
|
5501cd3e3c | ||
|
|
f9f37262f0 | ||
|
|
026039e0bc | ||
|
|
8739027772 | ||
|
|
cafc713a65 | ||
|
|
8f2cc820aa | ||
|
|
139fc92abc | ||
|
|
e9d793c104 | ||
|
|
c931017f03 | ||
|
|
448e06d350 | ||
|
|
27cc30ab8b | ||
|
|
a65e44ac02 | ||
|
|
a0cfe826ea | ||
|
|
2e974ad7ae | ||
|
|
b7b9cd5db5 | ||
|
|
bcb73c18de | ||
|
|
ed839b828d | ||
|
|
801b8c6cda | ||
|
|
a92bbb7fb6 | ||
|
|
3e40146281 | ||
|
|
ac7e1e43b5 | ||
|
|
f134d22540 | ||
|
|
79153c0f7d | ||
|
|
4fd47812f7 | ||
|
|
17c49d534b | ||
|
|
284b38bab4 | ||
|
|
d73dfe7db5 | ||
|
|
dc286e002c | ||
|
|
5c54ae788c | ||
|
|
bfcb1c9901 | ||
|
|
9bfe8517cb | ||
|
|
50ba185aab | ||
|
|
6ee928b0c7 | ||
|
|
979f59545b | ||
|
|
da89b3f45e | ||
|
|
498266e08e | ||
|
|
e2faa497ab | ||
|
|
8def955151 | ||
|
|
a18660121a | ||
|
|
d26f6d1e1b | ||
|
|
60a1f6073d | ||
|
|
2cc5409dd0 | ||
|
|
ad9cffe9e2 | ||
|
|
9af5010b79 | ||
|
|
19cd77afd2 | ||
|
|
91cd76e541 | ||
|
|
0faddce474 | ||
|
|
f7ba0c28b4 | ||
|
|
146dedea21 | ||
|
|
8336896979 | ||
|
|
785a74dfeb | ||
|
|
bd666e08d1 | ||
|
|
459aa24f89 | ||
|
|
a1e8e3e373 | ||
|
|
40c9ca5f38 | ||
|
|
97ae0044e7 | ||
|
|
55afe7676c | ||
|
|
b95582ae1a | ||
|
|
9aaa3e64d3 | ||
|
|
9e0f222125 | ||
|
|
0d6c1d7e35 | ||
|
|
5c14aeb48d | ||
|
|
21d30dee53 | ||
|
|
a061b8d369 | ||
|
|
68346717a5 | ||
|
|
2b7fbd2a0d | ||
|
|
4c5d288f03 | ||
|
|
09163ed4df | ||
|
|
f5c32915b9 |
2
.github/ISSUE_TEMPLATE/features_request.md
vendored
2
.github/ISSUE_TEMPLATE/features_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Features request
|
||||
about: 提出新功能建议
|
||||
title: "[Features]"
|
||||
labels: enhancement
|
||||
labels: 改进
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
2
.github/workflows/auto-assign.yml
vendored
2
.github/workflows/auto-assign.yml
vendored
@@ -15,5 +15,5 @@ jobs:
|
||||
uses: pozil/auto-assign-issue@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.AUTO_ASSIGN }}
|
||||
assignees: WJQSERVER, satomitoka
|
||||
assignees: WJQSERVER, satomitouka
|
||||
numOfAssignee: 2
|
||||
45
.github/workflows/build-dev.yml
vendored
45
.github/workflows/build-dev.yml
vendored
@@ -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,11 @@ jobs:
|
||||
else
|
||||
echo "DEV-VERSION file not found!" && exit 1
|
||||
fi
|
||||
- name: 拉取前端
|
||||
run: |
|
||||
sudo git clone https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend.git pages
|
||||
sudo rm -rf pages/.git/
|
||||
|
||||
- name: 安装 Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
@@ -48,10 +82,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 +98,7 @@ jobs:
|
||||
tag: ${{ env.VERSION }}
|
||||
allowUpdates: true
|
||||
prerelease: true
|
||||
body: ${{ env.VERSION }}
|
||||
env:
|
||||
export PATH: $PATH:/usr/local/go/bin
|
||||
|
||||
|
||||
71
.github/workflows/build.yml
vendored
71
.github/workflows/build.yml
vendored
@@ -9,18 +9,12 @@ on:
|
||||
- 'VERSION'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
GO_VERSION: 1.23.5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: main
|
||||
- name: 加载版本号
|
||||
run: |
|
||||
if [ -f VERSION ]; then
|
||||
@@ -28,6 +22,49 @@ jobs:
|
||||
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, darwin, freebsd]
|
||||
goarch: [amd64, arm64]
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
GO_VERSION: 1.24
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: main
|
||||
- name: 加载版本号
|
||||
run: |
|
||||
if [ -f VERSION ]; then
|
||||
echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
|
||||
else
|
||||
echo "VERSION file not found!" && exit 1
|
||||
fi
|
||||
- name: 拉取前端
|
||||
run: |
|
||||
sudo git clone https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend.git pages
|
||||
sudo rm -rf pages/.git/
|
||||
|
||||
- name: 安装 Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
@@ -45,12 +82,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 +97,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ env.VERSION }}
|
||||
allowUpdates: true
|
||||
body: ${{ env.VERSION }}
|
||||
env:
|
||||
export PATH: $PATH:/usr/local/go/bin
|
||||
|
||||
@@ -73,6 +111,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- name: Load VERSION
|
||||
run: |
|
||||
if [ -f VERSION ]; then
|
||||
@@ -101,4 +141,7 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.IMAGE_NAME }}:v4
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
wjqserver/ghproxy-touka:latest
|
||||
wjqserver/ghproxy-touka:${{ env.VERSION }}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,4 +1,9 @@
|
||||
demo
|
||||
demo.toml
|
||||
*.log
|
||||
*.bak
|
||||
*.bak
|
||||
list.json
|
||||
iplist.json
|
||||
repos
|
||||
pages
|
||||
*_test
|
||||
838
CHANGELOG.md
838
CHANGELOG.md
@@ -1,5 +1,841 @@
|
||||
# 更新日志
|
||||
|
||||
4.2.1 - 2025-07-25
|
||||
---
|
||||
- CHANGE: 更新主题样式, 新增`free`主题, `design`与`hub`主题样式更新
|
||||
|
||||
4.2.0 - 2025-07-22
|
||||
---
|
||||
- CHANGE: 支持根据IP(CDIR)进行白名单与屏蔽
|
||||
- CHANGE: 进一步推进`json/v2`支持
|
||||
|
||||
4.2.0-rc.0 - 2025-07-22
|
||||
---
|
||||
- PRE-RELEASE: v4.2.0-rc.0是v4.2.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 支持根据IP(CDIR)进行白名单与屏蔽
|
||||
- CHANGE: 深化json/v2改革, 预备go1.25 json/v2
|
||||
|
||||
4.1.7 - 2025-07-20
|
||||
---
|
||||
- CHANGE: 更新相关依赖
|
||||
- CHANGE: 改进代码结构, 完善处理
|
||||
|
||||
4.1.7-rc.0 - 2025-07-20
|
||||
---
|
||||
- PRE-RELEASE: v4.1.7-rc.0是v4.1.7预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新相关依赖
|
||||
- CHANGE: 改进代码结构, 完善处理
|
||||
|
||||
4.1.6 - 2025-07-07
|
||||
---
|
||||
- CHANGE: 更新[Touka框架](https://github.com/infinite-iroha/touka)版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配
|
||||
- CHANGE: 更新[Touka HTTPC](https://github.com/WJQSERVER-STUDIO/httpc)版本到`v0.8.0`, 使用`json/v2`的同时, 提升`io`相关操作性能并降低分配, 优化`debug`模式下打印输出性能
|
||||
|
||||
4.1.6-rc.0 - 2025-07-07
|
||||
---
|
||||
- PRE-RELEASE: v4.1.6-rc.0是v4.1.6预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新[Touka框架](https://github.com/infinite-iroha/touka)版本到`v0.2.9`, 提升`io`相关方式的性能并降低分配
|
||||
- CHANGE: 更新[Touka HTTPC](https://github.com/WJQSERVER-STUDIO/httpc)版本到`v0.8.0`, 使用`json/v2`的同时, 提升`io`相关操作性能并降低分配, 优化`debug`模式下打印输出性能
|
||||
|
||||
4.1.5 - 2025-07-03
|
||||
---
|
||||
- CHANGE: 更新`httpc`依赖以修正一些问题
|
||||
|
||||
4.1.5-rc.0 - 2025-07-03
|
||||
---
|
||||
- PRE-RELEASE: v4.1.5-rc.0是v4.1.5预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新`httpc`依赖以修正一些问题
|
||||
|
||||
4.1.4 - 2025-06-30
|
||||
---
|
||||
- CHANGE: 使用`touka`框架的内建httpc统一管理, 同时对httpc相关初始化进行改进
|
||||
- CHANGE: 更新`json/v2`版本
|
||||
|
||||
4.1.4-rc.0 - 2025-06-30
|
||||
---
|
||||
- PRE-RELEASE: v4.1.4-rc.0是v4.1.4预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 使用`touka`框架的内建httpc统一管理, 同时对httpc相关初始化进行改进
|
||||
- CHANGE: 更新`json/v2`版本
|
||||
|
||||
4.1.3 - 2025-06-25
|
||||
---
|
||||
- CHANGE: 更新`touka`版本, 使用新的方式配置slash重定向功能
|
||||
|
||||
4.1.3-rc.0 - 2025-06-25
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v4.1.3预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新`touka`版本, 使用新的方式配置slash重定向功能
|
||||
|
||||
4.1.2 - 2025-06-18
|
||||
---
|
||||
- CHANGE: 更新`design`主题, 更新默认配置生成
|
||||
|
||||
4.1.2-rc.0 - 2025-06-18
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v4.1.2预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新`design`主题, 更新默认配置生成
|
||||
|
||||
4.1.1 - 2025-06-18
|
||||
---
|
||||
- CHANGE: 更新touka框架到v0.2.6, 解决MidwareX的一些状态问题
|
||||
|
||||
4.1.0 - 2025-06-17
|
||||
---
|
||||
- ADD: 加入基于`basic auth`的docker鉴权支持
|
||||
|
||||
4.1.0-rc.0 - 2025-06-17
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v4.1.0预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 加入基于`basic auth`的docker鉴权支持
|
||||
|
||||
4.1.0-beta.0 - 2025-06-17
|
||||
---
|
||||
- BETA-TEST: 此版本是v4.1.0的测试版本,请勿在生产环境中使用;
|
||||
- ADD: 加入基于`basic auth`的docker鉴权支持
|
||||
|
||||
4.0.0 - 2025-06-16
|
||||
---
|
||||
- CHANGE: 移交到Touka框架
|
||||
- REMOVE: 移除req rate limit的total方式
|
||||
- CHANGE: 使用[reco](https://github.com/fenthope/reco)日志库, 异步使能
|
||||
- FIX: 更换HTTP框架以解决v3可能存在的内存分配与回收问题
|
||||
|
||||
4.0.0-rc.0 - 2025-06-16
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v4.0.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 移交到Touka框架
|
||||
- REMOVE: 移除req rate limit的total方式
|
||||
- CHANGE: 使用[reco](https://github.com/fenthope/reco)日志库, 异步使能
|
||||
|
||||
4.0.0-beta.0 - 2025-06-15
|
||||
---
|
||||
- BETA-TEST: 此版本是v4.0.0的测试版本,请勿在生产环境中使用;
|
||||
- CHANGE: 移交到Touka框架
|
||||
- REMOVE: 移除req rate limit的total方式
|
||||
- CHANGE: 使用[reco](https://github.com/fenthope/reco)日志库, 异步使能
|
||||
|
||||
3.5.6 - 2025-06-15
|
||||
---
|
||||
- FIX: 修正blob重写的生成问题
|
||||
- CHANGE: 改进302重定向逻辑
|
||||
|
||||
25w48c - 2025-06-15
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 加入内部301处理
|
||||
|
||||
25w48b - 2025-06-15
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用;
|
||||
- FIX: 修正blob重写的生成问题
|
||||
- CHANGE: 验证与连接释放相关的修正
|
||||
|
||||
25w48a - 2025-06-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.6预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 测试302重定向逻辑
|
||||
|
||||
3.5.5 - 2025-06-14
|
||||
---
|
||||
- CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖
|
||||
|
||||
25w47a - 2025-06-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.5预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 修正新匹配器的覆盖问题, 同时增加test的覆盖
|
||||
|
||||
3.5.4 - 2025-06-14
|
||||
---
|
||||
- CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑
|
||||
|
||||
25w46c - 2025-06-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 移植来自于[GHProxy-Touka](https://github.com/WJQSERVER-STUDIO/ghproxy-touka)的blob处理逻辑与302处理逻辑
|
||||
|
||||
25w46b - 2025-06-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 修改关闭行为以测试问题
|
||||
|
||||
25w46a - 2025-06-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.4预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 修改payload行为以测试问题
|
||||
|
||||
3.5.3 - 2025-06-13
|
||||
---
|
||||
- CHANGE: 显式配置`WithStreamBody(true)`
|
||||
|
||||
3.5.2 - 2025-06-11
|
||||
---
|
||||
- CHANGE: 加入MPL 2.0许可证, 项目转为双重许可
|
||||
|
||||
3.5.1 - 2025-06-09
|
||||
---
|
||||
- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op`
|
||||
|
||||
25w45a - 2025-06-09
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.1预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 大幅优化`Matcher`的性能, 实现零分配, 大幅提升性能; 单次操作时间: `254.3 ns/op` => `29.59 ns/op`
|
||||
|
||||
3.5.0 - 2025-06-05
|
||||
---
|
||||
- CHANGE: 更新许可证 v2.0 => v2.1
|
||||
- CHANGE: 修正工作流的一些问题
|
||||
- ADD: 增加`ForceAllowApiPassList`, 实现 #114
|
||||
|
||||
25w44a - 2025-06-05
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.5.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新许可证 v2.0 => v2.1
|
||||
- CHANGE: 修正工作流的一些问题
|
||||
- ADD: 增加`ForceAllowApiPassList`, 实现 #114
|
||||
|
||||
3.4.3 - 2025-06-05
|
||||
---
|
||||
- CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler`
|
||||
- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified`
|
||||
- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate`
|
||||
|
||||
25w43a - 2025-06-05
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.4.3预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 弃用`adaptor.GetCompatRequest`, 切换到`adaptor.HertzHandler`
|
||||
- CHANGE: 为`embedFS`使用包装器, 使其支持`Last-Modified`
|
||||
- CHANGE: 为静态资源增加`Cache-Control: public, max-age=3600, must-revalidate`
|
||||
|
||||
3.4.2 - 2025-06-03
|
||||
---
|
||||
- DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常
|
||||
|
||||
25w42a - 2025-06-02
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.4.2预发布版本,请勿在生产环境中使用;
|
||||
- DEP: 回滚 github.com/nyaruka/phonenumbers 版本到 v1.6.1, v1.6.3观测到了一些反射造成的内存占用异常
|
||||
|
||||
3.4.1 - 2025-05-29
|
||||
---
|
||||
- ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染
|
||||
- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic)
|
||||
|
||||
25w41b - 2025-05-28
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 把json库替换到[sonic](github.com/bytedance/sonic)
|
||||
|
||||
25w41a - 2025-05-28
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.4.1预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 为`errorpage`部分增加lru缓存, 避免重复渲染
|
||||
- CHANGE: 替换到实验性的`encoding/json/v2`
|
||||
|
||||
3.4.0 - 2025-05-21
|
||||
---
|
||||
- ADD: 初步实现多`target` Docker代理
|
||||
- ADD: 加入`weakcache`用于处理短期令牌
|
||||
- ADD: 新增`hub`主题
|
||||
- ADD: 新增`/api/shell_nest/status`与`/api/oci_proxy/status` API
|
||||
|
||||
25w40b - 2025-05-21
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 新增`hub`主题
|
||||
- ADD: 新增`/api/shell_nest/status`与`/api/oci_proxy/status` API
|
||||
- CHANGE: 对细节进行优化
|
||||
|
||||
25w40a - 2025-05-21
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.4.0预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 初步实现多`target` Docker代理
|
||||
- ADD: 加入`weakcache`用于处理短期令牌
|
||||
|
||||
3.3.3 - 2025-05-20
|
||||
---
|
||||
- CHANGE: 加入`senseClientDisconnection`与`async`配置项
|
||||
|
||||
25w39a - 2025-05-19
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.3.3预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 加入`senseClientDisconnection`与`async`配置项
|
||||
|
||||
3.3.2 - 2025-05-18
|
||||
---
|
||||
- CHANGE: 默认主题改为`design`
|
||||
|
||||
25w38a - 2025-05-18
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.3.2预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 默认主题改为`design`
|
||||
|
||||
3.3.1 - 2025-05-16
|
||||
- CHANGE: 为`target`放宽限制, 支持自定义
|
||||
- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0`
|
||||
|
||||
25w37a - 2025-05-16
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.3.1预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 为`target`放宽限制, 支持自定义
|
||||
- CHANGE: 更新`hertz`, `0.9.7`=>`0.10.0`
|
||||
|
||||
3.3.0 - 2025-05-15
|
||||
---
|
||||
- CHANGE: 为`httpc`加入`request builder`的`withcontext`选项
|
||||
- ADD: 加入带宽限制功能
|
||||
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
|
||||
|
||||
25w36d - 2025-05-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 为`netpoll`模式开启探测客户端是否断开功能
|
||||
|
||||
25w36c - 2025-05-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 加入带宽限制功能
|
||||
- CHANGE: 将`httpc`切换回主分支, `25w36b`测试的部分已被合入`httpc`主线
|
||||
|
||||
25w36b - 2025-05-13
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: `httpc`切换到`dev`, 测试在retry前检查ctx状态
|
||||
|
||||
25w36a - 2025-05-13
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.3.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 为`httpc`加入`request builder`的`withcontext`选项
|
||||
|
||||
3.2.4 - 2025-05-13
|
||||
---
|
||||
- CHANGE: 移除未使用的变量与相关计算
|
||||
|
||||
25w35a - 2025-05-12
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.2.4预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 移除未使用的变量与相关计算
|
||||
|
||||
3.2.3 - 2025-05-07
|
||||
---
|
||||
- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录
|
||||
- CHANGE: 更新Go版本到go1.24.3
|
||||
- CHANGE: 迁移httpc到新的仓库
|
||||
|
||||
25w34b - 2025-05-07
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新Go版本到go1.24.3
|
||||
|
||||
25w34a - 2025-05-05
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.2.3预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 迁移logger库到新的仓库, 开启异步日志记录
|
||||
- CHANGE: 迁移httpc到新的仓库
|
||||
|
||||
3.2.2 - 2025-04-29
|
||||
---
|
||||
- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制
|
||||
- FIX: 修正Header部分的一些处理问题
|
||||
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
|
||||
|
||||
25w33b - 2025-04-29
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.2.2预发布版本,请勿在生产环境中使用;
|
||||
- REVERT: 为`git clone`部分回滚 3.1.0中的 "使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`" 修改
|
||||
|
||||
25w33a - 2025-04-29
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.2.2预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 实验性的raw Header处置, 用于应对Github对zh-CN的限制
|
||||
- FIX: 修正Header部分的一些处理问题
|
||||
|
||||
3.2.1 - 2025-04-29
|
||||
---
|
||||
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
|
||||
|
||||
25w32a - 2025-04-29
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.2.1预发布版本,请勿在生产环境中使用;
|
||||
- FIX: 修复在`HertZ`路由匹配器下`matcher`键值不一致的问题
|
||||
|
||||
3.2.0 - 2025-04-27
|
||||
---
|
||||
- CHANGE: 加入`ghcr`和`dockerhub`反代功能
|
||||
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
|
||||
|
||||
25w31a - 2025-04-27
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.2.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 加入`ghcr`和`dockerhub`反代功能
|
||||
- FIX: 修复在`HertZ`路由匹配器下与用户名相关功能异常的问题
|
||||
|
||||
3.1.0 - 2025-04-24
|
||||
---
|
||||
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器, 以提升效率
|
||||
- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`
|
||||
- CHANGE: 使用`HertZ`的`requestContext`传递matcher参数, 而不是`25w30a`中的ctx
|
||||
- CHANGE: 改进`rate`模块, 避免并发竞争问题
|
||||
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
|
||||
- CHANGE: 修改部分log等级
|
||||
- FIX: 修正默认配置的填充错误
|
||||
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
|
||||
- CHANGE: 改进handle, 复用共同部分
|
||||
- CHANGE: 细化url匹配的返回码处理
|
||||
- CHANGE: 增加404界面
|
||||
|
||||
25w30e - 2025-04-24
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 改进`rate`模块, 避免并发竞争问题
|
||||
- CHANGE: 将大部分状态码返回改为新的`html/tmpl`方式处理
|
||||
- CHANGE: 修改部分log等级
|
||||
- FIX: 修正默认配置的填充错误
|
||||
|
||||
25w30d - 2025-04-22
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 使用go `html/tmpl`处理状态码页面, 同时实现错误信息显示
|
||||
|
||||
25w30c - 2025-04-21
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 改进handle, 复用共同部分
|
||||
- CHANGE: 细化url匹配的返回码处理
|
||||
- CHANGE: 增加404界面
|
||||
|
||||
25w30b - 2025-04-21
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 使用`bodystream`进行req方向的body复制, 而不是使用额外的`buffer reader`
|
||||
- CHANGE: 使用`HertZ`的`requestContext`传递matcher参数, 而不是`25w30a`中的标准ctx
|
||||
|
||||
25w30a - 2025-04-19
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.1.0预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 对标准url使用`HertZ`路由匹配器, 而不是自制匹配器
|
||||
|
||||
3.0.3 - 2025-04-19
|
||||
---
|
||||
- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息
|
||||
- FIX: 修正非预期的header操作行为
|
||||
- CHANGE: 合并header相关逻辑, 避免多次操作
|
||||
- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理
|
||||
- CHANGE: 增加`netlib`配置项
|
||||
|
||||
25w29b - 2025-04-19
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 增加`netlib`配置项
|
||||
|
||||
25w29a - 2025-04-17
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.3预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 增加移除部分header的处置, 避免向服务端/客户端透露过多信息
|
||||
- FIX: 修正非预期的header操作行为
|
||||
- CHANGE: 合并header相关逻辑, 避免多次操作
|
||||
- CHANGE: 对editor模式下的input进行处置, 增加隐式关闭处理
|
||||
|
||||
3.0.2 - 2025-04-15
|
||||
---
|
||||
- CHANGE: 避免重复的re编译操作
|
||||
- CHANGE: 去除不必要的请求
|
||||
- CHANGE: 改进`httpc`相关配置
|
||||
- CHANGE: 更新`httpc` 0.4.0
|
||||
- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进
|
||||
|
||||
25w28b - 2025-04-15
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.2预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 改进resp关闭
|
||||
- CHANGE: 避免重复的re编译操作
|
||||
|
||||
25w28a - 2025-04-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 去除不必要的请求
|
||||
- CHANGE: 改进`httpc`相关配置
|
||||
- CHANGE: 合入test版本修改
|
||||
|
||||
25w28t-2 - 2025-04-11
|
||||
---
|
||||
- TEST: 测试验证版本
|
||||
- CHANGE: 为不遵守`RFC 2616`, `RFC 9112`的客户端带来兼容性改进
|
||||
|
||||
25w28t-1 - 2025-04-11
|
||||
---
|
||||
- TEST: 测试验证版本
|
||||
- CHANGE: 更新httpc 0.4.0
|
||||
|
||||
3.0.1 - 2025-04-08
|
||||
---
|
||||
- CHANGE: 加入`memLimit`指示gc
|
||||
- CHANGE: 加入`hlog`输出路径配置
|
||||
- CHANGE: 修正H2C配置问题
|
||||
|
||||
25w27a - 2025-04-07
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 加入`memLimit`指示gc
|
||||
- CHANGE: 加入`hlog`输出路径配置
|
||||
- CHANGE: 修正H2C配置问题
|
||||
|
||||
3.0.0 - 2025-04-04
|
||||
---
|
||||
- RELEASE: Next Gen; 下一个起点;
|
||||
- CHANGE: 使用HertZ框架重构, 提升性能
|
||||
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
|
||||
- CHANGE: 加入`Mino`主题对接选项
|
||||
- FIX: 修正部分日志输出问题
|
||||
- CHANGE: 移除gin残留
|
||||
- CHANGE: 移除无用传入参数, 调整代码结构
|
||||
- CHANGE: 改进cli
|
||||
- CHANGE: 改进`脚本嵌套加速处理器`
|
||||
- CHANGE&FIX: 使用`c.SetBodyStream`方式, 修正此前`chunked`传输中存在的诸多问题, 参看[HertZ Issues #1309](https://github.com/cloudwego/hertz/issues/1309)
|
||||
- PORT: 从v2移植`matcher`相关改进
|
||||
- CHANGE: 增加默认配置生成
|
||||
- CHANGE: 优化前端资源加载
|
||||
- CHANGE: 将`cfg`flag改为`c`以符合`POSIX`规范
|
||||
- CHANGE: 为`smart-git`添加`no-cache`标头
|
||||
|
||||
25w26a - 2025-04-03
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3的预发布版本,请勿在生产环境中使用;
|
||||
|
||||
2.6.3 - 2025-03-30
|
||||
---
|
||||
- FIX: 修正一些`git clone`行为异常
|
||||
|
||||
25w25a - 2025-03-30
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.6.3的预发布版本,请勿在生产环境中使用;
|
||||
- FIX: 修正一些`git clone`行为异常
|
||||
|
||||
e3.0.7 -2025-03-29
|
||||
---
|
||||
- CHANGE: 将`cfg`flag改为`c`以符合`POSIX`规范
|
||||
- CHANGE: 为`smart-git`添加`no-cache`标头
|
||||
|
||||
2.6.2 - 2025-03-29
|
||||
---
|
||||
- BACKPORT: 反向移植前端资源加载改进
|
||||
|
||||
e3.0.6 - 2025-03-28
|
||||
---
|
||||
- ATTENTION: 此版本是实验性的, 请确保了解这一点
|
||||
- FIX: 修正状态码相关问题(开发遗留所致)
|
||||
|
||||
e3.0.5 - 2025-03-28
|
||||
---
|
||||
- ATTENTION: 此版本是实验性的, 请确保了解这一点
|
||||
- CHANGE: 增加默认配置生成
|
||||
- CHANGE: 优化前端资源加载
|
||||
|
||||
2.6.1 - 2025-03-27
|
||||
---
|
||||
- CHANGE: 改进`matcher`组件
|
||||
- CHANGE: 加入优雅关闭
|
||||
|
||||
e3.0.3 - 2025-03.27
|
||||
---
|
||||
- ATTENTION: 此版本是实验性的, 请确保了解这一点
|
||||
- E-RELEASE: 修正过往问题, 还请各位多多测试反馈
|
||||
- PORT: 从v2移植`matcher`相关改进
|
||||
- CHANGE&FIX: 使用`c.SetBodyStream`方式, 修正此前`chunked`传输中存在的诸多问题, 参看[HertZ Issues #1309](https://github.com/cloudwego/hertz/issues/1309)
|
||||
|
||||
25w24a - 2025-03-27
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.6.1的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 改进`matcher`组件
|
||||
- CHANGE: 加入优雅关闭
|
||||
|
||||
e3.0.3rc2 - 2025-03-27
|
||||
---
|
||||
- ATTENTION: 此版本是实验性的, 请确保了解这一点
|
||||
- PRE-RELEASE: 此版本是v3.0.3的候选版本,请勿在生产环境中使用;
|
||||
- PORT: 从v2移植`matcher`相关改进
|
||||
|
||||
e3.0.3rc1 - 2025-03-26
|
||||
---
|
||||
- ATTENTION: 此版本是实验性的, 请确保了解这一点
|
||||
- PRE-RELEASE: 此版本是v3.0.3的候选版本,请勿在生产环境中使用;
|
||||
- CHANGE&FIX: 使用`c.SetBodyStream`方式, 修正此前`chunked`传输中存在的诸多问题, 参看[HertZ Issues #1309](https://github.com/cloudwego/hertz/issues/1309)
|
||||
|
||||
2.6.0 - 2025-03-22
|
||||
---
|
||||
- BACKPORT: 将v3的功能性改进反向移植
|
||||
|
||||
25w23a - 2025-03-22
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.6.0的预发布版本,请勿在生产环境中使用;
|
||||
- BACKPORT: 将v3的功能性改进反向移植
|
||||
|
||||
e3.0.2 - 2025-03-21
|
||||
---
|
||||
- ATTENTION: 此版本是实验性的, 请确保了解这一点
|
||||
- RELEASE: 在此表达对各位的歉意, v3迁移到HertZ带来了许多问题; 此版本完善v3的同时, 修正已知问题;
|
||||
- FIX: 使用等效`c.Writer()`, 回归v2.5.0 func以修正问题
|
||||
- CHANGE: 更新相关依赖
|
||||
|
||||
25w22a - 2025-03-21
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
|
||||
- FIX: 使用等效`c.Writer()`, 回归v2.5.0 func以修正问题
|
||||
|
||||
e3.0.1 - 2025-03-21
|
||||
---
|
||||
- ATTENTION: 此版本是实验性的, 请确保了解这一点
|
||||
- RELEASE: Next Step; 下一步; 完善v3的同时, 修正已知问题;
|
||||
- CHANGE: 改进cli
|
||||
- CHANGE: 重写`ProcessLinksAndWriteChunked`(脚本嵌套加速处理器), 修正已知问题的同时提高性能与效率
|
||||
- CHANGE: 完善`gitreq`部分
|
||||
- FIX: 修正日志输出格式问题
|
||||
- FIX: 使用更新的`hwriter`以修正相关问题
|
||||
|
||||
25w21e - 2025-03-21
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 重写`ProcessLinksAndWriteChunked`(脚本嵌套加速处理器), 修正已知问题的同时提高性能与效率
|
||||
|
||||
25w21d - 2025-03-21
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
|
||||
- FIX: 使用更新的`hwriter`以修正相关问题
|
||||
|
||||
25w21c - 2025-03-20
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
|
||||
- TEST: 测试新的`hwriter`
|
||||
|
||||
25w21b - 2025-03-20
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
|
||||
- FIX: 修正日志输出格式问题
|
||||
|
||||
25w21a - 2025-03-20
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.1的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 改进cli
|
||||
- CHANGE: 完善`gitreq`部分
|
||||
|
||||
e3.0.0 - 2025-03-19
|
||||
---
|
||||
- ATTENTION: 此版本是实验性的, 请确保了解这一点
|
||||
- RELEASE: Next Gen; 下一个起点;
|
||||
- CHANGE: 使用HertZ框架重构, 提升性能
|
||||
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
|
||||
- CHANGE: 加入`Mino`主题对接选项
|
||||
- FIX: 修正部分日志输出问题
|
||||
- CHANGE: 移除gin残留
|
||||
- CHANGE: 移除无用传入参数, 调整代码结构
|
||||
|
||||
25w20b - 2025-03-19
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 加入`Mino`主题对接选项
|
||||
- FIX: 修正部分日志输出问题
|
||||
- CHANGE: 移除gin残留
|
||||
- CHANGE: 移除无用传入参数, 调整代码结构
|
||||
|
||||
25w20a - 2025-03-18
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v3.0.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 使用HertZ重构
|
||||
- CHANGE: 前端在构建时加入, 新增`Design`,`Metro`,`Classic`主题
|
||||
|
||||
2.5.0 - 2025-03-17
|
||||
---
|
||||
- ADD: 加入脚本嵌套加速功能
|
||||
- CHANGE: 改进Auth模块
|
||||
|
||||
25w19a - 2025-03-16
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.5.0的预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 加入脚本嵌套加速功能
|
||||
- CHANGE: 改进Auth模块
|
||||
- CHANGE: 将handler模块化改进
|
||||
|
||||
2.4.2 - 2025-03-14
|
||||
---
|
||||
- CHANGE: 在GitClone Cache模式下, 相关请求会使用独立httpc client
|
||||
- CHANGE: 为GitClone Cache的独立httpc client增加ForceH2C选项
|
||||
- FIX: 修正GitClone Cache模式下的Url生成问题
|
||||
|
||||
25w18a - 2025-03-14
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.4.2的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 在GitClone Cache模式下, 相关请求会使用独立httpc client
|
||||
- CHANGE: 为GitClone Cache的独立httpc client增加ForceH2C选项
|
||||
- FIX: 修正GitClone Cache模式下的Url生成问题
|
||||
|
||||
2.4.1 - 2025-03-13
|
||||
---
|
||||
- CHANGE: 重构路由匹配
|
||||
- CHANGE: 更新相关依赖以修复错误
|
||||
|
||||
25w17a - 2025-03-13
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.4.1的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 重构路由匹配
|
||||
- CHANGE: 更新相关依赖以修复错误
|
||||
|
||||
2.4.0 - 2025-03-12
|
||||
---
|
||||
- ADD: 支持通过[Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git)实现Git Clone缓存
|
||||
- CHANGE: 使用更高性能的Buffer Pool 实现, 调用 github.com/WJQSERVER-STUDIO/go-utils/copyb
|
||||
- CHANGE: 改进路由匹配
|
||||
- CHANGE: 更新依赖
|
||||
- CHANGE: 改进前端
|
||||
|
||||
25w16d - 2025-03-12
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 使用更高性能的Buffer Pool 实现
|
||||
|
||||
25w16c
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 使用更高性能的Buffer Pool 实现
|
||||
- CHANGE: 改进路由匹配
|
||||
|
||||
25w16b
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 修改路由
|
||||
- CHANGE: 改进前端
|
||||
|
||||
25w16a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.4.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 变更CORS配置
|
||||
- ADD: 使用GO-GIT实现git smart http服务端和客户端
|
||||
- CHANGE: 更新依赖
|
||||
|
||||
2.3.1
|
||||
---
|
||||
- CHANGE: 改进`Pages`在`External`模式下的路由
|
||||
- CHANGE: 使用`H2C` bool 代替 `enableH2C` string (2.4.0 弃用 `enableH2C`)
|
||||
- CHANGE: 使用`Mode` string 代替`Pages`内的 `enable` bool (2.4.0 弃用 `enable`)
|
||||
|
||||
25w15a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.3.1的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 改进`Pages`在`External`模式下的路由
|
||||
- CHANGE: 使用`H2C` bool 代替 `enableH2C` string (2.4.0 弃用 `enableH2C`)
|
||||
- CHANGE: 使用`Mode` string 代替`Pages`内的 `enable` bool (2.4.0 弃用 `enable`)
|
||||
|
||||
2.3.0
|
||||
---
|
||||
- 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正式版发布;祝各位新春快乐!
|
||||
@@ -897,4 +1733,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实现了缓存与速率限制
|
||||
@@ -1 +1 @@
|
||||
25w10b
|
||||
4.2.0-rc.0
|
||||
410
LICENSE
410
LICENSE
@@ -1,107 +1,373 @@
|
||||
WJQserver Studio 开源许可证
|
||||
版本 1.2
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
版权所有 © WJQserver Studio 2024
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
定义
|
||||
许可:指在本许可证内定义的使用、复制、分发与修改的条款与要求。
|
||||
授权方:指拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体。
|
||||
您:指行使本许可授予的权限的个人或法律实体。
|
||||
开源与自由软件
|
||||
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
|
||||
本项目不等同于自由软件,使用权限受到本许可证条款的限制。
|
||||
强调版权所有,所有权利均由 WJQserver Studio 保留。
|
||||
许可证条款
|
||||
1. 使用权限
|
||||
1.1 您被授予在私人环境中自由使用本软件的权限。
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2 您可以在不修改关键声明的前提下进行商用。
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
2. 复制与分发
|
||||
2.1 您可以复制和分发本软件的原始版本,前提是必须保留所有版权声明和本许可证。
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
3. 修改权限
|
||||
3.1 您可以在非商业用途下修改本软件,前提是继承本许可证并保留原版权声明。
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
3.2 禁止在修改后进行商业用途。
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
4. 专利引用
|
||||
4.1 若项目被专利相关引用,必须保留来源声明。
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
4.2 若为商业场景,需按照商用处理。
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
5. 免责声明
|
||||
5.1 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
5.2 在任何情况下,授权方均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害负责,即使已被告知可能发生此类损害。
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
5.3 用户需根据当地法律对待本项目,确保遵守所有适用法规。
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
6. 许可证期限
|
||||
6.1 本许可证自2024年开始生效,有效期暂为无限。
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
6.2 项目所有方有权修改许可证相关条例而不另行通知。
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
条款修订
|
||||
7.1 授权方保留随时修改本许可证条款的权利,以便更好地适应法律和技术的发展。
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
7.2 修订后的条款将在发布时生效,继续使用本软件即表示接受修订后的条款。
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
其他
|
||||
8.1 本许可证不影响您作为最终用户的法定权利。
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
8.2 若本许可证的某些条款被认定为不可执行,其余条款仍然有效。
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
WJQserver Studio Open Source License
|
||||
Version 1.2
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
Copyright © WJQserver Studio 2024
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
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 Terms
|
||||
1. Usage Rights
|
||||
1.1 You are granted the right to use this software freely in a private environment.
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
1.2 You may use it commercially without modifying key statements.
|
||||
2.1. Grants
|
||||
|
||||
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.
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
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.
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
3.2 Modifications cannot be used commercially.
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
4. Patent References
|
||||
4.1 If the project is cited in patent-related contexts, the source statement must be retained.
|
||||
2.2. Effective Date
|
||||
|
||||
4.2 For commercial scenarios, it must be treated as a commercial use.
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
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.
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
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.
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
5.3 Users must comply with all applicable laws regarding this project.
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
6. License Duration
|
||||
6.1 This license is effective from 2024, with an indefinite duration.
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
6.2 The project owner reserves the right to modify the license terms without prior notice.
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
Amendments
|
||||
7.1 The licensor reserves the right to amend this license at any time to better adapt to legal and technological developments.
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
7.2 Revised terms become effective upon publication, and continued use of the software indicates acceptance of the revised terms.
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
Miscellaneous
|
||||
8.1 This license does not affect your statutory rights as an end user.
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
8.2 If any provision of this license is held to be unenforceable, the remaining provisions shall remain in effect.
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
199
LICENSE-WSL
Normal file
199
LICENSE-WSL
Normal file
@@ -0,0 +1,199 @@
|
||||
WJQserver Studio 开源许可证
|
||||
版本 v2.1
|
||||
|
||||
版权所有 © WJQserver Studio 2024
|
||||
|
||||
定义
|
||||
|
||||
* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。
|
||||
* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。
|
||||
* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。
|
||||
* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。
|
||||
* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。
|
||||
* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于:
|
||||
* 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。
|
||||
* 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。
|
||||
* 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。
|
||||
* 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。
|
||||
* 内部运营用途 (非营利组织): 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。
|
||||
|
||||
开源与自由软件
|
||||
|
||||
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
|
||||
本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。
|
||||
强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。
|
||||
|
||||
许可证条款
|
||||
|
||||
1. 使用权限
|
||||
|
||||
* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。
|
||||
|
||||
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
|
||||
|
||||
* 1.2.1 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
|
||||
|
||||
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
|
||||
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
|
||||
* 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。
|
||||
* 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。
|
||||
|
||||
您必须选择以下两种方式之一:
|
||||
|
||||
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
|
||||
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
|
||||
|
||||
* 1.3 保持声明: 公开发布服务时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
|
||||
|
||||
2. 复制与分发
|
||||
|
||||
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
|
||||
|
||||
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
|
||||
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
|
||||
|
||||
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.3 条(开源继承与互惠共享)的约束。
|
||||
|
||||
3. 修改权限
|
||||
|
||||
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
|
||||
|
||||
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.3 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
|
||||
|
||||
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。
|
||||
|
||||
4. 专利权
|
||||
|
||||
* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。
|
||||
|
||||
* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。
|
||||
|
||||
5. 免责声明
|
||||
|
||||
* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
|
||||
|
||||
* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。
|
||||
|
||||
* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。
|
||||
|
||||
6. 许可证期限与终止
|
||||
|
||||
* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。
|
||||
|
||||
* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。
|
||||
|
||||
* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。
|
||||
|
||||
7. 条款修订
|
||||
|
||||
* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。
|
||||
|
||||
* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。
|
||||
|
||||
8. 其他
|
||||
|
||||
* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。
|
||||
|
||||
* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。
|
||||
|
||||
* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。
|
||||
|
||||
WJQserver Studio Open Source License
|
||||
Version v2.1
|
||||
|
||||
Copyright © WJQserver Studio 2024
|
||||
|
||||
Definitions
|
||||
|
||||
* 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. Permissions for Use
|
||||
|
||||
* 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.
|
||||
|
||||
* 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:
|
||||
|
||||
* 1.2.1 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:
|
||||
|
||||
* 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.
|
||||
|
||||
You must choose one of the following two options:
|
||||
|
||||
* 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.
|
||||
|
||||
* 1.3 Maintain Statements: When publish services to public, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software.
|
||||
|
||||
2. Reproduction and Distribution
|
||||
|
||||
* 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:
|
||||
|
||||
* 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.
|
||||
|
||||
* 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.3 of this License (Open Source Inheritance and Reciprocal Sharing).
|
||||
|
||||
3. Modification Permissions
|
||||
|
||||
* 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.
|
||||
|
||||
* 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.3 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.
|
||||
|
||||
* 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.
|
||||
181
README.md
181
README.md
@@ -1,56 +1,68 @@
|
||||
# GHProxy
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
|
||||
|
||||
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
|
||||
|
||||
[DEMO](https://ghproxy.1888866.xyz)
|
||||
|
||||
[TG讨论群组](https://t.me/ghproxy_go)
|
||||
|
||||
[版本更新介绍](https://blog.wjqserver.com/categories/my-program/)
|
||||
一个基于Go的高性能Github资源代理程序, 同时支持Docker镜像代理与脚本嵌套加速等多种功能
|
||||
|
||||
## 项目说明
|
||||
|
||||
### 项目特点
|
||||
|
||||
- 基于Go语言实现,使用[Gin框架](https://github.com/gin-gonic/gin)
|
||||
- 支持Git clone,raw,realeases等文件拉取
|
||||
- 支持Docker部署
|
||||
- 支持速率限制
|
||||
- 支持用户鉴权
|
||||
- 支持自定义黑名单/白名单
|
||||
- 基于[WJQSERVER-STUDIO/golang-temp](https://github.com/WJQSERVER-STUDIO/golang-temp)模板构建,具有标准化的日志记录与构建流程
|
||||
- ⚡ **基于 Go 语言实现,跨平台的同时提供高并发性能**
|
||||
- 🌐 **使用自有[Touka框架](https://github.com/infinite-iroha/touka)作为 HTTP服务端框架**
|
||||
- 📡 **使用 [Touka-HTTPC](https://github.com/WJQSERVER-STUDIO/httpc) 作为 HTTP 客户端**
|
||||
- 📥 **支持 Git clone、raw、releases 等文件拉取**
|
||||
- 🐳 **支持反代Docker, GHCR等镜像仓库**
|
||||
- 🎨 **支持多个前端主题**
|
||||
- 🚫 **支持自定义黑名单/白名单**
|
||||
- 🗄️ **支持 Git Clone 缓存(配合 [Smart-Git](https://github.com/WJQSERVER-STUDIO/smart-git))**
|
||||
- 🐳 **支持自托管与Docker容器化部署**
|
||||
- ⚡ **支持速率限制**
|
||||
- ⚡ **支持带宽速率限制**
|
||||
- 🔒 **支持用户鉴权**
|
||||
- 🐚 **支持 shell 脚本多层嵌套加速**
|
||||
|
||||
### 项目开发过程
|
||||
### 项目相关
|
||||
|
||||
**本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能**
|
||||
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
|
||||
[DEMO](https://ghproxy.1888866.xyz)
|
||||
|
||||
- V2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
|
||||
- V1.0.0 迁移至本仓库,并再次重构内容实现
|
||||
- v0.2.0 重构项目实现
|
||||
[TG讨论群组](https://t.me/ghproxy_go)
|
||||
|
||||
### LICENSE
|
||||
[GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护
|
||||
|
||||
本项目使用WSL LICENSE Version1.2 (WJQSERVER STUDIO LICENSE Version1.2)
|
||||
[相关文章](https://blog.wjqserver.com/categories/my-program/)
|
||||
|
||||
在v1.0.0版本之前,本项目继承于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的APACHE2.0 LICENSE VERSION
|
||||
代理相关推广: [Thordata](https://www.thordata.com/?ls=github&lk=WJQserver),市面上最具性价比的代理服务商,便宜好用,来自全球195个国家城市的6000万IP,轮换住宅/原生ISP/无限量仅从$0.65/GB 起,新用户$1=5GB .联系客户可获得免费测试.
|
||||
|
||||
## 使用示例
|
||||
### 使用示例
|
||||
|
||||
```
|
||||
```bash
|
||||
# 下载文件
|
||||
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
|
||||
|
||||
# Docker(OCI) 代理
|
||||
docker pull gh.example.com/wjqserver/ghproxy
|
||||
docker pull gh.example.com/adguard/adguardhome
|
||||
|
||||
docker pull gh.example.com/docker.io/wjqserver/ghproxy
|
||||
docker pull gh.example.com/docker.io/adguard/adguardhome
|
||||
|
||||
docker pull gh.example.com/ghcr.io/openfaas/queue-worker
|
||||
```
|
||||
|
||||
## 部署说明
|
||||
|
||||
可参考文章: https://blog.wjqserver.com/post/ghproxy-deploy-with-smart-git/
|
||||
|
||||
### Docker部署
|
||||
|
||||
- Docker-cli
|
||||
@@ -71,91 +83,44 @@ 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
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 外部配置文件
|
||||
|
||||
本项目采用`config.toml`作为外部配置,默认配置如下
|
||||
使用Docker部署时,慎重修改`config.toml`,以免造成不必要的麻烦
|
||||
|
||||
```toml
|
||||
[server]
|
||||
host = "0.0.0.0" # 监听地址
|
||||
port = 8080 # 监听端口
|
||||
sizeLimit = 125 # 125MB
|
||||
enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off
|
||||
|
||||
[pages]
|
||||
enabled = false # 是否开启内置静态页面(Docker版本请关闭此项)
|
||||
staticPath = "/data/www" # 静态页面文件路径
|
||||
|
||||
[log]
|
||||
logFilePath = "/data/ghproxy/log/ghproxy.log" # 日志文件路径
|
||||
maxLogSize = 5 # MB 日志文件最大大小
|
||||
|
||||
[cors]
|
||||
enabled = true # 是否开启跨域
|
||||
|
||||
[auth]
|
||||
authMethod = "parameters" # 鉴权方式,支持parameters,header
|
||||
authToken = "token" # 用户鉴权Token
|
||||
enabled = false # 是否开启用户鉴权
|
||||
|
||||
[blacklist]
|
||||
blacklistFile = "/data/ghproxy/config/blacklist.json" # 黑名单文件路径
|
||||
enabled = false # 是否开启黑名单
|
||||
|
||||
[whitelist]
|
||||
enabled = false # 是否开启白名单
|
||||
whitelistFile = "/data/ghproxy/config/whitelist.json" # 白名单文件路径
|
||||
|
||||
[rateLimit]
|
||||
enabled = false # 是否开启速率限制
|
||||
rateMethod = "total" # "ip" or "total" 速率限制方式
|
||||
ratePerMinute = 180 # 每分钟限制请求数量
|
||||
burst = 5 # 突发请求数量
|
||||
```
|
||||
|
||||
### 黑名单配置
|
||||
|
||||
黑名单配置位于config/blacklist.json,格式如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"blacklist": [
|
||||
"test/test1",
|
||||
"example/repo2",
|
||||
"another/*"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 白名单配置
|
||||
|
||||
白名单配置位于config/whitelist.json,格式如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"whitelist": [
|
||||
"test/test1",
|
||||
"example/repo2",
|
||||
"another/*"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Caddy反代配置
|
||||
|
||||
```Caddyfile
|
||||
example.com {
|
||||
reverse_proxy {
|
||||
to 127.0.0.1:7210
|
||||
}
|
||||
encode zstd gzip
|
||||
}
|
||||
```
|
||||
参看[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md)
|
||||
|
||||
### 前端页面
|
||||
|
||||

|
||||

|
||||
参看[GHProxy-Frontend](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
|
||||
|
||||
## 文档
|
||||
|
||||
* [GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护
|
||||
|
||||
* [](https://deepwiki.com/WJQSERVER-STUDIO/ghproxy) 可供参考, AI生成存在幻觉, 不完全可靠, 请注意辨别
|
||||
|
||||
## LICENSE
|
||||
|
||||
v3.5.2开始, 本项目使用 [WJQserver Studio License 2.1](https://wjqserver-studio.github.io/LICENSE/LICENSE.html) 和 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 双重许可, 您可从中选择一个使用
|
||||
|
||||
前端位于单独仓库中, 且各个主题均存在各自的许可证, 本项目许可证并不包括前端
|
||||
|
||||
在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
|
||||
|
||||
## 赞助
|
||||
|
||||
如果您觉得本项目对您有帮助,欢迎赞助支持,您的赞助将用于Demo服务器开支及开发者时间成本支出,感谢您的支持!
|
||||
|
||||
USDT(TRC20): `TNfSYG6F2vkiibd6J6mhhHNWDgWgNdF5hN`
|
||||
|
||||
### 捐赠列表
|
||||
|
||||
| 赞助人 |金额|
|
||||
|--------|------|
|
||||
| starry | 8 USDT (TRC20) |
|
||||
|
||||
20
SECURITY.MD
20
SECURITY.MD
@@ -6,17 +6,29 @@
|
||||
|
||||
| 版本 | 是否支持 |
|
||||
| --- | --- |
|
||||
| v1.x.x | :white_check_mark: |
|
||||
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
|
||||
| v4.x.x | :white_check_mark: 当前最新版本序列 |
|
||||
| v3.x.x | :x: 这些版本已结束生命周期,不受支持 |
|
||||
| v2.x.x | :x: 这些版本已结束生命周期,不受支持 |
|
||||
| v1.x.x | :x: 这些版本已结束生命周期,不受支持 |
|
||||
| *-rc.x | :warning: 此为PRE-RELEASE预发布版本,用于测试问题 |
|
||||
| *-beta.x | :warning: 此为Beta测试版本,用于开发与测试,可能存在未知的问题 |
|
||||
| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
|
||||
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
|
||||
| v0.x.x | :x: 这些版本不再受支持 |
|
||||
|
||||
### 用户须知
|
||||
|
||||
本项目为开源项目,开发者不对使用本项目造成的任何损失或问题承担责任。用户需自行评估并承担使用本项目的风险。
|
||||
|
||||
使用本项目,请遵循 **[WSL (WJQSERVER-STUDIO LICENSE)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。
|
||||
使用本项目,请遵循 **[WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议 或 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 。
|
||||
|
||||
本项目所有文件均受到 WSL (WJQSERVER-STUDIO LICENSE) 协议保护,任何人不得在任何情况下以非 WSL (WJQSERVER-STUDIO LICENSE) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。
|
||||
#### 选择WSL 2.1时
|
||||
|
||||
本项目所有文件均受到 WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1) 协议保护,任何人不得在任何情况下以非 WSL 2.1 (WJQSERVER-STUDIO LICENSE 2.1) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。
|
||||
|
||||
#### 选择MPL 2.0时
|
||||
|
||||
本项目内文件除特别版权标注声明外, 均受到 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/) 授权保护, 具体条款参看 [Mozilla Public License Version 2.0](https://mozilla.org/MPL/2.0/)
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
|
||||
170
api/api.go
170
api/api.go
@@ -1,127 +1,133 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"ghproxy/config"
|
||||
"ghproxy/middleware/nocache"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
var (
|
||||
router *gin.Engine
|
||||
cfg *config.Config
|
||||
)
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
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", NoCacheMiddleware())
|
||||
func InitHandleRouter(cfg *config.Config, r *touka.Engine, version string) {
|
||||
apiRouter := r.Group("/api", nocache.NoCacheMiddleware())
|
||||
{
|
||||
apiRouter.GET("/size_limit", func(c *gin.Context) {
|
||||
apiRouter.GET("/size_limit", func(c *touka.Context) {
|
||||
SizeLimitHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/whitelist/status", func(c *gin.Context) {
|
||||
WhiteListStatusHandler(c, cfg)
|
||||
apiRouter.GET("/whitelist/status", func(c *touka.Context) {
|
||||
WhiteListStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/blacklist/status", func(c *gin.Context) {
|
||||
BlackListStatusHandler(c, cfg)
|
||||
apiRouter.GET("/blacklist/status", func(c *touka.Context) {
|
||||
BlackListStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/cors/status", func(c *gin.Context) {
|
||||
CorsStatusHandler(c, cfg)
|
||||
apiRouter.GET("/cors/status", func(c *touka.Context) {
|
||||
CorsStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/healthcheck", func(c *gin.Context) {
|
||||
apiRouter.GET("/healthcheck", func(c *touka.Context) {
|
||||
HealthcheckHandler(c)
|
||||
})
|
||||
apiRouter.GET("/version", func(c *gin.Context) {
|
||||
apiRouter.GET("/ok", func(c *touka.Context) {
|
||||
HealthcheckHandler(c)
|
||||
})
|
||||
apiRouter.GET("/version", func(c *touka.Context) {
|
||||
VersionHandler(c, version)
|
||||
})
|
||||
apiRouter.GET("/rate_limit/status", func(c *gin.Context) {
|
||||
RateLimitStatusHandler(c, cfg)
|
||||
apiRouter.GET("/rate_limit/status", func(c *touka.Context) {
|
||||
RateLimitStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/rate_limit/limit", func(c *gin.Context) {
|
||||
RateLimitLimitHandler(c, cfg)
|
||||
apiRouter.GET("/rate_limit/limit", func(c *touka.Context) {
|
||||
RateLimitLimitHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/smartgit/status", func(c *touka.Context) {
|
||||
SmartGitStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/shell_nest/status", func(c *touka.Context) {
|
||||
shellNestStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/oci_proxy/status", func(c *touka.Context) {
|
||||
ociProxyStatusHandler(cfg, c)
|
||||
})
|
||||
}
|
||||
logInfo("API router Init success")
|
||||
}
|
||||
|
||||
func SizeLimitHandler(cfg *config.Config, c *gin.Context) {
|
||||
func SizeLimitHandler(cfg *config.Config, c *touka.Context) {
|
||||
sizeLimit := cfg.Server.SizeLimit
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"MaxResponseBodySize": sizeLimit,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func WhiteListStatusHandler(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{}{
|
||||
func WhiteListStatusHandler(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Whitelist": cfg.Whitelist.Enabled,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func BlackListStatusHandler(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{}{
|
||||
func BlackListStatusHandler(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Blacklist": cfg.Blacklist.Enabled,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
func CorsStatusHandler(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Cors": cfg.Server.Cors,
|
||||
}))
|
||||
}
|
||||
|
||||
func HealthcheckHandler(c *gin.Context) {
|
||||
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{}{
|
||||
func HealthcheckHandler(c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Status": "OK",
|
||||
})
|
||||
"Repo": "WJQSERVER-STUDIO/GHProxy",
|
||||
"Author": "WJQSERVER-STUDIO",
|
||||
}))
|
||||
}
|
||||
|
||||
func VersionHandler(c *gin.Context, version string) {
|
||||
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{}{
|
||||
func VersionHandler(c *touka.Context, version string) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Version": version,
|
||||
})
|
||||
"Repo": "WJQSERVER-STUDIO/GHProxy",
|
||||
"Author": "WJQSERVER-STUDIO",
|
||||
}))
|
||||
}
|
||||
|
||||
func RateLimitStatusHandler(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{}{
|
||||
func RateLimitStatusHandler(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"RateLimit": cfg.RateLimit.Enabled,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func RateLimitLimitHandler(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{}{
|
||||
func RateLimitLimitHandler(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"RatePerMinute": cfg.RateLimit.RatePerMinute,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func SmartGitStatusHandler(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"enabled": cfg.GitClone.Mode == "cache",
|
||||
}))
|
||||
}
|
||||
|
||||
func shellNestStatusHandler(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"enabled": cfg.Shell.Editor,
|
||||
}))
|
||||
}
|
||||
|
||||
func ociProxyStatusHandler(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"enabled": cfg.Docker.Enabled,
|
||||
"target": cfg.Docker.Target,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -4,27 +4,29 @@ import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
|
||||
func AuthHeaderHandler(c *touka.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)
|
||||
var authToken string
|
||||
if cfg.Auth.Key != "" {
|
||||
authToken = string(c.Request.Header.Get(cfg.Auth.Key))
|
||||
|
||||
} else {
|
||||
authToken = string(c.Request.Header.Get("GH-Auth"))
|
||||
}
|
||||
if authToken == "" {
|
||||
err := "Auth Header == nil"
|
||||
return false, err
|
||||
return false, fmt.Errorf("Auth token not found")
|
||||
}
|
||||
|
||||
isValid = authToken == cfg.Auth.AuthToken
|
||||
isValid = authToken == cfg.Auth.Token
|
||||
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
|
||||
}
|
||||
|
||||
@@ -4,28 +4,29 @@ import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
|
||||
func AuthParametersHandler(c *touka.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)
|
||||
var authToken string
|
||||
if cfg.Auth.Key != "" {
|
||||
authToken = c.Query(cfg.Auth.Key)
|
||||
} else {
|
||||
authToken = c.Query("auth_token")
|
||||
}
|
||||
|
||||
if authToken == "" {
|
||||
err := "Auth token == nil"
|
||||
return false, err
|
||||
return false, fmt.Errorf("Auth token not found")
|
||||
}
|
||||
|
||||
isValid = authToken == cfg.Auth.AuthToken
|
||||
isValid = authToken == cfg.Auth.Token
|
||||
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
|
||||
}
|
||||
|
||||
41
auth/auth.go
41
auth/auth.go
@@ -1,41 +1,40 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
func Init(cfg *config.Config) {
|
||||
func ListInit(cfg *config.Config) error {
|
||||
if cfg.Blacklist.Enabled {
|
||||
LoadBlacklist(cfg)
|
||||
err := InitBlacklist(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cfg.Whitelist.Enabled {
|
||||
LoadWhitelist(cfg)
|
||||
err := InitWhitelist(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
logInfo("Auth Init")
|
||||
return nil
|
||||
}
|
||||
|
||||
func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
|
||||
if cfg.Auth.AuthMethod == "parameters" {
|
||||
func AuthHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) {
|
||||
if cfg.Auth.Method == "parameters" {
|
||||
isValid, err = AuthParametersHandler(c, cfg)
|
||||
return isValid, err
|
||||
} else if cfg.Auth.AuthMethod == "header" {
|
||||
} else if cfg.Auth.Method == "header" {
|
||||
isValid, err = AuthHeaderHandler(c, cfg)
|
||||
return isValid, err
|
||||
} else if cfg.Auth.AuthMethod == "" {
|
||||
logWarning("Auth method not set")
|
||||
return true, ""
|
||||
} else if cfg.Auth.Method == "" {
|
||||
c.Errorf("Auth method not set")
|
||||
return true, nil
|
||||
} else {
|
||||
logWarning("Auth method not supported")
|
||||
return false, "Auth method not supported"
|
||||
c.Errorf("Auth method not supported %s", cfg.Auth.Method)
|
||||
return false, fmt.Errorf("%s", fmt.Sprintf("Auth method %s not supported", cfg.Auth.Method))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,91 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
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, ""
|
||||
}
|
||||
|
||||
60
auth/ipfilter.go
Normal file
60
auth/ipfilter.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"os"
|
||||
|
||||
"github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
)
|
||||
|
||||
func ReadIPFilterList(cfg *config.Config) (whitelist []string, blacklist []string, err error) {
|
||||
if cfg.IPFilter.IPFilterFile == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// 检查文件是否存在, 不存在则创建空json
|
||||
if _, err := os.Stat(cfg.IPFilter.IPFilterFile); os.IsNotExist(err) {
|
||||
if err := CreateEmptyIPFilterFile(cfg.IPFilter.IPFilterFile); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create empty IP filter file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cfg.IPFilter.IPFilterFile)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read IP filter file: %w", err)
|
||||
}
|
||||
|
||||
var ipFilterData struct {
|
||||
AllowList []string `json:"allow"`
|
||||
BlockList []string `json:"block"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &ipFilterData); err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid IP filter file format: %w", err)
|
||||
}
|
||||
|
||||
return ipFilterData.AllowList, ipFilterData.BlockList, nil
|
||||
}
|
||||
|
||||
// 创建空列表json
|
||||
func CreateEmptyIPFilterFile(filePath string) error {
|
||||
emptyData := struct {
|
||||
AllowList []string `json:"allow"`
|
||||
BlockList []string `json:"block"`
|
||||
}{
|
||||
AllowList: []string{},
|
||||
BlockList: []string{},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(emptyData, jsontext.Multiline(true), jsontext.WithIndent(" "))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal empty IP filter data: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filePath, jsonData, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write empty IP filter file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,59 +1,92 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-json-experiment/json"
|
||||
)
|
||||
|
||||
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, ""
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"blacklist": [
|
||||
"black/list",
|
||||
"test/test1",
|
||||
"example/*"
|
||||
"eviluser",
|
||||
"spamuser/bad-repo",
|
||||
"malwareuser/*"
|
||||
]
|
||||
}
|
||||
290
config/config.go
290
config/config.go
@@ -1,47 +1,122 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Pages PagesConfig
|
||||
Log LogConfig
|
||||
CORS CORSConfig
|
||||
Auth AuthConfig
|
||||
Blacklist BlacklistConfig
|
||||
Whitelist WhitelistConfig
|
||||
RateLimit RateLimitConfig
|
||||
Server ServerConfig `toml:"server"`
|
||||
Httpc HttpcConfig `toml:"httpc"`
|
||||
GitClone GitCloneConfig `toml:"gitclone"`
|
||||
Shell ShellConfig `toml:"shell"`
|
||||
Pages PagesConfig `toml:"pages"`
|
||||
Log LogConfig `toml:"log"`
|
||||
Auth AuthConfig `toml:"auth"`
|
||||
Blacklist BlacklistConfig `toml:"blacklist"`
|
||||
Whitelist WhitelistConfig `toml:"whitelist"`
|
||||
IPFilter IPFilterConfig `toml:"ipFilter"`
|
||||
RateLimit RateLimitConfig `toml:"rateLimit"`
|
||||
Outbound OutboundConfig `toml:"outbound"`
|
||||
Docker DockerConfig `toml:"docker"`
|
||||
}
|
||||
|
||||
/*
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
sizeLimit = 125 # MB
|
||||
memLimit = 0 # MB
|
||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||
debug = false
|
||||
*/
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `toml:"port"`
|
||||
Host string `toml:"host"`
|
||||
SizeLimit int `toml:"sizeLimit"`
|
||||
EnableH2C string `toml:"enableH2C"`
|
||||
MemLimit int64 `toml:"memLimit"`
|
||||
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
|
||||
useCustomRawHeaders = false
|
||||
*/
|
||||
type HttpcConfig struct {
|
||||
Mode string `toml:"mode"`
|
||||
MaxIdleConns int `toml:"maxIdleConns"`
|
||||
MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"`
|
||||
MaxConnsPerHost int `toml:"maxConnsPerHost"`
|
||||
UseCustomRawHeaders bool `toml:"useCustomRawHeaders"`
|
||||
}
|
||||
|
||||
/*
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
//cacheTimeout = 10
|
||||
ForceH2C = true
|
||||
*/
|
||||
type GitCloneConfig struct {
|
||||
Mode string `toml:"mode"`
|
||||
SmartGitAddr string `toml:"smartGitAddr"`
|
||||
//CacheTimeout int `toml:"cacheTimeout"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type CORSConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
MaxLogSize int64 `toml:"maxLogSize"`
|
||||
Level string `toml:"level"`
|
||||
}
|
||||
|
||||
/*
|
||||
[auth]
|
||||
Method = "parameters" # "header" or "parameters"
|
||||
Key = ""
|
||||
Token = "token"
|
||||
enabled = false
|
||||
passThrough = false
|
||||
ForceAllowApi = false
|
||||
ForceAllowApiPassList = false
|
||||
*/
|
||||
type AuthConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
AuthMethod string `toml:"authMethod"`
|
||||
AuthToken string `toml:"authToken"`
|
||||
PassThrough bool `toml:"passThrough"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
Method string `toml:"method"`
|
||||
Key string `toml:"key"`
|
||||
Token string `toml:"token"`
|
||||
PassThrough bool `toml:"passThrough"`
|
||||
ForceAllowApi bool `toml:"ForceAllowApi"`
|
||||
ForceAllowApiPassList bool `toml:"ForceAllowApiPassList"`
|
||||
}
|
||||
|
||||
type BlacklistConfig struct {
|
||||
@@ -54,18 +129,187 @@ type WhitelistConfig struct {
|
||||
WhitelistFile string `toml:"whitelistFile"`
|
||||
}
|
||||
|
||||
type IPFilterConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
EnableAllowList bool `toml:"enableAllowList"`
|
||||
EnableBlockList bool `toml:"enableBlockList"`
|
||||
IPFilterFile string `toml:"ipFilterFile"`
|
||||
}
|
||||
|
||||
/*
|
||||
[rateLimit]
|
||||
enabled = false
|
||||
ratePerMinute = 100
|
||||
burst = 10
|
||||
|
||||
[rateLimit.bandwidthLimit]
|
||||
enabled = false
|
||||
totalLimit = "100mbps"
|
||||
totalBurst = "100mbps"
|
||||
singleLimit = "10mbps"
|
||||
singleBurst = "10mbps"
|
||||
*/
|
||||
|
||||
type RateLimitConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
RateMethod string `toml:"rateMethod"`
|
||||
RatePerMinute int `toml:"ratePerMinute"`
|
||||
Burst int `toml:"burst"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
RatePerMinute int `toml:"ratePerMinute"`
|
||||
Burst int `toml:"burst"`
|
||||
BandwidthLimit BandwidthLimitConfig
|
||||
}
|
||||
|
||||
type BandwidthLimitConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
TotalLimit string `toml:"totalLimit"`
|
||||
TotalBurst string `toml:"totalBurst"`
|
||||
SingleLimit string `toml:"singleLimit"`
|
||||
SingleBurst string `toml:"singleBurst"`
|
||||
}
|
||||
|
||||
/*
|
||||
[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"`
|
||||
}
|
||||
|
||||
/*
|
||||
[docker]
|
||||
enabled = false
|
||||
target = "ghcr" # ghcr/dockerhub
|
||||
auth = false
|
||||
[docker.credentials]
|
||||
user1 = "testpass"
|
||||
test = "test123"
|
||||
*/
|
||||
type DockerConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Target string `toml:"target"`
|
||||
Auth bool `toml:"auth"`
|
||||
Credentials map[string]string `toml:"credentials"`
|
||||
AuthPassThrough bool `toml:"authPassThrough"`
|
||||
}
|
||||
|
||||
// LoadConfig 从 TOML 配置文件加载配置
|
||||
func LoadConfig(filePath string) (*Config, error) {
|
||||
if !FileExists(filePath) {
|
||||
// 楔入配置文件
|
||||
err := DefaultConfig().WriteConfig(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DefaultConfig(), nil
|
||||
}
|
||||
|
||||
var config Config
|
||||
if _, err := toml.DecodeFile(filePath, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// 写入配置文件
|
||||
func (c *Config) WriteConfig(filePath string) error {
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := toml.NewEncoder(file)
|
||||
return encoder.Encode(c)
|
||||
}
|
||||
|
||||
// 检测文件是否存在
|
||||
func FileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// 默认配置结构体
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: 8080,
|
||||
Host: "0.0.0.0",
|
||||
SizeLimit: 125,
|
||||
MemLimit: 0,
|
||||
Cors: "*",
|
||||
Debug: false,
|
||||
},
|
||||
Httpc: HttpcConfig{
|
||||
Mode: "auto",
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 60,
|
||||
MaxConnsPerHost: 0,
|
||||
},
|
||||
GitClone: GitCloneConfig{
|
||||
Mode: "bypass",
|
||||
SmartGitAddr: "http://127.0.0.1:8080",
|
||||
ForceH2C: false,
|
||||
},
|
||||
Shell: ShellConfig{
|
||||
Editor: false,
|
||||
RewriteAPI: false,
|
||||
},
|
||||
Pages: PagesConfig{
|
||||
Mode: "internal",
|
||||
Theme: "hub",
|
||||
StaticDir: "/data/www",
|
||||
},
|
||||
Log: LogConfig{
|
||||
LogFilePath: "/data/ghproxy/log/ghproxy.log",
|
||||
MaxLogSize: 10,
|
||||
Level: "info",
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
Enabled: false,
|
||||
Method: "parameters",
|
||||
Key: "",
|
||||
Token: "token",
|
||||
PassThrough: false,
|
||||
ForceAllowApi: false,
|
||||
ForceAllowApiPassList: false,
|
||||
},
|
||||
Blacklist: BlacklistConfig{
|
||||
Enabled: false,
|
||||
BlacklistFile: "/data/ghproxy/config/blacklist.json",
|
||||
},
|
||||
Whitelist: WhitelistConfig{
|
||||
Enabled: false,
|
||||
WhitelistFile: "/data/ghproxy/config/whitelist.json",
|
||||
},
|
||||
IPFilter: IPFilterConfig{
|
||||
Enabled: false,
|
||||
IPFilterFile: "/data/ghproxy/config/ipfilter.json",
|
||||
EnableAllowList: false,
|
||||
EnableBlockList: false,
|
||||
},
|
||||
RateLimit: RateLimitConfig{
|
||||
Enabled: false,
|
||||
RatePerMinute: 100,
|
||||
Burst: 10,
|
||||
BandwidthLimit: BandwidthLimitConfig{
|
||||
Enabled: false,
|
||||
TotalLimit: "100mbps",
|
||||
TotalBurst: "100mbps",
|
||||
SingleLimit: "10mbps",
|
||||
SingleBurst: "10mbps",
|
||||
},
|
||||
},
|
||||
Outbound: OutboundConfig{
|
||||
Enabled: false,
|
||||
Url: "socks5://127.0.0.1:1080",
|
||||
},
|
||||
Docker: DockerConfig{
|
||||
Enabled: false,
|
||||
Target: "dockerhub",
|
||||
Auth: false,
|
||||
Credentials: map[string]string{
|
||||
"testpass": "test123",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,44 @@
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
sizeLimit = 125 # MB
|
||||
enableH2C = "on" # "on" or "off"
|
||||
memLimit = 0 # MB
|
||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||
debug = false
|
||||
|
||||
[httpc]
|
||||
mode = "auto" # "auto" or "advanced"
|
||||
maxIdleConns = 100 # only for advanced mode
|
||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||
maxConnsPerHost = 0 # only for advanced mode
|
||||
useCustomRawHeaders = false
|
||||
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
ForceH2C = false
|
||||
|
||||
[shell]
|
||||
editor = false
|
||||
rewriteAPI = false
|
||||
|
||||
[pages]
|
||||
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" # debug, info, warn, error, none
|
||||
|
||||
[auth]
|
||||
authMethod = "parameters" # "header" or "parameters"
|
||||
authToken = "token"
|
||||
method = "parameters" # "header" or "parameters"
|
||||
token = "token"
|
||||
key = ""
|
||||
enabled = false
|
||||
passThrough = false
|
||||
ForceAllowApi = false
|
||||
ForceAllowApiPassList = false
|
||||
|
||||
[blacklist]
|
||||
blacklistFile = "/data/ghproxy/config/blacklist.json"
|
||||
@@ -30,8 +49,32 @@ enabled = false
|
||||
enabled = false
|
||||
whitelistFile = "/data/ghproxy/config/whitelist.json"
|
||||
|
||||
[ipFilter]
|
||||
enabled = false
|
||||
enableAllowList = false
|
||||
enableBlockList = false
|
||||
ipFilterFile = "/data/ghproxy/config/ipfilter.json"
|
||||
|
||||
[rateLimit]
|
||||
enabled = false
|
||||
rateMethod = "total" # "ip" or "total"
|
||||
ratePerMinute = 180
|
||||
burst = 5
|
||||
|
||||
[rateLimit.bandwidthLimit]
|
||||
enabled = false
|
||||
totalLimit = "100mbps"
|
||||
totalBurst = "100mbps"
|
||||
singleLimit = "10mbps"
|
||||
singleBurst = "10mbps"
|
||||
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
|
||||
[docker]
|
||||
enabled = false
|
||||
target = "dockerhub" # ghcr/dockerhub/ custom
|
||||
auth = false
|
||||
[docker.credentials]
|
||||
user1 = "testpass"
|
||||
test = "test123"
|
||||
11
config/ipfilter.json
Normal file
11
config/ipfilter.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"allow": [
|
||||
"127.0.0.1",
|
||||
"192.168.1.0/24",
|
||||
"::1"
|
||||
],
|
||||
"block": [
|
||||
"10.0.0.0/8",
|
||||
"192.168.1.0/24"
|
||||
]
|
||||
}
|
||||
@@ -1,26 +1,45 @@
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8080
|
||||
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||
sizeLimit = 125 # MB
|
||||
enableH2C = false
|
||||
memLimit = 0 # MB
|
||||
H2C = true
|
||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||
debug = false
|
||||
|
||||
[httpc]
|
||||
mode = "auto" # "auto" or "advanced"
|
||||
maxIdleConns = 100 # only for advanced mode
|
||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||
maxConnsPerHost = 0 # only for advanced mode
|
||||
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
ForceH2C = false
|
||||
|
||||
[shell]
|
||||
editor = false
|
||||
rewriteAPI = false
|
||||
|
||||
[pages]
|
||||
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
|
||||
hertzLogPath = "/usr/local/ghproxy/log/hertz.log"
|
||||
|
||||
[auth]
|
||||
authMethod = "parameters" # "header" or "parameters"
|
||||
authToken = "token"
|
||||
enabled = false
|
||||
passThrough = false
|
||||
ForceAllowApi = false
|
||||
|
||||
[blacklist]
|
||||
blacklistFile = "/usr/local/ghproxy/config/blacklist.json"
|
||||
@@ -35,3 +54,11 @@ enabled = false
|
||||
rateMethod = "total" # "ip" or "total"
|
||||
ratePerMinute = 180
|
||||
burst = 5
|
||||
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
|
||||
[docker]
|
||||
enabled = false
|
||||
target = "ghcr" # ghcr/dockerhub
|
||||
@@ -3,7 +3,7 @@ Description=Github Proxy Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c '/usr/local/ghproxy/ghproxy -cfg /usr/local/ghproxy/config/config.toml > /usr/local/ghproxy/log/run.log 2>&1'
|
||||
ExecStart=/bin/bash -c '/usr/local/ghproxy/ghproxy -c /usr/local/ghproxy/config/config.toml > /usr/local/ghproxy/log/run.log 2>&1'
|
||||
WorkingDirectory=/usr/local/ghproxy
|
||||
Restart=always
|
||||
User=root
|
||||
|
||||
@@ -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
|
||||
@@ -100,18 +80,18 @@ 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
|
||||
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 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/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
|
||||
|
||||
|
||||
# 下载配置文件
|
||||
@@ -120,7 +100,7 @@ if [ -f ${ghproxy_dir}/config/config.toml ]; then
|
||||
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
|
||||
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/config.toml
|
||||
fi
|
||||
|
||||
# 替换 port = 8080
|
||||
@@ -133,9 +113,27 @@ sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelist
|
||||
|
||||
# 下载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
|
||||
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/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 -c $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
|
||||
|
||||
@@ -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 -c $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
|
||||
|
||||
@@ -4,8 +4,7 @@ services:
|
||||
image: 'wjqserver/ghproxy:latest'
|
||||
restart: always
|
||||
volumes:
|
||||
- './ghproxy/log/run:/data/ghproxy/log'
|
||||
- './ghproxy/log/caddy:/data/caddy/log'
|
||||
- './ghproxy/log:/data/ghproxy/log'
|
||||
- './ghproxy/config:/data/ghproxy/config'
|
||||
ports:
|
||||
- '7210:8080'
|
||||
|
||||
@@ -3,6 +3,7 @@ FROM alpine:latest AS builder
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG BRANCH=dev
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
@@ -15,37 +16,36 @@ 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) && \
|
||||
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/DEV-VERSION) && \
|
||||
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
|
||||
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
|
||||
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
|
||||
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/dev/docker/dockerfile/dev/init.sh
|
||||
|
||||
# 拉取配置
|
||||
#RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/dev/caddyfile/dev/Caddyfile
|
||||
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/config.toml
|
||||
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/blacklist.json
|
||||
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/dev/config/whitelist.json
|
||||
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/config.toml
|
||||
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/blacklist.json
|
||||
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/whitelist.json
|
||||
|
||||
# 权限
|
||||
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
|
||||
RUN chmod +x /usr/local/bin/init.sh
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG BRANCH=v3
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=builder /data/www /data/www
|
||||
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
|
||||
COPY --from=builder /usr/local/bin/init.sh /usr/local/bin/init.sh
|
||||
|
||||
# 权限
|
||||
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
|
||||
RUN chmod +x /usr/local/bin/init.sh
|
||||
|
||||
CMD ["/usr/local/bin/init.sh"]
|
||||
CMD ["/data/ghproxy/ghproxy"]
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
APPLICATION=ghproxy
|
||||
|
||||
if [ ! -f /data/${APPLICATION}/config/blacklist.json ]; then
|
||||
cp /data/${APPLICATION}/blacklist.json /data/${APPLICATION}/config/blacklist.json
|
||||
fi
|
||||
|
||||
if [ ! -f /data/${APPLICATION}/config/whitelist.json ]; then
|
||||
cp /data/${APPLICATION}/whitelist.json /data/${APPLICATION}/config/whitelist.json
|
||||
fi
|
||||
|
||||
if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
|
||||
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
|
||||
fi
|
||||
|
||||
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1
|
||||
@@ -2,6 +2,7 @@ FROM alpine:latest AS builder
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG BRANCH=main
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -15,39 +16,38 @@ 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) && \
|
||||
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/VERSION) && \
|
||||
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
|
||||
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
|
||||
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
|
||||
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/release/init.sh
|
||||
|
||||
# 拉取配置
|
||||
#RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/release/Caddyfile
|
||||
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.toml
|
||||
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/blacklist.json
|
||||
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/whitelist.json
|
||||
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/config.toml
|
||||
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/blacklist.json
|
||||
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/${BRANCH}/config/whitelist.json
|
||||
|
||||
# 权限
|
||||
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
|
||||
RUN chmod +x /usr/local/bin/init.sh
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG BRANCH=v3
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=builder /data/www /data/www
|
||||
#COPY --from=builder /data/caddy /data/caddy
|
||||
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
|
||||
COPY --from=builder /usr/local/bin/init.sh /usr/local/bin/init.sh
|
||||
|
||||
# 权限
|
||||
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
|
||||
RUN chmod +x /usr/local/bin/init.sh
|
||||
|
||||
CMD ["/usr/local/bin/init.sh"]
|
||||
CMD ["/data/ghproxy/ghproxy"]
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
APPLICATION=ghproxy
|
||||
|
||||
if [ ! -f /data/${APPLICATION}/config/blacklist.json ]; then
|
||||
cp /data/${APPLICATION}/blacklist.json /data/${APPLICATION}/config/blacklist.json
|
||||
fi
|
||||
|
||||
if [ ! -f /data/${APPLICATION}/config/whitelist.json ]; then
|
||||
cp /data/${APPLICATION}/whitelist.json /data/${APPLICATION}/config/whitelist.json
|
||||
fi
|
||||
|
||||
if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
|
||||
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
|
||||
fi
|
||||
|
||||
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1
|
||||
52
go.mod
52
go.mod
@@ -1,39 +1,29 @@
|
||||
module ghproxy
|
||||
|
||||
go 1.23.5
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.1
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
golang.org/x/time v0.9.0
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/WJQSERVER-STUDIO/httpc v0.8.1
|
||||
golang.org/x/net v0.42.0
|
||||
golang.org/x/time v0.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.12.8 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.3 // 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/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/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.4 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2
|
||||
github.com/fenthope/bauth v0.0.1
|
||||
github.com/fenthope/ikumi v0.0.2
|
||||
github.com/fenthope/ipfilter v0.0.1
|
||||
github.com/fenthope/reco v0.0.3
|
||||
github.com/fenthope/record v0.0.3
|
||||
github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/infinite-iroha/touka v0.3.3
|
||||
github.com/wjqserver/modembed v0.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6 // indirect
|
||||
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
)
|
||||
|
||||
128
go.sum
128
go.sum
@@ -1,94 +1,34 @@
|
||||
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.1 h1:YS3q54SroxQpEM7c12ZKjLNAaSq++bNpxTujs9cTZ9c=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.1/go.mod h1:oW884JCCPDU6c906LI0uKXndWLiRvjb9LkGYC2cqRO8=
|
||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
|
||||
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
|
||||
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/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
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/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/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/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=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/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=
|
||||
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=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
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.6 h1:/50VJYXd6jcu+p5BnEBDyiX0nAyGxas1W3DCnrYMxMY=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.6/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbekZgxOuvVwiMfyM=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2 h1:8bBkKk6E2Zr+I5szL7gyc5f0DK8N9agIJCpM1Cqw2NE=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/limitreader v0.0.2/go.mod h1:yPX8xuZH+py7eLJwOYj3VVI/4/Yuy5+x8Mhq8qezcPg=
|
||||
github.com/WJQSERVER-STUDIO/httpc v0.8.1 h1:/eG8aYKL3WfQILIRbG+cbzQjPkNHEPTqfGUdQS5rtI4=
|
||||
github.com/WJQSERVER-STUDIO/httpc v0.8.1/go.mod h1:mxXBf2hqbQGNHkVy/7wfU7Xi2s09MyZpbY2hyR+4uD4=
|
||||
github.com/fenthope/bauth v0.0.1 h1:+4UIQshGx3mYD4L3f2S4MLZOi5PWU7fU5GK3wsZvwzE=
|
||||
github.com/fenthope/bauth v0.0.1/go.mod h1:1fveTpgfR1p+WXQ8MXm9BfBCeNYi55j23jxCOGOvBSA=
|
||||
github.com/fenthope/ikumi v0.0.2 h1:5oaSTf/Msp7M2O3o/X20omKWEQbFhX4KV0CVF21oCdk=
|
||||
github.com/fenthope/ikumi v0.0.2/go.mod h1:IYbxzOGndZv/yRrbVMyV6dxh06X2wXCbfxrTRM1IruU=
|
||||
github.com/fenthope/ipfilter v0.0.1 h1:HrYAyixCMvsDAz36GRyFfyCNtrgYwzrhMcY0XV7fGcM=
|
||||
github.com/fenthope/ipfilter v0.0.1/go.mod h1:QfY0GrpG0D82HROgdH4c9eog4js42ghLIfl/iM4MvvY=
|
||||
github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4=
|
||||
github.com/fenthope/reco v0.0.3/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y=
|
||||
github.com/fenthope/record v0.0.3 h1:v5urgs5LAkLMlljAT/MjW8fWuRHXPnAraTem5ui7rm4=
|
||||
github.com/fenthope/record v0.0.3/go.mod h1:KFEkSc4TDZ3QIhP/wglD32uYVA6X1OUcripiao1DEE4=
|
||||
github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d h1:+d6m5Bjvv0/RJct1VcOw2P5bvBOGjENmxORJYnSYDow=
|
||||
github.com/go-json-experiment/json v0.0.0-20250714165856-be8212f5270d/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/infinite-iroha/touka v0.3.3 h1:6Vy36bYjtbGKaBNiZBRcTne9Lcx8QTE6rpHqyMb3oiA=
|
||||
github.com/infinite-iroha/touka v0.3.3/go.mod h1:9Y/MWlvlBL/8cqA+2ZUsnBr4h3f7yo3nOxsegIcBduw=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/wjqserver/modembed v0.0.1 h1:8ZDz7t9M5DLrUFlYgBUUmrMzxWsZPmHvOazkr/T2jEs=
|
||||
github.com/wjqserver/modembed v0.0.1/go.mod h1:sYbQJMAjSBsdYQrUsuHY380XXE1CuRh8g9yyCztTXOQ=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
|
||||
18
init.sh
18
init.sh
@@ -1,18 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
APPLICATON=ghproxy
|
||||
|
||||
if [ ! -f /data/${APPLICATON}/config/blacklist.json ]; then
|
||||
cp /data/${APPLICATON}/blacklist.json /data/${APPLICATON}/config/blacklist.json
|
||||
fi
|
||||
|
||||
if [ ! -f /data/${APPLICATON}/config/whitelist.json ]; then
|
||||
cp /data/${APPLICATON}/whitelist.json /data/${APPLICATON}/config/whitelist.json
|
||||
fi
|
||||
|
||||
if [ ! -f /data/${APPLICATON}/config/config.yaml ]; then
|
||||
cp /data/${APPLICATON}/config.yaml /data/${APPLICATON}/config/config.yaml
|
||||
fi
|
||||
|
||||
/data/${APPLICATON}/${APPLICATON} > /data/${APPLICATON}/log/run.log 2>&1
|
||||
|
||||
530
main.go
530
main.go
@@ -4,33 +4,43 @@ import (
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"ghproxy/api"
|
||||
"ghproxy/auth"
|
||||
"ghproxy/config"
|
||||
"ghproxy/proxy"
|
||||
"ghproxy/rate"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/WJQSERVER-STUDIO/httpc"
|
||||
"github.com/fenthope/bauth"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"ghproxy/weakcache"
|
||||
|
||||
"github.com/fenthope/ikumi"
|
||||
"github.com/fenthope/ipfilter"
|
||||
"github.com/fenthope/reco"
|
||||
"github.com/fenthope/record"
|
||||
"github.com/infinite-iroha/touka"
|
||||
"github.com/wjqserver/modembed"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
_ "net/http/pprof"
|
||||
)
|
||||
|
||||
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
|
||||
r *touka.Engine
|
||||
configfile = "/data/ghproxy/config/config.toml"
|
||||
httpClient *httpc.Client
|
||||
cfgfile string
|
||||
version string
|
||||
runMode string
|
||||
showVersion bool
|
||||
showHelp bool
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -39,126 +49,452 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
wcache *weakcache.Cache[string] // docker token缓存
|
||||
)
|
||||
|
||||
var (
|
||||
logger *reco.Logger
|
||||
logDump = logger.Debugf
|
||||
logDebug = logger.Debugf
|
||||
logInfo = logger.Infof
|
||||
logWarning = logger.Warnf
|
||||
logError = logger.Errorf
|
||||
)
|
||||
|
||||
func readFlag() {
|
||||
flag.StringVar(&cfgfile, "cfg", configfile, "config file path")
|
||||
flag.StringVar(&cfgfile, "c", configfile, "config file path")
|
||||
flag.Func("cfg", "exit", func(s string) error {
|
||||
|
||||
// 被弃用的flag, fail退出
|
||||
fmt.Printf("\n")
|
||||
fmt.Println("[ERROR] cfg flag is deprecated, please use -c instead")
|
||||
fmt.Printf("\n")
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
return nil
|
||||
})
|
||||
flag.BoolVar(&showVersion, "v", false, "show version and exit") // 添加-v标志
|
||||
flag.BoolVar(&showHelp, "h", false, "show help message and exit") // 添加-h标志
|
||||
// 捕获未定义的 flag
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintln(os.Stderr, "\nInvalid flags:")
|
||||
|
||||
// 检查未定义的flags
|
||||
invalidFlags := []string{}
|
||||
for _, arg := range os.Args[1:] {
|
||||
if arg[0] == '-' && arg != "-h" && arg != "-v" { // 检查是否是flag, 排除 -h 和 -v
|
||||
defined := false
|
||||
flag.VisitAll(func(f *flag.Flag) {
|
||||
if "-"+f.Name == arg {
|
||||
defined = true
|
||||
}
|
||||
})
|
||||
if !defined {
|
||||
invalidFlags = append(invalidFlags, arg)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, flag := range invalidFlags {
|
||||
fmt.Fprintf(os.Stderr, " %s\n", flag)
|
||||
}
|
||||
if len(invalidFlags) > 0 {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() {
|
||||
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)
|
||||
if cfg.Log.Level == "" {
|
||||
cfg.Log.Level = "info"
|
||||
}
|
||||
recoLevel := reco.ParseLevel(cfg.Log.Level)
|
||||
logger, err = reco.New(reco.Config{
|
||||
Level: recoLevel,
|
||||
Mode: reco.ModeText,
|
||||
FilePath: cfg.Log.LogFilePath,
|
||||
MaxFileSizeMB: cfg.Log.MaxLogSize,
|
||||
EnableRotation: true,
|
||||
Async: true,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logger.SetLevel(recoLevel)
|
||||
|
||||
fmt.Printf("Log Level: %s\n", cfg.Log.Level)
|
||||
logger.Debugf("Config File Path: %s", cfgfile)
|
||||
logger.Debugf("Loaded config: %v", cfg)
|
||||
logger.Infof("Logger Initialized Successfully")
|
||||
}
|
||||
|
||||
func setMemLimit(cfg *config.Config) {
|
||||
if cfg.Server.MemLimit > 0 {
|
||||
debug.SetMemoryLimit((cfg.Server.MemLimit) * 1024 * 1024)
|
||||
logInfo("Set Memory Limit to %d MB", cfg.Server.MemLimit)
|
||||
}
|
||||
logInfo("Config File Path: ", cfgfile)
|
||||
logInfo("Loaded config: %v\n", cfg)
|
||||
logInfo("Init Completed")
|
||||
}
|
||||
|
||||
func loadlist(cfg *config.Config) {
|
||||
auth.Init(cfg)
|
||||
err := auth.ListInit(cfg)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to initialize list: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setupApi(cfg *config.Config, router *gin.Engine, version string) {
|
||||
api.InitHandleRouter(cfg, router, version)
|
||||
func setupApi(cfg *config.Config, r *touka.Engine, version string) {
|
||||
api.InitHandleRouter(cfg, r, version)
|
||||
}
|
||||
|
||||
func setupRateLimit(cfg *config.Config) {
|
||||
if cfg.RateLimit.Enabled {
|
||||
if cfg.RateLimit.RateMethod == "ip" {
|
||||
iplimiter = rate.NewIPRateLimiter(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
|
||||
} else if cfg.RateLimit.RateMethod == "total" {
|
||||
limiter = rate.New(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
|
||||
} else {
|
||||
logError("Invalid RateLimit Method: %s", cfg.RateLimit.RateMethod)
|
||||
}
|
||||
logInfo("Rate Limit Loaded")
|
||||
func InitReq(cfg *config.Config) {
|
||||
var err error
|
||||
httpClient, err = proxy.InitReq(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func InitReq() {
|
||||
proxy.InitReq()
|
||||
// loadEmbeddedPages 加载嵌入式页面资源
|
||||
func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
|
||||
pageFS := modembed.NewModTimeFS(pagesFS, time.Now())
|
||||
var pages fs.FS
|
||||
var err error
|
||||
switch cfg.Pages.Theme {
|
||||
case "bootstrap":
|
||||
pages, err = fs.Sub(pageFS, "pages/bootstrap")
|
||||
case "nebula":
|
||||
pages, err = fs.Sub(pageFS, "pages/nebula")
|
||||
case "design":
|
||||
pages, err = fs.Sub(pageFS, "pages/design")
|
||||
case "metro":
|
||||
pages, err = fs.Sub(pageFS, "pages/metro")
|
||||
case "classic":
|
||||
pages, err = fs.Sub(pageFS, "pages/classic")
|
||||
case "mino":
|
||||
pages, err = fs.Sub(pageFS, "pages/mino")
|
||||
case "hub":
|
||||
pages, err = fs.Sub(pageFS, "pages/hub")
|
||||
case "free":
|
||||
pages, err = fs.Sub(pageFS, "pages/free")
|
||||
default:
|
||||
pages, err = fs.Sub(pageFS, "pages/design") // 默认主题
|
||||
logWarning("Invalid Pages Theme: %s, using default theme 'design'", cfg.Pages.Theme)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err)
|
||||
}
|
||||
|
||||
// 初始化errPagesFs
|
||||
errPagesInitErr := proxy.InitErrPagesFS(pageFS)
|
||||
if errPagesInitErr != nil {
|
||||
logWarning("errPagesInitErr: %s", errPagesInitErr)
|
||||
}
|
||||
|
||||
var assets fs.FS
|
||||
assets, err = fs.Sub(pageFS, "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, r *touka.Engine) {
|
||||
switch cfg.Pages.Mode {
|
||||
case "internal":
|
||||
err := setInternalRoute(cfg, r)
|
||||
if err != nil {
|
||||
logError("Failed when processing internal pages: %s", err)
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
case "external":
|
||||
r.SetUnMatchFS(http.Dir(cfg.Pages.StaticDir))
|
||||
|
||||
default:
|
||||
// 处理无效的Pages Mode
|
||||
logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode)
|
||||
|
||||
err := setInternalRoute(cfg, r)
|
||||
if err != nil {
|
||||
logError("Failed when processing internal pages: %s", err)
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var viaString string = "WJQSERVER-STUDIO/GHProxy"
|
||||
|
||||
func pageCacheHeader() func(c *touka.Context) {
|
||||
return func(c *touka.Context) {
|
||||
c.AddHeader("Cache-Control", "public, max-age=3600, must-revalidate")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func viaHeader() func(c *touka.Context) {
|
||||
return func(c *touka.Context) {
|
||||
protoVersion := fmt.Sprintf("%d.%d", c.Request.ProtoMajor, c.Request.ProtoMinor)
|
||||
c.AddHeader("Via", protoVersion+" "+viaString)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func setInternalRoute(cfg *config.Config, r *touka.Engine) error {
|
||||
|
||||
// 加载嵌入式资源
|
||||
pages, assets, err := loadEmbeddedPages(cfg)
|
||||
if err != nil {
|
||||
logError("Failed when processing pages: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
r.HandleFunc([]string{"GET"}, "/favicon.ico", pageCacheHeader(), touka.FileServer(http.FS(assets)))
|
||||
r.HandleFunc([]string{"GET"}, "/", pageCacheHeader(), touka.FileServer(http.FS(pages)))
|
||||
r.HandleFunc([]string{"GET"}, "/script.js", pageCacheHeader(), touka.FileServer(http.FS(pages)))
|
||||
r.HandleFunc([]string{"GET"}, "/style.css", pageCacheHeader(), touka.FileServer(http.FS(pages)))
|
||||
r.HandleFunc([]string{"GET"}, "/bootstrap.min.css", pageCacheHeader(), touka.FileServer(http.FS(assets)))
|
||||
r.HandleFunc([]string{"GET"}, "/bootstrap.bundle.min.js", pageCacheHeader(), touka.FileServer(http.FS(assets)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
InitReq()
|
||||
loadlist(cfg)
|
||||
setupRateLimit(cfg)
|
||||
|
||||
if cfg.Server.Debug {
|
||||
dev = "true"
|
||||
version = "dev"
|
||||
}
|
||||
if dev == "true" {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
runMode = "dev"
|
||||
} else {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
runMode = "release"
|
||||
}
|
||||
|
||||
gin.LoggerWithWriter(io.Discard)
|
||||
router = gin.New()
|
||||
router.Use(gin.Recovery())
|
||||
//H2C默认值为true,而后遵循cfg.Server.EnableH2C的设置
|
||||
if cfg.Server.EnableH2C == "on" {
|
||||
router.UseH2C = true
|
||||
} else if cfg.Server.EnableH2C == "" {
|
||||
router.UseH2C = true
|
||||
} else {
|
||||
router.UseH2C = false
|
||||
}
|
||||
|
||||
setupApi(cfg, router, version)
|
||||
|
||||
if cfg.Pages.Enabled {
|
||||
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
|
||||
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.File(indexPagePath)
|
||||
logInfo("IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto)
|
||||
})
|
||||
router.StaticFile("/favicon.ico", faviconPath)
|
||||
} else if !cfg.Pages.Enabled {
|
||||
pages, err := fs.Sub(pagesFS, "pages")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed when processing pages: %s", err)
|
||||
if cfg != nil { // 在setupLogger前添加空值检查
|
||||
setupLogger(cfg)
|
||||
InitReq(cfg)
|
||||
setMemLimit(cfg)
|
||||
loadlist(cfg)
|
||||
if cfg.Docker.Enabled {
|
||||
wcache = proxy.InitWeakCache()
|
||||
}
|
||||
|
||||
if cfg.Server.Debug {
|
||||
runMode = "dev"
|
||||
} else {
|
||||
runMode = "release"
|
||||
}
|
||||
|
||||
if cfg.Server.Debug {
|
||||
version = "Dev" // 如果是Debug模式,版本设置为"Dev"
|
||||
}
|
||||
router.GET("/", gin.WrapH(http.FileServer(http.FS(pages))))
|
||||
router.GET("/favicon.ico", gin.WrapH(http.FileServer(http.FS(pages))))
|
||||
}
|
||||
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter, runMode)(c)
|
||||
})
|
||||
|
||||
fmt.Printf("GHProxy Version: %s\n", version)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
fmt.Println("Config not loaded, exiting.")
|
||||
return
|
||||
}
|
||||
|
||||
r := touka.Default()
|
||||
r.SetProtocols(&touka.ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2_Cleartext: true,
|
||||
})
|
||||
|
||||
r.Use(touka.Recovery()) // Recovery中间件
|
||||
r.SetLogger(logger)
|
||||
r.SetErrorHandler(proxy.UnifiedToukaErrorHandler)
|
||||
r.SetHTTPClient(httpClient)
|
||||
r.Use(record.Middleware()) // log中间件
|
||||
r.Use(viaHeader())
|
||||
/*
|
||||
r.Use(compress.Compression(compress.CompressOptions{
|
||||
Algorithms: map[string]compress.AlgorithmConfig{
|
||||
compress.EncodingGzip: {
|
||||
Level: gzip.BestCompression, // Gzip最高压缩比
|
||||
PoolEnabled: true, // 启用Gzip压缩器的对象池
|
||||
},
|
||||
compress.EncodingDeflate: {
|
||||
Level: flate.DefaultCompression, // Deflate默认压缩比
|
||||
PoolEnabled: false, // Deflate不启用对象池
|
||||
},
|
||||
compress.EncodingZstd: {
|
||||
Level: int(zstd.SpeedBestCompression), // Zstandard最佳压缩比
|
||||
PoolEnabled: true, // 启用Zstandard压缩器的对象池
|
||||
},
|
||||
},
|
||||
}))
|
||||
*/
|
||||
|
||||
if cfg.RateLimit.Enabled {
|
||||
r.Use(ikumi.TokenRateLimit(ikumi.TokenRateLimiterOptions{
|
||||
Limit: rate.Limit(cfg.RateLimit.RatePerMinute),
|
||||
Burst: cfg.RateLimit.Burst,
|
||||
}))
|
||||
}
|
||||
|
||||
if cfg.IPFilter.Enabled {
|
||||
var err error
|
||||
ipAllowList, ipBlockList, err := auth.ReadIPFilterList(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to read IP filter list: %v\n", err)
|
||||
logger.Errorf("Failed to read IP filter list: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ipBlockFilter, err := ipfilter.NewIPFilter(ipfilter.IPFilterConfig{
|
||||
EnableAllowList: cfg.IPFilter.EnableAllowList,
|
||||
EnableBlockList: cfg.IPFilter.EnableBlockList,
|
||||
AllowList: ipAllowList,
|
||||
BlockList: ipBlockList,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize IP filter: %v\n", err)
|
||||
logger.Errorf("Failed to initialize IP filter: %v", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
r.Use(ipBlockFilter)
|
||||
}
|
||||
}
|
||||
setupApi(cfg, r, version)
|
||||
setupPages(cfg, r)
|
||||
r.SetRedirectTrailingSlash(false)
|
||||
|
||||
r.GET("/github.com/:user/:repo/releases/*filepath", func(c *touka.Context) {
|
||||
c.Set("matcher", "releases")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
|
||||
r.GET("/github.com/:user/:repo/archive/*filepath", func(c *touka.Context) {
|
||||
c.Set("matcher", "releases")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
|
||||
r.GET("/github.com/:user/:repo/blob/*filepath", func(c *touka.Context) {
|
||||
c.Set("matcher", "blob")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
|
||||
r.GET("/github.com/:user/:repo/raw/*filepath", func(c *touka.Context) {
|
||||
c.Set("matcher", "raw")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
|
||||
r.GET("/github.com/:user/:repo/info/*filepath", func(c *touka.Context) {
|
||||
c.Set("matcher", "clone")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
r.GET("/github.com/:user/:repo/git-upload-pack", func(c *touka.Context) {
|
||||
c.Set("matcher", "clone")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
r.POST("/github.com/:user/:repo/git-upload-pack", func(c *touka.Context) {
|
||||
c.Set("matcher", "clone")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
|
||||
r.GET("/raw.githubusercontent.com/:user/:repo/*filepath", func(c *touka.Context) {
|
||||
c.Set("matcher", "raw")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
|
||||
r.GET("/gist.githubusercontent.com/:user/*filepath", func(c *touka.Context) {
|
||||
c.Set("matcher", "gist")
|
||||
proxy.NoRouteHandler(cfg)(c)
|
||||
})
|
||||
|
||||
r.ANY("/api.github.com/repos/:user/:repo/*filepath", func(c *touka.Context) {
|
||||
c.Set("matcher", "api")
|
||||
proxy.RoutingHandler(cfg)(c)
|
||||
})
|
||||
|
||||
r.GET("/v2/",
|
||||
r.UseIf(cfg.Docker.Auth, func() touka.HandlerFunc {
|
||||
return bauth.BasicAuthForStatic(cfg.Docker.Credentials, "GHProxy Docker Proxy")
|
||||
}),
|
||||
func(c *touka.Context) {
|
||||
emptyJSON := "{}"
|
||||
c.Header("Content-Type", "application/json")
|
||||
c.Header("Content-Length", fmt.Sprint(len(emptyJSON)))
|
||||
|
||||
c.Header("Docker-Distribution-API-Version", "registry/2.0")
|
||||
|
||||
c.Status(200)
|
||||
c.Writer.Write([]byte(emptyJSON))
|
||||
},
|
||||
)
|
||||
|
||||
r.GET("/v2", func(c *touka.Context) {
|
||||
// 重定向到 /v2/
|
||||
c.Redirect(http.StatusMovedPermanently, "/v2/")
|
||||
})
|
||||
|
||||
r.ANY("/v2/:target/:user/:repo/*filepath", func(c *touka.Context) {
|
||||
proxy.GhcrWithImageRouting(cfg)(c)
|
||||
})
|
||||
|
||||
r.NoRoute(func(c *touka.Context) {
|
||||
proxy.NoRouteHandler(cfg)(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")
|
||||
fmt.Printf("Power by Touka\n")
|
||||
|
||||
if cfg.Server.Debug {
|
||||
go func() {
|
||||
http.ListenAndServe("localhost:6060", nil)
|
||||
}()
|
||||
}
|
||||
if wcache != nil {
|
||||
defer wcache.StopCleanup()
|
||||
}
|
||||
|
||||
defer logger.Close()
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
err := r.RunShutdown(addr)
|
||||
if err != nil {
|
||||
logError("Server Run Error: %v", err)
|
||||
fmt.Printf("Server Run Error: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Println("Program Exit")
|
||||
}
|
||||
|
||||
15
middleware/nocache/nocache.go
Normal file
15
middleware/nocache/nocache.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package nocache
|
||||
|
||||
import (
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func NoCacheMiddleware() touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
// 设置禁止缓存的响应头
|
||||
c.SetHeader("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||
c.SetHeader("Pragma", "no-cache")
|
||||
c.SetHeader("Expires", "0")
|
||||
c.Next() // 继续处理请求
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
440
pages/index.html
440
pages/index.html
@@ -1,440 +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>
|
||||
<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: 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 © 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 = window.location.protocol + "//" + currentHost + "/github.com" + githubLink.substring(githubLink.indexOf("/", 8));
|
||||
} else if (githubLink.startsWith("github.com/")) {
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
|
||||
} else if (githubLink.startsWith("https://raw.githubusercontent.com/") || githubLink.startsWith("http://raw.githubusercontent.com/")) {
|
||||
formattedLink = window.location.protocol + "//" + currentHost + githubLink.substring(githubLink.indexOf("/", 7));
|
||||
} else if (githubLink.startsWith("raw.githubusercontent.com/")) {
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
|
||||
} else if (githubLink.startsWith("https://gist.githubusercontent.com/") || githubLink.startsWith("http://gist.githubusercontent.com/")) {
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/gist.github.com" + githubLink.substring(githubLink.indexOf("/", 18));
|
||||
} else if (githubLink.startsWith("gist.githubusercontent.com/")) {
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
|
||||
} else {
|
||||
showToast('请输入有效的GitHub链接');
|
||||
return null;
|
||||
}
|
||||
|
||||
return formattedLink;
|
||||
}
|
||||
|
||||
githubForm.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const formattedLink = formatGithubLink(githubLinkInput.value);
|
||||
if (formattedLink) {
|
||||
formattedLinkOutput.textContent = formattedLink;
|
||||
output.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
copyButton.addEventListener('click', function () {
|
||||
navigator.clipboard.writeText(formattedLinkOutput.textContent).then(() => {
|
||||
showToast('链接已复制到剪贴板');
|
||||
});
|
||||
});
|
||||
|
||||
openButton.addEventListener('click', function () {
|
||||
window.open(formattedLinkOutput.textContent, '_blank');
|
||||
});
|
||||
|
||||
function fetchAPI() {
|
||||
fetch('/api/size_limit')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('sizeLimitDisplay').textContent = `${data.MaxResponseBodySize} MB`;
|
||||
});
|
||||
|
||||
fetch('/api/whitelist/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('whiteListStatus').textContent = data.Whitelist ? '已开启' : '已关闭';
|
||||
});
|
||||
|
||||
fetch('/api/blacklist/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('blackListStatus').textContent = data.Blacklist ? '已开启' : '已关闭';
|
||||
});
|
||||
|
||||
fetch('/api/version')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
document.getElementById('versionBadge').textContent = data.Version;
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', fetchAPI);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
62
proxy/authparse.go
Normal file
62
proxy/authparse.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BearerAuthParams 用于存放解析出的 Bearer 认证参数
|
||||
type BearerAuthParams struct {
|
||||
Realm string
|
||||
Service string
|
||||
Scope string
|
||||
}
|
||||
|
||||
// parseBearerWWWAuthenticateHeader 解析 Bearer 方案的 Www-Authenticate Header。
|
||||
// 它期望格式为 'Bearer key1="value1",key2="value2",...'
|
||||
// 并尝试将已知参数解析到 BearerAuthParams struct 中。
|
||||
func parseBearerWWWAuthenticateHeader(headerValue string) (*BearerAuthParams, error) {
|
||||
if headerValue == "" {
|
||||
return nil, fmt.Errorf("header value is empty")
|
||||
}
|
||||
|
||||
// 检查 Scheme 是否是 "Bearer"
|
||||
parts := strings.SplitN(headerValue, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
return nil, fmt.Errorf("invalid or non-bearer header format: got '%s'", headerValue)
|
||||
}
|
||||
paramsStr := parts[1]
|
||||
|
||||
paramPairs := strings.Split(paramsStr, ",")
|
||||
tempMap := make(map[string]string)
|
||||
|
||||
for _, pair := range paramPairs {
|
||||
trimmedPair := strings.TrimSpace(pair)
|
||||
keyValue := strings.SplitN(trimmedPair, "=", 2)
|
||||
if len(keyValue) != 2 {
|
||||
//logWarning("Skipping malformed parameter '%s' in Www-Authenticate header: %s", pair, headerValue)
|
||||
continue
|
||||
}
|
||||
key := strings.TrimSpace(keyValue[0])
|
||||
value := strings.TrimSpace(keyValue[1])
|
||||
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
|
||||
value = value[1 : len(value)-1]
|
||||
}
|
||||
tempMap[key] = value
|
||||
}
|
||||
|
||||
//从 map 中提取值并填充到 struct
|
||||
authParams := &BearerAuthParams{}
|
||||
|
||||
if realm, ok := tempMap["realm"]; ok {
|
||||
authParams.Realm = realm
|
||||
}
|
||||
if service, ok := tempMap["service"]; ok {
|
||||
authParams.Service = service
|
||||
}
|
||||
if scope, ok := tempMap["scope"]; ok {
|
||||
authParams.Scope = scope
|
||||
}
|
||||
|
||||
return authParams, nil
|
||||
}
|
||||
@@ -4,21 +4,20 @@ import (
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func AuthPassThrough(c *gin.Context, cfg *config.Config, req *http.Request) {
|
||||
func AuthPassThrough(c *touka.Context, cfg *config.Config, req *http.Request) {
|
||||
if cfg.Auth.PassThrough {
|
||||
token := c.Query("token")
|
||||
if token != "" {
|
||||
switch cfg.Auth.AuthMethod {
|
||||
switch cfg.Auth.Method {
|
||||
case "parameters":
|
||||
if !cfg.Auth.Enabled {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
} else {
|
||||
logWarning("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
// 500 Internal Server Error
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Conflict Auth Method"})
|
||||
c.Warnf("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
|
||||
ErrorPage(c, NewErrorWithStatusLookup(500, "Conflict Auth Method"))
|
||||
return
|
||||
}
|
||||
case "header":
|
||||
@@ -26,9 +25,8 @@ func AuthPassThrough(c *gin.Context, cfg *config.Config, req *http.Request) {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
}
|
||||
default:
|
||||
logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
// 500 Internal Server Error
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid Auth Method / Auth Method is not be set"})
|
||||
c.Warnf("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
|
||||
ErrorPage(c, NewErrorWithStatusLookup(500, "Invalid Auth Method / Auth Method is not be set"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
58
proxy/bandwidth.go
Normal file
58
proxy/bandwidth.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
var (
|
||||
bandwidthLimit rate.Limit
|
||||
bandwidthBurst rate.Limit
|
||||
)
|
||||
|
||||
func UnDefiendRateStringErrHandle(err error) error {
|
||||
if errors.Is(err, &limitreader.UnDefiendRateStringErr{}) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func SetGlobalRateLimit(cfg *config.Config) error {
|
||||
if cfg.RateLimit.BandwidthLimit.Enabled {
|
||||
var err error
|
||||
var totalLimit rate.Limit
|
||||
var totalBurst rate.Limit
|
||||
totalLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalLimit)
|
||||
if UnDefiendRateStringErrHandle(err) != nil {
|
||||
return err
|
||||
}
|
||||
totalBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.TotalBurst)
|
||||
if UnDefiendRateStringErrHandle(err) != nil {
|
||||
return err
|
||||
}
|
||||
limitreader.SetGlobalRateLimit(totalLimit, int(totalBurst))
|
||||
err = SetBandwidthLimit(cfg)
|
||||
if UnDefiendRateStringErrHandle(err) != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
limitreader.SetGlobalRateLimit(rate.Inf, 0)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetBandwidthLimit(cfg *config.Config) error {
|
||||
var err error
|
||||
bandwidthLimit, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleLimit)
|
||||
if UnDefiendRateStringErrHandle(err) != nil {
|
||||
return err
|
||||
}
|
||||
bandwidthBurst, err = limitreader.ParseRate(cfg.RateLimit.BandwidthLimit.SingleBurst)
|
||||
if UnDefiendRateStringErrHandle(err) != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,179 +1,158 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
var BufferSize int = 32 * 1024 // 32KB
|
||||
func ChunkedProxyRequest(ctx context.Context, c *touka.Context, u string, cfg *config.Config, matcher string) {
|
||||
|
||||
var (
|
||||
cclient *http.Client
|
||||
ctr *http.Transport
|
||||
BufferPool *sync.Pool
|
||||
)
|
||||
var (
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
err error
|
||||
)
|
||||
|
||||
func InitReq() {
|
||||
initChunkedHTTPClient()
|
||||
initGitHTTPClient()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if req != nil && req.Body != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// 初始化固定大小的缓存池
|
||||
BufferPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, BufferSize)
|
||||
},
|
||||
}
|
||||
}
|
||||
rb := client.NewRequestBuilder(c.Request.Method, u)
|
||||
rb.NoDefaultHeaders()
|
||||
rb.SetBody(c.Request.Body)
|
||||
rb.WithContext(ctx)
|
||||
|
||||
func initChunkedHTTPClient() {
|
||||
ctr = &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxConnsPerHost: 60,
|
||||
IdleConnTimeout: 20 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
}
|
||||
cclient = &http.Client{
|
||||
Transport: ctr,
|
||||
}
|
||||
}
|
||||
|
||||
func ChunkedProxyRequest(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)
|
||||
|
||||
// 发送HEAD请求, 预获取Content-Length
|
||||
headReq, err := http.NewRequest("HEAD", u, nil)
|
||||
req, err = rb.Build()
|
||||
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头)
|
||||
AuthPassThrough(c, cfg, headReq)
|
||||
|
||||
headResp, err := cclient.Do(headReq)
|
||||
setRequestHeaders(c, req, cfg, matcher)
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
//defer headResp.Body.Close()
|
||||
defer func(Body io.ReadCloser) {
|
||||
if err := Body.Close(); err != nil {
|
||||
logError("Failed to close response body: %v", err)
|
||||
}
|
||||
}(headResp.Body)
|
||||
|
||||
contentLength := headResp.Header.Get("Content-Length")
|
||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > sizelimit {
|
||||
finalURL := headResp.Request.URL.String()
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
|
||||
// 错误处理(404)
|
||||
if resp.StatusCode == 404 {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
||||
return
|
||||
}
|
||||
|
||||
// 处理302情况
|
||||
if resp.StatusCode == 302 || resp.StatusCode == 301 {
|
||||
//c.Debugf("resp header %s", resp.Header)
|
||||
finalURL := resp.Header.Get("Location")
|
||||
if finalURL != "" {
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
c.Errorf("Failed to close response body: %v", err)
|
||||
}
|
||||
c.Infof("Internal Redirecting to %s", finalURL)
|
||||
ChunkedProxyRequest(ctx, c, finalURL, cfg, matcher)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
}
|
||||
|
||||
bodyReader := bytes.NewBuffer(body)
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequest(method, u, bodyReader)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("创建请求失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
setRequestHeaders(c, req)
|
||||
removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err := cclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("发送请求失败: %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
|
||||
}
|
||||
*/
|
||||
// 处理响应体大小限制
|
||||
|
||||
var (
|
||||
bodySize int
|
||||
contentLength string
|
||||
sizelimit int
|
||||
)
|
||||
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
|
||||
contentLength = resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > sizelimit {
|
||||
var err error
|
||||
bodySize, err = strconv.Atoi(contentLength)
|
||||
if err != nil {
|
||||
c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
|
||||
bodySize = -1
|
||||
}
|
||||
if err == nil && bodySize > 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)
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
c.Errorf("Failed to close response body: %v", err)
|
||||
}
|
||||
c.Redirect(301, finalURL)
|
||||
c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, bodySize)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
// 复制响应头,排除需要移除的 header
|
||||
c.SetHeaders(resp.Header)
|
||||
for key := range respHeadersToRemove {
|
||||
c.DelHeader(key)
|
||||
}
|
||||
|
||||
headersToRemove := map[string]struct{}{
|
||||
"Content-Security-Policy": {},
|
||||
"Referrer-Policy": {},
|
||||
"Strict-Transport-Security": {},
|
||||
}
|
||||
|
||||
for header := range headersToRemove {
|
||||
resp.Header.Del(header)
|
||||
}
|
||||
|
||||
if cfg.CORS.Enabled {
|
||||
switch cfg.Server.Cors {
|
||||
case "*":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
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)
|
||||
|
||||
// 使用固定32KB缓冲池
|
||||
buffer := BufferPool.Get().([]byte)
|
||||
defer BufferPool.Put(buffer)
|
||||
bodyReader := resp.Body
|
||||
|
||||
_, err = io.CopyBuffer(c.Writer, resp.Body, buffer)
|
||||
if err != nil {
|
||||
logError("%s %s %s %s %s 响应复制错误: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
} else {
|
||||
c.Writer.Flush() // 确保刷入
|
||||
if cfg.RateLimit.BandwidthLimit.Enabled {
|
||||
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
|
||||
}
|
||||
|
||||
defer bodyReader.Close()
|
||||
|
||||
if MatcherShell(u) && matchString(matcher) && cfg.Shell.Editor {
|
||||
// 判断body是不是gzip
|
||||
var compress string
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
compress = "gzip"
|
||||
}
|
||||
|
||||
c.Debugf("Use Shell Editor: %s %s %s %s %s", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto)
|
||||
c.Header("Content-Length", "")
|
||||
|
||||
var reader io.Reader
|
||||
|
||||
reader, _, err = processLinks(bodyReader, compress, c.Request.Host, cfg, c)
|
||||
c.WriteStream(reader)
|
||||
if err != nil {
|
||||
c.Errorf("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), c.Request.Method, u, c.UserAgent(), c.Request.Proto, err)
|
||||
ErrorPage(c, NewErrorWithStatusLookup(500, fmt.Sprintf("Failed to copy response body: %v", err)))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
||||
if contentLength != "" {
|
||||
c.SetHeader("Content-Length", contentLength)
|
||||
c.WriteStream(bodyReader)
|
||||
return
|
||||
}
|
||||
c.WriteStream(bodyReader)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
143
proxy/dial.go
Normal file
143
proxy/dial.go
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
made&PR by @lfhy
|
||||
https://github.com/WJQSERVER-STUDIO/ghproxy/pull/46
|
||||
*/
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"log"
|
||||
"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")
|
||||
log.Printf("Outbound proxy is not set, using environment variables")
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试解析代理 URL
|
||||
proxyInfo, err := url.Parse(cfg.Outbound.Url)
|
||||
if err != nil {
|
||||
// 如果解析失败,记录错误日志并使用环境变量中的代理配置
|
||||
log.Printf("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) 代理
|
||||
log.Printf("Using HTTP(S) proxy: %s", cfg.Outbound.Url)
|
||||
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")
|
||||
log.Printf("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
|
||||
}
|
||||
//logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
|
||||
log.Printf("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
|
||||
default: // 如果代理协议不支持
|
||||
//logError("Unsupported proxy scheme: %s", proxyInfo.Scheme)
|
||||
log.Printf("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)
|
||||
log.Printf("Failed to parse proxy URL %q: %v", proxyUrl, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查代理协议是否为 SOCKS5
|
||||
if urlInfo.Scheme != "socks5" {
|
||||
// logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
|
||||
log.Printf("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)
|
||||
log.Printf("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)
|
||||
}
|
||||
330
proxy/docker.go
Normal file
330
proxy/docker.go
Normal file
@@ -0,0 +1,330 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
|
||||
"ghproxy/config"
|
||||
"ghproxy/weakcache"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||
)
|
||||
|
||||
var (
|
||||
dockerhubTarget = "registry-1.docker.io"
|
||||
ghcrTarget = "ghcr.io"
|
||||
)
|
||||
|
||||
var cache *weakcache.Cache[string]
|
||||
|
||||
type imageInfo struct {
|
||||
User string
|
||||
Repo string
|
||||
Image string
|
||||
}
|
||||
|
||||
func InitWeakCache() *weakcache.Cache[string] {
|
||||
cache = weakcache.NewCache[string](weakcache.DefaultExpiration, 100)
|
||||
return cache
|
||||
}
|
||||
|
||||
func GhcrWithImageRouting(cfg *config.Config) touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
|
||||
charToFind := '.'
|
||||
reqTarget := c.Param("target")
|
||||
reqImageUser := c.Param("user")
|
||||
reqImageName := c.Param("repo")
|
||||
reqFilePath := c.Param("filepath")
|
||||
|
||||
path := fmt.Sprintf("%s/%s/%s", reqImageUser, reqImageName, reqFilePath)
|
||||
target := ""
|
||||
|
||||
if strings.ContainsRune(reqTarget, charToFind) {
|
||||
switch reqTarget {
|
||||
case "docker.io":
|
||||
target = dockerhubTarget
|
||||
case "ghcr.io":
|
||||
target = ghcrTarget
|
||||
default:
|
||||
target = reqTarget
|
||||
}
|
||||
} else {
|
||||
path = c.GetRequestURI()
|
||||
reqImageUser = c.Param("target")
|
||||
reqImageName = c.Param("user")
|
||||
}
|
||||
image := &imageInfo{
|
||||
User: reqImageUser,
|
||||
Repo: reqImageName,
|
||||
Image: fmt.Sprintf("%s/%s", reqImageUser, reqImageName),
|
||||
}
|
||||
|
||||
GhcrToTarget(c, cfg, target, path, image)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GhcrToTarget(c *touka.Context, cfg *config.Config, target string, path string, image *imageInfo) {
|
||||
if cfg.Docker.Enabled {
|
||||
var ctx = c.Request.Context()
|
||||
if target != "" {
|
||||
GhcrRequest(ctx, c, "https://"+target+"/v2/"+path+"?"+c.GetReqQueryString(), image, cfg, target)
|
||||
} else {
|
||||
if cfg.Docker.Target == "ghcr" {
|
||||
GhcrRequest(ctx, c, "https://"+ghcrTarget+c.GetRequestURI(), image, cfg, ghcrTarget)
|
||||
} else if cfg.Docker.Target == "dockerhub" {
|
||||
GhcrRequest(ctx, c, "https://"+dockerhubTarget+c.GetRequestURI(), image, cfg, dockerhubTarget)
|
||||
} else if cfg.Docker.Target != "" {
|
||||
// 自定义taget
|
||||
GhcrRequest(ctx, c, "https://"+cfg.Docker.Target+c.GetRequestURI(), image, cfg, cfg.Docker.Target)
|
||||
} else {
|
||||
// 配置为空
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker Target is not set"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Docker is not Allowed"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func GhcrRequest(ctx context.Context, c *touka.Context, u string, image *imageInfo, cfg *config.Config, target string) {
|
||||
|
||||
var (
|
||||
method string
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
err error
|
||||
)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if req != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
method = c.Request.Method
|
||||
ghcrclient := c.GetHTTPC()
|
||||
|
||||
rb := ghcrclient.NewRequestBuilder(method, u)
|
||||
rb.NoDefaultHeaders()
|
||||
rb.SetBody(c.Request.Body)
|
||||
rb.WithContext(ctx)
|
||||
|
||||
req, err = rb.Build()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
copyHeader(c.Request.Header, req.Header)
|
||||
|
||||
req.Header.Set("Host", target)
|
||||
if image != nil {
|
||||
token, exist := cache.Get(image.Image)
|
||||
if exist {
|
||||
c.Debugf("Use Cache Token: %s", token)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = ghcrclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
|
||||
case 401:
|
||||
// 请求target /v2/路径
|
||||
if string(c.GetRequestURIPath()) != "/v2/" {
|
||||
resp.Body.Close()
|
||||
if image == nil {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(401, "Unauthorized"))
|
||||
return
|
||||
}
|
||||
token := ChallengeReq(target, image, ctx, c)
|
||||
|
||||
// 更新kv
|
||||
if token != "" {
|
||||
c.Debugf("Update Cache Token: %s", token)
|
||||
cache.Put(image.Image, token)
|
||||
}
|
||||
|
||||
rb := ghcrclient.NewRequestBuilder(string(method), u)
|
||||
rb.NoDefaultHeaders()
|
||||
rb.SetBody(c.Request.Body)
|
||||
rb.WithContext(ctx)
|
||||
|
||||
req, err = rb.Build()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
copyHeader(c.Request.Header, req.Header)
|
||||
|
||||
req.Header.Set("Host", target)
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
resp, err = ghcrclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case 404: // 错误处理(404)
|
||||
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
||||
return
|
||||
case 302, 301:
|
||||
finalURL := resp.Header.Get("Location")
|
||||
if finalURL != "" {
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
c.Errorf("Failed to close response body: %v", err)
|
||||
}
|
||||
c.Infof("Internal Redirecting to %s", finalURL)
|
||||
GhcrRequest(ctx, c, finalURL, image, cfg, target)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
bodySize int
|
||||
contentLength string
|
||||
sizelimit int
|
||||
)
|
||||
|
||||
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
|
||||
contentLength = resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
var err error
|
||||
bodySize, err = strconv.Atoi(contentLength)
|
||||
if err != nil {
|
||||
c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
|
||||
bodySize = -1
|
||||
}
|
||||
if err == nil && bodySize > sizelimit {
|
||||
finalURL := resp.Request.URL.String()
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
c.Errorf("Failed to close response body: %v", err)
|
||||
}
|
||||
c.Redirect(301, finalURL)
|
||||
c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, bodySize)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.SetHeaders(resp.Header)
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
|
||||
bodyReader := resp.Body
|
||||
|
||||
if cfg.RateLimit.BandwidthLimit.Enabled {
|
||||
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
|
||||
}
|
||||
|
||||
if contentLength != "" {
|
||||
c.SetBodyStream(bodyReader, bodySize)
|
||||
return
|
||||
}
|
||||
c.SetBodyStream(bodyReader, -1)
|
||||
|
||||
}
|
||||
|
||||
type AuthToken struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
func ChallengeReq(target string, image *imageInfo, ctx context.Context, c *touka.Context) (token string) {
|
||||
var resp401 *http.Response
|
||||
var req401 *http.Request
|
||||
var err error
|
||||
ghcrclient := c.GetHTTPC()
|
||||
|
||||
rb401 := ghcrclient.NewRequestBuilder("GET", "https://"+target+"/v2/")
|
||||
rb401.NoDefaultHeaders()
|
||||
rb401.WithContext(ctx)
|
||||
rb401.AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ")
|
||||
req401, err = rb401.Build()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
req401.Header.Set("Host", target)
|
||||
|
||||
resp401, err = ghcrclient.Do(req401)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp401.Body.Close()
|
||||
bearer, err := parseBearerWWWAuthenticateHeader(resp401.Header.Get("Www-Authenticate"))
|
||||
if err != nil {
|
||||
c.Errorf("Failed to parse Www-Authenticate header: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
scope := fmt.Sprintf("repository:%s:pull", image.Image)
|
||||
|
||||
getAuthRB := ghcrclient.NewRequestBuilder("GET", bearer.Realm).
|
||||
NoDefaultHeaders().
|
||||
WithContext(ctx).
|
||||
AddHeader("User-Agent", "docker/28.1.1 go/go1.23.8 git-commit/01f442b kernel/6.12.25-amd64 os/linux arch/amd64 UpstreamClient(Docker-Client/28.1.1 ").
|
||||
SetHeader("Host", bearer.Service).
|
||||
AddQueryParam("service", bearer.Service).
|
||||
AddQueryParam("scope", scope)
|
||||
|
||||
getAuthReq, err := getAuthRB.Build()
|
||||
if err != nil {
|
||||
c.Errorf("Failed to create request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
authResp, err := ghcrclient.Do(getAuthReq)
|
||||
if err != nil {
|
||||
c.Errorf("Failed to send request: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer authResp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(authResp.Body)
|
||||
if err != nil {
|
||||
c.Errorf("Failed to read auth response body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 解码json
|
||||
var authToken AuthToken
|
||||
err = json.Unmarshal(bodyBytes, &authToken)
|
||||
if err != nil {
|
||||
c.Errorf("Failed to decode auth response body: %v", err)
|
||||
return
|
||||
}
|
||||
token = authToken.Token
|
||||
|
||||
return token
|
||||
|
||||
}
|
||||
370
proxy/error.go
Normal file
370
proxy/error.go
Normal file
@@ -0,0 +1,370 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func HandleError(c *touka.Context, message string) {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(500, message))
|
||||
c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, message)
|
||||
}
|
||||
|
||||
func UnifiedToukaErrorHandler(c *touka.Context, code int, err error) {
|
||||
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
c.Errorf("%s %s %s %s %s Error: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, errMsg)
|
||||
|
||||
constructedGHErr := NewErrorWithStatusLookup(code, errMsg)
|
||||
|
||||
ErrorPage(c, constructedGHErr)
|
||||
}
|
||||
|
||||
type GHProxyErrors struct {
|
||||
StatusCode int
|
||||
StatusDesc string
|
||||
StatusText string
|
||||
HelpInfo string
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidURL = &GHProxyErrors{
|
||||
StatusCode: 400,
|
||||
StatusDesc: "Bad Request",
|
||||
StatusText: "无效请求",
|
||||
HelpInfo: "请求的URL格式不正确,请检查后重试。",
|
||||
}
|
||||
ErrAuthHeaderUnavailable = &GHProxyErrors{
|
||||
StatusCode: 401,
|
||||
StatusDesc: "Unauthorized",
|
||||
StatusText: "认证失败",
|
||||
HelpInfo: "缺少或无效的鉴权信息。",
|
||||
}
|
||||
ErrForbidden = &GHProxyErrors{
|
||||
StatusCode: 403,
|
||||
StatusDesc: "Forbidden",
|
||||
StatusText: "权限不足",
|
||||
HelpInfo: "您没有权限访问此资源。",
|
||||
}
|
||||
ErrNotFound = &GHProxyErrors{
|
||||
StatusCode: 404,
|
||||
StatusDesc: "Not Found",
|
||||
StatusText: "页面未找到",
|
||||
HelpInfo: "抱歉,您访问的页面不存在。",
|
||||
}
|
||||
ErrTooManyRequests = &GHProxyErrors{
|
||||
StatusCode: 429,
|
||||
StatusDesc: "Too Many Requests",
|
||||
StatusText: "请求过于频繁",
|
||||
HelpInfo: "您的请求过于频繁,请稍后再试。",
|
||||
}
|
||||
ErrInternalServerError = &GHProxyErrors{
|
||||
StatusCode: 500,
|
||||
StatusDesc: "Internal Server Error",
|
||||
StatusText: "服务器内部错误",
|
||||
HelpInfo: "服务器处理您的请求时发生错误,请稍后重试或联系管理员。",
|
||||
}
|
||||
// 502
|
||||
ErrBadGateway = &GHProxyErrors{
|
||||
StatusCode: 502,
|
||||
StatusDesc: "Bad Gateway",
|
||||
StatusText: "网关错误",
|
||||
HelpInfo: "代理服务器从上游服务器接收到无效响应。",
|
||||
}
|
||||
ErrServiceUnavailable = &GHProxyErrors{
|
||||
StatusCode: 503,
|
||||
StatusDesc: "Service Unavailable",
|
||||
StatusText: "服务不可用",
|
||||
HelpInfo: "服务器目前无法处理请求,通常是由于服务器过载或停机维护。",
|
||||
}
|
||||
ErrGatewayTimeout = &GHProxyErrors{
|
||||
StatusCode: 504,
|
||||
StatusDesc: "Gateway Timeout",
|
||||
StatusText: "网关超时",
|
||||
HelpInfo: "代理服务器未能及时从上游服务器接收到响应。",
|
||||
}
|
||||
)
|
||||
|
||||
var statusErrorMap map[int]*GHProxyErrors
|
||||
|
||||
func init() {
|
||||
statusErrorMap = map[int]*GHProxyErrors{
|
||||
ErrInvalidURL.StatusCode: ErrInvalidURL,
|
||||
ErrAuthHeaderUnavailable.StatusCode: ErrAuthHeaderUnavailable,
|
||||
ErrForbidden.StatusCode: ErrForbidden,
|
||||
ErrNotFound.StatusCode: ErrNotFound,
|
||||
ErrTooManyRequests.StatusCode: ErrTooManyRequests,
|
||||
ErrInternalServerError.StatusCode: ErrInternalServerError,
|
||||
ErrBadGateway.StatusCode: ErrBadGateway,
|
||||
ErrServiceUnavailable.StatusCode: ErrServiceUnavailable,
|
||||
ErrGatewayTimeout.StatusCode: ErrGatewayTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
func NewErrorWithStatusLookup(statusCode int, errMsg string) *GHProxyErrors {
|
||||
baseErr, found := statusErrorMap[statusCode]
|
||||
|
||||
if found {
|
||||
return &GHProxyErrors{
|
||||
StatusCode: baseErr.StatusCode,
|
||||
StatusDesc: baseErr.StatusDesc,
|
||||
StatusText: baseErr.StatusText,
|
||||
HelpInfo: baseErr.HelpInfo,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
} else {
|
||||
return &GHProxyErrors{
|
||||
StatusCode: statusCode,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errPagesFs fs.FS
|
||||
|
||||
func InitErrPagesFS(pages fs.FS) error {
|
||||
var err error
|
||||
errPagesFs, err = fs.Sub(pages, "pages/err")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ErrorPageData struct {
|
||||
StatusCode int
|
||||
StatusDesc string
|
||||
StatusText string
|
||||
HelpInfo string
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// ToCacheKey 为 ErrorPageData 生成一个唯一的 SHA256 字符串键。
|
||||
// 使用 gob 序列化来确保结构体内容到字节序列的顺序一致性,然后计算哈希。
|
||||
func (d ErrorPageData) ToCacheKey() (string, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(d)
|
||||
if err != nil {
|
||||
//logError("Failed to gob encode ErrorPageData for cache key: %v", err)
|
||||
return "", fmt.Errorf("failed to gob encode ErrorPageData for cache key: %w", err)
|
||||
}
|
||||
|
||||
hasher := sha256.New()
|
||||
hasher.Write(buf.Bytes())
|
||||
return hex.EncodeToString(hasher.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func ErrPageUnwarper(errInfo *GHProxyErrors) ErrorPageData {
|
||||
return ErrorPageData{
|
||||
StatusCode: errInfo.StatusCode,
|
||||
StatusDesc: errInfo.StatusDesc,
|
||||
StatusText: errInfo.StatusText,
|
||||
HelpInfo: errInfo.HelpInfo,
|
||||
ErrorMessage: errInfo.ErrorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
// SizedLRUCache 实现了基于字节大小限制的 LRU 缓存。
|
||||
// 它包装了 hashicorp/golang-lru/v2.Cache,并额外管理缓存的总字节大小。
|
||||
type SizedLRUCache struct {
|
||||
cache *lru.Cache[string, []byte]
|
||||
mu sync.Mutex // 保护 currentBytes 字段
|
||||
maxBytes int64 // 缓存的最大字节容量
|
||||
currentBytes int64 // 缓存当前占用的字节数
|
||||
}
|
||||
|
||||
// NewSizedLRUCache 创建一个新的 SizedLRUCache 实例。
|
||||
// 内部的 lru.Cache 的条目容量被设置为一个较大的值 (例如 10000),
|
||||
// 因为主要的逐出逻辑将由字节大小限制来控制。
|
||||
func NewSizedLRUCache(maxBytes int64) (*SizedLRUCache, error) {
|
||||
if maxBytes <= 0 {
|
||||
return nil, fmt.Errorf("maxBytes must be positive")
|
||||
}
|
||||
|
||||
c := &SizedLRUCache{
|
||||
maxBytes: maxBytes,
|
||||
}
|
||||
|
||||
// 创建内部 LRU 缓存,并提供一个 OnEvictedFunc 回调函数。
|
||||
// 当内部 LRU 缓存因其自身的条目容量限制或 RemoveOldest 方法被调用而逐出条目时,
|
||||
// 此回调函数会被执行,从而更新 currentBytes。
|
||||
var err error
|
||||
//c.cache, err = lru.NewWithEvict[string, []byte](10000, func(key string, value []byte) {
|
||||
c.cache, err = lru.NewWithEvict(10000, func(key string, value []byte) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.currentBytes -= int64(len(value))
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Get 从缓存中检索值。
|
||||
func (c *SizedLRUCache) Get(key string) ([]byte, bool) {
|
||||
return c.cache.Get(key)
|
||||
}
|
||||
|
||||
// Add 向缓存中添加或更新一个键值对,并在必要时执行逐出以满足字节限制。
|
||||
func (c *SizedLRUCache) Add(key string, value []byte) {
|
||||
c.mu.Lock() // 保护 currentBytes 和逐出逻辑
|
||||
defer c.mu.Unlock()
|
||||
|
||||
itemSize := int64(len(value))
|
||||
|
||||
// 如果待添加的条目本身就大于缓存的最大容量,则不进行缓存。
|
||||
if itemSize > c.maxBytes {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果键已存在,则首先从 currentBytes 中减去旧值的大小,并从内部 LRU 中移除旧条目。
|
||||
if oldVal, ok := c.cache.Get(key); ok {
|
||||
c.currentBytes -= int64(len(oldVal))
|
||||
c.cache.Remove(key)
|
||||
}
|
||||
|
||||
// 主动逐出最旧的条目,直到有足够的空间容纳新条目。
|
||||
for c.currentBytes+itemSize > c.maxBytes && c.cache.Len() > 0 {
|
||||
_, _, existed := c.cache.RemoveOldest()
|
||||
if !existed {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新条目到内部 LRU 缓存。
|
||||
c.cache.Add(key, value)
|
||||
c.currentBytes += itemSize // 手动增加新条目的大小到 currentBytes。
|
||||
}
|
||||
|
||||
const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量:512KB
|
||||
|
||||
var errorPageCache *SizedLRUCache
|
||||
|
||||
func init() {
|
||||
// 初始化 SizedLRUCache。
|
||||
var err error
|
||||
errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// parsedTemplateOnce 用于确保 HTML 模板只被解析一次。
|
||||
var (
|
||||
parsedTemplateOnce sync.Once
|
||||
parsedTemplate *template.Template
|
||||
parsedTemplateErr error
|
||||
)
|
||||
|
||||
// getParsedTemplate 用于获取缓存的解析后的 HTML 模板。
|
||||
func getParsedTemplate() (*template.Template, error) {
|
||||
parsedTemplateOnce.Do(func() {
|
||||
tmplPath := "page.tmpl"
|
||||
// 确保 errPagesFs 已初始化。这要求在任何 ErrorPage 调用之前调用 InitErrPagesFS。
|
||||
if errPagesFs == nil {
|
||||
parsedTemplateErr = fmt.Errorf("errPagesFs not initialized. Call InitErrPagesFS first")
|
||||
return
|
||||
}
|
||||
parsedTemplate, parsedTemplateErr = template.ParseFS(errPagesFs, tmplPath)
|
||||
if parsedTemplateErr != nil {
|
||||
parsedTemplate = nil
|
||||
}
|
||||
})
|
||||
return parsedTemplate, parsedTemplateErr
|
||||
}
|
||||
|
||||
// htmlTemplateRender 修改为使用缓存的模板。
|
||||
func htmlTemplateRender(data interface{}) ([]byte, error) {
|
||||
tmpl, err := getParsedTemplate()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get parsed template: %w", err)
|
||||
}
|
||||
if tmpl == nil {
|
||||
return nil, fmt.Errorf("template is nil after parsing")
|
||||
}
|
||||
|
||||
// 创建一个 bytes.Buffer 用于存储渲染结果
|
||||
var buf bytes.Buffer
|
||||
|
||||
err = tmpl.Execute(&buf, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute template: %w", err)
|
||||
}
|
||||
|
||||
// 返回 buffer 的内容作为 []byte
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func ErrorPage(c *touka.Context, errInfo *GHProxyErrors) {
|
||||
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
return
|
||||
default:
|
||||
if c.Writer.Written() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 将 errInfo 转换为 ErrorPageData 结构体
|
||||
var err error
|
||||
var cacheKey string
|
||||
pageDataStruct := ErrPageUnwarper(errInfo)
|
||||
// 使用 ErrorPageData 生成一个唯一的 SHA256 缓存键
|
||||
cacheKey, err = pageDataStruct.ToCacheKey()
|
||||
if err != nil {
|
||||
c.Warnf("Failed to generate cache key for error page: %v", err)
|
||||
fallbackErrorJson(c, errInfo)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查生成的缓存键是否为空,这可能表示序列化或哈希计算失败
|
||||
|
||||
if cacheKey == "" {
|
||||
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
|
||||
c.Warnf("Failed to generate cache key for error page: %v", errInfo)
|
||||
return
|
||||
}
|
||||
|
||||
var pageData []byte
|
||||
|
||||
// 尝试从缓存中获取页面数据
|
||||
if cachedPage, found := errorPageCache.Get(cacheKey); found {
|
||||
pageData = cachedPage
|
||||
c.Debugf("Serving error page from cache (Key: %s)", cacheKey)
|
||||
} else {
|
||||
// 如果不在缓存中,则渲染页面
|
||||
pageData, err = htmlTemplateRender(pageDataStruct)
|
||||
if err != nil {
|
||||
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
|
||||
c.Warnf("Failed to render error page for status %d (Key: %s): %v", errInfo.StatusCode, cacheKey, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 将渲染结果存入缓存
|
||||
errorPageCache.Add(cacheKey, pageData)
|
||||
c.Debugf("Cached error page (Key: %s, Size: %d bytes)", cacheKey, len(pageData))
|
||||
}
|
||||
|
||||
c.Raw(errInfo.StatusCode, "text/html; charset=utf-8", pageData)
|
||||
}
|
||||
|
||||
func fallbackErrorJson(c *touka.Context, errInfo *GHProxyErrors) {
|
||||
c.JSON(errInfo.StatusCode, map[string]string{"error": errInfo.ErrorMessage})
|
||||
}
|
||||
183
proxy/gitreq.go
183
proxy/gitreq.go
@@ -1,125 +1,102 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
var (
|
||||
gclient *http.Client
|
||||
gtr *http.Transport
|
||||
)
|
||||
func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, mode string) {
|
||||
|
||||
func initGitHTTPClient() {
|
||||
gtr = &http.Transport{
|
||||
MaxIdleConns: 30,
|
||||
MaxConnsPerHost: 30,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
}
|
||||
gclient = &http.Client{
|
||||
Transport: gtr,
|
||||
}
|
||||
}
|
||||
var (
|
||||
resp *http.Response
|
||||
)
|
||||
|
||||
func GitReq(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)
|
||||
|
||||
// 创建HTTP客户端
|
||||
//client := &http.Client{}
|
||||
|
||||
// 发送HEAD请求, 预获取Content-Length
|
||||
headReq, err := http.NewRequest("HEAD", u, nil)
|
||||
reqBodyReader, err := c.GetReqBodyBuffer()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("创建HEAD请求失败: %v", err))
|
||||
return
|
||||
}
|
||||
setRequestHeaders(c, headReq)
|
||||
AuthPassThrough(c, cfg, headReq)
|
||||
|
||||
headResp, err := gclient.Do(headReq)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// defer headResp.Body.Close()
|
||||
defer func(Body io.ReadCloser) {
|
||||
if err := Body.Close(); err != nil {
|
||||
logError("Failed to close response body: %v", err)
|
||||
}
|
||||
}(headResp.Body)
|
||||
|
||||
contentLength := headResp.Header.Get("Content-Length")
|
||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > sizelimit {
|
||||
finalURL := headResp.Request.URL.String()
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
body, err := readRequestBody(c)
|
||||
if err != nil {
|
||||
HandleError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
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 := gclient.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)
|
||||
// 构建新url
|
||||
var paramStr string
|
||||
if len(queryParams) > 0 {
|
||||
paramStr = "?" + queryParams.Encode()
|
||||
}
|
||||
}(resp.Body)
|
||||
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + paramStr
|
||||
}
|
||||
|
||||
/*
|
||||
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)
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
rb := gitclient.NewRequestBuilder(c.Request.Method, u)
|
||||
rb.NoDefaultHeaders()
|
||||
rb.SetBody(reqBodyReader)
|
||||
rb.WithContext(ctx)
|
||||
|
||||
req, err := rb.Build()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
*/
|
||||
contentLength = resp.Header.Get("Content-Length")
|
||||
|
||||
setRequestHeaders(c, req, cfg, "clone")
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err = gitclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
} else {
|
||||
rb := client.NewRequestBuilder(c.Request.Method, u)
|
||||
rb.NoDefaultHeaders()
|
||||
rb.SetBody(reqBodyReader)
|
||||
rb.WithContext(ctx)
|
||||
|
||||
req, err := rb.Build()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
setRequestHeaders(c, req, cfg, "clone")
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
contentLength := resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
||||
if err != nil {
|
||||
c.Warnf("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, err)
|
||||
}
|
||||
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)
|
||||
c.Warnf("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto, finalURL, size)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
c.SetHeaders(resp.Header)
|
||||
|
||||
headersToRemove := map[string]struct{}{
|
||||
"Content-Security-Policy": {},
|
||||
@@ -131,15 +108,29 @@ func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode s
|
||||
resp.Header.Del(header)
|
||||
}
|
||||
|
||||
if cfg.CORS.Enabled {
|
||||
switch cfg.Server.Cors {
|
||||
case "*":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
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 := io.Copy(c.Writer, 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)
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
c.SetHeader("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||
c.SetHeader("Pragma", "no-cache")
|
||||
c.SetHeader("Expires", "0")
|
||||
}
|
||||
|
||||
bodyReader := resp.Body
|
||||
|
||||
if cfg.RateLimit.BandwidthLimit.Enabled {
|
||||
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
|
||||
}
|
||||
|
||||
c.SetBodyStream(bodyReader, -1)
|
||||
}
|
||||
|
||||
135
proxy/handler.go
135
proxy/handler.go
@@ -2,126 +2,77 @@ package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/auth"
|
||||
"ghproxy/config"
|
||||
"ghproxy/rate"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter, runMode string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 限制访问频率
|
||||
if cfg.RateLimit.Enabled {
|
||||
var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
|
||||
|
||||
var allowed bool
|
||||
func NoRouteHandler(cfg *config.Config) touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
var ctx = c.Request.Context()
|
||||
var shoudBreak bool
|
||||
|
||||
switch cfg.RateLimit.RateMethod {
|
||||
case "ip":
|
||||
allowed = iplimiter.Allow(c.ClientIP())
|
||||
case "total":
|
||||
allowed = limiter.Allow()
|
||||
default:
|
||||
logWarning("Invalid RateLimit Method")
|
||||
return
|
||||
}
|
||||
var (
|
||||
rawPath string
|
||||
matches []string
|
||||
)
|
||||
|
||||
if !allowed {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too Many Requests"})
|
||||
logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Request.Method, c.Request.URL.RequestURI(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/") // 去掉前缀/
|
||||
re := regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
|
||||
matches := re.FindStringSubmatch(rawPath) // 匹配路径
|
||||
rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/
|
||||
matches = re.FindStringSubmatch(rawPath) // 匹配路径
|
||||
|
||||
// 匹配路径错误处理
|
||||
if len(matches) < 3 {
|
||||
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
logWarning(errMsg)
|
||||
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
|
||||
c.Warnf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.UserAgent(), c.Request.Proto)
|
||||
ErrorPage(c, NewErrorWithStatusLookup(400, fmt.Sprintf("Invalid URL Format: %s", c.GetRequestURI())))
|
||||
return
|
||||
}
|
||||
|
||||
// 制作url
|
||||
rawPath = "https://" + matches[2]
|
||||
|
||||
username, repo := MatchUserRepo(rawPath, cfg, c, matches) // 匹配用户名和仓库名
|
||||
var (
|
||||
user string
|
||||
repo string
|
||||
matcher string
|
||||
)
|
||||
|
||||
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, username, repo)
|
||||
repouser := fmt.Sprintf("%s/%s", username, repo)
|
||||
|
||||
// 白名单检查
|
||||
if cfg.Whitelist.Enabled {
|
||||
whitelist := auth.CheckWhitelist(repouser, username, repo)
|
||||
if !whitelist {
|
||||
logErrMsg := fmt.Sprintf("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
|
||||
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
|
||||
logWarning(logErrMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 黑名单检查
|
||||
if cfg.Blacklist.Enabled {
|
||||
blacklist := auth.CheckBlacklist(repouser, username, repo)
|
||||
if blacklist {
|
||||
logErrMsg := fmt.Sprintf("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
|
||||
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
|
||||
logWarning(logErrMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
matches = CheckURL(rawPath, c)
|
||||
if matches == nil {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
logError("%s %s %s %s %s 404-NOMATCH", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
var matcherErr *GHProxyErrors
|
||||
user, repo, matcher, matcherErr = Matcher(rawPath, cfg)
|
||||
if matcherErr != nil {
|
||||
ErrorPage(c, matcherErr)
|
||||
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
|
||||
}
|
||||
shoudBreak = listCheck(cfg, c, user, repo, rawPath)
|
||||
if shoudBreak {
|
||||
return
|
||||
}
|
||||
|
||||
shoudBreak = authCheck(c, cfg, matcher, rawPath)
|
||||
if shoudBreak {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理blob/raw路径
|
||||
if exps[1].MatchString(rawPath) {
|
||||
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
||||
if matcher == "blob" {
|
||||
rawPath = rawPath[18:]
|
||||
rawPath = "https://raw.githubusercontent.com" + rawPath
|
||||
rawPath = strings.Replace(rawPath, "/blob/", "/", 1)
|
||||
matcher = "raw"
|
||||
}
|
||||
|
||||
// 鉴权
|
||||
authcheck, err := auth.AuthHandler(c, cfg)
|
||||
if !authcheck {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
|
||||
logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
}
|
||||
|
||||
// IP METHOD URL USERAGENT PROTO MATCHES
|
||||
logInfo("%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):
|
||||
//ProxyRequest(c, rawPath, cfg, "git", runMode)
|
||||
GitReq(c, rawPath, cfg, "git", runMode)
|
||||
switch matcher {
|
||||
case "releases", "blob", "raw", "gist", "api":
|
||||
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
|
||||
case "clone":
|
||||
GitReq(ctx, c, rawPath, cfg, "git")
|
||||
default:
|
||||
c.String(http.StatusForbidden, "Invalid input.")
|
||||
fmt.Println("Invalid input.")
|
||||
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
|
||||
c.Errorf("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.GetRequestURIPath(), rawPath, matcher)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
118
proxy/httpc.go
Normal file
118
proxy/httpc.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/httpc"
|
||||
)
|
||||
|
||||
var BufferSize int = 32 * 1024 // 32KB
|
||||
|
||||
var (
|
||||
tr *http.Transport
|
||||
gittr *http.Transport
|
||||
client *httpc.Client
|
||||
gitclient *httpc.Client
|
||||
)
|
||||
|
||||
func InitReq(cfg *config.Config) (*httpc.Client, error) {
|
||||
client := initHTTPClient(cfg)
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
initGitHTTPClient(cfg)
|
||||
}
|
||||
err := SetGlobalRateLimit(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
|
||||
}
|
||||
|
||||
func initHTTPClient(cfg *config.Config) *httpc.Client {
|
||||
var proTolcols = new(http.Protocols)
|
||||
proTolcols.SetHTTP1(true)
|
||||
proTolcols.SetHTTP2(true)
|
||||
proTolcols.SetUnencryptedHTTP2(true)
|
||||
|
||||
switch cfg.Httpc.Mode {
|
||||
case "auto", "":
|
||||
tr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
case "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,
|
||||
}
|
||||
default:
|
||||
panic("unknown httpc mode: " + cfg.Httpc.Mode)
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func initGitHTTPClient(cfg *config.Config) {
|
||||
switch cfg.Httpc.Mode {
|
||||
case "auto", "":
|
||||
gittr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
case "advanced":
|
||||
gittr = &http.Transport{
|
||||
MaxIdleConns: cfg.Httpc.MaxIdleConns,
|
||||
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
|
||||
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
default:
|
||||
panic("unknown httpc mode: " + cfg.Httpc.Mode)
|
||||
}
|
||||
|
||||
if cfg.Outbound.Enabled {
|
||||
initTransport(cfg, gittr)
|
||||
}
|
||||
|
||||
var opts []httpc.Option // 使用切片来收集选项
|
||||
opts = append(opts, httpc.WithTransport(gittr))
|
||||
var protocolsConfig httpc.ProtocolsConfig
|
||||
|
||||
if cfg.GitClone.ForceH2C {
|
||||
protocolsConfig.ForceH2C = true
|
||||
} else {
|
||||
protocolsConfig.Http1 = true
|
||||
protocolsConfig.Http2 = true
|
||||
protocolsConfig.Http2_Cleartext = true
|
||||
}
|
||||
opts = append(opts, httpc.WithProtocols(protocolsConfig))
|
||||
|
||||
if cfg.Server.Debug {
|
||||
opts = append(opts, httpc.WithDumpLog())
|
||||
}
|
||||
|
||||
gitclient = httpc.New(opts...)
|
||||
}
|
||||
211
proxy/match.go
Normal file
211
proxy/match.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
githubPrefix = "https://github.com/"
|
||||
rawPrefix = "https://raw.githubusercontent.com/"
|
||||
gistPrefix = "https://gist.github.com/"
|
||||
gistContentPrefix = "https://gist.githubusercontent.com/"
|
||||
apiPrefix = "https://api.github.com/"
|
||||
githubPrefixLen int
|
||||
rawPrefixLen int
|
||||
gistPrefixLen int
|
||||
gistContentPrefixLen int
|
||||
apiPrefixLen int
|
||||
)
|
||||
|
||||
func init() {
|
||||
githubPrefixLen = len(githubPrefix)
|
||||
rawPrefixLen = len(rawPrefix)
|
||||
gistPrefixLen = len(gistPrefix)
|
||||
gistContentPrefixLen = len(gistContentPrefix)
|
||||
apiPrefixLen = len(apiPrefix)
|
||||
}
|
||||
|
||||
// Matcher 从原始URL路径中高效地解析并匹配代理规则.
|
||||
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
|
||||
if len(rawPath) < 18 {
|
||||
return "", "", "", NewErrorWithStatusLookup(404, "path too short")
|
||||
}
|
||||
|
||||
// 匹配 "https://github.com/"
|
||||
if strings.HasPrefix(rawPath, githubPrefix) {
|
||||
remaining := rawPath[githubPrefixLen:]
|
||||
i := strings.IndexByte(remaining, '/')
|
||||
if i <= 0 {
|
||||
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing user")
|
||||
}
|
||||
user := remaining[:i]
|
||||
remaining = remaining[i+1:]
|
||||
i = strings.IndexByte(remaining, '/')
|
||||
if i <= 0 {
|
||||
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing repo")
|
||||
}
|
||||
repo := remaining[:i]
|
||||
remaining = remaining[i+1:]
|
||||
if len(remaining) == 0 {
|
||||
return "", "", "", NewErrorWithStatusLookup(400, "malformed github path: missing action")
|
||||
}
|
||||
i = strings.IndexByte(remaining, '/')
|
||||
action := remaining
|
||||
if i != -1 {
|
||||
action = remaining[:i]
|
||||
}
|
||||
var matcher string
|
||||
switch action {
|
||||
case "releases", "archive":
|
||||
matcher = "releases"
|
||||
case "blob":
|
||||
matcher = "blob"
|
||||
case "raw":
|
||||
matcher = "raw"
|
||||
case "info", "git-upload-pack":
|
||||
matcher = "clone"
|
||||
default:
|
||||
return "", "", "", NewErrorWithStatusLookup(400, fmt.Sprintf("unsupported github action: %s", action))
|
||||
}
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
|
||||
// 匹配 "https://raw.githubusercontent.com/"
|
||||
if strings.HasPrefix(rawPath, rawPrefix) {
|
||||
remaining := rawPath[rawPrefixLen:]
|
||||
// 这里的逻辑与 github.com 的类似, 需要提取 user, repo, branch, file...
|
||||
// 我们只需要 user 和 repo
|
||||
i := strings.IndexByte(remaining, '/')
|
||||
if i <= 0 {
|
||||
return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing user")
|
||||
}
|
||||
user := remaining[:i]
|
||||
remaining = remaining[i+1:]
|
||||
i = strings.IndexByte(remaining, '/')
|
||||
if i <= 0 {
|
||||
return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing repo")
|
||||
}
|
||||
repo := remaining[:i]
|
||||
// raw 链接至少需要 user/repo/branch 三部分
|
||||
remaining = remaining[i+1:]
|
||||
if len(remaining) == 0 {
|
||||
return "", "", "", NewErrorWithStatusLookup(400, "malformed raw url: missing branch/commit")
|
||||
}
|
||||
return user, repo, "raw", nil
|
||||
}
|
||||
|
||||
// 匹配 "https://gist.github.com/"
|
||||
if strings.HasPrefix(rawPath, gistPrefix) {
|
||||
remaining := rawPath[gistPrefixLen:]
|
||||
i := strings.IndexByte(remaining, '/')
|
||||
if i <= 0 {
|
||||
// case: https://gist.github.com/user
|
||||
// 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的
|
||||
if len(remaining) > 0 {
|
||||
return remaining, "", "gist", nil
|
||||
}
|
||||
return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user")
|
||||
}
|
||||
// case: https://gist.github.com/user/gist_id...
|
||||
user := remaining[:i]
|
||||
return user, "", "gist", nil
|
||||
}
|
||||
|
||||
// 匹配 "https://gist.githubusercontent.com/"
|
||||
if strings.HasPrefix(rawPath, gistContentPrefix) {
|
||||
remaining := rawPath[gistContentPrefixLen:]
|
||||
i := strings.IndexByte(remaining, '/')
|
||||
if i <= 0 {
|
||||
// case: https://gist.githubusercontent.com/user
|
||||
// 这种情况下, gist_id 缺失, 但我们仍然可以认为 user 是有效的
|
||||
if len(remaining) > 0 {
|
||||
return remaining, "", "gist", nil
|
||||
}
|
||||
return "", "", "", NewErrorWithStatusLookup(400, "malformed gist url: missing user")
|
||||
}
|
||||
// case: https://gist.githubusercontent.com/user/gist_id...
|
||||
user := remaining[:i]
|
||||
return user, "", "gist", nil
|
||||
}
|
||||
|
||||
// 匹配 "https://api.github.com/"
|
||||
if strings.HasPrefix(rawPath, apiPrefix) {
|
||||
if !cfg.Auth.ForceAllowApi && (cfg.Auth.Method != "header" || !cfg.Auth.Enabled) {
|
||||
return "", "", "", NewErrorWithStatusLookup(403, "API proxy requires header authentication")
|
||||
}
|
||||
remaining := rawPath[apiPrefixLen:]
|
||||
var user, repo string
|
||||
if strings.HasPrefix(remaining, "repos/") {
|
||||
parts := strings.SplitN(remaining[6:], "/", 3)
|
||||
if len(parts) >= 2 {
|
||||
user = parts[0]
|
||||
repo = parts[1]
|
||||
}
|
||||
} else if strings.HasPrefix(remaining, "users/") {
|
||||
parts := strings.SplitN(remaining[6:], "/", 2)
|
||||
if len(parts) >= 1 {
|
||||
user = parts[0]
|
||||
}
|
||||
}
|
||||
return user, repo, "api", nil
|
||||
}
|
||||
|
||||
return "", "", "", NewErrorWithStatusLookup(404, "no matcher found for the given path")
|
||||
}
|
||||
|
||||
var (
|
||||
proxyableMatchersMap map[string]struct{}
|
||||
initMatchersOnce sync.Once
|
||||
)
|
||||
|
||||
func initMatchers() {
|
||||
initMatchersOnce.Do(func() {
|
||||
matchers := []string{"blob", "raw", "gist"}
|
||||
proxyableMatchersMap = make(map[string]struct{}, len(matchers))
|
||||
for _, m := range matchers {
|
||||
proxyableMatchersMap[m] = struct{}{}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// matchString 与原始版本签名兼容
|
||||
func matchString(target string) bool {
|
||||
initMatchers()
|
||||
_, exists := proxyableMatchersMap[target]
|
||||
return exists
|
||||
}
|
||||
|
||||
// extractParts 与原始版本签名兼容
|
||||
func extractParts(rawURL string) (string, string, string, url.Values, error) {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", "", "", nil, err
|
||||
}
|
||||
|
||||
path := parsedURL.Path
|
||||
if len(path) > 0 && path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
parts := strings.SplitN(path, "/", 3)
|
||||
|
||||
if len(parts) < 2 {
|
||||
return "", "", "", nil, fmt.Errorf("URL path is too short")
|
||||
}
|
||||
|
||||
repoOwner := "/" + parts[0]
|
||||
repoName := "/" + parts[1]
|
||||
var remainingPath string
|
||||
if len(parts) > 2 {
|
||||
remainingPath = "/" + parts[2]
|
||||
}
|
||||
|
||||
return repoOwner, repoName, remainingPath, parsedURL.Query(), nil
|
||||
}
|
||||
|
||||
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)
|
||||
315
proxy/matcher_test.go
Normal file
315
proxy/matcher_test.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMatcher_Compatibility(t *testing.T) {
|
||||
// --- 准备各种配置用于测试 ---
|
||||
cfgWithAuth := &config.Config{
|
||||
Auth: config.AuthConfig{Enabled: true, Method: "header", ForceAllowApi: false},
|
||||
}
|
||||
cfgNoAuth := &config.Config{
|
||||
Auth: config.AuthConfig{Enabled: false},
|
||||
}
|
||||
cfgApiForceAllowed := &config.Config{
|
||||
Auth: config.AuthConfig{ForceAllowApi: true},
|
||||
}
|
||||
cfgWrongAuthMethod := &config.Config{
|
||||
Auth: config.AuthConfig{Enabled: true, Method: "none"},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rawPath string
|
||||
config *config.Config
|
||||
expectedUser string
|
||||
expectedRepo string
|
||||
expectedMatcher string
|
||||
expectError bool
|
||||
expectedErrCode int
|
||||
}{
|
||||
{
|
||||
name: "GH Releases Path",
|
||||
rawPath: "https://github.com/owner/repo/releases/download/v1.0/asset.zip",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "releases",
|
||||
},
|
||||
{
|
||||
name: "GH Archive Path",
|
||||
rawPath: "https://github.com/owner/repo.git/archive/main.zip",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "releases",
|
||||
},
|
||||
{
|
||||
name: "GH Blob Path",
|
||||
rawPath: "https://github.com/owner/repo/blob/main/path/to/file.go",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "blob",
|
||||
},
|
||||
{
|
||||
name: "GH Raw Path",
|
||||
rawPath: "https://github.com/owner/repo/raw/main/image.png",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw",
|
||||
},
|
||||
{
|
||||
name: "GH Clone Info Refs",
|
||||
rawPath: "https://github.com/owner/repo.git/info/refs?service=git-upload-pack",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo.git", expectedMatcher: "clone",
|
||||
},
|
||||
{
|
||||
name: "GH Clone Git Upload Pack",
|
||||
rawPath: "https://github.com/owner/repo/git-upload-pack",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "clone",
|
||||
},
|
||||
{
|
||||
name: "Girhub Broken Path",
|
||||
rawPath: "https://github.com/owner",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
|
||||
{
|
||||
name: "RawGHUserContent Path",
|
||||
rawPath: "https://raw.githubusercontent.com/owner/repo/branch/file.sh",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "raw",
|
||||
},
|
||||
{
|
||||
name: "Gist Path",
|
||||
rawPath: "https://gist.github.com/user/abcdef1234567890",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "user", expectedRepo: "", expectedMatcher: "gist",
|
||||
},
|
||||
{
|
||||
name: "Gist UserContent Path",
|
||||
rawPath: "https://gist.githubusercontent.com/user/abcdef1234567890",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "user", expectedRepo: "", expectedMatcher: "gist",
|
||||
},
|
||||
{
|
||||
name: "API Repos Path (with Auth)",
|
||||
rawPath: "https://api.github.com/repos/owner/repo/pulls",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api",
|
||||
},
|
||||
{
|
||||
name: "API Users Path (with Auth)",
|
||||
rawPath: "https://api.github.com/users/someuser/repos",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "someuser", expectedRepo: "", expectedMatcher: "api",
|
||||
},
|
||||
{
|
||||
name: "API Other Path (with Auth)",
|
||||
rawPath: "https://api.github.com/octocat",
|
||||
config: cfgWithAuth,
|
||||
expectedUser: "", expectedRepo: "", expectedMatcher: "api",
|
||||
},
|
||||
{
|
||||
name: "API Path (Force Allowed)",
|
||||
rawPath: "https://api.github.com/repos/owner/repo",
|
||||
config: cfgApiForceAllowed, // Auth disabled, but force allowed
|
||||
expectedUser: "owner", expectedRepo: "repo", expectedMatcher: "api",
|
||||
},
|
||||
{
|
||||
name: "Malformed GH Path (no repo)",
|
||||
rawPath: "https://github.com/owner/",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Malformed GH Path (no action)",
|
||||
rawPath: "https://github.com/owner/repo",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Malformed GH Path (empty user)",
|
||||
rawPath: "https://github.com//repo/blob/main/file.go",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Malformed Raw Path (no repo)",
|
||||
rawPath: "https://raw.githubusercontent.com/owner/",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Malformed Gist Path (no user)",
|
||||
rawPath: "https://gist.github.com/",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "Unsupported GH Action",
|
||||
rawPath: "https://github.com/owner/repo/issues/123",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 400,
|
||||
},
|
||||
{
|
||||
name: "API Path (No Auth)",
|
||||
rawPath: "https://api.github.com/user",
|
||||
config: cfgNoAuth,
|
||||
expectError: true, expectedErrCode: 403,
|
||||
},
|
||||
{
|
||||
name: "API Path (Wrong Auth Method)",
|
||||
rawPath: "https://api.github.com/user",
|
||||
config: cfgWrongAuthMethod,
|
||||
expectError: true, expectedErrCode: 403,
|
||||
},
|
||||
{
|
||||
name: "No Matcher Found (other domain)",
|
||||
rawPath: "https://bitbucket.org/owner/repo",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 404,
|
||||
},
|
||||
{
|
||||
name: "No Matcher Found (path too short)",
|
||||
rawPath: "https://a.co",
|
||||
config: cfgWithAuth,
|
||||
expectError: true, expectedErrCode: 404,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
user, repo, matcher, ghErr := Matcher(tc.rawPath, tc.config)
|
||||
|
||||
if tc.expectError {
|
||||
if ghErr == nil {
|
||||
t.Fatalf("Expected a GHProxyErrors error, but got nil")
|
||||
}
|
||||
if ghErr.StatusCode != tc.expectedErrCode {
|
||||
t.Errorf("Expected error code %d, but got %d (msg: %s)",
|
||||
tc.expectedErrCode, ghErr.StatusCode, ghErr.ErrorMessage)
|
||||
}
|
||||
} else {
|
||||
if ghErr != nil {
|
||||
t.Fatalf("Expected no error, but got: %s", ghErr.ErrorMessage)
|
||||
}
|
||||
if user != tc.expectedUser {
|
||||
t.Errorf("user: got %q, want %q", user, tc.expectedUser)
|
||||
}
|
||||
if repo != tc.expectedRepo {
|
||||
t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo)
|
||||
}
|
||||
if matcher != tc.expectedMatcher {
|
||||
t.Errorf("matcher: got %q, want %q", matcher, tc.expectedMatcher)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractParts_Compatibility(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
rawURL string
|
||||
expectedOwner string
|
||||
expectedRepo string
|
||||
expectedRem string
|
||||
expectedQuery url.Values
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Standard git clone URL",
|
||||
rawURL: "https://github.com/WJQSERVER-STUDIO/go-utils.git/info/refs?service=git-upload-pack",
|
||||
expectedOwner: "/WJQSERVER-STUDIO",
|
||||
expectedRepo: "/go-utils.git",
|
||||
expectedRem: "/info/refs",
|
||||
expectedQuery: url.Values{"service": []string{"git-upload-pack"}},
|
||||
},
|
||||
{
|
||||
name: "No remaining path",
|
||||
rawURL: "https://example.com/owner/repo",
|
||||
expectedOwner: "/owner",
|
||||
expectedRepo: "/repo",
|
||||
expectedRem: "",
|
||||
expectedQuery: url.Values{},
|
||||
},
|
||||
{
|
||||
name: "Root path only",
|
||||
rawURL: "https://example.com/",
|
||||
expectError: true, // Path is too short
|
||||
},
|
||||
{
|
||||
name: "One level path",
|
||||
rawURL: "https://example.com/owner",
|
||||
expectError: true, // Path is too short
|
||||
},
|
||||
{
|
||||
name: "Empty path segments",
|
||||
rawURL: "https://example.com//repo/a", // Will be treated as /repo/a
|
||||
expectedOwner: "", // First part is empty
|
||||
expectedRepo: "/repo",
|
||||
expectedRem: "/a",
|
||||
},
|
||||
{
|
||||
name: "Invalid URL format",
|
||||
rawURL: "://invalid",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
owner, repo, rem, query, err := extractParts(tc.rawURL)
|
||||
|
||||
if (err != nil) != tc.expectError {
|
||||
t.Fatalf("extractParts() error = %v, expectError %v", err, tc.expectError)
|
||||
}
|
||||
|
||||
if !tc.expectError {
|
||||
if owner != tc.expectedOwner {
|
||||
t.Errorf("owner: got %q, want %q", owner, tc.expectedOwner)
|
||||
}
|
||||
if repo != tc.expectedRepo {
|
||||
t.Errorf("repo: got %q, want %q", repo, tc.expectedRepo)
|
||||
}
|
||||
if rem != tc.expectedRem {
|
||||
t.Errorf("remaining path: got %q, want %q", rem, tc.expectedRem)
|
||||
}
|
||||
if !reflect.DeepEqual(query, tc.expectedQuery) {
|
||||
t.Errorf("query: got %v, want %v", query, tc.expectedQuery)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchString_Compatibility(t *testing.T) {
|
||||
testCases := []struct {
|
||||
target string
|
||||
expected bool
|
||||
}{
|
||||
{"blob", true}, {"raw", true}, {"gist", true},
|
||||
{"clone", false}, {"releases", false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.target, func(t *testing.T) {
|
||||
if got := matchString(tc.target); got != tc.expected {
|
||||
t.Errorf("matchString('%s') = %v; want %v", tc.target, got, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMatcher(b *testing.B) {
|
||||
cfg := &config.Config{}
|
||||
path := "https://github.com/WJQSERVER/speedtest-ex/releases/download/v1.2.0/speedtest-linux-amd64.tar.gz"
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _, _ = Matcher(path, cfg)
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 预定义regex
|
||||
var (
|
||||
pathRegex = regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`) // 匹配路径
|
||||
gistRegex = regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.*`) // 匹配gist路径
|
||||
)
|
||||
|
||||
// 提取用户名和仓库名
|
||||
func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches []string) (string, string) {
|
||||
if gistMatches := gistRegex.FindStringSubmatch(rawPath); len(gistMatches) == 3 {
|
||||
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], ""
|
||||
}
|
||||
// 定义路径
|
||||
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 "", ""
|
||||
}
|
||||
180
proxy/nest.go
Normal file
180
proxy/nest.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func EditorMatcher(rawPath string, cfg *config.Config) (bool, error) {
|
||||
// 匹配 "https://github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://github.com") {
|
||||
return true, nil
|
||||
}
|
||||
// 匹配 "https://raw.githubusercontent.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
|
||||
return true, nil
|
||||
}
|
||||
// 匹配 "https://raw.github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://raw.github.com") {
|
||||
return true, nil
|
||||
}
|
||||
// 匹配 "https://gist.githubusercontent.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
|
||||
return true, nil
|
||||
}
|
||||
// 匹配 "https://gist.github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://gist.github.com") {
|
||||
return true, nil
|
||||
}
|
||||
if cfg.Shell.RewriteAPI {
|
||||
// 匹配 "https://api.github.com/"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://api.github.com") {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 匹配文件扩展名是sh的rawPath
|
||||
func MatcherShell(rawPath string) bool {
|
||||
return strings.HasSuffix(rawPath, ".sh")
|
||||
}
|
||||
|
||||
// LinkProcessor 是一个函数类型,用于处理提取到的链接。
|
||||
type LinkProcessor func(string) string
|
||||
|
||||
// 自定义 URL 修改函数
|
||||
func modifyURL(url string, host string, cfg *config.Config) string {
|
||||
// 去除url内的https://或http://
|
||||
matched, err := EditorMatcher(url, cfg)
|
||||
if err != nil {
|
||||
return url
|
||||
}
|
||||
if matched {
|
||||
var u = url
|
||||
u = strings.TrimPrefix(u, "https://")
|
||||
u = strings.TrimPrefix(u, "http://")
|
||||
return "https://" + host + "/" + u
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// processLinks 处理链接,返回包含处理后数据的 io.Reader
|
||||
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config, c *touka.Context) (readerOut io.Reader, written int64, err error) {
|
||||
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
|
||||
readerOut = pipeReader
|
||||
|
||||
go func() { // 在 Goroutine 中执行写入操作
|
||||
defer func() {
|
||||
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
|
||||
if err != nil {
|
||||
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
|
||||
c.Errorf("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
|
||||
}
|
||||
} else {
|
||||
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
|
||||
c.Errorf("pipeWriter close failed: %v", closeErr)
|
||||
if err == nil { // 如果之前没有错误,记录关闭错误
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if err := input.Close(); err != nil {
|
||||
c.Errorf("input close failed: %v", err)
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
var bufReader *bufio.Reader
|
||||
|
||||
if compress == "gzip" {
|
||||
// 解压gzip
|
||||
gzipReader, gzipErr := gzip.NewReader(input)
|
||||
if gzipErr != nil {
|
||||
err = fmt.Errorf("gzip解压错误: %v", gzipErr)
|
||||
return // Goroutine 中使用 return 返回错误
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
bufReader = bufio.NewReader(gzipReader)
|
||||
} else {
|
||||
bufReader = bufio.NewReader(input)
|
||||
}
|
||||
|
||||
var bufWriter *bufio.Writer
|
||||
var gzipWriter *gzip.Writer
|
||||
|
||||
// 根据是否gzip确定 writer 的创建
|
||||
if compress == "gzip" {
|
||||
gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter
|
||||
bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小
|
||||
} else {
|
||||
bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter
|
||||
}
|
||||
|
||||
//确保writer关闭
|
||||
defer func() {
|
||||
var closeErr error // 局部变量,用于保存defer中可能发生的错误
|
||||
|
||||
if gzipWriter != nil {
|
||||
if closeErr = gzipWriter.Close(); closeErr != nil {
|
||||
c.Errorf("gzipWriter close failed %v", closeErr)
|
||||
// 如果已经存在错误,则保留。否则,记录此错误。
|
||||
if err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
if flushErr := bufWriter.Flush(); flushErr != nil {
|
||||
c.Errorf("writer flush failed %v", flushErr)
|
||||
// 如果已经存在错误,则保留。否则,记录此错误。
|
||||
if err == nil {
|
||||
err = flushErr
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 使用正则表达式匹配 http 和 https 链接
|
||||
for {
|
||||
line, readErr := bufReader.ReadString('\n')
|
||||
if readErr != nil {
|
||||
if readErr == io.EOF {
|
||||
break // 文件结束
|
||||
}
|
||||
err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误
|
||||
return // Goroutine 中使用 return 返回错误
|
||||
}
|
||||
|
||||
// 替换所有匹配的 URL
|
||||
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
|
||||
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
|
||||
})
|
||||
|
||||
n, writeErr := bufWriter.WriteString(modifiedLine)
|
||||
written += int64(n) // 更新写入的字节数
|
||||
if writeErr != nil {
|
||||
err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误
|
||||
return // Goroutine 中使用 return 返回错误
|
||||
}
|
||||
}
|
||||
|
||||
// 在返回之前,再刷新一次 (虽然 defer 中已经有 flush,但这里再加一次确保及时刷新)
|
||||
if flushErr := bufWriter.Flush(); flushErr != nil {
|
||||
if err == nil { // 避免覆盖之前的错误
|
||||
err = flushErr
|
||||
}
|
||||
return // Goroutine 中使用 return 返回错误
|
||||
}
|
||||
}()
|
||||
|
||||
return readerOut, written, nil // 返回 reader 和 written,error 由 Goroutine 通过 pipeWriter.CloseWithError 传递
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 日志模块
|
||||
var (
|
||||
logw = logger.Logw
|
||||
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 {
|
||||
return nil, fmt.Errorf("failed to read request body: %v", err)
|
||||
}
|
||||
defer c.Request.Body.Close()
|
||||
return body, nil
|
||||
}
|
||||
|
||||
/*
|
||||
func SendRequest(c *gin.Context, req *req.Request, method, url string) (*req.Response, error) {
|
||||
switch method {
|
||||
case "GET":
|
||||
return req.Get(url)
|
||||
case "POST":
|
||||
return req.Post(url)
|
||||
case "PUT":
|
||||
return req.Put(url)
|
||||
case "DELETE":
|
||||
return req.Delete(url)
|
||||
default:
|
||||
// IP METHOD URL USERAGENT PROTO UNSUPPORTED-METHOD
|
||||
errmsg := fmt.Sprintf("%s %s %s %s %s Unsupported method", c.ClientIP(), method, url, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
logWarning(errmsg)
|
||||
return nil, fmt.Errorf(errmsg)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func HandleError(c *gin.Context, message string) {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
|
||||
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
|
||||
}
|
||||
|
||||
/*
|
||||
// 处理响应大小
|
||||
func HandleResponseSize(resp *http.Response, cfg *config.Config, c *gin.Context) error {
|
||||
contentLength := resp.Header.Get("Content-Length")
|
||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > sizelimit {
|
||||
finalURL := resp.Request.URL.String()
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
|
||||
return fmt.Errorf("Path: %s size limit exceeded: %d", finalURL, size)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
@@ -1,79 +0,0 @@
|
||||
package proxy
|
||||
|
||||
/*
|
||||
func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
|
||||
method := c.Request.Method
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
|
||||
client := createHTTPClient(mode)
|
||||
if runMode == "dev" {
|
||||
client.DevMode()
|
||||
}
|
||||
|
||||
// 发送HEAD请求, 预获取Content-Length
|
||||
headReq := client.R()
|
||||
setRequestHeaders(c, headReq)
|
||||
AuthPassThrough(c, cfg, headReq)
|
||||
|
||||
headResp, err := headReq.Head(u)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer headResp.Body.Close()
|
||||
|
||||
if err := HandleResponseSize(headResp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := readRequestBody(c)
|
||||
if err != nil {
|
||||
HandleError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
req := client.R().SetBody(body)
|
||||
setRequestHeaders(c, req)
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err := SendRequest(c, req, method, u)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := HandleResponseSize(resp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
}
|
||||
|
||||
CopyResponseHeaders(resp, c, cfg)
|
||||
c.Status(resp.StatusCode)
|
||||
if err := copyResponseBody(c, resp.Body); err != nil {
|
||||
logError("%s %s %s %s %s Response-Copy-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制响应体
|
||||
func copyResponseBody(c *gin.Context, respBody io.Reader) error {
|
||||
_, err := io.Copy(c.Writer, respBody)
|
||||
return err
|
||||
}
|
||||
|
||||
// 判断并选择TLS指纹
|
||||
func createHTTPClient(mode string) *req.Client {
|
||||
client := req.C()
|
||||
switch mode {
|
||||
case "chrome":
|
||||
client.SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36").
|
||||
SetTLSFingerprintChrome().
|
||||
ImpersonateChrome()
|
||||
case "git":
|
||||
client.SetUserAgent("git/2.33.1")
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
*/
|
||||
@@ -1,21 +1,79 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
// 设置请求头
|
||||
func setRequestHeaders(c *gin.Context, req *http.Request) {
|
||||
for key, values := range c.Request.Header {
|
||||
for _, value := range values {
|
||||
req.Header.Set(key, value)
|
||||
var (
|
||||
respHeadersToRemove = map[string]struct{}{
|
||||
"Content-Security-Policy": {},
|
||||
"Referrer-Policy": {},
|
||||
"Strict-Transport-Security": {},
|
||||
"X-Github-Request-Id": {},
|
||||
"X-Timer": {},
|
||||
"X-Served-By": {},
|
||||
"X-Fastly-Request-Id": {},
|
||||
}
|
||||
|
||||
reqHeadersToRemove = map[string]struct{}{
|
||||
"CF-IPCountry": {},
|
||||
"CF-RAY": {},
|
||||
"CF-Visitor": {},
|
||||
"CF-Connecting-IP": {},
|
||||
"CF-EW-Via": {},
|
||||
"CDN-Loop": {},
|
||||
"Upgrade": {},
|
||||
"Connection": {},
|
||||
}
|
||||
|
||||
cloneHeadersToRemove = map[string]struct{}{
|
||||
"CF-IPCountry": {},
|
||||
"CF-RAY": {},
|
||||
"CF-Visitor": {},
|
||||
"CF-Connecting-IP": {},
|
||||
"CF-EW-Via": {},
|
||||
"CDN-Loop": {},
|
||||
}
|
||||
)
|
||||
|
||||
// 预定义headers
|
||||
var (
|
||||
defaultHeaders = map[string]string{
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Transfer-Encoding": "chunked",
|
||||
"User-Agent": "GHProxy/1.0",
|
||||
}
|
||||
)
|
||||
|
||||
// copyHeader 将所有头部从 src 复制到 dst。
|
||||
// 对于多值头部,它会为每个值调用 Add,从而保留所有值。
|
||||
func copyHeader(dst, src http.Header) {
|
||||
for k, vv := range src {
|
||||
for _, v := range vv {
|
||||
dst.Add(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeWSHeader(req *http.Request) {
|
||||
req.Header.Del("Upgrade")
|
||||
req.Header.Del("Connection")
|
||||
func setRequestHeaders(c *touka.Context, req *http.Request, cfg *config.Config, matcher string) {
|
||||
if matcher == "raw" && cfg.Httpc.UseCustomRawHeaders {
|
||||
// 使用预定义Header
|
||||
for key, value := range defaultHeaders {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
} else if matcher == "clone" {
|
||||
copyHeader(req.Header, c.Request.Header)
|
||||
for key := range cloneHeadersToRemove {
|
||||
req.Header.Del(key)
|
||||
}
|
||||
} else {
|
||||
copyHeader(req.Header, c.Request.Header)
|
||||
for key := range reqHeadersToRemove {
|
||||
req.Header.Del(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
69
proxy/routing.go
Normal file
69
proxy/routing.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"strings"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func RoutingHandler(cfg *config.Config) touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
|
||||
var shoudBreak bool
|
||||
|
||||
var (
|
||||
rawPath string
|
||||
)
|
||||
|
||||
rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/
|
||||
|
||||
var (
|
||||
user string
|
||||
repo string
|
||||
)
|
||||
|
||||
user = c.Param("user")
|
||||
repo = c.Param("repo")
|
||||
matcher, exists := c.GetString("matcher")
|
||||
if !exists {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(500, "Matcher Not Found in Context"))
|
||||
c.Errorf("Matcher Not Found in Context Path: %s", c.GetRequestURIPath())
|
||||
return
|
||||
}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
|
||||
shoudBreak = listCheck(cfg, c, user, repo, rawPath)
|
||||
if shoudBreak {
|
||||
return
|
||||
}
|
||||
|
||||
shoudBreak = authCheck(c, cfg, matcher, rawPath)
|
||||
if shoudBreak {
|
||||
return
|
||||
}
|
||||
|
||||
// 处理blob/raw路径
|
||||
if matcher == "blob" {
|
||||
rawPath = rawPath[10:]
|
||||
rawPath = "raw.githubusercontent.com" + rawPath
|
||||
rawPath = strings.Replace(rawPath, "/blob/", "/", 1)
|
||||
matcher = "raw"
|
||||
}
|
||||
|
||||
// 为rawpath加入https:// 头
|
||||
rawPath = "https://" + rawPath
|
||||
|
||||
switch matcher {
|
||||
case "releases", "blob", "raw", "gist", "api":
|
||||
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
|
||||
case "clone":
|
||||
GitReq(ctx, c, rawPath, cfg, "git")
|
||||
default:
|
||||
ErrorPage(c, NewErrorWithStatusLookup(500, "Matched But Not Matched"))
|
||||
c.Errorf("Matched But Not Matched Path: %s rawPath: %s matcher: %s", c.GetRequestURIPath(), rawPath, matcher)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
62
proxy/utils.go
Normal file
62
proxy/utils.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/auth"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func listCheck(cfg *config.Config, c *touka.Context, user string, repo string, rawPath string) bool {
|
||||
if cfg.Auth.ForceAllowApi && cfg.Auth.ForceAllowApiPassList {
|
||||
return false
|
||||
}
|
||||
// 白名单检查
|
||||
if cfg.Whitelist.Enabled {
|
||||
whitelist := auth.CheckWhitelist(user, repo)
|
||||
if !whitelist {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Whitelist Blocked repo: %s/%s", user, repo)))
|
||||
c.Infof("%s %s %s %s %s Whitelist Blocked repo: %s/%s", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, user, repo)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 黑名单检查
|
||||
if cfg.Blacklist.Enabled {
|
||||
blacklist := auth.CheckBlacklist(user, repo)
|
||||
if blacklist {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, fmt.Sprintf("Blacklist Blocked repo: %s/%s", user, repo)))
|
||||
c.Infof("%s %s %s %s %s Blacklist Blocked repo: %s/%s", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, user, repo)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 鉴权
|
||||
func authCheck(c *touka.Context, cfg *config.Config, matcher string, rawPath string) bool {
|
||||
var err error
|
||||
|
||||
if matcher == "api" && !cfg.Auth.ForceAllowApi {
|
||||
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(403, "Github API Req without AuthHeader is Not Allowed"))
|
||||
c.Infof("%s %s %s AuthHeader Unavailable", c.ClientIP(), c.Request.Method, rawPath)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 鉴权
|
||||
if cfg.Auth.Enabled {
|
||||
var authcheck bool
|
||||
authcheck, err = auth.AuthHandler(c, cfg)
|
||||
if !authcheck {
|
||||
ErrorPage(c, NewErrorWithStatusLookup(401, fmt.Sprintf("Unauthorized: %v", err)))
|
||||
c.Infof("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.UserAgent(), c.Request.Proto, err)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
63
rate/rate.go
63
rate/rate.go
@@ -1,63 +0,0 @@
|
||||
package rate
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// 日志输出
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
// 总体限流器
|
||||
type RateLimiter struct {
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
// 基于IP的限流器
|
||||
type IPRateLimiter struct {
|
||||
limiters map[string]*RateLimiter
|
||||
limit int
|
||||
burst int
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
func New(limit int, burst int, duration time.Duration) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
limiter: rate.NewLimiter(rate.Limit(float64(limit)/duration.Seconds()), burst),
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) Allow() bool {
|
||||
return rl.limiter.Allow()
|
||||
}
|
||||
|
||||
func NewIPRateLimiter(limit int, burst int, duration time.Duration) *IPRateLimiter {
|
||||
return &IPRateLimiter{
|
||||
limiters: make(map[string]*RateLimiter),
|
||||
limit: limit,
|
||||
burst: burst,
|
||||
duration: duration,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *IPRateLimiter) Allow(ip string) bool {
|
||||
if ip == "" {
|
||||
logWarning("empty ip")
|
||||
return false
|
||||
}
|
||||
|
||||
limiter, ok := rl.limiters[ip]
|
||||
if !ok {
|
||||
// 创建新的 RateLimiter 并存储
|
||||
limiter = New(rl.limit, rl.burst, rl.duration)
|
||||
rl.limiters[ip] = limiter
|
||||
}
|
||||
return limiter.Allow()
|
||||
}
|
||||
258
weakcache/weakcache.go
Normal file
258
weakcache/weakcache.go
Normal file
@@ -0,0 +1,258 @@
|
||||
package weakcache
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
"time"
|
||||
"weak" // Go 1.24 引入的 weak 包
|
||||
)
|
||||
|
||||
// DefaultExpiration 默认过期时间,这里设置为 15 分钟。
|
||||
// 这是一个导出的常量,方便用户使用包时引用默认值。
|
||||
const DefaultExpiration = 5 * time.Minute
|
||||
|
||||
// cleanupInterval 是后台清理 Go routine 的扫描间隔,这里设置为 5 分钟。
|
||||
// 这是一个内部常量,不导出。
|
||||
const cleanupInterval = 2 * time.Minute
|
||||
|
||||
// cacheEntry 缓存项的内部结构。不导出。
|
||||
type cacheEntry[T any] struct {
|
||||
Value T
|
||||
Expiration time.Time
|
||||
key string // 存储key,方便在list.Element中引用
|
||||
}
|
||||
|
||||
// Cache 是一个基于 weak.Pointer, 带有过期和大小上限 (FIFO) 的泛型缓存。
|
||||
// 这是一个导出的类型。
|
||||
type Cache[T any] struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
// 修正:缓存存储:key -> weak.Pointer 到 cacheEntry 结构体 (而不是指向结构体的指针)
|
||||
// weak.Make(*cacheEntry[T]) 返回 weak.Pointer[cacheEntry[T]]
|
||||
data map[string]weak.Pointer[cacheEntry[T]]
|
||||
|
||||
// FIFO 链表:存储 key 的 list.Element
|
||||
// 链表头部是最近放入的,尾部是最早放入的(最老的)
|
||||
fifoList *list.List
|
||||
// FIFO 元素的映射:key -> *list.Element
|
||||
fifoMap map[string]*list.Element
|
||||
|
||||
defaultExpiration time.Duration
|
||||
maxSize int // 缓存最大容量,0 表示无限制
|
||||
|
||||
stopCleanup chan struct{}
|
||||
wg sync.WaitGroup // 用于等待清理 Go routine 退出
|
||||
}
|
||||
|
||||
// NewCache 创建一个新的缓存实例。
|
||||
// expiration: 新添加项的默认过期时间。如果为 0,则使用 DefaultExpiration。
|
||||
// maxSize: 缓存的最大容量,0 表示无限制。当达到上限时,采用 FIFO 策略淘汰。
|
||||
// 这是一个导出的构造函数。
|
||||
func NewCache[T any](expiration time.Duration, maxSize int) *Cache[T] {
|
||||
if expiration <= 0 {
|
||||
expiration = DefaultExpiration
|
||||
}
|
||||
|
||||
c := &Cache[T]{
|
||||
// 修正:初始化 map,值类型已修正
|
||||
data: make(map[string]weak.Pointer[cacheEntry[T]]),
|
||||
fifoList: list.New(),
|
||||
fifoMap: make(map[string]*list.Element),
|
||||
defaultExpiration: expiration,
|
||||
maxSize: maxSize,
|
||||
stopCleanup: make(chan struct{}),
|
||||
}
|
||||
// 启动后台清理 Go routine
|
||||
c.wg.Add(1)
|
||||
go c.cleanupLoop()
|
||||
return c
|
||||
}
|
||||
|
||||
// Put 将值放入缓存。如果 key 已存在,会更新其值和过期时间。
|
||||
// 这是导出的方法。
|
||||
func (c *Cache[T]) Put(key string, value T) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
expiration := now.Add(c.defaultExpiration)
|
||||
|
||||
// 如果 key 已经存在,更新其值和过期时间。
|
||||
// 在 FIFO 策略中, Put 更新不改变其在链表中的位置,除非旧的 entry 已经被 GC。
|
||||
if elem, ok := c.fifoMap[key]; ok {
|
||||
// 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]]
|
||||
if wp, dataOk := c.data[key]; dataOk {
|
||||
// wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T]
|
||||
entry := wp.Value()
|
||||
if entry != nil {
|
||||
// 旧的 cacheEntry 仍在内存中,直接更新
|
||||
entry.Value = value
|
||||
entry.Expiration = expiration
|
||||
// 在严格 FIFO 中,更新不移动位置
|
||||
return
|
||||
}
|
||||
// 如果 weak.Pointer.Value() 为 nil,说明之前的 cacheEntry 已经被 GC 了
|
||||
// 此时需要创建一个新的 entry,并将其从旧位置移除,再重新添加
|
||||
c.fifoList.Remove(elem)
|
||||
delete(c.fifoMap, key)
|
||||
} else {
|
||||
c.fifoList.Remove(elem)
|
||||
delete(c.fifoMap, key)
|
||||
}
|
||||
}
|
||||
|
||||
// 新建缓存项 (注意这里是结构体值,而不是指针)
|
||||
// weak.Make 接收的是指针 *T
|
||||
entry := &cacheEntry[T]{ // 创建结构体指针
|
||||
Value: value,
|
||||
Expiration: expiration,
|
||||
key: key, // 存储 key
|
||||
}
|
||||
|
||||
// 将新的 *cacheEntry[T] 包装成 weak.Pointer[cacheEntry[T]] 存入 data map
|
||||
// weak.Make(entry) 现在返回 weak.Pointer[cacheEntry[T]],类型匹配 data map 的值类型
|
||||
c.data[key] = weak.Make(entry)
|
||||
|
||||
// 添加到 FIFO 链表头部 (最近放入/更新的在头部)
|
||||
// PushFront 返回新的 list.Element
|
||||
c.fifoMap[key] = c.fifoList.PushFront(key)
|
||||
|
||||
// 检查大小上限并进行淘汰 (淘汰尾部的最老项)
|
||||
c.evictIfNeeded()
|
||||
}
|
||||
|
||||
// Get 从缓存中获取值。返回获取到的值和是否存在/是否有效。
|
||||
// 这是导出的方法。
|
||||
func (c *Cache[T]) Get(key string) (T, bool) {
|
||||
c.mu.RLock() // 先读锁
|
||||
// 从 data map 中获取弱引用,wp 的类型现在是 weak.Pointer[cacheEntry[T]]
|
||||
wp, ok := c.data[key]
|
||||
c.mu.RUnlock() // 立即释放读锁,如果需要写操作(removeEntry)可以获得锁
|
||||
|
||||
var zero T // 零值
|
||||
|
||||
if !ok {
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// 尝试获取实际的 cacheEntry 指针
|
||||
// wp.Value() 返回 *cacheEntry[T], entry 的类型现在是 *cacheEntry[T]
|
||||
entry := wp.Value()
|
||||
|
||||
if entry == nil {
|
||||
// 对象已被GC回收,需要清理此弱引用
|
||||
c.removeEntry(key) // 内部会加写锁
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// 检查过期时间 (通过 entry 指针访问字段)
|
||||
if time.Now().After(entry.Expiration) {
|
||||
// 逻辑上已过期
|
||||
c.removeEntry(key) // 内部会加写锁
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// 在 FIFO 缓存中,Get 操作不改变项在链表中的位置
|
||||
return entry.Value, true // 通过 entry 指针访问值字段
|
||||
}
|
||||
|
||||
// removeEntry 从缓存中移除项。
|
||||
// 这个方法是内部使用的,不导出。需要被调用者确保持有写锁,或者内部自己加锁。
|
||||
// 考虑到 Get 和 cleanupLoop 可能会调用,让其内部自己加锁更安全。
|
||||
func (c *Cache[T]) removeEntry(key string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// 从 data map 中删除
|
||||
delete(c.data, key)
|
||||
|
||||
// 从 FIFO 链表和 fifoMap 中删除
|
||||
if elem, ok := c.fifoMap[key]; ok {
|
||||
c.fifoList.Remove(elem)
|
||||
delete(c.fifoMap, key)
|
||||
}
|
||||
}
|
||||
|
||||
// evictIfNeeded 检查是否需要淘汰最老(FIFO 链表尾部)的项。
|
||||
// 这个方法是内部使用的,不导出。必须在持有写锁的情况下调用。
|
||||
func (c *Cache[T]) evictIfNeeded() {
|
||||
if c.maxSize > 0 && c.fifoList.Len() > c.maxSize {
|
||||
// 淘汰 FIFO 链表尾部的元素 (最老的)
|
||||
oldest := c.fifoList.Back()
|
||||
if oldest != nil {
|
||||
keyToEvict := oldest.Value.(string) // 链表元素存储的是 key
|
||||
c.fifoList.Remove(oldest)
|
||||
delete(c.fifoMap, keyToEvict)
|
||||
delete(c.data, keyToEvict) // 移除弱引用
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size 返回当前缓存中的弱引用项数量。
|
||||
// 注意:这个数量可能包含已被 GC 回收但尚未清理的项。
|
||||
// 这是一个导出的方法。
|
||||
func (c *Cache[T]) Size() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.data)
|
||||
}
|
||||
|
||||
// cleanupLoop 后台清理 Go routine。不导出。
|
||||
func (c *Cache[T]) cleanupLoop() {
|
||||
defer c.wg.Done()
|
||||
// 使用内部常量 cleanupInterval
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.cleanupExpiredAndGCed()
|
||||
case <-c.stopCleanup:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupExpiredAndGCed 扫描并清理已过期或已被 GC 回收的项。不导出。
|
||||
func (c *Cache[T]) cleanupExpiredAndGCed() {
|
||||
c.mu.Lock() // 清理时需要写锁
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
keysToRemove := make([]string, 0, len(c.data)) // 预估容量
|
||||
|
||||
// 遍历 data map 查找需要清理的键
|
||||
for key, wp := range c.data {
|
||||
// wp 的类型是 weak.Pointer[cacheEntry[T]]
|
||||
// wp.Value() 返回 *cacheEntry[T], entry 的类型是 *cacheEntry[T]
|
||||
entry := wp.Value() // 尝试获取强引用
|
||||
|
||||
if entry == nil {
|
||||
// 已被 GC 回收
|
||||
keysToRemove = append(keysToRemove, key)
|
||||
} else if now.After(entry.Expiration) {
|
||||
// 逻辑过期 (通过 entry 指针访问字段)
|
||||
keysToRemove = append(keysToRemove, key)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行删除操作
|
||||
for _, key := range keysToRemove {
|
||||
// 从 data map 中删除
|
||||
delete(c.data, key)
|
||||
// 从 FIFO 链表和 fifoMap 中删除
|
||||
// 需要再次检查 fifoMap,因为在持有锁期间,evictIfNeeded 可能已经移除了这个 key
|
||||
if elem, ok := c.fifoMap[key]; ok {
|
||||
c.fifoList.Remove(elem)
|
||||
delete(c.fifoMap, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StopCleanup 停止后台清理 Go routine。
|
||||
// 这是一个导出的方法。
|
||||
func (c *Cache[T]) StopCleanup() {
|
||||
close(c.stopCleanup)
|
||||
c.wg.Wait() // 等待 Go routine 退出
|
||||
}
|
||||
Reference in New Issue
Block a user