Compare commits
441 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
286fa0f311 | ||
|
|
5d08993cbc | ||
|
|
6e787ced6e | ||
|
|
460b7514a9 | ||
|
|
c90140a898 | ||
|
|
f7e4fe71d7 | ||
|
|
33973b786d | ||
|
|
36fe815e35 | ||
|
|
c393191b93 | ||
|
|
beb441f0b0 | ||
|
|
c45adfb915 | ||
|
|
102dc00b27 | ||
|
|
b0042397c9 | ||
|
|
70b46c0fb2 | ||
|
|
5258046faa | ||
|
|
eddd37a59c | ||
|
|
7a57317a8b | ||
|
|
6f95e1c182 | ||
|
|
31b0b72450 | ||
|
|
00ae38753e | ||
|
|
2aa665d89a | ||
|
|
17a2ba173d | ||
|
|
a0e5846e11 | ||
|
|
972baee564 | ||
|
|
a281d4c779 | ||
|
|
e4252d0596 | ||
|
|
de65889a4d | ||
|
|
90b9c69dad | ||
|
|
acd38f4fe0 | ||
|
|
83e6b78a93 | ||
|
|
8371f9564f | ||
|
|
546a8ca981 | ||
|
|
be6314bd53 | ||
|
|
28331e9ec5 | ||
|
|
51f179f9e9 | ||
|
|
33eb0e2d34 | ||
|
|
9bfca20d11 | ||
|
|
ede418420d | ||
|
|
243172c988 | ||
|
|
a8d2b0700c | ||
|
|
891ce86101 | ||
|
|
1bf4eca13d | ||
|
|
e01028df08 | ||
|
|
708f25c02b | ||
|
|
31678b243c | ||
|
|
0944b81dcb | ||
|
|
723b849ee0 | ||
|
|
a537c09491 | ||
|
|
6f050d38ac | ||
|
|
d8c57b7191 | ||
|
|
a77f265a17 | ||
|
|
7b9a18225a | ||
|
|
44105fc0cf | ||
|
|
3d742960cc | ||
|
|
0fb7ee3679 | ||
|
|
efe734d976 | ||
|
|
890dc067b9 | ||
|
|
a43f1f20f6 | ||
|
|
e59c118475 | ||
|
|
d1f862e799 | ||
|
|
d5e3b7fc23 | ||
|
|
8dea98f795 | ||
|
|
b1a60017e4 | ||
|
|
96bd4a2c18 | ||
|
|
43c576bd82 | ||
|
|
580865d082 | ||
|
|
c2af045019 | ||
|
|
d297f785db | ||
|
|
f6f1ff7362 | ||
|
|
920b025fda | ||
|
|
59428e47aa | ||
|
|
e3f9a0d4c6 | ||
|
|
a14202f75d | ||
|
|
9d59731afe | ||
|
|
784407b2dd | ||
|
|
32fcc1071f | ||
|
|
32584f9516 | ||
|
|
0ce5c2c11c | ||
|
|
1bfb2af51d | ||
|
|
6bd4a84dc1 | ||
|
|
06dc764063 | ||
|
|
c55ae4a5b7 | ||
|
|
85896ffbfe | ||
|
|
4397bc2cf9 | ||
|
|
3ea6d34fb2 | ||
|
|
391c4b3594 | ||
|
|
0ab01a9b83 | ||
|
|
8787c11e06 | ||
|
|
06191e52e5 | ||
|
|
2113205123 | ||
|
|
67a006df0f | ||
|
|
45b99393e6 | ||
|
|
b6c4dbbf9c | ||
|
|
d9cc088dce | ||
|
|
723ab6c3e9 | ||
|
|
11ba7cb0e5 | ||
|
|
f1ff3e9d27 | ||
|
|
b1e3a9c1bd | ||
|
|
267dfafcb9 | ||
|
|
e32adadaff | ||
|
|
942dda86e7 | ||
|
|
bbfc7c0d6d | ||
|
|
5af898b3e6 | ||
|
|
3c162bf9d7 | ||
|
|
1eed79463d | ||
|
|
6f67f6f5b4 | ||
|
|
2a5570a447 | ||
|
|
eb45ab8798 | ||
|
|
2b7e949658 | ||
|
|
fe652821bd | ||
|
|
92b3e19954 | ||
|
|
19f753a515 | ||
|
|
157cbf9d3a | ||
|
|
f80782b803 | ||
|
|
b1c8658a1c | ||
|
|
57cd7e1ce7 | ||
|
|
29046382a5 | ||
|
|
2355447f97 | ||
|
|
8187464007 | ||
|
|
575e36ef90 | ||
|
|
c31e887ad3 | ||
|
|
b633bec69b | ||
|
|
052827bca7 | ||
|
|
0c1eb34e49 | ||
|
|
3489e6d744 | ||
|
|
a8af0fb687 | ||
|
|
c0af779642 | ||
|
|
d7949f625a | ||
|
|
ddd12729a4 | ||
|
|
91b7cfe533 | ||
|
|
aee81ba4fd | ||
|
|
e5d941414e | ||
|
|
c50f23c399 | ||
|
|
47d062a1c4 | ||
|
|
57ba06e01e | ||
|
|
52fdaf5f81 | ||
|
|
d6b8f2b812 | ||
|
|
00d2113904 | ||
|
|
670bca31ca | ||
|
|
f77de0d37a | ||
|
|
1c18ccc363 | ||
|
|
a386304d42 | ||
|
|
a1fdd0f477 | ||
|
|
e94a98668e | ||
|
|
22273f166c | ||
|
|
2187514da7 | ||
|
|
ac859c39a6 | ||
|
|
518bbf2600 | ||
|
|
d383c2d2a6 | ||
|
|
6d055e19e3 | ||
|
|
18f78385f3 | ||
|
|
2edb6f6dd7 | ||
|
|
d42ed119d4 | ||
|
|
072f73fdce | ||
|
|
02b4857334 | ||
|
|
a156f1b2e7 | ||
|
|
1d8c3914d2 | ||
|
|
b05dd37500 | ||
|
|
7fabd3c3e2 | ||
|
|
6dc20398ae | ||
|
|
03118a24a3 | ||
|
|
0cb7c9f948 | ||
|
|
824656f9d0 | ||
|
|
e3d56ae9b7 | ||
|
|
794ba22232 | ||
|
|
f71c95e381 | ||
|
|
997860f3ef | ||
|
|
81c35030e6 | ||
|
|
80d2bc0068 | ||
|
|
4173617e72 | ||
|
|
6a1a1b3c8b | ||
|
|
a21d5659ac | ||
|
|
a0de2f627d | ||
|
|
b47638d066 | ||
|
|
1b5b34d265 | ||
|
|
505c2e559e | ||
|
|
dd9a0c8adb | ||
|
|
f4cb77a72e | ||
|
|
4155b9cf4f | ||
|
|
00538f3d91 | ||
|
|
1fd1e3bc2a | ||
|
|
b83c242416 |
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: 报告问题与漏洞
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### 问题描述
|
||||
|
||||
请简要描述发现的问题是什么,以及如何重现。
|
||||
|
||||
### 复现步骤
|
||||
|
||||
1. 打开...
|
||||
2. 点击...
|
||||
3. 观察到...
|
||||
|
||||
### 预期行为
|
||||
|
||||
请描述在正常情况下应该发生什么。
|
||||
|
||||
### 实际行为
|
||||
|
||||
请描述实际发生了什么。
|
||||
|
||||
### 截图
|
||||
|
||||
如果适用,请添加截图以帮助解释您的问题。
|
||||
|
||||
### 环境信息
|
||||
|
||||
- 发行版: [例如 Debian12, Alpine-Edge ]
|
||||
- 部署方式: [可执行文件/Docker ]
|
||||
- GHProxy版本: [例如 1.0.0]
|
||||
|
||||
### 附加信息
|
||||
|
||||
请提供任何其他可能有助于我们解决问题的信息。
|
||||
20
.github/ISSUE_TEMPLATE/features_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/features_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Features request
|
||||
about: 提出新功能建议
|
||||
title: "[Features]"
|
||||
labels: 改进
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### 功能描述
|
||||
|
||||
请简要描述您希望增加的功能。
|
||||
|
||||
### 功能原因
|
||||
|
||||
请说明您为什么需要这个功能。
|
||||
|
||||
### 功能实现
|
||||
|
||||
请详细描述您期望的功能实现。
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -8,4 +8,4 @@ updates:
|
||||
- package-ecosystem: "gomod" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
interval: "daily"
|
||||
|
||||
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
|
||||
122
.github/workflows/build-dev.yml
vendored
122
.github/workflows/build-dev.yml
vendored
@@ -4,52 +4,88 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'dev'
|
||||
paths:
|
||||
- 'DEV-VERSION'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
GO_VERSION: 1.23.2
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Load VERSION
|
||||
- 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: Set up Go
|
||||
- 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, darwin, freebsd]
|
||||
goarch: [amd64, arm64]
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
GO_VERSION: 1.24
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev
|
||||
- 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: |
|
||||
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:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Install UPX
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install upx -y
|
||||
- name: Build
|
||||
- name: 编译
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
CGO_ENABLED=0 go build -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
|
||||
upx -9 ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
|
||||
- name: Package
|
||||
CGO_ENABLED=0 go build -ldflags "-X main.version=${{ env.VERSION }} -X main.dev=true" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
|
||||
- name: 打包
|
||||
run: |
|
||||
tar -czvf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
|
||||
- name: Upload to GitHub Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
mkdir ghproxyd
|
||||
cp ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/
|
||||
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 .
|
||||
ls
|
||||
- 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
|
||||
@@ -57,11 +93,12 @@ jobs:
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ env.VERSION }}
|
||||
artifacts: ./${{ env.OUTPUT_BINARY }}*
|
||||
artifacts: ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ env.VERSION }}
|
||||
allowUpdates: true
|
||||
prerelease: true
|
||||
body: ${{ env.VERSION }}
|
||||
env:
|
||||
export PATH: $PATH:/usr/local/go/bin
|
||||
|
||||
@@ -70,12 +107,14 @@ jobs:
|
||||
needs: build
|
||||
env:
|
||||
IMAGE_NAME: wjqserver/ghproxy
|
||||
PROG_NAME: ghproxy
|
||||
DOCKERFILE: docker/dockerfile/dev/Dockerfile
|
||||
DOCKERFILE_PATH: docker/dockerfile/dev
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev
|
||||
- name: Load VERSION
|
||||
run: |
|
||||
if [ -f DEV-VERSION ]; then
|
||||
@@ -96,25 +135,12 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t ${{ env.PROG_NAME }}:dev,${{ env.PROG_NAME }}:${{ env.VERSION }} -f ./${{ env.DOCKERFILE }}
|
||||
|
||||
- name: Install Slim
|
||||
run: |
|
||||
curl -L -o ds.tar.gz https://github.com/slimtoolkit/slim/releases/download/1.40.11/dist_linux.tar.gz
|
||||
tar -xvf ds.tar.gz
|
||||
mv dist_linux/slim /usr/local/bin/
|
||||
mv dist_linux/slim-sensor /usr/local/bin/
|
||||
rm -rf ds.tar.gz dist_linux
|
||||
|
||||
- name: Optimize Docker image using Slim
|
||||
run: |
|
||||
slimlim build --include-path /data --http-probe --continue-after 20 --tag ${{ env.PROG_NAME }}:${{ env.VERSION }} ${{ env.PROG_NAME }}:dev
|
||||
|
||||
- name: Push Docker image
|
||||
run: |
|
||||
docker tag ${{ env.PROG_NAME }}:${{ env.VERSION }} ${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
docker tag ${{ env.PROG_NAME }}:dev ${{ env.IMAGE_NAME }}:dev
|
||||
docker push ${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
docker push ${{ env.IMAGE_NAME }}:dev
|
||||
- name: 构建镜像
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./${{ env.DOCKERFILE }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
${{ env.IMAGE_NAME }}:dev
|
||||
91
.github/workflows/build.yml
vendored
91
.github/workflows/build.yml
vendored
@@ -9,47 +9,83 @@ on:
|
||||
- 'VERSION'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux]
|
||||
goarch: [amd64, arm64]
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
GO_VERSION: 1.23.2
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Load VERSION
|
||||
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: Set up Go
|
||||
- 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:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Install UPX
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y upx
|
||||
- name: Build
|
||||
- name: 编译
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
|
||||
upx -9 ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
|
||||
- name: Package
|
||||
CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${{ env.VERSION }}" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
|
||||
- name: 打包
|
||||
run: |
|
||||
tar -czvf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
|
||||
- name: Upload to GitHub Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
mkdir ghproxyd
|
||||
cp ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/
|
||||
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: 上传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
|
||||
@@ -57,12 +93,14 @@ jobs:
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ env.VERSION }}
|
||||
artifacts: ./${{ env.OUTPUT_BINARY }}*
|
||||
artifacts: ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ env.VERSION }}
|
||||
allowUpdates: true
|
||||
body: ${{ env.VERSION }}
|
||||
env:
|
||||
export PATH: $PATH:/usr/local/go/bin
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build # 确保这个作业在 build 作业完成后运行
|
||||
@@ -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 }}:v4
|
||||
${{ env.IMAGE_NAME }}:latest
|
||||
wjqserver/ghproxy-touka:latest
|
||||
wjqserver/ghproxy-touka:${{ env.VERSION }}
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
demo
|
||||
demo.toml
|
||||
*.log
|
||||
*.bak
|
||||
list.json
|
||||
repos
|
||||
pages
|
||||
*_test
|
||||
1344
CHANGELOG.md
1344
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
24w15a
|
||||
4.1.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.
|
||||
185
README.md
185
README.md
@@ -1,154 +1,129 @@
|
||||
# GhProxy
|
||||
# GHProxy
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
|
||||
|
||||
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
|
||||
|
||||
[DEMO](https://ghproxy.1888866.xyz)
|
||||
GHProxy是一个基于Go的支持代理Github仓库资源和API的项目, 同时支持Docker镜像代理与脚本嵌套加速等多种功能
|
||||
|
||||
## 项目说明
|
||||
|
||||
### 项目特点
|
||||
|
||||
- 基于Go语言实现,使用[Gin框架](https://github.com/gin-gonic/gin)与[req库](https://github.com/imroc/req)]
|
||||
- 支持Git clone,raw,realeases等文件拉取
|
||||
- 支持Docker部署
|
||||
- 支持速率限制
|
||||
- 支持用户鉴权
|
||||
- 支持自定义黑名单
|
||||
- 符合[RFC 7234](https://httpwg.org/specs/rfc7234.html)的HTTP Cache
|
||||
- 使用Caddy作为Web Server
|
||||
- 基于[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)的重构版本,实现了原项目原定功能的同时,进一步优化了性能**
|
||||
本项目源于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)与[WJQSERVER/ghproxy-go-0RTT](https://github.com/WJQSERVER/ghproxy-go-0RTT)两个项目,前者带来了实现框架与资源,后者带来了解决Git clone问题的办法,使得本项目从net/http标准库切换至Gin框架,已解决此困扰已久的问题,在此基础上,本项目进一步优化了性能,并添加了用户鉴权功能,使得部署更加安全可靠。
|
||||
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
|
||||
[DEMO](https://ghproxy.1888866.xyz)
|
||||
|
||||
- V1.0.0 迁移至本仓库,并再次重构内容实现
|
||||
- v0.2.0 重构项目实现,Git clone的实现完全自主化
|
||||
[TG讨论群组](https://t.me/ghproxy_go)
|
||||
|
||||
### LICENSE
|
||||
[相关文章](https://blog.wjqserver.com/categories/my-program/)
|
||||
|
||||
本项目使用WSL LICENSE Version1.2 (WJQSERVER STUDIO LICENSE Version1.2)
|
||||
[GHProxy项目文档](https://wjqserver-docs.pages.dev/docs/ghproxy/) 感谢 [@redbunnys](https://github.com/redbunnys)的维护
|
||||
|
||||
在v1.0.0版本之前,本项目继承于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的APACHE2.0 LICENSE VERSION
|
||||
### 使用示例
|
||||
|
||||
## 使用示例
|
||||
|
||||
```
|
||||
```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
|
||||
|
||||
```
|
||||
docker run -p 7210:80 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log -v ./ghproxy/config:/data/ghproxy/config --restart always wjqserver/ghproxy
|
||||
docker run -p 7210:8080 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log -v ./ghproxy/config:/data/ghproxy/config --restart always wjqserver/ghproxy
|
||||
```
|
||||
|
||||
- Docker-Compose
|
||||
- Docker-Compose (建议使用)
|
||||
|
||||
参看[docker-compose.yml](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docker/compose/docker-compose.yml)
|
||||
|
||||
### 外部配置文件
|
||||
### 二进制文件部署(不推荐)
|
||||
|
||||
本项目采用config.yaml作为外部配置,默认配置如下
|
||||
使用Docker部署时,慎重修改config.yaml,以免造成不必要的麻烦
|
||||
|
||||
```yaml
|
||||
# 核心配置
|
||||
server:
|
||||
port: 8080 # 监听端口(小白请勿修改)
|
||||
host: "127.0.0.1" # 监听地址(小白请勿修改)
|
||||
sizelimit: 131072000 # 125MB
|
||||
|
||||
# 日志配置
|
||||
logger:
|
||||
logfilepath: "/data/ghproxy/log/ghproxy.log" # 日志文件路径(小白请勿修改)
|
||||
maxlogsize: 5 # MB
|
||||
|
||||
# CORS 配置
|
||||
cors:
|
||||
enabled: true # 是否开启CORS
|
||||
|
||||
# 鉴权配置
|
||||
auth:
|
||||
enabled: false # 是否开启鉴权
|
||||
authtoken: "test" # 鉴权Token
|
||||
|
||||
# 黑名单配置
|
||||
blacklist:
|
||||
enabled: true # 是否开启黑名单
|
||||
blacklistfile: "/data/ghproxy/config/blacklist.json"
|
||||
|
||||
# 白名单配置
|
||||
whitelist:
|
||||
enabled: false # 是否开启白名单
|
||||
whitelistfile: "/data/ghproxy/config/whitelist.json"
|
||||
一键部署脚本:
|
||||
|
||||
```bash
|
||||
wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/install.sh && chmod +x install.sh &&./install.sh
|
||||
```
|
||||
|
||||
### 黑名单配置
|
||||
Dev一键部署脚本:
|
||||
|
||||
黑名单配置位于config/blacklist.json,格式如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"blacklist": [
|
||||
"test/test1",
|
||||
"example/repo2",
|
||||
"another/repo3"
|
||||
]
|
||||
}
|
||||
```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/whitelist.json,格式如下:
|
||||
参看[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md)
|
||||
|
||||
```json
|
||||
{
|
||||
"whitelist": [
|
||||
"test/test1",
|
||||
"example/repo2",
|
||||
"another/repo3"
|
||||
]
|
||||
}
|
||||
```
|
||||
### 前端页面
|
||||
|
||||
### Caddy反代配置
|
||||
参看[GHProxy-Frontend](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
|
||||
|
||||
```Caddyfile
|
||||
example.com {
|
||||
reverse_proxy {
|
||||
to 127.0.0.1:7210
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
|
||||
}
|
||||
encode zstd gzip
|
||||
}
|
||||
```
|
||||
## 项目简史
|
||||
|
||||
## TODO & DEV
|
||||
本项目旨在于构建一个高效且功能多样的GHProxy
|
||||
|
||||
### TODO
|
||||
- v4.0.0 迁移到[Touka框架](https://github.com/infinite-iroha/touka)
|
||||
- v3.0.0 迁移到HertZ框架, 进一步提升效率
|
||||
- v2.4.1 对路径匹配进行优化
|
||||
- v2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
|
||||
- v1.0.0 迁移至本仓库,并再次重构内容实现
|
||||
- v0.2.0 重构项目实现
|
||||
|
||||
- [x] 允许更多参数通过config结构传入
|
||||
- [x] 改进程序效率
|
||||
- [x] 用户鉴权
|
||||
- [x] 仓库黑名单
|
||||
- [x] 仓库白名单
|
||||
## LICENSE
|
||||
|
||||
### DEV
|
||||
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/) 双重许可, 您可从中选择一个使用
|
||||
|
||||
- [x] Docker Pull 代理
|
||||
前端位于单独仓库中, 且各个主题均存在各自的许可证, 本项目许可证并不包括前端
|
||||
|
||||
在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,15 +6,29 @@
|
||||
|
||||
| 版本 | 是否支持 |
|
||||
| --- | --- |
|
||||
| v1.x.x | :white_check_mark: |
|
||||
| **w**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 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/)
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
|
||||
133
api/api.go
Normal file
133
api/api.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"ghproxy/middleware/nocache"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func InitHandleRouter(cfg *config.Config, r *touka.Engine, version string) {
|
||||
apiRouter := r.Group("/api", nocache.NoCacheMiddleware())
|
||||
{
|
||||
apiRouter.GET("/size_limit", func(c *touka.Context) {
|
||||
SizeLimitHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/whitelist/status", func(c *touka.Context) {
|
||||
WhiteListStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/blacklist/status", func(c *touka.Context) {
|
||||
BlackListStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/cors/status", func(c *touka.Context) {
|
||||
CorsStatusHandler(cfg, c)
|
||||
})
|
||||
apiRouter.GET("/healthcheck", func(c *touka.Context) {
|
||||
HealthcheckHandler(c)
|
||||
})
|
||||
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 *touka.Context) {
|
||||
RateLimitStatusHandler(cfg, c)
|
||||
})
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func SizeLimitHandler(cfg *config.Config, c *touka.Context) {
|
||||
sizeLimit := cfg.Server.SizeLimit
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"MaxResponseBodySize": sizeLimit,
|
||||
}))
|
||||
}
|
||||
|
||||
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(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Blacklist": cfg.Blacklist.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 *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 *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(cfg *config.Config, c *touka.Context) {
|
||||
c.SetHeader("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"RateLimit": cfg.RateLimit.Enabled,
|
||||
}))
|
||||
}
|
||||
|
||||
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,
|
||||
}))
|
||||
}
|
||||
32
auth/auth-header.go
Normal file
32
auth/auth-header.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func AuthHeaderHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) {
|
||||
if !cfg.Auth.Enabled {
|
||||
return true, nil
|
||||
}
|
||||
// 获取"GH-Auth"的值
|
||||
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 == "" {
|
||||
return false, fmt.Errorf("Auth token not found")
|
||||
}
|
||||
|
||||
isValid = authToken == cfg.Auth.Token
|
||||
if !isValid {
|
||||
return false, fmt.Errorf("Auth token incorrect")
|
||||
}
|
||||
|
||||
return isValid, nil
|
||||
}
|
||||
32
auth/auth-parameters.go
Normal file
32
auth/auth-parameters.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func AuthParametersHandler(c *touka.Context, cfg *config.Config) (isValid bool, err error) {
|
||||
if !cfg.Auth.Enabled {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var authToken string
|
||||
if cfg.Auth.Key != "" {
|
||||
authToken = c.Query(cfg.Auth.Key)
|
||||
} else {
|
||||
authToken = c.Query("auth_token")
|
||||
}
|
||||
|
||||
if authToken == "" {
|
||||
return false, fmt.Errorf("Auth token not found")
|
||||
}
|
||||
|
||||
isValid = authToken == cfg.Auth.Token
|
||||
if !isValid {
|
||||
return false, fmt.Errorf("Auth token invalid")
|
||||
}
|
||||
|
||||
return isValid, nil
|
||||
}
|
||||
55
auth/auth.go
55
auth/auth.go
@@ -1,35 +1,40 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"ghproxy/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
var logw = logger.Logw
|
||||
|
||||
func AuthHandler(c *gin.Context, cfg *config.Config) bool {
|
||||
// 如果身份验证未启用,直接返回 true
|
||||
if !cfg.Auth.Enabled {
|
||||
return true
|
||||
func ListInit(cfg *config.Config) error {
|
||||
if cfg.Blacklist.Enabled {
|
||||
err := InitBlacklist(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 auth_token 参数
|
||||
authToken := c.Query("auth_token")
|
||||
logw("auth_token received: %s", authToken)
|
||||
|
||||
// 验证 token
|
||||
if authToken == "" {
|
||||
logw("auth FAILED: no auth_token provided")
|
||||
return false
|
||||
if cfg.Whitelist.Enabled {
|
||||
err := InitWhitelist(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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.Method == "header" {
|
||||
isValid, err = AuthHeaderHandler(c, cfg)
|
||||
return isValid, err
|
||||
} else if cfg.Auth.Method == "" {
|
||||
c.Errorf("Auth method not set")
|
||||
return true, nil
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
|
||||
isValid := authToken == cfg.Auth.AuthToken
|
||||
if !isValid {
|
||||
logw("auth FAILED: invalid auth_token: %s", authToken)
|
||||
}
|
||||
|
||||
logw("auth SUCCESS: %t", isValid)
|
||||
return isValid
|
||||
}
|
||||
|
||||
@@ -1,45 +1,91 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"encoding/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{}
|
||||
|
||||
data, err := os.ReadFile(blacklistfile)
|
||||
if err != nil {
|
||||
logw("Failed to read blacklist file: %v", err)
|
||||
// InitBlacklist 初始化黑名单(线程安全,仅执行一次)
|
||||
func InitBlacklist(cfg *config.Config) error {
|
||||
instance = &Blacklist{
|
||||
userSet: make(map[string]struct{}),
|
||||
repoSet: make(map[string]map[string]struct{}),
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, blacklist)
|
||||
data, err := os.ReadFile(cfg.Blacklist.BlacklistFile)
|
||||
if err != nil {
|
||||
logw("Failed to unmarshal blacklist JSON: %v", err)
|
||||
return fmt.Errorf("failed to read blacklist: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckBlacklist(fullrepo string) bool {
|
||||
return forRangeCheckBlacklist(blacklist.Blacklist, fullrepo)
|
||||
}
|
||||
var list struct {
|
||||
Entries []string `json:"blacklist"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &list); err != nil {
|
||||
return fmt.Errorf("invalid blacklist format: %w", err)
|
||||
}
|
||||
|
||||
func forRangeCheckBlacklist(blist []string, fullrepo string) bool {
|
||||
for _, blocked := range blist {
|
||||
if blocked == fullrepo {
|
||||
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, ""
|
||||
}
|
||||
|
||||
@@ -2,43 +2,90 @@ package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type WhitelistConfig struct {
|
||||
Whitelist []string `json:"whitelist"`
|
||||
// Whitelist 用于存储白名单信息
|
||||
type Whitelist struct {
|
||||
userSet map[string]struct{} // 用户级白名单
|
||||
repoSet map[string]map[string]struct{} // 仓库级白名单
|
||||
initOnce sync.Once // 确保初始化只执行一次
|
||||
initialized bool // 初始化状态标识
|
||||
}
|
||||
|
||||
var (
|
||||
whitelistfile = "/data/ghproxy/config/whitelist.json"
|
||||
whitelist *WhitelistConfig
|
||||
whitelistInstance *Whitelist
|
||||
whitelistInitErr error
|
||||
)
|
||||
|
||||
func LoadWhitelist(cfg *config.Config) {
|
||||
whitelistfile = cfg.Whitelist.WhitelistFile
|
||||
whitelist = &WhitelistConfig{}
|
||||
|
||||
data, err := os.ReadFile(whitelistfile)
|
||||
if err != nil {
|
||||
logw("Failed to read whitelist file: %v", err)
|
||||
// InitWhitelist 初始化白名单(线程安全,仅执行一次)
|
||||
func InitWhitelist(cfg *config.Config) error {
|
||||
whitelistInstance = &Whitelist{
|
||||
userSet: make(map[string]struct{}),
|
||||
repoSet: make(map[string]map[string]struct{}),
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, whitelist)
|
||||
data, err := os.ReadFile(cfg.Whitelist.WhitelistFile)
|
||||
if err != nil {
|
||||
logw("Failed to unmarshal whitelist JSON: %v", err)
|
||||
return fmt.Errorf("failed to read whitelist: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckWhitelist(fullrepo string) bool {
|
||||
return forRangeCheckWhitelist(whitelist.Whitelist, fullrepo)
|
||||
}
|
||||
var list struct {
|
||||
Entries []string `json:"whitelist"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &list); err != nil {
|
||||
return fmt.Errorf("invalid whitelist format: %w", err)
|
||||
}
|
||||
|
||||
func forRangeCheckWhitelist(blist []string, fullrepo string) bool {
|
||||
for _, blocked := range blist {
|
||||
if blocked == fullrepo {
|
||||
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,109 +0,0 @@
|
||||
{
|
||||
debug
|
||||
http_port 80
|
||||
https_port 443
|
||||
order cache before rewrite
|
||||
cache {
|
||||
cache_name GhProxyCache
|
||||
}
|
||||
log {
|
||||
level INFO
|
||||
output file /data/caddy/log/caddy.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(log) {
|
||||
log {
|
||||
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
|
||||
time_format "02/Jan/2006:15:04:05 -0700"
|
||||
}
|
||||
output file /data/caddy/log/{args[0]}/access.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
roll_keep_for 24h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(error_page) {
|
||||
handle_errors {
|
||||
rewrite * /{err.status_code}.html
|
||||
root * /data/caddy/pages/errors
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
(encode) {
|
||||
encode {
|
||||
zstd best
|
||||
br 5 v2
|
||||
gzip 5
|
||||
minimum_length 512
|
||||
}
|
||||
}
|
||||
|
||||
(cache) {
|
||||
cache {
|
||||
allowed_http_verbs GET
|
||||
stale {args[0]}
|
||||
ttl {args[1]}
|
||||
}
|
||||
}
|
||||
|
||||
(header_realip) {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
|
||||
}
|
||||
|
||||
(rate_limit) {
|
||||
route /* {
|
||||
rate_limit {remote.ip} {args[0]}r/m 10000 429
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
reverse_proxy {
|
||||
to 127.0.0.1:8080
|
||||
import header_realip
|
||||
}
|
||||
import log ghproxy
|
||||
import cache 0s 300s
|
||||
import error_page
|
||||
import encode
|
||||
import rate_limit 60
|
||||
header Age 10
|
||||
header Cache-Control "max-age=300"
|
||||
route / {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
}
|
||||
route /favicon.ico {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
}
|
||||
handle_errors {
|
||||
@redirects `{err.status_code} in [301, 302, 307]`
|
||||
reverse_proxy @redirects {
|
||||
header_up Location {http.response.header.Location}
|
||||
}
|
||||
}
|
||||
|
||||
route /v2* {
|
||||
reverse_proxy https://registry-1.docker.io {
|
||||
header_up Host registry-1.docker.io
|
||||
header_up X-Real-IP {remote}
|
||||
header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
|
||||
header_up X-Forwarded-Proto {scheme}
|
||||
header_up Authorization {http.request.header.Authorization}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
import /data/caddy/config.d/*
|
||||
@@ -1,93 +0,0 @@
|
||||
{
|
||||
debug
|
||||
http_port 80
|
||||
https_port 443
|
||||
order cache before rewrite
|
||||
cache {
|
||||
cache_name GhProxyCache
|
||||
}
|
||||
log {
|
||||
level INFO
|
||||
output file /data/caddy/log/caddy.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(log) {
|
||||
log {
|
||||
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
|
||||
time_format "02/Jan/2006:15:04:05 -0700"
|
||||
}
|
||||
output file /data/caddy/log/{args[0]}/access.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
roll_keep_for 24h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(error_page) {
|
||||
handle_errors {
|
||||
rewrite * /{err.status_code}.html
|
||||
root * /data/caddy/pages/errors
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
(encode) {
|
||||
encode {
|
||||
zstd best
|
||||
br 5 v2
|
||||
gzip 5
|
||||
minimum_length 512
|
||||
}
|
||||
}
|
||||
|
||||
(cache) {
|
||||
cache {
|
||||
allowed_http_verbs GET
|
||||
stale {args[0]}
|
||||
ttl {args[1]}
|
||||
}
|
||||
}
|
||||
|
||||
(header_realip) {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
|
||||
}
|
||||
|
||||
(rate_limit) {
|
||||
route /* {
|
||||
rate_limit {remote.ip} {args[0]}r/m 10000 429
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
reverse_proxy {
|
||||
to 127.0.0.1:8080
|
||||
import header_realip
|
||||
}
|
||||
import log ghproxy
|
||||
import cache 0s 300s
|
||||
import error_page
|
||||
import encode
|
||||
route /* {
|
||||
rate_limit {remote.ip} 60r/m 10000 429
|
||||
}
|
||||
route / {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
}
|
||||
route /favicon.ico {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
}
|
||||
}
|
||||
|
||||
import /data/caddy/config.d/*
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"blacklist": [
|
||||
"black/list",
|
||||
"test/test1",
|
||||
"example/repo2"
|
||||
]
|
||||
}
|
||||
|
||||
"blacklist": [
|
||||
"eviluser",
|
||||
"spamuser/bad-repo",
|
||||
"malwareuser/*"
|
||||
]
|
||||
}
|
||||
316
config/config.go
316
config/config.go
@@ -1,103 +1,295 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
/*type Config struct {
|
||||
Server struct {
|
||||
Port int `yaml:"port"`
|
||||
Host string `yaml:"host"`
|
||||
SizeLimit int `yaml:"sizelimit"`
|
||||
} `yaml:"server"`
|
||||
|
||||
Log struct {
|
||||
LogFilePath string `yaml:"logfilepath"`
|
||||
MaxLogSize int `yaml:"maxlogsize"`
|
||||
} `yaml:"logger"`
|
||||
|
||||
CORS struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
} `yaml:"cors"`
|
||||
|
||||
Auth struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
AuthToken string `yaml:"authtoken"`
|
||||
} `yaml:"auth"`
|
||||
|
||||
Blacklist struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
BlacklistFile string `yaml:"blacklistfile"`
|
||||
} `yaml:"blacklist"`
|
||||
|
||||
Whitelist struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
WhitelistFile string `yaml:"whitelistfile"`
|
||||
} `yaml:"whitelist"`
|
||||
}
|
||||
|
||||
// LoadConfig 从 YAML 配置文件加载配置
|
||||
func LoadConfig(filePath string) (*Config, error) {
|
||||
var config Config
|
||||
if err := loadYAML(filePath, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// LoadyamlConfig 从 YAML 配置文件加载配置
|
||||
func loadYAML(filePath string, out interface{}) error {
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return yaml.Unmarshal(data, out)
|
||||
}*/
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Log LoggerConfig
|
||||
CORS CORSConfig
|
||||
Httpc HttpcConfig
|
||||
GitClone GitCloneConfig
|
||||
Shell ShellConfig
|
||||
Pages PagesConfig
|
||||
Log LogConfig
|
||||
Auth AuthConfig
|
||||
Blacklist BlacklistConfig
|
||||
Whitelist WhitelistConfig
|
||||
RateLimit RateLimitConfig
|
||||
Outbound OutboundConfig
|
||||
Docker DockerConfig
|
||||
}
|
||||
|
||||
/*
|
||||
[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"`
|
||||
SizeLimit int `toml:"sizeLimit"`
|
||||
MemLimit int64 `toml:"memLimit"`
|
||||
Cors string `toml:"cors"`
|
||||
Debug bool `toml:"debug"`
|
||||
}
|
||||
|
||||
type LoggerConfig struct {
|
||||
LogFilePath string `toml:"logfilepath"`
|
||||
MaxLogSize int `toml:"maxlogsize"`
|
||||
/*
|
||||
[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"`
|
||||
}
|
||||
|
||||
type CORSConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
/*
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
ForceH2C = true
|
||||
*/
|
||||
type GitCloneConfig struct {
|
||||
Mode string `toml:"mode"`
|
||||
SmartGitAddr string `toml:"smartGitAddr"`
|
||||
ForceH2C bool `toml:"ForceH2C"`
|
||||
}
|
||||
|
||||
/*
|
||||
[shell]
|
||||
editor = true
|
||||
rewriteAPI = false
|
||||
*/
|
||||
type ShellConfig struct {
|
||||
Editor bool `toml:"editor"`
|
||||
RewriteAPI bool `toml:"rewriteAPI"`
|
||||
}
|
||||
|
||||
/*
|
||||
[pages]
|
||||
mode = "internal" # "internal" or "external"
|
||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||
staticDir = "/data/www"
|
||||
*/
|
||||
type PagesConfig struct {
|
||||
Mode string `toml:"mode"`
|
||||
Theme string `toml:"theme"`
|
||||
StaticDir string `toml:"staticDir"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
LogFilePath string `toml:"logFilePath"`
|
||||
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"`
|
||||
AuthToken string `toml:"authtoken"`
|
||||
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 {
|
||||
Enabled bool `toml:"enabled"`
|
||||
BlacklistFile string `toml:"blacklistfile"`
|
||||
BlacklistFile string `toml:"blacklistFile"`
|
||||
}
|
||||
|
||||
type WhitelistConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
WhitelistFile string `toml:"whitelistfile"`
|
||||
WhitelistFile string `toml:"whitelistFile"`
|
||||
}
|
||||
|
||||
/*
|
||||
[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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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: "bootstrap",
|
||||
StaticDir: "/data/www",
|
||||
},
|
||||
Log: LogConfig{
|
||||
LogFilePath: "/data/ghproxy/log/ghproxy.log",
|
||||
MaxLogSize: 10,
|
||||
Level: "info",
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
Enabled: false,
|
||||
Method: "parameters",
|
||||
Key: "",
|
||||
Token: "token",
|
||||
PassThrough: false,
|
||||
ForceAllowApi: false,
|
||||
ForceAllowApiPassList: false,
|
||||
},
|
||||
Blacklist: BlacklistConfig{
|
||||
Enabled: false,
|
||||
BlacklistFile: "/data/ghproxy/config/blacklist.json",
|
||||
},
|
||||
Whitelist: WhitelistConfig{
|
||||
Enabled: false,
|
||||
WhitelistFile: "/data/ghproxy/config/whitelist.json",
|
||||
},
|
||||
RateLimit: RateLimitConfig{
|
||||
Enabled: false,
|
||||
//RateMethod: "total",
|
||||
RatePerMinute: 100,
|
||||
Burst: 10,
|
||||
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: "ghcr",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,74 @@
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
sizelimit = 131072000 # 125MB
|
||||
sizeLimit = 125 # MB
|
||||
memLimit = 0 # MB
|
||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||
debug = false
|
||||
|
||||
[logger]
|
||||
logfilepath = "/data/ghproxy/log/ghproxy.log"
|
||||
maxlogsize = 5 # MB
|
||||
[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
|
||||
|
||||
[cors]
|
||||
enabled = true
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
ForceH2C = false
|
||||
|
||||
[shell]
|
||||
editor = false
|
||||
rewriteAPI = false
|
||||
|
||||
[pages]
|
||||
mode = "internal" # "internal" or "external"
|
||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||
staticDir = "/data/www"
|
||||
|
||||
[log]
|
||||
logFilePath = "/data/ghproxy/log/ghproxy.log"
|
||||
maxLogSize = 5 # MB
|
||||
level = "info" # debug, info, warn, error, none
|
||||
|
||||
[auth]
|
||||
authtoken = "test"
|
||||
method = "parameters" # "header" or "parameters"
|
||||
token = "token"
|
||||
key = ""
|
||||
enabled = false
|
||||
passThrough = false
|
||||
ForceAllowApi = false
|
||||
ForceAllowApiPassList = false
|
||||
|
||||
[blacklist]
|
||||
blacklistfile = "/data/ghproxy/config/blacklist.json"
|
||||
blacklistFile = "/data/ghproxy/config/blacklist.json"
|
||||
enabled = false
|
||||
|
||||
[whitelist]
|
||||
enabled = false
|
||||
whitelistfile = "/data/ghproxy/config/whitelist.json"
|
||||
whitelistFile = "/data/ghproxy/config/whitelist.json"
|
||||
|
||||
[rateLimit]
|
||||
enabled = false
|
||||
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"
|
||||
@@ -1,29 +0,0 @@
|
||||
# Server Configuration
|
||||
server:
|
||||
port: 8080
|
||||
host: "127.0.0.1"
|
||||
sizelimit: 131072000 # 125MB
|
||||
|
||||
# Logging Configuration
|
||||
logger:
|
||||
logfilepath: "/data/ghproxy/log/ghproxy.log"
|
||||
maxlogsize: 5 # MB
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
enabled: true
|
||||
|
||||
# Authentication Configuration
|
||||
auth:
|
||||
enabled: false
|
||||
authtoken: "test"
|
||||
|
||||
# Blacklist Configuration
|
||||
blacklist:
|
||||
enabled: false
|
||||
blacklistfile: "/data/ghproxy/config/blacklist.json"
|
||||
|
||||
# Whitelist Configuration
|
||||
whitelist:
|
||||
enabled: false
|
||||
whitelistfile: "/data/ghproxy/config/whitelist.json"
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"whitelist": [
|
||||
"white/list",
|
||||
"white/test1",
|
||||
"example/white"
|
||||
]
|
||||
}
|
||||
|
||||
"whitelist": [
|
||||
"white/list",
|
||||
"white/test1",
|
||||
"example/*"
|
||||
]
|
||||
}
|
||||
64
deploy/config.toml
Normal file
64
deploy/config.toml
Normal file
@@ -0,0 +1,64 @@
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 8080
|
||||
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||
sizeLimit = 125 # MB
|
||||
memLimit = 0 # MB
|
||||
H2C = true
|
||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||
debug = false
|
||||
|
||||
[httpc]
|
||||
mode = "auto" # "auto" or "advanced"
|
||||
maxIdleConns = 100 # only for advanced mode
|
||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||
maxConnsPerHost = 0 # only for advanced mode
|
||||
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
ForceH2C = false
|
||||
|
||||
[shell]
|
||||
editor = false
|
||||
rewriteAPI = false
|
||||
|
||||
[pages]
|
||||
mode = "internal" # "internal" or "external"
|
||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||
staticDir = "/usr/local/ghproxy/pages"
|
||||
|
||||
[log]
|
||||
logFilePath = "/usr/local/ghproxy/log/ghproxy.log"
|
||||
maxLogSize = 5 # MB
|
||||
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"
|
||||
enabled = false
|
||||
|
||||
[whitelist]
|
||||
enabled = false
|
||||
whitelistFile = "/usr/local/ghproxy/config/whitelist.json"
|
||||
|
||||
[rateLimit]
|
||||
enabled = false
|
||||
rateMethod = "total" # "ip" or "total"
|
||||
ratePerMinute = 180
|
||||
burst = 5
|
||||
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
|
||||
[docker]
|
||||
enabled = false
|
||||
target = "ghcr" # ghcr/dockerhub
|
||||
13
deploy/ghproxy.service
Normal file
13
deploy/ghproxy.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Github Proxy Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
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
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
144
deploy/install-dev.sh
Normal file
144
deploy/install-dev.sh
Normal file
@@ -0,0 +1,144 @@
|
||||
# /bin/bash
|
||||
# https://github.com/WJQSERVER-STUDIO/ghproxy
|
||||
|
||||
ghproxy_dir="/usr/local/ghproxy"
|
||||
|
||||
# install packages
|
||||
install() {
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "ARGS NOT FOUND"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for package in "$@"; do
|
||||
if ! command -v "$package" &>/dev/null; then
|
||||
if command -v dnf &>/dev/null; then
|
||||
dnf -y update && dnf install -y "$package"
|
||||
elif command -v yum &>/dev/null; then
|
||||
yum -y update && yum -y install "$package"
|
||||
elif command -v apt &>/dev/null; then
|
||||
apt update -y && apt install -y "$package"
|
||||
elif command -v apk &>/dev/null; then
|
||||
apk update && apk add "$package"
|
||||
else
|
||||
echo "UNKNOWN PACKAGE MANAGER"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
# 检查是否为root用户
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "请以root用户运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装依赖包
|
||||
install curl wget sed
|
||||
|
||||
# 查看当前架构是否为linux/amd64或linux/arm64
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
|
||||
echo " $ARCH 架构不被支持"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 重写架构值,改为amd64或arm64
|
||||
if [ "$ARCH" == "x86_64" ]; then
|
||||
ARCH="amd64"
|
||||
elif [ "$ARCH" == "aarch64" ]; then
|
||||
ARCH="arm64"
|
||||
fi
|
||||
|
||||
# 获取监听端口
|
||||
read -p "请输入程序监听的端口(默认8080): " PORT
|
||||
if [ -z "$PORT" ]; then
|
||||
PORT=8080
|
||||
fi
|
||||
|
||||
# 本机监听/泛监听(127.0.0.1/0.0.0.0)
|
||||
read -p "请键入程序监听的IP(默认127.0.0.1)(0.0.0.0为泛监听): " IP
|
||||
if [ -z "$IP" ]; then
|
||||
IP="127.0.0.1"
|
||||
fi
|
||||
|
||||
# 安装目录
|
||||
read -p "请输入安装目录(默认/usr/local/ghproxy): " ghproxy_dir
|
||||
if [ -z "$ghproxy_dir" ]; then
|
||||
ghproxy_dir="/usr/local/ghproxy"
|
||||
fi
|
||||
|
||||
# 创建目录
|
||||
mkdir -p ${ghproxy_dir}
|
||||
mkdir -p ${ghproxy_dir}/config
|
||||
mkdir -p ${ghproxy_dir}/log
|
||||
mkdir -p ${ghproxy_dir}/pages
|
||||
|
||||
# 获取最新版本号
|
||||
VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/DEV-VERSION)
|
||||
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/DEV-VERSION
|
||||
|
||||
# 下载ghproxy
|
||||
wget -q -O ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH.tar.gz
|
||||
install tar
|
||||
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
|
||||
chmod +x ${ghproxy_dir}/ghproxy
|
||||
|
||||
# 下载pages
|
||||
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/pages/bootstrap/index.html
|
||||
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/pages/bootstrap/favicon.ico
|
||||
|
||||
|
||||
# 下载配置文件
|
||||
if [ -f ${ghproxy_dir}/config/config.toml ]; then
|
||||
echo "配置文件已存在, 跳过下载"
|
||||
echo "[WARNING] 请检查配置文件是否正确,DEV版本升级时请注意配置文件兼容性"
|
||||
sleep 2
|
||||
else
|
||||
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/config.toml
|
||||
fi
|
||||
|
||||
# 替换 port = 8080
|
||||
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
|
||||
|
||||
# 下载systemd服务文件
|
||||
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
|
||||
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/ghproxy.service
|
||||
else
|
||||
|
||||
cat <<EOF > /etc/systemd/system/ghproxy.service
|
||||
|
||||
[Unit]
|
||||
Description=Github Proxy Service
|
||||
After=network.target
|
||||
|
||||
[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
|
||||
systemctl daemon-reload
|
||||
systemctl enable ghproxy
|
||||
systemctl start ghproxy
|
||||
|
||||
echo "ghproxy 安装成功, 监听端口为 $PORT"
|
||||
144
deploy/install.sh
Normal file
144
deploy/install.sh
Normal file
@@ -0,0 +1,144 @@
|
||||
# /bin/bash
|
||||
# https://github.com/WJQSERVER-STUDIO/ghproxy
|
||||
|
||||
ghproxy_dir="/usr/local/ghproxy"
|
||||
|
||||
# install packages
|
||||
install() {
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "ARGS NOT FOUND"
|
||||
return 1
|
||||
fi
|
||||
|
||||
for package in "$@"; do
|
||||
if ! command -v "$package" &>/dev/null; then
|
||||
if command -v dnf &>/dev/null; then
|
||||
dnf -y update && dnf install -y "$package"
|
||||
elif command -v yum &>/dev/null; then
|
||||
yum -y update && yum -y install "$package"
|
||||
elif command -v apt &>/dev/null; then
|
||||
apt update -y && apt install -y "$package"
|
||||
elif command -v apk &>/dev/null; then
|
||||
apk update && apk add "$package"
|
||||
else
|
||||
echo "UNKNOWN PACKAGE MANAGER"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
|
||||
# 检查是否为root用户
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "请以root用户运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 安装依赖包
|
||||
install curl wget sed
|
||||
|
||||
# 查看当前架构是否为linux/amd64或linux/arm64
|
||||
ARCH=$(uname -m)
|
||||
if [ "$ARCH" != "x86_64" ] && [ "$ARCH" != "aarch64" ]; then
|
||||
echo " $ARCH 架构不被支持"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 重写架构值,改为amd64或arm64
|
||||
if [ "$ARCH" == "x86_64" ]; then
|
||||
ARCH="amd64"
|
||||
elif [ "$ARCH" == "aarch64" ]; then
|
||||
ARCH="arm64"
|
||||
fi
|
||||
|
||||
# 获取监听端口
|
||||
read -p "请输入程序监听的端口(默认8080): " PORT
|
||||
if [ -z "$PORT" ]; then
|
||||
PORT=8080
|
||||
fi
|
||||
|
||||
# 本机监听/泛监听(127.0.0.1/0.0.0.0)
|
||||
read -p "请键入程序监听的IP(默认127.0.0.1)(0.0.0.0为泛监听): " IP
|
||||
if [ -z "$IP" ]; then
|
||||
IP="127.0.0.1"
|
||||
fi
|
||||
|
||||
# 安装目录
|
||||
read -p "请输入安装目录(默认/usr/local/ghproxy): " ghproxy_dir
|
||||
if [ -z "$ghproxy_dir" ]; then
|
||||
ghproxy_dir="/usr/local/ghproxy"
|
||||
fi
|
||||
|
||||
# 创建目录
|
||||
mkdir -p ${ghproxy_dir}
|
||||
mkdir -p ${ghproxy_dir}/config
|
||||
mkdir -p ${ghproxy_dir}/log
|
||||
mkdir -p ${ghproxy_dir}/pages
|
||||
|
||||
# 获取最新版本号
|
||||
VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/VERSION)
|
||||
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/VERSION
|
||||
|
||||
# 下载ghproxy
|
||||
wget -q -O ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/${VERSION}/ghproxy-linux-${ARCH}.tar.gz
|
||||
install tar
|
||||
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
|
||||
chmod +x ${ghproxy_dir}/ghproxy
|
||||
|
||||
# 下载pages
|
||||
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/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
|
||||
|
||||
|
||||
# 下载配置文件
|
||||
if [ -f ${ghproxy_dir}/config/config.toml ]; then
|
||||
echo "配置文件已存在, 跳过下载"
|
||||
echo "[WARNING] 请检查配置文件是否正确,DEV版本升级时请注意配置文件兼容性"
|
||||
sleep 2
|
||||
else
|
||||
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/config.toml
|
||||
fi
|
||||
|
||||
# 替换 port = 8080
|
||||
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
|
||||
|
||||
# 下载systemd服务文件
|
||||
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
|
||||
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
|
||||
else
|
||||
|
||||
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
|
||||
systemctl daemon-reload
|
||||
systemctl enable ghproxy
|
||||
systemctl start ghproxy
|
||||
|
||||
echo "ghproxy 安装成功, 监听端口为 $PORT"
|
||||
27
deploy/uninstall.sh
Normal file
27
deploy/uninstall.sh
Normal file
@@ -0,0 +1,27 @@
|
||||
# /bin/bash
|
||||
|
||||
# 停止 ghproxy 服务
|
||||
systemctl stop ghproxy
|
||||
|
||||
# 删除 ghproxy 服务
|
||||
systemctl disable ghproxy
|
||||
rm /etc/systemd/system/ghproxy.service
|
||||
|
||||
# 获取安装文件夹
|
||||
read -p "请输入 ghproxy 安装文件夹路径(默认 /usr/local/ghproxy): " install_path
|
||||
if [ -z "$install_path" ]; then
|
||||
install_path="/usr/local/ghproxy"
|
||||
fi
|
||||
|
||||
# 删除 ghproxy 文件夹
|
||||
# 检查目录是否存在ghproxy文件
|
||||
if [ -f "$install_path" ]; then
|
||||
echo "ghproxy 未安装或安装路径错误"
|
||||
exit 1
|
||||
else
|
||||
echo "ghproxy 安装目录已确认,正在卸载..."
|
||||
rm -r $install_path
|
||||
fi
|
||||
|
||||
|
||||
echo "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:80'
|
||||
- '7210:8080'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
FROM wjqserver/caddy:daily-alpine AS builder
|
||||
FROM alpine:latest AS builder
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG BRANCH=dev
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
@@ -13,37 +14,38 @@ RUN mkdir -p /data/${APPLICATION}/config
|
||||
RUN mkdir -p /data/${APPLICATION}/log
|
||||
|
||||
# 安装依赖
|
||||
RUN apk add --no-cache curl wget
|
||||
|
||||
# 前端
|
||||
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 apk add --no-cache curl wget tar
|
||||
|
||||
# 后端
|
||||
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/DEV-VERSION) && \
|
||||
wget -O /data/${APPLICATION}/${APPLICATION} https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}
|
||||
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/dev/init.sh
|
||||
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 /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/dev/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 wjqserver/caddy:daily-alpine
|
||||
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,27 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
APPLICATION=ghproxy
|
||||
|
||||
if [ ! -f /data/caddy/config/Caddyfile ]; then
|
||||
cp /data/caddy/Caddyfile /data/caddy/config/Caddyfile
|
||||
fi
|
||||
|
||||
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.yaml ]; then
|
||||
cp /data/${APPLICATION}/config.yaml /data/${APPLICATION}/config/config.yaml
|
||||
fi
|
||||
|
||||
/data/caddy/caddy run --config /data/caddy/config/Caddyfile > /data/${APPLICATION}/log/caddy.log 2>&1 &
|
||||
|
||||
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1 &
|
||||
|
||||
while true; do
|
||||
sleep 1
|
||||
done
|
||||
@@ -1,7 +1,8 @@
|
||||
FROM wjqserver/caddy:2.9.0-rc-alpine AS builder
|
||||
FROM alpine:latest AS builder
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG BRANCH=main
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -13,37 +14,40 @@ RUN mkdir -p /data/${APPLICATION}/config
|
||||
RUN mkdir -p /data/${APPLICATION}/log
|
||||
|
||||
# 安装依赖
|
||||
RUN apk add --no-cache curl wget
|
||||
|
||||
# 前端
|
||||
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 apk add --no-cache curl wget tar
|
||||
|
||||
# 后端
|
||||
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/VERSION) && \
|
||||
wget -O /data/${APPLICATION}/${APPLICATION} https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}
|
||||
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/release/init.sh
|
||||
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 /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/release/Caddyfile
|
||||
RUN wget -O /data/${APPLICATION}/config.yaml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.yaml
|
||||
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 wjqserver/caddy:2.9.0-rc-alpine
|
||||
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,27 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
APPLICATION=ghproxy
|
||||
|
||||
if [ ! -f /data/caddy/config/Caddyfile ]; then
|
||||
cp /data/caddy/Caddyfile /data/caddy/config/Caddyfile
|
||||
fi
|
||||
|
||||
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.yaml ]; then
|
||||
cp /data/${APPLICATION}/config.yaml /data/${APPLICATION}/config/config.yaml
|
||||
fi
|
||||
|
||||
/data/caddy/caddy run --config /data/caddy/config/Caddyfile > /data/${APPLICATION}/log/caddy.log 2>&1 &
|
||||
|
||||
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.yaml > /data/${APPLICATION}/log/run.log 2>&1 &
|
||||
|
||||
while true; do
|
||||
sleep 1
|
||||
done
|
||||
65
go.mod
65
go.mod
@@ -1,54 +1,27 @@
|
||||
module ghproxy
|
||||
|
||||
go 1.23.2
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/imroc/req/v3 v3.46.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/WJQSERVER-STUDIO/httpc v0.7.0
|
||||
golang.org/x/net v0.41.0
|
||||
golang.org/x/time v0.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.12.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.4.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||
github.com/gin-contrib/sse v0.1.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.22.1 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20241008150032-332c0e1a4a34 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.10 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // 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/onsi/ginkgo/v2 v2.20.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.47.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.35.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/reco v0.0.3
|
||||
github.com/fenthope/record v0.0.3
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/infinite-iroha/touka v0.2.5
|
||||
github.com/wjqserver/modembed v0.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
)
|
||||
|
||||
189
go.sum
189
go.sum
@@ -1,159 +1,30 @@
|
||||
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/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
|
||||
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
|
||||
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
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/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.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
|
||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/pprof v0.0.0-20241008150032-332c0e1a4a34 h1:4iExbL0TFWhkSCZx6nfKwjM+CbnBySx18KssYmdL1fc=
|
||||
github.com/google/pprof v0.0.0-20241008150032-332c0e1a4a34/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/imroc/req/v3 v3.46.1 h1:oahr2hBTb3AaFI4P6jkN0Elj2ZVKJcdQ/IjWqeIKjvc=
|
||||
github.com/imroc/req/v3 v3.46.1/go.mod h1:weam9gmyb00QnOtu6HXSnk44dNFkIUQb5QdMx13FeUU=
|
||||
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/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
|
||||
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
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/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
|
||||
github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E=
|
||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
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.9.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=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
|
||||
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6 h1:1wqE9dj9NpSm04INVsJhhEUzhuDVjbcyKH91sVyPATw=
|
||||
golang.org/x/exp v0.0.0-20241004190924-225e2abe05e6/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/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.7.0 h1:iHhqlxppJBjlmvsIjvLZKRbWXqSdbeSGGofjHGmqGJc=
|
||||
github.com/WJQSERVER-STUDIO/httpc v0.7.0/go.mod h1:M7KNUZjjhCkzzcg9lBPs9YfkImI+7vqjAyjdA19+joE=
|
||||
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/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-20250517221953-25912455fbc8 h1:o8UqXPI6SVwQt04RGsqKp3qqmbOfTNMqDrWsc4O47kk=
|
||||
github.com/go-json-experiment/json v0.0.0-20250517221953-25912455fbc8/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.2.5 h1:x7lcKk0MIHGrPb9TMmgC+nG57G4SeFGflwrta2Lz3jo=
|
||||
github.com/infinite-iroha/touka v0.2.5/go.mod h1:2MBPtsM+5ClIZ/E1mPEKx1Rb+KIndTwZlIa2CwRPV7A=
|
||||
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.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
|
||||
28
init.sh
28
init.sh
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
APPLICATON=ghproxy
|
||||
|
||||
if [ ! -f /data/caddy/config/Caddyfile ]; then
|
||||
cp /data/caddy/Caddyfile /data/caddy/config/Caddyfile
|
||||
fi
|
||||
|
||||
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/caddy/caddy run --config /data/caddy/config/Caddyfile > /data/${APPLICATON}/log/caddy.log 2>&1 &
|
||||
|
||||
/data/${APPLICATON}/${APPLICATON} > /data/${APPLICATON}/log/run.log 2>&1 &
|
||||
|
||||
while [[ true ]]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
159
logger/logger.go
159
logger/logger.go
@@ -1,159 +0,0 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
logw = Logw
|
||||
logFile *os.File
|
||||
logger *log.Logger
|
||||
logChannel = make(chan string, 100)
|
||||
quitChannel = make(chan struct{})
|
||||
logFileMutex sync.Mutex // 保护 logFile 的互斥锁
|
||||
)
|
||||
|
||||
// Init 初始化日志记录器,接受日志文件路径作为参数
|
||||
func Init(logFilePath string, maxLogsize int) error {
|
||||
logFileMutex.Lock()
|
||||
defer logFileMutex.Unlock()
|
||||
|
||||
var err error
|
||||
logFile, err = os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger = log.New(logFile, "", 0)
|
||||
|
||||
go logWorker()
|
||||
go monitorLogSize(logFilePath, maxLogsize)
|
||||
return nil
|
||||
}
|
||||
|
||||
// logWorker 处理日志记录
|
||||
func logWorker() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-logChannel:
|
||||
timestamp := time.Now().Format("02/Jan/2006:15:04:05 -0700")
|
||||
logger.Println(timestamp + " - " + msg)
|
||||
case <-quitChannel:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log 直接记录日志的函数
|
||||
func Log(customMessage string) {
|
||||
logChannel <- customMessage
|
||||
}
|
||||
|
||||
// Logw 用于格式化日志记录
|
||||
func Logw(format string, args ...interface{}) {
|
||||
message := fmt.Sprintf(format, args...)
|
||||
Log(message)
|
||||
}
|
||||
|
||||
// Close 关闭日志文件
|
||||
func Close() {
|
||||
logFileMutex.Lock()
|
||||
defer logFileMutex.Unlock()
|
||||
|
||||
if logFile != nil {
|
||||
quitChannel <- struct{}{}
|
||||
if err := logFile.Close(); err != nil {
|
||||
fmt.Printf("Error closing log file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func monitorLogSize(logFilePath string, maxLogsize int) {
|
||||
var maxLogsizeBytes int64 = int64(maxLogsize) * 1024 * 1024 // 最大日志文件大小,单位为MB
|
||||
for {
|
||||
time.Sleep(600 * time.Second) // 每10分钟检查一次
|
||||
logFileMutex.Lock()
|
||||
info, err := logFile.Stat()
|
||||
logFileMutex.Unlock()
|
||||
|
||||
if err == nil && info.Size() > maxLogsizeBytes {
|
||||
if err := rotateLogFile(logFilePath); err != nil {
|
||||
logw("Log Rotation Failed: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rotateLogFile(logFilePath string) error {
|
||||
logFileMutex.Lock()
|
||||
defer logFileMutex.Unlock()
|
||||
|
||||
if logFile != nil {
|
||||
if err := logFile.Close(); err != nil {
|
||||
logw("Error closing log file for rotation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开当前日志文件
|
||||
logFile, err := os.Open(logFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open log file: %s, error: %w", logFilePath, err)
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
newLogFilePath := logFilePath + "-" + time.Now().Format("20060102-150405") + ".tar.gz"
|
||||
outFile, err := os.Create(newLogFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create gz file: %s, error: %w", newLogFilePath, err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
gzWriter, err := gzip.NewWriterLevel(outFile, gzip.BestCompression)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create gz writer: %w", err)
|
||||
}
|
||||
defer gzWriter.Close()
|
||||
|
||||
tarWriter := tar.NewWriter(gzWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
logFileStat, err := logFile.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stat log file: %s, error: %w", logFilePath, err)
|
||||
}
|
||||
|
||||
logFileHeader := &tar.Header{
|
||||
Name: filepath.Base(logFilePath),
|
||||
Size: logFileStat.Size(),
|
||||
Mode: 0644,
|
||||
ModTime: logFileStat.ModTime(),
|
||||
}
|
||||
|
||||
if err := tarWriter.WriteHeader(logFileHeader); err != nil {
|
||||
return fmt.Errorf("failed to write log file header: %s, error: %w", logFilePath, err)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(tarWriter, logFile); err != nil {
|
||||
return fmt.Errorf("failed to copy log file: %s, error: %w", logFilePath, err)
|
||||
}
|
||||
|
||||
if err := os.Truncate(logFilePath, 0); err != nil {
|
||||
return fmt.Errorf("failed to truncate log file: %s, error: %w", logFilePath, err)
|
||||
}
|
||||
|
||||
// 重新打开日志文件
|
||||
logFile, err = os.OpenFile(logFilePath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reopen log file: %s, error: %w", logFilePath, err)
|
||||
}
|
||||
logger.SetOutput(logFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
489
main.go
489
main.go
@@ -1,102 +1,471 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"ghproxy/api"
|
||||
"ghproxy/auth"
|
||||
"ghproxy/config"
|
||||
"ghproxy/logger"
|
||||
"ghproxy/proxy"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/fenthope/bauth"
|
||||
|
||||
"ghproxy/weakcache"
|
||||
|
||||
"github.com/fenthope/ikumi"
|
||||
"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
|
||||
logw = logger.Logw
|
||||
router *gin.Engine
|
||||
configfile = "/data/ghproxy/config/config.yaml"
|
||||
cfg *config.Config
|
||||
r *touka.Engine
|
||||
configfile = "/data/ghproxy/config/config.toml"
|
||||
hertZfile *os.File
|
||||
cfgfile string
|
||||
version string
|
||||
runMode string
|
||||
showVersion bool
|
||||
showHelp bool
|
||||
)
|
||||
|
||||
func ReadFlag() {
|
||||
cfgfile := flag.String("cfg", configfile, "config file path")
|
||||
configfile = *cfgfile
|
||||
var (
|
||||
//go:embed pages/*
|
||||
pagesFS embed.FS
|
||||
)
|
||||
|
||||
var (
|
||||
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, "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(configfile)
|
||||
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.Printf("Loaded config: %v\n", cfg)
|
||||
}
|
||||
|
||||
func setupLogger() {
|
||||
// 初始化日志模块
|
||||
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"
|
||||
}
|
||||
logw("Logger initialized")
|
||||
logw("Init Completed")
|
||||
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 Loadlist(cfg *config.Config) {
|
||||
auth.LoadBlacklist(cfg)
|
||||
auth.LoadWhitelist(cfg)
|
||||
func setMemLimit(cfg *config.Config) {
|
||||
if cfg.Server.MemLimit > 0 {
|
||||
debug.SetMemoryLimit((cfg.Server.MemLimit) * 1024 * 1024)
|
||||
logInfo("Set Memory Limit to %d MB", cfg.Server.MemLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func loadlist(cfg *config.Config) {
|
||||
err := auth.ListInit(cfg)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to initialize list: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setupApi(cfg *config.Config, r *touka.Engine, version string) {
|
||||
api.InitHandleRouter(cfg, r, version)
|
||||
}
|
||||
|
||||
func InitReq(cfg *config.Config) {
|
||||
err := proxy.InitReq(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to initialize request: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 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")
|
||||
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()
|
||||
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()
|
||||
Loadlist(cfg)
|
||||
if cfg != nil { // 在setupLogger前添加空值检查
|
||||
setupLogger(cfg)
|
||||
InitReq(cfg)
|
||||
setMemLimit(cfg)
|
||||
loadlist(cfg)
|
||||
if cfg.Docker.Enabled {
|
||||
wcache = proxy.InitWeakCache()
|
||||
}
|
||||
|
||||
// 设置 Gin 模式
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
if cfg.Server.Debug {
|
||||
runMode = "dev"
|
||||
} else {
|
||||
runMode = "release"
|
||||
}
|
||||
|
||||
// 初始化路由
|
||||
router = gin.Default()
|
||||
|
||||
// 定义路由
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.Redirect(http.StatusMovedPermanently, "https://ghproxy0rtt.1888866.xyz/")
|
||||
})
|
||||
|
||||
router.GET("/api", api)
|
||||
|
||||
// 健康检查
|
||||
router.GET("/api/healthcheck", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "OK")
|
||||
})
|
||||
|
||||
// 未匹配路由处理
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
proxy.NoRouteHandler(cfg)(c)
|
||||
})
|
||||
if cfg.Server.Debug {
|
||||
version = "Dev" // 如果是Debug模式,版本设置为"Dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 启动服务器
|
||||
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port))
|
||||
if err != nil {
|
||||
log.Fatalf("Error starting server: %v\n", err)
|
||||
if showVersion || showHelp {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Program finished")
|
||||
}
|
||||
if cfg == nil {
|
||||
fmt.Println("Config not loaded, exiting.")
|
||||
return
|
||||
}
|
||||
|
||||
func api(c *gin.Context) {
|
||||
// 设置响应头
|
||||
c.Writer.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
|
||||
"MaxResponseBodySize": cfg.Server.SizeLimit,
|
||||
r := touka.Default()
|
||||
r.SetProtocols(&touka.ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2_Cleartext: true,
|
||||
})
|
||||
|
||||
r.Use(touka.Recovery()) // Recovery中间件
|
||||
r.SetLogger(logger)
|
||||
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,
|
||||
}))
|
||||
}
|
||||
setupApi(cfg, r, version)
|
||||
setupPages(cfg, r)
|
||||
r.RedirectTrailingSlash = 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, 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.Any("/v2/:target/*filepath", func( c *touka.Context) {
|
||||
proxy.GhcrRouting(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 |
226
pages/index.html
226
pages/index.html
@@ -1,226 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Github文件加速">
|
||||
<title>Github文件加速</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:400,700:MiSans">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fac5;
|
||||
font-family: 'Misans', Arial, sans-serif;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
text-align: center;
|
||||
min-height: 45vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
margin-bottom: 75px;
|
||||
}
|
||||
|
||||
.rounded-button {
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
padding: 10px 30px;
|
||||
background-color: #39c5bb;
|
||||
color: white;
|
||||
border: none;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rounded-button:hover {
|
||||
background-color: #39c5bcda;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.tips>p:first-child::before {
|
||||
position: sticky;
|
||||
color: #7b7b7b;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
padding: 20px 20px;
|
||||
margin: 10px 0;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #ff5f56;
|
||||
border-radius: 50%;
|
||||
box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 0.875em;
|
||||
|
||||
}
|
||||
|
||||
.code {
|
||||
position: relative;
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 10px;
|
||||
background: rgba(118, 119, 121, 0.7);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
pre:hover .copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#visitor-info {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
line-height: 0.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Github文件加速</h1>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" id="githubLinkInput" placeholder="键入Github链接">
|
||||
</div>
|
||||
<button class="btn rounded-button" id="formatButton">获取文件链接</button>
|
||||
|
||||
<div class="code" id="outputBlock">
|
||||
<button class="copy-button" id="copyButton" onclick="copyCode(this)">Copy</button>
|
||||
<pre id="formattedLinkOutput"></pre>
|
||||
</div>
|
||||
<div class="tips">
|
||||
<p>GitHub链接带不带协议头均可,支持release、archive以及文件,转换后链接均可使用</a>。</p>
|
||||
<p id="sizeLimitDisplay">文件大小限制: ...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function formatGithubLink() {
|
||||
var githubLinkInput = document.getElementById('githubLinkInput');
|
||||
var currentHost = window.location.host;
|
||||
var formattedLink = "";
|
||||
|
||||
if (githubLinkInput.value.startsWith("https://github.com/") || githubLinkInput.value.startsWith("http://github.com/")) {
|
||||
formattedLink = "https://" + currentHost + "/github.com" + githubLinkInput.value.substring(githubLinkInput.value.indexOf("/", 8));
|
||||
} else if (githubLinkInput.value.startsWith("github.com/")) {
|
||||
formattedLink = "https://" + currentHost + "/" + githubLinkInput.value;
|
||||
} else if (githubLinkInput.value.startsWith("https://raw.githubusercontent.com/") || githubLinkInput.value.startsWith("http://raw.githubusercontent.com/")) {
|
||||
formattedLink = "https://" + currentHost + githubLinkInput.value.substring(githubLinkInput.value.indexOf("/", 7));
|
||||
} else if (githubLinkInput.value.startsWith("raw.githubusercontent.com/")) {
|
||||
formattedLink = "https://" + currentHost + "/" + githubLinkInput.value;
|
||||
} else if (!githubLinkInput.value.trim()) {
|
||||
alert('请输入有效的GitHub链接');
|
||||
}
|
||||
var formattedLinkOutput = document.getElementById('formattedLinkOutput');
|
||||
formattedLinkOutput.textContent = formattedLink;
|
||||
}
|
||||
|
||||
document.getElementById('formatButton').addEventListener('click', formatGithubLink);
|
||||
document.getElementById('copyButton').addEventListener('click', function () {
|
||||
const output = document.getElementById('formattedLinkOutput');
|
||||
const range = document.createRange();
|
||||
range.selectNode(output);
|
||||
window.getSelection().removeAllRanges();
|
||||
window.getSelection().addRange(range);
|
||||
document.execCommand('copy');
|
||||
window.getSelection().removeAllRanges();
|
||||
alert('链接已复制到剪贴板');
|
||||
});
|
||||
|
||||
function fetchAPI() {
|
||||
fetch(window.location.origin + '/api')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const sizeLimitDisplay = document.getElementById('sizeLimitDisplay');
|
||||
const sizeInMB = (data.MaxResponseBodySize / (1024 * 1024)).toFixed(0);
|
||||
sizeLimitDisplay.textContent = `文件大小限制: ${sizeInMB} MB`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching API:', error);
|
||||
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', fetchAPI);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
<footer>
|
||||
<p>
|
||||
Copyright © 2024 WJQSERVER-STUDIO
|
||||
</p>
|
||||
<p>
|
||||
GitHub仓库地址:<a
|
||||
href="https://github.com/WJQSERVER-STUDIO/ghproxy">https://github.com/WJQSERVER-STUDIO/ghproxy</a>
|
||||
</p>
|
||||
<div id="visitor-info" style="text-align: center; margin-top: 15px;">
|
||||
<p>您的IP地址: <span id="visitor-ip"></span></p>
|
||||
<p>当前位置: <span id="visitor-country"></span> <img id="visitor-flag" src="" alt="" width="24" height="16"></p>
|
||||
</div>
|
||||
<script>
|
||||
fetch('https://ip.1888866.xyz/ip-lookup')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('网络响应失败');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
document.getElementById('visitor-ip').textContent = data.ip;
|
||||
document.getElementById('visitor-country').textContent = data.country_name;
|
||||
document.getElementById('visitor-flag').src = `https://flagcdn.com/w20/${data.country_code.toLowerCase()}.png`;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取地理位置信息失败:', error);
|
||||
const visitorInfo = document.getElementById('visitor-info');
|
||||
visitorInfo.innerHTML = '<p>无法获取您的地理位置信息,请稍后再试。</p>';
|
||||
});
|
||||
</script>
|
||||
</footer>
|
||||
|
||||
</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
|
||||
}
|
||||
34
proxy/authpass.go
Normal file
34
proxy/authpass.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func AuthPassThrough(c *touka.Context, cfg *config.Config, req *http.Request) {
|
||||
if cfg.Auth.PassThrough {
|
||||
token := c.Query("token")
|
||||
if token != "" {
|
||||
switch cfg.Auth.Method {
|
||||
case "parameters":
|
||||
if !cfg.Auth.Enabled {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
} else {
|
||||
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":
|
||||
if cfg.Auth.Enabled {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
}
|
||||
default:
|
||||
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
|
||||
}
|
||||
158
proxy/chunkreq.go
Normal file
158
proxy/chunkreq.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func ChunkedProxyRequest(ctx context.Context, c *touka.Context, u string, cfg *config.Config, matcher string) {
|
||||
|
||||
var (
|
||||
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 != nil {
|
||||
req.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
rb := client.NewRequestBuilder(c.Request.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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 错误处理(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
|
||||
}
|
||||
}
|
||||
|
||||
// 处理响应体大小限制
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 复制响应头,排除需要移除的 header
|
||||
c.SetHeaders(resp.Header)
|
||||
for key := range respHeadersToRemove {
|
||||
c.DelHeader(key)
|
||||
}
|
||||
|
||||
switch cfg.Server.Cors {
|
||||
case "*":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
case "":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
case "nil":
|
||||
c.Header("Access-Control-Allow-Origin", "")
|
||||
default:
|
||||
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
|
||||
bodyReader := resp.Body
|
||||
|
||||
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)
|
||||
}
|
||||
335
proxy/docker.go
Normal file
335
proxy/docker.go
Normal file
@@ -0,0 +1,335 @@
|
||||
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) {
|
||||
|
||||
if reqTarget == "docker.io" {
|
||||
target = dockerhubTarget
|
||||
} else if reqTarget == "ghcr.io" {
|
||||
target = ghcrTarget
|
||||
} else {
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
//c.Request.Header.VisitAll(func(key, value []byte) {
|
||||
// headerKey := string(key)
|
||||
// headerValue := string(value)
|
||||
// req.Header.Add(headerKey, headerValue)
|
||||
//})
|
||||
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
|
||||
}
|
||||
|
||||
// 处理状态码
|
||||
if resp.StatusCode == 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
|
||||
}
|
||||
/*
|
||||
c.Request.Header.VisitAll(func(key, value []byte) {
|
||||
headerKey := string(key)
|
||||
headerValue := string(value)
|
||||
req.Header.Add(headerKey, headerValue)
|
||||
})
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
} else if resp.StatusCode == 404 { // 错误处理(404)
|
||||
ErrorPage(c, NewErrorWithStatusLookup(404, "Page Not Found (From Github)"))
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
bodySize int
|
||||
contentLength string
|
||||
sizelimit int
|
||||
)
|
||||
|
||||
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
|
||||
contentLength = resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
var err error
|
||||
bodySize, err = strconv.Atoi(contentLength)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 复制响应头,排除需要移除的 header
|
||||
/*
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Response.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
*/
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
331
proxy/error.go
Normal file
331
proxy/error.go
Normal file
@@ -0,0 +1,331 @@
|
||||
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)
|
||||
}
|
||||
|
||||
type GHProxyErrors struct {
|
||||
StatusCode int
|
||||
StatusDesc string
|
||||
StatusText string
|
||||
HelpInfo string
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidURL = &GHProxyErrors{
|
||||
StatusCode: 400,
|
||||
StatusDesc: "Bad Request",
|
||||
StatusText: "无效请求",
|
||||
HelpInfo: "请求的URL格式不正确,请检查后重试。",
|
||||
}
|
||||
ErrAuthHeaderUnavailable = &GHProxyErrors{
|
||||
StatusCode: 401,
|
||||
StatusDesc: "Unauthorized",
|
||||
StatusText: "认证失败",
|
||||
HelpInfo: "缺少或无效的鉴权信息。",
|
||||
}
|
||||
ErrForbidden = &GHProxyErrors{
|
||||
StatusCode: 403,
|
||||
StatusDesc: "Forbidden",
|
||||
StatusText: "权限不足",
|
||||
HelpInfo: "您没有权限访问此资源。",
|
||||
}
|
||||
ErrNotFound = &GHProxyErrors{
|
||||
StatusCode: 404,
|
||||
StatusDesc: "Not Found",
|
||||
StatusText: "页面未找到",
|
||||
HelpInfo: "抱歉,您访问的页面不存在。",
|
||||
}
|
||||
ErrTooManyRequests = &GHProxyErrors{
|
||||
StatusCode: 429,
|
||||
StatusDesc: "Too Many Requests",
|
||||
StatusText: "请求过于频繁",
|
||||
HelpInfo: "您的请求过于频繁,请稍后再试。",
|
||||
}
|
||||
ErrInternalServerError = &GHProxyErrors{
|
||||
StatusCode: 500,
|
||||
StatusDesc: "Internal Server Error",
|
||||
StatusText: "服务器内部错误",
|
||||
HelpInfo: "服务器处理您的请求时发生错误,请稍后重试或联系管理员。",
|
||||
}
|
||||
)
|
||||
|
||||
var statusErrorMap map[int]*GHProxyErrors
|
||||
|
||||
func init() {
|
||||
statusErrorMap = map[int]*GHProxyErrors{
|
||||
ErrInvalidURL.StatusCode: ErrInvalidURL,
|
||||
ErrAuthHeaderUnavailable.StatusCode: ErrAuthHeaderUnavailable,
|
||||
ErrForbidden.StatusCode: ErrForbidden,
|
||||
ErrNotFound.StatusCode: ErrNotFound,
|
||||
ErrTooManyRequests.StatusCode: ErrTooManyRequests,
|
||||
ErrInternalServerError.StatusCode: ErrInternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
func NewErrorWithStatusLookup(statusCode int, errMsg string) *GHProxyErrors {
|
||||
baseErr, found := statusErrorMap[statusCode]
|
||||
|
||||
if found {
|
||||
return &GHProxyErrors{
|
||||
StatusCode: baseErr.StatusCode,
|
||||
StatusDesc: baseErr.StatusDesc,
|
||||
StatusText: baseErr.StatusText,
|
||||
HelpInfo: baseErr.HelpInfo,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
} else {
|
||||
return &GHProxyErrors{
|
||||
StatusCode: statusCode,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errPagesFs fs.FS
|
||||
|
||||
func InitErrPagesFS(pages fs.FS) error {
|
||||
var err error
|
||||
errPagesFs, err = fs.Sub(pages, "pages/err")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ErrorPageData struct {
|
||||
StatusCode int
|
||||
StatusDesc string
|
||||
StatusText string
|
||||
HelpInfo string
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// 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.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.currentBytes -= int64(len(value))
|
||||
//logDebug("LRU evicted key: %s, size: %d, current total: %d", key, len(value), c.currentBytes)
|
||||
})
|
||||
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 {
|
||||
//c.Warnf("Item key %s (size %d) larger than cache max capacity %d. Not caching.", key, itemSize, c.maxBytes)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果键已存在,则首先从 currentBytes 中减去旧值的大小,并从内部 LRU 中移除旧条目。
|
||||
if oldVal, ok := c.cache.Get(key); ok {
|
||||
c.currentBytes -= int64(len(oldVal))
|
||||
c.cache.Remove(key)
|
||||
//logDebug("Key %s exists, removed old size %d. Current total: %d", key, len(oldVal), c.currentBytes)
|
||||
}
|
||||
|
||||
// 主动逐出最旧的条目,直到有足够的空间容纳新条目。
|
||||
for c.currentBytes+itemSize > c.maxBytes && c.cache.Len() > 0 {
|
||||
_, _, existed := c.cache.RemoveOldest()
|
||||
if !existed {
|
||||
//c.Warnf("Attempted to remove oldest, but item not found.")
|
||||
break
|
||||
}
|
||||
//logDebug("Proactively evicted item (size %d) to free space. Current total: %d", len(oldVal), c.currentBytes)
|
||||
}
|
||||
|
||||
// 添加新条目到内部 LRU 缓存。
|
||||
c.cache.Add(key, value)
|
||||
c.currentBytes += itemSize // 手动增加新条目的大小到 currentBytes。
|
||||
//logDebug("Item added: key %s, size: %d, current total: %d", key, itemSize, c.currentBytes)
|
||||
}
|
||||
|
||||
const maxErrorPageCacheBytes = 512 * 1024 // 错误页面缓存的最大容量:512KB
|
||||
|
||||
var errorPageCache *SizedLRUCache
|
||||
|
||||
func init() {
|
||||
// 初始化 SizedLRUCache。
|
||||
var err error
|
||||
errorPageCache, err = NewSizedLRUCache(maxErrorPageCacheBytes)
|
||||
if err != nil {
|
||||
// logError("Failed to initialize error page LRU cache: %v", err)
|
||||
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) {
|
||||
// 将 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})
|
||||
}
|
||||
162
proxy/gitreq.go
Normal file
162
proxy/gitreq.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/limitreader"
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
func GitReq(ctx context.Context, c *touka.Context, u string, cfg *config.Config, mode string) {
|
||||
|
||||
var (
|
||||
resp *http.Response
|
||||
)
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
if resp != nil && resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
/*
|
||||
fullBody, err := c.GetReqBodyFull()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
|
||||
return
|
||||
}
|
||||
reqBodyReader := bytes.NewBuffer(fullBody)
|
||||
*/
|
||||
|
||||
reqBodyReader, err := c.GetReqBodyBuffer()
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
//bodyReader := c.Request.BodyStream() // 不可替换为此实现
|
||||
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to extract parts from URL: %v", err))
|
||||
return
|
||||
}
|
||||
// 构建新url
|
||||
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
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.Response.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
*/
|
||||
//copyHeader( resp.Header)
|
||||
c.SetHeaders(resp.Header)
|
||||
|
||||
headersToRemove := map[string]struct{}{
|
||||
"Content-Security-Policy": {},
|
||||
"Referrer-Policy": {},
|
||||
"Strict-Transport-Security": {},
|
||||
}
|
||||
|
||||
for header := range headersToRemove {
|
||||
resp.Header.Del(header)
|
||||
}
|
||||
|
||||
switch cfg.Server.Cors {
|
||||
case "*":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
case "":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
case "nil":
|
||||
c.Header("Access-Control-Allow-Origin", "")
|
||||
default:
|
||||
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
if 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
|
||||
|
||||
// 读取body内容
|
||||
//bodyContent, _ := io.ReadAll(bodyReader)
|
||||
// c.Infof("%s", bodyContent)
|
||||
|
||||
if cfg.RateLimit.BandwidthLimit.Enabled {
|
||||
bodyReader = limitreader.NewRateLimitedReader(bodyReader, bandwidthLimit, int(bandwidthBurst), ctx)
|
||||
}
|
||||
|
||||
c.SetBodyStream(bodyReader, -1)
|
||||
}
|
||||
83
proxy/handler.go
Normal file
83
proxy/handler.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
|
||||
|
||||
func NoRouteHandler(cfg *config.Config) touka.HandlerFunc {
|
||||
return func(c *touka.Context) {
|
||||
var ctx = c.Request.Context()
|
||||
var shoudBreak bool
|
||||
// shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
|
||||
// if shoudBreak {
|
||||
// return
|
||||
// }
|
||||
|
||||
var (
|
||||
rawPath string
|
||||
matches []string
|
||||
)
|
||||
|
||||
rawPath = strings.TrimPrefix(c.GetRequestURI(), "/") // 去掉前缀/
|
||||
matches = re.FindStringSubmatch(rawPath) // 匹配路径
|
||||
|
||||
// 匹配路径错误处理
|
||||
if len(matches) < 3 {
|
||||
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]
|
||||
|
||||
var (
|
||||
user string
|
||||
repo string
|
||||
matcher string
|
||||
)
|
||||
|
||||
var matcherErr *GHProxyErrors
|
||||
user, repo, matcher, matcherErr = Matcher(rawPath, cfg)
|
||||
if matcherErr != nil {
|
||||
ErrorPage(c, matcherErr)
|
||||
return
|
||||
}
|
||||
|
||||
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[18:]
|
||||
rawPath = "https://raw.githubusercontent.com" + rawPath
|
||||
rawPath = strings.Replace(rawPath, "/blob/", "/", 1)
|
||||
matcher = "raw"
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
174
proxy/httpc.go
Normal file
174
proxy/httpc.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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
|
||||
ghcrtr *http.Transport
|
||||
ghcrclient *httpc.Client
|
||||
)
|
||||
|
||||
func InitReq(cfg *config.Config) error {
|
||||
initHTTPClient(cfg)
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
initGitHTTPClient(cfg)
|
||||
}
|
||||
initGhcrHTTPClient(cfg)
|
||||
err := SetGlobalRateLimit(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func initHTTPClient(cfg *config.Config) {
|
||||
var proTolcols = new(http.Protocols)
|
||||
proTolcols.SetHTTP1(true)
|
||||
proTolcols.SetHTTP2(true)
|
||||
proTolcols.SetUnencryptedHTTP2(true)
|
||||
if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
|
||||
|
||||
tr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
} else if cfg.Httpc.Mode == "advanced" {
|
||||
tr = &http.Transport{
|
||||
MaxIdleConns: cfg.Httpc.MaxIdleConns,
|
||||
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
|
||||
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
} else {
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func initGitHTTPClient(cfg *config.Config) {
|
||||
|
||||
if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
|
||||
gittr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
} else if cfg.Httpc.Mode == "advanced" {
|
||||
gittr = &http.Transport{
|
||||
MaxIdleConns: cfg.Httpc.MaxIdleConns,
|
||||
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
|
||||
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
} else {
|
||||
panic("unknown httpc mode: " + cfg.Httpc.Mode)
|
||||
}
|
||||
if cfg.Outbound.Enabled {
|
||||
initTransport(cfg, gittr)
|
||||
}
|
||||
if cfg.Server.Debug && cfg.GitClone.ForceH2C {
|
||||
gitclient = httpc.New(
|
||||
httpc.WithTransport(gittr),
|
||||
httpc.WithDumpLog(),
|
||||
httpc.WithProtocols(httpc.ProtocolsConfig{
|
||||
ForceH2C: true,
|
||||
}),
|
||||
)
|
||||
} else if !cfg.Server.Debug && cfg.GitClone.ForceH2C {
|
||||
gitclient = httpc.New(
|
||||
httpc.WithTransport(gittr),
|
||||
httpc.WithProtocols(httpc.ProtocolsConfig{
|
||||
ForceH2C: true,
|
||||
}),
|
||||
)
|
||||
} else if cfg.Server.Debug && !cfg.GitClone.ForceH2C {
|
||||
gitclient = httpc.New(
|
||||
httpc.WithTransport(gittr),
|
||||
httpc.WithDumpLog(),
|
||||
httpc.WithProtocols(httpc.ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2: true,
|
||||
Http2_Cleartext: true,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
gitclient = httpc.New(
|
||||
httpc.WithTransport(gittr),
|
||||
httpc.WithProtocols(httpc.ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2: true,
|
||||
Http2_Cleartext: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func initGhcrHTTPClient(cfg *config.Config) {
|
||||
var proTolcols = new(http.Protocols)
|
||||
proTolcols.SetHTTP1(true)
|
||||
proTolcols.SetHTTP2(true)
|
||||
if cfg.Httpc.Mode == "auto" || cfg.Httpc.Mode == "" {
|
||||
|
||||
ghcrtr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
} else if cfg.Httpc.Mode == "advanced" {
|
||||
ghcrtr = &http.Transport{
|
||||
MaxIdleConns: cfg.Httpc.MaxIdleConns,
|
||||
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
|
||||
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
} else {
|
||||
panic(fmt.Sprintf("unknown httpc mode: %s", cfg.Httpc.Mode))
|
||||
}
|
||||
if cfg.Outbound.Enabled {
|
||||
initTransport(cfg, ghcrtr)
|
||||
}
|
||||
if cfg.Server.Debug {
|
||||
ghcrclient = httpc.New(
|
||||
httpc.WithTransport(ghcrtr),
|
||||
httpc.WithDumpLog(),
|
||||
)
|
||||
} else {
|
||||
ghcrclient = httpc.New(
|
||||
httpc.WithTransport(ghcrtr),
|
||||
)
|
||||
}
|
||||
}
|
||||
311
proxy/match.go
Normal file
311
proxy/match.go
Normal file
@@ -0,0 +1,311 @@
|
||||
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)
|
||||
//log.Printf("githubPrefixLen: %d, rawPrefixLen: %d, gistPrefixLen: %d, apiPrefixLen: %d", githubPrefixLen, rawPrefixLen, gistPrefixLen, apiPrefixLen)
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 原实现
|
||||
/*
|
||||
func Matcher(rawPath string, cfg *config.Config) (string, string, string, *GHProxyErrors) {
|
||||
var (
|
||||
user string
|
||||
repo string
|
||||
matcher string
|
||||
)
|
||||
// 匹配 "https://github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://github.com") {
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
|
||||
|
||||
//if strings.HasPrefix(remainingPath, "/") {
|
||||
// remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
//}
|
||||
|
||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
// 预期格式/user/repo/more...
|
||||
// 取出user和repo和最后部分
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
if len(parts) <= 2 {
|
||||
errMsg := "Not enough parts in path after matching 'https://github.com*'"
|
||||
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
|
||||
}
|
||||
user = parts[0]
|
||||
repo = parts[1]
|
||||
// 匹配 "https://github.com"开头的链接
|
||||
if len(parts) >= 3 {
|
||||
switch parts[2] {
|
||||
case "releases", "archive":
|
||||
matcher = "releases"
|
||||
case "blob":
|
||||
matcher = "blob"
|
||||
case "raw":
|
||||
matcher = "raw"
|
||||
case "info", "git-upload-pack":
|
||||
matcher = "clone"
|
||||
default:
|
||||
errMsg := "Url Matched 'https://github.com*', but didn't match the next matcher"
|
||||
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
|
||||
}
|
||||
}
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
// 匹配 "https://raw"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://raw") {
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://")
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
if len(parts) <= 3 {
|
||||
errMsg := "URL after matched 'https://raw*' should have at least 4 parts (user/repo/branch/file)."
|
||||
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
|
||||
}
|
||||
user = parts[1]
|
||||
repo = parts[2]
|
||||
matcher = "raw"
|
||||
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
// 匹配 "https://gist"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://gist") {
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://")
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
if len(parts) <= 3 {
|
||||
errMsg := "URL after matched 'https://gist*' should have at least 4 parts (user/gist_id)."
|
||||
return "", "", "", NewErrorWithStatusLookup(400, errMsg)
|
||||
}
|
||||
user = parts[1]
|
||||
repo = ""
|
||||
matcher = "gist"
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
// 匹配 "https://api.github.com/"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://api.github.com/") {
|
||||
matcher = "api"
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/")
|
||||
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
if parts[0] == "repos" {
|
||||
user = parts[1]
|
||||
repo = parts[2]
|
||||
}
|
||||
if parts[0] == "users" {
|
||||
user = parts[1]
|
||||
}
|
||||
if !cfg.Auth.ForceAllowApi {
|
||||
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
|
||||
//return "", "", "", ErrAuthHeaderUnavailable
|
||||
errMsg := "AuthHeader Unavailable, Need to open header auth to enable api proxy"
|
||||
return "", "", "", NewErrorWithStatusLookup(403, errMsg)
|
||||
}
|
||||
}
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
//return "", "", "", ErrNotFound
|
||||
errMsg := "Didn't match any matcher"
|
||||
return "", "", "", NewErrorWithStatusLookup(404, errMsg)
|
||||
}
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 传递
|
||||
}
|
||||
232
proxy/proxy.go
232
proxy/proxy.go
@@ -1,232 +0,0 @@
|
||||
// proxy/proxy.go 实验性
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ghproxy/auth"
|
||||
"ghproxy/config"
|
||||
"ghproxy/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/imroc/req/v3"
|
||||
)
|
||||
|
||||
var logw = logger.Logw
|
||||
|
||||
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\.com/([^/]+)/.+?/.+`),
|
||||
}
|
||||
|
||||
func NoRouteHandler(cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/")
|
||||
re := regexp.MustCompile(`^(http:|https:)?/?/?(.*)`)
|
||||
matches := re.FindStringSubmatch(rawPath)
|
||||
|
||||
if len(matches) < 3 {
|
||||
logw("Invalid URL: %s", rawPath)
|
||||
c.String(http.StatusForbidden, "Invalid URL.")
|
||||
return
|
||||
}
|
||||
|
||||
rawPath = "https://" + matches[2]
|
||||
|
||||
// 提取用户名和仓库名,格式为 handle/<username>/<repo>/*
|
||||
pathmatches := regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`)
|
||||
pathParts := pathmatches.FindStringSubmatch(matches[2])
|
||||
if len(pathParts) < 4 {
|
||||
logw("Invalid path: %s", rawPath)
|
||||
c.String(http.StatusForbidden, "Invalid path; expected username/repo.")
|
||||
return
|
||||
}
|
||||
|
||||
username := pathParts[2]
|
||||
repo := pathParts[3]
|
||||
logw("Blacklist Check > Username: %s, Repo: %s", username, repo)
|
||||
fullrepo := fmt.Sprintf("%s/%s", username, repo)
|
||||
|
||||
// 白名单检查
|
||||
if cfg.Whitelist.Enabled {
|
||||
whitelistpass := auth.CheckWhitelist(fullrepo)
|
||||
if !whitelistpass {
|
||||
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", fullrepo)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
|
||||
logw(errMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 黑名单检查
|
||||
if cfg.Blacklist.Enabled {
|
||||
blacklistpass := auth.CheckBlacklist(fullrepo)
|
||||
if blacklistpass {
|
||||
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", fullrepo)
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
|
||||
logw(errMsg)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
matches = CheckURL(rawPath)
|
||||
if matches == nil {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if exps[1].MatchString(rawPath) {
|
||||
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
||||
}
|
||||
|
||||
if !auth.AuthHandler(c, cfg) {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
|
||||
logw("Unauthorized request: %s", rawPath)
|
||||
return
|
||||
}
|
||||
|
||||
logw("Matches: %v", matches)
|
||||
|
||||
switch {
|
||||
case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath):
|
||||
logw("%s Matched - USE proxy-chrome", rawPath)
|
||||
ProxyRequest(c, rawPath, cfg, "chrome")
|
||||
case exps[2].MatchString(rawPath):
|
||||
logw("%s Matched - USE proxy-git", rawPath)
|
||||
ProxyRequest(c, rawPath, cfg, "git")
|
||||
default:
|
||||
c.String(http.StatusForbidden, "Invalid input.")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) {
|
||||
method := c.Request.Method
|
||||
logw("%s %s", method, u)
|
||||
|
||||
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/128.0.0.0 Safari/537.36").
|
||||
SetTLSFingerprintChrome().
|
||||
ImpersonateChrome()
|
||||
case "git":
|
||||
client.SetUserAgent("git/2.33.1")
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to read request body: %v", err))
|
||||
return
|
||||
}
|
||||
defer c.Request.Body.Close()
|
||||
|
||||
req := client.R().SetBody(body)
|
||||
|
||||
for key, values := range c.Request.Header {
|
||||
for _, value := range values {
|
||||
req.SetHeader(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := SendRequest(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 {
|
||||
logw("Error handling response size: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
CopyResponseHeaders(resp, c, cfg)
|
||||
c.Status(resp.StatusCode)
|
||||
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
||||
logw("Failed to copy response body: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func SendRequest(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:
|
||||
logw("Unsupported method: %s", method)
|
||||
return nil, fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleResponseSize(resp *req.Response, cfg *config.Config, c *gin.Context) error {
|
||||
contentLength := resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > cfg.Server.SizeLimit {
|
||||
finalURL := resp.Request.URL.String()
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logw("Redirecting to %s due to size limit (%d bytes)", finalURL, size)
|
||||
return fmt.Errorf("response size exceeds limit")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CopyResponseHeaders(resp *req.Response, c *gin.Context, cfg *config.Config) {
|
||||
headersToRemove := map[string]struct{}{
|
||||
"Content-Security-Policy": {},
|
||||
"Referrer-Policy": {},
|
||||
"Strict-Transport-Security": {},
|
||||
}
|
||||
|
||||
for header := range headersToRemove {
|
||||
resp.Header.Del(header)
|
||||
}
|
||||
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
c.Header("Access-Control-Allow-Origin", "")
|
||||
if cfg.CORS.Enabled {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
c.Header("Age", "10")
|
||||
c.Header("Cache-Control", "max-age=300")
|
||||
}
|
||||
|
||||
func HandleError(c *gin.Context, message string) {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
|
||||
logw(message)
|
||||
}
|
||||
|
||||
func CheckURL(u string) []string {
|
||||
for _, exp := range exps {
|
||||
if matches := exp.FindStringSubmatch(u); matches != nil {
|
||||
logw("URL matched: %s, Matches: %v", u, matches[1:])
|
||||
return matches[1:]
|
||||
}
|
||||
}
|
||||
errMsg := fmt.Sprintf("Invalid URL: %s", u)
|
||||
logw(errMsg)
|
||||
return nil
|
||||
}
|
||||
79
proxy/reqheader.go
Normal file
79
proxy/reqheader.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
|
||||
"github.com/infinite-iroha/touka"
|
||||
)
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
74
proxy/routing.go
Normal file
74
proxy/routing.go
Normal file
@@ -0,0 +1,74 @@
|
||||
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
|
||||
|
||||
// shoudBreak = rateCheck(cfg, c, limiter, iplimiter)
|
||||
// if shoudBreak {
|
||||
// return
|
||||
//}
|
||||
|
||||
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
|
||||
}
|
||||
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