重构优化
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
|
- name: Build and push Docker image
|
||||||
run: |
|
run: |
|
||||||
cd ghproxy
|
cd src
|
||||||
docker buildx build --push \
|
docker buildx build --push \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--tag ghcr.io/${{ env.REPO_LOWER }}:${{ env.VERSION }} \
|
--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
|
WORKDIR /app
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
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
|
FROM alpine
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ WORKDIR /root/
|
|||||||
# 安装skopeo
|
# 安装skopeo
|
||||||
RUN apk add --no-cache skopeo && mkdir -p temp && chmod 700 temp
|
RUN apk add --no-cache skopeo && mkdir -p temp && chmod 700 temp
|
||||||
|
|
||||||
COPY --from=builder /app/ghproxy .
|
COPY --from=builder /app/hubproxy .
|
||||||
COPY --from=builder /app/config.json .
|
COPY --from=builder /app/config.toml .
|
||||||
COPY --from=builder /app/public ./public
|
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
|
module hubproxy
|
||||||
|
|
||||||
go 1.23.0
|
go 1.24.0
|
||||||
|
|
||||||
toolchain go1.24.1
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
require (
|
github.com/google/go-containerregistry v0.20.5
|
||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gorilla/websocket v1.5.1
|
||||||
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/sync v0.14.0
|
||||||
golang.org/x/time v0.11.0
|
golang.org/x/time v0.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.11.6 // indirect
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/docker/cli v28.1.1+incompatible // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
golang.org/x/net v0.25.0 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
google.golang.org/protobuf v1.34.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.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 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
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 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||||
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/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
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/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 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
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/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/docker/cli v28.1.1+incompatible h1:eyUemzeI45DY7eDPuwUcmDyDj1pM98oD5MdSpiItp8k=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/docker/cli v28.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
github.com/google/go-containerregistry v0.20.5 h1:4RnlYcDs5hoA++CeFjlbZ/U9Yp1EuWr+UhhTyYQjOP0=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/google/go-containerregistry v0.20.5/go.mod h1:Q14vdOOzug02bwnhMkZKD4e30pDaD9W65qzXpyzF49E=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"fmt"
|
||||||
"fmt"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gin-gonic/gin"
|
"io"
|
||||||
"io"
|
"net/http"
|
||||||
"net"
|
"regexp"
|
||||||
"net/http"
|
"strconv"
|
||||||
"os"
|
"strings"
|
||||||
"regexp"
|
)
|
||||||
"strconv"
|
|
||||||
"strings"
|
var (
|
||||||
"sync"
|
exps = []*regexp.Regexp{
|
||||||
"time"
|
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*$`),
|
||||||
)
|
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*$`),
|
||||||
|
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*$`),
|
||||||
const (
|
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+$`),
|
||||||
sizeLimit = 1024 * 1024 * 1024 * 2 // 允许的文件大小,默认2GB
|
regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/.+?/.+`),
|
||||||
host = "0.0.0.0" // 监听地址
|
regexp.MustCompile(`^(?:https?://)?api\.github\.com/repos/([^/]+)/([^/]+)/.*`),
|
||||||
port = 5000 // 监听端口
|
regexp.MustCompile(`^(?:https?://)?huggingface\.co(?:/spaces)?/([^/]+)/(.+)$`),
|
||||||
)
|
regexp.MustCompile(`^(?:https?://)?cdn-lfs\.hf\.co(?:/spaces)?/([^/]+)/([^/]+)(?:/(.*))?$`),
|
||||||
|
regexp.MustCompile(`^(?:https?://)?download\.docker\.com/([^/]+)/.*\.(tgz|zip)$`),
|
||||||
var (
|
regexp.MustCompile(`^(?:https?://)?(github|opengraph)\.githubassets\.com/([^/]+)/.+?$`),
|
||||||
exps = []*regexp.Regexp{
|
}
|
||||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:releases|archive)/.*$`),
|
globalLimiter *IPRateLimiter
|
||||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:blob|raw)/.*$`),
|
)
|
||||||
regexp.MustCompile(`^(?:https?://)?github\.com/([^/]+)/([^/]+)/(?:info|git-).*$`),
|
|
||||||
regexp.MustCompile(`^(?:https?://)?raw\.github(?:usercontent|)\.com/([^/]+)/([^/]+)/.+?/.+$`),
|
func main() {
|
||||||
regexp.MustCompile(`^(?:https?://)?gist\.github(?:usercontent|)\.com/([^/]+)/.+?/.+`),
|
// 加载配置
|
||||||
regexp.MustCompile(`^(?:https?://)?api\.github\.com/repos/([^/]+)/([^/]+)/.*`),
|
if err := LoadConfig(); err != nil {
|
||||||
regexp.MustCompile(`^(?:https?://)?huggingface\.co(?:/spaces)?/([^/]+)/(.+)$`),
|
fmt.Printf("配置加载失败: %v\n", err)
|
||||||
regexp.MustCompile(`^(?:https?://)?cdn-lfs\.hf\.co(?:/spaces)?/([^/]+)/([^/]+)(?:/(.*))?$`),
|
return
|
||||||
regexp.MustCompile(`^(?:https?://)?download\.docker\.com/([^/]+)/.*\.(tgz|zip)$`),
|
}
|
||||||
regexp.MustCompile(`^(?:https?://)?(github|opengraph)\.githubassets\.com/([^/]+)/.+?$`),
|
|
||||||
}
|
// 初始化HTTP客户端
|
||||||
httpClient *http.Client
|
initHTTPClients()
|
||||||
config *Config
|
|
||||||
configLock sync.RWMutex
|
// 初始化限流器
|
||||||
)
|
initLimiter()
|
||||||
|
|
||||||
type Config struct {
|
// 初始化Docker流式代理
|
||||||
WhiteList []string `json:"whiteList"`
|
initDockerProxy()
|
||||||
BlackList []string `json:"blackList"`
|
|
||||||
}
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
router := gin.Default()
|
||||||
func main() {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
// 初始化skopeo路由(静态文件和API路由)
|
||||||
router := gin.Default()
|
initSkopeoRoutes(router)
|
||||||
|
|
||||||
httpClient = &http.Client{
|
// 单独处理根路径请求
|
||||||
Transport: &http.Transport{
|
router.GET("/", func(c *gin.Context) {
|
||||||
DialContext: (&net.Dialer{
|
c.File("./public/index.html")
|
||||||
Timeout: 30 * time.Second,
|
})
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
}).DialContext,
|
// 指定具体的静态文件路径
|
||||||
MaxIdleConns: 1000,
|
router.Static("/public", "./public")
|
||||||
MaxIdleConnsPerHost: 1000,
|
router.GET("/skopeo.html", func(c *gin.Context) {
|
||||||
IdleConnTimeout: 90 * time.Second,
|
c.File("./public/skopeo.html")
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
})
|
||||||
ExpectContinueTimeout: 1 * time.Second,
|
router.GET("/search.html", func(c *gin.Context) {
|
||||||
ResponseHeaderTimeout: 300 * time.Second,
|
c.File("./public/search.html")
|
||||||
},
|
})
|
||||||
}
|
router.GET("/favicon.ico", func(c *gin.Context) {
|
||||||
|
c.File("./public/favicon.ico")
|
||||||
loadConfig()
|
})
|
||||||
go func() {
|
|
||||||
for {
|
// 注册dockerhub搜索路由
|
||||||
time.Sleep(10 * time.Minute)
|
RegisterSearchRoute(router)
|
||||||
loadConfig()
|
|
||||||
}
|
// 注册Docker认证路由(/token*)
|
||||||
}()
|
router.Any("/token", RateLimitMiddleware(globalLimiter), ProxyDockerAuthGin)
|
||||||
|
router.Any("/token/*path", RateLimitMiddleware(globalLimiter), ProxyDockerAuthGin)
|
||||||
// 初始化Skopeo相关路由 - 在任何通配符路由之前注册
|
|
||||||
initSkopeoRoutes(router)
|
// 注册Docker Registry代理路由
|
||||||
|
router.Any("/v2/*path", RateLimitMiddleware(globalLimiter), ProxyDockerRegistryGin)
|
||||||
// 单独处理根路径请求,避免冲突
|
|
||||||
router.GET("/", func(c *gin.Context) {
|
|
||||||
c.File("./public/index.html")
|
// 注册NoRoute处理器,应用限流中间件
|
||||||
})
|
router.NoRoute(RateLimitMiddleware(globalLimiter), handler)
|
||||||
|
|
||||||
// 指定具体的静态文件路径,避免使用通配符
|
cfg := GetConfig()
|
||||||
router.Static("/public", "./public")
|
fmt.Printf("启动成功,项目地址:https://github.com/sky22333/hubproxy \n")
|
||||||
|
|
||||||
// 对于.html等特定文件注册
|
err := router.Run(fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port))
|
||||||
router.GET("/skopeo.html", func(c *gin.Context) {
|
if err != nil {
|
||||||
c.File("./public/skopeo.html")
|
fmt.Printf("启动服务失败: %v\n", err)
|
||||||
})
|
}
|
||||||
router.GET("/search.html", func(c *gin.Context) {
|
}
|
||||||
c.File("./public/search.html")
|
|
||||||
})
|
func handler(c *gin.Context) {
|
||||||
|
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/")
|
||||||
// 图标文件
|
|
||||||
router.GET("/favicon.ico", func(c *gin.Context) {
|
for strings.HasPrefix(rawPath, "/") {
|
||||||
c.File("./public/favicon.ico")
|
rawPath = strings.TrimPrefix(rawPath, "/")
|
||||||
})
|
}
|
||||||
|
|
||||||
// 注册dockerhub搜索路由
|
if !strings.HasPrefix(rawPath, "http") {
|
||||||
RegisterSearchRoute(router)
|
c.String(http.StatusForbidden, "无效输入")
|
||||||
// 创建GitHub文件下载专用的限流器
|
return
|
||||||
githubLimiter := NewIPRateLimiter()
|
}
|
||||||
|
|
||||||
// 注册NoRoute处理器,应用限流中间件
|
matches := checkURL(rawPath)
|
||||||
router.NoRoute(RateLimitMiddleware(githubLimiter), handler)
|
if matches != nil {
|
||||||
|
// GitHub仓库访问控制检查
|
||||||
err := router.Run(fmt.Sprintf("%s:%d", host, port))
|
if allowed, reason := GlobalAccessController.CheckGitHubAccess(matches); !allowed {
|
||||||
if err != nil {
|
// 构建仓库名用于日志
|
||||||
fmt.Printf("Error starting server: %v\n", err)
|
var repoPath string
|
||||||
}
|
if len(matches) >= 2 {
|
||||||
}
|
username := matches[0]
|
||||||
|
repoName := strings.TrimSuffix(matches[1], ".git")
|
||||||
func handler(c *gin.Context) {
|
repoPath = username + "/" + repoName
|
||||||
rawPath := strings.TrimPrefix(c.Request.URL.RequestURI(), "/")
|
}
|
||||||
|
fmt.Printf("GitHub仓库 %s 访问被拒绝: %s\n", repoPath, reason)
|
||||||
for strings.HasPrefix(rawPath, "/") {
|
c.String(http.StatusForbidden, reason)
|
||||||
rawPath = strings.TrimPrefix(rawPath, "/")
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
if !strings.HasPrefix(rawPath, "http") {
|
c.String(http.StatusForbidden, "无效输入")
|
||||||
c.String(http.StatusForbidden, "无效输入")
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
if exps[1].MatchString(rawPath) {
|
||||||
matches := checkURL(rawPath)
|
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
||||||
if matches != nil {
|
}
|
||||||
if len(config.WhiteList) > 0 && !checkList(matches, config.WhiteList) {
|
|
||||||
c.String(http.StatusForbidden, "不在白名单内,限制访问。")
|
proxy(c, rawPath)
|
||||||
return
|
}
|
||||||
}
|
|
||||||
if len(config.BlackList) > 0 && checkList(matches, config.BlackList) {
|
|
||||||
c.String(http.StatusForbidden, "黑名单限制访问")
|
func proxy(c *gin.Context, u string) {
|
||||||
return
|
proxyWithRedirect(c, u, 0)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
c.String(http.StatusForbidden, "无效输入")
|
|
||||||
return
|
func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
|
||||||
}
|
// 限制最大重定向次数,防止无限递归
|
||||||
|
const maxRedirects = 20
|
||||||
if exps[1].MatchString(rawPath) {
|
if redirectCount > maxRedirects {
|
||||||
rawPath = strings.Replace(rawPath, "/blob/", "/raw/", 1)
|
c.String(http.StatusLoopDetected, "重定向次数过多,可能存在循环重定向")
|
||||||
}
|
return
|
||||||
|
}
|
||||||
proxy(c, rawPath)
|
req, err := http.NewRequest(c.Request.Method, u, c.Request.Body)
|
||||||
}
|
if err != nil {
|
||||||
|
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err))
|
||||||
func proxy(c *gin.Context, u string) {
|
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))
|
for key, values := range c.Request.Header {
|
||||||
return
|
for _, value := range values {
|
||||||
}
|
req.Header.Add(key, value)
|
||||||
|
}
|
||||||
for key, values := range c.Request.Header {
|
}
|
||||||
for _, value := range values {
|
req.Header.Del("Host")
|
||||||
req.Header.Add(key, value)
|
|
||||||
}
|
resp, err := GetGlobalHTTPClient().Do(req)
|
||||||
}
|
if err != nil {
|
||||||
req.Header.Del("Host")
|
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err))
|
||||||
|
return
|
||||||
resp, err := httpClient.Do(req)
|
}
|
||||||
if err != nil {
|
defer func() {
|
||||||
c.String(http.StatusInternalServerError, fmt.Sprintf("server error %v", err))
|
if err := resp.Body.Close(); err != nil {
|
||||||
return
|
fmt.Printf("关闭响应体失败: %v\n", err)
|
||||||
}
|
}
|
||||||
defer func(Body io.ReadCloser) {
|
}()
|
||||||
err := Body.Close()
|
|
||||||
if err != nil {
|
// 检查文件大小限制
|
||||||
|
cfg := GetConfig()
|
||||||
}
|
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
||||||
}(resp.Body)
|
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)))
|
||||||
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
return
|
||||||
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")
|
||||||
resp.Header.Del("Content-Security-Policy")
|
|
||||||
resp.Header.Del("Referrer-Policy")
|
// 对于需要处理的shell文件,使用chunked传输
|
||||||
resp.Header.Del("Strict-Transport-Security")
|
isShellFile := strings.HasSuffix(strings.ToLower(u), ".sh")
|
||||||
|
if isShellFile {
|
||||||
// 对于需要处理的shell文件,使用chunked传输
|
resp.Header.Del("Content-Length")
|
||||||
isShellFile := strings.HasSuffix(strings.ToLower(u), ".sh")
|
resp.Header.Set("Transfer-Encoding", "chunked")
|
||||||
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)
|
||||||
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)
|
||||||
if location := resp.Header.Get("Location"); location != "" {
|
} else {
|
||||||
if checkURL(location) != nil {
|
// 递归处理重定向,增加计数防止无限循环
|
||||||
c.Header("Location", "/"+location)
|
proxyWithRedirect(c, location, redirectCount+1)
|
||||||
} else {
|
return
|
||||||
proxy(c, location)
|
}
|
||||||
return
|
}
|
||||||
}
|
|
||||||
}
|
c.Status(resp.StatusCode)
|
||||||
|
|
||||||
c.Status(resp.StatusCode)
|
// 处理响应体
|
||||||
|
if isShellFile {
|
||||||
// 处理响应体
|
// 获取真实域名
|
||||||
if isShellFile {
|
realHost := c.Request.Header.Get("X-Forwarded-Host")
|
||||||
// 获取真实域名
|
if realHost == "" {
|
||||||
realHost := c.Request.Header.Get("X-Forwarded-Host")
|
realHost = c.Request.Host
|
||||||
if realHost == "" {
|
}
|
||||||
realHost = c.Request.Host
|
// 如果域名中没有协议前缀,添加https://
|
||||||
}
|
if !strings.HasPrefix(realHost, "http://") && !strings.HasPrefix(realHost, "https://") {
|
||||||
// 如果域名中没有协议前缀,添加https://
|
realHost = "https://" + realHost
|
||||||
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)
|
||||||
// 使用ProcessGitHubURLs处理.sh文件
|
if err != nil {
|
||||||
processedBody, _, err := ProcessGitHubURLs(resp.Body, resp.Header.Get("Content-Encoding") == "gzip", realHost, true)
|
c.String(http.StatusInternalServerError, fmt.Sprintf("处理shell文件时发生错误: %v", err))
|
||||||
if err != nil {
|
return
|
||||||
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))
|
||||||
if _, err := io.Copy(c.Writer, processedBody); err != nil {
|
return
|
||||||
c.String(http.StatusInternalServerError, fmt.Sprintf("写入响应时发生错误: %v", err))
|
}
|
||||||
return
|
} else {
|
||||||
}
|
// 对于非.sh文件,直接复制响应体
|
||||||
} else {
|
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
||||||
// 对于非.sh文件,直接复制响应体
|
return
|
||||||
if _, err := io.Copy(c.Writer, resp.Body); err != nil {
|
}
|
||||||
return
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
func checkURL(u string) []string {
|
||||||
|
for _, exp := range exps {
|
||||||
func loadConfig() {
|
if matches := exp.FindStringSubmatch(u); matches != nil {
|
||||||
file, err := os.Open("config.json")
|
return matches[1:]
|
||||||
if err != nil {
|
}
|
||||||
fmt.Printf("Error loading config: %v\n", err)
|
}
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,192 +1,192 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// gitHubDomains 定义所有支持的GitHub相关域名
|
// gitHubDomains 定义所有支持的GitHub相关域名
|
||||||
gitHubDomains = []string{
|
gitHubDomains = []string{
|
||||||
"github.com",
|
"github.com",
|
||||||
"raw.githubusercontent.com",
|
"raw.githubusercontent.com",
|
||||||
"raw.github.com",
|
"raw.github.com",
|
||||||
"gist.githubusercontent.com",
|
"gist.githubusercontent.com",
|
||||||
"gist.github.com",
|
"gist.github.com",
|
||||||
"api.github.com",
|
"api.github.com",
|
||||||
}
|
}
|
||||||
|
|
||||||
// urlPattern 使用gitHubDomains构建正则表达式
|
// urlPattern 使用gitHubDomains构建正则表达式
|
||||||
urlPattern = regexp.MustCompile(`https?://(?:` + strings.Join(gitHubDomains, "|") + `)[^\s'"]+`)
|
urlPattern = regexp.MustCompile(`https?://(?:` + strings.Join(gitHubDomains, "|") + `)[^\s'"]+`)
|
||||||
|
|
||||||
// 是否启用脚本嵌套代理的调试日志
|
// 是否启用脚本嵌套代理的调试日志
|
||||||
DebugLog = true
|
DebugLog = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// 打印调试日志的辅助函数
|
// 打印调试日志的辅助函数
|
||||||
func debugPrintf(format string, args ...interface{}) {
|
func debugPrintf(format string, args ...interface{}) {
|
||||||
if DebugLog {
|
if DebugLog {
|
||||||
fmt.Printf(format, args...)
|
fmt.Printf(format, args...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessGitHubURLs 处理数据流中的GitHub URL,将其替换为代理URL。
|
// ProcessGitHubURLs 处理数据流中的GitHub URL,将其替换为代理URL。
|
||||||
// 此处思路借鉴了 https://github.com/WJQSERVER-STUDIO/ghproxy/blob/main/proxy/nest.go
|
// 此处思路借鉴了 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) {
|
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)
|
debugPrintf("开始处理文件: isCompressed=%v, host=%s, isShellFile=%v\n", isCompressed, host, isShellFile)
|
||||||
|
|
||||||
if !isShellFile {
|
if !isShellFile {
|
||||||
debugPrintf("非shell文件,跳过处理\n")
|
debugPrintf("非shell文件,跳过处理\n")
|
||||||
return input, 0, nil
|
return input, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用更大的缓冲区以提高性能
|
// 使用更大的缓冲区以提高性能
|
||||||
pipeReader, pipeWriter := io.Pipe()
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
var written int64
|
var written int64
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
var err error
|
var err error
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugPrintf("处理过程中发生错误: %v\n", err)
|
debugPrintf("处理过程中发生错误: %v\n", err)
|
||||||
_ = pipeWriter.CloseWithError(err)
|
_ = pipeWriter.CloseWithError(err)
|
||||||
} else {
|
} else {
|
||||||
_ = pipeWriter.Close()
|
_ = pipeWriter.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
defer input.Close()
|
defer input.Close()
|
||||||
|
|
||||||
var reader io.Reader = input
|
var reader io.Reader = input
|
||||||
if isCompressed {
|
if isCompressed {
|
||||||
debugPrintf("检测到压缩文件,进行解压处理\n")
|
debugPrintf("检测到压缩文件,进行解压处理\n")
|
||||||
gzipReader, gzipErr := gzip.NewReader(input)
|
gzipReader, gzipErr := gzip.NewReader(input)
|
||||||
if gzipErr != nil {
|
if gzipErr != nil {
|
||||||
err = gzipErr
|
err = gzipErr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer gzipReader.Close()
|
defer gzipReader.Close()
|
||||||
reader = gzipReader
|
reader = gzipReader
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用更大的缓冲区
|
// 使用更大的缓冲区
|
||||||
bufReader := bufio.NewReaderSize(reader, 32*1024) // 32KB buffer
|
bufReader := bufio.NewReaderSize(reader, 32*1024) // 32KB buffer
|
||||||
var writer io.Writer = pipeWriter
|
var writer io.Writer = pipeWriter
|
||||||
|
|
||||||
if isCompressed {
|
if isCompressed {
|
||||||
gzipWriter := gzip.NewWriter(writer)
|
gzipWriter := gzip.NewWriter(writer)
|
||||||
defer gzipWriter.Close()
|
defer gzipWriter.Close()
|
||||||
writer = gzipWriter
|
writer = gzipWriter
|
||||||
}
|
}
|
||||||
|
|
||||||
bufWriter := bufio.NewWriterSize(writer, 32*1024) // 32KB buffer
|
bufWriter := bufio.NewWriterSize(writer, 32*1024) // 32KB buffer
|
||||||
defer bufWriter.Flush()
|
defer bufWriter.Flush()
|
||||||
|
|
||||||
written, err = processContent(bufReader, bufWriter, host)
|
written, err = processContent(bufReader, bufWriter, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
debugPrintf("处理内容时发生错误: %v\n", err)
|
debugPrintf("处理内容时发生错误: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrintf("文件处理完成,共处理 %d 字节\n", written)
|
debugPrintf("文件处理完成,共处理 %d 字节\n", written)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return pipeReader, written, nil
|
return pipeReader, written, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processContent 优化处理文件内容的函数
|
// processContent 优化处理文件内容的函数
|
||||||
func processContent(reader *bufio.Reader, writer *bufio.Writer, host string) (int64, error) {
|
func processContent(reader *bufio.Reader, writer *bufio.Writer, host string) (int64, error) {
|
||||||
var written int64
|
var written int64
|
||||||
lineNum := 0
|
lineNum := 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
lineNum++
|
lineNum++
|
||||||
line, err := reader.ReadString('\n')
|
line, err := reader.ReadString('\n')
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
return written, fmt.Errorf("读取行时发生错误: %w", err)
|
return written, fmt.Errorf("读取行时发生错误: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if line != "" {
|
if line != "" {
|
||||||
// 在处理前先检查是否包含GitHub URL
|
// 在处理前先检查是否包含GitHub URL
|
||||||
if strings.Contains(line, "github.com") ||
|
if strings.Contains(line, "github.com") ||
|
||||||
strings.Contains(line, "raw.githubusercontent.com") {
|
strings.Contains(line, "raw.githubusercontent.com") {
|
||||||
matches := urlPattern.FindAllString(line, -1)
|
matches := urlPattern.FindAllString(line, -1)
|
||||||
if len(matches) > 0 {
|
if len(matches) > 0 {
|
||||||
debugPrintf("\n在第 %d 行发现 %d 个GitHub URL:\n", lineNum, len(matches))
|
debugPrintf("\n在第 %d 行发现 %d 个GitHub URL:\n", lineNum, len(matches))
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
debugPrintf("原始URL: %s\n", match)
|
debugPrintf("原始URL: %s\n", match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modifiedLine := processLine(line, host, lineNum)
|
modifiedLine := processLine(line, host, lineNum)
|
||||||
n, writeErr := writer.WriteString(modifiedLine)
|
n, writeErr := writer.WriteString(modifiedLine)
|
||||||
if writeErr != nil {
|
if writeErr != nil {
|
||||||
return written, fmt.Errorf("写入修改后的行时发生错误: %w", writeErr)
|
return written, fmt.Errorf("写入修改后的行时发生错误: %w", writeErr)
|
||||||
}
|
}
|
||||||
written += int64(n)
|
written += int64(n)
|
||||||
} else {
|
} else {
|
||||||
// 如果行中没有GitHub URL,直接写入
|
// 如果行中没有GitHub URL,直接写入
|
||||||
n, writeErr := writer.WriteString(line)
|
n, writeErr := writer.WriteString(line)
|
||||||
if writeErr != nil {
|
if writeErr != nil {
|
||||||
return written, fmt.Errorf("写入原始行时发生错误: %w", writeErr)
|
return written, fmt.Errorf("写入原始行时发生错误: %w", writeErr)
|
||||||
}
|
}
|
||||||
written += int64(n)
|
written += int64(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保所有数据都被写入
|
// 确保所有数据都被写入
|
||||||
if err := writer.Flush(); err != nil {
|
if err := writer.Flush(); err != nil {
|
||||||
return written, fmt.Errorf("刷新缓冲区时发生错误: %w", err)
|
return written, fmt.Errorf("刷新缓冲区时发生错误: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return written, nil
|
return written, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processLine 处理单行文本,替换所有匹配的GitHub URL
|
// processLine 处理单行文本,替换所有匹配的GitHub URL
|
||||||
func processLine(line string, host string, lineNum int) string {
|
func processLine(line string, host string, lineNum int) string {
|
||||||
return urlPattern.ReplaceAllStringFunc(line, func(url string) string {
|
return urlPattern.ReplaceAllStringFunc(line, func(url string) string {
|
||||||
newURL := modifyGitHubURL(url, host)
|
newURL := modifyGitHubURL(url, host)
|
||||||
if newURL != url {
|
if newURL != url {
|
||||||
debugPrintf("第 %d 行URL替换:\n 原始: %s\n 替换后: %s\n", lineNum, url, newURL)
|
debugPrintf("第 %d 行URL替换:\n 原始: %s\n 替换后: %s\n", lineNum, url, newURL)
|
||||||
}
|
}
|
||||||
return newURL
|
return newURL
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// modifyGitHubURL 修改GitHub URL,添加代理域名前缀
|
// 判断代理域名前缀
|
||||||
func modifyGitHubURL(url string, host string) string {
|
func modifyGitHubURL(url string, host string) string {
|
||||||
for _, domain := range gitHubDomains {
|
for _, domain := range gitHubDomains {
|
||||||
hasHttps := strings.HasPrefix(url, "https://"+domain)
|
hasHttps := strings.HasPrefix(url, "https://"+domain)
|
||||||
hasHttp := strings.HasPrefix(url, "http://"+domain)
|
hasHttp := strings.HasPrefix(url, "http://"+domain)
|
||||||
|
|
||||||
if hasHttps || hasHttp || strings.HasPrefix(url, domain) {
|
if hasHttps || hasHttp || strings.HasPrefix(url, domain) {
|
||||||
if !hasHttps && !hasHttp {
|
if !hasHttps && !hasHttp {
|
||||||
url = "https://" + url
|
url = "https://" + url
|
||||||
}
|
}
|
||||||
if hasHttp {
|
if hasHttp {
|
||||||
url = "https://" + strings.TrimPrefix(url, "http://")
|
url = "https://" + strings.TrimPrefix(url, "http://")
|
||||||
}
|
}
|
||||||
// 移除host开头的协议头(如果有)
|
// 移除host开头的协议头(如果有)
|
||||||
host = strings.TrimPrefix(host, "https://")
|
host = strings.TrimPrefix(host, "https://")
|
||||||
host = strings.TrimPrefix(host, "http://")
|
host = strings.TrimPrefix(host, "http://")
|
||||||
// 返回组合后的URL
|
// 返回组合后的URL
|
||||||
return host + "/" + url
|
return host + "/" + url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsShellFile 检查文件是否为shell文件(基于文件名)
|
// IsShellFile 检查文件是否为shell文件(基于文件名)
|
||||||
func IsShellFile(filename string) bool {
|
func IsShellFile(filename string) bool {
|
||||||
return strings.HasSuffix(filename, ".sh")
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,33 +11,14 @@ import (
|
|||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IP限流配置
|
const (
|
||||||
var (
|
// 清理间隔
|
||||||
// 默认限流:每个IP每1小时允许20个请求
|
CleanupInterval = 10 * time.Minute
|
||||||
DefaultRateLimit = 20.0 // 默认限制请求数
|
// 最大IP缓存数量,防止内存过度占用
|
||||||
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限流器缓存上限,超过此数量将触发清理
|
|
||||||
MaxIPCacheSize = 10000
|
MaxIPCacheSize = 10000
|
||||||
)
|
)
|
||||||
|
|
||||||
// IPRateLimiter 定义IP限流器结构
|
// IPRateLimiter IP限流器结构体
|
||||||
type IPRateLimiter struct {
|
type IPRateLimiter struct {
|
||||||
ips map[string]*rateLimiterEntry // IP到限流器的映射
|
ips map[string]*rateLimiterEntry // IP到限流器的映射
|
||||||
mu *sync.RWMutex // 读写锁,保证并发安全
|
mu *sync.RWMutex // 读写锁,保证并发安全
|
||||||
@@ -49,45 +28,20 @@ type IPRateLimiter struct {
|
|||||||
blacklist []*net.IPNet // 黑名单IP段
|
blacklist []*net.IPNet // 黑名单IP段
|
||||||
}
|
}
|
||||||
|
|
||||||
// rateLimiterEntry 限流器条目,包含限流器和最后访问时间
|
// rateLimiterEntry 限流器条目
|
||||||
type rateLimiterEntry struct {
|
type rateLimiterEntry struct {
|
||||||
limiter *rate.Limiter // 限流器
|
limiter *rate.Limiter // 限流器
|
||||||
lastAccess time.Time // 最后访问时间
|
lastAccess time.Time // 最后访问时间
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIPRateLimiter 创建新的IP限流器
|
// initGlobalLimiter 初始化全局限流器
|
||||||
func NewIPRateLimiter() *IPRateLimiter {
|
func initGlobalLimiter() *IPRateLimiter {
|
||||||
// 从环境变量读取限流配置(如果有)
|
// 获取配置
|
||||||
rateLimit := DefaultRateLimit
|
cfg := GetConfig()
|
||||||
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, ",")...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析白名单IP段
|
// 解析白名单IP段
|
||||||
whitelist := make([]*net.IPNet, 0, len(whitelistIPs))
|
whitelist := make([]*net.IPNet, 0, len(cfg.Security.WhiteList))
|
||||||
for _, item := range whitelistIPs {
|
for _, item := range cfg.Security.WhiteList {
|
||||||
if item = strings.TrimSpace(item); item != "" {
|
if item = strings.TrimSpace(item); item != "" {
|
||||||
if !strings.Contains(item, "/") {
|
if !strings.Contains(item, "/") {
|
||||||
item = item + "/32" // 单个IP转为CIDR格式
|
item = item + "/32" // 单个IP转为CIDR格式
|
||||||
@@ -95,13 +49,15 @@ func NewIPRateLimiter() *IPRateLimiter {
|
|||||||
_, ipnet, err := net.ParseCIDR(item)
|
_, ipnet, err := net.ParseCIDR(item)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
whitelist = append(whitelist, ipnet)
|
whitelist = append(whitelist, ipnet)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("警告: 无效的白名单IP格式: %s\n", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析黑名单IP段
|
// 解析黑名单IP段
|
||||||
blacklist := make([]*net.IPNet, 0, len(blacklistIPs))
|
blacklist := make([]*net.IPNet, 0, len(cfg.Security.BlackList))
|
||||||
for _, item := range blacklistIPs {
|
for _, item := range cfg.Security.BlackList {
|
||||||
if item = strings.TrimSpace(item); item != "" {
|
if item = strings.TrimSpace(item); item != "" {
|
||||||
if !strings.Contains(item, "/") {
|
if !strings.Contains(item, "/") {
|
||||||
item = item + "/32" // 单个IP转为CIDR格式
|
item = item + "/32" // 单个IP转为CIDR格式
|
||||||
@@ -109,19 +65,26 @@ func NewIPRateLimiter() *IPRateLimiter {
|
|||||||
_, ipnet, err := net.ParseCIDR(item)
|
_, ipnet, err := net.ParseCIDR(item)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
blacklist = append(blacklist, ipnet)
|
blacklist = append(blacklist, ipnet)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("警告: 无效的黑名单IP格式: %s\n", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算速率:将 "每N小时X个请求" 转换为 "每秒Y个请求"
|
// 计算速率:将 "每N小时X个请求" 转换为 "每秒Y个请求"
|
||||||
// rate.Limit的单位是每秒允许的请求数
|
ratePerSecond := rate.Limit(float64(cfg.RateLimit.RequestLimit) / (cfg.RateLimit.PeriodHours * 3600))
|
||||||
ratePerSecond := rate.Limit(rateLimit / (ratePeriod * 3600))
|
|
||||||
|
// 令牌桶容量设置为最大突发请求数,建议设为限制值的一半以允许合理突发
|
||||||
|
burstSize := cfg.RateLimit.RequestLimit
|
||||||
|
if burstSize < 1 {
|
||||||
|
burstSize = 1 // 至少允许1个请求
|
||||||
|
}
|
||||||
|
|
||||||
limiter := &IPRateLimiter{
|
limiter := &IPRateLimiter{
|
||||||
ips: make(map[string]*rateLimiterEntry),
|
ips: make(map[string]*rateLimiterEntry),
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
r: ratePerSecond,
|
r: ratePerSecond,
|
||||||
b: int(rateLimit), // 令牌桶容量设为允许的请求总数
|
b: burstSize,
|
||||||
whitelist: whitelist,
|
whitelist: whitelist,
|
||||||
blacklist: blacklist,
|
blacklist: blacklist,
|
||||||
}
|
}
|
||||||
@@ -129,9 +92,17 @@ func NewIPRateLimiter() *IPRateLimiter {
|
|||||||
// 启动定期清理goroutine
|
// 启动定期清理goroutine
|
||||||
go limiter.cleanupRoutine()
|
go limiter.cleanupRoutine()
|
||||||
|
|
||||||
|
fmt.Printf("限流器初始化: %d请求/%g小时, 白名单 %d个, 黑名单 %d个\n",
|
||||||
|
cfg.RateLimit.RequestLimit, cfg.RateLimit.PeriodHours, len(whitelist), len(blacklist))
|
||||||
|
|
||||||
return limiter
|
return limiter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initLimiter 初始化限流器(保持向后兼容)
|
||||||
|
func initLimiter() {
|
||||||
|
globalLimiter = initGlobalLimiter()
|
||||||
|
}
|
||||||
|
|
||||||
// cleanupRoutine 定期清理过期的限流器
|
// cleanupRoutine 定期清理过期的限流器
|
||||||
func (i *IPRateLimiter) cleanupRoutine() {
|
func (i *IPRateLimiter) cleanupRoutine() {
|
||||||
ticker := time.NewTicker(CleanupInterval)
|
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列表中
|
// isIPInCIDRList 检查IP是否在CIDR列表中
|
||||||
func isIPInCIDRList(ip string, cidrList []*net.IPNet) bool {
|
func isIPInCIDRList(ip string, cidrList []*net.IPNet) bool {
|
||||||
parsedIP := net.ParseIP(ip)
|
// 先提取纯IP地址
|
||||||
|
cleanIP := extractIPFromAddress(ip)
|
||||||
|
parsedIP := net.ParseIP(cleanIP)
|
||||||
if parsedIP == nil {
|
if parsedIP == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -185,19 +176,22 @@ func isIPInCIDRList(ip string, cidrList []*net.IPNet) bool {
|
|||||||
|
|
||||||
// GetLimiter 获取指定IP的限流器,同时返回是否允许访问
|
// GetLimiter 获取指定IP的限流器,同时返回是否允许访问
|
||||||
func (i *IPRateLimiter) GetLimiter(ip string) (*rate.Limiter, bool) {
|
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不允许访问
|
return nil, false // 黑名单中的IP不允许访问
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否在白名单中
|
// 检查是否在白名单中
|
||||||
if isIPInCIDRList(ip, i.whitelist) {
|
if isIPInCIDRList(cleanIP, i.whitelist) {
|
||||||
return rate.NewLimiter(rate.Inf, i.b), true // 白名单中的IP不受限制
|
return rate.NewLimiter(rate.Inf, i.b), true // 白名单中的IP不受限制
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从缓存获取限流器
|
// 使用纯IP作为缓存键
|
||||||
i.mu.RLock()
|
i.mu.RLock()
|
||||||
entry, exists := i.ips[ip]
|
entry, exists := i.ips[cleanIP]
|
||||||
i.mu.RUnlock()
|
i.mu.RUnlock()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -209,7 +203,7 @@ func (i *IPRateLimiter) GetLimiter(ip string) (*rate.Limiter, bool) {
|
|||||||
limiter: rate.NewLimiter(i.r, i.b),
|
limiter: rate.NewLimiter(i.r, i.b),
|
||||||
lastAccess: now,
|
lastAccess: now,
|
||||||
}
|
}
|
||||||
i.ips[ip] = entry
|
i.ips[cleanIP] = entry
|
||||||
i.mu.Unlock()
|
i.mu.Unlock()
|
||||||
} else {
|
} else {
|
||||||
// 更新最后访问时间
|
// 更新最后访问时间
|
||||||
@@ -244,14 +238,18 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
|
|||||||
ip = c.ClientIP()
|
ip = c.ClientIP()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 日志记录请求IP和头信息(调试用)
|
// 提取纯IP地址(去除端口号)
|
||||||
fmt.Printf("请求IP: %s, X-Forwarded-For: %s, X-Real-IP: %s\n",
|
cleanIP := extractIPFromAddress(ip)
|
||||||
|
|
||||||
|
// 日志记录请求IP和头信息
|
||||||
|
fmt.Printf("请求IP: %s (去除端口后: %s), X-Forwarded-For: %s, X-Real-IP: %s\n",
|
||||||
ip,
|
ip,
|
||||||
|
cleanIP,
|
||||||
c.GetHeader("X-Forwarded-For"),
|
c.GetHeader("X-Forwarded-For"),
|
||||||
c.GetHeader("X-Real-IP"))
|
c.GetHeader("X-Real-IP"))
|
||||||
|
|
||||||
// 获取限流器并检查是否允许访问
|
// 获取限流器并检查是否允许访问
|
||||||
ipLimiter, allowed := limiter.GetLimiter(ip)
|
ipLimiter, allowed := limiter.GetLimiter(cleanIP)
|
||||||
|
|
||||||
// 如果IP在黑名单中
|
// 如果IP在黑名单中
|
||||||
if !allowed {
|
if !allowed {
|
||||||
@@ -278,8 +276,11 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
|
|||||||
|
|
||||||
// ApplyRateLimit 应用限流到特定路由
|
// ApplyRateLimit 应用限流到特定路由
|
||||||
func ApplyRateLimit(router *gin.Engine, path string, method string, handler gin.HandlerFunc) {
|
func ApplyRateLimit(router *gin.Engine, path string, method string, handler gin.HandlerFunc) {
|
||||||
// 创建限流器(如果未创建)
|
// 使用全局限流器
|
||||||
limiter := NewIPRateLimiter()
|
limiter := globalLimiter
|
||||||
|
if limiter == nil {
|
||||||
|
limiter = initGlobalLimiter()
|
||||||
|
}
|
||||||
|
|
||||||
// 根据HTTP方法应用限流
|
// 根据HTTP方法应用限流
|
||||||
switch method {
|
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