Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97ae0044e7 | ||
|
|
b95582ae1a | ||
|
|
9aaa3e64d3 | ||
|
|
9e0f222125 | ||
|
|
21d30dee53 | ||
|
|
a061b8d369 | ||
|
|
68346717a5 | ||
|
|
2b7fbd2a0d | ||
|
|
4c5d288f03 | ||
|
|
09163ed4df | ||
|
|
f5c32915b9 | ||
|
|
286fa0f311 | ||
|
|
5d08993cbc | ||
|
|
6e787ced6e | ||
|
|
460b7514a9 | ||
|
|
c90140a898 | ||
|
|
f7e4fe71d7 | ||
|
|
33973b786d | ||
|
|
36fe815e35 |
40
.github/workflows/build-dev.yml
vendored
40
.github/workflows/build-dev.yml
vendored
@@ -9,15 +9,44 @@ on:
|
||||
- 'DEV-VERSION'
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 加载版本号
|
||||
run: |
|
||||
if [ -f DEV-VERSION ]; then
|
||||
echo "VERSION=$(cat DEV-VERSION)" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DEV-VERSION file not found!" && exit 1
|
||||
fi
|
||||
- name: 输出版本号
|
||||
run: |
|
||||
echo "Version: ${{ env.VERSION }}"
|
||||
- name: 预先创建Pre-release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ env.VERSION }}
|
||||
artifacts: ./DEV-VERSION
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ env.VERSION }}
|
||||
allowUpdates: true
|
||||
prerelease: true
|
||||
body: ${{ env.VERSION }}
|
||||
env:
|
||||
export PATH: $PATH:/usr/local/go/bin
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux]
|
||||
goos: [linux, darwin, freebsd]
|
||||
goarch: [amd64, arm64]
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
GO_VERSION: 1.23.5
|
||||
GO_VERSION: 1.24
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -48,10 +77,10 @@ jobs:
|
||||
cp LICENSE ./ghproxyd/
|
||||
tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd .
|
||||
ls
|
||||
- name: Upload to GitHub Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: 上传Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OUTPUT_BINARY }}
|
||||
name: ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
|
||||
path: |
|
||||
./${{ env.OUTPUT_BINARY }}*
|
||||
- name: 上传至Release
|
||||
@@ -64,6 +93,7 @@ jobs:
|
||||
tag: ${{ env.VERSION }}
|
||||
allowUpdates: true
|
||||
prerelease: true
|
||||
body: ${{ env.VERSION }}
|
||||
env:
|
||||
export PATH: $PATH:/usr/local/go/bin
|
||||
|
||||
|
||||
41
.github/workflows/build.yml
vendored
41
.github/workflows/build.yml
vendored
@@ -9,15 +9,43 @@ on:
|
||||
- 'VERSION'
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 加载版本号
|
||||
run: |
|
||||
if [ -f VERSION ]; then
|
||||
echo "VERSION=$(cat VERSION)" >> $GITHUB_ENV
|
||||
else
|
||||
echo "VERSION file not found!" && exit 1
|
||||
fi
|
||||
- name: 输出版本号
|
||||
run: |
|
||||
echo "Version: ${{ env.VERSION }}"
|
||||
- name: 预先创建release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
name: ${{ env.VERSION }}
|
||||
artifacts: ./VERSION
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ env.VERSION }}
|
||||
allowUpdates: true
|
||||
body: ${{ env.VERSION }}
|
||||
env:
|
||||
export PATH: $PATH:/usr/local/go/bin
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: prepare # 确保这个作业在 prepare 作业完成后运行
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux]
|
||||
goos: [linux, darwin, freebsd]
|
||||
goarch: [amd64, arm64]
|
||||
env:
|
||||
OUTPUT_BINARY: ghproxy
|
||||
GO_VERSION: 1.23.5
|
||||
GO_VERSION: 1.24
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -45,12 +73,12 @@ jobs:
|
||||
mv ./ghproxyd/${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/${{ env.OUTPUT_BINARY }}
|
||||
cp LICENSE ./ghproxyd/
|
||||
tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd .
|
||||
- name: Upload to GitHub Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
- name: 上传Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OUTPUT_BINARY }}
|
||||
name: ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
|
||||
path: |
|
||||
./${{ env.OUTPUT_BINARY }}*
|
||||
./${{ env.OUTPUT_BINARY }}*
|
||||
- name: 上传至Release
|
||||
id: create_release
|
||||
uses: ncipollo/release-action@v1
|
||||
@@ -60,6 +88,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ env.VERSION }}
|
||||
allowUpdates: true
|
||||
body: ${{ env.VERSION }}
|
||||
env:
|
||||
export PATH: $PATH:/usr/local/go/bin
|
||||
|
||||
|
||||
165
CHANGELOG.md
165
CHANGELOG.md
@@ -1,5 +1,170 @@
|
||||
# 更新日志
|
||||
|
||||
2.2.0
|
||||
---
|
||||
- RELEASE: v2.2.0正式版发布;
|
||||
- CHANGE: 更新Go版本至1.24.0
|
||||
- ADD: 加入`Socks5`和`HTTP(S)`出站支持
|
||||
- CHANGE: 配置新增`Outbound`配置块
|
||||
|
||||
25w13b
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.2.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新Go版本至1.24.0
|
||||
|
||||
25w13a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.2.0的预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 加入`Socks5`和`HTTP(S)`出站支持
|
||||
- CHANGE: 配置新增`Outbound`配置块
|
||||
|
||||
2.1.0
|
||||
---
|
||||
- RELEASE: v2.1.0正式版发布;
|
||||
- CHANGE: 加入`FreeBSD`与`Darwin`系统支持
|
||||
- CHANGE: 更新安全政策, v1和24w版本序列生命周期正式结束
|
||||
- ADD: 加入`timing`中间件记录响应时间
|
||||
- ADD: 加入`loggin`中间件包装日志输出
|
||||
- CHANGE: 更新logger版本至v1.3.0
|
||||
- CHANGE: 改进日志相关
|
||||
- ADD: 加入日志等级配置项
|
||||
|
||||
25w12d
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.1.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 处理类型断言相关问题
|
||||
|
||||
25w12c
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.1.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 加入`FreeBSD`与`Darwin`系统支持
|
||||
|
||||
25w12b
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.8/v2.1.0的预发布版本,请勿在生产环境中使用;
|
||||
- ADD: 加入`timing`中间件记录响应时间
|
||||
- ADD: 加入`loggin`中间件包装日志输出
|
||||
- CHANGE: 更新安全政策, v1和24w版本序列生命周期正式结束
|
||||
|
||||
25w12a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.8/v2.1.0的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新logger版本至v1.3.0
|
||||
- CHANGE: 改进日志相关
|
||||
- ADD: 加入日志等级配置项
|
||||
|
||||
2.0.7
|
||||
---
|
||||
- RELEASE: v2.0.7正式版发布;
|
||||
- CHANGE: 更新Go版本至1.23.6
|
||||
- CHANGE: 更新Logger版本至v1.2.0
|
||||
|
||||
25w11a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.7的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 更新Go版本至1.23.6
|
||||
- CHANGE: 更新Logger版本至v1.2.0
|
||||
|
||||
2.0.6
|
||||
---
|
||||
- RELEASE: v2.0.6正式版发布;祝各位新春快乐!
|
||||
- CHANGE: 优化前端的连接转换逻辑
|
||||
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
|
||||
- 优化`HTTP Client`参数
|
||||
- CHANGE: 为api路由组增加no-cache标头
|
||||
|
||||
25w10b
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.6的预发布版本,请勿在生产环境中使用;祝各位新春快乐!
|
||||
- CHANGE: 为api路由组增加no-cache标头
|
||||
|
||||
25w10a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.6的预发布版本,请勿在生产环境中使用;祝各位新春快乐!
|
||||
- CHANGE: 改进前端的连接转换逻辑
|
||||
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
|
||||
- 优化`HTTP Client`参数
|
||||
|
||||
2.0.5
|
||||
---
|
||||
- RELEASE: v2.0.5正式版发布;
|
||||
- CHANGE: 优化响应体分块复制实现
|
||||
- ADD: 加入缓存池
|
||||
- CHANGE: 改进缓存实现
|
||||
- CHANGE: 部分杂项改进
|
||||
|
||||
25w09b
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.5的预发布版本,请勿在生产环境中使用;
|
||||
- REMOVE: 移除残留配置
|
||||
|
||||
25w09a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.5的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 改进缓存实现
|
||||
- ADD: 加入缓存池
|
||||
|
||||
2.0.4
|
||||
---
|
||||
- RELEASE: v2.0.4正式版发布;
|
||||
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
|
||||
- CHANGE: 优化Matches
|
||||
- REMOVE: 移除Caddyfile残留
|
||||
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
|
||||
|
||||
25w08b
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.4的预发布版本,请勿在生产环境中使用;
|
||||
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
|
||||
|
||||
25w08a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.4的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
|
||||
- CHANGE: 优化Matches
|
||||
- REMOVE: 移除Caddyfile残留
|
||||
|
||||
2.0.3
|
||||
---
|
||||
- RELEASE: v2.0.3正式版发布;
|
||||
- CHANGE: 优化`HTTP Client`参数, 使其更符合本项目使用场景
|
||||
|
||||
25w07b
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.3的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 改进`HTTP Client`参数
|
||||
|
||||
25w07a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.3的预发布版本,请勿在生产环境中使用;
|
||||
- CHANGE: 为`HTTP Client`增加复用, 对性能有所优化
|
||||
- CHANGE: 优化`HTTP Client`参数, 使其更符合本项目使用场景
|
||||
|
||||
2.0.2
|
||||
---
|
||||
- RELEASE: v2.0.2正式版发布; 此版本是v2.0.1改进
|
||||
- CHANGE: 由于用户使用了不符合`RFC 9113`规范的请求头, 导致`ghproxy`无法正常工作, 在此版本为用户的错误行为提供补丁;
|
||||
|
||||
25w06b
|
||||
---
|
||||
- PRE-RELEASE: 此版本是改进验证版本,普通用户请勿使用;
|
||||
- CHANGE: 由于用户使用了不符合`RFC 9113`规范的请求头, 导致`ghproxy`无法正常工作, 在此版本为用户的错误行为提供补丁;
|
||||
|
||||
25w06a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是改进验证版本,普通用户请勿使用;
|
||||
- CHANGE: Remove `Conection: Upgrade` header, which is not currently supported by some web server configurations.
|
||||
|
||||
v2.0.1
|
||||
---
|
||||
- RELEASE: v2.0.1正式版发布; 此版本是v2.0.0的小修复版本, 主要修复了Docker启动脚本存在的一些问题
|
||||
- FIX: 修复Docker启动脚本存在的一些问题
|
||||
|
||||
25w05a
|
||||
---
|
||||
- PRE-RELEASE: 此版本是v2.0.1的候选版本,请勿在生产环境中使用;
|
||||
- FIX: 修复Docker启动脚本存在的一些问题
|
||||
|
||||
2.0.0
|
||||
---
|
||||
- RELEASE: v2.0.0正式版发布; 此版本圆了几个月前画的饼, 在大文件下载的内存占用方面做出了巨大改进
|
||||
|
||||
@@ -1 +1 @@
|
||||
25w04c
|
||||
25w13b
|
||||
45
README.md
45
README.md
@@ -1,6 +1,7 @@
|
||||
# GHProxy
|
||||
|
||||

|
||||

|
||||
[](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
|
||||
|
||||
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
|
||||
@@ -15,14 +16,12 @@
|
||||
|
||||
### 项目特点
|
||||
|
||||
- 基于Go语言实现,使用[Gin框架](https://github.com/gin-gonic/gin)与[req库](https://github.com/imroc/req)]
|
||||
- 基于Go语言实现,使用[Gin框架](https://github.com/gin-gonic/gin)
|
||||
- 支持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)模板构建,具有标准化的日志记录与构建流程
|
||||
|
||||
### 项目开发过程
|
||||
@@ -30,6 +29,7 @@
|
||||
**本项目是[WJQSERVER-STUDIO/ghproxy-go](https://github.com/WJQSERVER-STUDIO/ghproxy-go)的重构版本,实现了原项目原定功能的同时,进一步优化了性能**
|
||||
关于此项目的详细开发过程,请参看Commit记录与[CHANGELOG.md](https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/CHANGELOG.md)
|
||||
|
||||
- V2.0.0 对`proxy`核心模块进行了重构,大幅优化内存占用
|
||||
- V1.0.0 迁移至本仓库,并再次重构内容实现
|
||||
- v0.2.0 重构项目实现
|
||||
|
||||
@@ -42,8 +42,10 @@
|
||||
## 使用示例
|
||||
|
||||
```
|
||||
# 下载文件
|
||||
https://ghproxy.1888866.xyz/raw.githubusercontent.com/WJQSERVER-STUDIO/tools-stable/main/tools-stable-ghproxy.sh
|
||||
|
||||
# 克隆仓库
|
||||
git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
|
||||
```
|
||||
|
||||
@@ -54,7 +56,7 @@ git clone https://ghproxy.1888866.xyz/github.com/WJQSERVER-STUDIO/ghproxy.git
|
||||
- Docker-cli
|
||||
|
||||
```
|
||||
docker run -p 7210:80 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log -v ./ghproxy/config:/data/ghproxy/config --restart always wjqserver/ghproxy
|
||||
docker run -p 7210:8080 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/caddy:/data/caddy/log -v ./ghproxy/config:/data/ghproxy/config --restart always wjqserver/ghproxy
|
||||
```
|
||||
|
||||
- Docker-Compose (建议使用)
|
||||
@@ -69,6 +71,12 @@ docker run -p 7210:80 -v ./ghproxy/log/run:/data/ghproxy/log -v ./ghproxy/log/ca
|
||||
wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/install.sh && chmod +x install.sh &&./install.sh
|
||||
```
|
||||
|
||||
Dev一键部署脚本:
|
||||
|
||||
```bash
|
||||
wget -O install-dev.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/install-dev.sh && chmod +x install-dev.sh && ./install-dev.sh
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 外部配置文件
|
||||
@@ -78,10 +86,9 @@ wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/ma
|
||||
|
||||
```toml
|
||||
[server]
|
||||
host = "127.0.0.1" # 监听地址
|
||||
host = "0.0.0.0" # 监听地址
|
||||
port = 8080 # 监听端口
|
||||
sizeLimit = 125 # 125MB
|
||||
bufferSize = 4096 # Bytes 缓冲区大小
|
||||
enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off
|
||||
|
||||
[pages]
|
||||
@@ -91,6 +98,7 @@ staticPath = "/data/www" # 静态页面文件路径
|
||||
[log]
|
||||
logFilePath = "/data/ghproxy/log/ghproxy.log" # 日志文件路径
|
||||
maxLogSize = 5 # MB 日志文件最大大小
|
||||
level = "info" # 日志级别 dump, debug, info, warn, error, none
|
||||
|
||||
[cors]
|
||||
enabled = true # 是否开启跨域
|
||||
@@ -110,9 +118,13 @@ whitelistFile = "/data/ghproxy/config/whitelist.json" # 白名单文件路径
|
||||
|
||||
[rateLimit]
|
||||
enabled = false # 是否开启速率限制
|
||||
rateMrthod = "total" # "ip" or "total" 速率限制方式
|
||||
rateMethod = "total" # "ip" or "total" 速率限制方式
|
||||
ratePerMinute = 180 # 每分钟限制请求数量
|
||||
burst = 5 # 突发请求数量
|
||||
|
||||
[outbound]
|
||||
enabled = false # 是否使用自定义代理出站
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890" 支持Socks5/HTTP(S)出站传输
|
||||
```
|
||||
|
||||
### 黑名单配置
|
||||
@@ -149,10 +161,6 @@ burst = 5 # 突发请求数量
|
||||
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
|
||||
}
|
||||
@@ -163,9 +171,14 @@ example.com {
|
||||

|
||||

|
||||
|
||||
结语
|
||||
---
|
||||
## 赞助
|
||||
|
||||
本项目基于Go语言实现,使用Gin框架与req库
|
||||
Docker镜像基于[WJQSERVER-STUDIO/caddy](https://github.com/WJQSERVER-STUDIO/caddy)
|
||||
本项目使用WSL LICENSE Version1.2 (WJQSERVER STUDIO LICENSE Version1.2) 授权协议,请遵守相关条例。
|
||||
如果您觉得本项目对您有帮助,欢迎赞助支持,您的赞助将用于Demo服务器开支及开发者时间成本支出,感谢您的支持!
|
||||
|
||||
为爱发电,开源不易
|
||||
|
||||
爱发电: https://afdian.com/a/wjqserver
|
||||
|
||||
### 捐赠列表
|
||||
|
||||
虚位以待...
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
| 版本 | 是否支持 |
|
||||
| --- | --- |
|
||||
| v1.x.x | :white_check_mark: |
|
||||
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
|
||||
| v2.x.x | :white_check_mark: 当前最新版本序列, 受支持 |
|
||||
| v1.x.x | :x: 这些版本已结束生命周期,不再受支持 |
|
||||
| 25w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 |
|
||||
| 24w*a/b/c... | :warning: 此为PRE-RELEASE版本,用于开发与测试,可能存在未知的问题 生命周期已完全结束 |
|
||||
| v0.x.x | :x: 这些版本不再受支持 |
|
||||
|
||||
### 用户须知
|
||||
|
||||
14
api/api.go
14
api/api.go
@@ -15,13 +15,25 @@ var (
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
LogDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
func NoCacheMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 设置禁止缓存的响应头
|
||||
c.Header("Cache-Control", "no-store, no-cache, must-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
c.Next() // 继续处理请求
|
||||
}
|
||||
}
|
||||
|
||||
func InitHandleRouter(cfg *config.Config, router *gin.Engine, version string) {
|
||||
apiRouter := router.Group("api")
|
||||
apiRouter := router.Group("api", NoCacheMiddleware())
|
||||
{
|
||||
apiRouter.GET("/size_limit", func(c *gin.Context) {
|
||||
SizeLimitHandler(cfg, c)
|
||||
|
||||
@@ -13,7 +13,7 @@ func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err st
|
||||
}
|
||||
// 获取"GH-Auth"的值
|
||||
authToken := c.GetHeader("GH-Auth")
|
||||
logInfo("%s %s %s %s %s AUTH_TOKEN: %s", c.Request.Method, c.Request.Host, c.Request.URL.Path, c.Request.Proto, c.Request.RemoteAddr, authToken)
|
||||
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.Request.Method, c.Request.Host, c.Request.URL.Path, c.Request.Proto, c.Request.RemoteAddr, authToken)
|
||||
if authToken == "" {
|
||||
err := "Auth Header == nil"
|
||||
return false, err
|
||||
@@ -25,6 +25,5 @@ func AuthHeaderHandler(c *gin.Context, cfg *config.Config) (isValid bool, err st
|
||||
return false, err
|
||||
}
|
||||
|
||||
logInfo("auth SUCCESS: %t", isValid)
|
||||
return isValid, ""
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, er
|
||||
}
|
||||
|
||||
authToken := c.Query("auth_token")
|
||||
logInfo("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto, authToken)
|
||||
logDebug("%s %s %s %s %s AUTH_TOKEN: %s", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Request.Proto, authToken)
|
||||
|
||||
if authToken == "" {
|
||||
err := "Auth token == nil"
|
||||
@@ -26,6 +26,5 @@ func AuthParametersHandler(c *gin.Context, cfg *config.Config) (isValid bool, er
|
||||
return false, err
|
||||
}
|
||||
|
||||
logInfo("auth SUCCESS: %t", isValid)
|
||||
return isValid, ""
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
LogDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
@@ -21,7 +23,7 @@ func Init(cfg *config.Config) {
|
||||
if cfg.Whitelist.Enabled {
|
||||
LoadWhitelist(cfg)
|
||||
}
|
||||
logInfo("Auth Init")
|
||||
logDebug("Auth Init")
|
||||
}
|
||||
|
||||
func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string) {
|
||||
@@ -32,10 +34,10 @@ func AuthHandler(c *gin.Context, cfg *config.Config) (isValid bool, err string)
|
||||
isValid, err = AuthHeaderHandler(c, cfg)
|
||||
return isValid, err
|
||||
} else if cfg.Auth.AuthMethod == "" {
|
||||
logWarning("Auth method not set")
|
||||
logError("Auth method not set")
|
||||
return true, ""
|
||||
} else {
|
||||
logWarning("Auth method not supported")
|
||||
logError("Auth method not supported")
|
||||
return false, "Auth method not supported"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
{
|
||||
debug
|
||||
http_port 80
|
||||
https_port 443
|
||||
order cache before rewrite
|
||||
cache {
|
||||
cache_name GHProxyCache
|
||||
}
|
||||
log {
|
||||
level INFO
|
||||
output file /data/caddy/log/caddy.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
}
|
||||
}
|
||||
servers :80 {
|
||||
protocols h1 h2c
|
||||
}
|
||||
}
|
||||
|
||||
(log) {
|
||||
log {
|
||||
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
|
||||
time_format "02/Jan/2006:15:04:05 -0700"
|
||||
}
|
||||
output file /data/caddy/log/{args[0]}/access.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
roll_keep_for 24h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(error_page) {
|
||||
handle_errors {
|
||||
rewrite * /{err.status_code}.html
|
||||
root * /data/caddy/pages/errors
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
(encode) {
|
||||
encode {
|
||||
zstd best
|
||||
br 5 v2
|
||||
gzip 5
|
||||
minimum_length 512
|
||||
}
|
||||
}
|
||||
|
||||
(cache) {
|
||||
cache {
|
||||
allowed_http_verbs GET
|
||||
stale {args[0]}
|
||||
ttl {args[1]}
|
||||
}
|
||||
}
|
||||
|
||||
(header_realip) {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
|
||||
}
|
||||
|
||||
(rate_limit) {
|
||||
route /* {
|
||||
rate_limit {remote.ip} {args[0]}r/m 10000 429
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
reverse_proxy {
|
||||
to 127.0.0.1:8080
|
||||
import header_realip
|
||||
transport http {
|
||||
versions 1.1 h2c
|
||||
}
|
||||
}
|
||||
import log ghproxy
|
||||
import cache 0s 300s
|
||||
import error_page
|
||||
import encode
|
||||
import rate_limit 60
|
||||
route / {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
}
|
||||
route /favicon.ico {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
}
|
||||
|
||||
route /api* {
|
||||
rate_limit {remote.ip} 15r/m 10000 429
|
||||
import cache 0s 6h
|
||||
}
|
||||
}
|
||||
|
||||
import /data/caddy/config.d/*
|
||||
@@ -1,99 +0,0 @@
|
||||
{
|
||||
debug
|
||||
http_port 80
|
||||
https_port 443
|
||||
order cache before rewrite
|
||||
cache {
|
||||
cache_name GHProxyCache
|
||||
}
|
||||
log {
|
||||
level INFO
|
||||
output file /data/caddy/log/caddy.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
}
|
||||
}
|
||||
servers :80 {
|
||||
protocols h1 h2c
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(log) {
|
||||
log {
|
||||
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
|
||||
time_format "02/Jan/2006:15:04:05 -0700"
|
||||
}
|
||||
output file /data/caddy/log/{args[0]}/access.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
roll_keep_for 24h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(error_page) {
|
||||
handle_errors {
|
||||
rewrite * /{err.status_code}.html
|
||||
root * /data/caddy/pages/errors
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
(encode) {
|
||||
encode {
|
||||
zstd best
|
||||
br 5 v2
|
||||
gzip 5
|
||||
minimum_length 256
|
||||
}
|
||||
}
|
||||
|
||||
(cache) {
|
||||
cache {
|
||||
allowed_http_verbs GET
|
||||
stale {args[0]}
|
||||
ttl {args[1]}
|
||||
}
|
||||
}
|
||||
|
||||
(header_realip) {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
|
||||
}
|
||||
|
||||
(rate_limit) {
|
||||
route /* {
|
||||
rate_limit {remote.ip} {args[0]}r/m 10000 429
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
reverse_proxy {
|
||||
to h2c://127.0.0.1:8080
|
||||
import header_realip
|
||||
}
|
||||
import log ghproxy
|
||||
import error_page
|
||||
import encode
|
||||
import rate_limit 60
|
||||
route / {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 300s
|
||||
}
|
||||
route /favicon.ico {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 300s
|
||||
}
|
||||
|
||||
route /api* {
|
||||
rate_limit {remote.ip} 15r/m 10000 429
|
||||
import cache 300s
|
||||
}
|
||||
}
|
||||
|
||||
import /data/caddy/config.d/*
|
||||
@@ -1,103 +0,0 @@
|
||||
{
|
||||
debug
|
||||
http_port 80
|
||||
https_port 443
|
||||
order cache before rewrite
|
||||
cache {
|
||||
cache_name GHProxyCache
|
||||
}
|
||||
log {
|
||||
level INFO
|
||||
output file /data/caddy/log/caddy.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
}
|
||||
}
|
||||
servers :80 {
|
||||
protocols h1 h2c
|
||||
}
|
||||
}
|
||||
|
||||
(log) {
|
||||
log {
|
||||
format transform `{request>headers>X-Forwarded-For>[0]:request>remote_ip} - {user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
|
||||
time_format "02/Jan/2006:15:04:05 -0700"
|
||||
}
|
||||
output file /data/caddy/log/{args[0]}/access.log {
|
||||
roll_size 5MB
|
||||
roll_keep 10
|
||||
roll_keep_for 24h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(error_page) {
|
||||
handle_errors {
|
||||
rewrite * /{err.status_code}.html
|
||||
root * /data/caddy/pages/errors
|
||||
file_server
|
||||
}
|
||||
}
|
||||
|
||||
(encode) {
|
||||
encode {
|
||||
zstd best
|
||||
br 5 v2
|
||||
gzip 5
|
||||
minimum_length 512
|
||||
}
|
||||
}
|
||||
|
||||
(cache) {
|
||||
cache {
|
||||
allowed_http_verbs GET
|
||||
stale {args[0]}
|
||||
ttl {args[1]}
|
||||
}
|
||||
}
|
||||
|
||||
(header_realip) {
|
||||
header_up X-Real-IP {remote_host}
|
||||
header_up X-Real-IP {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
|
||||
header_up X-Forwarded-Proto {http.request.header.CF-Visitor}
|
||||
}
|
||||
|
||||
(rate_limit) {
|
||||
route /* {
|
||||
rate_limit {remote.ip} {args[0]}r/m 10000 429
|
||||
}
|
||||
}
|
||||
|
||||
:80 {
|
||||
reverse_proxy {
|
||||
to 127.0.0.1:8080
|
||||
import header_realip
|
||||
transport http {
|
||||
versions 1.1 h2c
|
||||
}
|
||||
}
|
||||
import log ghproxy
|
||||
import cache 0s 300s
|
||||
import error_page
|
||||
import encode
|
||||
import rate_limit 60
|
||||
route / {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
}
|
||||
route /favicon.ico {
|
||||
root /data/www
|
||||
file_server
|
||||
import cache 0s 24h
|
||||
|
||||
}
|
||||
|
||||
route /api* {
|
||||
rate_limit {remote.ip} 15r/m 10000 429
|
||||
import cache 0s 6h
|
||||
}
|
||||
}
|
||||
|
||||
import /data/caddy/config.d/*
|
||||
@@ -13,15 +13,15 @@ type Config struct {
|
||||
Blacklist BlacklistConfig
|
||||
Whitelist WhitelistConfig
|
||||
RateLimit RateLimitConfig
|
||||
Outbound OutboundConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port int `toml:"port"`
|
||||
Host string `toml:"host"`
|
||||
SizeLimit int `toml:"sizeLimit"`
|
||||
EnableH2C string `toml:"enableH2C"`
|
||||
BufferSize int `toml:"bufferSize"`
|
||||
Debug bool `toml:"debug"`
|
||||
Port int `toml:"port"`
|
||||
Host string `toml:"host"`
|
||||
SizeLimit int `toml:"sizeLimit"`
|
||||
EnableH2C string `toml:"enableH2C"`
|
||||
Debug bool `toml:"debug"`
|
||||
}
|
||||
|
||||
type PagesConfig struct {
|
||||
@@ -32,6 +32,7 @@ type PagesConfig struct {
|
||||
type LogConfig struct {
|
||||
LogFilePath string `toml:"logFilePath"`
|
||||
MaxLogSize int `toml:"maxLogSize"`
|
||||
Level string `toml:"level"`
|
||||
}
|
||||
|
||||
type CORSConfig struct {
|
||||
@@ -62,6 +63,16 @@ type RateLimitConfig struct {
|
||||
Burst int `toml:"burst"`
|
||||
}
|
||||
|
||||
/*
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
*/
|
||||
type OutboundConfig struct {
|
||||
Enabled bool `toml:"enabled"`
|
||||
Url string `toml:"url"`
|
||||
}
|
||||
|
||||
// LoadConfig 从 TOML 配置文件加载配置
|
||||
func LoadConfig(filePath string) (*Config, error) {
|
||||
var config Config
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
host = "0.0.0.0"
|
||||
port = 8080
|
||||
sizeLimit = 125 # MB
|
||||
enableH2C = "on" # "on" or "off"
|
||||
bufferSize = 4096 # Bytes
|
||||
debug = false
|
||||
|
||||
[pages]
|
||||
@@ -13,6 +12,7 @@ staticDir = "/data/www"
|
||||
[log]
|
||||
logFilePath = "/data/ghproxy/log/ghproxy.log"
|
||||
maxLogSize = 5 # MB
|
||||
level = "info" # dump, debug, info, warn, error, none
|
||||
|
||||
[cors]
|
||||
enabled = true
|
||||
@@ -36,3 +36,7 @@ enabled = false
|
||||
rateMethod = "total" # "ip" or "total"
|
||||
ratePerMinute = 180
|
||||
burst = 5
|
||||
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
@@ -2,8 +2,7 @@
|
||||
host = "127.0.0.1"
|
||||
port = 8080
|
||||
sizeLimit = 125 # MB
|
||||
bufferSize = 4096 # Bytes
|
||||
enableH2C = false
|
||||
enableH2C = "on"
|
||||
debug = false
|
||||
|
||||
[pages]
|
||||
@@ -13,6 +12,7 @@ staticDir = "/usr/local/ghproxy/pages"
|
||||
[log]
|
||||
logFilePath = "/usr/local/ghproxy/log/ghproxy.log"
|
||||
maxLogSize = 5 # MB
|
||||
level = "info" # dump, debug, info, warn, error, none
|
||||
|
||||
[cors]
|
||||
enabled = true
|
||||
@@ -36,3 +36,7 @@ enabled = false
|
||||
rateMethod = "total" # "ip" or "total"
|
||||
ratePerMinute = 180
|
||||
burst = 5
|
||||
|
||||
[outbound]
|
||||
enabled = false
|
||||
url = "socks5://127.0.0.1:1080" # "http://127.0.0.1:7890"
|
||||
|
||||
@@ -73,8 +73,51 @@ if [ -z "$ghproxy_dir" ]; then
|
||||
ghproxy_dir="/usr/local/ghproxy"
|
||||
fi
|
||||
|
||||
make_systemd_service() {
|
||||
# 创建目录
|
||||
mkdir -p ${ghproxy_dir}
|
||||
mkdir -p ${ghproxy_dir}/config
|
||||
mkdir -p ${ghproxy_dir}/log
|
||||
mkdir -p ${ghproxy_dir}/pages
|
||||
|
||||
# 获取最新版本号
|
||||
VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/DEV-VERSION)
|
||||
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/DEV-VERSION
|
||||
|
||||
# 下载ghproxy
|
||||
wget -q -O ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH.tar.gz
|
||||
install tar
|
||||
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
|
||||
chmod +x ${ghproxy_dir}/ghproxy
|
||||
|
||||
# 下载pages
|
||||
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/pages/index.html
|
||||
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/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/dev/deploy/config.toml
|
||||
fi
|
||||
|
||||
# 替换 port = 8080
|
||||
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
|
||||
|
||||
# 下载systemd服务文件
|
||||
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
|
||||
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/dev/deploy/ghproxy.service
|
||||
else
|
||||
|
||||
cat <<EOF > /etc/systemd/system/ghproxy.service
|
||||
|
||||
[Unit]
|
||||
Description=Github Proxy Service
|
||||
After=network.target
|
||||
@@ -91,51 +134,6 @@ WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
|
||||
}
|
||||
|
||||
# 创建目录
|
||||
mkdir -p ${ghproxy_dir}
|
||||
mkdir -p ${ghproxy_dir}/config
|
||||
mkdir -p ${ghproxy_dir}/log
|
||||
mkdir -p ${ghproxy_dir}/pages
|
||||
|
||||
# 获取最新版本号
|
||||
VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/DEV-VERSION)
|
||||
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/DEV-VERSION
|
||||
|
||||
# 下载ghproxy
|
||||
wget -q -O ${ghproxy_dir}/ghproxy https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/$VERSION/ghproxy-linux-$ARCH.tar.gz
|
||||
install tar
|
||||
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
|
||||
chmod +x ${ghproxy_dir}/ghproxy
|
||||
|
||||
# 下载pages
|
||||
wget -q -O ${ghproxy_dir}/pages/index.html https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/index.html
|
||||
wget -q -O ${ghproxy_dir}/pages/favicon.ico https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/pages/favicon.ico
|
||||
|
||||
|
||||
# 下载配置文件
|
||||
if [ -f ${ghproxy_dir}/config/config.toml ]; then
|
||||
echo "配置文件已存在, 跳过下载"
|
||||
echo "[WARNING] 请检查配置文件是否正确,DEV版本升级时请注意配置文件兼容性"
|
||||
sleep 2
|
||||
else
|
||||
wget -q -O ${ghproxy_dir}/config/config.toml https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/config.toml
|
||||
fi
|
||||
|
||||
# 替换 port = 8080
|
||||
sed -i "s/port = 8080/port = $PORT/g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i 's/host = "127.0.0.1"/host = "'"$IP"'"/g' ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|staticDir = \"/usr/local/ghproxy/pages\"|staticDir = \"${ghproxy_dir}/pages\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|logFilePath = \"/usr/local/ghproxy/log/ghproxy.log\"|logFilePath = \"${ghproxy_dir}/log/ghproxy.log\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|blacklistFile = \"/usr/local/ghproxy/config/blacklist.json\"|blacklistFile = \"${ghproxy_dir}/config/blacklist.json\"|g" ${ghproxy_dir}/config/config.toml
|
||||
sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelistFile = \"${ghproxy_dir}/config/whitelist.json\"|g" ${ghproxy_dir}/config/config.toml
|
||||
|
||||
# 下载systemd服务文件
|
||||
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
|
||||
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
|
||||
else
|
||||
make_systemd_service()
|
||||
fi
|
||||
|
||||
# 启动ghproxy
|
||||
|
||||
@@ -73,26 +73,6 @@ if [ -z "$ghproxy_dir" ]; then
|
||||
ghproxy_dir="/usr/local/ghproxy"
|
||||
fi
|
||||
|
||||
make_systemd_service() {
|
||||
cat <<EOF > /etc/systemd/system/ghproxy.service
|
||||
[Unit]
|
||||
Description=Github Proxy Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
|
||||
WorkingDirectory=$ghproxy_dir
|
||||
Restart=always
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
|
||||
}
|
||||
|
||||
# 创建目录
|
||||
mkdir -p ${ghproxy_dir}
|
||||
mkdir -p ${ghproxy_dir}/config
|
||||
@@ -104,7 +84,7 @@ VERSION=$(curl -s https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/mai
|
||||
wget -q -O ${ghproxy_dir}/VERSION https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/VERSION
|
||||
|
||||
# 下载ghproxy
|
||||
wget -q -O ${ghproxy_dir}/ghproxy https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/${VERSION}/ghproxy-linux-${ARCH}.tar.gz
|
||||
wget -q -O ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz https://github.com/WJQSERVER-STUDIO/ghproxy/releases/download/${VERSION}/ghproxy-linux-${ARCH}.tar.gz
|
||||
install tar
|
||||
tar -zxvf ${ghproxy_dir}/ghproxy-linux-$ARCH.tar.gz -C ${ghproxy_dir}
|
||||
chmod +x ${ghproxy_dir}/ghproxy
|
||||
@@ -135,7 +115,25 @@ sed -i "s|whitelistFile = \"/usr/local/ghproxy/config/whitelist.json\"|whitelist
|
||||
if [ "$ghproxy_dir" = "/usr/local/ghproxy" ]; then
|
||||
wget -q -O /etc/systemd/system/ghproxy.service https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/main/deploy/ghproxy.service
|
||||
else
|
||||
make_systemd_service()
|
||||
|
||||
cat <<EOF > /etc/systemd/system/ghproxy.service
|
||||
|
||||
[Unit]
|
||||
Description=Github Proxy Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/bin/bash -c '$ghproxy_dir/ghproxy -cfg $ghproxy_dir/config/config.toml > $ghproxy_dir/log/run.log 2>&1'
|
||||
WorkingDirectory=$ghproxy_dir
|
||||
Restart=always
|
||||
User=root
|
||||
Group=root
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
EOF
|
||||
|
||||
fi
|
||||
|
||||
# 启动ghproxy
|
||||
|
||||
@@ -3,15 +3,9 @@ services:
|
||||
ghproxy:
|
||||
image: 'wjqserver/ghproxy:latest'
|
||||
restart: always
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:80/api/healthcheck"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
volumes:
|
||||
- './ghproxy/log/run:/data/ghproxy/log'
|
||||
- './ghproxy/log/caddy:/data/caddy/log'
|
||||
- './ghproxy/config:/data/ghproxy/config'
|
||||
ports:
|
||||
- '7210:80'
|
||||
- '7210:8080'
|
||||
|
||||
@@ -14,12 +14,4 @@ if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
|
||||
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
|
||||
fi
|
||||
|
||||
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1 &
|
||||
|
||||
sleep 30
|
||||
|
||||
while [[ true ]]; do
|
||||
# Failure Circuit Breaker
|
||||
curl -f --max-time 5 -retry 3 http://127.0.0.1:8080/api/healthcheck || exit 1
|
||||
sleep 120
|
||||
done
|
||||
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
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
|
||||
@@ -18,14 +14,4 @@ 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 &
|
||||
|
||||
sleep 30
|
||||
|
||||
while [[ true ]]; do
|
||||
# Failure Circuit Breaker
|
||||
curl -f --max-time 5 -retry 3 http://127.0.0.1:8080/api/healthcheck || exit 1
|
||||
sleep 120
|
||||
done
|
||||
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.log 2>&1
|
||||
|
||||
22
go.mod
22
go.mod
@@ -1,16 +1,17 @@
|
||||
module ghproxy
|
||||
|
||||
go 1.23.5
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.4.0
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.0
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.3.0
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
golang.org/x/time v0.9.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/time v0.10.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.12.7 // indirect
|
||||
github.com/bytedance/sonic v1.12.8 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
@@ -18,7 +19,7 @@ require (
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.24.0 // indirect
|
||||
github.com/goccy/go-json v0.10.4 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
@@ -29,11 +30,10 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.13.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
golang.org/x/arch v0.14.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
38
go.sum
38
go.sum
@@ -1,9 +1,9 @@
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.0 h1:OUrAOWb8xK0kxpWextJYUasmol+5KKqG2az52X2ae64=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.0/go.mod h1:sAqHVYSucoUnycyHMAZc1fMH5dS2bQwgwo8NUiAIcyk=
|
||||
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q=
|
||||
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.3.0 h1:rOvutC4zYfvtSGN2CNZrycjtq8dLpfu7ypy7tTEErPY=
|
||||
github.com/WJQSERVER-STUDIO/go-utils/logger v1.3.0/go.mod h1:oW884JCCPDU6c906LI0uKXndWLiRvjb9LkGYC2cqRO8=
|
||||
github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
|
||||
github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
|
||||
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
@@ -27,8 +27,8 @@ 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.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
|
||||
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||
github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
|
||||
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@@ -67,21 +67,23 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
|
||||
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/arch v0.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
|
||||
golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
15
init.sh
15
init.sh
@@ -2,10 +2,6 @@
|
||||
|
||||
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
|
||||
@@ -18,14 +14,5 @@ if [ ! -f /data/${APPLICATON}/config/config.yaml ]; then
|
||||
cp /data/${APPLICATON}/config.yaml /data/${APPLICATON}/config/config.yaml
|
||||
fi
|
||||
|
||||
/data/caddy/caddy run --config /data/caddy/config/Caddyfile > /data/${APPLICATON}/log/caddy.log 2>&1 &
|
||||
|
||||
/data/${APPLICATON}/${APPLICATON} > /data/${APPLICATON}/log/run.log 2>&1 &
|
||||
|
||||
sleep 30
|
||||
|
||||
while [[ true ]]; do
|
||||
curl -f http://localhost:8080/api/healthcheck || exit 1
|
||||
sleep 120
|
||||
done
|
||||
/data/${APPLICATON}/${APPLICATON} > /data/${APPLICATON}/log/run.log 2>&1
|
||||
|
||||
|
||||
34
loggin/loggin.go
Normal file
34
loggin/loggin.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package loggin
|
||||
|
||||
import (
|
||||
"ghproxy/timing"
|
||||
"time"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
LogDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
)
|
||||
|
||||
// 日志中间件
|
||||
func Middleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
var timingResults time.Duration
|
||||
|
||||
// 获取计时结果
|
||||
timingResults, _ = timing.Get(c)
|
||||
|
||||
// 记录日志 IP METHOD URL USERAGENT PROTOCOL STATUS TIMING
|
||||
logInfo("%s %s %s %s %d %s ", c.ClientIP(), c.Request.Method, c.Request.URL.Path, c.Request.UserAgent(), c.Writer.Status(), timingResults)
|
||||
}
|
||||
}
|
||||
46
main.go
46
main.go
@@ -6,15 +6,16 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ghproxy/api"
|
||||
"ghproxy/auth"
|
||||
"ghproxy/config"
|
||||
"ghproxy/loggin"
|
||||
"ghproxy/proxy"
|
||||
"ghproxy/rate"
|
||||
"ghproxy/timing"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
|
||||
@@ -40,6 +41,8 @@ var (
|
||||
|
||||
var (
|
||||
logw = logger.Logw
|
||||
LogDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
@@ -53,20 +56,27 @@ func loadConfig() {
|
||||
var err error
|
||||
cfg, err = config.LoadConfig(cfgfile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
fmt.Printf("Failed to load config: %v\n", err)
|
||||
}
|
||||
if cfg.Server.Debug {
|
||||
fmt.Println("Config File Path: ", cfgfile)
|
||||
fmt.Printf("Loaded config: %v\n", cfg)
|
||||
}
|
||||
fmt.Println("Config File Path: ", cfgfile)
|
||||
fmt.Printf("Loaded config: %v\n", cfg)
|
||||
}
|
||||
|
||||
func setupLogger(cfg *config.Config) {
|
||||
var err error
|
||||
err = logger.Init(cfg.Log.LogFilePath, cfg.Log.MaxLogSize)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize logger: %v", err)
|
||||
fmt.Printf("Failed to initialize logger: %v\n", err)
|
||||
}
|
||||
logInfo("Config File Path: ", cfgfile)
|
||||
logInfo("Loaded config: %v\n", cfg)
|
||||
err = logger.SetLogLevel(cfg.Log.Level)
|
||||
if err != nil {
|
||||
fmt.Printf("Logger Level Error: %v\n", err)
|
||||
}
|
||||
fmt.Printf("Log Level: %s\n", cfg.Log.Level)
|
||||
logDebug("Config File Path: ", cfgfile)
|
||||
logDebug("Loaded config: %v\n", cfg)
|
||||
logInfo("Init Completed")
|
||||
}
|
||||
|
||||
@@ -87,12 +97,11 @@ func setupRateLimit(cfg *config.Config) {
|
||||
} else {
|
||||
logError("Invalid RateLimit Method: %s", cfg.RateLimit.RateMethod)
|
||||
}
|
||||
logInfo("Rate Limit Loaded")
|
||||
}
|
||||
}
|
||||
|
||||
func initBufferSize() {
|
||||
proxy.InitChunkedBufferSize(cfg.Server.BufferSize)
|
||||
func InitReq(cfg *config.Config) {
|
||||
proxy.InitReq(cfg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -100,7 +109,7 @@ func init() {
|
||||
flag.Parse()
|
||||
loadConfig()
|
||||
setupLogger(cfg)
|
||||
initBufferSize()
|
||||
InitReq(cfg)
|
||||
loadlist(cfg)
|
||||
setupRateLimit(cfg)
|
||||
|
||||
@@ -116,9 +125,20 @@ func init() {
|
||||
runMode = "release"
|
||||
}
|
||||
|
||||
logDebug("Run Mode: %s", runMode)
|
||||
|
||||
gin.LoggerWithWriter(io.Discard)
|
||||
router = gin.New()
|
||||
|
||||
// 添加recovery中间件
|
||||
router.Use(gin.Recovery())
|
||||
|
||||
// 添加log中间件
|
||||
router.Use(loggin.Middleware())
|
||||
|
||||
// 添加计时中间件
|
||||
router.Use(timing.Middleware())
|
||||
|
||||
//H2C默认值为true,而后遵循cfg.Server.EnableH2C的设置
|
||||
if cfg.Server.EnableH2C == "on" {
|
||||
router.UseH2C = true
|
||||
@@ -141,7 +161,7 @@ func init() {
|
||||
} else if !cfg.Pages.Enabled {
|
||||
pages, err := fs.Sub(pagesFS, "pages")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed when processing pages: %s", err)
|
||||
logError("Failed when processing pages: %s", err)
|
||||
}
|
||||
router.GET("/", gin.WrapH(http.FileServer(http.FS(pages))))
|
||||
router.GET("/favicon.ico", gin.WrapH(http.FileServer(http.FS(pages))))
|
||||
@@ -152,6 +172,8 @@ func init() {
|
||||
})
|
||||
|
||||
fmt.Printf("GHProxy Version: %s\n", version)
|
||||
fmt.Printf("A Go Based High-Performance Github Proxy \n")
|
||||
fmt.Printf("Made by WJQSERVER-STUDIO\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@100..900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/* 通用样式 */
|
||||
:root {
|
||||
@@ -40,7 +39,7 @@
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
font-family: "Noto Sans SC", sans-serif;
|
||||
font-family: sans-serif;
|
||||
line-height: 1.8;
|
||||
font-size: 15px;
|
||||
margin: 0;
|
||||
@@ -370,17 +369,17 @@
|
||||
let formattedLink = "";
|
||||
|
||||
if (githubLink.startsWith("https://github.com/") || githubLink.startsWith("http://github.com/")) {
|
||||
formattedLink = "https://" + currentHost + "/github.com" + githubLink.substring(githubLink.indexOf("/", 8));
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/github.com" + githubLink.substring(githubLink.indexOf("/", 8));
|
||||
} else if (githubLink.startsWith("github.com/")) {
|
||||
formattedLink = "https://" + currentHost + "/" + githubLink;
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
|
||||
} else if (githubLink.startsWith("https://raw.githubusercontent.com/") || githubLink.startsWith("http://raw.githubusercontent.com/")) {
|
||||
formattedLink = "https://" + currentHost + githubLink.substring(githubLink.indexOf("/", 7));
|
||||
formattedLink = window.location.protocol + "//" + currentHost + githubLink.substring(githubLink.indexOf("/", 7));
|
||||
} else if (githubLink.startsWith("raw.githubusercontent.com/")) {
|
||||
formattedLink = "https://" + currentHost + "/" + githubLink;
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
|
||||
} else if (githubLink.startsWith("https://gist.githubusercontent.com/") || githubLink.startsWith("http://gist.githubusercontent.com/")) {
|
||||
formattedLink = "https://" + currentHost + "/gist.github.com" + githubLink.substring(githubLink.indexOf("/", 18));
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/gist.github.com" + githubLink.substring(githubLink.indexOf("/", 18));
|
||||
} else if (githubLink.startsWith("gist.githubusercontent.com/")) {
|
||||
formattedLink = "https://" + currentHost + "/" + githubLink;
|
||||
formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
|
||||
} else {
|
||||
showToast('请输入有效的GitHub链接');
|
||||
return null;
|
||||
@@ -438,4 +437,4 @@
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -11,6 +11,7 @@ func AuthPassThrough(c *gin.Context, cfg *config.Config, req *http.Request) {
|
||||
if cfg.Auth.PassThrough {
|
||||
token := c.Query("token")
|
||||
if token != "" {
|
||||
logDebug("%s %s %s %s %s Auth-PassThrough: token %s", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, token)
|
||||
switch cfg.Auth.AuthMethod {
|
||||
case "parameters":
|
||||
if !cfg.Auth.Enabled {
|
||||
|
||||
@@ -5,49 +5,100 @@ import (
|
||||
"fmt"
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var chunkedBufferSize int
|
||||
var BufferSize int = 32 * 1024 // 32KB
|
||||
|
||||
func InitChunkedBufferSize(cfgBufferSize int) {
|
||||
if cfgBufferSize == 0 {
|
||||
chunkedBufferSize = 4096 // 默认缓冲区大小
|
||||
} else {
|
||||
chunkedBufferSize = cfgBufferSize
|
||||
var (
|
||||
cclient *http.Client
|
||||
ctr *http.Transport
|
||||
BufferPool *sync.Pool
|
||||
)
|
||||
|
||||
func InitReq(cfg *config.Config) {
|
||||
initChunkedHTTPClient(cfg)
|
||||
initGitHTTPClient(cfg)
|
||||
|
||||
// 初始化固定大小的缓存池
|
||||
BufferPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, BufferSize)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func initChunkedHTTPClient(cfg *config.Config) {
|
||||
ctr = &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxConnsPerHost: 60,
|
||||
IdleConnTimeout: 20 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
}
|
||||
if cfg.Outbound.Enabled {
|
||||
initTransport(cfg, ctr)
|
||||
}
|
||||
cclient = &http.Client{
|
||||
Transport: ctr,
|
||||
}
|
||||
}
|
||||
|
||||
func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
|
||||
method := c.Request.Method
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{}
|
||||
|
||||
// 发送HEAD请求, 预获取Content-Length
|
||||
headReq, err := http.NewRequest("HEAD", u, nil)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("创建HEAD请求失败: %v", err))
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
setRequestHeaders(c, headReq)
|
||||
removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
|
||||
AuthPassThrough(c, cfg, headReq)
|
||||
|
||||
headResp, err := client.Do(headReq)
|
||||
headResp, err := cclient.Do(headReq)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer headResp.Body.Close()
|
||||
//defer headResp.Body.Close()
|
||||
defer func(Body io.ReadCloser) {
|
||||
if err := Body.Close(); err != nil {
|
||||
logError("Failed to close response body: %v", err)
|
||||
}
|
||||
}(headResp.Body)
|
||||
|
||||
if err := HandleResponseSize(headResp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
contentLength := headResp.Header.Get("Content-Length")
|
||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > sizelimit {
|
||||
finalURL := headResp.Request.URL.String()
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if err := HandleResponseSize(headResp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
body, err := readRequestBody(c)
|
||||
if err != nil {
|
||||
HandleError(c, err.Error())
|
||||
@@ -59,51 +110,72 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri
|
||||
// 创建请求
|
||||
req, err := http.NewRequest(method, u, bodyReader)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("创建请求失败: %v", err))
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Transfer-Encoding", "chunked") // 确保设置分块传输编码
|
||||
setRequestHeaders(c, req)
|
||||
removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := cclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("发送请求失败: %v", err))
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := HandleResponseSize(resp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
/*
|
||||
if err := HandleResponseSize(resp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
contentLength = resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > sizelimit {
|
||||
finalURL := resp.Request.URL.String()
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
CopyResponseHeaders(resp, c, cfg)
|
||||
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)
|
||||
}
|
||||
|
||||
if cfg.CORS.Enabled {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", "")
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
if err := chunkedCopyResponseBody(c, resp.Body); err != nil {
|
||||
logError("%s %s %s %s %s 响应复制错误: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制响应体
|
||||
func chunkedCopyResponseBody(c *gin.Context, respBody io.Reader) error {
|
||||
buf := make([]byte, chunkedBufferSize)
|
||||
for {
|
||||
n, err := respBody.Read(buf)
|
||||
if n > 0 {
|
||||
if _, err := c.Writer.Write(buf[:n]); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Writer.Flush() // 确保每次写入后刷新
|
||||
}
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return err
|
||||
}
|
||||
// 使用固定32KB缓冲池
|
||||
buffer := BufferPool.Get().([]byte)
|
||||
defer BufferPool.Put(buffer)
|
||||
|
||||
_, err = io.CopyBuffer(c.Writer, resp.Body, buffer)
|
||||
if err != nil {
|
||||
logError("%s %s %s %s %s Failed to copy response body: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
} else {
|
||||
c.Writer.Flush() // 确保刷入
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
135
proxy/dial.go
Normal file
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)
|
||||
}
|
||||
109
proxy/gitreq.go
109
proxy/gitreq.go
@@ -6,36 +6,70 @@ import (
|
||||
"ghproxy/config"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
gclient *http.Client
|
||||
gtr *http.Transport
|
||||
)
|
||||
|
||||
func initGitHTTPClient(cfg *config.Config) {
|
||||
gtr = &http.Transport{
|
||||
MaxIdleConns: 30,
|
||||
MaxConnsPerHost: 30,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
}
|
||||
if cfg.Outbound.Enabled {
|
||||
initTransport(cfg, gtr)
|
||||
}
|
||||
gclient = &http.Client{
|
||||
Transport: gtr,
|
||||
}
|
||||
}
|
||||
|
||||
func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
|
||||
method := c.Request.Method
|
||||
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{}
|
||||
//client := &http.Client{}
|
||||
|
||||
// 发送HEAD请求, 预获取Content-Length
|
||||
headReq, err := http.NewRequest("HEAD", u, nil)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("创建HEAD请求失败: %v", err))
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
setRequestHeaders(c, headReq)
|
||||
AuthPassThrough(c, cfg, headReq)
|
||||
|
||||
headResp, err := client.Do(headReq)
|
||||
headResp, err := gclient.Do(headReq)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer headResp.Body.Close()
|
||||
|
||||
if err := HandleResponseSize(headResp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
// defer headResp.Body.Close()
|
||||
defer func(Body io.ReadCloser) {
|
||||
if err := Body.Close(); err != nil {
|
||||
logError("Failed to close response body: %v", err)
|
||||
}
|
||||
}(headResp.Body)
|
||||
|
||||
contentLength := headResp.Header.Get("Content-Length")
|
||||
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > sizelimit {
|
||||
finalURL := headResp.Request.URL.String()
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
body, err := readRequestBody(c)
|
||||
@@ -49,33 +83,66 @@ func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode s
|
||||
// 创建请求
|
||||
req, err := http.NewRequest(method, u, bodyReader)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("创建请求失败: %v", err))
|
||||
HandleError(c, fmt.Sprintf("Failed to create request: %v", err))
|
||||
return
|
||||
}
|
||||
setRequestHeaders(c, req)
|
||||
AuthPassThrough(c, cfg, req)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
resp, err := gclient.Do(req)
|
||||
if err != nil {
|
||||
HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
//defer resp.Body.Close()
|
||||
defer func(Body io.ReadCloser) {
|
||||
if err := Body.Close(); err != nil {
|
||||
logError("Failed to close response body: %v", err)
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
if err := HandleResponseSize(resp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
/*
|
||||
if err := HandleResponseSize(resp, cfg, c); err != nil {
|
||||
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
return
|
||||
}
|
||||
*/
|
||||
contentLength = resp.Header.Get("Content-Length")
|
||||
if contentLength != "" {
|
||||
size, err := strconv.Atoi(contentLength)
|
||||
if err == nil && size > sizelimit {
|
||||
finalURL := resp.Request.URL.String()
|
||||
c.Redirect(http.StatusMovedPermanently, finalURL)
|
||||
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
headersToRemove := map[string]struct{}{
|
||||
"Content-Security-Policy": {},
|
||||
"Referrer-Policy": {},
|
||||
"Strict-Transport-Security": {},
|
||||
}
|
||||
|
||||
for header := range headersToRemove {
|
||||
resp.Header.Del(header)
|
||||
}
|
||||
|
||||
if cfg.CORS.Enabled {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", "")
|
||||
}
|
||||
|
||||
CopyResponseHeaders(resp, c, cfg)
|
||||
c.Status(resp.StatusCode)
|
||||
if err := gitCopyResponseBody(c, resp.Body); err != nil {
|
||||
|
||||
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
||||
logError("%s %s %s %s %s Response-Copy-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制响应体
|
||||
func gitCopyResponseBody(c *gin.Context, respBody io.Reader) error {
|
||||
_, err := io.Copy(c.Writer, respBody)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *rate.IPRateLimiter, runMode string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
// 限制访问频率
|
||||
if cfg.RateLimit.Enabled {
|
||||
|
||||
@@ -54,6 +55,8 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
|
||||
username, repo := MatchUserRepo(rawPath, cfg, c, matches) // 匹配用户名和仓库名
|
||||
|
||||
logInfo("%s %s %s %s %s Matched-Username: %s, Matched-Repo: %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, username, repo)
|
||||
// dump log 记录详细信息 c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, full Header
|
||||
LogDump("%s %s %s %s %s %s", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, c.Request.Header)
|
||||
repouser := fmt.Sprintf("%s/%s", username, repo)
|
||||
|
||||
// 白名单检查
|
||||
@@ -83,7 +86,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
|
||||
matches = CheckURL(rawPath, c)
|
||||
if matches == nil {
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
logError("%s %s %s %s %s 404-NOMATCH", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
logWarning("%s %s %s %s %s 404-NOMATCH", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -91,7 +94,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
|
||||
if exps[5].MatchString(rawPath) {
|
||||
if cfg.Auth.AuthMethod != "header" || !cfg.Auth.Enabled {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "HeaderAuth is not enabled."})
|
||||
logWarning("%s %s %s %s %s HeaderAuth-Error: HeaderAuth is not enabled.", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
logError("%s %s %s %s %s HeaderAuth-Error: HeaderAuth is not enabled.", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -110,7 +113,7 @@ func NoRouteHandler(cfg *config.Config, limiter *rate.RateLimiter, iplimiter *ra
|
||||
}
|
||||
|
||||
// IP METHOD URL USERAGENT PROTO MATCHES
|
||||
logInfo("%s %s %s %s %s Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, matches)
|
||||
logDebug("%s %s %s %s %s Matches: %v", c.ClientIP(), c.Request.Method, rawPath, c.Request.Header.Get("User-Agent"), c.Request.Proto, matches)
|
||||
|
||||
switch {
|
||||
case exps[0].MatchString(rawPath), exps[1].MatchString(rawPath), exps[3].MatchString(rawPath), exps[4].MatchString(rawPath):
|
||||
|
||||
@@ -9,17 +9,19 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 预定义regex
|
||||
var (
|
||||
pathRegex = regexp.MustCompile(`^([^/]+)/([^/]+)/([^/]+)/.*`) // 匹配路径
|
||||
gistRegex = regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.*`) // 匹配gist路径
|
||||
)
|
||||
|
||||
// 提取用户名和仓库名
|
||||
func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches []string) (string, string) {
|
||||
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], ""
|
||||
if gistMatches := gistRegex.FindStringSubmatch(rawPath); len(gistMatches) == 3 {
|
||||
LogDump("%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]
|
||||
}
|
||||
|
||||
@@ -5,9 +5,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"ghproxy/config"
|
||||
|
||||
"github.com/WJQSERVER-STUDIO/go-utils/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -16,6 +13,8 @@ import (
|
||||
// 日志模块
|
||||
var (
|
||||
logw = logger.Logw
|
||||
LogDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
@@ -34,6 +33,7 @@ var exps = []*regexp.Regexp{
|
||||
func readRequestBody(c *gin.Context) ([]byte, error) {
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
logError("failed to read request body: %v", err)
|
||||
return nil, fmt.Errorf("failed to read request body: %v", err)
|
||||
}
|
||||
defer c.Request.Body.Close()
|
||||
@@ -60,6 +60,23 @@ func SendRequest(c *gin.Context, req *req.Request, method, url string) (*req.Res
|
||||
}
|
||||
*/
|
||||
|
||||
func HandleError(c *gin.Context, message string) {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
|
||||
logError(message)
|
||||
}
|
||||
|
||||
func CheckURL(u string, c *gin.Context) []string {
|
||||
for _, exp := range exps {
|
||||
if matches := exp.FindStringSubmatch(u); matches != nil {
|
||||
return matches[1:]
|
||||
}
|
||||
}
|
||||
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
logError(errMsg)
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
// 处理响应大小
|
||||
func HandleResponseSize(resp *http.Response, cfg *config.Config, c *gin.Context) error {
|
||||
contentLength := resp.Header.Get("Content-Length")
|
||||
@@ -75,19 +92,4 @@ func HandleResponseSize(resp *http.Response, cfg *config.Config, c *gin.Context)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleError(c *gin.Context, message string) {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
|
||||
logWarning(message)
|
||||
}
|
||||
|
||||
func CheckURL(u string, c *gin.Context) []string {
|
||||
for _, exp := range exps {
|
||||
if matches := exp.FindStringSubmatch(u); matches != nil {
|
||||
return matches[1:]
|
||||
}
|
||||
}
|
||||
errMsg := fmt.Sprintf("%s %s %s %s %s Invalid URL", c.ClientIP(), c.Request.Method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
|
||||
logWarning(errMsg)
|
||||
return nil
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -14,3 +14,8 @@ func setRequestHeaders(c *gin.Context, req *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeWSHeader(req *http.Request) {
|
||||
req.Header.Del("Upgrade")
|
||||
req.Header.Del("Connection")
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"ghproxy/config"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CopyResponseHeaders(resp *http.Response, c *gin.Context, cfg *config.Config) {
|
||||
|
||||
copyHeaders(resp, c)
|
||||
|
||||
removeHeaders(resp)
|
||||
|
||||
setCORSHeaders(c, cfg)
|
||||
|
||||
setDefaultHeaders(c)
|
||||
}
|
||||
|
||||
// 复制响应头
|
||||
func copyHeaders(resp *http.Response, c *gin.Context) {
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除指定响应头
|
||||
func removeHeaders(resp *http.Response) {
|
||||
headersToRemove := map[string]struct{}{
|
||||
"Content-Security-Policy": {},
|
||||
"Referrer-Policy": {},
|
||||
"Strict-Transport-Security": {},
|
||||
}
|
||||
|
||||
for header := range headersToRemove {
|
||||
resp.Header.Del(header)
|
||||
}
|
||||
}
|
||||
|
||||
// CORS配置
|
||||
func setCORSHeaders(c *gin.Context, cfg *config.Config) {
|
||||
if cfg.CORS.Enabled {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", "")
|
||||
}
|
||||
}
|
||||
|
||||
// 默认响应
|
||||
func setDefaultHeaders(c *gin.Context) {
|
||||
c.Header("Age", "10")
|
||||
c.Header("Cache-Control", "max-age=300")
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
// 日志输出
|
||||
var (
|
||||
logw = logger.Logw
|
||||
LogDump = logger.LogDump
|
||||
logDebug = logger.LogDebug
|
||||
logInfo = logger.LogInfo
|
||||
logWarning = logger.LogWarning
|
||||
logError = logger.LogError
|
||||
|
||||
86
timing/timing.go
Normal file
86
timing/timing.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package timing
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 阶段计时结构(固定数组优化)
|
||||
type timingData struct {
|
||||
phases [8]struct { // 预分配8个阶段存储
|
||||
name string
|
||||
dur time.Duration
|
||||
}
|
||||
count int
|
||||
start time.Time
|
||||
}
|
||||
|
||||
// 对象池(内存重用优化)
|
||||
var pool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(timingData)
|
||||
},
|
||||
}
|
||||
|
||||
// 中间件入口
|
||||
func Middleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从池中获取计时器
|
||||
td := pool.Get().(*timingData)
|
||||
td.start = time.Now()
|
||||
td.count = 0
|
||||
|
||||
// 存储到上下文
|
||||
c.Set("timing", td)
|
||||
|
||||
// 请求完成后回收对象
|
||||
defer func() {
|
||||
pool.Put(td)
|
||||
}()
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// 记录阶段耗时
|
||||
func Record(c *gin.Context, name string) {
|
||||
if val, exists := c.Get("timing"); exists {
|
||||
//td := val.(*timingData)
|
||||
td, ok := val.(*timingData)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if td.count < len(td.phases) {
|
||||
td.phases[td.count].name = name
|
||||
td.phases[td.count].dur = time.Since(td.start) // 直接记录当前时间
|
||||
td.count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取计时结果(日志输出用)
|
||||
func Get(c *gin.Context) (total time.Duration, phases []struct {
|
||||
Name string
|
||||
Dur time.Duration
|
||||
}) {
|
||||
if val, exists := c.Get("timing"); exists {
|
||||
//td := val.(*timingData)
|
||||
td, ok := val.(*timingData)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for i := 0; i < td.count; i++ {
|
||||
phases = append(phases, struct {
|
||||
Name string
|
||||
Dur time.Duration
|
||||
}{
|
||||
Name: td.phases[i].name,
|
||||
Dur: td.phases[i].dur,
|
||||
})
|
||||
}
|
||||
total = time.Since(td.start)
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user