Compare commits

...

4 Commits

Author SHA1 Message Date
WJQSERVER
f5c32915b9 2.0.7 (#45)
2.0.7
---
- RELEASE: v2.0.7正式版发布;
- CHANGE: 更新Go版本至1.23.6
- CHANGE: 更新Logger版本至v1.2.0
2025-02-05 19:17:36 +08:00
WJQSERVER
286fa0f311 2.0.6 (#42)
2.0.6
---
- RELEASE: v2.0.6正式版发布;祝各位新春快乐!
- CHANGE: 优化前端的连接转换逻辑
- CHANGE: 优化代码内不必要的函数化, 1.4之后, 函数化疑似有点太多了
- 优化`HTTP Client`参数
- CHANGE: 为api路由组增加no-cache标头
2025-01-28 23:54:51 +08:00
WJQSERVER
5d08993cbc 2.0.5 (#40)
- RELEASE: v2.0.5正式版发布;
- CHANGE: 优化响应体分块复制实现
- ADD: 加入缓存池
- CHANGE: 改进缓存实现
- CHANGE: 部分杂项改进
2025-01-27 15:00:19 +08:00
WJQSERVER
6e787ced6e 2.0.4 (#39)
- RELEASE: v2.0.4正式版发布;
- CHANGE: 优化GitReq的`HTTP Client`参数, 使其更符合本项目使用场景
- CHANGE: 优化Matches
- REMOVE: 移除Caddyfile残留
- REMOVE: 由于v2改进后稳定性增强, 故移除健康检测
2025-01-26 16:24:37 +08:00
25 changed files with 346 additions and 542 deletions

View File

@@ -17,7 +17,7 @@ jobs:
goarch: [amd64, arm64] goarch: [amd64, arm64]
env: env:
OUTPUT_BINARY: ghproxy OUTPUT_BINARY: ghproxy
GO_VERSION: 1.23.5 GO_VERSION: 1.23.6
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -48,10 +48,10 @@ jobs:
cp LICENSE ./ghproxyd/ cp LICENSE ./ghproxyd/
tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd . tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd .
ls ls
- name: Upload to GitHub Artifacts - name: 上传Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.OUTPUT_BINARY }} name: ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
path: | path: |
./${{ env.OUTPUT_BINARY }}* ./${{ env.OUTPUT_BINARY }}*
- name: 上传至Release - name: 上传至Release

View File

@@ -17,7 +17,7 @@ jobs:
goarch: [amd64, arm64] goarch: [amd64, arm64]
env: env:
OUTPUT_BINARY: ghproxy OUTPUT_BINARY: ghproxy
GO_VERSION: 1.23.5 GO_VERSION: 1.23.6
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -45,12 +45,12 @@ jobs:
mv ./ghproxyd/${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/${{ env.OUTPUT_BINARY }} mv ./ghproxyd/${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}} ./ghproxyd/${{ env.OUTPUT_BINARY }}
cp LICENSE ./ghproxyd/ cp LICENSE ./ghproxyd/
tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd . tar -czf ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}.tar.gz -C ghproxyd .
- name: Upload to GitHub Artifacts - name: 上传Artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.OUTPUT_BINARY }} name: ${{ env.OUTPUT_BINARY }}-${{matrix.goos}}-${{matrix.goarch}}
path: | path: |
./${{ env.OUTPUT_BINARY }}* ./${{ env.OUTPUT_BINARY }}*
- name: 上传至Release - name: 上传至Release
id: create_release id: create_release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1

View File

@@ -1,5 +1,76 @@
# 更新日志 # 更新日志
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 2.0.3
--- ---
- RELEASE: v2.0.3正式版发布; - RELEASE: v2.0.3正式版发布;

View File

@@ -1 +1 @@
25w07b 25w11a

View File

@@ -1,6 +1,7 @@
# GHProxy # GHProxy
![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg) ![pull](https://img.shields.io/docker/pulls/wjqserver/ghproxy.svg)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/wjqserver/ghproxy/latest)
[![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy) [![Go Report Card](https://goreportcard.com/badge/github.com/WJQSERVER-STUDIO/ghproxy)](https://goreportcard.com/report/github.com/WJQSERVER-STUDIO/ghproxy)
使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署 使用Go实现的GHProxy,用于加速部分地区Github仓库的拉取,支持速率限制,用户鉴权,支持Docker部署
@@ -82,7 +83,6 @@ wget -O install.sh https://raw.githubusercontent.com/WJQSERVER-STUDIO/ghproxy/ma
host = "0.0.0.0" # 监听地址 host = "0.0.0.0" # 监听地址
port = 8080 # 监听端口 port = 8080 # 监听端口
sizeLimit = 125 # 125MB sizeLimit = 125 # 125MB
bufferSize = 4096 # Bytes 缓冲区大小
enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off enableH2C = "on" # 是否开启H2C传输(latest和dev版本请开启) on/off
[pages] [pages]

View File

@@ -1 +1 @@
2.0.3 2.0.7

View File

@@ -20,8 +20,18 @@ var (
logError = logger.LogError 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) { 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) { apiRouter.GET("/size_limit", func(c *gin.Context) {
SizeLimitHandler(cfg, c) SizeLimitHandler(cfg, c)

View File

@@ -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/*

View File

@@ -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/*

View File

@@ -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/*

View File

@@ -16,12 +16,11 @@ type Config struct {
} }
type ServerConfig struct { type ServerConfig struct {
Port int `toml:"port"` Port int `toml:"port"`
Host string `toml:"host"` Host string `toml:"host"`
SizeLimit int `toml:"sizeLimit"` SizeLimit int `toml:"sizeLimit"`
EnableH2C string `toml:"enableH2C"` EnableH2C string `toml:"enableH2C"`
BufferSize int `toml:"bufferSize"` Debug bool `toml:"debug"`
Debug bool `toml:"debug"`
} }
type PagesConfig struct { type PagesConfig struct {

View File

@@ -3,7 +3,6 @@ host = "0.0.0.0"
port = 8080 port = 8080
sizeLimit = 125 # MB sizeLimit = 125 # MB
enableH2C = "on" # "on" or "off" enableH2C = "on" # "on" or "off"
bufferSize = 4096 # Bytes
debug = false debug = false
[pages] [pages]

View File

@@ -2,7 +2,6 @@
host = "127.0.0.1" host = "127.0.0.1"
port = 8080 port = 8080
sizeLimit = 125 # MB sizeLimit = 125 # MB
bufferSize = 4096 # Bytes
enableH2C = false enableH2C = false
debug = false debug = false

View File

@@ -14,12 +14,4 @@ if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
fi fi
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.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

View File

@@ -14,12 +14,4 @@ if [ ! -f /data/${APPLICATION}/config/config.toml ]; then
cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml cp /data/${APPLICATION}/config.toml /data/${APPLICATION}/config/config.toml
fi fi
/data/${APPLICATION}/${APPLICATION} -cfg /data/${APPLICATION}/config/config.toml > /data/${APPLICATION}/log/run.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

18
go.mod
View File

@@ -1,16 +1,16 @@
module ghproxy module ghproxy
go 1.23.5 go 1.23.6
require ( require (
github.com/BurntSushi/toml v1.4.0 github.com/BurntSushi/toml v1.4.0
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.1 github.com/WJQSERVER-STUDIO/go-utils/logger v1.2.0
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
golang.org/x/time v0.9.0 golang.org/x/time v0.10.0
) )
require ( 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/bytedance/sonic/loader v0.2.3 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
@@ -18,7 +18,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.24.0 // 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/google/go-cmp v0.6.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect
@@ -29,11 +29,11 @@ require (
github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.13.0 // indirect golang.org/x/arch v0.14.0 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.32.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect google.golang.org/protobuf v1.36.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

32
go.sum
View File

@@ -1,9 +1,9 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= 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/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.1 h1:YS3q54SroxQpEM7c12ZKjLNAaSq++bNpxTujs9cTZ9c= github.com/WJQSERVER-STUDIO/go-utils/logger v1.2.0 h1:6JkiVGRasfdBxEV8Qnv5x00Bq4qX1Tg6f58Ar/D2YX0=
github.com/WJQSERVER-STUDIO/go-utils/logger v1.1.1/go.mod h1:oW884JCCPDU6c906LI0uKXndWLiRvjb9LkGYC2cqRO8= github.com/WJQSERVER-STUDIO/go-utils/logger v1.2.0/go.mod h1:oW884JCCPDU6c906LI0uKXndWLiRvjb9LkGYC2cqRO8=
github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs=
github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= 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.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/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/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 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= 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.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -67,21 +67,21 @@ 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/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 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 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.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -14,12 +14,5 @@ if [ ! -f /data/${APPLICATON}/config/config.yaml ]; then
cp /data/${APPLICATON}/config.yaml /data/${APPLICATON}/config/config.yaml cp /data/${APPLICATON}/config.yaml /data/${APPLICATON}/config/config.yaml
fi fi
/data/${APPLICATON}/${APPLICATON} > /data/${APPLICATON}/log/run.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

View File

@@ -91,8 +91,8 @@ func setupRateLimit(cfg *config.Config) {
} }
} }
func InitChunkedReq() { func InitReq() {
proxy.InitChunkedReq(cfg.Server.BufferSize) proxy.InitReq()
} }
func init() { func init() {
@@ -100,7 +100,7 @@ func init() {
flag.Parse() flag.Parse()
loadConfig() loadConfig()
setupLogger(cfg) setupLogger(cfg)
InitChunkedReq() InitReq()
loadlist(cfg) loadlist(cfg)
setupRateLimit(cfg) setupRateLimit(cfg)

View File

@@ -8,7 +8,6 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <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.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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> <style>
/* 通用样式 */ /* 通用样式 */
:root { :root {
@@ -40,7 +39,7 @@
body { body {
background-color: var(--background-color); background-color: var(--background-color);
color: var(--text-color); color: var(--text-color);
font-family: "Noto Sans SC", sans-serif; font-family: sans-serif;
line-height: 1.8; line-height: 1.8;
font-size: 15px; font-size: 15px;
margin: 0; margin: 0;
@@ -370,17 +369,17 @@
let formattedLink = ""; let formattedLink = "";
if (githubLink.startsWith("https://github.com/") || githubLink.startsWith("http://github.com/")) { 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/")) { } 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/")) { } 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/")) { } 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/")) { } 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/")) { } else if (githubLink.startsWith("gist.githubusercontent.com/")) {
formattedLink = "https://" + currentHost + "/" + githubLink; formattedLink = window.location.protocol + "//" + currentHost + "/" + githubLink;
} else { } else {
showToast('请输入有效的GitHub链接'); showToast('请输入有效的GitHub链接');
return null; return null;
@@ -438,4 +437,4 @@
</script> </script>
</body> </body>
</html> </html>

View File

@@ -5,40 +5,50 @@ import (
"fmt" "fmt"
"ghproxy/config" "ghproxy/config"
"io" "io"
"net"
"net/http" "net/http"
"strconv"
"sync"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var chunkedBufferSize int var BufferSize int = 32 * 1024 // 32KB
var ( var (
client *http.Client cclient *http.Client
tr *http.Transport ctr *http.Transport
BufferPool *sync.Pool
) )
func InitChunkedReq(cfgBufferSize int) { func InitReq() {
initChunkedBufferSize(cfgBufferSize)
initChunkedHTTPClient() initChunkedHTTPClient()
} initGitHTTPClient()
func initChunkedBufferSize(cfgBufferSize int) { // 初始化固定大小的缓存池
if cfgBufferSize == 0 { BufferPool = &sync.Pool{
chunkedBufferSize = 4096 // 默认缓冲区大小 New: func() interface{} {
} else { return make([]byte, BufferSize)
chunkedBufferSize = cfgBufferSize },
} }
} }
func initChunkedHTTPClient() { func initChunkedHTTPClient() {
tr = &http.Transport{ ctr = &http.Transport{
MaxIdleConns: 100, MaxIdleConns: 100,
MaxConnsPerHost: 60, MaxConnsPerHost: 60,
IdleConnTimeout: 20 * time.Second, 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,
} }
client = &http.Client{ cclient = &http.Client{
Transport: tr, Transport: ctr,
} }
} }
@@ -46,9 +56,6 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri
method := c.Request.Method method := c.Request.Method
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto) 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 // 发送HEAD请求, 预获取Content-Length
headReq, err := http.NewRequest("HEAD", u, nil) headReq, err := http.NewRequest("HEAD", u, nil)
if err != nil { if err != nil {
@@ -59,18 +66,37 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri
removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) removeWSHeader(headReq) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
AuthPassThrough(c, cfg, headReq) AuthPassThrough(c, cfg, headReq)
headResp, err := client.Do(headReq) headResp, err := cclient.Do(headReq)
if err != nil { if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return 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 { contentLength := headResp.Header.Get("Content-Length")
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err) sizelimit := cfg.Server.SizeLimit * 1024 * 1024
return 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) body, err := readRequestBody(c)
if err != nil { if err != nil {
HandleError(c, err.Error()) HandleError(c, err.Error())
@@ -86,49 +112,68 @@ func ChunkedProxyRequest(c *gin.Context, u string, cfg *config.Config, mode stri
return return
} }
req.Header.Set("Transfer-Encoding", "chunked") // 确保设置分块传输编码
setRequestHeaders(c, req) setRequestHeaders(c, req)
removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头) removeWSHeader(req) // 删除Conection Upgrade头, 避免与HTTP/2冲突(检查是否存在Upgrade头)
AuthPassThrough(c, cfg, req) AuthPassThrough(c, cfg, req)
resp, err := client.Do(req) resp, err := cclient.Do(req)
if err != nil { if err != nil {
HandleError(c, fmt.Sprintf("发送请求失败: %v", err)) HandleError(c, fmt.Sprintf("发送请求失败: %v", err))
return return
} }
defer resp.Body.Close() 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) if err := HandleResponseSize(resp, cfg, c); err != nil {
return 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) c.Status(resp.StatusCode)
if err := chunkedCopyResponseBody(c, resp.Body); err != nil { // 使用固定32KB缓冲池
logError("%s %s %s %s %s 响应复制错误: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err) buffer := BufferPool.Get().([]byte)
} defer BufferPool.Put(buffer)
}
// 复制响应体 _, err = io.CopyBuffer(c.Writer, resp.Body, buffer)
func chunkedCopyResponseBody(c *gin.Context, respBody io.Reader) error { if err != nil {
buf := make([]byte, chunkedBufferSize) logError("%s %s %s %s %s 响应复制错误: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err)
for { return
n, err := respBody.Read(buf) } else {
if n > 0 { c.Writer.Flush() // 确保刷入
if _, err := c.Writer.Write(buf[:n]); err != nil {
return err
}
c.Writer.Flush() // 确保每次写入后刷新
}
if err != nil {
if err == io.EOF {
break
}
return err
}
} }
return nil
} }

View File

@@ -6,16 +6,34 @@ import (
"ghproxy/config" "ghproxy/config"
"io" "io"
"net/http" "net/http"
"strconv"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var (
gclient *http.Client
gtr *http.Transport
)
func initGitHTTPClient() {
gtr = &http.Transport{
MaxIdleConns: 30,
MaxConnsPerHost: 30,
IdleConnTimeout: 30 * time.Second,
}
gclient = &http.Client{
Transport: gtr,
}
}
func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) { func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode string) {
method := c.Request.Method method := c.Request.Method
logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto) logInfo("%s %s %s %s %s", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto)
// 创建HTTP客户端 // 创建HTTP客户端
client := &http.Client{} //client := &http.Client{}
// 发送HEAD请求, 预获取Content-Length // 发送HEAD请求, 预获取Content-Length
headReq, err := http.NewRequest("HEAD", u, nil) headReq, err := http.NewRequest("HEAD", u, nil)
@@ -26,16 +44,29 @@ func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode s
setRequestHeaders(c, headReq) setRequestHeaders(c, headReq)
AuthPassThrough(c, cfg, headReq) AuthPassThrough(c, cfg, headReq)
headResp, err := client.Do(headReq) headResp, err := gclient.Do(headReq)
if err != nil { if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return return
} }
defer headResp.Body.Close()
if err := HandleResponseSize(headResp, cfg, c); err != nil { // defer headResp.Body.Close()
logWarning("%s %s %s %s %s Response-Size-Error: %v", c.ClientIP(), method, u, c.Request.Header.Get("User-Agent"), c.Request.Proto, err) defer func(Body io.ReadCloser) {
return 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) body, err := readRequestBody(c)
@@ -55,27 +86,60 @@ func GitReq(c *gin.Context, u string, cfg *config.Config, mode string, runMode s
setRequestHeaders(c, req) setRequestHeaders(c, req)
AuthPassThrough(c, cfg, req) AuthPassThrough(c, cfg, req)
resp, err := client.Do(req) resp, err := gclient.Do(req)
if err != nil { if err != nil {
HandleError(c, fmt.Sprintf("Failed to send request: %v", err)) HandleError(c, fmt.Sprintf("Failed to send request: %v", err))
return 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) if err := HandleResponseSize(resp, cfg, c); err != nil {
return 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) 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) 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
}

View File

@@ -9,17 +9,19 @@ import (
"github.com/gin-gonic/gin" "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) { func MatchUserRepo(rawPath string, cfg *config.Config, c *gin.Context, matches []string) (string, string) {
var gistregex = regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.*`) if gistMatches := gistRegex.FindStringSubmatch(rawPath); len(gistMatches) == 3 {
var gistmatches []string 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])
if gistregex.MatchString(rawPath) { return gistMatches[1], ""
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 { if pathMatches := pathRegex.FindStringSubmatch(matches[2]); len(pathMatches) >= 4 {
return pathMatches[2], pathMatches[3] return pathMatches[2], pathMatches[3]
} }

View File

@@ -5,9 +5,6 @@ import (
"io" "io"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"ghproxy/config"
"github.com/WJQSERVER-STUDIO/go-utils/logger" "github.com/WJQSERVER-STUDIO/go-utils/logger"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -60,22 +57,6 @@ func SendRequest(c *gin.Context, req *req.Request, method, url string) (*req.Res
} }
*/ */
// 处理响应大小
func HandleResponseSize(resp *http.Response, cfg *config.Config, c *gin.Context) error {
contentLength := resp.Header.Get("Content-Length")
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > sizelimit {
finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return fmt.Errorf("Path: %s size limit exceeded: %d", finalURL, size)
}
}
return nil
}
func HandleError(c *gin.Context, message string) { func HandleError(c *gin.Context, message string) {
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message)) c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", message))
logWarning(message) logWarning(message)
@@ -91,3 +72,21 @@ func CheckURL(u string, c *gin.Context) []string {
logWarning(errMsg) logWarning(errMsg)
return nil return nil
} }
/*
// 处理响应大小
func HandleResponseSize(resp *http.Response, cfg *config.Config, c *gin.Context) error {
contentLength := resp.Header.Get("Content-Length")
sizelimit := cfg.Server.SizeLimit * 1024 * 1024
if contentLength != "" {
size, err := strconv.Atoi(contentLength)
if err == nil && size > sizelimit {
finalURL := resp.Request.URL.String()
c.Redirect(http.StatusMovedPermanently, finalURL)
logWarning("%s %s %s %s %s Final-URL: %s Size-Limit-Exceeded: %d", c.ClientIP(), c.Request.Method, c.Request.URL.String(), c.Request.Header.Get("User-Agent"), c.Request.Proto, finalURL, size)
return fmt.Errorf("Path: %s size limit exceeded: %d", finalURL, size)
}
}
return nil
}
*/

View File

@@ -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")
}