Merge commit 'refs/pull/origin/28'

This commit is contained in:
user123456
2025-06-21 00:30:51 +08:00
13 changed files with 2092 additions and 2128 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.idea
.vscode
.DS_Store
hubproxy*

View File

@@ -138,11 +138,17 @@ blackList = [
"baduser/*"
]
# SOCKS5代理配置,支持有用户名/密码认证和无认证模式
# 代理配置,支持有用户名/密码认证和无认证模式
# 无认证: socks5://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]
# 批量下载离线镜像数量限制

View File

@@ -85,15 +85,15 @@ func (ac *AccessController) CheckDockerAccess(image string) (allowed bool, reaso
imageInfo := ac.ParseDockerImage(image)
// 检查白名单(如果配置了白名单,则只允许白名单中的镜像)
if len(cfg.Proxy.WhiteList) > 0 {
if !ac.matchImageInList(imageInfo, cfg.Proxy.WhiteList) {
if len(cfg.Access.WhiteList) > 0 {
if !ac.matchImageInList(imageInfo, cfg.Access.WhiteList) {
return false, "不在Docker镜像白名单内"
}
}
// 检查黑名单
if len(cfg.Proxy.BlackList) > 0 {
if ac.matchImageInList(imageInfo, cfg.Proxy.BlackList) {
if len(cfg.Access.BlackList) > 0 {
if ac.matchImageInList(imageInfo, cfg.Access.BlackList) {
return false, "Docker镜像在黑名单内"
}
}
@@ -110,12 +110,12 @@ func (ac *AccessController) CheckGitHubAccess(matches []string) (allowed bool, r
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仓库白名单内"
}
// 检查黑名单
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仓库在黑名单内"
}
@@ -210,5 +210,3 @@ func (ac *AccessController) checkList(matches, list []string) bool {
}
return false
}

View File

@@ -37,11 +37,11 @@ type AppConfig struct {
BlackList []string `toml:"blackList"` // 黑名单IP/CIDR列表
} `toml:"security"`
Proxy struct {
Access struct {
WhiteList []string `toml:"whiteList"` // 代理白名单(仓库级别)
BlackList []string `toml:"blackList"` // 代理黑名单(仓库级别)
Socks5 string `toml:"socks5"` // SOCKS5代理地址: socks5://[user:pass@]host:port
} `toml:"proxy"`
Proxy string `toml:"proxy"` // 代理地址: 支持 http/https/socks5/socks5h
} `toml:"access"`
Download struct {
MaxImages int `toml:"maxImages"` // 单次下载最大镜像数量限制
@@ -65,6 +65,7 @@ var (
configCacheMutex sync.RWMutex
)
// todo:Refactoring is needed
// DefaultConfig 返回默认配置
func DefaultConfig() *AppConfig {
return &AppConfig{
@@ -91,14 +92,14 @@ func DefaultConfig() *AppConfig {
WhiteList: []string{},
BlackList: []string{},
},
Proxy: struct {
Access: struct {
WhiteList []string `toml:"whiteList"`
BlackList []string `toml:"blackList"`
Socks5 string `toml:"socks5"`
Proxy string `toml:"proxy"`
}{
WhiteList: []string{},
BlackList: []string{},
Socks5: "", // 默认不使用代理
Proxy: "", // 默认不使用代理
},
Download: struct {
MaxImages int `toml:"maxImages"`
@@ -173,8 +174,8 @@ func GetConfig() *AppConfig {
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...)
configCopy.Access.WhiteList = append([]string(nil), appConfig.Access.WhiteList...)
configCopy.Access.BlackList = append([]string(nil), appConfig.Access.BlackList...)
appConfigLock.RUnlock()
cachedConfig = &configCopy

View File

@@ -26,7 +26,7 @@ blackList = [
"192.168.100.0/24"
]
[proxy]
[access]
# 代理服务白名单支持GitHub仓库和Docker镜像支持通配符
# 只允许访问白名单中的仓库/镜像,为空时不限制
whiteList = []
@@ -39,11 +39,17 @@ blackList = [
"baduser/*"
]
# SOCKS5代理配置,支持有用户名/密码认证和无认证模式
# 代理配置,支持有用户名/密码认证和无认证模式
# 无认证: socks5://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]
# 批量下载离线镜像数量限制

View File

@@ -6,7 +6,6 @@ require (
github.com/gin-gonic/gin v1.10.0
github.com/google/go-containerregistry v0.20.5
github.com/pelletier/go-toml/v2 v2.2.3
golang.org/x/net v0.33.0
golang.org/x/time v0.11.0
)
@@ -44,6 +43,7 @@ require (
github.com/vbatts/tar-split v0.12.1 // indirect
golang.org/x/arch v0.8.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/sys v0.33.0 // indirect
golang.org/x/text v0.21.0 // indirect

View File

@@ -1,14 +1,10 @@
package main
import (
"context"
"log"
"net"
"net/http"
"net/url"
"os"
"time"
"golang.org/x/net/proxy"
)
var (
@@ -22,63 +18,18 @@ var (
func initHTTPClients() {
cfg := GetConfig()
// 创建DialContext函数支持SOCKS5代理
createDialContext := func(timeout time.Duration) func(ctx context.Context, network, addr string) (net.Conn, error) {
if cfg.Proxy.Socks5 == "" {
// 没有配置代理,使用直连
dialer := &net.Dialer{
Timeout: timeout,
KeepAlive: 30 * time.Second,
}
return dialer.DialContext
}
// 解析SOCKS5代理URL
proxyURL, err := url.Parse(cfg.Proxy.Socks5)
if err != nil {
log.Printf("SOCKS5代理配置错误使用直连: %v", err)
dialer := &net.Dialer{
Timeout: timeout,
KeepAlive: 30 * time.Second,
}
return dialer.DialContext
}
// 创建基础dialer
baseDialer := &net.Dialer{
Timeout: timeout,
KeepAlive: 30 * time.Second,
}
// 创建SOCKS5代理dialer
var auth *proxy.Auth
if proxyURL.User != nil {
if password, ok := proxyURL.User.Password(); ok {
auth = &proxy.Auth{
User: proxyURL.User.Username(),
Password: password,
}
}
}
socks5Dialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, baseDialer)
if err != nil {
log.Printf("创建SOCKS5代理失败使用直连: %v", err)
return baseDialer.DialContext
}
log.Printf("使用SOCKS5代理: %s", proxyURL.Host)
// 返回带上下文的dial函数
return func(ctx context.Context, network, addr string) (net.Conn, error) {
return socks5Dialer.Dial(network, addr)
}
if p := cfg.Access.Proxy; p != "" {
os.Setenv("HTTP_PROXY", p)
os.Setenv("HTTPS_PROXY", p)
}
// 代理客户端配置 - 适用于大文件传输
globalHTTPClient = &http.Client{
Transport: &http.Transport{
DialContext: createDialContext(30 * time.Second),
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 1000,
MaxIdleConnsPerHost: 1000,
IdleConnTimeout: 90 * time.Second,
@@ -92,7 +43,11 @@ func initHTTPClients() {
searchHTTPClient = &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
DialContext: createDialContext(5 * time.Second),
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,

View File

@@ -385,12 +385,11 @@ func (is *ImageStreamer) streamDockerFormatWithReturn(ctx context.Context, tarWr
log.Printf("已处理层 %d/%d", i+1, len(layers))
}
// 构建单个镜像的manifest信息
singleManifest := map[string]interface{}{
"Config": configDigest.String() + ".json",
"RepoTags": []string{imageRef},
"Layers": func() []string {
"Layers": func() []string {
var layers []string
for _, digest := range layerDigests {
layers = append(layers, digest+"/layer.tar")
@@ -549,8 +548,8 @@ func (is *ImageStreamer) selectPlatformImage(desc *remote.Descriptor, options *S
}
if m.Platform.OS == targetOS &&
m.Platform.Architecture == targetArch &&
m.Platform.Variant == targetVariant {
m.Platform.Architecture == targetArch &&
m.Platform.Variant == targetVariant {
selectedDesc = &m
break
}
@@ -632,7 +631,7 @@ func handleDirectImageDownload(c *gin.Context) {
if !singleImageDebouncer.ShouldAllow(userID, contentKey) {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "请求过于频繁,请稍后再试",
"error": "请求过于频繁,请稍后再试",
"retry_after": 5,
})
return
@@ -692,7 +691,7 @@ func handleSimpleBatchDownload(c *gin.Context) {
if !batchImageDebouncer.ShouldAllow(userID, contentKey) {
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "批量下载请求过于频繁,请稍后再试",
"error": "批量下载请求过于频繁,请稍后再试",
"retry_after": 60,
})
return

View File

@@ -122,7 +122,6 @@ func main() {
// 注册Docker Registry代理路由
router.Any("/v2/*path", ProxyDockerRegistryGin)
// 注册NoRoute处理器
router.NoRoute(handler)
@@ -177,12 +176,10 @@ func handler(c *gin.Context) {
proxyRequest(c, rawPath)
}
func proxyRequest(c *gin.Context, u string) {
proxyWithRedirect(c, u, 0)
}
func proxyWithRedirect(c *gin.Context, u string, redirectCount int) {
// 限制最大重定向次数,防止无限递归
const maxRedirects = 20

View File

@@ -14,7 +14,7 @@ import (
const (
// 清理间隔
CleanupInterval = 10 * time.Minute
MaxIPCacheSize = 10000
MaxIPCacheSize = 10000
)
// IPRateLimiter IP限流器结构体
@@ -233,7 +233,7 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
// 静态文件豁免:跳过限流检查
path := c.Request.URL.Path
if path == "/" || path == "/favicon.ico" || path == "/images.html" || path == "/search.html" ||
strings.HasPrefix(path, "/public/") {
strings.HasPrefix(path, "/public/") {
c.Next()
return
}
@@ -299,5 +299,3 @@ func RateLimitMiddleware(limiter *IPRateLimiter) gin.HandlerFunc {
c.Next()
}
}

View File

@@ -13,10 +13,10 @@ import (
// CachedItem 通用缓存项支持Token和Manifest
type CachedItem struct {
Data []byte // 缓存数据(token字符串或manifest字节)
ContentType string // 内容类型
Data []byte // 缓存数据(token字符串或manifest字节)
ContentType string // 内容类型
Headers map[string]string // 额外的响应头
ExpiresAt time.Time // 过期时间
ExpiresAt time.Time // 过期时间
}
// UniversalCache 通用缓存支持Token和Manifest
@@ -86,7 +86,7 @@ func getManifestTTL(reference string) time.Duration {
// mutable tag的智能判断
if reference == "latest" || reference == "main" || reference == "master" ||
reference == "dev" || reference == "develop" {
reference == "dev" || reference == "develop" {
// 热门可变标签: 短期缓存
return 10 * time.Minute
}