Merge commit 'refs/pull/origin/28'
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/*"
|
||||
]
|
||||
|
||||
# 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]
|
||||
# 批量下载离线镜像数量限制
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
# 批量下载离线镜像数量限制
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user