Compare commits

...

55 Commits
1.5.1 ... 1.7.0

Author SHA1 Message Date
WJQSERVER
11ba7cb0e5 1.7.0 2024-11-04 18:38:47 +08:00
WJQSERVER
f1ff3e9d27 1.7.0 2024-11-04 18:38:12 +08:00
WJQSERVER
b1e3a9c1bd 24w21d 2024-11-04 05:59:39 +08:00
WJQSERVER
267dfafcb9 24w21d 2024-11-04 05:53:53 +08:00
WJQSERVER
e32adadaff 24w21c-hotfix 2024-11-03 07:39:08 +08:00
WJQSERVER
942dda86e7 24w21c-hotfix 2024-11-03 07:37:49 +08:00
WJQSERVER
bbfc7c0d6d 24w21c 2024-11-03 07:33:34 +08:00
WJQSERVER
5af898b3e6 24w21c 2024-11-03 07:32:48 +08:00
WJQSERVER
3c162bf9d7 24w21b 2024-11-03 07:13:54 +08:00
WJQSERVER
1eed79463d 24w21a 2024-11-01 04:12:28 +08:00
WJQSERVER
6f67f6f5b4 24w21a 2024-11-01 03:58:47 +08:00
WJQSERVER
2a5570a447 1.6.2 2024-11-01 00:53:25 +08:00
WJQSERVER
eb45ab8798 1.6.2 2024-11-01 00:46:14 +08:00
WJQSERVER
2b7e949658 24w20b 2024-10-29 03:49:12 +08:00
WJQSERVER
fe652821bd 24w20b 2024-10-29 03:08:03 +08:00
WJQSERVER
92b3e19954 24w20a 2024-10-28 18:02:58 +08:00
WJQSERVER
19f753a515 update 2024-10-27 18:08:12 +08:00
WJQSERVER
157cbf9d3a deploy shell update 2024-10-25 18:12:25 +08:00
WJQSERVER
f80782b803 deploy shell update 2024-10-25 18:00:31 +08:00
WJQSERVER
b1c8658a1c hotfix 2024-10-25 17:01:06 +08:00
WJQSERVER
57cd7e1ce7 1.6.1 2024-10-24 22:30:41 +08:00
WJQSERVER
29046382a5 24w19d 2024-10-24 22:21:12 +08:00
WJQSERVER
2355447f97 24w19d 2024-10-24 22:10:36 +08:00
WJQSERVER
8187464007 24w19d 2024-10-24 21:50:53 +08:00
WJQSERVER
575e36ef90 24w19d 2024-10-24 21:50:38 +08:00
WJQSERVER
c31e887ad3 Merge branch 'main' of https://github.com/WJQSERVER-STUDIO/ghproxy 2024-10-24 14:44:40 +08:00
WJQSERVER
b633bec69b 24w19c 2024-10-24 14:44:38 +08:00
WJQSERVER
052827bca7 Update issue templates 2024-10-24 02:15:10 +08:00
WJQSERVER
0c1eb34e49 24w19b 2024-10-24 01:59:45 +08:00
WJQSERVER
3489e6d744 24w19b 2024-10-24 01:46:03 +08:00
WJQSERVER
a8af0fb687 24w19a 2024-10-24 01:23:01 +08:00
WJQSERVER
c0af779642 24w19a 2024-10-24 01:17:57 +08:00
WJQSERVER
d7949f625a 1.6.0 2024-10-20 20:14:39 +08:00
WJQSERVER
ddd12729a4 24w18f 2024-10-20 18:56:37 +08:00
WJQSERVER
91b7cfe533 24w18e 2024-10-20 18:16:30 +08:00
WJQSERVER
aee81ba4fd 24w18e 2024-10-20 17:48:31 +08:00
WJQSERVER
e5d941414e 24w18e 2024-10-20 17:39:27 +08:00
WJQSERVER
c50f23c399 24w18d 2024-10-19 16:05:18 +08:00
WJQSERVER
47d062a1c4 hotfix 2024-10-17 18:01:15 +08:00
WJQSERVER
57ba06e01e 24w18c 2024-10-17 17:45:29 +08:00
WJQSERVER
52fdaf5f81 24w18b 2024-10-17 10:43:36 +08:00
WJQSERVER
d6b8f2b812 hotfix 2024-10-17 09:57:14 +08:00
WJQSERVER
00d2113904 hotfix 2024-10-17 00:04:29 +08:00
WJQSERVER
670bca31ca 24w18a 2024-10-16 23:52:07 +08:00
WJQSERVER
f77de0d37a 24w18a 2024-10-16 23:32:25 +08:00
WJQSERVER
1c18ccc363 24w18a 2024-10-16 21:51:24 +08:00
WJQSERVER
a386304d42 update 2024-10-16 17:45:51 +08:00
WJQSERVER
a1fdd0f477 1.5.2 2024-10-16 15:19:10 +08:00
WJQSERVER
e94a98668e 24w17b 2024-10-16 13:08:58 +08:00
WJQSERVER
22273f166c 24w17b 2024-10-16 12:54:21 +08:00
WJQSERVER
2187514da7 24w17b 2024-10-16 12:48:02 +08:00
WJQSERVER
ac859c39a6 24w17a 2024-10-16 06:31:36 +08:00
WJQSERVER
518bbf2600 24w17a 2024-10-16 06:27:28 +08:00
WJQSERVER
d383c2d2a6 24w17a 2024-10-16 06:25:59 +08:00
WJQSERVER
6d055e19e3 1.5.1 2024-10-14 16:16:56 +08:00
34 changed files with 1337 additions and 285 deletions

40
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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]
### 附加信息
请提供任何其他可能有助于我们解决问题的信息。

View File

@@ -0,0 +1,20 @@
---
name: Features request
about: 提出新功能建议
title: "[Features]"
labels: enhancement
assignees: ''
---
### 功能描述
请简要描述您希望增加的功能。
### 功能原因
请说明您为什么需要这个功能。
### 功能实现
请详细描述您期望的功能实现。

View File

@@ -21,31 +21,36 @@ jobs:
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: 安装 Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install UPX
- name: 安装UPX
run: |
sudo apt update
sudo apt install upx -y
- name: Build
- name: 编译
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
CGO_ENABLED=0 go build -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
CGO_ENABLED=0 go build -ldflags "-X main.version=${{ env.VERSION }}" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
upx -9 ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
- name: Package
- name: 打包
run: |
tar -czvf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
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: Upload to GitHub Artifacts
uses: actions/upload-artifact@v3
with:
@@ -57,7 +62,7 @@ jobs:
uses: ncipollo/release-action@v1
with:
name: ${{ env.VERSION }}
artifacts: ./${{ env.OUTPUT_BINARY }}*
artifacts: ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true

View File

@@ -21,31 +21,35 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Load VERSION
- 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: 安装 Go
uses: actions/setup-go@v3
with:
go-version: ${{ env.GO_VERSION }}
- name: Install UPX
- name: 安装 UPX
run: |
sudo apt-get update
sudo apt-get install -y upx
- name: Build
- name: 编译
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
CGO_ENABLED=0 go build -ldflags="-s -w" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${{ env.VERSION }}" -o ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./main.go
upx -9 ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
- name: Package
- name: 打包
run: |
tar -czvf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
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: Upload to GitHub Artifacts
uses: actions/upload-artifact@v3
with:
@@ -57,12 +61,13 @@ jobs:
uses: ncipollo/release-action@v1
with:
name: ${{ env.VERSION }}
artifacts: ./${{ env.OUTPUT_BINARY }}*
artifacts: ./${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ env.VERSION }}
allowUpdates: true
env:
export PATH: $PATH:/usr/local/go/bin
docker:
runs-on: ubuntu-latest
needs: build # 确保这个作业在 build 作业完成后运行
@@ -102,3 +107,45 @@ jobs:
tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }}
${{ env.IMAGE_NAME }}:latest
docker-nocache:
runs-on: ubuntu-latest
needs: build # 确保这个作业在 build 作业完成后运行
env:
IMAGE_NAME: wjqserver/ghproxy # 定义镜像名称变量
DOCKERFILE: docker/dockerfile/nocache/Dockerfile # 定义 Dockerfile 路径变量
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Load VERSION
run: |
if [ -f VERSION ]; then
echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
else
echo "VERSION file not found!" && exit 1
fi
- name: Wait for Compile
run: sleep 300s
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 构建镜像
uses: docker/build-push-action@v6
with:
file: ./${{ env.DOCKERFILE }}
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.IMAGE_NAME }}:${{ env.VERSION }}-nocache
${{ env.IMAGE_NAME }}:nocache

View File

@@ -1,5 +1,175 @@
# 更新日志
v1.7.0
---
- ADD: 加入`rate`模块,实现内置速率限制
- CHANGE: 优化`blacklist``whitelist`模块的匹配算法,提升性能;由原先的完整匹配改为切片匹配,提升匹配效率
- ADD: 加入`version`相关表示与API接口
- ADD: 加入`rate`相关API接口
- CHANGE: 优化前端界面,优化部分样式
- CHANGE: 更新相关依赖库
- CHANGE: 对编译打包进行改进,此后不再提供独立可执行文件,请改为拉取`tar.gz`压缩包
24w21d
---
- PRE-RELEASE: 此版本是v1.7.0的预发布版本,请勿在生产环境中使用
- ADD: 新增`ratePerMinute` API可供查询
- ADD: 前端新增 version 标识
- ADD: 前端新增 `重定向` 按钮,用于重定向到代理后的链接
- CHANGE: 优化输出代码块,使样式更加美观
- CHANGE: 更新相关依赖库
- CHANGE: 对黑名单模块进行实验性功能优化,提升性能(改进匹配算法,在切片后优先匹配user,减少无效匹配)
24w21c
---
- PRE-RELEASE: 此版本是v1.7.0的预发布版本,请勿在生产环境中使用
- CHANGE: 对编译打包进行改进,此后不再提供独立可执行文件,请改为拉取`tar.gz`压缩包
- CHANGE: 由于上述原因,对Docker打包进行相应改进
24w21b
---
- PRE-RELEASE: 此版本是v1.7.0的预发布版本,请勿在生产环境中使用
- ADD: 加入版本号标识与对应API接口
- ADD: 加入速率限制API接口
- CHANGE: 修改打包部分
24w21a
---
- PRE-RELEASE: 此版本是v1.7.0的预发布版本,请勿在生产环境中使用
- ADD: 尝试加入程序内置速率限制
- CHANGE: 更新相关依赖库
- CHANGE: 更换Dev版本底包,于release版本保持一致
v1.6.2
---
- CHANGE: 优化前端界面,优化部分样式
- ADD: 前端加入黑夜模式
- CHANGE: 优化移动端适配
- CHANGE: 优化一键部署脚本,使其更加易用,并增加更多的功能(已于早些时候hotfix)
- CHANGE: 优化部分代码结构,提升性能
- CHANGE: 优化日志记录,对各个部分的日志记录进行统一格式,并对部分重复日志进行合并
24w20b
---
- PRE-RELEASE: 此版本是v1.6.2的预发布版本,请勿在生产环境中使用
- CHANGE: 优化前端界面,优化部分样式
- ADD: 前端加入黑夜模式
- CHANGE: 优化移动端适配
24w20a
---
- PRE-RELEASE: 此版本是v1.6.2的预发布版本,请勿在生产环境中使用
- CHANGE: 大幅修改日志记录,对各个部分的日志记录进行统一格式,并对部分重复日志进行合并
- CHANGE: 大幅优化一键部署脚本,使其更加易用,并增加更多的功能(已于早些时候hotfix)
- CHANGE: 优化部分代码结构,提升性能
v1.6.1
---
- CHANGE: 根据社区建议,将`sizeLimit`由过去的以`byte`为单位,改为以`MB`为单位,以便于直观理解
- ADD: 新增`nocache`版本,供由用户自行优化缓存策略
- CHANGE: 优化`Proxy`核心模块内部结构,提升性能
- REMOVE: 移除`Proxy`模块内部分无用`logInfo`
- FIX & ADD: 修复前端对gist的匹配问题,添加对`gist.githubusercontent.com`的前端转换支持
- CHANGE: 改变部分前端匹配逻辑
- CHANGE: 更新相关依赖库
24w19d
---
- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用
- ADD: 新增nocache版本,供由用户自行优化缓存策略
- CHANGE: 优化`Proxy`核心模块内部结构,提升性能
- REMOVE: 移除`Proxy`模块内部分无用`logInfo`
24w19c
---
- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用
- FIX & ADD: 修复前端对gist的匹配问题,添加对`gist.githubusercontent.com`的前端转换支持
- CHANGE: 改变部分前端匹配逻辑
- CHANGE: 更新相关依赖库
24w19b
---
- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用
- FIX: 修复`sizeLimit`单位更改导致API返回值错误的问题
- FIX: 修正Gist匹配
24w19a
---
- PRE-RELEASE: 此版本是v1.6.1的预发布版本,请勿在生产环境中使用
- CHANGE: 根据社区建议,将`sizeLimit`由过去的以`byte`为单位,改为以`MB`为单位,以便于直观理解
- CHANGE: 更新相关依赖
- CHANGE: 对`Proxy`模块的核心函数进行模块化,为后续修改和扩展提供空间
v1.6.0
---
- CHANGE: 优化代码结构,提升性能
- CHANGE: 引入H2C支持,支持无加密HTTP/2请求,一定程度上提升传输性能
- ADD: 在核心程序内加入静态页面支持,支持不通过caddy等web server提供前端页面
- CHANGE: 优化日志记录,带来更多的可观测性
- CHANGE: 改进前端界面,优化用户体验; 对原有Alert提示进行优化改为ShowToast提示
- CHANGE: 规范化部分函数命名,提升可读性; 同时对config.toml内的参数命名进行规范化(部分参数名称已过时,请注意更新)
- CHANGE: 修改日志检查周期,降低检查频率,避免不必要的资源浪费
- ADD: 增加CORS状态API
24w18f
---
- PRE-RELEASE: 此版本是v1.6.0的预发布版本,请勿在生产环境中使用
- CHANGE: 修正前端页面的部分样式问题
- FIX: 修正部分问题
24w18e
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 引入H2C协议支持,支持无加密HTTP/2请求
- ADD: 尝试在核心程序内加入静态页面支持
- CHANGE: 优化日志记录
- CHANGE: 去除部分无用/重复配置
- CHANGE: 规范化部分函数命名
24w18d
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 更新相关依赖库
- ADD: 增加CORS状态API
- CHANGE: 优化部分函数执行顺序
- CHANGE: 优化前端界面
24w18c
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 修正配置命名,改为驼峰式命名
- CHANGE: 修正函数命名
24w18b
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 经团队考量,移除 Docker 代理功能,若造成了不便敬请谅解
- CHANGE: 修改日志检查周期
24w18a
---
- PRE-RELEASE: 此版本是预发布版本,请勿在生产环境中使用
- CHANGE: 改进Docker 代理
- CHANGE: 改进前端页面的copy提示,弃用alert提示
v1.5.2
---
- FIX: 修正flag传入问题
- CHANGE: 去除/路径重定向,改为返回403,并记录对应请求日志
- CHANGE: 优化Proxy模块的日志记录,记录请求详细信息
24w17b
---
- PRE-RELEASE: 此版本是v1.5.2的预发布版本,请勿在生产环境中使用
- FIX: 修正flag传入问题
- CHANGE: 去除/路径重定向,改为返回403,并记录对应请求日志
- CHANGE: 优化Proxy模块的日志记录,记录请求详细信息
24w17a
---
- PRE-RELEASE: 此版本是v1.5.2的预发布版本,请勿在生产环境中使用
- FIX: 初步修正flag传入问题,但仍有可能存在其他问题
v1.5.1
---
- CHANGE: 优化代码结构,提升性能

View File

@@ -1 +1 @@
24w16a
24w21d

View File

@@ -1,4 +1,4 @@
# GhProxy
# GHProxy
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)
[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
@@ -7,6 +7,10 @@
[DEMO](https://ghproxy.1888866.xyz)
[TG讨论群组](https://t.me/ghproxy_go)
[版本更新介绍](https://blog.wjqserver.com/categories/my-program/)
## 项目说明
### 项目特点
@@ -24,11 +28,10 @@
### 项目开发过程
**本项目是[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)
- V1.0.0 迁移至本仓库,并再次重构内容实现
- v0.2.0 重构项目实现,Git clone的实现完全自主化
- v0.2.0 重构项目实现
### LICENSE
@@ -54,11 +57,11 @@ git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
docker run -p 7210:80 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log -v ./ghproxy/config:/data/ghproxy/config --restart always wjqserver/ghproxy
```
- Docker-Compose
- Docker-Compose (建议使用)
参看[docker-compose.yml](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/docker/compose/docker-compose.yml)
### 二进制文件部署
### 二进制文件部署(不推荐)
一键部署脚本:
@@ -75,28 +78,32 @@ wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/ma
```toml
[server]
host = "127.0.0.1" # 监听地址(小白请勿修改)
port = 8080 #监听端口(小白请勿修改)
sizelimit = 131072000 # 125MB
host = "127.0.0.1" # 监听地址
port = 8080 # 监听端口
sizeLimit = 125 # 125MB
[pages]
enabled = false # 是否开启内置静态页面(Docker版本请关闭此项)
staticPath = "/data/www" # 静态页面文件路径
[log]
logfilepath = "/data/ghproxy/log/ghproxy.log" # 日志文件路径(小白请勿修改)
maxlogsize = 5 # MB
logFilePath = "/data/ghproxy/log/ghproxy.log" # 日志文件路径
maxLogSize = 5 # MB 日志文件最大大小
[cors]
enabled = true # 是否开启CORS
enabled = true # 是否开启跨域
[auth]
authtoken = "test" # 鉴权Token
enabled = false # 是否开启鉴权
authToken = "token" # 用户鉴权Token
enabled = false # 是否开启用户鉴权
[blacklist]
blacklistfile = "/data/ghproxy/config/blacklist.json" # 黑名单文件路径
blacklistFile = "/data/ghproxy/config/blacklist.json" # 黑名单文件路径
enabled = false # 是否开启黑名单
[whitelist]
enabled = false # 是否开启白名单
whitelistfile = "/data/ghproxy/config/whitelist.json" # 白名单文件路径
whitelistFile = "/data/ghproxy/config/whitelist.json" # 白名单文件路径
```
@@ -147,13 +154,9 @@ example.com {
![ghproxy-demo-v1.5.0.png](https://webp.wjqserver.com/ghproxy/ghproxy-demo-v1.5.0.png)
## TODO & DEV
结语
---
### TODO
- [x] 用户鉴权
- [x] 仓库黑名单
- [x] 仓库白名单
### DEV
- [x] Docker Pull 代理
本项目基于Go语言实现的GHProxy,使用Gin框架与req库
Docker镜像基于[WJQSERVER-STUDIO/caddy](https://github.com/WJQSERVER-STUDIO/caddy)
本项目使用WSL LICENSE Version1.2 (WJQSERVER STUDIO LICENSE Version1.2) 授权协议,请遵守相关条例。

View File

@@ -7,7 +7,7 @@
| 版本 | 是否支持 |
| --- | --- |
| v1.x.x | :white_check_mark: |
| **w**a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
| v0.x.x | :x: 这些版本不再受支持 |
### 用户须知
@@ -16,6 +16,8 @@
使用本项目,请遵循 **[WSL (WJQSERVER-STUDIO LICENSE)](https://wjqserver-studio.github.io/LICENSE/LICENSE.html)** 协议。
本项目所有文件均受到 WSL (WJQSERVER-STUDIO LICENSE) 协议保护,任何人不得在任何情况下以非 WSL (WJQSERVER-STUDIO LICENSE) 协议内规定的方式使用,复制,修改,编译,发布,分发,再许可,或者出售本项目的任何部分。
## 报告漏洞
如果您发现本项目存在安全漏洞,请通过发送ISSUES或尝试联系项目维护者来报告。请在您的报告中包含以下信息:

View File

@@ -1 +1 @@
1.5.1
1.7.0

View File

@@ -13,16 +13,14 @@ var (
cfg *config.Config
)
// 日志模块
var (
logw = logger.Logw
logInfo = logger.LogInfo
LogWarning = logger.LogWarning
logWarning = logger.LogWarning
logError = logger.LogError
)
func InitHandleRouter(cfg *config.Config, router *gin.Engine) {
// 设置路由
func InitHandleRouter(cfg *config.Config, router *gin.Engine, version string) {
apiRouter := router.Group("api")
{
apiRouter.GET("/size_limit", func(c *gin.Context) {
@@ -34,17 +32,28 @@ func InitHandleRouter(cfg *config.Config, router *gin.Engine) {
apiRouter.GET("/blacklist/status", func(c *gin.Context) {
BlackListStatusHandler(c, cfg)
})
apiRouter.GET("/cors/status", func(c *gin.Context) {
CorsStatusHandler(c, cfg)
})
apiRouter.GET("/healthcheck", func(c *gin.Context) {
HealthcheckHandler(c)
})
apiRouter.GET("/version", func(c *gin.Context) {
VersionHandler(c, version)
})
apiRouter.GET("/rate_limit/status", func(c *gin.Context) {
RateLimitStatusHandler(c, cfg)
})
apiRouter.GET("/rate_limit/limit", func(c *gin.Context) {
RateLimitLimitHandler(c, cfg)
})
}
logInfo("API router Init success")
}
func SizeLimitHandler(cfg *config.Config, c *gin.Context) {
// 转换为MB
sizeLimit := cfg.Server.SizeLimit / 1024 / 1024
// 设置响应头
sizeLimit := cfg.Server.SizeLimit
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"MaxResponseBodySize": sizeLimit,
@@ -52,7 +61,7 @@ func SizeLimitHandler(cfg *config.Config, c *gin.Context) {
}
func WhiteListStatusHandler(c *gin.Context, cfg *config.Config) {
// 设置响应头
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Whitelist": cfg.Whitelist.Enabled,
@@ -60,17 +69,49 @@ func WhiteListStatusHandler(c *gin.Context, cfg *config.Config) {
}
func BlackListStatusHandler(c *gin.Context, cfg *config.Config) {
// 设置响应头
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Blacklist": cfg.Blacklist.Enabled,
})
}
func CorsStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Cors": cfg.CORS.Enabled,
})
}
func HealthcheckHandler(c *gin.Context) {
// 设置响应头
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Status": "OK",
})
}
func VersionHandler(c *gin.Context, version string) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"Version": version,
})
}
func RateLimitStatusHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"RateLimit": cfg.RateLimit.Enabled,
})
}
func RateLimitLimitHandler(c *gin.Context, cfg *config.Config) {
logInfo("%s %s %s %s %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto)
c.Writer.Header().Set("Content-Type", "application/json")
json.NewEncoder(c.Writer).Encode(map[string]interface{}{
"RatePerMinute": cfg.RateLimit.RatePerMinute,
})
}

View File

@@ -1,21 +1,20 @@
package auth
import (
"fmt"
"ghproxy/config"
"ghproxy/logger"
"github.com/gin-gonic/gin"
)
// 日志模块
var (
logw = logger.Logw
logInfo = logger.LogInfo
LogWarning = logger.LogWarning
logWarning = logger.LogWarning
logError = logger.LogError
)
// Auth Init
func Init(cfg *config.Config) {
if cfg.Blacklist.Enabled {
LoadBlacklist(cfg)
@@ -26,27 +25,25 @@ func Init(cfg *config.Config) {
logInfo("Auth Init")
}
func AuthHandler(c *gin.Context, cfg *config.Config) bool {
// 如果身份验证未启用,直接返回 true
func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
if !cfg.Auth.Enabled {
return true
return true, ""
}
// 获取 auth_token 参数
authToken := c.Query("auth_token")
logInfo("auth_token received: %s", authToken)
logInfo("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto, authToken)
// 验证 token
if authToken == "" {
LogWarning("auth FAILED: no auth_token provided")
return false
err := "Auth token == nil"
return false, err
}
isValid := authToken == cfg.Auth.AuthToken
isValid = authToken == cfg.Auth.AuthToken
if !isValid {
LogWarning("auth FAILED: invalid auth_token: %s", authToken)
err := fmt.Sprintf("Auth token incorrect: %s", authToken)
return false, err
}
logInfo("auth SUCCESS: %t", isValid)
return isValid
return isValid, ""
}

View File

@@ -32,9 +32,8 @@ func LoadBlacklist(cfg *config.Config) {
}
}
// fullrepo: "owner/repo" or "owner/*"
func CheckBlacklist(fullrepo string) bool {
return forRangeCheckBlacklist(blacklist.Blacklist, fullrepo)
func CheckBlacklist(repouser string, user string, repo string) bool {
return forRangeCheckBlacklist(blacklist.Blacklist, repouser, user)
}
func sliceRepoName_Blacklist(fullrepo string) (string, string) {
@@ -45,11 +44,16 @@ func sliceRepoName_Blacklist(fullrepo string) (string, string) {
return s[0], s[1]
}
func forRangeCheckBlacklist(blist []string, fullrepo string) bool {
repoUser, _ := sliceRepoName_Blacklist(fullrepo)
func forRangeCheckBlacklist(blist []string, fullrepo string, user string) bool {
for _, blocked := range blist {
if blocked == fullrepo || (strings.HasSuffix(blocked, "/*") && strings.HasPrefix(repoUser, blocked[:len(blocked)-2])) {
return true
users, _ := sliceRepoName_Blacklist(blocked)
if user == users {
if strings.HasSuffix(blocked, "/*") {
return true
}
if fullrepo == blocked {
return true
}
}
}
return false

View File

@@ -31,8 +31,8 @@ func LoadWhitelist(cfg *config.Config) {
}
}
func CheckWhitelist(fullrepo string) bool {
return forRangeCheckWhitelist(whitelist.Whitelist, fullrepo)
func CheckWhitelist(fullrepo string, user string, repo string) bool {
return forRangeCheckWhitelist(whitelist.Whitelist, fullrepo, user)
}
func sliceRepoName_Whitelist(fullrepo string) (string, string) {
@@ -43,11 +43,16 @@ func sliceRepoName_Whitelist(fullrepo string) (string, string) {
return s[0], s[1]
}
func forRangeCheckWhitelist(wlist []string, fullrepo string) bool {
repoUser, _ := sliceRepoName_Whitelist(fullrepo)
for _, blocked := range wlist {
if blocked == fullrepo || (strings.HasSuffix(blocked, "/*") && strings.HasPrefix(repoUser, blocked[:len(blocked)-2])) {
return true
func forRangeCheckWhitelist(wlist []string, fullrepo string, user string) bool {
for _, passd := range wlist {
users, _ := sliceRepoName_Whitelist(passd)
if users == user {
if strings.HasSuffix(passd, "/*") {
return true
}
if fullrepo == passd {
return true
}
}
}
return false

View File

@@ -4,7 +4,7 @@
https_port 443
order cache before rewrite
cache {
cache_name GhProxyCache
cache_name GHProxyCache
}
log {
level INFO
@@ -68,7 +68,7 @@
:80 {
reverse_proxy {
to 127.0.0.1:8080
to h2c://127.0.0.1:8080
import header_realip
}
import log ghproxy
@@ -76,8 +76,6 @@
import error_page
import encode
import rate_limit 60
header Age 10
header Cache-Control "max-age=300"
route / {
root /data/www
file_server
@@ -88,21 +86,10 @@
file_server
import cache 0s 24h
}
handle_errors {
@redirects `{err.status_code} in [301, 302, 307]`
reverse_proxy @redirects {
header_up Location {http.response.header.Location}
}
}
route /v2* {
reverse_proxy https://registry-1.docker.io {
header_up Host registry-1.docker.io
header_up X-Real-IP {remote}
header_up X-Forwarded-For {http.request.header.X-Forwarded-For}
header_up X-Forwarded-Proto {scheme}
header_up Authorization {http.request.header.Authorization}
}
route /api* {
rate_limit {remote.ip} 15r/m 10000 429
import cache 0s 6h
}
}

View File

@@ -0,0 +1,96 @@
{
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 256
}
}
(cache) {
cache {
allowed_http_verbs GET
stale {args[0]}
ttl {args[1]}
}
}
(header_realip) {
header_up X-Real-IP {remote_host}
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
}
(rate_limit) {
route /* {
rate_limit {remote.ip} {args[0]}r/m 10000 429
}
}
:80 {
reverse_proxy {
to h2c://127.0.0.1:8080
import header_realip
}
import log ghproxy
import error_page
import encode
import rate_limit 60
route / {
root /data/www
file_server
import cache 300s
}
route /favicon.ico {
root /data/www
file_server
import cache 300s
}
route /api* {
rate_limit {remote.ip} 15r/m 10000 429
import cache 300s
}
}
import /data/caddy/config.d/*

View File

@@ -4,7 +4,7 @@
https_port 443
order cache before rewrite
cache {
cache_name GhProxyCache
cache_name GHProxyCache
}
log {
level INFO
@@ -68,16 +68,14 @@
:80 {
reverse_proxy {
to 127.0.0.1:8080
to h2c://127.0.0.1:8080
import header_realip
}
import log ghproxy
import cache 0s 300s
import error_page
import encode
route /* {
rate_limit {remote.ip} 60r/m 10000 429
}
import rate_limit 60
route / {
root /data/www
file_server
@@ -87,6 +85,12 @@
root /data/www
file_server
import cache 0s 24h
}
route /api* {
rate_limit {remote.ip} 15r/m 10000 429
import cache 0s 6h
}
}

View File

@@ -6,22 +6,29 @@ import (
type Config struct {
Server ServerConfig
Pages PagesConfig
Log LogConfig
CORS CORSConfig
Auth AuthConfig
Blacklist BlacklistConfig
Whitelist WhitelistConfig
RateLimit RateLimitConfig
}
type ServerConfig struct {
Port int `toml:"port"`
Host string `toml:"host"`
SizeLimit int `toml:"sizelimit"`
SizeLimit int `toml:"sizeLimit"`
}
type PagesConfig struct {
Enabled bool `toml:"enabled"`
StaticDir string `toml:"staticDir"`
}
type LogConfig struct {
LogFilePath string `toml:"logfilepath"`
MaxLogSize int `toml:"maxlogsize"`
LogFilePath string `toml:"logFilePath"`
MaxLogSize int `toml:"maxLogSize"`
}
type CORSConfig struct {
@@ -30,17 +37,23 @@ type CORSConfig struct {
type AuthConfig struct {
Enabled bool `toml:"enabled"`
AuthToken string `toml:"authtoken"`
AuthToken string `toml:"authToken"`
}
type BlacklistConfig struct {
Enabled bool `toml:"enabled"`
BlacklistFile string `toml:"blacklistfile"`
BlacklistFile string `toml:"blacklistFile"`
}
type WhitelistConfig struct {
Enabled bool `toml:"enabled"`
WhitelistFile string `toml:"whitelistfile"`
WhitelistFile string `toml:"whitelistFile"`
}
type RateLimitConfig struct {
Enabled bool `toml:"enabled"`
RatePerMinute int `toml:"ratePerMinute"`
Burst int `toml:"burst"`
}
// LoadConfig 从 TOML 配置文件加载配置

View File

@@ -1,23 +1,32 @@
[server]
host = "127.0.0.1"
port = 8080
sizelimit = 131072000 # 125MB
sizeLimit = 125 # MB
[pages]
enabled = false
staticDir = "/data/www"
[log]
logfilepath = "/data/ghproxy/log/ghproxy.log"
maxlogsize = 5 # MB
logFilePath = "/data/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
[cors]
enabled = true
[auth]
authtoken = "test"
authToken = "token"
enabled = false
[blacklist]
blacklistfile = "/data/ghproxy/config/blacklist.json"
blacklistFile = "/data/ghproxy/config/blacklist.json"
enabled = false
[whitelist]
enabled = false
whitelistfile = "/data/ghproxy/config/whitelist.json"
whitelistFile = "/data/ghproxy/config/whitelist.json"
[rateLimit]
enabled = false
ratePerMinute = 180
burst = 5

View File

@@ -1,23 +1,32 @@
[server]
host = "127.0.0.1"
port = 8080
sizelimit = 131072000 # 125MB
sizeLimit = 125 # MB
[pages]
enabled = true
staticDir = "/usr/local/ghproxy/pages"
[log]
logfilepath = "/root/data/ghproxy/log/ghproxy.log"
maxlogsize = 5 # MB
logFilePath = "/usr/local/ghproxy/log/ghproxy.log"
maxLogSize = 5 # MB
[cors]
enabled = true
[auth]
authtoken = "test"
authToken = "token"
enabled = false
[blacklist]
blacklistfile = "/root/data/ghproxy/config/blacklist.json"
blacklistFile = "/usr/local/ghproxy/config/blacklist.json"
enabled = false
[whitelist]
enabled = false
whitelistfile = "/root/data/ghproxy/config/whitelist.json"
whitelistFile = "/usr/local/ghproxy/config/whitelist.json"
[rateLimit]
enabled = false
ratePerMinute = 180
burst = 5

View File

@@ -3,8 +3,8 @@ Description=Github Proxy Service
After=network.target
[Service]
ExecStart=/bin/bash -c '/root/data/ghproxy/ghproxy -cfg /root/data/ghproxy/config/config.toml > /root/data/ghproxy/log/run.log 2>&1'
WorkingDirectory=/root/data/ghproxy
ExecStart=/bin/bash -c '/usr/local/ghproxy/ghproxy -cfg /usr/local/ghproxy/config/config.toml > /usr/local/ghproxy/log/run.log 2>&1'
WorkingDirectory=/usr/local/ghproxy
Restart=always
User=root
Group=root

138
deploy/install-dev.sh Normal file
View File

@@ -0,0 +1,138 @@
# /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
}
make_systemd_service() {
cat <<EOF > /etc/systemd/system/ghproxy.service
[Unit]
Description=Github Proxy Service
After=network.target
[Service]
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
WorkingDirectory=$ghproxy_dir
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
EOF
}
# 安装依赖包
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/DEV-VERSION)
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/DEV-VERSION
# 下载ghproxy
wget -q -O ${ghproxy_dir}/ghproxy https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH.tar.gz
install tar
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
chmod +x ${ghproxy_dir}/ghproxy
# 下载pages
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/index.html
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/favicon.ico
# 下载配置文件
if [ -f ${ghproxy_dir}/config/config.toml ]; then
echo "配置文件已存在, 跳过下载"
echo "[WARNING] 请检查配置文件是否正确DEV版本升级时请注意配置文件兼容性"
sleep 2
else
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/config.toml
fi
# 替换 port = 8080
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
# 下载systemd服务文件
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
else
make_systemd_service
fi
# 启动ghproxy
systemctl daemon-reload
systemctl enable ghproxy
systemctl start ghproxy
echo "ghproxy 安装成功, 监听端口为 $PORT"

View File

@@ -1,4 +1,7 @@
# /bin/bash
# https://github.com/WJQSERVER-STUDIO/ghproxy
ghproxy_dir="/usr/local/ghproxy"
# install packages
install() {
@@ -27,6 +30,26 @@ install() {
return 0
}
make_systemd_service() {
cat <<EOF > /etc/systemd/system/ghproxy.service
[Unit]
Description=Github Proxy Service
After=network.target
[Service]
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
WorkingDirectory=$ghproxy_dir
Restart=always
User=root
Group=root
[Install]
WantedBy=multi-user.target
EOF
}
# 安装依赖包
install curl wget sed
@@ -44,31 +67,68 @@ 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 /root/data/ghproxy
mkdir -p /root/data/ghproxy/config
mkdir -p /root/data/ghproxy/log
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 -O /root/data/ghproxy/VERSION 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 -O /root/data/ghproxy/ghproxy https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH
chmod +x /root/data/ghproxy/ghproxy
wget -q -O ${ghproxy_dir}/ghproxy https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH.tar.gz
install tar
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
chmod +x ${ghproxy_dir}/ghproxy
# 下载pages
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/index.html
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/favicon.ico
# 下载配置文件
wget -O /root/data/ghproxy/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/config.toml
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" /root/data/ghproxy/config/config.toml
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服务文件
wget -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
else
make_systemd_service
fi
# 启动ghproxy
systemctl daemon-reload

View File

@@ -7,7 +7,21 @@ systemctl stop 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 文件夹
rm -r /root/data/ghproxy
# 检查目录是否存在ghproxy文件
if [ -f "$install_path" ]; then
echo "ghproxy 未安装或安装路径错误"
exit 1
else
echo "ghproxy 安装目录已确认,正在卸载..."
rm -r $install_path
fi
echo "ghproxy 已成功卸载"

View File

@@ -1,4 +1,4 @@
FROM wjqserver/caddy:daily-alpine AS builder
FROM wjqserver/caddy:2.9.0-rc-alpine AS builder
ARG USER=WJQSERVER-STUDIO
ARG REPO=ghproxy
@@ -13,7 +13,7 @@ RUN mkdir -p /data/${APPLICATION}/config
RUN mkdir -p /data/${APPLICATION}/log
# 安装依赖
RUN apk add --no-cache curl wget
RUN apk add --no-cache curl wget tar
# 前端
RUN wget -O /data/www/index.html https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/index.html
@@ -21,7 +21,9 @@ RUN wget -O /data/www/favicon.ico https://raw.githubusercontent.com/${USER}/${RE
# 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/DEV-VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION} https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/dev/init.sh
# 拉取配置
@@ -34,7 +36,7 @@ RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.co
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
FROM wjqserver/caddy:daily-alpine
FROM wjqserver/caddy:2.9.0-rc-alpine
COPY --from=builder /data/www /data/www
COPY --from=builder /data/caddy /data/caddy

View File

@@ -0,0 +1,51 @@
FROM wjqserver/caddy:2.9.0-rc-alpine AS builder
ARG USER=WJQSERVER-STUDIO
ARG REPO=ghproxy
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 apk add --no-cache curl wget tar
# 前端
RUN wget -O /data/www/index.html https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/index.html
RUN wget -O /data/www/favicon.ico https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/favicon.ico
# 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/nocache/init.sh
# 拉取配置
RUN wget -O /data/caddy/Caddyfile https://raw.githubusercontent.com/${USER}/${REPO}/main/caddyfile/nocache/Caddyfile
RUN wget -O /data/${APPLICATION}/config.toml https://raw.githubusercontent.com/${USER}/${REPO}/main/config/config.toml
RUN wget -O /data/${APPLICATION}/blacklist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/blacklist.json
RUN wget -O /data/${APPLICATION}/whitelist.json https://raw.githubusercontent.com/${USER}/${REPO}/main/config/whitelist.json
# 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
FROM wjqserver/caddy:2.9.0-rc-alpine
COPY --from=builder /data/www /data/www
COPY --from=builder /data/caddy /data/caddy
COPY --from=builder /data/${APPLICATION} /data/${APPLICATION}
COPY --from=builder /usr/local/bin/init.sh /usr/local/bin/init.sh
# 权限
RUN chmod +x /data/${APPLICATION}/${APPLICATION}
RUN chmod +x /usr/local/bin/init.sh
CMD ["/usr/local/bin/init.sh"]

View File

@@ -0,0 +1,27 @@
#!/bin/sh
APPLICATION=ghproxy
if [ ! -f /data/caddy/config/Caddyfile ]; then
cp /data/caddy/Caddyfile /data/caddy/config/Caddyfile
fi
if [ ! -f /data/${APPLICATION}/config/blacklist.json ]; then
cp /data/${APPLICATION}/blacklist.json /data/${APPLICATION}/config/blacklist.json
fi
if [ ! -f /data/${APPLICATION}/config/whitelist.json ]; then
cp /data/${APPLICATION}/whitelist.json /data/${APPLICATION}/config/whitelist.json
fi
if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
fi
/data/caddy/caddy run --config /data/caddy/config/Caddyfile > /data/${APPLICATION}/log/caddy.log 2>&1 &
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1 &
while true; do
sleep 1
done

View File

@@ -13,7 +13,7 @@ RUN mkdir -p /data/${APPLICATION}/config
RUN mkdir -p /data/${APPLICATION}/log
# 安装依赖
RUN apk add --no-cache curl wget
RUN apk add --no-cache curl wget tar
# 前端
RUN wget -O /data/www/index.html https://raw.githubusercontent.com/${USER}/${REPO}/main/pages/index.html
@@ -21,7 +21,9 @@ RUN wget -O /data/www/favicon.ico https://raw.githubusercontent.com/${USER}/${RE
# 后端
RUN VERSION=$(curl -s https://raw.githubusercontent.com/${USER}/${REPO}/main/VERSION) && \
wget -O /data/${APPLICATION}/${APPLICATION} https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}
wget -O /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz https://github.com/${USER}/${REPO}/releases/download/$VERSION/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz && \
tar -zxvf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz -C /data/${APPLICATION} && \
rm -rf /data/${APPLICATION}/${APPLICATION}-${TARGETOS}-${TARGETARCH}.tar.gz
RUN wget -O /usr/local/bin/init.sh https://raw.githubusercontent.com/${USER}/${REPO}/main/docker/dockerfile/release/init.sh
# 拉取配置

13
go.mod
View File

@@ -6,23 +6,24 @@ require (
github.com/BurntSushi/toml v1.4.0
github.com/gin-gonic/gin v1.10.0
github.com/imroc/req/v3 v3.48.0
golang.org/x/time v0.7.0
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/bytedance/sonic v1.12.3 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/google/pprof v0.0.0-20241009165004-a3522334989c // indirect
github.com/google/pprof v0.0.0-20241101162523-b92577c0c142 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -32,14 +33,14 @@ require (
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/onsi/ginkgo/v2 v2.21.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.47.0 // indirect
github.com/quic-go/quic-go v0.48.1 // 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
go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.11.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect

24
go.sum
View File

@@ -7,6 +7,8 @@ github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKz
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
@@ -18,6 +20,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
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=
@@ -32,6 +36,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
@@ -41,6 +46,14 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241009165004-a3522334989c h1:NDovD0SMpBYXlE1zJmS1q55vWB/fUQBcPAqAboZSccA=
github.com/google/pprof v0.0.0-20241009165004-a3522334989c/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20241017200806-017d972448fc h1:NGyrhhFhwvRAZg02jnYVg3GBQy0qGBKmFQJwaPmpmxs=
github.com/google/pprof v0.0.0-20241017200806-017d972448fc/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20241023014458-598669927662 h1:SKMkD83p7FwUqKmBsPdLHF5dNyxq3jOWwu9w9UyH5vA=
github.com/google/pprof v0.0.0-20241023014458-598669927662/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20241101162523-b92577c0c142 h1:sAGdeJj0bnMgUNVeUpp6AYlVdCt3/GdI3pGRqsNSQLs=
github.com/google/pprof v0.0.0-20241101162523-b92577c0c142/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=
@@ -67,8 +80,11 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/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/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -77,6 +93,10 @@ 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/quic-go/quic-go v0.48.0 h1:2TCyvBrMu1Z25rvIAlnp2dPT4lgh/uTqLqiXVpp5AeU=
github.com/quic-go/quic-go v0.48.0/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA=
github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -97,6 +117,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
@@ -117,6 +139,8 @@ golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=

View File

@@ -18,11 +18,11 @@ var (
logger *log.Logger
logChannel = make(chan string, 100)
quitChannel = make(chan struct{})
logFileMutex sync.Mutex // 保护 logFile 的互斥锁
logFileMutex sync.Mutex
logFilePath = "/data/ghproxy/log/ghproxy.log"
)
// Init 初始化日志记录器,接受日志文件路径作为参数
// 初始化
func Init(logFilePath_input string, maxLogsize int) error {
logFileMutex.Lock()
defer logFileMutex.Unlock()
@@ -40,7 +40,6 @@ func Init(logFilePath_input string, maxLogsize int) error {
return nil
}
// logWorker 处理日志记录
func logWorker() {
for {
select {
@@ -53,38 +52,35 @@ func logWorker() {
}
}
// Log 直接记录日志的函数
func Log(customMessage string) {
logChannel <- customMessage
}
// Logw 用于格式化日志记录
func Logw(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
Log(message)
}
// 日志等级INFO
// INFO
func LogInfo(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
output := fmt.Sprintf("[INFO] %s", message)
Log(output)
}
// 日志等级WARNING
// WARNING
func LogWarning(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
output := fmt.Sprintf("[WARNING] %s", message)
Log(output)
}
// 日志等级ERROR
// ERROR
func LogError(format string, args ...interface{}) {
message := fmt.Sprintf(format, args...)
Log(message)
}
// Close 关闭日志文件
func Close() {
logFileMutex.Lock()
defer logFileMutex.Unlock()
@@ -98,9 +94,9 @@ func Close() {
}
func monitorLogSize(logFilePath string, maxLogsize int) {
var maxLogsizeBytes int64 = int64(maxLogsize) * 1024 * 1024 // 最大日志文件大小单位为MB
var maxLogsizeBytes int64 = int64(maxLogsize) * 1024 * 1024
for {
time.Sleep(600 * time.Second) // 每10分钟检查一次
time.Sleep(120 * time.Minute) // 每120分钟检查一次日志文件大小
logFileMutex.Lock()
info, err := logFile.Stat()
logFileMutex.Unlock()
@@ -123,7 +119,6 @@ func rotateLogFile(logFilePath string) error {
}
}
// 打开当前日志文件
logFile, err := os.Open(logFilePath)
if err != nil {
return fmt.Errorf("failed to open log file: %s, error: %w", logFilePath, err)
@@ -170,7 +165,6 @@ func rotateLogFile(logFilePath string) error {
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)

74
main.go
View File

@@ -5,12 +5,14 @@ import (
"fmt"
"log"
"net/http"
"time"
"ghproxy/api"
"ghproxy/auth"
"ghproxy/config"
"ghproxy/logger"
"ghproxy/proxy"
"ghproxy/rate"
"github.com/gin-gonic/gin"
)
@@ -19,82 +21,98 @@ var (
cfg *config.Config
router *gin.Engine
configfile = "/data/ghproxy/config/config.toml"
cfgfile string
limiter *rate.RateLimiter
version string
)
// 日志模块
var (
logw = logger.Logw
logInfo = logger.LogInfo
LogWarning = logger.LogWarning
logWarning = logger.LogWarning
logError = logger.LogError
)
func ReadFlag() {
cfgfile := flag.String("cfg", configfile, "config file path")
configfile = *cfgfile
func readFlag() {
flag.StringVar(&cfgfile, "cfg", configfile, "config file path")
}
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.Println("Config File Path: ", cfgfile)
fmt.Printf("Loaded config: %v\n", cfg)
}
func setupLogger(cfg *config.Config) {
// 初始化日志模块
var err error
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize) // 传递日志文件路径
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
if err != nil {
log.Fatalf("Failed to initialize logger: %v", err)
}
logInfo("Logger initialized")
logInfo("Config File Path: ", cfgfile)
logInfo("Loaded config: %v\n", cfg)
logInfo("Init Completed")
}
func Loadlist(cfg *config.Config) {
func loadlist(cfg *config.Config) {
auth.Init(cfg)
}
func setupApi(cfg *config.Config, router *gin.Engine) {
// 注册 API 接口
api.InitHandleRouter(cfg, router)
func setupApi(cfg *config.Config, router *gin.Engine, version string) {
api.InitHandleRouter(cfg, router, version)
}
func setupRateLimit(cfg *config.Config) {
if cfg.RateLimit.Enabled {
limiter = rate.New(cfg.RateLimit.RatePerMinute, cfg.RateLimit.Burst, 1*time.Minute)
logInfo("Rate Limit Loaded")
}
}
func init() {
ReadFlag()
readFlag()
flag.Parse()
loadConfig()
setupLogger(cfg)
Loadlist(cfg)
loadlist(cfg)
setupRateLimit(cfg)
// 设置 Gin 模式
gin.SetMode(gin.ReleaseMode)
// 初始化路由
router = gin.Default()
router.UseH2C = true
setupApi(cfg, router)
setupApi(cfg, router, version)
// 定义路由
router.GET("/", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://ghproxy0rtt.1888866.xyz/")
})
if cfg.Pages.Enabled {
indexPagePath := fmt.Sprintf("%s/index.html", cfg.Pages.StaticDir)
faviconPath := fmt.Sprintf("%s/favicon.ico", cfg.Pages.StaticDir)
router.GET("/", func(c *gin.Context) {
c.File(indexPagePath)
logInfo("IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto)
})
router.StaticFile("/favicon.ico", faviconPath)
} else if !cfg.Pages.Enabled {
router.GET("/", func(c *gin.Context) {
c.String(http.StatusForbidden, "403 Forbidden Access")
logWarning("403 > Path:/ IP:%s UA:%s METHOD:%s HTTPv:%s", c.ClientIP(), c.Request.UserAgent(), c.Request.Method, c.Request.Proto)
})
}
// 未匹配路由处理
router.NoRoute(func(c *gin.Context) {
proxy.NoRouteHandler(cfg)(c)
proxy.NoRouteHandler(cfg, limiter)(c)
})
}
func main() {
// 启动服务器
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port))
if err != nil {
logError("Error starting server: %v\n", err)
logError("Failed to start server: %v\n", err)
}
fmt.Println("Program finished")
fmt.Println("Program Exit")
}

View File

@@ -5,12 +5,30 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Github文件加速">
<meta name="keywords" content="Github,文件加速,ghproxy">
<meta name="color-scheme" content="dark light">
<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>
:root {
--color: #dadada;
--fontcolor: #333;
--inputcolor: #a19f9f;
}
@media (prefers-color-scheme: dark) {
:root {
--color: #53535338;
--fontcolor: #b8b8b8;
--inputcolor: #012333;
--inputcolor-font: #969696d8;
}
}
body {
background-color: #f8f9fac5;
background-color: var(--color);
color: var(--fontcolor);
font-family: 'Misans', Arial, sans-serif;
padding: 30px;
display: flex;
@@ -22,25 +40,61 @@
position: relative;
}
.version {
width: 12.5%;
height: 2%;
background-color: #39c5bb;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.8rem;
border-radius: 0.5rem;
position: fixed;
bottom: 0%;
right: 0%;
}
.version p {
margin: 0px;
padding: 0px;
}
*::-webkit-scrollbar {
height: 10px;
margin-top: 0px;
}
*::-webkit-scrollbar-track {
background-color: black;
}
*::-webkit-scrollbar-thumb {
background: #39c5bb;
border-radius: 10px;
}
.container {
max-width: 800px;
max-width: 80%;
text-align: center;
min-height: 45vh;
min-height: 65%;
line-height: 1.25;
}
h1 {
color: var(--fontcolor);
font-weight: bold;
margin-bottom: 75px;
margin-bottom: 20%;
}
.rounded-button {
border-radius: 6px;
transition: background-color 0.3s, transform 0.2s;
padding: 10px 30px;
background-color: #39c5bb;
color: white;
background-color: #555c5c;
color: rgb(255, 255, 255);
border: none;
margin-bottom: 10px;
margin-bottom: 3%;
}
.rounded-button:hover {
@@ -51,23 +105,26 @@
.tips>p:first-child::before {
position: sticky;
color: #7b7b7b;
margin-bottom: 5px;
margin-bottom: 1%;
font-size: 60%;
}
footer {
line-height: 1.25;
position: absolute;
bottom: 0;
left: 0;
right: 0;
text-align: center;
font-size: 1rem;
}
pre {
background: #2d2d2d;
color: #f8f8f2;
padding: 20px 20px;
margin: 10px 0;
border-radius: 8px;
background: #012333;
color: #39c5bc;
padding: 15px 20px 15px 20px;
margin: 0px 0;
border-radius: 0.5rem;
overflow-x: auto;
position: relative;
}
@@ -76,30 +133,103 @@
content: " ";
display: block;
position: absolute;
top: 10px;
left: 10px;
width: 12px;
height: 12px;
background: #ff5f56;
top: 6px;
left: 6px;
width: 10px;
height: 10px;
background: #bd3c35;
border-radius: 50%;
box-shadow: 20px 0 0 #ffbd2e, 40px 0 0 #27c93f;
box-shadow: 20px 0 0 #d69f27, 40px 0 0 #39c5bb;
}
code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.875em;
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 0.9em;
margin-bottom: 0px;
}
@media (max-width: 768px) {
footer {
font-size: 0.85rem;
}
.container {
max-width: 100%;
font-size: 0.8rem;
}
.tips {
font-size: 0.8rem;
}
.tips-content {
font-size: 0.8rem;
}
.status-container {
font-size: 0.8rem;
}
}
@media (min-width: 768px) {
footer {
font-size: 1rem;
}
.container {
max-width: 65%;
font-size: 1rem;
}
.tips {
font-size: 1.1rem;
}
.tips-content {
font-size: 1.1rem;
}
.status-container {
font-size: 1.05rem;
}
h1 {
margin-bottom: 10%;
}
.version {
width: 7.5%;
}
}
.form-group {
margin-bottom: 3%;
}
.form-control {
background-color: var(--inputcolor);
color: var(--inputcolor-font);
}
.form-control:focus {
background-color: var(--inputcolor);
color: var(--inputcolor-font);
}
.tips-content {
margin-bottom: 0px;
}
.status-container {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 5px;
margin-bottom: 1px;
margin-top: -2%;
}
.status-container p {
margin: 0px 5px;
margin: 0px 1px;
}
@@ -110,9 +240,9 @@
.copy-button {
position: absolute;
top: 5px;
top: 10px;
right: 10px;
background: rgba(118, 119, 121, 0.7);
background: rgba(0, 217, 224, 0.822);
color: white;
border: none;
padding: 5px 10px;
@@ -120,6 +250,24 @@
cursor: pointer;
transition: opacity 0.3s;
z-index: 1;
font-size: 0.85rem;
display: none;
}
.redir-button {
position: absolute;
top: 10px;
right: 65px;
background: rgba(0, 217, 224, 0.822);
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
transition: opacity 0.3s;
z-index: 1;
font-size: 0.85rem;
display: none;
}
pre:hover .copy-button {
@@ -129,25 +277,44 @@
#visitor-info {
margin-top: 10px;
text-align: center;
line-height: 0.5;
line-height: 0;
}
#toast {
position: fixed;
top: 10%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #39c5bcde;
color: white;
padding: 15px 20px;
border-radius: 10px;
font-size: 90%;
z-index: 1000;
}
</style>
</head>
<body>
<div class="version">
<p id="version"></p>
</div>
<div class="container">
<h1>Github文件加速</h1>
<div class="form-group">
<input type="text" class="form-control" id="githubLinkInput" placeholder="键入Github链接">
<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>
<button class="copy-button" id="copyButton">复制</button>
<button class="redir-button" id="redirButton">打开</button>
<pre id="formattedLinkOutput"></pre>
</div>
<div class="tips">
<p>GitHub链接带不带协议头均可支持release、archive以及文件转换后链接均可使用</a></p>
<div class="tips-content">
<p>GitHub链接带不带协议头均可支持release、archive以及文件转换后链接均可使用</a></p><br>
</div>
<div class="status-container">
<p id="sizeLimitDisplay">文件大小限制: ...</p>
<p id="whiteListStatus">白名单状态: ...</p>
@@ -155,6 +322,10 @@
</div>
</div>
</div>
<div id="toast" style="display:none;">
链接已复制到剪贴板
</div>
<script>
function formatGithubLink() {
@@ -164,19 +335,42 @@
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));
displayButton();
} else if (githubLinkInput.value.startsWith("github.com/")) {
formattedLink = "https://" + currentHost + "/" + githubLinkInput.value;
displayButton();
} 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));
displayButton();
} else if (githubLinkInput.value.startsWith("raw.githubusercontent.com/")) {
formattedLink = "https://" + currentHost + "/" + githubLinkInput.value;
} else if (!githubLinkInput.value.trim()) {
alert('请输入有效的GitHub链接');
displayButton();
} else if (githubLinkInput.value.startsWith("https://gist.githubusercontent.com/") || githubLinkInput.value.startsWith("http://gist.githubusercontent.com/")) {
formattedLink = "https://" + currentHost + "/gist.github.com" + githubLinkInput.value.substring(githubLinkInput.value.indexOf("/", 18));
displayButton();
} else if (githubLinkInput.value.startsWith("gist.githubusercontent.com/")) {
formattedLink = "https://" + currentHost + "/" + githubLinkInput.value;
displayButton();
} else {
showToast('请输入有效的GitHub链接');
}
var formattedLinkOutput = document.getElementById('formattedLinkOutput');
formattedLinkOutput.textContent = formattedLink;
}
function displayButton() {
var copyButton = document.getElementById('copyButton');
var redirButton = document.getElementById('redirButton');
copyButton.style.display = 'block';
redirButton.style.display = 'block';
}
function redirToFormattedLink() {
var formattedLinkOutput = document.getElementById('formattedLinkOutput');
console.log(formattedLinkOutput.textContent);
window.open(formattedLinkOutput.textContent);
}
document.getElementById('formatButton').addEventListener('click', formatGithubLink);
document.getElementById('copyButton').addEventListener('click', function () {
const output = document.getElementById('formattedLinkOutput');
@@ -186,8 +380,19 @@
window.getSelection().addRange(range);
document.execCommand('copy');
window.getSelection().removeAllRanges();
alert('链接已复制到剪贴板');
showToast('链接已复制到剪贴板');
});
document.getElementById('redirButton').addEventListener('click', redirToFormattedLink);
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.style.display = 'block';
setTimeout(() => {
toast.style.display = 'none';
}, 3000); // 3秒后隐藏
}
function fetchSizeLimit() {
fetch(window.location.origin + '/api/size_limit')
@@ -235,10 +440,22 @@
console.error('Error fetching API:', error);
});
}
function fetchVersion() {
fetch(window.location.origin + '/api/version')
.then(response => response.json())
.then(data => {
const version = document.getElementById('version');
version.textContent = `${data.Version}`;
})
.catch(error => {
console.error('Error fetching API:', error);
});
}
function fetchAPI() {
fetchSizeLimit();
fetchWhiteList();
fetchBlackList();
fetchVersion();
}
document.addEventListener('DOMContentLoaded', fetchAPI);
</script>
@@ -246,10 +463,9 @@
<footer>
<p>
Copyright &copy; 2024 WJQSERVER-STUDIO
</p>
<p>
GitHub仓库地址<a href="https://github.com/WJQSERVER-STUDIO/ghproxy">https://github.com/WJQSERVER-STUDIO/ghproxy</a>
Copyright &copy; 2024 WJQSERVER-STUDIO<br>
GitHub仓库地址<a href="https://github.com/WJQSERVER-STUDIO/ghproxy">WJQSERVER-STUDIO/ghproxy</a>
<br><a href="https://t.me/ghproxy_go">Telegram交流群</a>
</p>
<div id="visitor-info" style="text-align: center; margin-top: 15px;">
<p>您的IP地址: <span id="visitor-ip"></span></p>

View File

@@ -1,4 +1,3 @@
// proxy/proxy.go 实验性
package proxy
import (
@@ -12,6 +11,7 @@ import (
"ghproxy/auth"
"ghproxy/config"
"ghproxy/logger"
"ghproxy/rate"
"github.com/gin-gonic/gin"
"github.com/imroc/req/v3"
@@ -21,7 +21,7 @@ import (
var (
logw = logger.Logw
logInfo = logger.LogInfo
LogWarning = logger.LogWarning
logWarning = logger.LogWarning
logError = logger.LogError
)
@@ -30,62 +30,67 @@ var exps = []*regexp.Regexp{
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/([^/]+)/.+?/.+`),
regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/.+?/.+`),
}
func NoRouteHandler(cfg *config.Config) gin.HandlerFunc {
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter) gin.HandlerFunc {
return func(c *gin.Context) {
// 限制访问频率
if cfg.RateLimit.Enabled {
logInfo("Rate_Limit Enabled")
if !limiter.Allow() {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "Too Many Requests"})
logWarning("%s %s %s %s %s 429-TooManyRequests", c.ClientIP(), c.Request.Method, c.Request.URL.RequestURI(), c.Request.Header.Get("User-Agent"), c.Request.Proto)
return
}
}
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/")
re := regexp.MustCompile(`^(http:|https:)?/?/?(.*)`)
matches := re.FindStringSubmatch(rawPath)
if len(matches) < 3 {
LogWarning("Invalid URL: %s", rawPath)
c.String(http.StatusForbidden, "Invalid URL.")
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errMsg)
c.String(http.StatusForbidden, "Invalid URL Format. Path: %s", rawPath)
return
}
rawPath = "https://" + matches[2]
// 提取用户名和仓库名,格式为 handle/<username>/<repo>/*
pathmatches := regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`)
pathParts := pathmatches.FindStringSubmatch(matches[2])
if len(pathParts) < 4 {
LogWarning("Invalid path: %s", rawPath)
c.String(http.StatusForbidden, "Invalid path; expected username/repo.")
return
}
username, repo := MatchUserRepo(rawPath, cfg, c, matches)
username := pathParts[2]
repo := pathParts[3]
LogWarning("Blacklist Check > Username: %s, Repo: %s", username, repo)
fullrepo := fmt.Sprintf("%s/%s", username, repo)
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, username, repo)
repouser := fmt.Sprintf("%s/%s", username, repo)
// 白名单检查
if cfg.Whitelist.Enabled {
whitelistpass := auth.CheckWhitelist(fullrepo)
if !whitelistpass {
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", fullrepo)
whitelist := auth.CheckWhitelist(repouser, username, repo)
if !whitelist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
errMsg := fmt.Sprintf("Whitelist Blocked repo: %s", repouser)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
LogWarning(errMsg)
logWarning(logErrMsg)
return
}
}
// 黑名单检查
if cfg.Blacklist.Enabled {
blacklistpass := auth.CheckBlacklist(fullrepo)
if blacklistpass {
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", fullrepo)
blacklist := auth.CheckBlacklist(repouser, username, repo)
if blacklist {
logErrMsg := fmt.Sprintf("%s %s %s %s %s Whitelist Blocked repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, repouser)
errMsg := fmt.Sprintf("Blacklist Blocked repo: %s", repouser)
c.JSON(http.StatusForbidden, gin.H{"error": errMsg})
LogWarning(errMsg)
logWarning(logErrMsg)
return
}
}
matches = CheckURL(rawPath)
matches = CheckURL(rawPath, c)
if matches == nil {
c.AbortWithStatus(http.StatusNotFound)
logError("%s %s %s %s %s 404-NOMATCH", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
return
}
@@ -93,31 +98,55 @@ func NoRouteHandler(cfg *config.Config) gin.HandlerFunc {
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
}
if !auth.AuthHandler(c, cfg) {
// 鉴权
authcheck, err := auth.AuthHandler(c, cfg)
if !authcheck {
c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
LogWarning("Unauthorized request: %s", rawPath)
logWarning("%s %s %s %s %s Auth-Error: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
}
logInfo("Matches: %v", matches)
// IP METHOD URL USERAGENT PROTO MATCHES
logInfo("%s %s %s %s %s Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, matches)
switch {
case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath):
logInfo("%s Matched - USE proxy-chrome", rawPath)
ProxyRequest(c, rawPath, cfg, "chrome")
case exps[2].MatchString(rawPath):
logInfo("%s Matched - USE proxy-git", rawPath)
ProxyRequest(c, rawPath, cfg, "git")
default:
c.String(http.StatusForbidden, "Invalid input.")
fmt.Println("Invalid input.")
return
}
}
}
// 提取用户名和仓库名
func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches []string) (string, string) {
var gistregex = regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.*`)
var gistmatches []string
if gistregex.MatchString(rawPath) {
gistmatches = gistregex.FindStringSubmatch(rawPath)
logInfo("%s %s %s %s %s Matched-Username: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, gistmatches[1])
return gistmatches[1], ""
}
// 定义路径
pathRegex := regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`)
if pathMatches := pathRegex.FindStringSubmatch(matches[2]); len(pathMatches) >= 4 {
return pathMatches[2], pathMatches[3]
}
// 返回错误信息
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errMsg)
c.String(http.StatusForbidden, "Invalid path; expected username/repo, Path: %s", rawPath)
return "", ""
}
func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) {
method := c.Request.Method
logInfo("%s %s", method, u)
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
client := createHTTPClient(mode)
@@ -130,7 +159,7 @@ func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) {
req := client.R().SetBody(body)
setRequestHeaders(c, req)
resp, err := SendRequest(req, method, u)
resp, err := SendRequest(c, req, method, u)
if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return
@@ -138,23 +167,23 @@ func ProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string) {
defer resp.Body.Close()
if err := HandleResponseSize(resp, cfg, c); err != nil {
LogWarning("Error handling response size: %v", err)
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
return
}
CopyResponseHeaders(resp, c, cfg)
c.Status(resp.StatusCode)
if err := copyResponseBody(c, resp.Body); err != nil {
logError("Failed to copy response body: %v", err)
logError("%s %s %s %s %s Response-Copy-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
}
}
// createHTTPClient 创建并配置 HTTP 客户端
// 判断并选择TLS指纹
func createHTTPClient(mode string) *req.Client {
client := req.C()
switch mode {
case "chrome":
client.SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36").
client.SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36").
SetTLSFingerprintChrome().
ImpersonateChrome()
case "git":
@@ -163,7 +192,7 @@ func createHTTPClient(mode string) *req.Client {
return client
}
// readRequestBody 读取请求体
// 读取请求体
func readRequestBody(c *gin.Context) ([]byte, error) {
body, err := io.ReadAll(c.Request.Body)
if err != nil {
@@ -173,7 +202,7 @@ func readRequestBody(c *gin.Context) ([]byte, error) {
return body, nil
}
// setRequestHeaders 设置请求头
// 设置请求头
func setRequestHeaders(c *gin.Context, req *req.Request) {
for key, values := range c.Request.Header {
for _, value := range values {
@@ -182,13 +211,13 @@ func setRequestHeaders(c *gin.Context, req *req.Request) {
}
}
// copyResponseBody 复制响应体到客户端
// 复制响应体
func copyResponseBody(c *gin.Context, respBody io.Reader) error {
_, err := io.Copy(c.Writer, respBody)
return err
}
func SendRequest(req *req.Request, method, url string) (*req.Response, error) {
func SendRequest(c *gin.Context, req *req.Request, method, url string) (*req.Response, error) {
switch method {
case "GET":
return req.Get(url)
@@ -199,36 +228,40 @@ func SendRequest(req *req.Request, method, url string) (*req.Response, error) {
case "DELETE":
return req.Delete(url)
default:
logInfo("Unsupported method: %s", method)
return nil, fmt.Errorf("unsupported method: %s", method)
// IP METHOD URL USERAGENT PROTO UNSUPPORTED-METHOD
errmsg := fmt.Sprintf("%s %s %s %s %s Unsupported method", c.ClientIP(), method, url, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errmsg)
return nil, fmt.Errorf(errmsg)
}
}
func HandleResponseSize(resp *req.Response, cfg *config.Config, c *gin.Context) error {
contentLength := resp.Header.Get("Content-Length")
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > cfg.Server.SizeLimit {
if err == nil && size > sizelimit {
finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
LogWarning("Size limit exceeded: %s, Size: %d", finalURL, size)
return fmt.Errorf("size limit exceeded: %d", size)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return fmt.Errorf("Path: %s size limit exceeded: %d", finalURL, size)
}
}
return nil
}
func CopyResponseHeaders(resp *req.Response, c *gin.Context, cfg *config.Config) {
removeHeaders(resp)
copyHeaders(resp, c)
removeHeaders(resp)
setCORSHeaders(c, cfg)
setDefaultHeaders(c)
}
// removeHeaders 移除指定响应头
// 移除指定响应头
func removeHeaders(resp *req.Response) {
headersToRemove := map[string]struct{}{
"Content-Security-Policy": {},
@@ -241,7 +274,7 @@ func removeHeaders(resp *req.Response) {
}
}
// copyHeaders 复制响应头到 Gin 上下文
// 复制响应头
func copyHeaders(resp *req.Response, c *gin.Context) {
for key, values := range resp.Header {
for _, value := range values {
@@ -250,7 +283,7 @@ func copyHeaders(resp *req.Response, c *gin.Context) {
}
}
// setCORSHeaders 设置 CORS 相关的响应头
// CORS配置
func setCORSHeaders(c *gin.Context, cfg *config.Config) {
if cfg.CORS.Enabled {
c.Header("Access-Control-Allow-Origin", "*")
@@ -259,7 +292,7 @@ func setCORSHeaders(c *gin.Context, cfg *config.Config) {
}
}
// setDefaultHeaders 设置默认响应
// 默认响应
func setDefaultHeaders(c *gin.Context) {
c.Header("Age", "10")
c.Header("Cache-Control", "max-age=300")
@@ -267,17 +300,16 @@ func setDefaultHeaders(c *gin.Context) {
func HandleError(c *gin.Context, message string) {
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
LogWarning(message)
logWarning(message)
}
func CheckURL(u string) []string {
func CheckURL(u string, c *gin.Context) []string {
for _, exp := range exps {
if matches := exp.FindStringSubmatch(u); matches != nil {
logInfo("URL matched: %s, Matches: %v", u, matches[1:])
return matches[1:]
}
}
errMsg := fmt.Sprintf("Invalid URL: %s", u)
LogWarning(errMsg)
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
logWarning(errMsg)
return nil
}

21
rate/rate.go Normal file
View File

@@ -0,0 +1,21 @@
package rate
import (
"time"
"golang.org/x/time/rate"
)
type RateLimiter struct {
limiter *rate.Limiter
}
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()
}