重构优化
This commit is contained in:
2
.github/workflows/docker-ghcr.yml
vendored
2
.github/workflows/docker-ghcr.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
cd ghproxy
|
||||
cd src
|
||||
docker buildx build --push \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ghcr.io/${{ env.REPO_LOWER }}:${{ env.VERSION }} \
|
||||
|
||||
15
Caddyfile
15
Caddyfile
@@ -1,15 +0,0 @@
|
||||
hub.{$DOMAIN} {
|
||||
reverse_proxy * ghproxy:5000
|
||||
}
|
||||
|
||||
docker.{$DOMAIN} {
|
||||
@v2_manifest_blob path_regexp v2_rewrite ^/v2/([^/]+)/(manifests|blobs)/(.*)$
|
||||
handle @v2_manifest_blob {
|
||||
rewrite * /v2/library/{re.v2_rewrite.1}/{re.v2_rewrite.2}/{re.v2_rewrite.3}
|
||||
}
|
||||
reverse_proxy * docker:5000
|
||||
}
|
||||
|
||||
ghcr.{$DOMAIN} {
|
||||
reverse_proxy * ghcr:5000
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
services:
|
||||
caddy:
|
||||
image: caddy:alpine
|
||||
container_name: caddy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
environment:
|
||||
- DOMAIN=example.com # 修改为你的根域名
|
||||
restart: always
|
||||
|
||||
ghcr:
|
||||
image: "registry:2.8.3"
|
||||
container_name: "ghcr"
|
||||
restart: "always"
|
||||
volumes:
|
||||
- "./ghcr/config.yml:/etc/docker/registry/config.yml"
|
||||
|
||||
docker:
|
||||
image: "registry:2.8.3"
|
||||
container_name: "docker"
|
||||
restart: "always"
|
||||
volumes:
|
||||
- "./docker/config.yml:/etc/docker/registry/config.yml"
|
||||
|
||||
ghproxy:
|
||||
image: "ghcr.io/sky22333/hubproxy"
|
||||
container_name: "ghproxy"
|
||||
restart: "always"
|
||||
@@ -1,16 +0,0 @@
|
||||
version: 0.1
|
||||
storage:
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/registry
|
||||
delete:
|
||||
enabled: true
|
||||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: true
|
||||
age: 72h
|
||||
dryrun: false
|
||||
interval: 1m
|
||||
http:
|
||||
addr: 0.0.0.0:5000
|
||||
proxy:
|
||||
remoteurl: https://registry-1.docker.io
|
||||
@@ -1,16 +0,0 @@
|
||||
version: 0.1
|
||||
storage:
|
||||
filesystem:
|
||||
rootdirectory: /var/lib/registry
|
||||
delete:
|
||||
enabled: true
|
||||
maintenance:
|
||||
uploadpurging:
|
||||
enabled: true
|
||||
age: 72h
|
||||
dryrun: false
|
||||
interval: 1m
|
||||
http:
|
||||
addr: 0.0.0.0:5000
|
||||
proxy:
|
||||
remoteurl: https://ghcr.io
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"whiteList": [
|
||||
],
|
||||
"blackList": [
|
||||
"example1",
|
||||
"login"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
services:
|
||||
ghproxy:
|
||||
build: .
|
||||
restart: always
|
||||
ports:
|
||||
- '5000:5000'
|
||||
@@ -1,11 +1,11 @@
|
||||
FROM golang:1.23-alpine AS builder
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o ghproxy .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -trimpath -o hubproxy .
|
||||
|
||||
FROM alpine
|
||||
|
||||
@@ -14,8 +14,8 @@ WORKDIR /root/
|
||||
# 安装skopeo
|
||||
RUN apk add --no-cache skopeo && mkdir -p temp && chmod 700 temp
|
||||
|
||||
COPY --from=builder /app/ghproxy .
|
||||
COPY --from=builder /app/config.json .
|
||||
COPY --from=builder /app/hubproxy .
|
||||
COPY --from=builder /app/config.toml .
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
CMD ["./ghproxy"]
|
||||
CMD ["./hubproxy"]
|
||||
226
src/access_control.go
Normal file
226
src/access_control.go
Normal file
@@ -0,0 +1,226 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ResourceType 资源类型
|
||||
type ResourceType string
|
||||
|
||||
const (
|
||||
ResourceTypeGitHub ResourceType = "github"
|
||||
ResourceTypeDocker ResourceType = "docker"
|
||||
)
|
||||
|
||||
// AccessController 统一访问控制器
|
||||
type AccessController struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// DockerImageInfo Docker镜像信息
|
||||
type DockerImageInfo struct {
|
||||
Namespace string
|
||||
Repository string
|
||||
Tag string
|
||||
FullName string
|
||||
}
|
||||
|
||||
// 全局访问控制器实例
|
||||
var GlobalAccessController = &AccessController{}
|
||||
|
||||
// ParseDockerImage 解析Docker镜像名称
|
||||
func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
|
||||
// 移除可能的协议前缀
|
||||
image = strings.TrimPrefix(image, "docker://")
|
||||
|
||||
// 分离标签
|
||||
var tag string
|
||||
if idx := strings.LastIndex(image, ":"); idx != -1 {
|
||||
// 检查是否是端口号而不是标签(包含斜杠)
|
||||
part := image[idx+1:]
|
||||
if !strings.Contains(part, "/") {
|
||||
tag = part
|
||||
image = image[:idx]
|
||||
}
|
||||
}
|
||||
if tag == "" {
|
||||
tag = "latest"
|
||||
}
|
||||
|
||||
// 分离命名空间和仓库名
|
||||
var namespace, repository string
|
||||
if strings.Contains(image, "/") {
|
||||
// 处理自定义registry的情况,如 registry.com/user/repo
|
||||
parts := strings.Split(image, "/")
|
||||
if len(parts) >= 2 {
|
||||
// 检查第一部分是否是域名(包含.)
|
||||
if strings.Contains(parts[0], ".") {
|
||||
// 跳过registry域名,取用户名和仓库名
|
||||
if len(parts) >= 3 {
|
||||
namespace = parts[1]
|
||||
repository = parts[2]
|
||||
} else {
|
||||
namespace = "library"
|
||||
repository = parts[1]
|
||||
}
|
||||
} else {
|
||||
// 标准格式:user/repo
|
||||
namespace = parts[0]
|
||||
repository = parts[1]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 官方镜像,如 nginx
|
||||
namespace = "library"
|
||||
repository = image
|
||||
}
|
||||
|
||||
fullName := namespace + "/" + repository
|
||||
|
||||
return DockerImageInfo{
|
||||
Namespace: namespace,
|
||||
Repository: repository,
|
||||
Tag: tag,
|
||||
FullName: fullName,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckDockerAccess 检查Docker镜像访问权限
|
||||
func (ac *AccessController) CheckDockerAccess(image string) (allowed bool, reason string) {
|
||||
cfg := GetConfig()
|
||||
|
||||
// 解析镜像名称
|
||||
imageInfo := ac.ParseDockerImage(image)
|
||||
|
||||
// 检查白名单(如果配置了白名单,则只允许白名单中的镜像)
|
||||
if len(cfg.Proxy.WhiteList) > 0 {
|
||||
if !ac.matchImageInList(imageInfo, cfg.Proxy.WhiteList) {
|
||||
return false, "不在Docker镜像白名单内"
|
||||
}
|
||||
}
|
||||
|
||||
// 检查黑名单
|
||||
if len(cfg.Proxy.BlackList) > 0 {
|
||||
if ac.matchImageInList(imageInfo, cfg.Proxy.BlackList) {
|
||||
return false, "Docker镜像在黑名单内"
|
||||
}
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// CheckGitHubAccess 检查GitHub仓库访问权限
|
||||
func (ac *AccessController) CheckGitHubAccess(matches []string) (allowed bool, reason string) {
|
||||
if len(matches) < 2 {
|
||||
return false, "无效的GitHub仓库格式"
|
||||
}
|
||||
|
||||
cfg := GetConfig()
|
||||
|
||||
// 检查白名单
|
||||
if len(cfg.Proxy.WhiteList) > 0 && !ac.checkList(matches, cfg.Proxy.WhiteList) {
|
||||
return false, "不在GitHub仓库白名单内"
|
||||
}
|
||||
|
||||
// 检查黑名单
|
||||
if len(cfg.Proxy.BlackList) > 0 && ac.checkList(matches, cfg.Proxy.BlackList) {
|
||||
return false, "GitHub仓库在黑名单内"
|
||||
}
|
||||
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// matchImageInList 检查Docker镜像是否在指定列表中
|
||||
func (ac *AccessController) matchImageInList(imageInfo DockerImageInfo, list []string) bool {
|
||||
fullName := strings.ToLower(imageInfo.FullName)
|
||||
namespace := strings.ToLower(imageInfo.Namespace)
|
||||
|
||||
for _, item := range list {
|
||||
item = strings.ToLower(strings.TrimSpace(item))
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if fullName == item {
|
||||
return true
|
||||
}
|
||||
|
||||
if item == namespace || item == namespace+"/*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasSuffix(item, "*") {
|
||||
prefix := strings.TrimSuffix(item, "*")
|
||||
if strings.HasPrefix(fullName, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(item, "*/") {
|
||||
repoPattern := strings.TrimPrefix(item, "*/")
|
||||
if strings.HasSuffix(repoPattern, "*") {
|
||||
repoPrefix := strings.TrimSuffix(repoPattern, "*")
|
||||
if strings.HasPrefix(imageInfo.Repository, repoPrefix) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if strings.ToLower(imageInfo.Repository) == repoPattern {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 子仓库匹配(防止 user/repo 匹配到 user/repo-fork)
|
||||
if strings.HasPrefix(fullName, item+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// checkList GitHub仓库检查逻辑
|
||||
func (ac *AccessController) checkList(matches, list []string) bool {
|
||||
if len(matches) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 组合用户名和仓库名,处理.git后缀
|
||||
username := strings.ToLower(strings.TrimSpace(matches[0]))
|
||||
repoName := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(matches[1], ".git")))
|
||||
fullRepo := username + "/" + repoName
|
||||
|
||||
for _, item := range list {
|
||||
item = strings.ToLower(strings.TrimSpace(item))
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 支持多种匹配模式:
|
||||
// 1. 精确匹配: "vaxilu/x-ui"
|
||||
// 2. 用户级匹配: "vaxilu/*" 或 "vaxilu"
|
||||
// 3. 前缀匹配: "vaxilu/x-ui-*"
|
||||
if fullRepo == item {
|
||||
return true
|
||||
}
|
||||
|
||||
// 用户级匹配
|
||||
if item == username || item == username+"/*" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 前缀匹配(支持通配符)
|
||||
if strings.HasSuffix(item, "*") {
|
||||
prefix := strings.TrimSuffix(item, "*")
|
||||
if strings.HasPrefix(fullRepo, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 子仓库匹配(防止 user/repo 匹配到 user/repo-fork)
|
||||
if strings.HasPrefix(fullRepo, item+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
195
src/config.go
Normal file
195
src/config.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// AppConfig 应用配置结构体
|
||||
type AppConfig struct {
|
||||
Server struct {
|
||||
Host string `toml:"host"` // 监听地址
|
||||
Port int `toml:"port"` // 监听端口
|
||||
FileSize int64 `toml:"fileSize"` // 文件大小限制(字节)
|
||||
} `toml:"server"`
|
||||
|
||||
RateLimit struct {
|
||||
RequestLimit int `toml:"requestLimit"` // 每小时请求限制
|
||||
PeriodHours float64 `toml:"periodHours"` // 限制周期(小时)
|
||||
} `toml:"rateLimit"`
|
||||
|
||||
Security struct {
|
||||
WhiteList []string `toml:"whiteList"` // 白名单IP/CIDR列表
|
||||
BlackList []string `toml:"blackList"` // 黑名单IP/CIDR列表
|
||||
} `toml:"security"`
|
||||
|
||||
Proxy struct {
|
||||
WhiteList []string `toml:"whiteList"` // 代理白名单(仓库级别)
|
||||
BlackList []string `toml:"blackList"` // 代理黑名单(仓库级别)
|
||||
} `toml:"proxy"`
|
||||
|
||||
Download struct {
|
||||
MaxImages int `toml:"maxImages"` // 单次下载最大镜像数量限制
|
||||
} `toml:"download"`
|
||||
}
|
||||
|
||||
var (
|
||||
appConfig *AppConfig
|
||||
appConfigLock sync.RWMutex
|
||||
)
|
||||
|
||||
// DefaultConfig 返回默认配置
|
||||
func DefaultConfig() *AppConfig {
|
||||
return &AppConfig{
|
||||
Server: struct {
|
||||
Host string `toml:"host"`
|
||||
Port int `toml:"port"`
|
||||
FileSize int64 `toml:"fileSize"`
|
||||
}{
|
||||
Host: "0.0.0.0",
|
||||
Port: 5000,
|
||||
FileSize: 2 * 1024 * 1024 * 1024, // 2GB
|
||||
},
|
||||
RateLimit: struct {
|
||||
RequestLimit int `toml:"requestLimit"`
|
||||
PeriodHours float64 `toml:"periodHours"`
|
||||
}{
|
||||
RequestLimit: 20,
|
||||
PeriodHours: 1.0,
|
||||
},
|
||||
Security: struct {
|
||||
WhiteList []string `toml:"whiteList"`
|
||||
BlackList []string `toml:"blackList"`
|
||||
}{
|
||||
WhiteList: []string{},
|
||||
BlackList: []string{},
|
||||
},
|
||||
Proxy: struct {
|
||||
WhiteList []string `toml:"whiteList"`
|
||||
BlackList []string `toml:"blackList"`
|
||||
}{
|
||||
WhiteList: []string{},
|
||||
BlackList: []string{},
|
||||
},
|
||||
Download: struct {
|
||||
MaxImages int `toml:"maxImages"`
|
||||
}{
|
||||
MaxImages: 10, // 默认值:最多同时下载10个镜像
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 安全地获取配置副本
|
||||
func GetConfig() *AppConfig {
|
||||
appConfigLock.RLock()
|
||||
defer appConfigLock.RUnlock()
|
||||
|
||||
if appConfig == nil {
|
||||
return DefaultConfig()
|
||||
}
|
||||
|
||||
// 返回配置的深拷贝
|
||||
configCopy := *appConfig
|
||||
configCopy.Security.WhiteList = append([]string(nil), appConfig.Security.WhiteList...)
|
||||
configCopy.Security.BlackList = append([]string(nil), appConfig.Security.BlackList...)
|
||||
configCopy.Proxy.WhiteList = append([]string(nil), appConfig.Proxy.WhiteList...)
|
||||
configCopy.Proxy.BlackList = append([]string(nil), appConfig.Proxy.BlackList...)
|
||||
|
||||
return &configCopy
|
||||
}
|
||||
|
||||
// setConfig 安全地设置配置
|
||||
func setConfig(cfg *AppConfig) {
|
||||
appConfigLock.Lock()
|
||||
defer appConfigLock.Unlock()
|
||||
appConfig = cfg
|
||||
}
|
||||
|
||||
// LoadConfig 加载配置文件
|
||||
func LoadConfig() error {
|
||||
// 首先使用默认配置
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// 尝试加载TOML配置文件
|
||||
if data, err := os.ReadFile("config.toml"); err == nil {
|
||||
if err := toml.Unmarshal(data, cfg); err != nil {
|
||||
return fmt.Errorf("解析配置文件失败: %v", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("未找到config.toml,使用默认配置")
|
||||
}
|
||||
|
||||
// 从环境变量覆盖配置
|
||||
overrideFromEnv(cfg)
|
||||
|
||||
// 设置配置
|
||||
setConfig(cfg)
|
||||
|
||||
fmt.Printf("配置加载成功: 监听 %s:%d, 文件大小限制 %d MB, 限流 %d请求/%g小时, 离线镜像并发数 %d\n",
|
||||
cfg.Server.Host, cfg.Server.Port, cfg.Server.FileSize/(1024*1024),
|
||||
cfg.RateLimit.RequestLimit, cfg.RateLimit.PeriodHours, cfg.Download.MaxImages)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// overrideFromEnv 从环境变量覆盖配置
|
||||
func overrideFromEnv(cfg *AppConfig) {
|
||||
// 服务器配置
|
||||
if val := os.Getenv("SERVER_HOST"); val != "" {
|
||||
cfg.Server.Host = val
|
||||
}
|
||||
if val := os.Getenv("SERVER_PORT"); val != "" {
|
||||
if port, err := strconv.Atoi(val); err == nil && port > 0 {
|
||||
cfg.Server.Port = port
|
||||
}
|
||||
}
|
||||
if val := os.Getenv("MAX_FILE_SIZE"); val != "" {
|
||||
if size, err := strconv.ParseInt(val, 10, 64); err == nil && size > 0 {
|
||||
cfg.Server.FileSize = size
|
||||
}
|
||||
}
|
||||
|
||||
// 限流配置
|
||||
if val := os.Getenv("RATE_LIMIT"); val != "" {
|
||||
if limit, err := strconv.Atoi(val); err == nil && limit > 0 {
|
||||
cfg.RateLimit.RequestLimit = limit
|
||||
}
|
||||
}
|
||||
if val := os.Getenv("RATE_PERIOD_HOURS"); val != "" {
|
||||
if period, err := strconv.ParseFloat(val, 64); err == nil && period > 0 {
|
||||
cfg.RateLimit.PeriodHours = period
|
||||
}
|
||||
}
|
||||
|
||||
// IP限制配置
|
||||
if val := os.Getenv("IP_WHITELIST"); val != "" {
|
||||
cfg.Security.WhiteList = append(cfg.Security.WhiteList, strings.Split(val, ",")...)
|
||||
}
|
||||
if val := os.Getenv("IP_BLACKLIST"); val != "" {
|
||||
cfg.Security.BlackList = append(cfg.Security.BlackList, strings.Split(val, ",")...)
|
||||
}
|
||||
|
||||
// 下载限制配置
|
||||
if val := os.Getenv("MAX_IMAGES"); val != "" {
|
||||
if maxImages, err := strconv.Atoi(val); err == nil && maxImages > 0 {
|
||||
cfg.Download.MaxImages = maxImages
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CreateDefaultConfigFile 创建默认配置文件
|
||||
func CreateDefaultConfigFile() error {
|
||||
cfg := DefaultConfig()
|
||||
|
||||
data, err := toml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化默认配置失败: %v", err)
|
||||
}
|
||||
|
||||
return os.WriteFile("config.toml", data, 0644)
|
||||
}
|
||||
45
src/config.toml
Normal file
45
src/config.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[server]
|
||||
# 监听地址,默认监听所有接口
|
||||
host = "0.0.0.0"
|
||||
# 监听端口
|
||||
port = 5000
|
||||
# 文件大小限制(字节),默认2GB
|
||||
fileSize = 2147483648
|
||||
|
||||
[rateLimit]
|
||||
# 每个IP每小时允许的请求数
|
||||
requestLimit = 200
|
||||
# 限流周期(小时)
|
||||
periodHours = 1.0
|
||||
|
||||
[security]
|
||||
# IP白名单,支持单个IP或CIDR格式
|
||||
# 白名单中的IP不受限流限制
|
||||
whiteList = [
|
||||
"127.0.0.1",
|
||||
"192.168.1.0/24"
|
||||
]
|
||||
|
||||
# IP黑名单,支持单个IP或CIDR格式
|
||||
# 黑名单中的IP将被直接拒绝访问
|
||||
blackList = [
|
||||
"192.168.100.1"
|
||||
]
|
||||
|
||||
[proxy]
|
||||
# 代理服务白名单(支持GitHub仓库和Docker镜像,支持通配符)
|
||||
# 只允许访问白名单中的仓库/镜像,为空时不限制
|
||||
whiteList = []
|
||||
|
||||
# 代理服务黑名单(支持GitHub仓库和Docker镜像,支持通配符)
|
||||
# 禁止访问黑名单中的仓库/镜像
|
||||
blackList = [
|
||||
"baduser/malicious-repo",
|
||||
"thesadboy/x-ui",
|
||||
"vaxilu/x-ui",
|
||||
"vaxilu/*"
|
||||
]
|
||||
|
||||
[download]
|
||||
# 单次并发下载离线镜像数量限制
|
||||
maxImages = 10
|
||||
8
src/docker-compose.yml
Normal file
8
src/docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
ghproxy:
|
||||
build: .
|
||||
restart: always
|
||||
ports:
|
||||
- '5000:5000'
|
||||
volumes:
|
||||
- ./config.toml:/root/config.toml
|
||||
323
src/docker.go
Normal file
323
src/docker.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
// DockerProxy Docker代理配置
|
||||
type DockerProxy struct {
|
||||
registry name.Registry
|
||||
options []remote.Option
|
||||
}
|
||||
|
||||
var dockerProxy *DockerProxy
|
||||
|
||||
// 初始化Docker代理
|
||||
func initDockerProxy() {
|
||||
// 创建目标registry
|
||||
registry, err := name.NewRegistry("registry-1.docker.io")
|
||||
if err != nil {
|
||||
fmt.Printf("创建Docker registry失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 配置代理选项
|
||||
options := []remote.Option{
|
||||
remote.WithAuth(authn.Anonymous),
|
||||
remote.WithUserAgent("ghproxy/go-containerregistry"),
|
||||
}
|
||||
|
||||
dockerProxy = &DockerProxy{
|
||||
registry: registry,
|
||||
options: options,
|
||||
}
|
||||
|
||||
fmt.Printf("Docker代理已初始化\n")
|
||||
}
|
||||
|
||||
// ProxyDockerRegistryGin 标准Docker Registry API v2代理
|
||||
func ProxyDockerRegistryGin(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
|
||||
// 处理 /v2/ API版本检查
|
||||
if path == "/v2/" {
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
return
|
||||
}
|
||||
|
||||
// 处理不同的API端点
|
||||
if strings.HasPrefix(path, "/v2/") {
|
||||
handleRegistryRequest(c, path)
|
||||
} else {
|
||||
c.String(http.StatusNotFound, "Docker Registry API v2 only")
|
||||
}
|
||||
}
|
||||
|
||||
// handleRegistryRequest 处理Registry请求
|
||||
func handleRegistryRequest(c *gin.Context, path string) {
|
||||
// 移除 /v2/ 前缀
|
||||
pathWithoutV2 := strings.TrimPrefix(path, "/v2/")
|
||||
|
||||
// 解析路径
|
||||
imageName, apiType, reference := parseRegistryPath(pathWithoutV2)
|
||||
if imageName == "" || apiType == "" {
|
||||
c.String(http.StatusBadRequest, "Invalid path format")
|
||||
return
|
||||
}
|
||||
|
||||
// 自动处理官方镜像的library命名空间
|
||||
if !strings.Contains(imageName, "/") {
|
||||
imageName = "library/" + imageName
|
||||
}
|
||||
|
||||
// Docker镜像访问控制检查
|
||||
if allowed, reason := GlobalAccessController.CheckDockerAccess(imageName); !allowed {
|
||||
fmt.Printf("Docker镜像 %s 访问被拒绝: %s\n", imageName, reason)
|
||||
c.String(http.StatusForbidden, "镜像访问被限制")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建完整的镜像引用
|
||||
imageRef := fmt.Sprintf("%s/%s", dockerProxy.registry.Name(), imageName)
|
||||
|
||||
switch apiType {
|
||||
case "manifests":
|
||||
handleManifestRequest(c, imageRef, reference)
|
||||
case "blobs":
|
||||
handleBlobRequest(c, imageRef, reference)
|
||||
case "tags":
|
||||
handleTagsRequest(c, imageRef)
|
||||
default:
|
||||
c.String(http.StatusNotFound, "API endpoint not found")
|
||||
}
|
||||
}
|
||||
|
||||
// parseRegistryPath 解析Registry路径
|
||||
func parseRegistryPath(path string) (imageName, apiType, reference string) {
|
||||
// 查找API端点关键字
|
||||
if idx := strings.Index(path, "/manifests/"); idx != -1 {
|
||||
imageName = path[:idx]
|
||||
apiType = "manifests"
|
||||
reference = path[idx+len("/manifests/"):]
|
||||
return
|
||||
}
|
||||
|
||||
if idx := strings.Index(path, "/blobs/"); idx != -1 {
|
||||
imageName = path[:idx]
|
||||
apiType = "blobs"
|
||||
reference = path[idx+len("/blobs/"):]
|
||||
return
|
||||
}
|
||||
|
||||
if idx := strings.Index(path, "/tags/list"); idx != -1 {
|
||||
imageName = path[:idx]
|
||||
apiType = "tags"
|
||||
reference = "list"
|
||||
return
|
||||
}
|
||||
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
// handleManifestRequest 处理manifest请求
|
||||
func handleManifestRequest(c *gin.Context, imageRef, reference string) {
|
||||
var ref name.Reference
|
||||
var err error
|
||||
|
||||
// 判断reference是digest还是tag
|
||||
if strings.HasPrefix(reference, "sha256:") {
|
||||
// 是digest
|
||||
ref, err = name.NewDigest(fmt.Sprintf("%s@%s", imageRef, reference))
|
||||
} else {
|
||||
// 是tag
|
||||
ref, err = name.NewTag(fmt.Sprintf("%s:%s", imageRef, reference))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("解析镜像引用失败: %v\n", err)
|
||||
c.String(http.StatusBadRequest, "Invalid reference")
|
||||
return
|
||||
}
|
||||
|
||||
// 根据请求方法选择操作
|
||||
if c.Request.Method == http.MethodHead {
|
||||
// HEAD请求,使用remote.Head
|
||||
desc, err := remote.Head(ref, dockerProxy.options...)
|
||||
if err != nil {
|
||||
fmt.Printf("HEAD请求失败: %v\n", err)
|
||||
c.String(http.StatusNotFound, "Manifest not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", string(desc.MediaType))
|
||||
c.Header("Docker-Content-Digest", desc.Digest.String())
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", desc.Size))
|
||||
c.Status(http.StatusOK)
|
||||
} else {
|
||||
// GET请求,使用remote.Get
|
||||
desc, err := remote.Get(ref, dockerProxy.options...)
|
||||
if err != nil {
|
||||
fmt.Printf("GET请求失败: %v\n", err)
|
||||
c.String(http.StatusNotFound, "Manifest not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", string(desc.MediaType))
|
||||
c.Header("Docker-Content-Digest", desc.Digest.String())
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", len(desc.Manifest)))
|
||||
|
||||
// 返回manifest内容
|
||||
c.Data(http.StatusOK, string(desc.MediaType), desc.Manifest)
|
||||
}
|
||||
}
|
||||
|
||||
// handleBlobRequest 处理blob请求
|
||||
func handleBlobRequest(c *gin.Context, imageRef, digest string) {
|
||||
// 构建digest引用
|
||||
digestRef, err := name.NewDigest(fmt.Sprintf("%s@%s", imageRef, digest))
|
||||
if err != nil {
|
||||
fmt.Printf("解析digest引用失败: %v\n", err)
|
||||
c.String(http.StatusBadRequest, "Invalid digest reference")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用remote.Layer获取layer
|
||||
layer, err := remote.Layer(digestRef, dockerProxy.options...)
|
||||
if err != nil {
|
||||
fmt.Printf("获取layer失败: %v\n", err)
|
||||
c.String(http.StatusNotFound, "Layer not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取layer信息
|
||||
size, err := layer.Size()
|
||||
if err != nil {
|
||||
fmt.Printf("获取layer大小失败: %v\n", err)
|
||||
c.String(http.StatusInternalServerError, "Failed to get layer size")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取layer内容
|
||||
reader, err := layer.Compressed()
|
||||
if err != nil {
|
||||
fmt.Printf("获取layer内容失败: %v\n", err)
|
||||
c.String(http.StatusInternalServerError, "Failed to get layer content")
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", size))
|
||||
c.Header("Docker-Content-Digest", digest)
|
||||
|
||||
// 流式传输blob内容
|
||||
c.Status(http.StatusOK)
|
||||
io.Copy(c.Writer, reader)
|
||||
}
|
||||
|
||||
// handleTagsRequest 处理tags列表请求
|
||||
func handleTagsRequest(c *gin.Context, imageRef string) {
|
||||
// 解析repository
|
||||
repo, err := name.NewRepository(imageRef)
|
||||
if err != nil {
|
||||
fmt.Printf("解析repository失败: %v\n", err)
|
||||
c.String(http.StatusBadRequest, "Invalid repository")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用remote.List获取tags
|
||||
tags, err := remote.List(repo, dockerProxy.options...)
|
||||
if err != nil {
|
||||
fmt.Printf("获取tags失败: %v\n", err)
|
||||
c.String(http.StatusNotFound, "Tags not found")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
response := map[string]interface{}{
|
||||
"name": strings.TrimPrefix(imageRef, dockerProxy.registry.Name()+"/"),
|
||||
"tags": tags,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// ProxyDockerAuthGin Docker认证代理
|
||||
func ProxyDockerAuthGin(c *gin.Context) {
|
||||
// 构建认证URL
|
||||
authURL := "https://auth.docker.io" + c.Request.URL.Path
|
||||
if c.Request.URL.RawQuery != "" {
|
||||
authURL += "?" + c.Request.URL.RawQuery
|
||||
}
|
||||
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
req, err := http.NewRequestWithContext(
|
||||
context.Background(),
|
||||
c.Request.Method,
|
||||
authURL,
|
||||
c.Request.Body,
|
||||
)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, "Failed to create request")
|
||||
return
|
||||
}
|
||||
|
||||
// 复制请求头
|
||||
for key, values := range c.Request.Header {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.String(http.StatusBadGateway, "Auth request failed")
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 获取当前代理的Host地址
|
||||
proxyHost := c.Request.Host
|
||||
if proxyHost == "" {
|
||||
// 使用配置中的服务器地址和端口
|
||||
cfg := GetConfig()
|
||||
proxyHost = fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
if cfg.Server.Host == "0.0.0.0" {
|
||||
proxyHost = fmt.Sprintf("localhost:%d", cfg.Server.Port)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制响应头并重写认证URL
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
// 重写WWW-Authenticate头中的realm URL
|
||||
if key == "Www-Authenticate" && strings.Contains(value, "auth.docker.io") {
|
||||
value = strings.ReplaceAll(value, "https://auth.docker.io", "http://"+proxyHost)
|
||||
}
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// 返回响应
|
||||
c.Status(resp.StatusCode)
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
@@ -1,41 +1,51 @@
|
||||
module ghproxy
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/time v0.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
module hubproxy
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/google/go-containerregistry v0.20.5
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/pelletier/go-toml/v2 v2.2.2
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/time v0.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.36.3 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -1,95 +1,120 @@
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
|
||||
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.20.5 h1:4RnlYcDs5hoA++CeFjlbZ/U9Yp1EuWr+UhhTyYQjOP0=
|
||||
github.com/google/go-containerregistry v0.20.5/go.mod h1:Q14vdOOzug02bwnhMkZKD4e30pDaD9W65qzXpyzF49E=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/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=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
59
src/http_client.go
Normal file
59
src/http_client.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// 全局HTTP客户端 - 用于代理请求(长超时)
|
||||
globalHTTPClient *http.Client
|
||||
// 搜索HTTP客户端 - 用于API请求(短超时)
|
||||
searchHTTPClient *http.Client
|
||||
)
|
||||
|
||||
// initHTTPClients 初始化HTTP客户端
|
||||
func initHTTPClients() {
|
||||
// 代理客户端配置 - 适用于大文件传输
|
||||
globalHTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 300 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
// 搜索客户端配置 - 适用于API调用
|
||||
searchHTTPClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
DisableCompression: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetGlobalHTTPClient 获取全局HTTP客户端(用于代理)
|
||||
func GetGlobalHTTPClient() *http.Client {
|
||||
return globalHTTPClient
|
||||
}
|
||||
|
||||
// GetSearchHTTPClient 获取搜索HTTP客户端(用于API调用)
|
||||
func GetSearchHTTPClient() *http.Client {
|
||||
return searchHTTPClient
|
||||
}
|
||||
@@ -1,280 +1,246 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
sizeLimit = 1024 * 1024 * 1024 * 2 // 允许的文件大小,默认2GB
|
||||
host = "0.0.0.0" // 监听地址
|
||||
port = 5000 // 监听端口
|
||||
)
|
||||
|
||||
var (
|
||||
exps = []*regexp.Regexp{
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*$`),
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*$`),
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*$`),
|
||||
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+$`),
|
||||
regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/.+?/.+`),
|
||||
regexp.MustCompile(`^(?:https?://)?api\.github\.com/repos/([^/]+)/([^/]+)/.*`),
|
||||
regexp.MustCompile(`^(?:https?://)?huggingface\.co(?:/spaces)?/([^/]+)/(.+)$`),
|
||||
regexp.MustCompile(`^(?:https?://)?cdn-lfs\.hf\.co(?:/spaces)?/([^/]+)/([^/]+)(?:/(.*))?$`),
|
||||
regexp.MustCompile(`^(?:https?://)?download\.docker\.com/([^/]+)/.*\.(tgz|zip)$`),
|
||||
regexp.MustCompile(`^(?:https?://)?(github|opengraph)\.githubassets\.com/([^/]+)/.+?$`),
|
||||
}
|
||||
httpClient *http.Client
|
||||
config *Config
|
||||
configLock sync.RWMutex
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
WhiteList []string `json:"whiteList"`
|
||||
BlackList []string `json:"blackList"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.Default()
|
||||
|
||||
httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).DialContext,
|
||||
MaxIdleConns: 1000,
|
||||
MaxIdleConnsPerHost: 1000,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
ResponseHeaderTimeout: 300 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
loadConfig()
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(10 * time.Minute)
|
||||
loadConfig()
|
||||
}
|
||||
}()
|
||||
|
||||
// 初始化Skopeo相关路由 - 在任何通配符路由之前注册
|
||||
initSkopeoRoutes(router)
|
||||
|
||||
// 单独处理根路径请求,避免冲突
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.File("./public/index.html")
|
||||
})
|
||||
|
||||
// 指定具体的静态文件路径,避免使用通配符
|
||||
router.Static("/public", "./public")
|
||||
|
||||
// 对于.html等特定文件注册
|
||||
router.GET("/skopeo.html", func(c *gin.Context) {
|
||||
c.File("./public/skopeo.html")
|
||||
})
|
||||
router.GET("/search.html", func(c *gin.Context) {
|
||||
c.File("./public/search.html")
|
||||
})
|
||||
|
||||
// 图标文件
|
||||
router.GET("/favicon.ico", func(c *gin.Context) {
|
||||
c.File("./public/favicon.ico")
|
||||
})
|
||||
|
||||
// 注册dockerhub搜索路由
|
||||
RegisterSearchRoute(router)
|
||||
// 创建GitHub文件下载专用的限流器
|
||||
githubLimiter := NewIPRateLimiter()
|
||||
|
||||
// 注册NoRoute处理器,应用限流中间件
|
||||
router.NoRoute(RateLimitMiddleware(githubLimiter), handler)
|
||||
|
||||
err := router.Run(fmt.Sprintf("%s:%d", host, port))
|
||||
if err != nil {
|
||||
fmt.Printf("Error starting server: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handler(c *gin.Context) {
|
||||
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/")
|
||||
|
||||
for strings.HasPrefix(rawPath, "/") {
|
||||
rawPath = strings.TrimPrefix(rawPath, "/")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(rawPath, "http") {
|
||||
c.String(http.StatusForbidden, "无效输入")
|
||||
return
|
||||
}
|
||||
|
||||
matches := checkURL(rawPath)
|
||||
if matches != nil {
|
||||
if len(config.WhiteList) > 0 && !checkList(matches, config.WhiteList) {
|
||||
c.String(http.StatusForbidden, "不在白名单内,限制访问。")
|
||||
return
|
||||
}
|
||||
if len(config.BlackList) > 0 && checkList(matches, config.BlackList) {
|
||||
c.String(http.StatusForbidden, "黑名单限制访问")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.String(http.StatusForbidden, "无效输入")
|
||||
return
|
||||
}
|
||||
|
||||
if exps[1].MatchString(rawPath) {
|
||||
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
||||
}
|
||||
|
||||
proxy(c, rawPath)
|
||||
}
|
||||
|
||||
func proxy(c *gin.Context, u string) {
|
||||
req, err := http.NewRequest(c.Request.Method, u, c.Request.Body)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
for key, values := range c.Request.Header {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
req.Header.Del("Host")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err))
|
||||
return
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
// 检查文件大小限制
|
||||
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
||||
if size, err := strconv.Atoi(contentLength); err == nil && size > sizeLimit {
|
||||
c.String(http.StatusRequestEntityTooLarge, "File too large.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 清理安全相关的头
|
||||
resp.Header.Del("Content-Security-Policy")
|
||||
resp.Header.Del("Referrer-Policy")
|
||||
resp.Header.Del("Strict-Transport-Security")
|
||||
|
||||
// 对于需要处理的shell文件,使用chunked传输
|
||||
isShellFile := strings.HasSuffix(strings.ToLower(u), ".sh")
|
||||
if isShellFile {
|
||||
resp.Header.Del("Content-Length")
|
||||
resp.Header.Set("Transfer-Encoding", "chunked")
|
||||
}
|
||||
|
||||
// 复制其他响应头
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if location := resp.Header.Get("Location"); location != "" {
|
||||
if checkURL(location) != nil {
|
||||
c.Header("Location", "/"+location)
|
||||
} else {
|
||||
proxy(c, location)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
|
||||
// 处理响应体
|
||||
if isShellFile {
|
||||
// 获取真实域名
|
||||
realHost := c.Request.Header.Get("X-Forwarded-Host")
|
||||
if realHost == "" {
|
||||
realHost = c.Request.Host
|
||||
}
|
||||
// 如果域名中没有协议前缀,添加https://
|
||||
if !strings.HasPrefix(realHost, "http://") && !strings.HasPrefix(realHost, "https://") {
|
||||
realHost = "https://" + realHost
|
||||
}
|
||||
// 使用ProcessGitHubURLs处理.sh文件
|
||||
processedBody, _, err := ProcessGitHubURLs(resp.Body, resp.Header.Get("Content-Encoding") == "gzip", realHost, true)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("处理shell文件时发生错误: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(c.Writer, processedBody); err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("写入响应时发生错误: %v", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 对于非.sh文件,直接复制响应体
|
||||
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() {
|
||||
file, err := os.Open("config.json")
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer func(file *os.File) {
|
||||
err := file.Close()
|
||||
if err != nil {
|
||||
|
||||
}
|
||||
}(file)
|
||||
|
||||
var newConfig Config
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(&newConfig); err != nil {
|
||||
fmt.Printf("Error decoding config: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
configLock.Lock()
|
||||
config = &newConfig
|
||||
configLock.Unlock()
|
||||
}
|
||||
|
||||
func checkURL(u string) []string {
|
||||
for _, exp := range exps {
|
||||
if matches := exp.FindStringSubmatch(u); matches != nil {
|
||||
return matches[1:]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkList(matches, list []string) bool {
|
||||
for _, item := range list {
|
||||
if strings.HasPrefix(matches[0], item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
exps = []*regexp.Regexp{
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*$`),
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*$`),
|
||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*$`),
|
||||
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+$`),
|
||||
regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/.+?/.+`),
|
||||
regexp.MustCompile(`^(?:https?://)?api\.github\.com/repos/([^/]+)/([^/]+)/.*`),
|
||||
regexp.MustCompile(`^(?:https?://)?huggingface\.co(?:/spaces)?/([^/]+)/(.+)$`),
|
||||
regexp.MustCompile(`^(?:https?://)?cdn-lfs\.hf\.co(?:/spaces)?/([^/]+)/([^/]+)(?:/(.*))?$`),
|
||||
regexp.MustCompile(`^(?:https?://)?download\.docker\.com/([^/]+)/.*\.(tgz|zip)$`),
|
||||
regexp.MustCompile(`^(?:https?://)?(github|opengraph)\.githubassets\.com/([^/]+)/.+?$`),
|
||||
}
|
||||
globalLimiter *IPRateLimiter
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 加载配置
|
||||
if err := LoadConfig(); err != nil {
|
||||
fmt.Printf("配置加载失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化HTTP客户端
|
||||
initHTTPClients()
|
||||
|
||||
// 初始化限流器
|
||||
initLimiter()
|
||||
|
||||
// 初始化Docker流式代理
|
||||
initDockerProxy()
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := gin.Default()
|
||||
|
||||
// 初始化skopeo路由(静态文件和API路由)
|
||||
initSkopeoRoutes(router)
|
||||
|
||||
// 单独处理根路径请求
|
||||
router.GET("/", func(c *gin.Context) {
|
||||
c.File("./public/index.html")
|
||||
})
|
||||
|
||||
// 指定具体的静态文件路径
|
||||
router.Static("/public", "./public")
|
||||
router.GET("/skopeo.html", func(c *gin.Context) {
|
||||
c.File("./public/skopeo.html")
|
||||
})
|
||||
router.GET("/search.html", func(c *gin.Context) {
|
||||
c.File("./public/search.html")
|
||||
})
|
||||
router.GET("/favicon.ico", func(c *gin.Context) {
|
||||
c.File("./public/favicon.ico")
|
||||
})
|
||||
|
||||
// 注册dockerhub搜索路由
|
||||
RegisterSearchRoute(router)
|
||||
|
||||
// 注册Docker认证路由(/token*)
|
||||
router.Any("/token", RateLimitMiddleware(globalLimiter), ProxyDockerAuthGin)
|
||||
router.Any("/token/*path", RateLimitMiddleware(globalLimiter), ProxyDockerAuthGin)
|
||||
|
||||
// 注册Docker Registry代理路由
|
||||
router.Any("/v2/*path", RateLimitMiddleware(globalLimiter), ProxyDockerRegistryGin)
|
||||
|
||||
|
||||
// 注册NoRoute处理器,应用限流中间件
|
||||
router.NoRoute(RateLimitMiddleware(globalLimiter), handler)
|
||||
|
||||
cfg := GetConfig()
|
||||
fmt.Printf("启动成功,项目地址:https://github.com/sky22333/hubproxy \n")
|
||||
|
||||
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port))
|
||||
if err != nil {
|
||||
fmt.Printf("启动服务失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func handler(c *gin.Context) {
|
||||
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/")
|
||||
|
||||
for strings.HasPrefix(rawPath, "/") {
|
||||
rawPath = strings.TrimPrefix(rawPath, "/")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(rawPath, "http") {
|
||||
c.String(http.StatusForbidden, "无效输入")
|
||||
return
|
||||
}
|
||||
|
||||
matches := checkURL(rawPath)
|
||||
if matches != nil {
|
||||
// GitHub仓库访问控制检查
|
||||
if allowed, reason := GlobalAccessController.CheckGitHubAccess(matches); !allowed {
|
||||
// 构建仓库名用于日志
|
||||
var repoPath string
|
||||
if len(matches) >= 2 {
|
||||
username := matches[0]
|
||||
repoName := strings.TrimSuffix(matches[1], ".git")
|
||||
repoPath = username + "/" + repoName
|
||||
}
|
||||
fmt.Printf("GitHub仓库 %s 访问被拒绝: %s\n", repoPath, reason)
|
||||
c.String(http.StatusForbidden, reason)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
c.String(http.StatusForbidden, "无效输入")
|
||||
return
|
||||
}
|
||||
|
||||
if exps[1].MatchString(rawPath) {
|
||||
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
||||
}
|
||||
|
||||
proxy(c, rawPath)
|
||||
}
|
||||
|
||||
|
||||
func proxy(c *gin.Context, u string) {
|
||||
proxyWithRedirect(c, u, 0)
|
||||
}
|
||||
|
||||
|
||||
func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
|
||||
// 限制最大重定向次数,防止无限递归
|
||||
const maxRedirects = 20
|
||||
if redirectCount > maxRedirects {
|
||||
c.String(http.StatusLoopDetected, "重定向次数过多,可能存在循环重定向")
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequest(c.Request.Method, u, c.Request.Body)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
for key, values := range c.Request.Header {
|
||||
for _, value := range values {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
}
|
||||
req.Header.Del("Host")
|
||||
|
||||
resp, err := GetGlobalHTTPClient().Do(req)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err))
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
fmt.Printf("关闭响应体失败: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 检查文件大小限制
|
||||
cfg := GetConfig()
|
||||
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
||||
if size, err := strconv.ParseInt(contentLength, 10, 64); err == nil && size > cfg.Server.FileSize {
|
||||
c.String(http.StatusRequestEntityTooLarge,
|
||||
fmt.Sprintf("文件过大,限制大小: %d MB", cfg.Server.FileSize/(1024*1024)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 清理安全相关的头
|
||||
resp.Header.Del("Content-Security-Policy")
|
||||
resp.Header.Del("Referrer-Policy")
|
||||
resp.Header.Del("Strict-Transport-Security")
|
||||
|
||||
// 对于需要处理的shell文件,使用chunked传输
|
||||
isShellFile := strings.HasSuffix(strings.ToLower(u), ".sh")
|
||||
if isShellFile {
|
||||
resp.Header.Del("Content-Length")
|
||||
resp.Header.Set("Transfer-Encoding", "chunked")
|
||||
}
|
||||
|
||||
// 复制其他响应头
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Header(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
if location := resp.Header.Get("Location"); location != "" {
|
||||
if checkURL(location) != nil {
|
||||
c.Header("Location", "/"+location)
|
||||
} else {
|
||||
// 递归处理重定向,增加计数防止无限循环
|
||||
proxyWithRedirect(c, location, redirectCount+1)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
|
||||
// 处理响应体
|
||||
if isShellFile {
|
||||
// 获取真实域名
|
||||
realHost := c.Request.Header.Get("X-Forwarded-Host")
|
||||
if realHost == "" {
|
||||
realHost = c.Request.Host
|
||||
}
|
||||
// 如果域名中没有协议前缀,添加https://
|
||||
if !strings.HasPrefix(realHost, "http://") && !strings.HasPrefix(realHost, "https://") {
|
||||
realHost = "https://" + realHost
|
||||
}
|
||||
// 使用ProcessGitHubURLs处理.sh文件
|
||||
processedBody, _, err := ProcessGitHubURLs(resp.Body, resp.Header.Get("Content-Encoding") == "gzip", realHost, true)
|
||||
if err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("处理shell文件时发生错误: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err := io.Copy(c.Writer, processedBody); err != nil {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("写入响应时发生错误: %v", err))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 对于非.sh文件,直接复制响应体
|
||||
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkURL(u string) []string {
|
||||
for _, exp := range exps {
|
||||
if matches := exp.FindStringSubmatch(u); matches != nil {
|
||||
return matches[1:]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,192 +1,192 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// gitHubDomains 定义所有支持的GitHub相关域名
|
||||
gitHubDomains = []string{
|
||||
"github.com",
|
||||
"raw.githubusercontent.com",
|
||||
"raw.github.com",
|
||||
"gist.githubusercontent.com",
|
||||
"gist.github.com",
|
||||
"api.github.com",
|
||||
}
|
||||
|
||||
// urlPattern 使用gitHubDomains构建正则表达式
|
||||
urlPattern = regexp.MustCompile(`https?://(?:` + strings.Join(gitHubDomains, "|") + `)[^\s'"]+`)
|
||||
|
||||
// 是否启用脚本嵌套代理的调试日志
|
||||
DebugLog = true
|
||||
)
|
||||
|
||||
// 打印调试日志的辅助函数
|
||||
func debugPrintf(format string, args ...interface{}) {
|
||||
if DebugLog {
|
||||
fmt.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessGitHubURLs 处理数据流中的GitHub URL,将其替换为代理URL。
|
||||
// 此处思路借鉴了 https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/proxy/nest.go
|
||||
|
||||
func ProcessGitHubURLs(input io.ReadCloser, isCompressed bool, host string, isShellFile bool) (io.Reader, int64, error) {
|
||||
debugPrintf("开始处理文件: isCompressed=%v, host=%s, isShellFile=%v\n", isCompressed, host, isShellFile)
|
||||
|
||||
if !isShellFile {
|
||||
debugPrintf("非shell文件,跳过处理\n")
|
||||
return input, 0, nil
|
||||
}
|
||||
|
||||
// 使用更大的缓冲区以提高性能
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
var written int64
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
debugPrintf("处理过程中发生错误: %v\n", err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
} else {
|
||||
_ = pipeWriter.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
defer input.Close()
|
||||
|
||||
var reader io.Reader = input
|
||||
if isCompressed {
|
||||
debugPrintf("检测到压缩文件,进行解压处理\n")
|
||||
gzipReader, gzipErr := gzip.NewReader(input)
|
||||
if gzipErr != nil {
|
||||
err = gzipErr
|
||||
return
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
reader = gzipReader
|
||||
}
|
||||
|
||||
// 使用更大的缓冲区
|
||||
bufReader := bufio.NewReaderSize(reader, 32*1024) // 32KB buffer
|
||||
var writer io.Writer = pipeWriter
|
||||
|
||||
if isCompressed {
|
||||
gzipWriter := gzip.NewWriter(writer)
|
||||
defer gzipWriter.Close()
|
||||
writer = gzipWriter
|
||||
}
|
||||
|
||||
bufWriter := bufio.NewWriterSize(writer, 32*1024) // 32KB buffer
|
||||
defer bufWriter.Flush()
|
||||
|
||||
written, err = processContent(bufReader, bufWriter, host)
|
||||
if err != nil {
|
||||
debugPrintf("处理内容时发生错误: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
debugPrintf("文件处理完成,共处理 %d 字节\n", written)
|
||||
}()
|
||||
|
||||
return pipeReader, written, nil
|
||||
}
|
||||
|
||||
// processContent 优化处理文件内容的函数
|
||||
func processContent(reader *bufio.Reader, writer *bufio.Writer, host string) (int64, error) {
|
||||
var written int64
|
||||
lineNum := 0
|
||||
|
||||
for {
|
||||
lineNum++
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return written, fmt.Errorf("读取行时发生错误: %w", err)
|
||||
}
|
||||
|
||||
if line != "" {
|
||||
// 在处理前先检查是否包含GitHub URL
|
||||
if strings.Contains(line, "github.com") ||
|
||||
strings.Contains(line, "raw.githubusercontent.com") {
|
||||
matches := urlPattern.FindAllString(line, -1)
|
||||
if len(matches) > 0 {
|
||||
debugPrintf("\n在第 %d 行发现 %d 个GitHub URL:\n", lineNum, len(matches))
|
||||
for _, match := range matches {
|
||||
debugPrintf("原始URL: %s\n", match)
|
||||
}
|
||||
}
|
||||
|
||||
modifiedLine := processLine(line, host, lineNum)
|
||||
n, writeErr := writer.WriteString(modifiedLine)
|
||||
if writeErr != nil {
|
||||
return written, fmt.Errorf("写入修改后的行时发生错误: %w", writeErr)
|
||||
}
|
||||
written += int64(n)
|
||||
} else {
|
||||
// 如果行中没有GitHub URL,直接写入
|
||||
n, writeErr := writer.WriteString(line)
|
||||
if writeErr != nil {
|
||||
return written, fmt.Errorf("写入原始行时发生错误: %w", writeErr)
|
||||
}
|
||||
written += int64(n)
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 确保所有数据都被写入
|
||||
if err := writer.Flush(); err != nil {
|
||||
return written, fmt.Errorf("刷新缓冲区时发生错误: %w", err)
|
||||
}
|
||||
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// processLine 处理单行文本,替换所有匹配的GitHub URL
|
||||
func processLine(line string, host string, lineNum int) string {
|
||||
return urlPattern.ReplaceAllStringFunc(line, func(url string) string {
|
||||
newURL := modifyGitHubURL(url, host)
|
||||
if newURL != url {
|
||||
debugPrintf("第 %d 行URL替换:\n 原始: %s\n 替换后: %s\n", lineNum, url, newURL)
|
||||
}
|
||||
return newURL
|
||||
})
|
||||
}
|
||||
|
||||
// modifyGitHubURL 修改GitHub URL,添加代理域名前缀
|
||||
func modifyGitHubURL(url string, host string) string {
|
||||
for _, domain := range gitHubDomains {
|
||||
hasHttps := strings.HasPrefix(url, "https://"+domain)
|
||||
hasHttp := strings.HasPrefix(url, "http://"+domain)
|
||||
|
||||
if hasHttps || hasHttp || strings.HasPrefix(url, domain) {
|
||||
if !hasHttps && !hasHttp {
|
||||
url = "https://" + url
|
||||
}
|
||||
if hasHttp {
|
||||
url = "https://" + strings.TrimPrefix(url, "http://")
|
||||
}
|
||||
// 移除host开头的协议头(如果有)
|
||||
host = strings.TrimPrefix(host, "https://")
|
||||
host = strings.TrimPrefix(host, "http://")
|
||||
// 返回组合后的URL
|
||||
return host + "/" + url
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// IsShellFile 检查文件是否为shell文件(基于文件名)
|
||||
func IsShellFile(filename string) bool {
|
||||
return strings.HasSuffix(filename, ".sh")
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// gitHubDomains 定义所有支持的GitHub相关域名
|
||||
gitHubDomains = []string{
|
||||
"github.com",
|
||||
"raw.githubusercontent.com",
|
||||
"raw.github.com",
|
||||
"gist.githubusercontent.com",
|
||||
"gist.github.com",
|
||||
"api.github.com",
|
||||
}
|
||||
|
||||
// urlPattern 使用gitHubDomains构建正则表达式
|
||||
urlPattern = regexp.MustCompile(`https?://(?:` + strings.Join(gitHubDomains, "|") + `)[^\s'"]+`)
|
||||
|
||||
// 是否启用脚本嵌套代理的调试日志
|
||||
DebugLog = true
|
||||
)
|
||||
|
||||
// 打印调试日志的辅助函数
|
||||
func debugPrintf(format string, args ...interface{}) {
|
||||
if DebugLog {
|
||||
fmt.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessGitHubURLs 处理数据流中的GitHub URL,将其替换为代理URL。
|
||||
// 此处思路借鉴了 https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/proxy/nest.go
|
||||
|
||||
func ProcessGitHubURLs(input io.ReadCloser, isCompressed bool, host string, isShellFile bool) (io.Reader, int64, error) {
|
||||
debugPrintf("开始处理文件: isCompressed=%v, host=%s, isShellFile=%v\n", isCompressed, host, isShellFile)
|
||||
|
||||
if !isShellFile {
|
||||
debugPrintf("非shell文件,跳过处理\n")
|
||||
return input, 0, nil
|
||||
}
|
||||
|
||||
// 使用更大的缓冲区以提高性能
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
var written int64
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
defer func() {
|
||||
if err != nil {
|
||||
debugPrintf("处理过程中发生错误: %v\n", err)
|
||||
_ = pipeWriter.CloseWithError(err)
|
||||
} else {
|
||||
_ = pipeWriter.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
defer input.Close()
|
||||
|
||||
var reader io.Reader = input
|
||||
if isCompressed {
|
||||
debugPrintf("检测到压缩文件,进行解压处理\n")
|
||||
gzipReader, gzipErr := gzip.NewReader(input)
|
||||
if gzipErr != nil {
|
||||
err = gzipErr
|
||||
return
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
reader = gzipReader
|
||||
}
|
||||
|
||||
// 使用更大的缓冲区
|
||||
bufReader := bufio.NewReaderSize(reader, 32*1024) // 32KB buffer
|
||||
var writer io.Writer = pipeWriter
|
||||
|
||||
if isCompressed {
|
||||
gzipWriter := gzip.NewWriter(writer)
|
||||
defer gzipWriter.Close()
|
||||
writer = gzipWriter
|
||||
}
|
||||
|
||||
bufWriter := bufio.NewWriterSize(writer, 32*1024) // 32KB buffer
|
||||
defer bufWriter.Flush()
|
||||
|
||||
written, err = processContent(bufReader, bufWriter, host)
|
||||
if err != nil {
|
||||
debugPrintf("处理内容时发生错误: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
debugPrintf("文件处理完成,共处理 %d 字节\n", written)
|
||||
}()
|
||||
|
||||
return pipeReader, written, nil
|
||||
}
|
||||
|
||||
// processContent 优化处理文件内容的函数
|
||||
func processContent(reader *bufio.Reader, writer *bufio.Writer, host string) (int64, error) {
|
||||
var written int64
|
||||
lineNum := 0
|
||||
|
||||
for {
|
||||
lineNum++
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil && err != io.EOF {
|
||||
return written, fmt.Errorf("读取行时发生错误: %w", err)
|
||||
}
|
||||
|
||||
if line != "" {
|
||||
// 在处理前先检查是否包含GitHub URL
|
||||
if strings.Contains(line, "github.com") ||
|
||||
strings.Contains(line, "raw.githubusercontent.com") {
|
||||
matches := urlPattern.FindAllString(line, -1)
|
||||
if len(matches) > 0 {
|
||||
debugPrintf("\n在第 %d 行发现 %d 个GitHub URL:\n", lineNum, len(matches))
|
||||
for _, match := range matches {
|
||||
debugPrintf("原始URL: %s\n", match)
|
||||
}
|
||||
}
|
||||
|
||||
modifiedLine := processLine(line, host, lineNum)
|
||||
n, writeErr := writer.WriteString(modifiedLine)
|
||||
if writeErr != nil {
|
||||
return written, fmt.Errorf("写入修改后的行时发生错误: %w", writeErr)
|
||||
}
|
||||
written += int64(n)
|
||||
} else {
|
||||
// 如果行中没有GitHub URL,直接写入
|
||||
n, writeErr := writer.WriteString(line)
|
||||
if writeErr != nil {
|
||||
return written, fmt.Errorf("写入原始行时发生错误: %w", writeErr)
|
||||
}
|
||||
written += int64(n)
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 确保所有数据都被写入
|
||||
if err := writer.Flush(); err != nil {
|
||||
return written, fmt.Errorf("刷新缓冲区时发生错误: %w", err)
|
||||
}
|
||||
|
||||
return written, nil
|
||||
}
|
||||
|
||||
// processLine 处理单行文本,替换所有匹配的GitHub URL
|
||||
func processLine(line string, host string, lineNum int) string {
|
||||
return urlPattern.ReplaceAllStringFunc(line, func(url string) string {
|
||||
newURL := modifyGitHubURL(url, host)
|
||||
if newURL != url {
|
||||
debugPrintf("第 %d 行URL替换:\n 原始: %s\n 替换后: %s\n", lineNum, url, newURL)
|
||||
}
|
||||
return newURL
|
||||
})
|
||||
}
|
||||
|
||||
// 判断代理域名前缀
|
||||
func modifyGitHubURL(url string, host string) string {
|
||||
for _, domain := range gitHubDomains {
|
||||
hasHttps := strings.HasPrefix(url, "https://"+domain)
|
||||
hasHttp := strings.HasPrefix(url, "http://"+domain)
|
||||
|
||||
if hasHttps || hasHttp || strings.HasPrefix(url, domain) {
|
||||
if !hasHttps && !hasHttp {
|
||||
url = "https://" + url
|
||||
}
|
||||
if hasHttp {
|
||||
url = "https://" + strings.TrimPrefix(url, "http://")
|
||||
}
|
||||
// 移除host开头的协议头(如果有)
|
||||
host = strings.TrimPrefix(host, "https://")
|
||||
host = strings.TrimPrefix(host, "http://")
|
||||
// 返回组合后的URL
|
||||
return host + "/" + url
|
||||
}
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// IsShellFile 检查文件是否为shell文件(基于文件名)
|
||||
func IsShellFile(filename string) bool {
|
||||
return strings.HasSuffix(filename, ".sh")
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -13,33 +11,14 @@ import (
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// IP限流配置
|
||||
var (
|
||||
// 默认限流:每个IP每1小时允许20个请求
|
||||
DefaultRateLimit = 20.0 // 默认限制请求数
|
||||
DefaultRatePeriodHours = 1.0 // 默认时间周期(小时)
|
||||
|
||||
// 白名单列表,支持IP和CIDR格式,如:"192.168.1.1", "10.0.0.0/8"
|
||||
WhitelistIPs = []string{
|
||||
"127.0.0.1", // 本地回环地址
|
||||
"10.0.0.0/8", // 内网地址段
|
||||
"172.16.0.0/12", // 内网地址段
|
||||
"192.168.0.0/16", // 内网地址段
|
||||
}
|
||||
|
||||
// 黑名单列表,支持IP和CIDR格式
|
||||
BlacklistIPs = []string{
|
||||
// 示例: "1.2.3.4", "5.6.7.0/24"
|
||||
}
|
||||
|
||||
// 清理间隔:多久清理一次过期的限流器
|
||||
CleanupInterval = 1 * time.Hour
|
||||
|
||||
// IP限流器缓存上限,超过此数量将触发清理
|
||||
const (
|
||||
// 清理间隔
|
||||
CleanupInterval = 10 * time.Minute
|
||||
// 最大IP缓存数量,防止内存过度占用
|
||||
MaxIPCacheSize = 10000
|
||||
)
|
||||
|
||||
// IPRateLimiter 定义IP限流器结构
|
||||
// IPRateLimiter IP限流器结构体
|
||||
type IPRateLimiter struct {
|
||||
ips map[string]*rateLimiterEntry // IP到限流器的映射
|
||||
mu *sync.RWMutex // 读写锁,保证并发安全
|
||||
@@ -49,45 +28,20 @@ type IPRateLimiter struct {
|
||||
blacklist []*net.IPNet // 黑名单IP段
|
||||
}
|
||||
|
||||
// rateLimiterEntry 限流器条目,包含限流器和最后访问时间
|
||||
// rateLimiterEntry 限流器条目
|
||||
type rateLimiterEntry struct {
|
||||
limiter *rate.Limiter // 限流器
|
||||
lastAccess time.Time // 最后访问时间
|
||||
}
|
||||
|
||||
// NewIPRateLimiter 创建新的IP限流器
|
||||
func NewIPRateLimiter() *IPRateLimiter {
|
||||
// 从环境变量读取限流配置(如果有)
|
||||
rateLimit := DefaultRateLimit
|
||||
ratePeriod := DefaultRatePeriodHours
|
||||
|
||||
if val, exists := os.LookupEnv("RATE_LIMIT"); exists {
|
||||
if parsed, err := strconv.ParseFloat(val, 64); err == nil && parsed > 0 {
|
||||
rateLimit = parsed
|
||||
}
|
||||
}
|
||||
|
||||
if val, exists := os.LookupEnv("RATE_PERIOD_HOURS"); exists {
|
||||
if parsed, err := strconv.ParseFloat(val, 64); err == nil && parsed > 0 {
|
||||
ratePeriod = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// 从环境变量读取白名单(如果有)
|
||||
whitelistIPs := WhitelistIPs
|
||||
if val, exists := os.LookupEnv("IP_WHITELIST"); exists && val != "" {
|
||||
whitelistIPs = append(whitelistIPs, strings.Split(val, ",")...)
|
||||
}
|
||||
|
||||
// 从环境变量读取黑名单(如果有)
|
||||
blacklistIPs := BlacklistIPs
|
||||
if val, exists := os.LookupEnv("IP_BLACKLIST"); exists && val != "" {
|
||||
blacklistIPs = append(blacklistIPs, strings.Split(val, ",")...)
|
||||
}
|
||||
// initGlobalLimiter 初始化全局限流器
|
||||
func initGlobalLimiter() *IPRateLimiter {
|
||||
// 获取配置
|
||||
cfg := GetConfig()
|
||||
|
||||
// 解析白名单IP段
|
||||
whitelist := make([]*net.IPNet, 0, len(whitelistIPs))
|
||||
for _, item := range whitelistIPs {
|
||||
whitelist := make([]*net.IPNet, 0, len(cfg.Security.WhiteList))
|
||||
for _, item := range cfg.Security.WhiteList {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
if !strings.Contains(item, "/") {
|
||||
item = item + "/32" // 单个IP转为CIDR格式
|
||||
@@ -95,13 +49,15 @@ func NewIPRateLimiter() *IPRateLimiter {
|
||||
_, ipnet, err := net.ParseCIDR(item)
|
||||
if err == nil {
|
||||
whitelist = append(whitelist, ipnet)
|
||||
} else {
|
||||
fmt.Printf("警告: 无效的白名单IP格式: %s\n", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析黑名单IP段
|
||||
blacklist := make([]*net.IPNet, 0, len(blacklistIPs))
|
||||
for _, item := range blacklistIPs {
|
||||
blacklist := make([]*net.IPNet, 0, len(cfg.Security.BlackList))
|
||||
for _, item := range cfg.Security.BlackList {
|
||||
if item = strings.TrimSpace(item); item != "" {
|
||||
if !strings.Contains(item, "/") {
|
||||
item = item + "/32" // 单个IP转为CIDR格式
|
||||
@@ -109,19 +65,26 @@ func NewIPRateLimiter() *IPRateLimiter {
|
||||
_, ipnet, err := net.ParseCIDR(item)
|
||||
if err == nil {
|
||||
blacklist = append(blacklist, ipnet)
|
||||
} else {
|
||||
fmt.Printf("警告: 无效的黑名单IP格式: %s\n", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算速率:将 "每N小时X个请求" 转换为 "每秒Y个请求"
|
||||
// rate.Limit的单位是每秒允许的请求数
|
||||
ratePerSecond := rate.Limit(rateLimit / (ratePeriod * 3600))
|
||||
ratePerSecond := rate.Limit(float64(cfg.RateLimit.RequestLimit) / (cfg.RateLimit.PeriodHours * 3600))
|
||||
|
||||
// 令牌桶容量设置为最大突发请求数,建议设为限制值的一半以允许合理突发
|
||||
burstSize := cfg.RateLimit.RequestLimit
|
||||
if burstSize < 1 {
|
||||
burstSize = 1 // 至少允许1个请求
|
||||
}
|
||||
|
||||
limiter := &IPRateLimiter{
|
||||
ips: make(map[string]*rateLimiterEntry),
|
||||
mu: &sync.RWMutex{},
|
||||
r: ratePerSecond,
|
||||
b: int(rateLimit), // 令牌桶容量设为允许的请求总数
|
||||
b: burstSize,
|
||||
whitelist: whitelist,
|
||||
blacklist: blacklist,
|
||||
}
|
||||
@@ -129,9 +92,17 @@ func NewIPRateLimiter() *IPRateLimiter {
|
||||
// 启动定期清理goroutine
|
||||
go limiter.cleanupRoutine()
|
||||
|
||||
fmt.Printf("限流器初始化: %d请求/%g小时, 白名单 %d个, 黑名单 %d个\n",
|
||||
cfg.RateLimit.RequestLimit, cfg.RateLimit.PeriodHours, len(whitelist), len(blacklist))
|
||||
|
||||
return limiter
|
||||
}
|
||||
|
||||
// initLimiter 初始化限流器(保持向后兼容)
|
||||
func initLimiter() {
|
||||
globalLimiter = initGlobalLimiter()
|
||||
}
|
||||
|
||||
// cleanupRoutine 定期清理过期的限流器
|
||||
func (i *IPRateLimiter) cleanupRoutine() {
|
||||
ticker := time.NewTicker(CleanupInterval)
|
||||
@@ -168,9 +139,29 @@ func (i *IPRateLimiter) cleanupRoutine() {
|
||||
}
|
||||
}
|
||||
|
||||
// extractIPFromAddress 从地址中提取纯IP,去除端口号
|
||||
func extractIPFromAddress(address string) string {
|
||||
// 处理IPv6地址 [::1]:8080 格式
|
||||
if strings.HasPrefix(address, "[") {
|
||||
if endIndex := strings.Index(address, "]"); endIndex != -1 {
|
||||
return address[1:endIndex]
|
||||
}
|
||||
}
|
||||
|
||||
// 处理IPv4地址 192.168.1.1:8080 格式
|
||||
if lastColon := strings.LastIndex(address, ":"); lastColon != -1 {
|
||||
return address[:lastColon]
|
||||
}
|
||||
|
||||
// 如果没有端口号,直接返回
|
||||
return address
|
||||
}
|
||||
|
||||
// isIPInCIDRList 检查IP是否在CIDR列表中
|
||||
func isIPInCIDRList(ip string, cidrList []*net.IPNet) bool {
|
||||
parsedIP := net.ParseIP(ip)
|
||||
// 先提取纯IP地址
|
||||
cleanIP := extractIPFromAddress(ip)
|
||||
parsedIP := net.ParseIP(cleanIP)
|
||||
if parsedIP == nil {
|
||||
return false
|
||||
}
|
||||
@@ -185,19 +176,22 @@ func isIPInCIDRList(ip string, cidrList []*net.IPNet) bool {
|
||||
|
||||
// GetLimiter 获取指定IP的限流器,同时返回是否允许访问
|
||||
func (i *IPRateLimiter) GetLimiter(ip string) (*rate.Limiter, bool) {
|
||||
// 提取纯IP地址
|
||||
cleanIP := extractIPFromAddress(ip)
|
||||
|
||||
// 检查是否在黑名单中
|
||||
if isIPInCIDRList(ip, i.blacklist) {
|
||||
if isIPInCIDRList(cleanIP, i.blacklist) {
|
||||
return nil, false // 黑名单中的IP不允许访问
|
||||
}
|
||||
|
||||
// 检查是否在白名单中
|
||||
if isIPInCIDRList(ip, i.whitelist) {
|
||||
if isIPInCIDRList(cleanIP, i.whitelist) {
|
||||
return rate.NewLimiter(rate.Inf, i.b), true // 白名单中的IP不受限制
|
||||
}
|
||||
|
||||
// 从缓存获取限流器
|
||||
// 使用纯IP作为缓存键
|
||||
i.mu.RLock()
|
||||
entry, exists := i.ips[ip]
|
||||
entry, exists := i.ips[cleanIP]
|
||||
i.mu.RUnlock()
|
||||
|
||||
now := time.Now()
|
||||
@@ -209,7 +203,7 @@ func (i *IPRateLimiter) GetLimiter(ip string) (*rate.Limiter, bool) {
|
||||
limiter: rate.NewLimiter(i.r, i.b),
|
||||
lastAccess: now,
|
||||
}
|
||||
i.ips[ip] = entry
|
||||
i.ips[cleanIP] = entry
|
||||
i.mu.Unlock()
|
||||
} else {
|
||||
// 更新最后访问时间
|
||||
@@ -244,14 +238,18 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
|
||||
ip = c.ClientIP()
|
||||
}
|
||||
|
||||
// 日志记录请求IP和头信息(调试用)
|
||||
fmt.Printf("请求IP: %s, X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||
// 提取纯IP地址(去除端口号)
|
||||
cleanIP := extractIPFromAddress(ip)
|
||||
|
||||
// 日志记录请求IP和头信息
|
||||
fmt.Printf("请求IP: %s (去除端口后: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||
ip,
|
||||
cleanIP,
|
||||
c.GetHeader("X-Forwarded-For"),
|
||||
c.GetHeader("X-Real-IP"))
|
||||
|
||||
// 获取限流器并检查是否允许访问
|
||||
ipLimiter, allowed := limiter.GetLimiter(ip)
|
||||
ipLimiter, allowed := limiter.GetLimiter(cleanIP)
|
||||
|
||||
// 如果IP在黑名单中
|
||||
if !allowed {
|
||||
@@ -278,8 +276,11 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
|
||||
|
||||
// ApplyRateLimit 应用限流到特定路由
|
||||
func ApplyRateLimit(router *gin.Engine, path string, method string, handler gin.HandlerFunc) {
|
||||
// 创建限流器(如果未创建)
|
||||
limiter := NewIPRateLimiter()
|
||||
// 使用全局限流器
|
||||
limiter := globalLimiter
|
||||
if limiter == nil {
|
||||
limiter = initGlobalLimiter()
|
||||
}
|
||||
|
||||
// 根据HTTP方法应用限流
|
||||
switch method {
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user