op http client proxy
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
hubproxy*
|
||||||
10
README.md
10
README.md
@@ -138,11 +138,17 @@ blackList = [
|
|||||||
"baduser/*"
|
"baduser/*"
|
||||||
]
|
]
|
||||||
|
|
||||||
# SOCKS5代理配置,支持有用户名/密码认证和无认证模式
|
# 代理配置,支持有用户名/密码认证和无认证模式
|
||||||
# 无认证: socks5://127.0.0.1:1080
|
# 无认证: socks5://127.0.0.1:1080
|
||||||
# 有认证: socks5://username:password@127.0.0.1:1080
|
# 有认证: socks5://username:password@127.0.0.1:1080
|
||||||
|
# HTTP 代理示例
|
||||||
|
# http://username:password@127.0.0.1:7890
|
||||||
|
# SOCKS5 代理示例
|
||||||
|
# socks5://username:password@127.0.0.1:1080
|
||||||
|
# SOCKS5H 代理示例
|
||||||
|
# socks5h://username:password@127.0.0.1:1080
|
||||||
# 留空不使用代理
|
# 留空不使用代理
|
||||||
socks5 = ""
|
proxy = ""
|
||||||
|
|
||||||
[download]
|
[download]
|
||||||
# 批量下载离线镜像数量限制
|
# 批量下载离线镜像数量限制
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ var GlobalAccessController = &AccessController{}
|
|||||||
// ParseDockerImage 解析Docker镜像名称
|
// ParseDockerImage 解析Docker镜像名称
|
||||||
func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
|
func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
|
||||||
image = strings.TrimPrefix(image, "docker://")
|
image = strings.TrimPrefix(image, "docker://")
|
||||||
|
|
||||||
var tag string
|
var tag string
|
||||||
if idx := strings.LastIndex(image, ":"); idx != -1 {
|
if idx := strings.LastIndex(image, ":"); idx != -1 {
|
||||||
part := image[idx+1:]
|
part := image[idx+1:]
|
||||||
@@ -44,7 +44,7 @@ func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
|
|||||||
if tag == "" {
|
if tag == "" {
|
||||||
tag = "latest"
|
tag = "latest"
|
||||||
}
|
}
|
||||||
|
|
||||||
var namespace, repository string
|
var namespace, repository string
|
||||||
if strings.Contains(image, "/") {
|
if strings.Contains(image, "/") {
|
||||||
parts := strings.Split(image, "/")
|
parts := strings.Split(image, "/")
|
||||||
@@ -66,9 +66,9 @@ func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
|
|||||||
namespace = "library"
|
namespace = "library"
|
||||||
repository = image
|
repository = image
|
||||||
}
|
}
|
||||||
|
|
||||||
fullName := namespace + "/" + repository
|
fullName := namespace + "/" + repository
|
||||||
|
|
||||||
return DockerImageInfo{
|
return DockerImageInfo{
|
||||||
Namespace: namespace,
|
Namespace: namespace,
|
||||||
Repository: repository,
|
Repository: repository,
|
||||||
@@ -80,24 +80,24 @@ func (ac *AccessController) ParseDockerImage(image string) DockerImageInfo {
|
|||||||
// CheckDockerAccess 检查Docker镜像访问权限
|
// CheckDockerAccess 检查Docker镜像访问权限
|
||||||
func (ac *AccessController) CheckDockerAccess(image string) (allowed bool, reason string) {
|
func (ac *AccessController) CheckDockerAccess(image string) (allowed bool, reason string) {
|
||||||
cfg := GetConfig()
|
cfg := GetConfig()
|
||||||
|
|
||||||
// 解析镜像名称
|
// 解析镜像名称
|
||||||
imageInfo := ac.ParseDockerImage(image)
|
imageInfo := ac.ParseDockerImage(image)
|
||||||
|
|
||||||
// 检查白名单(如果配置了白名单,则只允许白名单中的镜像)
|
// 检查白名单(如果配置了白名单,则只允许白名单中的镜像)
|
||||||
if len(cfg.Proxy.WhiteList) > 0 {
|
if len(cfg.Access.WhiteList) > 0 {
|
||||||
if !ac.matchImageInList(imageInfo, cfg.Proxy.WhiteList) {
|
if !ac.matchImageInList(imageInfo, cfg.Access.WhiteList) {
|
||||||
return false, "不在Docker镜像白名单内"
|
return false, "不在Docker镜像白名单内"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查黑名单
|
// 检查黑名单
|
||||||
if len(cfg.Proxy.BlackList) > 0 {
|
if len(cfg.Access.BlackList) > 0 {
|
||||||
if ac.matchImageInList(imageInfo, cfg.Proxy.BlackList) {
|
if ac.matchImageInList(imageInfo, cfg.Access.BlackList) {
|
||||||
return false, "Docker镜像在黑名单内"
|
return false, "Docker镜像在黑名单内"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,19 +106,19 @@ func (ac *AccessController) CheckGitHubAccess(matches []string) (allowed bool, r
|
|||||||
if len(matches) < 2 {
|
if len(matches) < 2 {
|
||||||
return false, "无效的GitHub仓库格式"
|
return false, "无效的GitHub仓库格式"
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := GetConfig()
|
cfg := GetConfig()
|
||||||
|
|
||||||
// 检查白名单
|
// 检查白名单
|
||||||
if len(cfg.Proxy.WhiteList) > 0 && !ac.checkList(matches, cfg.Proxy.WhiteList) {
|
if len(cfg.Access.WhiteList) > 0 && !ac.checkList(matches, cfg.Access.WhiteList) {
|
||||||
return false, "不在GitHub仓库白名单内"
|
return false, "不在GitHub仓库白名单内"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查黑名单
|
// 检查黑名单
|
||||||
if len(cfg.Proxy.BlackList) > 0 && ac.checkList(matches, cfg.Proxy.BlackList) {
|
if len(cfg.Access.BlackList) > 0 && ac.checkList(matches, cfg.Access.BlackList) {
|
||||||
return false, "GitHub仓库在黑名单内"
|
return false, "GitHub仓库在黑名单内"
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, ""
|
return true, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,28 +126,28 @@ func (ac *AccessController) CheckGitHubAccess(matches []string) (allowed bool, r
|
|||||||
func (ac *AccessController) matchImageInList(imageInfo DockerImageInfo, list []string) bool {
|
func (ac *AccessController) matchImageInList(imageInfo DockerImageInfo, list []string) bool {
|
||||||
fullName := strings.ToLower(imageInfo.FullName)
|
fullName := strings.ToLower(imageInfo.FullName)
|
||||||
namespace := strings.ToLower(imageInfo.Namespace)
|
namespace := strings.ToLower(imageInfo.Namespace)
|
||||||
|
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
item = strings.ToLower(strings.TrimSpace(item))
|
item = strings.ToLower(strings.TrimSpace(item))
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if fullName == item {
|
if fullName == item {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if item == namespace || item == namespace+"/*" {
|
if item == namespace || item == namespace+"/*" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasSuffix(item, "*") {
|
if strings.HasSuffix(item, "*") {
|
||||||
prefix := strings.TrimSuffix(item, "*")
|
prefix := strings.TrimSuffix(item, "*")
|
||||||
if strings.HasPrefix(fullName, prefix) {
|
if strings.HasPrefix(fullName, prefix) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(item, "*/") {
|
if strings.HasPrefix(item, "*/") {
|
||||||
repoPattern := strings.TrimPrefix(item, "*/")
|
repoPattern := strings.TrimPrefix(item, "*/")
|
||||||
if strings.HasSuffix(repoPattern, "*") {
|
if strings.HasSuffix(repoPattern, "*") {
|
||||||
@@ -161,7 +161,7 @@ func (ac *AccessController) matchImageInList(imageInfo DockerImageInfo, list []s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(fullName, item+"/") {
|
if strings.HasPrefix(fullName, item+"/") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -174,27 +174,27 @@ func (ac *AccessController) checkList(matches, list []string) bool {
|
|||||||
if len(matches) < 2 {
|
if len(matches) < 2 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
username := strings.ToLower(strings.TrimSpace(matches[0]))
|
username := strings.ToLower(strings.TrimSpace(matches[0]))
|
||||||
repoName := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(matches[1], ".git")))
|
repoName := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(matches[1], ".git")))
|
||||||
fullRepo := username + "/" + repoName
|
fullRepo := username + "/" + repoName
|
||||||
|
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
item = strings.ToLower(strings.TrimSpace(item))
|
item = strings.ToLower(strings.TrimSpace(item))
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 支持多种匹配模式
|
// 支持多种匹配模式
|
||||||
if fullRepo == item {
|
if fullRepo == item {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户级匹配
|
// 用户级匹配
|
||||||
if item == username || item == username+"/*" {
|
if item == username || item == username+"/*" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 前缀匹配(支持通配符)
|
// 前缀匹配(支持通配符)
|
||||||
if strings.HasSuffix(item, "*") {
|
if strings.HasSuffix(item, "*") {
|
||||||
prefix := strings.TrimSuffix(item, "*")
|
prefix := strings.TrimSuffix(item, "*")
|
||||||
@@ -202,7 +202,7 @@ func (ac *AccessController) checkList(matches, list []string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 子仓库匹配(防止 user/repo 匹配到 user/repo-fork)
|
// 子仓库匹配(防止 user/repo 匹配到 user/repo-fork)
|
||||||
if strings.HasPrefix(fullRepo, item+"/") {
|
if strings.HasPrefix(fullRepo, item+"/") {
|
||||||
return true
|
return true
|
||||||
@@ -210,5 +210,3 @@ func (ac *AccessController) checkList(matches, list []string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
551
src/config.go
551
src/config.go
@@ -1,275 +1,276 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
"github.com/pelletier/go-toml/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegistryMapping Registry映射配置
|
// RegistryMapping Registry映射配置
|
||||||
type RegistryMapping struct {
|
type RegistryMapping struct {
|
||||||
Upstream string `toml:"upstream"` // 上游Registry地址
|
Upstream string `toml:"upstream"` // 上游Registry地址
|
||||||
AuthHost string `toml:"authHost"` // 认证服务器地址
|
AuthHost string `toml:"authHost"` // 认证服务器地址
|
||||||
AuthType string `toml:"authType"` // 认证类型: docker/github/google/basic
|
AuthType string `toml:"authType"` // 认证类型: docker/github/google/basic
|
||||||
Enabled bool `toml:"enabled"` // 是否启用
|
Enabled bool `toml:"enabled"` // 是否启用
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppConfig 应用配置结构体
|
// AppConfig 应用配置结构体
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
Server struct {
|
Server struct {
|
||||||
Host string `toml:"host"` // 监听地址
|
Host string `toml:"host"` // 监听地址
|
||||||
Port int `toml:"port"` // 监听端口
|
Port int `toml:"port"` // 监听端口
|
||||||
FileSize int64 `toml:"fileSize"` // 文件大小限制(字节)
|
FileSize int64 `toml:"fileSize"` // 文件大小限制(字节)
|
||||||
} `toml:"server"`
|
} `toml:"server"`
|
||||||
|
|
||||||
RateLimit struct {
|
RateLimit struct {
|
||||||
RequestLimit int `toml:"requestLimit"` // 每小时请求限制
|
RequestLimit int `toml:"requestLimit"` // 每小时请求限制
|
||||||
PeriodHours float64 `toml:"periodHours"` // 限制周期(小时)
|
PeriodHours float64 `toml:"periodHours"` // 限制周期(小时)
|
||||||
} `toml:"rateLimit"`
|
} `toml:"rateLimit"`
|
||||||
|
|
||||||
Security struct {
|
Security struct {
|
||||||
WhiteList []string `toml:"whiteList"` // 白名单IP/CIDR列表
|
WhiteList []string `toml:"whiteList"` // 白名单IP/CIDR列表
|
||||||
BlackList []string `toml:"blackList"` // 黑名单IP/CIDR列表
|
BlackList []string `toml:"blackList"` // 黑名单IP/CIDR列表
|
||||||
} `toml:"security"`
|
} `toml:"security"`
|
||||||
|
|
||||||
Proxy struct {
|
Access struct {
|
||||||
WhiteList []string `toml:"whiteList"` // 代理白名单(仓库级别)
|
WhiteList []string `toml:"whiteList"` // 代理白名单(仓库级别)
|
||||||
BlackList []string `toml:"blackList"` // 代理黑名单(仓库级别)
|
BlackList []string `toml:"blackList"` // 代理黑名单(仓库级别)
|
||||||
Socks5 string `toml:"socks5"` // SOCKS5代理地址: socks5://[user:pass@]host:port
|
Proxy string `toml:"proxy"` // 代理地址: 支持 http/https/socks5/socks5h
|
||||||
} `toml:"proxy"`
|
} `toml:"proxy"`
|
||||||
|
|
||||||
Download struct {
|
Download struct {
|
||||||
MaxImages int `toml:"maxImages"` // 单次下载最大镜像数量限制
|
MaxImages int `toml:"maxImages"` // 单次下载最大镜像数量限制
|
||||||
} `toml:"download"`
|
} `toml:"download"`
|
||||||
|
|
||||||
Registries map[string]RegistryMapping `toml:"registries"`
|
Registries map[string]RegistryMapping `toml:"registries"`
|
||||||
|
|
||||||
TokenCache struct {
|
TokenCache struct {
|
||||||
Enabled bool `toml:"enabled"` // 是否启用token缓存
|
Enabled bool `toml:"enabled"` // 是否启用token缓存
|
||||||
DefaultTTL string `toml:"defaultTTL"` // 默认缓存时间
|
DefaultTTL string `toml:"defaultTTL"` // 默认缓存时间
|
||||||
} `toml:"tokenCache"`
|
} `toml:"tokenCache"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
appConfig *AppConfig
|
appConfig *AppConfig
|
||||||
appConfigLock sync.RWMutex
|
appConfigLock sync.RWMutex
|
||||||
|
|
||||||
cachedConfig *AppConfig
|
cachedConfig *AppConfig
|
||||||
configCacheTime time.Time
|
configCacheTime time.Time
|
||||||
configCacheTTL = 5 * time.Second
|
configCacheTTL = 5 * time.Second
|
||||||
configCacheMutex sync.RWMutex
|
configCacheMutex sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultConfig 返回默认配置
|
// todo:Refactoring is needed
|
||||||
func DefaultConfig() *AppConfig {
|
// DefaultConfig 返回默认配置
|
||||||
return &AppConfig{
|
func DefaultConfig() *AppConfig {
|
||||||
Server: struct {
|
return &AppConfig{
|
||||||
Host string `toml:"host"`
|
Server: struct {
|
||||||
Port int `toml:"port"`
|
Host string `toml:"host"`
|
||||||
FileSize int64 `toml:"fileSize"`
|
Port int `toml:"port"`
|
||||||
}{
|
FileSize int64 `toml:"fileSize"`
|
||||||
Host: "0.0.0.0",
|
}{
|
||||||
Port: 5000,
|
Host: "0.0.0.0",
|
||||||
FileSize: 2 * 1024 * 1024 * 1024, // 2GB
|
Port: 5000,
|
||||||
},
|
FileSize: 2 * 1024 * 1024 * 1024, // 2GB
|
||||||
RateLimit: struct {
|
},
|
||||||
RequestLimit int `toml:"requestLimit"`
|
RateLimit: struct {
|
||||||
PeriodHours float64 `toml:"periodHours"`
|
RequestLimit int `toml:"requestLimit"`
|
||||||
}{
|
PeriodHours float64 `toml:"periodHours"`
|
||||||
RequestLimit: 20,
|
}{
|
||||||
PeriodHours: 1.0,
|
RequestLimit: 20,
|
||||||
},
|
PeriodHours: 1.0,
|
||||||
Security: struct {
|
},
|
||||||
WhiteList []string `toml:"whiteList"`
|
Security: struct {
|
||||||
BlackList []string `toml:"blackList"`
|
WhiteList []string `toml:"whiteList"`
|
||||||
}{
|
BlackList []string `toml:"blackList"`
|
||||||
WhiteList: []string{},
|
}{
|
||||||
BlackList: []string{},
|
WhiteList: []string{},
|
||||||
},
|
BlackList: []string{},
|
||||||
Proxy: struct {
|
},
|
||||||
WhiteList []string `toml:"whiteList"`
|
Access: struct {
|
||||||
BlackList []string `toml:"blackList"`
|
WhiteList []string `toml:"whiteList"`
|
||||||
Socks5 string `toml:"socks5"`
|
BlackList []string `toml:"blackList"`
|
||||||
}{
|
Proxy string `toml:"proxy"`
|
||||||
WhiteList: []string{},
|
}{
|
||||||
BlackList: []string{},
|
WhiteList: []string{},
|
||||||
Socks5: "", // 默认不使用代理
|
BlackList: []string{},
|
||||||
},
|
Proxy: "", // 默认不使用代理
|
||||||
Download: struct {
|
},
|
||||||
MaxImages int `toml:"maxImages"`
|
Download: struct {
|
||||||
}{
|
MaxImages int `toml:"maxImages"`
|
||||||
MaxImages: 10, // 默认值:最多同时下载10个镜像
|
}{
|
||||||
},
|
MaxImages: 10, // 默认值:最多同时下载10个镜像
|
||||||
Registries: map[string]RegistryMapping{
|
},
|
||||||
"ghcr.io": {
|
Registries: map[string]RegistryMapping{
|
||||||
Upstream: "ghcr.io",
|
"ghcr.io": {
|
||||||
AuthHost: "ghcr.io/token",
|
Upstream: "ghcr.io",
|
||||||
AuthType: "github",
|
AuthHost: "ghcr.io/token",
|
||||||
Enabled: true,
|
AuthType: "github",
|
||||||
},
|
Enabled: true,
|
||||||
"gcr.io": {
|
},
|
||||||
Upstream: "gcr.io",
|
"gcr.io": {
|
||||||
AuthHost: "gcr.io/v2/token",
|
Upstream: "gcr.io",
|
||||||
AuthType: "google",
|
AuthHost: "gcr.io/v2/token",
|
||||||
Enabled: true,
|
AuthType: "google",
|
||||||
},
|
Enabled: true,
|
||||||
"quay.io": {
|
},
|
||||||
Upstream: "quay.io",
|
"quay.io": {
|
||||||
AuthHost: "quay.io/v2/auth",
|
Upstream: "quay.io",
|
||||||
AuthType: "quay",
|
AuthHost: "quay.io/v2/auth",
|
||||||
Enabled: true,
|
AuthType: "quay",
|
||||||
},
|
Enabled: true,
|
||||||
"registry.k8s.io": {
|
},
|
||||||
Upstream: "registry.k8s.io",
|
"registry.k8s.io": {
|
||||||
AuthHost: "registry.k8s.io",
|
Upstream: "registry.k8s.io",
|
||||||
AuthType: "anonymous",
|
AuthHost: "registry.k8s.io",
|
||||||
Enabled: true,
|
AuthType: "anonymous",
|
||||||
},
|
Enabled: true,
|
||||||
},
|
},
|
||||||
TokenCache: struct {
|
},
|
||||||
Enabled bool `toml:"enabled"`
|
TokenCache: struct {
|
||||||
DefaultTTL string `toml:"defaultTTL"`
|
Enabled bool `toml:"enabled"`
|
||||||
}{
|
DefaultTTL string `toml:"defaultTTL"`
|
||||||
Enabled: true, // docker认证的匿名Token缓存配置,用于提升性能
|
}{
|
||||||
DefaultTTL: "20m",
|
Enabled: true, // docker认证的匿名Token缓存配置,用于提升性能
|
||||||
},
|
DefaultTTL: "20m",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// GetConfig 安全地获取配置副本
|
|
||||||
func GetConfig() *AppConfig {
|
// GetConfig 安全地获取配置副本
|
||||||
configCacheMutex.RLock()
|
func GetConfig() *AppConfig {
|
||||||
if cachedConfig != nil && time.Since(configCacheTime) < configCacheTTL {
|
configCacheMutex.RLock()
|
||||||
config := cachedConfig
|
if cachedConfig != nil && time.Since(configCacheTime) < configCacheTTL {
|
||||||
configCacheMutex.RUnlock()
|
config := cachedConfig
|
||||||
return config
|
configCacheMutex.RUnlock()
|
||||||
}
|
return config
|
||||||
configCacheMutex.RUnlock()
|
}
|
||||||
|
configCacheMutex.RUnlock()
|
||||||
// 缓存过期,重新生成配置
|
|
||||||
configCacheMutex.Lock()
|
// 缓存过期,重新生成配置
|
||||||
defer configCacheMutex.Unlock()
|
configCacheMutex.Lock()
|
||||||
|
defer configCacheMutex.Unlock()
|
||||||
// 双重检查,防止重复生成
|
|
||||||
if cachedConfig != nil && time.Since(configCacheTime) < configCacheTTL {
|
// 双重检查,防止重复生成
|
||||||
return cachedConfig
|
if cachedConfig != nil && time.Since(configCacheTime) < configCacheTTL {
|
||||||
}
|
return cachedConfig
|
||||||
|
}
|
||||||
appConfigLock.RLock()
|
|
||||||
if appConfig == nil {
|
appConfigLock.RLock()
|
||||||
appConfigLock.RUnlock()
|
if appConfig == nil {
|
||||||
defaultCfg := DefaultConfig()
|
appConfigLock.RUnlock()
|
||||||
cachedConfig = defaultCfg
|
defaultCfg := DefaultConfig()
|
||||||
configCacheTime = time.Now()
|
cachedConfig = defaultCfg
|
||||||
return defaultCfg
|
configCacheTime = time.Now()
|
||||||
}
|
return defaultCfg
|
||||||
|
}
|
||||||
// 生成新的配置深拷贝
|
|
||||||
configCopy := *appConfig
|
// 生成新的配置深拷贝
|
||||||
configCopy.Security.WhiteList = append([]string(nil), appConfig.Security.WhiteList...)
|
configCopy := *appConfig
|
||||||
configCopy.Security.BlackList = append([]string(nil), appConfig.Security.BlackList...)
|
configCopy.Security.WhiteList = append([]string(nil), appConfig.Security.WhiteList...)
|
||||||
configCopy.Proxy.WhiteList = append([]string(nil), appConfig.Proxy.WhiteList...)
|
configCopy.Security.BlackList = append([]string(nil), appConfig.Security.BlackList...)
|
||||||
configCopy.Proxy.BlackList = append([]string(nil), appConfig.Proxy.BlackList...)
|
configCopy.Access.WhiteList = append([]string(nil), appConfig.Access.WhiteList...)
|
||||||
appConfigLock.RUnlock()
|
configCopy.Access.BlackList = append([]string(nil), appConfig.Access.BlackList...)
|
||||||
|
appConfigLock.RUnlock()
|
||||||
cachedConfig = &configCopy
|
|
||||||
configCacheTime = time.Now()
|
cachedConfig = &configCopy
|
||||||
|
configCacheTime = time.Now()
|
||||||
return cachedConfig
|
|
||||||
}
|
return cachedConfig
|
||||||
|
}
|
||||||
// setConfig 安全地设置配置
|
|
||||||
func setConfig(cfg *AppConfig) {
|
// setConfig 安全地设置配置
|
||||||
appConfigLock.Lock()
|
func setConfig(cfg *AppConfig) {
|
||||||
defer appConfigLock.Unlock()
|
appConfigLock.Lock()
|
||||||
appConfig = cfg
|
defer appConfigLock.Unlock()
|
||||||
|
appConfig = cfg
|
||||||
configCacheMutex.Lock()
|
|
||||||
cachedConfig = nil
|
configCacheMutex.Lock()
|
||||||
configCacheMutex.Unlock()
|
cachedConfig = nil
|
||||||
}
|
configCacheMutex.Unlock()
|
||||||
|
}
|
||||||
// LoadConfig 加载配置文件
|
|
||||||
func LoadConfig() error {
|
// LoadConfig 加载配置文件
|
||||||
// 首先使用默认配置
|
func LoadConfig() error {
|
||||||
cfg := DefaultConfig()
|
// 首先使用默认配置
|
||||||
|
cfg := DefaultConfig()
|
||||||
// 尝试加载TOML配置文件
|
|
||||||
if data, err := os.ReadFile("config.toml"); err == nil {
|
// 尝试加载TOML配置文件
|
||||||
if err := toml.Unmarshal(data, cfg); err != nil {
|
if data, err := os.ReadFile("config.toml"); err == nil {
|
||||||
return fmt.Errorf("解析配置文件失败: %v", err)
|
if err := toml.Unmarshal(data, cfg); err != nil {
|
||||||
}
|
return fmt.Errorf("解析配置文件失败: %v", err)
|
||||||
} else {
|
}
|
||||||
fmt.Println("未找到config.toml,使用默认配置")
|
} else {
|
||||||
}
|
fmt.Println("未找到config.toml,使用默认配置")
|
||||||
|
}
|
||||||
// 从环境变量覆盖配置
|
|
||||||
overrideFromEnv(cfg)
|
// 从环境变量覆盖配置
|
||||||
|
overrideFromEnv(cfg)
|
||||||
// 设置配置
|
|
||||||
setConfig(cfg)
|
// 设置配置
|
||||||
|
setConfig(cfg)
|
||||||
return nil
|
|
||||||
}
|
return nil
|
||||||
|
}
|
||||||
// overrideFromEnv 从环境变量覆盖配置
|
|
||||||
func overrideFromEnv(cfg *AppConfig) {
|
// overrideFromEnv 从环境变量覆盖配置
|
||||||
// 服务器配置
|
func overrideFromEnv(cfg *AppConfig) {
|
||||||
if val := os.Getenv("SERVER_HOST"); val != "" {
|
// 服务器配置
|
||||||
cfg.Server.Host = val
|
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 {
|
if val := os.Getenv("SERVER_PORT"); val != "" {
|
||||||
cfg.Server.Port = port
|
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 {
|
if val := os.Getenv("MAX_FILE_SIZE"); val != "" {
|
||||||
cfg.Server.FileSize = size
|
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 {
|
if val := os.Getenv("RATE_LIMIT"); val != "" {
|
||||||
cfg.RateLimit.RequestLimit = limit
|
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 {
|
if val := os.Getenv("RATE_PERIOD_HOURS"); val != "" {
|
||||||
cfg.RateLimit.PeriodHours = period
|
if period, err := strconv.ParseFloat(val, 64); err == nil && period > 0 {
|
||||||
}
|
cfg.RateLimit.PeriodHours = period
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// IP限制配置
|
|
||||||
if val := os.Getenv("IP_WHITELIST"); val != "" {
|
// IP限制配置
|
||||||
cfg.Security.WhiteList = append(cfg.Security.WhiteList, strings.Split(val, ",")...)
|
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("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 {
|
if val := os.Getenv("MAX_IMAGES"); val != "" {
|
||||||
cfg.Download.MaxImages = maxImages
|
if maxImages, err := strconv.Atoi(val); err == nil && maxImages > 0 {
|
||||||
}
|
cfg.Download.MaxImages = maxImages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// CreateDefaultConfigFile 创建默认配置文件
|
|
||||||
func CreateDefaultConfigFile() error {
|
// CreateDefaultConfigFile 创建默认配置文件
|
||||||
cfg := DefaultConfig()
|
func CreateDefaultConfigFile() error {
|
||||||
|
cfg := DefaultConfig()
|
||||||
data, err := toml.Marshal(cfg)
|
|
||||||
if err != nil {
|
data, err := toml.Marshal(cfg)
|
||||||
return fmt.Errorf("序列化默认配置失败: %v", err)
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("序列化默认配置失败: %v", err)
|
||||||
|
}
|
||||||
return os.WriteFile("config.toml", data, 0644)
|
|
||||||
}
|
return os.WriteFile("config.toml", data, 0644)
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ blackList = [
|
|||||||
"192.168.100.0/24"
|
"192.168.100.0/24"
|
||||||
]
|
]
|
||||||
|
|
||||||
[proxy]
|
[access]
|
||||||
# 代理服务白名单(支持GitHub仓库和Docker镜像,支持通配符)
|
# 代理服务白名单(支持GitHub仓库和Docker镜像,支持通配符)
|
||||||
# 只允许访问白名单中的仓库/镜像,为空时不限制
|
# 只允许访问白名单中的仓库/镜像,为空时不限制
|
||||||
whiteList = []
|
whiteList = []
|
||||||
@@ -39,11 +39,17 @@ blackList = [
|
|||||||
"baduser/*"
|
"baduser/*"
|
||||||
]
|
]
|
||||||
|
|
||||||
# SOCKS5代理配置,支持有用户名/密码认证和无认证模式
|
# 代理配置,支持有用户名/密码认证和无认证模式
|
||||||
# 无认证: socks5://127.0.0.1:1080
|
# 无认证: socks5://127.0.0.1:1080
|
||||||
# 有认证: socks5://username:password@127.0.0.1:1080
|
# 有认证: socks5://username:password@127.0.0.1:1080
|
||||||
|
# HTTP 代理示例
|
||||||
|
# http://username:password@127.0.0.1:7890
|
||||||
|
# SOCKS5 代理示例
|
||||||
|
# socks5://username:password@127.0.0.1:1080
|
||||||
|
# SOCKS5H 代理示例
|
||||||
|
# socks5h://username:password@127.0.0.1:1080
|
||||||
# 留空不使用代理
|
# 留空不使用代理
|
||||||
socks5 = ""
|
proxy = ""
|
||||||
|
|
||||||
[download]
|
[download]
|
||||||
# 批量下载离线镜像数量限制
|
# 批量下载离线镜像数量限制
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/google/go-containerregistry v0.20.5
|
github.com/google/go-containerregistry v0.20.5
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3
|
github.com/pelletier/go-toml/v2 v2.2.3
|
||||||
golang.org/x/net v0.33.0
|
|
||||||
golang.org/x/time v0.11.0
|
golang.org/x/time v0.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,6 +43,7 @@ require (
|
|||||||
github.com/vbatts/tar-split v0.12.1 // indirect
|
github.com/vbatts/tar-split v0.12.1 // indirect
|
||||||
golang.org/x/arch v0.8.0 // indirect
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
golang.org/x/crypto v0.32.0 // indirect
|
golang.org/x/crypto v0.32.0 // indirect
|
||||||
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sync v0.14.0 // indirect
|
golang.org/x/sync v0.14.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
|
|||||||
@@ -1,113 +1,68 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"net"
|
||||||
"log"
|
"net/http"
|
||||||
"net"
|
"os"
|
||||||
"net/http"
|
"time"
|
||||||
"net/url"
|
)
|
||||||
"time"
|
|
||||||
|
var (
|
||||||
"golang.org/x/net/proxy"
|
// 全局HTTP客户端 - 用于代理请求(长超时)
|
||||||
)
|
globalHTTPClient *http.Client
|
||||||
|
// 搜索HTTP客户端 - 用于API请求(短超时)
|
||||||
var (
|
searchHTTPClient *http.Client
|
||||||
// 全局HTTP客户端 - 用于代理请求(长超时)
|
)
|
||||||
globalHTTPClient *http.Client
|
|
||||||
// 搜索HTTP客户端 - 用于API请求(短超时)
|
// initHTTPClients 初始化HTTP客户端
|
||||||
searchHTTPClient *http.Client
|
func initHTTPClients() {
|
||||||
)
|
cfg := GetConfig()
|
||||||
|
|
||||||
// initHTTPClients 初始化HTTP客户端
|
if p := cfg.Access.Proxy; p != "" {
|
||||||
func initHTTPClients() {
|
os.Setenv("HTTP_PROXY", p)
|
||||||
cfg := GetConfig()
|
os.Setenv("HTTPS_PROXY", p)
|
||||||
|
}
|
||||||
// 创建DialContext函数,支持SOCKS5代理
|
// 代理客户端配置 - 适用于大文件传输
|
||||||
createDialContext := func(timeout time.Duration) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
globalHTTPClient = &http.Client{
|
||||||
if cfg.Proxy.Socks5 == "" {
|
Transport: &http.Transport{
|
||||||
// 没有配置代理,使用直连
|
Proxy: http.ProxyFromEnvironment,
|
||||||
dialer := &net.Dialer{
|
DialContext: (&net.Dialer{
|
||||||
Timeout: timeout,
|
Timeout: 30 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
KeepAlive: 30 * time.Second,
|
||||||
}
|
}).DialContext,
|
||||||
return dialer.DialContext
|
MaxIdleConns: 1000,
|
||||||
}
|
MaxIdleConnsPerHost: 1000,
|
||||||
|
IdleConnTimeout: 90 * time.Second,
|
||||||
// 解析SOCKS5代理URL
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
proxyURL, err := url.Parse(cfg.Proxy.Socks5)
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
if err != nil {
|
ResponseHeaderTimeout: 300 * time.Second,
|
||||||
log.Printf("SOCKS5代理配置错误,使用直连: %v", err)
|
},
|
||||||
dialer := &net.Dialer{
|
}
|
||||||
Timeout: timeout,
|
|
||||||
KeepAlive: 30 * time.Second,
|
// 搜索客户端配置 - 适用于API调用
|
||||||
}
|
searchHTTPClient = &http.Client{
|
||||||
return dialer.DialContext
|
Timeout: 10 * time.Second,
|
||||||
}
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
// 创建基础dialer
|
DialContext: (&net.Dialer{
|
||||||
baseDialer := &net.Dialer{
|
Timeout: 5 * time.Second,
|
||||||
Timeout: timeout,
|
KeepAlive: 30 * time.Second,
|
||||||
KeepAlive: 30 * time.Second,
|
}).DialContext,
|
||||||
}
|
MaxIdleConns: 100,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
// 创建SOCKS5代理dialer
|
IdleConnTimeout: 90 * time.Second,
|
||||||
var auth *proxy.Auth
|
TLSHandshakeTimeout: 5 * time.Second,
|
||||||
if proxyURL.User != nil {
|
DisableCompression: false,
|
||||||
if password, ok := proxyURL.User.Password(); ok {
|
},
|
||||||
auth = &proxy.Auth{
|
}
|
||||||
User: proxyURL.User.Username(),
|
}
|
||||||
Password: password,
|
|
||||||
}
|
// GetGlobalHTTPClient 获取全局HTTP客户端(用于代理)
|
||||||
}
|
func GetGlobalHTTPClient() *http.Client {
|
||||||
}
|
return globalHTTPClient
|
||||||
|
}
|
||||||
socks5Dialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, baseDialer)
|
|
||||||
if err != nil {
|
// GetSearchHTTPClient 获取搜索HTTP客户端(用于API调用)
|
||||||
log.Printf("创建SOCKS5代理失败,使用直连: %v", err)
|
func GetSearchHTTPClient() *http.Client {
|
||||||
return baseDialer.DialContext
|
return searchHTTPClient
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("使用SOCKS5代理: %s", proxyURL.Host)
|
|
||||||
|
|
||||||
// 返回带上下文的dial函数
|
|
||||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return socks5Dialer.Dial(network, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 代理客户端配置 - 适用于大文件传输
|
|
||||||
globalHTTPClient = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: createDialContext(30 * time.Second),
|
|
||||||
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: createDialContext(5 * time.Second),
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user