mirror of
https://github.com/sky22333/hubproxy.git
synced 2026-06-26 18:12:27 +08:00
239 lines
6.1 KiB
Go
239 lines
6.1 KiB
Go
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
|
||
}
|
||
|
||
// 🔥 Reload 热重载访问控制规则
|
||
func (ac *AccessController) Reload() {
|
||
ac.mu.Lock()
|
||
defer ac.mu.Unlock()
|
||
|
||
// 访问控制器本身不缓存配置,每次检查时都会调用GetConfig()
|
||
// 所以这里只需要确保锁的原子性,实际的重载在GetConfig()中完成
|
||
// 可以在这里添加一些初始化逻辑,比如预编译正则表达式等
|
||
|
||
// 目前访问控制器设计为无状态的,每次检查都读取最新配置
|
||
// 这样设计的好处是配置更新后无需额外处理,自动生效
|
||
} |