Compare commits
351 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c656aa41ca | ||
|
|
0b052f9c7f | ||
|
|
6fb7e1150e | ||
|
|
5e0f95dae3 | ||
|
|
c1c39a5a1f | ||
|
|
dd2f5b5a12 | ||
|
|
7e5b12dff8 | ||
|
|
26a42b6510 | ||
|
|
254c9a8bad | ||
|
|
060453f070 | ||
|
|
f110c96c1f | ||
|
|
73aac79c1b | ||
|
|
bed6c486dc | ||
|
|
ab77c5c7da | ||
|
|
bf21bd197a | ||
|
|
8af107c584 | ||
|
|
d6d54b222f | ||
|
|
005a4543d4 | ||
|
|
a85eb38de5 | ||
|
|
152fb8aa71 | ||
|
|
3e9e43cd44 | ||
|
|
8a50b311fc | ||
|
|
dcc50401c4 | ||
|
|
d62a1f9769 | ||
|
|
c522eba7ae | ||
|
|
3da54f0599 | ||
|
|
886c99f53d | ||
|
|
d3520a2133 | ||
|
|
1f0b43ec43 | ||
|
|
36646ebf7e | ||
|
|
d7ed4fc6ad | ||
|
|
7cbce12316 | ||
|
|
ff412f94ec | ||
|
|
b02aaeba8a | ||
|
|
395f641468 | ||
|
|
978ece6fa0 | ||
|
|
1adc3a3192 | ||
|
|
a66452cf10 | ||
|
|
d231fd839f | ||
|
|
4b37c6bb2b | ||
|
|
153b544024 | ||
|
|
635c22f9a7 | ||
|
|
f342312b40 | ||
|
|
acaf38b88d | ||
|
|
50cfd64db8 | ||
|
|
53e115242a | ||
|
|
cef0338d36 | ||
|
|
f8edb0e0bc | ||
|
|
c11f368a9c | ||
|
|
db38b2a402 | ||
|
|
accb52b952 | ||
|
|
70fb808acf | ||
|
|
b684227191 | ||
|
|
1498156f56 | ||
|
|
55158c0cb1 | ||
|
|
6c3280f850 | ||
|
|
866275aad3 | ||
|
|
f4cd7eecf1 | ||
|
|
5501cd3e3c | ||
|
|
f9f37262f0 | ||
|
|
026039e0bc | ||
|
|
8739027772 | ||
|
|
cafc713a65 | ||
|
|
8f2cc820aa | ||
|
|
139fc92abc | ||
|
|
e9d793c104 | ||
|
|
c931017f03 | ||
|
|
448e06d350 | ||
|
|
27cc30ab8b | ||
|
|
a65e44ac02 | ||
|
|
a0cfe826ea | ||
|
|
2e974ad7ae | ||
|
|
b7b9cd5db5 | ||
|
|
bcb73c18de | ||
|
|
ed839b828d | ||
|
|
801b8c6cda | ||
|
|
a92bbb7fb6 | ||
|
|
3e40146281 | ||
|
|
ac7e1e43b5 | ||
|
|
f134d22540 | ||
|
|
79153c0f7d | ||
|
|
4fd47812f7 | ||
|
|
17c49d534b | ||
|
|
284b38bab4 | ||
|
|
d73dfe7db5 | ||
|
|
dc286e002c | ||
|
|
5c54ae788c | ||
|
|
bfcb1c9901 | ||
|
|
9bfe8517cb | ||
|
|
50ba185aab | ||
|
|
6ee928b0c7 | ||
|
|
979f59545b | ||
|
|
da89b3f45e | ||
|
|
498266e08e | ||
|
|
e2faa497ab | ||
|
|
8def955151 | ||
|
|
a18660121a | ||
|
|
d26f6d1e1b | ||
|
|
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 | ||
|
|
69d4d53a51 | ||
|
|
6864925dbe | ||
|
|
2bdc2d0ace | ||
|
|
1e496fb09e | ||
|
|
23bb463490 | ||
|
|
8eb6c09562 | ||
|
|
1277af445c | ||
|
|
62ee8ede1a | ||
|
|
87b62e0342 | ||
|
|
245040a497 | ||
|
|
1b5b34d265 | ||
|
|
96fba543a2 | ||
|
|
38430a3a68 | ||
|
|
71c5f25d1d | ||
|
|
2a53ac1b72 | ||
|
|
505c2e559e | ||
|
|
dd9a0c8adb | ||
|
|
f4cb77a72e | ||
|
|
4155b9cf4f | ||
|
|
00538f3d91 | ||
|
|
1fd1e3bc2a | ||
|
|
b83c242416 | ||
|
|
57146483ec | ||
|
|
03c33449db | ||
|
|
6b9b864a4f | ||
|
|
db7f8f4bcb | ||
|
|
bfcb835608 | ||
|
|
1eeeb56e9b | ||
|
|
d0b20a8828 | ||
|
|
b57aa84bda | ||
|
|
4eb9d1a899 | ||
|
|
d3d9f78820 | ||
|
|
f3a49b83f2 | ||
|
|
fd7491aaa7 | ||
|
|
b11a864495 | ||
|
|
564364db74 | ||
|
|
a1f0eaafa6 | ||
|
|
2bbb539dda | ||
|
|
7ba2b94a00 | ||
|
|
5e764a6cd7 | ||
|
|
abeb6ee8eb | ||
|
|
df93a90081 | ||
|
|
05032158d4 | ||
|
|
b5bfc809a2 | ||
|
|
dc8f4a28e9 | ||
|
|
f5d7d0994e | ||
|
|
e3cffe70d9 | ||
|
|
822b6c8cea | ||
|
|
5ac08bba56 | ||
|
|
61e741c9b3 |
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
|
||||
92
.github/workflows/build-dev.yml
vendored
92
.github/workflows/build-dev.yml
vendored
@@ -4,54 +4,101 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'dev'
|
||||
paths:
|
||||
- 'DEV-VERSION'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
OUTPUT_ARCHIVE: ghproxy.tar.gz
|
||||
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/GHPrxoy-Frontend.git pages
|
||||
sudo rm -rf pages/.git/
|
||||
|
||||
- name: 安装 Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Build
|
||||
- name: 编译
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
go build -o ${{ env.OUTPUT_BINARY }} ./main.go
|
||||
- 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_ARCHIVE }} ./${{ env.OUTPUT_BINARY }}
|
||||
- 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_ARCHIVE }}
|
||||
./${{ env.OUTPUT_BINARY }}
|
||||
./${{ env.OUTPUT_BINARY }}*
|
||||
- name: 上传至Release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ env.VERSION }}
|
||||
artifacts: ./${{ env.OUTPUT_ARCHIVE }}, ./${{ 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
|
||||
|
||||
@@ -61,10 +108,13 @@ jobs:
|
||||
env:
|
||||
IMAGE_NAME: wjqserver/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
|
||||
@@ -86,10 +136,10 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: 构建镜像
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./${{ env.DOCKERFILE }}
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
|
||||
98
.github/workflows/build.yml
vendored
98
.github/workflows/build.yml
vendored
@@ -9,48 +9,95 @@ on:
|
||||
- 'VERSION'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
OUTPUT_ARCHIVE: ghproxy.tar.gz
|
||||
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/GHPrxoy-Frontend.git pages
|
||||
sudo rm -rf pages/.git/
|
||||
|
||||
- name: 安装 Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
- name: Build
|
||||
- name: 编译
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
go build -o ${{ env.OUTPUT_BINARY }} ./main.go
|
||||
- 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_ARCHIVE }} ./${{ env.OUTPUT_BINARY }}
|
||||
- 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_ARCHIVE }}
|
||||
./${{ env.OUTPUT_BINARY }}
|
||||
./${{ env.OUTPUT_BINARY }}*
|
||||
- name: 上传至Release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ env.VERSION }}
|
||||
artifacts: ./${{ env.OUTPUT_ARCHIVE }}, ./${{ 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
|
||||
|
||||
@@ -59,20 +106,21 @@ jobs:
|
||||
needs: build # 确保这个作业在 build 作业完成后运行
|
||||
env:
|
||||
IMAGE_NAME: wjqserver/ghproxy # 定义镜像名称变量
|
||||
DOCKERFILE: docker/dockerfile/release/Dockerfile
|
||||
DOCKERFILE: docker/dockerfile/release/Dockerfile # 定义 Dockerfile 路径变量
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
- name: Load VERSION
|
||||
run: |
|
||||
if [ -f DEV-VERSION ]; then
|
||||
echo "VERSION=$(cat DEV-VERSION)" >> $GITHUB_ENV
|
||||
if [ -f VERSION ]; then
|
||||
echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DEV-VERSION file not found!" && exit 1
|
||||
echo "VERSION file not found!" && exit 1
|
||||
fi
|
||||
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -86,10 +134,10 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: 构建镜像
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: ./${{ env.DOCKERFILE }}
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
|
||||
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
demo
|
||||
demo.toml
|
||||
*.log
|
||||
*.bak
|
||||
list.json
|
||||
repos
|
||||
pages
|
||||
1153
CHANGELOG.md
1153
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
||||
24w09a
|
||||
25w29b
|
||||
214
LICENSE
214
LICENSE
@@ -1,107 +1,197 @@
|
||||
WJQserver Studio 开源许可证
|
||||
版本 1.2
|
||||
版本 v2.0
|
||||
|
||||
版权所有 © WJQserver Studio 2024
|
||||
|
||||
定义
|
||||
许可:指在本许可证内定义的使用、复制、分发与修改的条款与要求。
|
||||
授权方:指拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体。
|
||||
您:指行使本许可授予的权限的个人或法律实体。
|
||||
开源与自由软件
|
||||
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
|
||||
本项目不等同于自由软件,使用权限受到本许可证条款的限制。
|
||||
强调版权所有,所有权利均由 WJQserver Studio 保留。
|
||||
许可证条款
|
||||
1. 使用权限
|
||||
1.1 您被授予在私人环境中自由使用本软件的权限。
|
||||
|
||||
1.2 您可以在不修改关键声明的前提下进行商用。
|
||||
* 许可 (License): 指的是在本许可证内定义的使用、复制、分发与修改软件的条款与要求。
|
||||
* 授权方 (Licensor): 指的是拥有版权的个人或组织,亦或是拥有版权的个人或组织所指派的实体,在本许可证中特指 WJQserver Studio。
|
||||
* 贡献者 (Contributor): 指的是授权方以及根据本许可证授予贡献代码或软件的个人或实体。
|
||||
* 您 (You): 指的是行使本许可授予的权限的个人或法律实体。
|
||||
* 衍生作品 (Derivative Works): 指的是基于本软件或本软件任何部分的修改作品,无论修改程度如何。这包括但不限于基于本软件或其任何部分的修改、修订、改编、翻译或其他形式的创作,以及包含本软件或其部分的集合作品。
|
||||
* 非营利性使用 (Non-profit Use): 指的是不以直接商业盈利为主要目的的使用方式,包括但不限于:
|
||||
* 个人用途: 由个人为了个人学习、研究、实验、非商业项目、个人网站搭建、毕业设计、家庭内部娱乐等非直接商业目的使用软件。
|
||||
* 教育用途: 在教育机构(如学校、大学、培训机构)内部用于教学、研究、学术交流等活动。
|
||||
* 科研用途: 在科研院所、实验室等机构内部用于科学研究、实验开发等活动。
|
||||
* 慈善与公益用途: 由慈善机构、公益组织等非营利性组织为了其公益使命或慈善事业内部运营使用,或对外提供不直接产生商业利润的公益服务。
|
||||
* 内部运营用途 (非营利组织): 非营利性组织在其内部运营中使用软件,例如用于行政管理、会员管理、内部沟通、项目管理等非直接营利性活动。
|
||||
|
||||
开源与自由软件
|
||||
|
||||
本项目为开源软件,允许用户在遵循本许可证的前提下访问和使用源代码。
|
||||
本项目旨在向用户提供尽可能广泛的非商业使用自由,同时保障社区的共同发展和良性生态,并为商业创新提供清晰的路径。
|
||||
强调版权所有,所有权利由 WJQserver Studio 及贡献者共同保留。
|
||||
|
||||
许可证条款
|
||||
|
||||
1. 使用权限
|
||||
|
||||
* 1.1 非营利性使用: 您被授予在非营利性使用场景下,为了任何目的,自由使用本软件的权限。 非营利性使用的具体场景包括但不限于定义部分所列举的各种情况。
|
||||
|
||||
* 1.2 商业使用: 您可以在商业环境中使用本软件,无需获得额外授权,但您的商业使用行为必须遵守以下条款:
|
||||
|
||||
* 1.2.1 保持声明: 您在进行商业使用时,不得移除或修改软件中包含的原始版权声明、许可证声明以及来源声明。
|
||||
* 1.2.2 开源继承 (Copyleft) 与互惠共享: 如果您或您的组织希望将本软件或其衍生作品用于任何商业用途,包括但不限于:
|
||||
|
||||
* 盈利性分发: 销售、出租、许可分发本软件或其衍生作品。
|
||||
* 盈利性服务: 基于本软件或其衍生作品提供商业服务,例如 SaaS 服务、咨询服务、定制开发服务、收费技术支持服务等。
|
||||
* 嵌入式商业应用: 将本软件或其衍生作品嵌入到商业产品或解决方案中进行销售。
|
||||
* 组织内部商业运营: 在营利性组织的内部运营中使用修改后的版本以直接支持其商业活动,例如定制化内部系统,通过例如但不限于在软件或相关服务中投放广告 (例如 Google Ads 等),应用内购买 (内购), 会员订阅, 增值功能收费等方式直接或间接产生商业收入。
|
||||
|
||||
您必须选择以下两种方式之一:
|
||||
|
||||
* i) 继承本许可证并开源: 您必须以本许可证或兼容的开源许可证分发您的衍生作品,并公开您的衍生作品的全部源代码,使得您的衍生作品的接收者也享有与您相同的权利,包括进一步修改和商业使用的权利。 本选项旨在促进社区的共同发展和知识共享,确保基于本软件的商业创新成果也能回馈社区。
|
||||
* ii) 获得授权方明确授权: 如果您不希望以开源方式发布您的衍生作品,或者希望使用其他许可证进行分发,或者您希望在商业运营中使用修改后的版本但不开源,您必须事先获得 WJQserver Studio 的明确书面授权。 授权的具体条款和条件将由 WJQserver Studio 另行协商确定。
|
||||
|
||||
2. 复制与分发
|
||||
2.1 您可以复制和分发本软件的原始版本,前提是必须保留所有版权声明和本许可证。
|
||||
|
||||
* 2.1 原始版本复制与分发: 您可以复制和分发本软件的原始版本,前提是必须满足以下条件:
|
||||
|
||||
* 保留所有声明: 完整保留所有原始版权声明、许可证声明、来源声明以及其他所有权声明。
|
||||
* 附带许可证: 在分发软件时,必须同时附带本许可证的完整文本,确保接收者知悉并理解本许可证的全部条款。
|
||||
|
||||
* 2.2 衍生作品复制与分发: 您可以复制和分发基于本软件的衍生作品,您对衍生作品的分发行为将受到本许可证第 1.2.2 条(开源继承与互惠共享)的约束。
|
||||
|
||||
3. 修改权限
|
||||
3.1 您可以在非商业用途下修改本软件,前提是继承本许可证并保留原版权声明。
|
||||
|
||||
3.2 禁止在修改后进行商业用途。
|
||||
* 3.1 自由修改: 您被授予自由修改本软件的权限,无论修改目的是非营利性使用还是商业用途。
|
||||
|
||||
4. 专利引用
|
||||
4.1 若项目被专利相关引用,必须保留来源声明。
|
||||
* 3.2 修改后使用与分发约束: 当您将修改后的版本用于商业用途或分发修改后的版本时,您需要遵守本许可证第 1.2.2 条(开源继承与互惠共享)以及第 2 条(复制与分发)的规定。 即使您不分发修改后的版本,只要您将其用于商业目的,也需要遵守开源继承条款或获得授权。
|
||||
|
||||
4.2 若为商业场景,需按照商用处理。
|
||||
* 3.3 贡献接受: WJQserver Studio 鼓励社区贡献代码。如果您向本项目贡献代码,您需要同意您的贡献代码按照本许可证条款进行许可。
|
||||
|
||||
4. 专利权
|
||||
|
||||
* 4.1 无专利担保,风险自担: 本软件以“现状”提供,授权方及贡献者明确声明,不对本软件的专利侵权问题做任何形式的担保,亦不承担任何因专利侵权可能产生的责任与后果。 用户理解并同意,使用本软件的专利风险完全由用户自行承担。
|
||||
|
||||
* 4.2 专利纠纷应对: 如因用户使用本软件而引发任何专利侵权指控、诉讼或索赔,用户应自行负责处理并承担全部法律责任。 授权方及贡献者无义务参与任何相关法律程序,亦不承担任何由此产生的费用或赔偿。
|
||||
|
||||
5. 免责声明
|
||||
5.1 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
|
||||
|
||||
5.2 在任何情况下,授权方均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害负责,即使已被告知可能发生此类损害。
|
||||
* 5.1 “现状”提供,无任何保证: 本软件按“现状”提供,不提供任何明示或暗示的保证,包括但不限于适销性、特定用途适用性及非侵权性。
|
||||
|
||||
5.3 用户需根据当地法律对待本项目,确保遵守所有适用法规。
|
||||
* 5.2 责任限制: 在适用法律允许的最大范围内,在任何情况下,授权方或任何贡献者均不对因使用或无法使用本软件而产生的任何直接、间接、偶然、特殊、惩罚性或后果性损害(包括但不限于采购替代商品或服务;损失使用、数据或利润;或业务中断)负责,无论其是如何造成的,也无论依据何种责任理论,即使已被告知可能发生此类损害。
|
||||
|
||||
6. 许可证期限
|
||||
6.1 本许可证自2024年开始生效,有效期暂为无限。
|
||||
* 5.3 用户法律责任: 用户需根据当地法律对待本项目,确保遵守所有适用法规。
|
||||
|
||||
6.2 项目所有方有权修改许可证相关条例而不另行通知。
|
||||
6. 许可证期限与终止
|
||||
|
||||
条款修订
|
||||
7.1 授权方保留随时修改本许可证条款的权利,以便更好地适应法律和技术的发展。
|
||||
* 6.1 许可证期限: 除版权所有人主动宣布放弃本软件版权外,本许可证无限期生效。
|
||||
|
||||
7.2 修订后的条款将在发布时生效,继续使用本软件即表示接受修订后的条款。
|
||||
* 6.2 许可证终止: 如果您未能遵守本许可证的任何条款或条件,授权方有权终止本许可证。 您的许可证将在您违反本许可证条款时自动终止。
|
||||
|
||||
其他
|
||||
8.1 本许可证不影响您作为最终用户的法定权利。
|
||||
* 6.3 终止后的效力: 许可证终止后,您根据本许可证所享有的所有权利将立即终止,但您在许可证终止前已合法分发的软件副本,其接收者所获得的许可及权利将不受影响,继续有效。 免责声明(第 5 条)和责任限制(第 5.2 条)在本许可证终止后仍然有效。
|
||||
|
||||
8.2 若本许可证的某些条款被认定为不可执行,其余条款仍然有效。
|
||||
7. 条款修订
|
||||
|
||||
* 7.1 修订权利保留: 授权方保留随时修改本许可证条款的权利,以便更好地适应法律、技术发展以及社区需求。
|
||||
|
||||
* 7.2 修订生效与接受: 修订后的条款将在发布时生效,除非另行声明,否则继续使用、复制、分发或修改本软件即表示您接受修订后的条款。授权方鼓励用户定期查阅本许可证的最新版本。
|
||||
|
||||
8. 其他
|
||||
|
||||
* 8.1 法定权利: 本许可证不影响您作为最终用户在适用法律下的法定权利。
|
||||
|
||||
* 8.2 条款可分割性: 若本许可证的某些条款被认定为不可执行,其余条款仍然完全有效。
|
||||
|
||||
* 8.3 版本更新: 授权方可能会发布本许可证的修订版本或新版本。您可以选择是继续使用本许可证的旧版本还是选择适用新版本。
|
||||
|
||||
WJQserver Studio Open Source License
|
||||
Version 1.2
|
||||
Version v2.0
|
||||
|
||||
Copyright © WJQserver Studio 2024
|
||||
|
||||
Definitions
|
||||
License: The terms and conditions defined within this license for use, copying, distribution, and modification.
|
||||
Licensor: The individual or organization holding the copyright, or the entity designated by them.
|
||||
You: The individual or legal entity exercising the permissions granted by this license.
|
||||
Open Source vs. Free Software
|
||||
This project is open source, allowing users to access and use the source code under the terms of this license.
|
||||
This project is not equivalent to free software; usage rights are restricted by this license.
|
||||
Copyright is emphasized, with all rights reserved by WJQserver Studio.
|
||||
|
||||
* License: Refers to the terms and requirements for use, reproduction, distribution, and modification defined within this license.
|
||||
* Licensor: Refers to the individual or organization that holds the copyright, or the entity designated by the copyright holder, specifically WJQserver Studio in this license.
|
||||
* Contributor: Refers to the Licensor and individuals or entities who contribute code or software under this License.
|
||||
* You: Refers to the individual or legal entity exercising permissions granted by this License.
|
||||
* Derivative Works: Refers to works modified based on the Software or any part thereof, regardless of the extent of modification. This includes but is not limited to modifications, revisions, adaptations, translations, or other forms of creation based on the Software or any part thereof, as well as collective works containing the Software or parts thereof.
|
||||
* Non-profit Use: Refers to uses not primarily intended for direct commercial profit, including but not limited to:
|
||||
* Personal Use: Use by an individual for personal learning, research, experimentation, non-commercial projects, personal website development, graduation projects, home entertainment, and other non-directly commercial purposes.
|
||||
* Educational Use: Use within educational institutions (such as schools, universities, training organizations) for activities such as teaching, research, and academic exchange.
|
||||
* Scientific Research Use: Use within scientific research institutions, laboratories, and similar organizations for activities such as scientific research and experimental development.
|
||||
* Charitable and Public Welfare Use: Use by charitable organizations, public welfare organizations, and similar non-profit entities for their public missions or internal operation of charitable activities, or to provide public services that do not directly generate commercial profit.
|
||||
* Internal Operational Use (Non-profit Organizations): Use within the internal operations of non-profit organizations, such as for administrative management, membership management, internal communication, project management, and other non-directly profit-generating activities.
|
||||
|
||||
Open Source and Free Software
|
||||
|
||||
This project is open-source software, allowing users to access and use the source code under the premise of complying with this License.
|
||||
This project aims to provide users with the broadest possible freedom for non-commercial use while ensuring the common development and healthy ecosystem of the community, and providing a clear path for commercial innovation.
|
||||
Copyright is emphasized; all rights are jointly reserved by WJQserver Studio and Contributors.
|
||||
|
||||
License Terms
|
||||
1. Usage Rights
|
||||
1.1 You are granted the right to use this software freely in a private environment.
|
||||
|
||||
1.2 You may use it commercially without modifying key statements.
|
||||
1. Permissions for Use
|
||||
|
||||
2. Copying and Distribution
|
||||
2.1 You may copy and distribute the original version of this software, provided all copyright notices and this license are retained.
|
||||
* 1.1 Non-profit Use: You are granted permission to freely use the Software for any purpose in non-profit use scenarios. Specific non-profit use scenarios include but are not limited to the various situations listed in the Definition section.
|
||||
|
||||
3. Modification Rights
|
||||
3.1 You may modify this software for non-commercial purposes, provided you inherit this license and retain the original copyright notice.
|
||||
* 1.2 Commercial Use: You may use the Software in a commercial environment without additional authorization, but your commercial use must comply with the following terms:
|
||||
|
||||
3.2 Modifications cannot be used commercially.
|
||||
* 1.2.1 Maintain Statements: When conducting commercial use, you must not remove or modify the original copyright notices, license notices, and source statements contained in the Software.
|
||||
* 1.2.2 Open Source Inheritance (Copyleft) and Reciprocal Sharing: If you or your organization wish to use the Software or its Derivative Works for any commercial purpose, including but not limited to:
|
||||
|
||||
4. Patent References
|
||||
4.1 If the project is cited in patent-related contexts, the source statement must be retained.
|
||||
* Profit-generating Distribution: Selling, renting, licensing, or distributing the Software or its Derivative Works.
|
||||
* Profit-generating Services: Providing commercial services based on the Software or its Derivative Works, such as SaaS services, consulting services, custom development services, and paid technical support services.
|
||||
* Embedded Commercial Applications: Embedding the Software or its Derivative Works into commercial products or solutions for sale.
|
||||
* Internal Commercial Operations: Using modified versions within the internal operations of for-profit organizations to directly support their commercial activities, such as customized internal systems, generating commercial revenue directly or indirectly through means including but not limited to placing advertisements in the software or related services (e.g., Google Ads), in-app purchases, membership subscriptions, and charging for value-added features.
|
||||
|
||||
4.2 For commercial scenarios, it must be treated as a commercial use.
|
||||
You must choose one of the following two options:
|
||||
|
||||
5. Disclaimer
|
||||
5.1 This software is provided "as is", without any express or implied warranties, including but not limited to merchantability, fitness for a particular purpose, and non-infringement.
|
||||
* i) Inherit this License and Open Source: You must distribute your Derivative Works under this License or a compatible open-source license and publicly disclose the entire source code of your Derivative Works, so that recipients of your Derivative Works also enjoy the same rights as you, including the right to further modify and use commercially. This option aims to promote the common development and knowledge sharing of the community, ensuring that commercial innovation achievements based on this Software can also contribute back to the community.
|
||||
* ii) Obtain Explicit Authorization from the Licensor: If you do not wish to release your Derivative Works in an open-source manner, or wish to distribute them under another license, or you wish to use a modified version in commercial operations without open-sourcing it, you must obtain explicit written authorization from WJQserver Studio in advance. The specific terms and conditions of authorization will be determined separately by WJQserver Studio through negotiation.
|
||||
|
||||
5.2 In no event shall the licensor be liable for any direct, indirect, incidental, special, punitive, or consequential damages arising out of the use or inability to use this software, even if advised of the possibility of such damages.
|
||||
2. Reproduction and Distribution
|
||||
|
||||
5.3 Users must comply with all applicable laws regarding this project.
|
||||
* 2.1 Reproduction and Distribution of Original Version: You may reproduce and distribute the original version of the Software, provided that the following conditions are met:
|
||||
|
||||
6. License Duration
|
||||
6.1 This license is effective from 2024, with an indefinite duration.
|
||||
* Retain All Statements: Completely retain all original copyright notices, license notices, source statements, and other proprietary notices.
|
||||
* Accompany with License: When distributing the Software, you must also include the full text of this License to ensure that recipients are aware of and understand all terms of this License.
|
||||
|
||||
6.2 The project owner reserves the right to modify the license terms without prior notice.
|
||||
* 2.2 Reproduction and Distribution of Derivative Works: You may reproduce and distribute Derivative Works based on the Software. Your distribution of Derivative Works will be subject to the constraints of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing).
|
||||
|
||||
Amendments
|
||||
7.1 The licensor reserves the right to amend this license at any time to better adapt to legal and technological developments.
|
||||
3. Modification Permissions
|
||||
|
||||
7.2 Revised terms become effective upon publication, and continued use of the software indicates acceptance of the revised terms.
|
||||
* 3.1 Free Modification: You are granted permission to freely modify the Software, regardless of whether the purpose of modification is for non-profit use or commercial use.
|
||||
|
||||
Miscellaneous
|
||||
8.1 This license does not affect your statutory rights as an end user.
|
||||
* 3.2 Constraints on Use and Distribution after Modification: When you use a modified version for commercial purposes or distribute a modified version, you need to comply with the provisions of Clause 1.2.2 of this License (Open Source Inheritance and Reciprocal Sharing) and Clause 2 (Reproduction and Distribution). Even if you do not distribute the modified version, as long as you use it for commercial purposes, you also need to comply with the open-source inheritance clause or obtain authorization.
|
||||
|
||||
8.2 If any provision of this license is held to be unenforceable, the remaining provisions shall remain in effect.
|
||||
* 3.3 Contribution Acceptance: WJQserver Studio encourages community contribution of code. If you contribute code to this project, you need to agree that your contributed code is licensed under the terms of this License.
|
||||
|
||||
4. Patent Rights
|
||||
|
||||
* 4.1 No Patent Warranty, Risk Self-Bearing: The software is provided “AS IS”, and the Licensor and Contributors explicitly declare that they do not provide any form of warranty regarding patent infringement issues of this software, nor do they assume any responsibility and consequences arising from patent infringement. Users understand and agree that the patent risk of using this software is entirely borne by the users themselves.
|
||||
|
||||
* 4.2 Handling of Patent Disputes: If any patent infringement allegations, lawsuits, or claims arise due to the user's use of this Software, the user shall be solely responsible for handling and bear all legal liabilities. The Licensor and Contributors are under no obligation to participate in any related legal proceedings, nor do they bear any costs or compensation arising therefrom.
|
||||
|
||||
5. Disclaimer of Warranty
|
||||
|
||||
* 5.1 “AS IS” Provision, No Warranty: The software is provided “AS IS” without any express or implied warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement.
|
||||
|
||||
* 5.2 Limitation of Liability: To the maximum extent permitted by applicable law, in no event shall the Licensor or any Contributor be liable for any direct, indirect, incidental, special, punitive, or consequential damages (including but not limited to procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
|
||||
|
||||
* 5.3 User Legal Responsibility: Users shall treat this project in accordance with local laws and regulations to ensure compliance with all applicable laws and regulations.
|
||||
|
||||
6. License Term and Termination
|
||||
|
||||
* 6.1 License Term: Unless the copyright holder proactively announces the abandonment of the copyright of this software, this License shall be effective indefinitely from the date of your acceptance.
|
||||
|
||||
* 6.2 License Termination: If you fail to comply with any terms or conditions of this License, the Licensor has the right to terminate this License. Your License will automatically terminate upon your violation of the terms of this License.
|
||||
|
||||
* 6.3 Effect after Termination: Upon termination of the License, all rights granted to you under this License will terminate immediately, but the licenses and rights obtained by recipients of software copies you have legally distributed before the termination of the License will not be affected and will remain valid. The Disclaimer of Warranty (Clause 5) and Limitation of Liability (Clause 5.2) shall remain in effect after the termination of this License.
|
||||
|
||||
7. Revision of Terms
|
||||
|
||||
* 7.1 Reservation of Revision Rights: The Licensor reserves the right to modify the terms of this License at any time to better adapt to legal, technological developments, and community needs.
|
||||
|
||||
* 7.2 Effectiveness and Acceptance of Revisions: Revised terms will take effect upon publication, and unless otherwise stated, continued use, reproduction, distribution, or modification of the Software indicates your acceptance of the revised terms. The Licensor encourages users to periodically review the latest version of this License.
|
||||
|
||||
8. Other
|
||||
|
||||
* 8.1 Statutory Rights: This License does not affect your statutory rights as an end-user under applicable laws.
|
||||
|
||||
* 8.2 Severability of Terms: If certain terms of this License are deemed unenforceable, the remaining terms shall remain in full force and effect.
|
||||
|
||||
* 8.3 Version Updates: The Licensor may publish revised versions or new versions of this License. You may choose to continue using the old version of this License or choose to apply the new version.
|
||||
|
||||
151
README.md
151
README.md
@@ -1,120 +1,117 @@
|
||||
# GhProxy
|
||||
# GHProxy
|
||||
|
||||

|
||||
[](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
|
||||
[](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
|
||||
|
||||
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
|
||||
|
||||
[DEMO](ghproxy.1888866.xyz)
|
||||
支持 Git clone、raw、releases的 Github 加速项目, 支持自托管的同时带来卓越的性能与极低的资源占用(Golang和HertZ带来的优势), 同时支持多种额外功能
|
||||
|
||||
## 项目说明
|
||||
|
||||
### 项目特点
|
||||
|
||||
- 基于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 语言实现,跨平台的同时提供高并发性能**
|
||||
- 🌐 **使用字节旗下的 [HertZ](https://github.com/cloudwego/hertz) 作为 Web 框架**
|
||||
- 📡 **使用 [Touka-HTTPC](https://github.com/satomitouka/touka-httpc) 作为 HTTP 客户端**
|
||||
- 📥 **支持 Git clone、raw、releases 等文件拉取**
|
||||
- 🎨 **支持多个前端主题**
|
||||
- 🚫 **支持自定义黑名单/白名单**
|
||||
- 🗄️ **支持 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)
|
||||
[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/menu.md)
|
||||
|
||||
在v1.0.0版本之前,本项目继承于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的APACHE2.0 LICENSE VERSION
|
||||
|
||||
## 使用示例
|
||||
### 使用示例
|
||||
|
||||
```
|
||||
# 下载文件
|
||||
https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
|
||||
https://ghproxy.1888866.xyz/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
|
||||
```
|
||||
|
||||
## 部署说明
|
||||
|
||||
可参考文章: 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 --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,以免造成不必要的麻烦
|
||||
|
||||
```
|
||||
# 核心配置
|
||||
server:
|
||||
port: 8080 # 监听端口(小白请勿修改)
|
||||
host: "127.0.0.1" # 监听地址(小白请勿修改)
|
||||
sizelimit: 131072000 # 125MB
|
||||
|
||||
# 日志配置
|
||||
logger:
|
||||
logfilepath: "/data/ghproxy/log/ghproxy.log" # 日志文件路径(小白请勿修改)
|
||||
maxlogsize: 25 # MB
|
||||
|
||||
# CORS 配置
|
||||
cors:
|
||||
enabled: true # 是否开启CORS
|
||||
|
||||
# 鉴权配置
|
||||
auth:
|
||||
enabled: false # 是否开启鉴权
|
||||
authtoken: "test" # 鉴权Token
|
||||
|
||||
# 黑名单配置
|
||||
blacklist:
|
||||
enabled: true
|
||||
blacklistfile: "/data/ghproxy/config/blacklist.yaml"
|
||||
一键部署脚本:
|
||||
|
||||
```bash
|
||||
wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/install.sh && chmod +x install.sh &&./install.sh
|
||||
```
|
||||
|
||||
### Caddy反代配置
|
||||
Dev一键部署脚本:
|
||||
|
||||
```
|
||||
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
|
||||
}
|
||||
```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
|
||||
```
|
||||
|
||||
## TODO & DEV
|
||||
## 配置说明
|
||||
|
||||
### TODO
|
||||
参看[项目文档](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md)
|
||||
|
||||
- [x] 允许更多参数通过config结构传入
|
||||
- [x] 改进程序效率
|
||||
- [x] 用户鉴权
|
||||
- [ ] 仓库黑名单
|
||||
### 前端页面
|
||||
|
||||
### DEV
|
||||
参看[GHProxy-Frontend](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
|
||||
|
||||
- [x] Docker Pull 代理
|
||||
- [x] 仓库黑名单
|
||||
## 项目简史
|
||||
|
||||
**本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能**
|
||||
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
|
||||
|
||||
- v3.0.0 迁移到HertZ框架, 进一步提升效率
|
||||
- v2.4.1 对路径匹配进行优化
|
||||
- v2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
|
||||
- v1.0.0 迁移至本仓库,并再次重构内容实现
|
||||
- v0.2.0 重构项目实现
|
||||
|
||||
## LICENSE
|
||||
|
||||
本项目使用WJQserver Studio License 2.0 [WJQserver Studio License 2.0](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)
|
||||
|
||||
在v2.3.0之前, 本项目使用WJQserver Studio License 1.2
|
||||
|
||||
在v1.0.0版本之前,本项目继承于[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的APACHE2.0 LICENSE VERSION
|
||||
|
||||
## 赞助
|
||||
|
||||
如果您觉得本项目对您有帮助,欢迎赞助支持,您的赞助将用于Demo服务器开支及开发者时间成本支出,感谢您的支持!
|
||||
|
||||
为爱发电,开源不易
|
||||
|
||||
爱发电: https://afdian.com/a/wjqserver
|
||||
|
||||
USDT(TRC20): `TNfSYG6F2vkiibd6J6mhhHNWDgWgNdF5hN`
|
||||
|
||||
### 捐赠列表
|
||||
|
||||
| 赞助人 |金额|
|
||||
|--------|------|
|
||||
| starry | 8 USDT (TRC20) |
|
||||
|
||||
17
SECURITY.MD
17
SECURITY.MD
@@ -6,21 +6,20 @@
|
||||
|
||||
| 版本 | 是否支持 |
|
||||
| --- | --- |
|
||||
| v1.x.x | :white_check_mark: |
|
||||
| **w**a/b | :warning: 这是测试版本,用于开发测试,可能存在未知的安全隐患 |
|
||||
| v3.x.x | :white_check_mark: 当前最新版本序列 |
|
||||
| v2.x.x | :x: 这些版本已结束生命周期,不受支持 |
|
||||
| v1.x.x | :x: 这些版本已结束生命周期,不受支持 |
|
||||
| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
|
||||
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
|
||||
| v0.x.x | :x: 这些版本不再受支持 |
|
||||
|
||||
### 版本说明
|
||||
|
||||
- **v1.0.0**: 这是正式发布的版本
|
||||
- **xx W xx A/B***: 这是开发测试版本,可能存在未知的安全隐患。不推荐在生产环境中使用。
|
||||
- **v0.x.x**: 这些早期版本不再受支持,建议尽快升级到 v1.0.0 或更高版本。
|
||||
|
||||
### 用户须知
|
||||
|
||||
本项目为开源项目,开发者不对使用本项目造成的任何损失或问题承担责任。用户需自行评估并承担使用本项目的风险。
|
||||
|
||||
使用本项目,请遵循 **[WSL (WJQSERVER-STUDIO LICENSE)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。
|
||||
使用本项目,请遵循 **[WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。
|
||||
|
||||
本项目所有文件均受到 WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0) 协议保护,任何人不得在任何情况下以非 WSL 2.0 (WJQSERVER-STUDIO LICENSE 2.0) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
|
||||
128
api/api.go
Normal file
128
api/api.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"ghproxy/config"
|
||||
"ghproxy/middleware/nocache"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/cloudwego/hertz/pkg/app/server"
|
||||
)
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
func InitHandleRouter(cfg *config.Config, r *server.Hertz, version string) {
|
||||
apiRouter := r.Group("/api", nocache.NoCacheMiddleware())
|
||||
{
|
||||
apiRouter.GET("/size_limit", func(ctx context.Context, c *app.RequestContext) {
|
||||
SizeLimitHandler(cfg, c, ctx)
|
||||
})
|
||||
apiRouter.GET("/whitelist/status", func(ctx context.Context, c *app.RequestContext) {
|
||||
WhiteListStatusHandler(cfg, c, ctx)
|
||||
})
|
||||
apiRouter.GET("/blacklist/status", func(ctx context.Context, c *app.RequestContext) {
|
||||
BlackListStatusHandler(cfg, c, ctx)
|
||||
})
|
||||
apiRouter.GET("/cors/status", func(ctx context.Context, c *app.RequestContext) {
|
||||
CorsStatusHandler(cfg, c, ctx)
|
||||
})
|
||||
apiRouter.GET("/healthcheck", func(ctx context.Context, c *app.RequestContext) {
|
||||
HealthcheckHandler(c, ctx)
|
||||
})
|
||||
apiRouter.GET("/version", func(ctx context.Context, c *app.RequestContext) {
|
||||
VersionHandler(c, ctx, version)
|
||||
})
|
||||
apiRouter.GET("/rate_limit/status", func(ctx context.Context, c *app.RequestContext) {
|
||||
RateLimitStatusHandler(cfg, c, ctx)
|
||||
})
|
||||
apiRouter.GET("/rate_limit/limit", func(ctx context.Context, c *app.RequestContext) {
|
||||
RateLimitLimitHandler(cfg, c, ctx)
|
||||
})
|
||||
apiRouter.GET("/smartgit/status", func(ctx context.Context, c *app.RequestContext) {
|
||||
SmartGitStatusHandler(cfg, c, ctx)
|
||||
})
|
||||
|
||||
}
|
||||
logInfo("API router Init success")
|
||||
}
|
||||
|
||||
func SizeLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
||||
sizeLimit := cfg.Server.SizeLimit
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"MaxResponseBodySize": sizeLimit,
|
||||
}))
|
||||
}
|
||||
|
||||
func WhiteListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Whitelist": cfg.Whitelist.Enabled,
|
||||
}))
|
||||
}
|
||||
|
||||
func BlackListStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Blacklist": cfg.Blacklist.Enabled,
|
||||
}))
|
||||
}
|
||||
|
||||
func CorsStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Cors": cfg.Server.Cors,
|
||||
}))
|
||||
}
|
||||
|
||||
func HealthcheckHandler(c *app.RequestContext, ctx context.Context) {
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Status": "OK",
|
||||
}))
|
||||
}
|
||||
|
||||
func VersionHandler(c *app.RequestContext, ctx context.Context, version string) {
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"Version": version,
|
||||
}))
|
||||
}
|
||||
|
||||
func RateLimitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"RateLimit": cfg.RateLimit.Enabled,
|
||||
}))
|
||||
}
|
||||
|
||||
func RateLimitLimitHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"RatePerMinute": cfg.RateLimit.RatePerMinute,
|
||||
}))
|
||||
}
|
||||
|
||||
func SmartGitStatusHandler(cfg *config.Config, c *app.RequestContext, ctx context.Context) {
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
c.Response.Header.Set("Content-Type", "application/json")
|
||||
c.JSON(200, (map[string]interface{}{
|
||||
"enabled": cfg.GitClone.Mode == "cache",
|
||||
}))
|
||||
}
|
||||
33
auth/auth-header.go
Normal file
33
auth/auth-header.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
func AuthHeaderHandler(c *app.RequestContext, 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.GetHeader(cfg.Auth.Key))
|
||||
|
||||
} else {
|
||||
authToken = string(c.GetHeader("GH-Auth"))
|
||||
}
|
||||
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken)
|
||||
if authToken == "" {
|
||||
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
|
||||
}
|
||||
34
auth/auth-parameters.go
Normal file
34
auth/auth-parameters.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
func AuthParametersHandler(c *app.RequestContext, 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")
|
||||
}
|
||||
|
||||
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Method(), string(c.Path()), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), authToken)
|
||||
|
||||
if authToken == "" {
|
||||
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
|
||||
}
|
||||
66
auth/auth.go
66
auth/auth.go
@@ -1,35 +1,53 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"ghproxy/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
var logw = logger.Logw
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
func AuthHandler(c *gin.Context, cfg *config.Config) bool {
|
||||
// 如果身份验证未启用,直接返回 true
|
||||
if !cfg.Auth.Enabled {
|
||||
logw("auth PASSED")
|
||||
return true
|
||||
func Init(cfg *config.Config) {
|
||||
if cfg.Blacklist.Enabled {
|
||||
err := InitBlacklist(cfg)
|
||||
if err != nil {
|
||||
logError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 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 {
|
||||
logError(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
logDebug("Auth Init")
|
||||
}
|
||||
|
||||
func AuthHandler(ctx context.Context, c *app.RequestContext, 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 == "" {
|
||||
logError("Auth method not set")
|
||||
return true, nil
|
||||
} else {
|
||||
logError("Auth method not supported")
|
||||
return false, fmt.Errorf(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)
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
@@ -2,45 +2,89 @@ package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Config 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
|
||||
configfile = "/data/ghproxy/config/config.yaml"
|
||||
blacklistfile = "/data/ghproxy/config/blacklist.json"
|
||||
blacklist *Config
|
||||
instance *Blacklist
|
||||
initErr error
|
||||
)
|
||||
|
||||
func init() {
|
||||
blacklist = &Config{}
|
||||
|
||||
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 forRangeCheck(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 forRangeCheck(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{}{}
|
||||
}
|
||||
}
|
||||
logw("%s not in blacklist", fullrepo)
|
||||
|
||||
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, ""
|
||||
}
|
||||
|
||||
91
auth/whitelist.go
Normal file
91
auth/whitelist.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Whitelist 用于存储白名单信息
|
||||
type Whitelist struct {
|
||||
userSet map[string]struct{} // 用户级白名单
|
||||
repoSet map[string]map[string]struct{} // 仓库级白名单
|
||||
initOnce sync.Once // 确保初始化只执行一次
|
||||
initialized bool // 初始化状态标识
|
||||
}
|
||||
|
||||
var (
|
||||
whitelistInstance *Whitelist
|
||||
whitelistInitErr error
|
||||
)
|
||||
|
||||
// InitWhitelist 初始化白名单(线程安全,仅执行一次)
|
||||
func InitWhitelist(cfg *config.Config) error {
|
||||
whitelistInstance = &Whitelist{
|
||||
userSet: make(map[string]struct{}),
|
||||
repoSet: make(map[string]map[string]struct{}),
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(cfg.Whitelist.WhitelistFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read whitelist: %w", err)
|
||||
}
|
||||
|
||||
var list struct {
|
||||
Entries []string `json:"whitelist"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &list); err != nil {
|
||||
return fmt.Errorf("invalid whitelist format: %w", err)
|
||||
}
|
||||
|
||||
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,107 +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 600s
|
||||
import error_page
|
||||
import encode
|
||||
import rate_limit 60
|
||||
route / {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
}
|
||||
route /favicon.ico {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 60s 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,99 +0,0 @@
|
||||
{
|
||||
debug
|
||||
http_port 80
|
||||
https_port 443
|
||||
order cache before rewrite
|
||||
cache {
|
||||
cache_name GhProxyCache
|
||||
}
|
||||
log {
|
||||
level INFO
|
||||
output file /data/caddy/log/caddy.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(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}
|
||||
}
|
||||
|
||||
(buffer) {
|
||||
flush_interval 2000s
|
||||
buffer_responses
|
||||
max_buffer_size 256k
|
||||
}
|
||||
|
||||
(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 600s
|
||||
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 60s 24h
|
||||
}
|
||||
}
|
||||
|
||||
import /data/caddy/config.d/*
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"blacklist": [
|
||||
"test/test1",
|
||||
"example/repo2",
|
||||
"another/repo3"
|
||||
]
|
||||
}
|
||||
|
||||
"blacklist": [
|
||||
"eviluser",
|
||||
"spamuser/bad-repo",
|
||||
"malwareuser/*"
|
||||
]
|
||||
}
|
||||
258
config/config.go
258
config/config.go
@@ -3,50 +3,246 @@ package config
|
||||
import (
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"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"`
|
||||
Server ServerConfig
|
||||
Httpc HttpcConfig
|
||||
GitClone GitCloneConfig
|
||||
Shell ShellConfig
|
||||
Pages PagesConfig
|
||||
Log LogConfig
|
||||
Auth AuthConfig
|
||||
Blacklist BlacklistConfig
|
||||
Whitelist WhitelistConfig
|
||||
RateLimit RateLimitConfig
|
||||
Outbound OutboundConfig
|
||||
}
|
||||
|
||||
// LoadConfig 从 YAML 配置文件加载配置
|
||||
/*
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||
sizeLimit = 125 # MB
|
||||
memLimit = 0 # MB
|
||||
H2C = true
|
||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||
debug = false
|
||||
*/
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `toml:"port"`
|
||||
Host string `toml:"host"`
|
||||
NetLib string `toml:"netlib"`
|
||||
SizeLimit int `toml:"sizeLimit"`
|
||||
MemLimit int64 `toml:"memLimit"`
|
||||
H2C bool `toml:"H2C"`
|
||||
Cors string `toml:"cors"`
|
||||
Debug bool `toml:"debug"`
|
||||
}
|
||||
|
||||
/*
|
||||
[httpc]
|
||||
mode = "auto" # "auto" or "advanced"
|
||||
maxIdleConns = 100 # only for advanced mode
|
||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||
maxConnsPerHost = 0 # only for advanced mode
|
||||
*/
|
||||
type HttpcConfig struct {
|
||||
Mode string `toml:"mode"`
|
||||
MaxIdleConns int `toml:"maxIdleConns"`
|
||||
MaxIdleConnsPerHost int `toml:"maxIdleConnsPerHost"`
|
||||
MaxConnsPerHost int `toml:"maxConnsPerHost"`
|
||||
}
|
||||
|
||||
/*
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
ForceH2C = true
|
||||
*/
|
||||
type GitCloneConfig struct {
|
||||
Mode string `toml:"mode"`
|
||||
SmartGitAddr string `toml:"smartGitAddr"`
|
||||
ForceH2C bool `toml:"ForceH2C"`
|
||||
}
|
||||
|
||||
/*
|
||||
[shell]
|
||||
editor = true
|
||||
rewriteAPI = false
|
||||
*/
|
||||
type ShellConfig struct {
|
||||
Editor bool `toml:"editor"`
|
||||
RewriteAPI bool `toml:"rewriteAPI"`
|
||||
}
|
||||
|
||||
/*
|
||||
[pages]
|
||||
mode = "internal" # "internal" or "external"
|
||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||
staticDir = "/data/www"
|
||||
*/
|
||||
type PagesConfig struct {
|
||||
Mode string `toml:"mode"`
|
||||
Theme string `toml:"theme"`
|
||||
StaticDir string `toml:"staticDir"`
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
LogFilePath string `toml:"logFilePath"`
|
||||
MaxLogSize int `toml:"maxLogSize"`
|
||||
Level string `toml:"level"`
|
||||
HertZLogPath string `toml:"hertzLogPath"`
|
||||
}
|
||||
|
||||
/*
|
||||
[auth]
|
||||
Method = "parameters" # "header" or "parameters"
|
||||
Key = ""
|
||||
Token = "token"
|
||||
enabled = false
|
||||
passThrough = false
|
||||
ForceAllowApi = true
|
||||
*/
|
||||
type AuthConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Method string `toml:"method"`
|
||||
Key string `toml:"key"`
|
||||
Token string `toml:"token"`
|
||||
PassThrough bool `toml:"passThrough"`
|
||||
ForceAllowApi bool `toml:"ForceAllowApi"`
|
||||
}
|
||||
|
||||
type BlacklistConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
BlacklistFile string `toml:"blacklistFile"`
|
||||
}
|
||||
|
||||
type WhitelistConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
WhitelistFile string `toml:"whitelistFile"`
|
||||
}
|
||||
|
||||
type RateLimitConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
RateMethod string `toml:"rateMethod"`
|
||||
RatePerMinute int `toml:"ratePerMinute"`
|
||||
Burst int `toml:"burst"`
|
||||
}
|
||||
|
||||
/*
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
*/
|
||||
type OutboundConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Url string `toml:"url"`
|
||||
}
|
||||
|
||||
// LoadConfig 从 TOML 配置文件加载配置
|
||||
func LoadConfig(filePath string) (*Config, error) {
|
||||
if !FileExists(filePath) {
|
||||
// 楔入配置文件
|
||||
err := DefaultConfig().WriteConfig(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DefaultConfig(), nil
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := loadYAML(filePath, &config); err != nil {
|
||||
if _, err := toml.DecodeFile(filePath, &config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// LoadyamlConfig 从 YAML 配置文件加载配置
|
||||
func loadYAML(filePath string, out interface{}) error {
|
||||
data, err := os.ReadFile(filePath)
|
||||
// 写入配置文件
|
||||
func (c *Config) WriteConfig(filePath string) error {
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return yaml.Unmarshal(data, out)
|
||||
defer file.Close()
|
||||
|
||||
encoder := toml.NewEncoder(file)
|
||||
return encoder.Encode(c)
|
||||
}
|
||||
|
||||
// 检测文件是否存在
|
||||
func FileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
return !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
// 默认配置结构体
|
||||
func DefaultConfig() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: 8080,
|
||||
Host: "0.0.0.0",
|
||||
NetLib: "netpoll",
|
||||
SizeLimit: 125,
|
||||
MemLimit: 0,
|
||||
H2C: true,
|
||||
Cors: "*",
|
||||
Debug: false,
|
||||
},
|
||||
Httpc: HttpcConfig{
|
||||
Mode: "auto",
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 60,
|
||||
MaxConnsPerHost: 0,
|
||||
},
|
||||
GitClone: GitCloneConfig{
|
||||
Mode: "bypass",
|
||||
SmartGitAddr: "http://127.0.0.1:8080",
|
||||
ForceH2C: false,
|
||||
},
|
||||
Shell: ShellConfig{
|
||||
Editor: false,
|
||||
RewriteAPI: false,
|
||||
},
|
||||
Pages: PagesConfig{
|
||||
Mode: "internal",
|
||||
Theme: "bootstrap",
|
||||
StaticDir: "/data/www",
|
||||
},
|
||||
Log: LogConfig{
|
||||
LogFilePath: "/data/ghproxy/log/ghproxy.log",
|
||||
MaxLogSize: 10,
|
||||
Level: "info",
|
||||
HertZLogPath: "/data/ghproxy/log/hertz.log",
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
Enabled: false,
|
||||
Method: "parameters",
|
||||
Key: "",
|
||||
Token: "token",
|
||||
PassThrough: false,
|
||||
ForceAllowApi: false,
|
||||
},
|
||||
Blacklist: BlacklistConfig{
|
||||
Enabled: false,
|
||||
BlacklistFile: "/data/ghproxy/config/blacklist.txt",
|
||||
},
|
||||
Whitelist: WhitelistConfig{
|
||||
Enabled: false,
|
||||
WhitelistFile: "/data/ghproxy/config/whitelist.txt",
|
||||
},
|
||||
RateLimit: RateLimitConfig{
|
||||
Enabled: false,
|
||||
RateMethod: "total",
|
||||
RatePerMinute: 100,
|
||||
Burst: 10,
|
||||
},
|
||||
Outbound: OutboundConfig{
|
||||
Enabled: false,
|
||||
Url: "socks5://127.0.0.1:1080",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
61
config/config.toml
Normal file
61
config/config.toml
Normal file
@@ -0,0 +1,61 @@
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||
sizeLimit = 125 # MB
|
||||
memLimit = 0 # MB
|
||||
H2C = true
|
||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||
debug = false
|
||||
|
||||
[httpc]
|
||||
mode = "auto" # "auto" or "advanced"
|
||||
maxIdleConns = 100 # only for advanced mode
|
||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||
maxConnsPerHost = 0 # only for advanced mode
|
||||
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
ForceH2C = false
|
||||
|
||||
[shell]
|
||||
editor = false
|
||||
rewriteAPI = false
|
||||
|
||||
[pages]
|
||||
mode = "internal" # "internal" or "external"
|
||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||
staticDir = "/data/www"
|
||||
|
||||
[log]
|
||||
logFilePath = "/data/ghproxy/log/ghproxy.log"
|
||||
maxLogSize = 5 # MB
|
||||
level = "info" # dump, debug, info, warn, error, none
|
||||
hertzLogPath = "/data/ghproxy/log/hertz.log"
|
||||
|
||||
[auth]
|
||||
method = "parameters" # "header" or "parameters"
|
||||
token = "token"
|
||||
key = ""
|
||||
enabled = false
|
||||
passThrough = false
|
||||
ForceAllowApi = false
|
||||
|
||||
[blacklist]
|
||||
blacklistFile = "/data/ghproxy/config/blacklist.json"
|
||||
enabled = false
|
||||
|
||||
[whitelist]
|
||||
enabled = false
|
||||
whitelistFile = "/data/ghproxy/config/whitelist.json"
|
||||
|
||||
[rateLimit]
|
||||
enabled = false
|
||||
rateMethod = "total" # "ip" or "total"
|
||||
ratePerMinute = 180
|
||||
burst = 5
|
||||
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
@@ -1,24 +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: 25 # MB
|
||||
|
||||
# CORS Configuration
|
||||
cors:
|
||||
enabled: true
|
||||
|
||||
# Authentication Configuration
|
||||
auth:
|
||||
enabled: false
|
||||
authtoken: "test"
|
||||
|
||||
# Blacklist Configuration
|
||||
blacklist:
|
||||
enabled: true
|
||||
blacklistfile: "/data/ghproxy/config/blacklist.yaml"
|
||||
7
config/whitelist.json
Normal file
7
config/whitelist.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"whitelist": [
|
||||
"white/list",
|
||||
"white/test1",
|
||||
"example/*"
|
||||
]
|
||||
}
|
||||
60
deploy/config.toml
Normal file
60
deploy/config.toml
Normal file
@@ -0,0 +1,60 @@
|
||||
[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"
|
||||
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,22 +1,51 @@
|
||||
FROM wjqserver/caddy:daily
|
||||
FROM alpine:latest AS builder
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG BRANCH=dev
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# 创建文件夹
|
||||
RUN mkdir -p /data/www
|
||||
RUN mkdir -p /data/${APPLICATION}/config
|
||||
RUN mkdir -p /data/${APPLICATION}/log
|
||||
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 wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/release/Caddyfile
|
||||
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}
|
||||
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 /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/init.sh
|
||||
|
||||
# 安装依赖
|
||||
RUN apk add --no-cache curl wget tar
|
||||
|
||||
# 后端
|
||||
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/${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
|
||||
|
||||
CMD ["/usr/local/bin/init.sh"]
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG BRANCH=v3
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=builder /data/www /data/www
|
||||
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
|
||||
|
||||
# 权限
|
||||
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
|
||||
|
||||
CMD ["/data/ghproxy/ghproxy"]
|
||||
@@ -1,21 +1,53 @@
|
||||
FROM wjqserver/caddy:latest
|
||||
FROM alpine:latest AS builder
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG BRANCH=main
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# 创建文件夹
|
||||
RUN mkdir -p /data/www
|
||||
RUN mkdir -p /data/${APPLICATION}/config
|
||||
RUN mkdir -p /data/${APPLICATION}/log
|
||||
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 wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/release/Caddyfile
|
||||
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}
|
||||
RUN wget -O /data/${APPLICATION}/config.yaml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.yaml
|
||||
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/init.sh
|
||||
|
||||
# 安装依赖
|
||||
RUN apk add --no-cache curl wget tar
|
||||
|
||||
# 后端
|
||||
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/${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
|
||||
|
||||
CMD ["/usr/local/bin/init.sh"]
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
ARG USER=WJQSERVER-STUDIO
|
||||
ARG REPO=ghproxy
|
||||
ARG BRANCH=v3
|
||||
ARG APPLICATION=ghproxy
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=builder /data/www /data/www
|
||||
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
|
||||
|
||||
# 权限
|
||||
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
|
||||
|
||||
CMD ["/data/ghproxy/ghproxy"]
|
||||
|
||||
|
||||
|
||||
343
docs/config.md
Normal file
343
docs/config.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# ghproxy 用户配置文档
|
||||
|
||||
`ghproxy` 的配置主要通过修改 `config` 目录下的 `config.toml`、`blacklist.json` 和 `whitelist.json` 文件来实现。本文档将详细介绍这些配置文件的作用以及用户可以自定义的配置选项。
|
||||
|
||||
## `config.toml` - 主配置文件
|
||||
|
||||
`config.toml` 是 `ghproxy` 的主配置文件,采用 TOML 格式。您可以通过修改此文件来定制 `ghproxy` 的各项功能,例如服务器端口、连接设置、Git 克隆模式、日志级别、认证方式、黑白名单以及限速策略等。
|
||||
|
||||
以下是 `config.toml` 文件的详细配置项说明:
|
||||
|
||||
```toml name=config/config.toml
|
||||
[server]
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
netlib = "netpoll" # "netpoll" / "std" "standard" "net/http" "net"
|
||||
sizeLimit = 125 # MB
|
||||
memLimit = 0 # MB
|
||||
H2C = true
|
||||
cors = "*" # "*"/"" -> "*" ; "nil" -> "" ;
|
||||
debug = false
|
||||
|
||||
[httpc]
|
||||
mode = "auto" # "auto" or "advanced"
|
||||
maxIdleConns = 100 # only for advanced mode
|
||||
maxIdleConnsPerHost = 60 # only for advanced mode
|
||||
maxConnsPerHost = 0 # only for advanced mode
|
||||
|
||||
[gitclone]
|
||||
mode = "bypass" # bypass / cache
|
||||
smartGitAddr = "http://127.0.0.1:8080"
|
||||
ForceH2C = false
|
||||
|
||||
[shell]
|
||||
editor = false
|
||||
rewriteAPI = false
|
||||
|
||||
[pages]
|
||||
mode = "internal" # "internal" or "external"
|
||||
theme = "bootstrap" # "bootstrap" or "nebula"
|
||||
staticDir = "/data/www"
|
||||
|
||||
[log]
|
||||
logFilePath = "/data/ghproxy/log/ghproxy.log"
|
||||
maxLogSize = 5 # MB
|
||||
level = "info" # dump, debug, info, warn, error, none
|
||||
hertzLogPath = "/data/ghproxy/log/hertz.log"
|
||||
|
||||
[auth]
|
||||
method = "parameters" # "header" or "parameters"
|
||||
token = "token"
|
||||
key = ""
|
||||
enabled = false
|
||||
passThrough = false
|
||||
ForceAllowApi = false
|
||||
|
||||
[blacklist]
|
||||
blacklistFile = "/data/ghproxy/config/blacklist.json"
|
||||
enabled = false
|
||||
|
||||
[whitelist]
|
||||
enabled = false
|
||||
whitelistFile = "/data/ghproxy/config/whitelist.json"
|
||||
|
||||
[rateLimit]
|
||||
enabled = false
|
||||
rateMethod = "total" # "ip" or "total"
|
||||
ratePerMinute = 180
|
||||
burst = 5
|
||||
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
```
|
||||
|
||||
### 配置项详细说明
|
||||
|
||||
* **`[server]` - 服务器配置**
|
||||
|
||||
* `host`: 监听地址。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"0.0.0.0"` (监听所有)
|
||||
* 说明: 设置 `ghproxy` 监听的网络地址。通常设置为 `"0.0.0.0"` 以监听所有可用的网络接口。
|
||||
* `port`: 监听端口。
|
||||
* 类型: 整数 (`int`)
|
||||
* 默认值: `8080`
|
||||
* 说明: 设置 `ghproxy` 监听的端口号。
|
||||
* `netlib`: 底层网络库。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `""` (HertZ默认处置)
|
||||
* 说明: `"std"` `"standard"` `"net/http"` `"net"` 均会被设置为go标准库`net/http`, 设置为`"netpoll"`或`""`会由`HertZ`默认逻辑处理
|
||||
* `sizeLimit`: 请求体大小限制。
|
||||
* 类型: 整数 (`int`)
|
||||
* 默认值: `125` (MB)
|
||||
* 说明: 限制允许接收的请求体最大大小,单位为 MB。用于防止过大的请求导致服务压力过大。
|
||||
* `memLimit`: `runtime`内存限制
|
||||
* 类型: 整数 (`int64`)
|
||||
* 默认值: `0` (不传入)
|
||||
* 说明: 给`runtime`的指标, 让gc行为更高效
|
||||
* `H2C`: 是否启用 H2C (HTTP/2 Cleartext) 传输。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `true` (启用)
|
||||
* 说明: 启用后,允许客户端使用 HTTP/2 协议进行无加密传输,提升性能。
|
||||
* `cors`: CORS (跨域资源共享) 设置。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"*"` (允许所有来源)
|
||||
* 可选值:
|
||||
* `""` 或`"*"`: 允许所有来源跨域访问。
|
||||
* `"nil"`: 禁用 CORS。
|
||||
* 具体的域名: 例如 `"https://example.com"`,只允许来自指定域名的跨域请求。
|
||||
* 说明: 配置 CORS 策略,用于控制哪些域名可以跨域访问 `ghproxy` 服务。
|
||||
* `debug`: 是否启用调试模式。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (禁用)
|
||||
* 说明: 启用后,`ghproxy` 会输出更详细的日志信息,用于开发和调试。
|
||||
|
||||
* **`[httpc]` - HTTP 客户端配置**
|
||||
|
||||
* `mode`: HTTP 客户端模式。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"auto"` (自动模式)
|
||||
* 可选值:
|
||||
* `"auto"`: 自动模式,使用默认的 HTTP 客户端配置,适用于大多数场景。
|
||||
* `"advanced"`: 高级模式,允许自定义连接池参数,可以更精细地控制 HTTP 客户端的行为。
|
||||
* 说明: 选择 HTTP 客户端的运行模式。
|
||||
* `maxIdleConns`: 最大空闲连接数 (仅在高级模式下生效)。
|
||||
* 类型: 整数 (`int`)
|
||||
* 默认值: `100`
|
||||
* 说明: 设置 HTTP 客户端连接池中保持的最大空闲连接数。
|
||||
* `maxIdleConnsPerHost`: 每个主机最大空闲连接数 (仅在高级模式下生效)。
|
||||
* 类型: 整数 (`int`)
|
||||
* 默认值: `60`
|
||||
* 说明: 设置 HTTP 客户端连接池中,每个主机允许保持的最大空闲连接数。
|
||||
* `maxConnsPerHost`: 每个主机最大连接数 (仅在高级模式下生效)。
|
||||
* 类型: 整数 (`int`)
|
||||
* 默认值: `0` (不限制)
|
||||
* 说明: 设置 HTTP 客户端连接池中,每个主机允许建立的最大连接数。设置为 `0` 表示不限制。
|
||||
|
||||
* **`[gitclone]` - Git 克隆配置**
|
||||
|
||||
* `mode`: Git 克隆模式。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"bypass"` (绕过模式)
|
||||
* 可选值:
|
||||
* `"bypass"`: 绕过模式,直接克隆 GitHub 仓库,不使用任何缓存加速。
|
||||
* `"cache"`: 缓存模式,使用智能 Git 服务加速克隆,需要配置 `smartGitAddr`。
|
||||
* 说明: 选择 Git 克隆的模式。
|
||||
* `smartGitAddr`: 智能 Git 服务地址 (仅在缓存模式下生效)。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"http://127.0.0.1:8080"`
|
||||
* 说明: 当 `mode` 设置为 `"cache"` 时,需要配置智能 Git 服务的地址,用于加速 Git 克隆。
|
||||
* `ForceH2C`: 是否强制使用 H2C 连接到智能 Git 服务。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (不强制)
|
||||
* 说明: 如果智能 Git 服务支持 H2C,可以设置为 `true` 以强制使用 H2C 连接,提升性能。
|
||||
|
||||
* **`[shell]` - Shell 嵌套加速功能配置**
|
||||
|
||||
* `editor`: 是否启用编辑(嵌套加速)功能。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (禁用)
|
||||
* 说明: 启用后, 会修改`.sh`文件内容以实现嵌套加速
|
||||
* `rewriteAPI`: 是否重写 API 地址。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (禁用)
|
||||
* 说明: 启用后,`ghproxy` 会重写脚本内的Github API地址。
|
||||
|
||||
* **`[pages]` - Pages 服务配置**
|
||||
|
||||
* `mode`: Pages 服务模式。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"internal"` (内置 Pages 服务)
|
||||
* 可选值:
|
||||
* `"internal"`: 使用 `ghproxy` 内置的 Pages 服务。
|
||||
* `"external"`: 使用外部 Pages 位置。
|
||||
* 说明: 选择 Pages 服务的运行模式。
|
||||
* `theme`: Pages 主题。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"bootstrap"`
|
||||
* 可选值: 参看[GHProxy项目前端仓库](https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend)
|
||||
* 说明: 设置内置 Pages 服务使用的主题。
|
||||
* `staticDir`: 静态文件目录。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"/data/www"`
|
||||
* 说明: 指定外置 Pages 服务使用的静态文件目录。
|
||||
|
||||
* **`[log]` - 日志配置**
|
||||
|
||||
* `logFilePath`: 日志文件路径。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"/data/ghproxy/log/ghproxy.log"`
|
||||
* 说明: 设置 `ghproxy` 日志文件的存储路径。
|
||||
* `maxLogSize`: 最大日志文件大小。
|
||||
* 类型: 整数 (`int`)
|
||||
* 默认值: `5` (MB)
|
||||
* 说明: 设置单个日志文件的最大大小,单位为 MB。当日志文件大小超过此限制时,会进行日志轮转。
|
||||
* `level`: 日志级别。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"info"`
|
||||
* 可选值: `"dump"`, `"debug"`, `"info"`, `"warn"`, `"error"`, `"none"`
|
||||
* 说明: 设置日志输出的级别。级别越高,输出的日志信息越少。
|
||||
* `"dump"`: 输出所有日志,包括最详细的调试信息。
|
||||
* `"debug"`: 输出调试信息、信息、警告和错误日志。
|
||||
* `"info"`: 输出信息、警告和错误日志。
|
||||
* `"warn"`: 输出警告和错误日志。
|
||||
* `"error"`: 仅输出错误日志。
|
||||
* `"none"`: 禁用所有日志输出。
|
||||
* `hertzLogPath`: `HertZ`日志文件路径。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"/data/ghproxy/log/hertz.log"`
|
||||
* 说明: 设置 `HertZ` 日志文件的存储路径。
|
||||
|
||||
* **`[auth]` - 认证配置**
|
||||
|
||||
* `enabled`: 是否启用认证。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (禁用)
|
||||
* 说明: 启用后,需要提供正确的认证信息才能访问 `ghproxy` 服务。
|
||||
* `method`: 认证方法。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"parameters"` (URL 参数)
|
||||
* 可选值: `"header"` 或 `"parameters"`
|
||||
* `"header"`: 通过请求头 `GH-Auth` 或自定义请求头 (通过 `key` 配置) 传递认证 Token。
|
||||
* `"parameters"`: 通过 URL 参数 `auth_token` 或自定义 URL 参数名 (通过 `Key` 配置) 传递认证 Token。
|
||||
* 说明: 选择认证信息传递的方式。
|
||||
* `key`: 自定义认证 Key。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `""` (空字符串,使用默认的 `GH-Auth` 请求头或 `auth_token` URL 参数名)
|
||||
* 说明: 可以自定义认证时使用的请求头名称或 URL 参数名。如果为空,则使用默认的 `GH-Auth` 请求头或 `auth_token` URL 参数名。
|
||||
* `token`: 认证 Token。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"token"`
|
||||
* 说明: 设置认证时需要提供的 Token 值。
|
||||
* `passThrough`: 是否认证参数透穿到Github。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (不允许)
|
||||
* 说明: 如果设置为 `true`,相关参数会被透穿到Github。
|
||||
* `ForceAllowApi`: 是否强制允许 API 访问。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (不强制允许)
|
||||
* 说明: 如果设置为 `true`,则强制允许对 GitHub API 的访问,即使未启用认证或认证失败。
|
||||
|
||||
* **`[blacklist]` - 黑名单配置**
|
||||
|
||||
* `enabled`: 是否启用黑名单。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (禁用)
|
||||
* 说明: 启用后,`ghproxy` 将根据 `blacklist.json` 文件中的规则阻止对特定用户或仓库的访问。
|
||||
* `blacklistFile`: 黑名单文件路径。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"/data/ghproxy/config/blacklist.json"`
|
||||
* 说明: 指定黑名单配置文件的路径。
|
||||
|
||||
* **`[whitelist]` - 白名单配置**
|
||||
|
||||
* `enabled`: 是否启用白名单。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (禁用)
|
||||
* 说明: 启用后,`ghproxy` 将只允许访问 `whitelist.json` 文件中规则指定的用户或仓库。白名单的优先级高于黑名单。
|
||||
* `whitelistFile`: 白名单文件路径。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"/data/ghproxy/config/whitelist.json"`
|
||||
* 说明: 指定白名单配置文件的路径。
|
||||
|
||||
* **`[rateLimit]` - 限速配置**
|
||||
|
||||
* `enabled`: 是否启用限速。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (禁用)
|
||||
* 说明: 启用后,`ghproxy` 将根据配置的策略限制请求速率,防止服务被滥用。
|
||||
* `rateMethod`: 限速方法。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"total"` (全局限速)
|
||||
* 可选值: `"ip"` 或 `"total"`
|
||||
* `"ip"`: 基于客户端 IP 地址进行限速,每个 IP 地址都有独立的速率限制。
|
||||
* `"total"`: 全局限速,所有客户端共享同一个速率限制。
|
||||
* 说明: 选择限速的策略。
|
||||
* `ratePerMinute`: 每分钟允许的请求数。
|
||||
* 类型: 整数 (`int`)
|
||||
* 默认值: `180`
|
||||
* 说明: 设置每分钟允许通过的最大请求数。
|
||||
* `burst`: 突发请求数。
|
||||
* 类型: 整数 (`int`)
|
||||
* 默认值: `5`
|
||||
* 说明: 允许在短时间内超过 `ratePerMinute` 的突发请求数。
|
||||
|
||||
* **`[outbound]` - 出站代理配置**
|
||||
|
||||
* `enabled`: 是否启用出站代理。
|
||||
* 类型: 布尔值 (`bool`)
|
||||
* 默认值: `false` (禁用)
|
||||
* 说明: 启用后,`ghproxy` 将通过配置的代理服务器转发所有出站请求。
|
||||
* `url`: 出站代理 URL。
|
||||
* 类型: 字符串 (`string`)
|
||||
* 默认值: `"socks5://127.0.0.1:1080"`
|
||||
* 支持协议: `socks5://` 和 `http://`
|
||||
* 说明: 设置出站代理服务器的 URL。支持 SOCKS5 和 HTTP 代理协议。
|
||||
|
||||
## `blacklist.json` - 黑名单配置
|
||||
|
||||
`blacklist.json` 文件用于配置黑名单规则,阻止对特定用户或仓库的访问。
|
||||
|
||||
```json name=config/blacklist.json
|
||||
{
|
||||
"blacklist": [
|
||||
"eviluser",
|
||||
"spamuser/bad-repo",
|
||||
"malwareuser/*"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 黑名单规则说明
|
||||
|
||||
* `blacklist`: 一个 JSON 数组,包含黑名单规则,每条规则为一个字符串。
|
||||
* **用户名**: 例如 `"eviluser"`,阻止所有名为 `eviluser` 的用户的访问。
|
||||
* **仓库名**: 例如 `"spamuser/bad-repo"`,阻止访问 `spamuser` 用户下的 `bad-repo` 仓库。
|
||||
* **通配符**: 例如 `"malwareuser/*"`,使用 `*` 通配符,阻止访问 `malwareuser` 用户下的所有仓库。
|
||||
* **缩略写法**: 例如 `"example"`, 等同于 `"example/*"`, 允许访问 `example` 用户下的所有仓库。
|
||||
|
||||
## `whitelist.json` - 白名单配置
|
||||
|
||||
`whitelist.json` 文件用于配置白名单规则,只允许访问白名单中指定的用户或仓库。白名单的优先级高于黑名单,如果一个请求同时匹配黑名单和白名单,则白名单生效,请求将被允许。
|
||||
|
||||
```json name=config/whitelist.json
|
||||
{
|
||||
"whitelist": [
|
||||
"white/list",
|
||||
"white/test1",
|
||||
"example/*",
|
||||
"example"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 白名单规则说明
|
||||
|
||||
* `whitelist`: 一个 JSON 数组,包含白名单规则,每条规则为一个字符串。
|
||||
* **仓库名**: 例如 `"white/list"`,允许访问 `white` 用户下的 `list` 仓库。
|
||||
* **仓库名**: 例如 `"white/test1"`,允许访问 `white` 用户下的 `test1` 仓库。
|
||||
* **通配符**: 例如 `"example/*"`,使用 `*` 通配符,允许访问 `example` 用户下的所有仓库。
|
||||
* **缩略写法**: 例如 `"example"`, 等同于 `"example/*"`, 允许访问 `example` 用户下的所有仓库。
|
||||
|
||||
---
|
||||
24
docs/flag.md
Normal file
24
docs/flag.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Flag
|
||||
|
||||
GHProxy接受以下flag传入
|
||||
|
||||
```bash
|
||||
root@root:/data/ghproxy$ ghproxy -h
|
||||
-c string
|
||||
config file path (default "/data/ghproxy/config/config.toml")
|
||||
-cfg value
|
||||
exit
|
||||
-h show help message and exit
|
||||
-v show version and exit
|
||||
```
|
||||
|
||||
- `-c`
|
||||
类型: `string`
|
||||
默认值: `/data/ghproxy/config/config.toml`
|
||||
示例: `ghproxy -c /data/ghproxy/demo.toml`
|
||||
- `-cfg`
|
||||
已弃用, 被`-c`替代
|
||||
- `-h`
|
||||
显示帮助信息
|
||||
- `-v`
|
||||
显示版本号
|
||||
17
docs/menu.md
Normal file
17
docs/menu.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## GHProxy 文档
|
||||
|
||||
### 配置文件
|
||||
|
||||
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/config.md
|
||||
|
||||
### Flag
|
||||
|
||||
https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docs/flag.md
|
||||
|
||||
### 部署
|
||||
|
||||
参看 https://blog.wjqserver.com/post/ghproxy-deploy-with-smart-git/
|
||||
|
||||
### 前端
|
||||
|
||||
https://github.com/WJQSERVER-STUDIO/GHProxy-Frontend
|
||||
78
go.mod
78
go.mod
@@ -1,50 +1,38 @@
|
||||
module ghproxy
|
||||
|
||||
go 1.23.2
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // 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.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.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.20.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/imroc/req/v3 v3.46.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // 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.2 // 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.8.0 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/net v0.29.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
golang.org/x/tools v0.25.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0
|
||||
github.com/cloudwego/hertz v0.9.7
|
||||
github.com/hertz-contrib/http2 v0.1.8
|
||||
github.com/satomitouka/touka-httpc v0.4.0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/time v0.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 // indirect
|
||||
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.2 // indirect
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/cloudwego/gopkg v0.1.4 // indirect
|
||||
github.com/cloudwego/netpoll v0.7.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/nyaruka/phonenumbers v1.6.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
golang.org/x/arch v0.16.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
||||
216
go.sum
216
go.sum
@@ -1,122 +1,146 @@
|
||||
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/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4 h1:JLtFd00AdFg/TP+dtvIzLkdHwKUGPOAijN1sMtEYoFg=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/copyb v0.0.4/go.mod h1:FZ6XE+4TKy4MOfX1xWKe6Rwsg0ucYFCdNh1KLvyKTfc=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1 h1:gJEQspQPB527Vp2FPcdOrynQEj3YYtrg1ixVSB/JvZM=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/log v0.0.1/go.mod h1:j9Q+xnwpOfve7/uJnZ2izRQw6NNoXjvJHz7vUQAaLZE=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0 h1:Uk4N7Sh4OPth3am3xVv17JlAm7tsna97ZLQRpQj7r5c=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.5.0/go.mod h1:mtxlnDdwsHcqDDpAQLa94nxbPFwNHSAHbBbIXQAA3po=
|
||||
github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/gopkg v0.1.2 h1:8o2feYuxknDpN+O7kPwvSXfMEKfYvJYiA2K7aonoMEQ=
|
||||
github.com/bytedance/gopkg v0.1.2/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/mockey v1.2.12 h1:aeszOmGw8CPX8CRx1DZ/Glzb1yXvhjDh6jdFBNZjsU4=
|
||||
github.com/bytedance/mockey v1.2.12/go.mod h1:3ZA4MQasmqC87Tw0w7Ygdy7eHIc2xgpZ8Pona5rsYIk=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/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/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50=
|
||||
github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI=
|
||||
github.com/cloudwego/hertz v0.9.7 h1:tAVaiO+vTf+ZkQhvNhKbDJ0hmC4oJ7bzwDi1KhvhHy4=
|
||||
github.com/cloudwego/hertz v0.9.7/go.mod h1:t6d7NcoQxPmETvzPMMIVPHMn5C5QzpqIiFsaavoLJYQ=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4=
|
||||
github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.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/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-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/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/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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/hertz-contrib/http2 v0.1.8 h1:kjfCGkUxJZHgfPsnRjx1FLJBG55KvtvSQD214guBQLw=
|
||||
github.com/hertz-contrib/http2 v0.1.8/go.mod h1:m42hrl8fiTwE4p8c7JdRUZpkePEthvV89q3elL2GeD0=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
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.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/nyaruka/phonenumbers v1.6.0 h1:r9ax45fFg+YLUs2X4bNXm5RAxWl00hYjFgNlv32vtHk=
|
||||
github.com/nyaruka/phonenumbers v1.6.0/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/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/satomitouka/touka-httpc v0.4.0 h1:cnOONdyJHJImMY8L64bvYF+7Ow/5CPf2Yr3RQRRMZOU=
|
||||
github.com/satomitouka/touka-httpc v0.4.0/go.mod h1:sNXyW5XBufkwB9ZJ+PIlgN/6xiJ7aZV1fWGrXR0u3bA=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.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 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/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/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/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/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/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.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/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/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=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.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=
|
||||
|
||||
24
init.sh
24
init.sh
@@ -1,24 +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/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/ghproxy/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
|
||||
}
|
||||
503
main.go
503
main.go
@@ -1,99 +1,484 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"ghproxy/api"
|
||||
"ghproxy/auth"
|
||||
"ghproxy/config"
|
||||
"ghproxy/logger"
|
||||
"ghproxy/middleware/loggin"
|
||||
"ghproxy/proxy"
|
||||
"ghproxy/rate"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/hertz-contrib/http2/factory"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
"github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
|
||||
"github.com/cloudwego/hertz/pkg/app/server"
|
||||
"github.com/cloudwego/hertz/pkg/common/adaptor"
|
||||
"github.com/cloudwego/hertz/pkg/common/hlog"
|
||||
"github.com/cloudwego/hertz/pkg/network/standard"
|
||||
|
||||
_ "net/http/pprof"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *config.Config
|
||||
r *server.Hertz
|
||||
configfile = "/data/ghproxy/config/config.toml"
|
||||
hertZfile *os.File
|
||||
cfgfile string
|
||||
version string
|
||||
runMode string
|
||||
limiter *rate.RateLimiter
|
||||
iplimiter *rate.IPRateLimiter
|
||||
showVersion bool
|
||||
showHelp bool
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed pages/*
|
||||
pagesFS embed.FS
|
||||
)
|
||||
|
||||
var (
|
||||
cfg *config.Config
|
||||
logw = logger.Logw
|
||||
router *gin.Engine
|
||||
configfile = "/data/ghproxy/config/config.yaml"
|
||||
logDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
var (
|
||||
exps = []*regexp.Regexp{
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*`),
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*`),
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*`),
|
||||
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+`),
|
||||
regexp.MustCompile(`^(?:https?://)?gist\.github\.com/([^/]+)/.+?/.+`),
|
||||
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) // 传递日志文件路径
|
||||
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize logger: %v", err)
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logw("Logger initialized")
|
||||
logw("Init Completed")
|
||||
err = logger.SetLogLevel(cfg.Log.Level)
|
||||
if err != nil {
|
||||
fmt.Printf("Logger Level Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Log Level: %s\n", cfg.Log.Level)
|
||||
logDebug("Config File Path: ", cfgfile)
|
||||
logDebug("Loaded config: %v\n", cfg)
|
||||
logInfo("Logger Initialized Successfully")
|
||||
}
|
||||
|
||||
func setupHertZLogger(cfg *config.Config) {
|
||||
var err error
|
||||
|
||||
if cfg.Log.HertZLogPath != "" {
|
||||
hertZfile, err = os.OpenFile(cfg.Log.HertZLogPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
hlog.SetOutput(os.Stdout)
|
||||
logWarning("Failed to open hertz log file: %v", err)
|
||||
} else {
|
||||
hlog.SetOutput(hertZfile)
|
||||
}
|
||||
hlog.SetLevel(hlog.LevelInfo)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setMemLimit(cfg *config.Config) {
|
||||
if cfg.Server.MemLimit > 0 {
|
||||
debug.SetMemoryLimit((cfg.Server.MemLimit) * 1024 * 1024)
|
||||
logInfo("Set Memory Limit to %d MB", cfg.Server.MemLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func loadlist(cfg *config.Config) {
|
||||
auth.Init(cfg)
|
||||
}
|
||||
|
||||
func setupApi(cfg *config.Config, r *server.Hertz, version string) {
|
||||
api.InitHandleRouter(cfg, r, version)
|
||||
}
|
||||
|
||||
func setupRateLimit(cfg *config.Config) {
|
||||
if cfg.RateLimit.Enabled {
|
||||
if cfg.RateLimit.RateMethod == "ip" {
|
||||
iplimiter = rate.NewIPRateLimiter(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
|
||||
} else if cfg.RateLimit.RateMethod == "total" {
|
||||
limiter = rate.New(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
|
||||
} else {
|
||||
logError("Invalid RateLimit Method: %s", cfg.RateLimit.RateMethod)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InitReq(cfg *config.Config) {
|
||||
proxy.InitReq(cfg)
|
||||
}
|
||||
|
||||
// loadEmbeddedPages 加载嵌入式页面资源
|
||||
func loadEmbeddedPages(cfg *config.Config) (fs.FS, fs.FS, error) {
|
||||
var pages fs.FS
|
||||
var err error
|
||||
switch cfg.Pages.Theme {
|
||||
case "bootstrap":
|
||||
pages, err = fs.Sub(pagesFS, "pages/bootstrap")
|
||||
case "nebula":
|
||||
pages, err = fs.Sub(pagesFS, "pages/nebula")
|
||||
case "design":
|
||||
pages, err = fs.Sub(pagesFS, "pages/design")
|
||||
case "metro":
|
||||
pages, err = fs.Sub(pagesFS, "pages/metro")
|
||||
case "classic":
|
||||
pages, err = fs.Sub(pagesFS, "pages/classic")
|
||||
case "mino":
|
||||
pages, err = fs.Sub(pagesFS, "pages/mino")
|
||||
default:
|
||||
pages, err = fs.Sub(pagesFS, "pages/bootstrap") // 默认主题
|
||||
logWarning("Invalid Pages Theme: %s, using default theme 'bootstrap'", cfg.Pages.Theme)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to load embedded pages: %w", err)
|
||||
}
|
||||
|
||||
var assets fs.FS
|
||||
assets, err = fs.Sub(pagesFS, "pages/assets")
|
||||
return pages, assets, nil
|
||||
}
|
||||
|
||||
// setupPages 设置页面路由
|
||||
func setupPages(cfg *config.Config, r *server.Hertz) {
|
||||
switch cfg.Pages.Mode {
|
||||
case "internal":
|
||||
err := setInternalRoute(cfg, r)
|
||||
if err != nil {
|
||||
logError("Failed when processing internal pages: %s", err)
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
case "external":
|
||||
// 设置外部资源路径
|
||||
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
|
||||
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
|
||||
javascriptsPath := fmt.Sprintf("%s/script.js", cfg.Pages.StaticDir)
|
||||
stylesheetsPath := fmt.Sprintf("%s/style.css", cfg.Pages.StaticDir)
|
||||
bootstrapPath := fmt.Sprintf("%s/bootstrap.min.css", cfg.Pages.StaticDir)
|
||||
bootstrapBundlePath := fmt.Sprintf("%s/bootstrap.bundle.min.js", cfg.Pages.StaticDir)
|
||||
|
||||
// 设置外部资源路由
|
||||
r.StaticFile("/", indexPagePath)
|
||||
r.StaticFile("/favicon.ico", faviconPath)
|
||||
r.StaticFile("/script.js", javascriptsPath)
|
||||
r.StaticFile("/style.css", stylesheetsPath)
|
||||
r.StaticFile("/bootstrap.min.css", bootstrapPath)
|
||||
r.StaticFile("/bootstrap.bundle.min.js", bootstrapBundlePath)
|
||||
|
||||
default:
|
||||
// 处理无效的Pages Mode
|
||||
logWarning("Invalid Pages Mode: %s, using default embedded theme", cfg.Pages.Mode)
|
||||
|
||||
err := setInternalRoute(cfg, r)
|
||||
if err != nil {
|
||||
logError("Failed when processing internal pages: %s", err)
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func setInternalRoute(cfg *config.Config, r *server.Hertz) error {
|
||||
|
||||
// 加载嵌入式资源
|
||||
pages, assets, err := loadEmbeddedPages(cfg)
|
||||
if err != nil {
|
||||
logError("Failed when processing pages: %s", err)
|
||||
return err
|
||||
}
|
||||
// 设置嵌入式资源路由
|
||||
r.GET("/", func(ctx context.Context, c *app.RequestContext) {
|
||||
staticServer := http.FileServer(http.FS(pages))
|
||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||
if err != nil {
|
||||
logError("%s", err)
|
||||
return
|
||||
}
|
||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||
})
|
||||
r.GET("/favicon.ico", func(ctx context.Context, c *app.RequestContext) {
|
||||
staticServer := http.FileServer(http.FS(pages))
|
||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||
if err != nil {
|
||||
logError("%s", err)
|
||||
return
|
||||
}
|
||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||
})
|
||||
r.GET("/script.js", func(ctx context.Context, c *app.RequestContext) {
|
||||
staticServer := http.FileServer(http.FS(pages))
|
||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||
if err != nil {
|
||||
logError("%s", err)
|
||||
return
|
||||
}
|
||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||
})
|
||||
r.GET("/style.css", func(ctx context.Context, c *app.RequestContext) {
|
||||
staticServer := http.FileServer(http.FS(pages))
|
||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||
if err != nil {
|
||||
logError("%s", err)
|
||||
return
|
||||
}
|
||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||
})
|
||||
r.GET("/bootstrap.min.css", func(ctx context.Context, c *app.RequestContext) {
|
||||
staticServer := http.FileServer(http.FS(assets))
|
||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||
if err != nil {
|
||||
logError("%s", err)
|
||||
return
|
||||
}
|
||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||
})
|
||||
r.GET("/bootstrap.bundle.min.js", func(ctx context.Context, c *app.RequestContext) {
|
||||
staticServer := http.FileServer(http.FS(assets))
|
||||
req, err := adaptor.GetCompatRequest(&c.Request)
|
||||
if err != nil {
|
||||
logError("%s", err)
|
||||
return
|
||||
}
|
||||
staticServer.ServeHTTP(adaptor.GetCompatResponseWriter(&c.Response), req)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
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()
|
||||
if cfg != nil { // 在setupLogger前添加空值检查
|
||||
setupLogger(cfg)
|
||||
setupHertZLogger(cfg)
|
||||
InitReq(cfg)
|
||||
setMemLimit(cfg)
|
||||
loadlist(cfg)
|
||||
setupRateLimit(cfg)
|
||||
|
||||
// 设置 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
|
||||
}
|
||||
logDebug("Run Mode: %s Netlib: %s", runMode, cfg.Server.NetLib)
|
||||
|
||||
if cfg == nil {
|
||||
fmt.Println("Config not loaded, exiting.")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Program finished")
|
||||
}
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
if cfg.Server.NetLib == "std" || cfg.Server.NetLib == "standard" || cfg.Server.NetLib == "net" || cfg.Server.NetLib == "net/http" {
|
||||
if cfg.Server.H2C {
|
||||
r = server.New(
|
||||
server.WithH2C(true),
|
||||
server.WithHostPorts(addr),
|
||||
server.WithTransport(standard.NewTransporter),
|
||||
)
|
||||
r.AddProtocol("h2", factory.NewServerFactory())
|
||||
} else {
|
||||
r = server.New(
|
||||
server.WithHostPorts(addr),
|
||||
server.WithTransport(standard.NewTransporter),
|
||||
)
|
||||
}
|
||||
} else if cfg.Server.NetLib == "netpoll" || cfg.Server.NetLib == "" {
|
||||
if cfg.Server.H2C {
|
||||
r = server.New(
|
||||
server.WithH2C(true),
|
||||
server.WithHostPorts(addr),
|
||||
)
|
||||
r.AddProtocol("h2", factory.NewServerFactory())
|
||||
} else {
|
||||
r = server.New(
|
||||
server.WithHostPorts(addr),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logError("Invalid NetLib: %s", cfg.Server.NetLib)
|
||||
fmt.Printf("Invalid NetLib: %s\n", cfg.Server.NetLib)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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,
|
||||
// 添加Recovery中间件
|
||||
r.Use(recovery.Recovery())
|
||||
// 添加log中间件
|
||||
r.Use(loggin.Middleware())
|
||||
|
||||
setupApi(cfg, r, version)
|
||||
|
||||
setupPages(cfg, r)
|
||||
|
||||
/*
|
||||
// 1. GitHub Releases/Archive - Use distinct path segments for type
|
||||
r.GET("/github.com/:username/:repo/releases/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for releases
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
r.GET("/github.com/:username/:repo/archive/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for archive
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
// 2. GitHub Blob/Raw - Use distinct path segments for type
|
||||
r.GET("/github.com/:username/:repo/blob/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for blob
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
r.GET("/github.com/:username/:repo/raw/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for raw
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
r.GET("/github.com/:username/:repo/info/*filepath", func(ctx context.Context, c *app.RequestContext) { // Distinct path for info
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
r.GET("/github.com/:username/:repo/git-upload-pack", func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
// 4. Raw GitHubusercontent - Keep as is (assuming it's distinct enough)
|
||||
r.GET("/raw.githubusercontent.com/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
// 5. Gist GitHubusercontent - Keep as is (assuming it's distinct enough)
|
||||
r.GET("/gist.githubusercontent.com/:username/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
// 6. GitHub API Repos - Keep as is (assuming it's distinct enough)
|
||||
r.GET("/api.github.com/repos/:username/:repo/*filepath", func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
*/
|
||||
|
||||
r.NoRoute(func(ctx context.Context, c *app.RequestContext) {
|
||||
proxy.NoRouteHandler(cfg, limiter, iplimiter)(ctx, c)
|
||||
})
|
||||
|
||||
fmt.Printf("GHProxy Version: %s\n", version)
|
||||
fmt.Printf("A Go Based High-Performance Github Proxy \n")
|
||||
fmt.Printf("Made by WJQSERVER-STUDIO\n")
|
||||
|
||||
if cfg.Server.Debug {
|
||||
go func() {
|
||||
http.ListenAndServe("localhost:6060", nil)
|
||||
}()
|
||||
}
|
||||
|
||||
r.Spin()
|
||||
defer logger.Close()
|
||||
defer func() {
|
||||
if hertZfile != nil {
|
||||
var err error
|
||||
err = hertZfile.Close()
|
||||
if err != nil {
|
||||
logError("Failed to close hertz log file: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
fmt.Println("Program Exit")
|
||||
}
|
||||
|
||||
32
middleware/loggin/loggin.go
Normal file
32
middleware/loggin/loggin.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package loggin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
// 日志中间件
|
||||
func Middleware() app.HandlerFunc {
|
||||
return func(ctx context.Context, c *app.RequestContext) {
|
||||
startTime := time.Now()
|
||||
|
||||
c.Next(ctx)
|
||||
|
||||
endTime := time.Now()
|
||||
timingResults := endTime.Sub(startTime)
|
||||
|
||||
logInfo("%s %s %s %s %s %d %v ", c.ClientIP(), c.Method(), c.Request.Header.GetProtocol(), string(c.Path()), c.Request.Header.UserAgent(), c.Response.StatusCode(), timingResults)
|
||||
}
|
||||
}
|
||||
17
middleware/nocache/nocache.go
Normal file
17
middleware/nocache/nocache.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package nocache
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
func NoCacheMiddleware() app.HandlerFunc {
|
||||
return func(ctx context.Context, c *app.RequestContext) {
|
||||
// 设置禁止缓存的响应头
|
||||
c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||
c.Response.Header.Set("Pragma", "no-cache")
|
||||
c.Response.Header.Set("Expires", "0")
|
||||
c.Next(ctx) // 继续处理请求
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB |
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>
|
||||
37
proxy/authpass.go
Normal file
37
proxy/authpass.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
func AuthPassThrough(c *app.RequestContext, cfg *config.Config, req *http.Request) {
|
||||
if cfg.Auth.PassThrough {
|
||||
token := c.Query("token")
|
||||
if token != "" {
|
||||
logDebug("%s %s %s %s %s Auth-PassThrough: token %s", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol(), token)
|
||||
switch cfg.Auth.Method {
|
||||
case "parameters":
|
||||
if !cfg.Auth.Enabled {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
} else {
|
||||
logWarning("%s %s %s %s %s Auth-Error: Conflict Auth Method", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol())
|
||||
// 500 Internal Server Error
|
||||
c.JSON(http.StatusInternalServerError, map[string]string{"error": "Conflict Auth Method"})
|
||||
return
|
||||
}
|
||||
case "header":
|
||||
if cfg.Auth.Enabled {
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
}
|
||||
default:
|
||||
logWarning("%s %s %s %s %s Invalid Auth Method / Auth Method is not be set", c.ClientIP(), c.Method(), string(c.Path()), c.UserAgent(), c.Request.Header.GetProtocol())
|
||||
// 500 Internal Server Error
|
||||
c.JSON(http.StatusInternalServerError, map[string]string{"error": "Invalid Auth Method / Auth Method is not be set"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
163
proxy/chunkreq.go
Normal file
163
proxy/chunkreq.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
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": {},
|
||||
}
|
||||
)
|
||||
|
||||
func ChunkedProxyRequest(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, matcher string) {
|
||||
|
||||
var (
|
||||
method []byte
|
||||
bodyReader *bytes.Buffer
|
||||
req *http.Request
|
||||
resp *http.Response
|
||||
err error
|
||||
)
|
||||
|
||||
method = c.Request.Method()
|
||||
bodyReader = bytes.NewBuffer(c.Request.Body())
|
||||
|
||||
req, err = client.NewRequest(string(method), u, bodyReader)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
setRequestHeaders(c, req)
|
||||
//removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 错误处理(404)
|
||||
if resp.StatusCode == 404 {
|
||||
//c.String(http.StatusNotFound, "File Not Found")
|
||||
c.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
bodySize int
|
||||
contentLength string
|
||||
sizelimit int
|
||||
)
|
||||
sizelimit = cfg.Server.SizeLimit * 1024 * 1024
|
||||
contentLength = resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
var err error
|
||||
bodySize, err = strconv.Atoi(contentLength)
|
||||
if err != nil {
|
||||
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
|
||||
bodySize = -1
|
||||
}
|
||||
if err == nil && bodySize > sizelimit {
|
||||
var finalURL string
|
||||
finalURL = resp.Request.URL.String()
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
logError("Failed to close response body: %v", err)
|
||||
}
|
||||
c.Redirect(http.StatusMovedPermanently, []byte(finalURL))
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), finalURL, bodySize)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
for header := range headersToRemove {
|
||||
resp.Header.Del(header)
|
||||
}
|
||||
|
||||
for key := range resp.Header {
|
||||
var values []string = resp.Header.Values(key)
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// 复制响应头,排除需要移除的 header
|
||||
for key, values := range resp.Header {
|
||||
if _, shouldRemove := respHeadersToRemove[key]; !shouldRemove {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch cfg.Server.Cors {
|
||||
case "*":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
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 MatcherShell(u) && matchString(matcher, matchedMatchers) && cfg.Shell.Editor {
|
||||
// 判断body是不是gzip
|
||||
var compress string
|
||||
if resp.Header.Get("Content-Encoding") == "gzip" {
|
||||
compress = "gzip"
|
||||
}
|
||||
|
||||
logInfo("Is Shell: %s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol())
|
||||
c.Header("Content-Length", "")
|
||||
|
||||
var reader io.Reader
|
||||
|
||||
reader, _, err = processLinks(resp.Body, compress, string(c.Request.Host()), cfg)
|
||||
c.SetBodyStream(reader, -1)
|
||||
|
||||
if err != nil {
|
||||
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if contentLength != "" {
|
||||
c.SetBodyStream(resp.Body, bodySize)
|
||||
return
|
||||
}
|
||||
c.SetBodyStream(resp.Body, -1)
|
||||
}
|
||||
|
||||
}
|
||||
135
proxy/dial.go
Normal file
135
proxy/dial.go
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
made&PR by @lfhy
|
||||
https://github.com/WJQSERVER-STUDIO/ghproxy/pull/46
|
||||
*/
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
// initTransport 初始化 HTTP 传输层的代理设置
|
||||
func initTransport(cfg *config.Config, transport *http.Transport) {
|
||||
// 如果代理功能未启用,直接返回
|
||||
if !cfg.Outbound.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果代理 URL 未设置,使用环境变量中的代理配置
|
||||
if cfg.Outbound.Url == "" {
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
logWarning("Outbound proxy is not set, using environment variables")
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试解析代理 URL
|
||||
proxyInfo, err := url.Parse(cfg.Outbound.Url)
|
||||
if err != nil {
|
||||
// 如果解析失败,记录错误日志并使用环境变量中的代理配置
|
||||
logError("Failed to parse outbound proxy URL %v", err)
|
||||
transport.Proxy = http.ProxyFromEnvironment
|
||||
return
|
||||
}
|
||||
|
||||
// 根据代理 URL 的 scheme(协议类型)选择代理类型
|
||||
switch strings.ToLower(proxyInfo.Scheme) {
|
||||
case "http", "https": // 如果是 HTTP/HTTPS 代理
|
||||
transport.Proxy = http.ProxyURL(proxyInfo) // 设置 HTTP(S) 代理
|
||||
logInfo("Using HTTP(S) proxy: %s", proxyInfo.Redacted())
|
||||
case "socks5": // 如果是 SOCKS5 代理
|
||||
// 调用 newProxyDial 创建 SOCKS5 代理拨号器
|
||||
proxyDialer := newProxyDial(cfg.Outbound.Url)
|
||||
transport.Proxy = nil // 禁用 HTTP Proxy 设置,因为 SOCKS5 不需要 HTTP Proxy
|
||||
|
||||
// 尝试将 Dialer 转换为支持上下文的 ContextDialer
|
||||
if contextDialer, ok := proxyDialer.(proxy.ContextDialer); ok {
|
||||
transport.DialContext = contextDialer.DialContext
|
||||
} else {
|
||||
// 如果不支持 ContextDialer,则回退到传统的 Dial 方法
|
||||
transport.Dial = proxyDialer.Dial
|
||||
logWarning("SOCKS5 dialer does not support ContextDialer, using legacy Dial")
|
||||
}
|
||||
logInfo("Using SOCKS5 proxy chain: %s", cfg.Outbound.Url)
|
||||
default: // 如果代理协议不支持
|
||||
logError("Unsupported proxy scheme: %s", proxyInfo.Scheme)
|
||||
transport.Proxy = http.ProxyFromEnvironment // 回退到环境变量代理
|
||||
}
|
||||
}
|
||||
|
||||
// newProxyDial 创建一个 SOCKS5 代理拨号器
|
||||
func newProxyDial(proxyUrls string) proxy.Dialer {
|
||||
var proxyDialer proxy.Dialer = proxy.Direct // 初始为直接连接,不使用代理
|
||||
|
||||
// 支持多个代理 URL(以逗号分隔)
|
||||
for _, proxyUrl := range strings.Split(proxyUrls, ",") {
|
||||
proxyUrl = strings.TrimSpace(proxyUrl) // 去除首尾空格
|
||||
if proxyUrl == "" { // 跳过空的代理 URL
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析代理 URL
|
||||
urlInfo, err := url.Parse(proxyUrl)
|
||||
if err != nil {
|
||||
// 如果 URL 解析失败,记录错误日志并跳过
|
||||
logError("Failed to parse proxy URL %q: %v", proxyUrl, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查代理协议是否为 SOCKS5
|
||||
if urlInfo.Scheme != "socks5" {
|
||||
logWarning("Skipping non-SOCKS5 proxy: %s", urlInfo.Scheme)
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析代理认证信息(用户名和密码)
|
||||
auth := parseAuth(urlInfo)
|
||||
|
||||
// 创建 SOCKS5 代理拨号器
|
||||
dialer, err := createSocksDialer(urlInfo.Host, auth, proxyDialer)
|
||||
if err != nil {
|
||||
// 如果创建失败,记录错误日志并跳过
|
||||
logError("Failed to create SOCKS5 dialer for %q: %v", proxyUrl, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新代理拨号器,支持代理链
|
||||
proxyDialer = dialer
|
||||
}
|
||||
|
||||
return proxyDialer
|
||||
}
|
||||
|
||||
// parseAuth 解析代理 URL 中的认证信息(用户名和密码)
|
||||
func parseAuth(urlInfo *url.URL) *proxy.Auth {
|
||||
// 如果 URL 中没有用户信息,返回 nil
|
||||
if urlInfo.User == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取用户名
|
||||
username := urlInfo.User.Username()
|
||||
|
||||
// 获取密码(注意:Password() 返回两个值,需要显式处理第二个值)
|
||||
password, passwordSet := urlInfo.User.Password()
|
||||
if !passwordSet {
|
||||
password = "" // 如果密码未设置,使用空字符串
|
||||
}
|
||||
|
||||
// 返回包含用户名和密码的认证信息
|
||||
return &proxy.Auth{
|
||||
User: username,
|
||||
Password: password, // 允许空密码
|
||||
}
|
||||
}
|
||||
|
||||
// createSocksDialer 创建 SOCKS5 拨号器
|
||||
func createSocksDialer(host string, auth *proxy.Auth, previous proxy.Dialer) (proxy.Dialer, error) {
|
||||
// 调用 golang.org/x/net/proxy 提供的 SOCKS5 方法创建拨号器
|
||||
return proxy.SOCKS5("tcp", host, auth, previous)
|
||||
}
|
||||
23
proxy/error.go
Normal file
23
proxy/error.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
// 日志模块
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
func HandleError(c *app.RequestContext, message string) {
|
||||
c.JSON(http.StatusInternalServerError, map[string]string{"error": message})
|
||||
logError(message)
|
||||
}
|
||||
121
proxy/gitreq.go
Normal file
121
proxy/gitreq.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
func GitReq(ctx context.Context, c *app.RequestContext, u string, cfg *config.Config, mode string) {
|
||||
method := string(c.Request.Method())
|
||||
|
||||
logDump("Url Before FMT:%s", u)
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
userPath, repoPath, remainingPath, queryParams, err := extractParts(u)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to extract parts from URL: %v", err))
|
||||
return
|
||||
}
|
||||
// 构建新url
|
||||
u = cfg.GitClone.SmartGitAddr + userPath + repoPath + remainingPath + "?" + queryParams.Encode()
|
||||
logDump("New Url After FMT:%s", u)
|
||||
}
|
||||
|
||||
var (
|
||||
resp *http.Response
|
||||
//err error
|
||||
)
|
||||
|
||||
body := c.Request.Body()
|
||||
|
||||
bodyReader := bytes.NewBuffer(body)
|
||||
// 创建请求
|
||||
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
req, err := gitclient.NewRequest(method, u, bodyReader)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
setRequestHeaders(c, req)
|
||||
//removeWSHeader(req)
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err = gitclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
req, err := client.NewRequest(method, u, bodyReader)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
setRequestHeaders(c, req)
|
||||
//removeWSHeader(req)
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
contentLength := resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
||||
if err != nil {
|
||||
logWarning("%s %s %s %s %s Content-Length header is not a valid integer: %v", c.ClientIP(), c.Method(), c.Path(), c.UserAgent(), c.Request.Header.GetProtocol(), err)
|
||||
}
|
||||
if err == nil && size > sizelimit {
|
||||
finalURL := []byte(resp.Request.URL.String())
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Method(), c.Path(), c.Request.Header.Get("User-Agent"), c.Request.Header.GetProtocol(), finalURL, size)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
headersToRemove := map[string]struct{}{
|
||||
"Content-Security-Policy": {},
|
||||
"Referrer-Policy": {},
|
||||
"Strict-Transport-Security": {},
|
||||
}
|
||||
|
||||
for header := range headersToRemove {
|
||||
resp.Header.Del(header)
|
||||
}
|
||||
|
||||
switch cfg.Server.Cors {
|
||||
case "*":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
case "":
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
case "nil":
|
||||
c.Header("Access-Control-Allow-Origin", "")
|
||||
default:
|
||||
c.Header("Access-Control-Allow-Origin", cfg.Server.Cors)
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
c.Response.Header.Set("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||
c.Response.Header.Set("Pragma", "no-cache")
|
||||
c.Response.Header.Set("Expires", "0")
|
||||
}
|
||||
|
||||
c.SetBodyStream(resp.Body, -1)
|
||||
}
|
||||
149
proxy/handler.go
Normal file
149
proxy/handler.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"ghproxy/auth"
|
||||
"ghproxy/config"
|
||||
"ghproxy/rate"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
var re = regexp.MustCompile(`^(http:|https:)?/?/?(.*)`) // 匹配http://或https://开头的路径
|
||||
|
||||
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter) app.HandlerFunc {
|
||||
return func(ctx context.Context, c *app.RequestContext) {
|
||||
|
||||
// 限制访问频率
|
||||
if cfg.RateLimit.Enabled {
|
||||
|
||||
var allowed bool
|
||||
|
||||
switch cfg.RateLimit.RateMethod {
|
||||
case "ip":
|
||||
allowed = iplimiter.Allow(c.ClientIP())
|
||||
case "total":
|
||||
allowed = limiter.Allow()
|
||||
default:
|
||||
logWarning("Invalid RateLimit Method")
|
||||
return
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
c.JSON(http.StatusTooManyRequests, map[string]string{"error": "Too Many Requests"})
|
||||
logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Method(), c.Request.RequestURI(), c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
rawPath string
|
||||
matches []string
|
||||
errMsg string
|
||||
)
|
||||
|
||||
rawPath = strings.TrimPrefix(string(c.Request.RequestURI()), "/") // 去掉前缀/
|
||||
matches = re.FindStringSubmatch(rawPath) // 匹配路径
|
||||
logInfo("URL: %v", matches)
|
||||
|
||||
// 匹配路径错误处理
|
||||
if len(matches) < 3 {
|
||||
errMsg = fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol())
|
||||
logWarning(errMsg)
|
||||
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
|
||||
return
|
||||
}
|
||||
|
||||
// 制作url
|
||||
rawPath = "https://" + matches[2]
|
||||
|
||||
var (
|
||||
user string
|
||||
repo string
|
||||
matcher string
|
||||
err error
|
||||
)
|
||||
|
||||
user, repo, matcher, err = Matcher(rawPath, cfg)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrInvalidURL) {
|
||||
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
|
||||
logWarning(err.Error())
|
||||
return
|
||||
}
|
||||
if errors.Is(err, ErrAuthHeaderUnavailable) {
|
||||
c.String(http.StatusForbidden, "AuthHeader Unavailable")
|
||||
logWarning(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), user, repo)
|
||||
// dump log 记录详细信息 c.ClientIP(), c.Method(), rawPath,c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), full Header
|
||||
logDump("%s %s %s %s %s %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), c.Request.Header.Header())
|
||||
var repouser string
|
||||
repouser = fmt.Sprintf("%s/%s", user, repo)
|
||||
|
||||
// 白名单检查
|
||||
if cfg.Whitelist.Enabled {
|
||||
var whitelist bool
|
||||
whitelist = auth.CheckWhitelist(user, repo)
|
||||
if !whitelist {
|
||||
errMsg = fmt.Sprintf("Whitelist Blocked repo: %s", repouser)
|
||||
c.JSON(http.StatusForbidden, map[string]string{"error": errMsg})
|
||||
logWarning("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 黑名单检查
|
||||
if cfg.Blacklist.Enabled {
|
||||
var blacklist bool
|
||||
blacklist = auth.CheckBlacklist(user, repo)
|
||||
if blacklist {
|
||||
errMsg = fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
|
||||
c.JSON(http.StatusForbidden, map[string]string{"error": errMsg})
|
||||
logWarning("%s %s %s %s %s Blacklist Blocked repo: %s", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), repouser)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 若匹配api.github.com/repos/用户名/仓库名/路径, 则检查是否开启HeaderAuth
|
||||
|
||||
// 处理blob/raw路径
|
||||
if matcher == "blob" {
|
||||
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
||||
}
|
||||
|
||||
// 鉴权
|
||||
if cfg.Auth.Enabled {
|
||||
var authcheck bool
|
||||
authcheck, err = auth.AuthHandler(ctx, c, cfg)
|
||||
if !authcheck {
|
||||
//c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
|
||||
c.AbortWithStatusJSON(401, map[string]string{"error": "Unauthorized"})
|
||||
logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// IP METHOD URL USERAGENT PROTO MATCHES
|
||||
logDebug("%s %s %s %s %s Matched: %v", c.ClientIP(), c.Method(), rawPath, c.Request.Header.UserAgent(), c.Request.Header.GetProtocol(), matcher)
|
||||
|
||||
switch matcher {
|
||||
case "releases", "blob", "raw", "gist", "api":
|
||||
ChunkedProxyRequest(ctx, c, rawPath, cfg, matcher)
|
||||
case "clone":
|
||||
GitReq(ctx, c, rawPath, cfg, "git")
|
||||
default:
|
||||
c.String(http.StatusForbidden, "Invalid input.")
|
||||
fmt.Println("Invalid input.")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
144
proxy/httpc.go
Normal file
144
proxy/httpc.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
httpc "github.com/satomitouka/touka-httpc"
|
||||
)
|
||||
|
||||
var BufferSize int = 32 * 1024 // 32KB
|
||||
|
||||
var (
|
||||
tr *http.Transport
|
||||
gittr *http.Transport
|
||||
client *httpc.Client
|
||||
gitclient *httpc.Client
|
||||
)
|
||||
|
||||
func InitReq(cfg *config.Config) {
|
||||
initHTTPClient(cfg)
|
||||
if cfg.GitClone.Mode == "cache" {
|
||||
initGitHTTPClient(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func initHTTPClient(cfg *config.Config) {
|
||||
var proTolcols = new(http.Protocols)
|
||||
proTolcols.SetHTTP1(true)
|
||||
proTolcols.SetHTTP2(true)
|
||||
proTolcols.SetUnencryptedHTTP2(true)
|
||||
if cfg.Httpc.Mode == "auto" {
|
||||
|
||||
tr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
} else if cfg.Httpc.Mode == "advanced" {
|
||||
tr = &http.Transport{
|
||||
MaxIdleConns: cfg.Httpc.MaxIdleConns,
|
||||
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
|
||||
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
Protocols: proTolcols,
|
||||
}
|
||||
} else {
|
||||
// 错误的模式
|
||||
logError("unknown httpc mode: %s", cfg.Httpc.Mode)
|
||||
fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
|
||||
logWarning("use Auto to Run HTTP Client")
|
||||
fmt.Println("use Auto to Run HTTP Client")
|
||||
tr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
}
|
||||
if cfg.Outbound.Enabled {
|
||||
initTransport(cfg, tr)
|
||||
}
|
||||
if cfg.Server.Debug {
|
||||
client = httpc.New(
|
||||
httpc.WithTransport(tr),
|
||||
httpc.WithDumpLog(),
|
||||
)
|
||||
} else {
|
||||
client = httpc.New(
|
||||
httpc.WithTransport(tr),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func initGitHTTPClient(cfg *config.Config) {
|
||||
|
||||
if cfg.Httpc.Mode == "auto" {
|
||||
gittr = &http.Transport{
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
} else if cfg.Httpc.Mode == "advanced" {
|
||||
gittr = &http.Transport{
|
||||
MaxIdleConns: cfg.Httpc.MaxIdleConns,
|
||||
MaxConnsPerHost: cfg.Httpc.MaxConnsPerHost,
|
||||
MaxIdleConnsPerHost: cfg.Httpc.MaxIdleConnsPerHost,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
} else {
|
||||
// 错误的模式
|
||||
logError("unknown httpc mode: %s", cfg.Httpc.Mode)
|
||||
fmt.Println("unknown httpc mode: ", cfg.Httpc.Mode)
|
||||
logWarning("use Auto to Run HTTP Client")
|
||||
fmt.Println("use Auto to Run HTTP Client")
|
||||
gittr = &http.Transport{
|
||||
//MaxIdleConns: 160,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
WriteBufferSize: 32 * 1024, // 32KB
|
||||
ReadBufferSize: 32 * 1024, // 32KB
|
||||
}
|
||||
}
|
||||
if cfg.Outbound.Enabled {
|
||||
initTransport(cfg, gittr)
|
||||
}
|
||||
if cfg.Server.Debug && cfg.GitClone.ForceH2C {
|
||||
gitclient = httpc.New(
|
||||
httpc.WithTransport(gittr),
|
||||
httpc.WithDumpLog(),
|
||||
httpc.WithProtocols(httpc.ProtocolsConfig{
|
||||
ForceH2C: true,
|
||||
}),
|
||||
)
|
||||
} else if !cfg.Server.Debug && cfg.GitClone.ForceH2C {
|
||||
gitclient = httpc.New(
|
||||
httpc.WithTransport(gittr),
|
||||
httpc.WithProtocols(httpc.ProtocolsConfig{
|
||||
ForceH2C: true,
|
||||
}),
|
||||
)
|
||||
} else if cfg.Server.Debug && !cfg.GitClone.ForceH2C {
|
||||
gitclient = httpc.New(
|
||||
httpc.WithTransport(gittr),
|
||||
httpc.WithDumpLog(),
|
||||
httpc.WithProtocols(httpc.ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2: true,
|
||||
Http2_Cleartext: true,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
gitclient = httpc.New(
|
||||
httpc.WithTransport(gittr),
|
||||
httpc.WithProtocols(httpc.ProtocolsConfig{
|
||||
Http1: true,
|
||||
Http2: true,
|
||||
Http2_Cleartext: true,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
361
proxy/match.go
Normal file
361
proxy/match.go
Normal file
@@ -0,0 +1,361 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 定义错误类型, error承载描述, 便于处理
|
||||
type MatcherErrors struct {
|
||||
Code int
|
||||
Msg string
|
||||
Err error
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidURL = &MatcherErrors{
|
||||
Code: 403,
|
||||
Msg: "Invalid URL Format",
|
||||
}
|
||||
ErrAuthHeaderUnavailable = &MatcherErrors{
|
||||
Code: 403,
|
||||
Msg: "AuthHeader Unavailable",
|
||||
}
|
||||
)
|
||||
|
||||
func (e *MatcherErrors) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("Code: %d, Msg: %s, Err: %s", e.Code, e.Msg, e.Err.Error())
|
||||
}
|
||||
return fmt.Sprintf("Code: %d, Msg: %s", e.Code, e.Msg)
|
||||
}
|
||||
|
||||
func (e *MatcherErrors) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
func Matcher(rawPath string, cfg *config.Config) (string, string, string, error) {
|
||||
var (
|
||||
user string
|
||||
repo string
|
||||
matcher string
|
||||
)
|
||||
// 匹配 "https://github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://github.com") {
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
|
||||
if strings.HasPrefix(remainingPath, "/") {
|
||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
}
|
||||
// 预期格式/user/repo/more...
|
||||
// 取出user和repo和最后部分
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
if len(parts) <= 2 {
|
||||
return "", "", "", ErrInvalidURL
|
||||
}
|
||||
user = parts[0]
|
||||
repo = parts[1]
|
||||
// 匹配 "https://github.com"开头的链接
|
||||
if len(parts) >= 3 {
|
||||
switch parts[2] {
|
||||
case "releases", "archive":
|
||||
matcher = "releases"
|
||||
case "blob", "raw":
|
||||
matcher = "blob"
|
||||
case "info", "git-upload-pack":
|
||||
matcher = "clone"
|
||||
default:
|
||||
return "", "", "", ErrInvalidURL
|
||||
}
|
||||
}
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
// 匹配 "https://raw"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://raw") {
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://")
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
if len(parts) <= 3 {
|
||||
return "", "", "", ErrInvalidURL
|
||||
}
|
||||
user = parts[1]
|
||||
repo = parts[2]
|
||||
matcher = "raw"
|
||||
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
// 匹配 "https://gist"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://gist") {
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://")
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
if len(parts) <= 3 {
|
||||
return "", "", "", ErrInvalidURL
|
||||
}
|
||||
user = parts[1]
|
||||
repo = ""
|
||||
matcher = "gist"
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
// 匹配 "https://api.github.com/"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://api.github.com/") {
|
||||
matcher = "api"
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://api.github.com/")
|
||||
|
||||
parts := strings.Split(remainingPath, "/")
|
||||
if parts[0] == "repos" {
|
||||
user = parts[1]
|
||||
repo = parts[2]
|
||||
}
|
||||
if parts[0] == "users" {
|
||||
user = parts[1]
|
||||
}
|
||||
if !cfg.Auth.ForceAllowApi {
|
||||
if cfg.Auth.Method != "header" || !cfg.Auth.Enabled {
|
||||
return "", "", "", ErrAuthHeaderUnavailable
|
||||
}
|
||||
}
|
||||
return user, repo, matcher, nil
|
||||
}
|
||||
return "", "", "", ErrInvalidURL
|
||||
}
|
||||
|
||||
func EditorMatcher(rawPath string, cfg *config.Config) (bool, string, error) {
|
||||
var (
|
||||
matcher string
|
||||
)
|
||||
// 匹配 "https://github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://github.com") {
|
||||
remainingPath := strings.TrimPrefix(rawPath, "https://github.com")
|
||||
if strings.HasPrefix(remainingPath, "/") {
|
||||
remainingPath = strings.TrimPrefix(remainingPath, "/")
|
||||
}
|
||||
return true, "", nil
|
||||
}
|
||||
// 匹配 "https://raw.githubusercontent.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://raw.githubusercontent.com") {
|
||||
return true, matcher, nil
|
||||
}
|
||||
// 匹配 "https://raw.github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://raw.github.com") {
|
||||
return true, matcher, nil
|
||||
}
|
||||
// 匹配 "https://gist.githubusercontent.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://gist.githubusercontent.com") {
|
||||
return true, matcher, nil
|
||||
}
|
||||
// 匹配 "https://gist.github.com"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://gist.github.com") {
|
||||
return true, matcher, nil
|
||||
}
|
||||
if cfg.Shell.RewriteAPI {
|
||||
// 匹配 "https://api.github.com/"开头的链接
|
||||
if strings.HasPrefix(rawPath, "https://api.github.com") {
|
||||
matcher = "api"
|
||||
return true, matcher, nil
|
||||
}
|
||||
}
|
||||
return false, "", ErrInvalidURL
|
||||
}
|
||||
|
||||
// 匹配文件扩展名是sh的rawPath
|
||||
func MatcherShell(rawPath string) bool {
|
||||
/*
|
||||
if strings.HasSuffix(rawPath, ".sh") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
*/
|
||||
return strings.HasSuffix(rawPath, ".sh")
|
||||
}
|
||||
|
||||
// LinkProcessor 是一个函数类型,用于处理提取到的链接。
|
||||
type LinkProcessor func(string) string
|
||||
|
||||
// 自定义 URL 修改函数
|
||||
func modifyURL(url string, host string, cfg *config.Config) string {
|
||||
// 去除url内的https://或http://
|
||||
matched, _, err := EditorMatcher(url, cfg)
|
||||
if err != nil {
|
||||
logDump("Invalid URL: %s", url)
|
||||
return url
|
||||
}
|
||||
if matched {
|
||||
var u = url
|
||||
u = strings.TrimPrefix(u, "https://")
|
||||
u = strings.TrimPrefix(u, "http://")
|
||||
logDump("Modified URL: %s", "https://"+host+"/"+u)
|
||||
return "https://" + host + "/" + u
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
var (
|
||||
matchedMatchers = []string{
|
||||
"blob",
|
||||
"raw",
|
||||
"gist",
|
||||
}
|
||||
)
|
||||
|
||||
// matchString 检查目标字符串是否在给定的字符串集合中
|
||||
func matchString(target string, stringsToMatch []string) bool {
|
||||
matchMap := make(map[string]struct{}, len(stringsToMatch))
|
||||
for _, str := range stringsToMatch {
|
||||
matchMap[str] = struct{}{}
|
||||
}
|
||||
_, exists := matchMap[target]
|
||||
return exists
|
||||
}
|
||||
|
||||
// extractParts 从给定的 URL 中提取所需的部分
|
||||
func extractParts(rawURL string) (string, string, string, url.Values, error) {
|
||||
// 解析 URL
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", "", "", nil, err
|
||||
}
|
||||
|
||||
// 获取路径部分并分割
|
||||
pathParts := strings.Split(parsedURL.Path, "/")
|
||||
|
||||
// 提取所需的部分
|
||||
if len(pathParts) < 3 {
|
||||
return "", "", "", nil, fmt.Errorf("URL path is too short")
|
||||
}
|
||||
|
||||
// 提取 /WJQSERVER-STUDIO 和 /go-utils.git
|
||||
repoOwner := "/" + pathParts[1]
|
||||
repoName := "/" + pathParts[2]
|
||||
|
||||
// 剩余部分
|
||||
remainingPath := strings.Join(pathParts[3:], "/")
|
||||
if remainingPath != "" {
|
||||
remainingPath = "/" + remainingPath
|
||||
}
|
||||
|
||||
// 查询参数
|
||||
queryParams := parsedURL.Query()
|
||||
|
||||
return repoOwner, repoName, remainingPath, queryParams, nil
|
||||
}
|
||||
|
||||
var urlPattern = regexp.MustCompile(`https?://[^\s'"]+`)
|
||||
|
||||
// processLinks 处理链接,返回包含处理后数据的 io.Reader
|
||||
func processLinks(input io.ReadCloser, compress string, host string, cfg *config.Config) (readerOut io.Reader, written int64, err error) {
|
||||
pipeReader, pipeWriter := io.Pipe() // 创建 io.Pipe
|
||||
readerOut = pipeReader
|
||||
|
||||
go func() { // 在 Goroutine 中执行写入操作
|
||||
defer func() {
|
||||
if pipeWriter != nil { // 确保 pipeWriter 关闭,即使发生错误
|
||||
if err != nil {
|
||||
if closeErr := pipeWriter.CloseWithError(err); closeErr != nil { // 如果有错误,传递错误给 reader
|
||||
logError("pipeWriter close with error failed: %v, original error: %v", closeErr, err)
|
||||
}
|
||||
} else {
|
||||
if closeErr := pipeWriter.Close(); closeErr != nil { // 没有错误,正常关闭
|
||||
logError("pipeWriter close failed: %v", closeErr)
|
||||
if err == nil { // 如果之前没有错误,记录关闭错误
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if err := input.Close(); err != nil {
|
||||
logError("input close failed: %v", err)
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
var bufReader *bufio.Reader
|
||||
|
||||
if compress == "gzip" {
|
||||
// 解压gzip
|
||||
gzipReader, gzipErr := gzip.NewReader(input)
|
||||
if gzipErr != nil {
|
||||
err = fmt.Errorf("gzip解压错误: %v", gzipErr)
|
||||
return // Goroutine 中使用 return 返回错误
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
bufReader = bufio.NewReader(gzipReader)
|
||||
} else {
|
||||
bufReader = bufio.NewReader(input)
|
||||
}
|
||||
|
||||
var bufWriter *bufio.Writer
|
||||
var gzipWriter *gzip.Writer
|
||||
|
||||
// 根据是否gzip确定 writer 的创建
|
||||
if compress == "gzip" {
|
||||
gzipWriter = gzip.NewWriter(pipeWriter) // 使用 pipeWriter
|
||||
bufWriter = bufio.NewWriterSize(gzipWriter, 4096) //设置缓冲区大小
|
||||
} else {
|
||||
bufWriter = bufio.NewWriterSize(pipeWriter, 4096) // 使用 pipeWriter
|
||||
}
|
||||
|
||||
//确保writer关闭
|
||||
defer func() {
|
||||
var closeErr error // 局部变量,用于保存defer中可能发生的错误
|
||||
|
||||
if gzipWriter != nil {
|
||||
if closeErr = gzipWriter.Close(); closeErr != nil {
|
||||
logError("gzipWriter close failed %v", closeErr)
|
||||
// 如果已经存在错误,则保留。否则,记录此错误。
|
||||
if err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
if flushErr := bufWriter.Flush(); flushErr != nil {
|
||||
logError("writer flush failed %v", flushErr)
|
||||
// 如果已经存在错误,则保留。否则,记录此错误。
|
||||
if err == nil {
|
||||
err = flushErr
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 使用正则表达式匹配 http 和 https 链接
|
||||
for {
|
||||
line, readErr := bufReader.ReadString('\n')
|
||||
if readErr != nil {
|
||||
if readErr == io.EOF {
|
||||
break // 文件结束
|
||||
}
|
||||
err = fmt.Errorf("读取行错误: %v", readErr) // 传递错误
|
||||
return // Goroutine 中使用 return 返回错误
|
||||
}
|
||||
|
||||
// 替换所有匹配的 URL
|
||||
modifiedLine := urlPattern.ReplaceAllStringFunc(line, func(originalURL string) string {
|
||||
logDump("originalURL: %s", originalURL)
|
||||
return modifyURL(originalURL, host, cfg) // 假设 modifyURL 函数已定义
|
||||
})
|
||||
|
||||
n, writeErr := bufWriter.WriteString(modifiedLine)
|
||||
written += int64(n) // 更新写入的字节数
|
||||
if writeErr != nil {
|
||||
err = fmt.Errorf("写入文件错误: %v", writeErr) // 传递错误
|
||||
return // Goroutine 中使用 return 返回错误
|
||||
}
|
||||
}
|
||||
|
||||
// 在返回之前,再刷新一次 (虽然 defer 中已经有 flush,但这里再加一次确保及时刷新)
|
||||
if flushErr := bufWriter.Flush(); flushErr != nil {
|
||||
if err == nil { // 避免覆盖之前的错误
|
||||
err = flushErr
|
||||
}
|
||||
return // Goroutine 中使用 return 返回错误
|
||||
}
|
||||
}()
|
||||
|
||||
return readerOut, written, nil // 返回 reader 和 written,error 由 Goroutine 通过 pipeWriter.CloseWithError 传递
|
||||
}
|
||||
217
proxy/proxy.go
217
proxy/proxy.go
@@ -1,217 +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)
|
||||
logw("Matches: %v", matches[2])
|
||||
|
||||
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)
|
||||
|
||||
// 黑名单检查
|
||||
blacklistpass := auth.CheckBlacklist(fullrepo)
|
||||
if blacklistpass {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
logw("Blacklisted repo: %s", fullrepo)
|
||||
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("Request: %s %s", c.Request.Method, rawPath)
|
||||
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 Method: %s", u, method)
|
||||
|
||||
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", "*")
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
37
proxy/reqheader.go
Normal file
37
proxy/reqheader.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cloudwego/hertz/pkg/app"
|
||||
)
|
||||
|
||||
/*
|
||||
// 设置请求头
|
||||
func setRequestHeaders(c *app.RequestContext, req *http.Request) {
|
||||
c.Request.Header.VisitAll(func(key, value []byte) {
|
||||
req.Header.Set(string(key), string(value))
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
func setRequestHeaders(c *app.RequestContext, req *http.Request) {
|
||||
c.Request.Header.VisitAll(func(key, value []byte) {
|
||||
headerKey := string(key)
|
||||
headerValue := string(value)
|
||||
if _, shouldRemove := reqHeadersToRemove[headerKey]; !shouldRemove {
|
||||
req.Header.Set(headerKey, headerValue)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
// removeWSHeader removes the "Upgrade" and "Connection" headers from the given
|
||||
// Request, which are added by the client when it wants to upgrade the
|
||||
// connection to a WebSocket connection.
|
||||
func removeWSHeader(req *http.Request) {
|
||||
req.Header.Del("Upgrade")
|
||||
req.Header.Del("Connection")
|
||||
}
|
||||
*/
|
||||
65
rate/rate.go
Normal file
65
rate/rate.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package rate
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// 日志输出
|
||||
var (
|
||||
logw = logger.Logw
|
||||
logDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
// 总体限流器
|
||||
type RateLimiter struct {
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
// 基于IP的限流器
|
||||
type IPRateLimiter struct {
|
||||
limiters map[string]*RateLimiter
|
||||
limit int
|
||||
burst int
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
func New(limit int, burst int, duration time.Duration) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
limiter: rate.NewLimiter(rate.Limit(float64(limit)/duration.Seconds()), burst),
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *RateLimiter) Allow() bool {
|
||||
return rl.limiter.Allow()
|
||||
}
|
||||
|
||||
func NewIPRateLimiter(limit int, burst int, duration time.Duration) *IPRateLimiter {
|
||||
return &IPRateLimiter{
|
||||
limiters: make(map[string]*RateLimiter),
|
||||
limit: limit,
|
||||
burst: burst,
|
||||
duration: duration,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *IPRateLimiter) Allow(ip string) bool {
|
||||
if ip == "" {
|
||||
logWarning("empty ip")
|
||||
return false
|
||||
}
|
||||
|
||||
limiter, ok := rl.limiters[ip]
|
||||
if !ok {
|
||||
// 创建新的 RateLimiter 并存储
|
||||
limiter = New(rl.limit, rl.burst, rl.duration)
|
||||
rl.limiters[ip] = limiter
|
||||
}
|
||||
return limiter.Allow()
|
||||
}
|
||||
Reference in New Issue
Block a user