feat(plugin): 实现插件安全验证和审计日志功能
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Failing after 19s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been skipped
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Failing after 19s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Has been skipped
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Has been skipped
- 添加插件签名验证机制,支持远程证书吊销列表 - 增加插件安装时的安全检查和签名验证 - 实现插件版本存储的HMAC完整性校验 - 添加插件审计日志记录插件安装和验证事件 - 增加JS插件沙箱安全限制配置 - 添加插件商店API的签名URL字段支持 - 实现安全配置的自动刷新机制
This commit is contained in:
@@ -5,11 +5,13 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gotunnel/internal/client/tunnel"
|
"github.com/gotunnel/internal/client/tunnel"
|
||||||
"github.com/gotunnel/pkg/crypto"
|
"github.com/gotunnel/pkg/crypto"
|
||||||
"github.com/gotunnel/pkg/plugin"
|
"github.com/gotunnel/pkg/plugin"
|
||||||
"github.com/gotunnel/pkg/plugin/builtin"
|
"github.com/gotunnel/pkg/plugin/builtin"
|
||||||
|
"github.com/gotunnel/pkg/plugin/sign"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -40,6 +42,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化安全配置
|
||||||
|
initSecurityConfig()
|
||||||
|
|
||||||
// 初始化插件系统
|
// 初始化插件系统
|
||||||
registry := plugin.NewRegistry()
|
registry := plugin.NewRegistry()
|
||||||
for _, h := range builtin.GetClientPlugins() {
|
for _, h := range builtin.GetClientPlugins() {
|
||||||
@@ -52,3 +57,38 @@ func main() {
|
|||||||
|
|
||||||
client.Run()
|
client.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 官方安全配置 URL(与服务端保持一致)
|
||||||
|
const (
|
||||||
|
officialRevocationURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/security/revocation.json"
|
||||||
|
officialKeyListURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/security/keys.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// initSecurityConfig 初始化安全配置
|
||||||
|
func initSecurityConfig() {
|
||||||
|
// 配置撤销列表
|
||||||
|
sign.SetRevocationConfig(sign.RevocationConfig{
|
||||||
|
RemoteURL: officialRevocationURL,
|
||||||
|
FetchInterval: 1 * time.Hour,
|
||||||
|
RequestTimeout: 10 * time.Second,
|
||||||
|
VerifySignature: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 配置公钥列表
|
||||||
|
sign.SetKeyListConfig(sign.KeyListConfig{
|
||||||
|
RemoteURL: officialKeyListURL,
|
||||||
|
FetchInterval: 24 * time.Hour,
|
||||||
|
RequestTimeout: 10 * time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动后台刷新
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
go sign.StartRevocationRefresher(stopCh)
|
||||||
|
|
||||||
|
// 立即拉取一次
|
||||||
|
if err := sign.FetchRemoteKeyList(); err != nil {
|
||||||
|
log.Printf("[Security] Fetch key list failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Security] Initialized")
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ func main() {
|
|||||||
log.Printf("[Server] TLS enabled")
|
log.Printf("[Server] TLS enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化安全配置(撤销列表和公钥列表)
|
||||||
|
initSecurityConfig()
|
||||||
|
|
||||||
// 初始化插件系统
|
// 初始化插件系统
|
||||||
registry := plugin.NewRegistry()
|
registry := plugin.NewRegistry()
|
||||||
if err := registry.RegisterAllServer(builtin.GetServerPlugins()); err != nil {
|
if err := registry.RegisterAllServer(builtin.GetServerPlugins()); err != nil {
|
||||||
@@ -188,3 +191,32 @@ func verifyPluginSignature(name, source, signature string) error {
|
|||||||
// 验证签名
|
// 验证签名
|
||||||
return sign.VerifyPlugin(pubKey, signed, source)
|
return sign.VerifyPlugin(pubKey, signed, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// initSecurityConfig 初始化安全配置
|
||||||
|
func initSecurityConfig() {
|
||||||
|
// 配置撤销列表
|
||||||
|
sign.SetRevocationConfig(sign.RevocationConfig{
|
||||||
|
RemoteURL: config.OfficialRevocationURL,
|
||||||
|
FetchInterval: 1 * time.Hour,
|
||||||
|
RequestTimeout: 10 * time.Second,
|
||||||
|
VerifySignature: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 配置公钥列表
|
||||||
|
sign.SetKeyListConfig(sign.KeyListConfig{
|
||||||
|
RemoteURL: config.OfficialKeyListURL,
|
||||||
|
FetchInterval: 24 * time.Hour,
|
||||||
|
RequestTimeout: 10 * time.Second,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动后台刷新
|
||||||
|
stopCh := make(chan struct{})
|
||||||
|
go sign.StartRevocationRefresher(stopCh)
|
||||||
|
|
||||||
|
// 立即拉取一次公钥列表
|
||||||
|
if err := sign.FetchRemoteKeyList(); err != nil {
|
||||||
|
log.Printf("[Security] Fetch key list failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[Security] Initialized with remote revocation and key list")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
package tunnel
|
package tunnel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// versionStoreData 版本存储数据结构(带 HMAC)
|
||||||
|
type versionStoreData struct {
|
||||||
|
Versions map[string]string `json:"versions"`
|
||||||
|
HMAC string `json:"hmac"`
|
||||||
|
}
|
||||||
|
|
||||||
// PluginVersionStore 插件版本存储
|
// PluginVersionStore 插件版本存储
|
||||||
type PluginVersionStore struct {
|
type PluginVersionStore struct {
|
||||||
path string
|
path string
|
||||||
|
hmacKey []byte
|
||||||
versions map[string]string // pluginName -> version
|
versions map[string]string // pluginName -> version
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
@@ -18,6 +29,7 @@ type PluginVersionStore struct {
|
|||||||
func NewPluginVersionStore(dataDir string) (*PluginVersionStore, error) {
|
func NewPluginVersionStore(dataDir string) (*PluginVersionStore, error) {
|
||||||
store := &PluginVersionStore{
|
store := &PluginVersionStore{
|
||||||
path: filepath.Join(dataDir, "plugin_versions.json"),
|
path: filepath.Join(dataDir, "plugin_versions.json"),
|
||||||
|
hmacKey: deriveHMACKey(dataDir),
|
||||||
versions: make(map[string]string),
|
versions: make(map[string]string),
|
||||||
}
|
}
|
||||||
if err := store.load(); err != nil {
|
if err := store.load(); err != nil {
|
||||||
@@ -26,6 +38,15 @@ func NewPluginVersionStore(dataDir string) (*PluginVersionStore, error) {
|
|||||||
return store, nil
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deriveHMACKey 从数据目录派生 HMAC 密钥
|
||||||
|
func deriveHMACKey(dataDir string) []byte {
|
||||||
|
// 使用数据目录路径和机器特征派生密钥
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
seed := fmt.Sprintf("gotunnel-version-store:%s:%s", dataDir, hostname)
|
||||||
|
hash := sha256.Sum256([]byte(seed))
|
||||||
|
return hash[:]
|
||||||
|
}
|
||||||
|
|
||||||
// load 从文件加载版本信息
|
// load 从文件加载版本信息
|
||||||
func (s *PluginVersionStore) load() error {
|
func (s *PluginVersionStore) load() error {
|
||||||
data, err := os.ReadFile(s.path)
|
data, err := os.ReadFile(s.path)
|
||||||
@@ -35,7 +56,26 @@ func (s *PluginVersionStore) load() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return json.Unmarshal(data, &s.versions)
|
|
||||||
|
var storeData versionStoreData
|
||||||
|
if err := json.Unmarshal(data, &storeData); err != nil {
|
||||||
|
// 尝试兼容旧格式(无 HMAC)
|
||||||
|
if err := json.Unmarshal(data, &s.versions); err != nil {
|
||||||
|
return fmt.Errorf("invalid version store format: %w", err)
|
||||||
|
}
|
||||||
|
// 迁移到新格式
|
||||||
|
return s.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证 HMAC
|
||||||
|
if !s.verifyHMAC(storeData.Versions, storeData.HMAC) {
|
||||||
|
// HMAC 验证失败,可能被篡改,重置版本信息
|
||||||
|
s.versions = make(map[string]string)
|
||||||
|
return fmt.Errorf("version store integrity check failed, data may be tampered")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.versions = storeData.Versions
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// save 保存版本信息到文件
|
// save 保存版本信息到文件
|
||||||
@@ -44,7 +84,16 @@ func (s *PluginVersionStore) save() error {
|
|||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data, err := json.MarshalIndent(s.versions, "", " ")
|
|
||||||
|
// 计算 HMAC
|
||||||
|
hmacValue := s.computeHMAC(s.versions)
|
||||||
|
|
||||||
|
storeData := versionStoreData{
|
||||||
|
Versions: s.versions,
|
||||||
|
HMAC: hmacValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(storeData, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -65,3 +114,17 @@ func (s *PluginVersionStore) SetVersion(name, version string) error {
|
|||||||
s.versions[name] = version
|
s.versions[name] = version
|
||||||
return s.save()
|
return s.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// computeHMAC 计算版本数据的 HMAC
|
||||||
|
func (s *PluginVersionStore) computeHMAC(versions map[string]string) string {
|
||||||
|
data, _ := json.Marshal(versions)
|
||||||
|
h := hmac.New(sha256.New, s.hmacKey)
|
||||||
|
h.Write(data)
|
||||||
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyHMAC 验证 HMAC
|
||||||
|
func (s *PluginVersionStore) verifyHMAC(versions map[string]string, expectedHMAC string) bool {
|
||||||
|
computed := s.computeHMAC(versions)
|
||||||
|
return hmac.Equal([]byte(computed), []byte(expectedHMAC))
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ type PluginStoreSettings struct {
|
|||||||
// 官方插件商店(不可配置)
|
// 官方插件商店(不可配置)
|
||||||
const OfficialPluginStoreURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/store.json"
|
const OfficialPluginStoreURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/store.json"
|
||||||
|
|
||||||
|
// 官方安全配置 URL
|
||||||
|
const (
|
||||||
|
OfficialRevocationURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/security/revocation.json"
|
||||||
|
OfficialKeyListURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/security/keys.json"
|
||||||
|
)
|
||||||
|
|
||||||
// ServerSettings 服务端设置
|
// ServerSettings 服务端设置
|
||||||
type ServerSettings struct {
|
type ServerSettings struct {
|
||||||
BindAddr string `yaml:"bind_addr"`
|
BindAddr string `yaml:"bind_addr"`
|
||||||
|
|||||||
@@ -581,12 +581,14 @@ type StorePluginInfo struct {
|
|||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Icon string `json:"icon,omitempty"`
|
Icon string `json:"icon,omitempty"`
|
||||||
DownloadURL string `json:"download_url,omitempty"`
|
DownloadURL string `json:"download_url,omitempty"`
|
||||||
|
SignatureURL string `json:"signature_url,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StorePluginInstallRequest 从商店安装插件的请求
|
// StorePluginInstallRequest 从商店安装插件的请求
|
||||||
type StorePluginInstallRequest struct {
|
type StorePluginInstallRequest struct {
|
||||||
PluginName string `json:"plugin_name"`
|
PluginName string `json:"plugin_name"`
|
||||||
DownloadURL string `json:"download_url"`
|
DownloadURL string `json:"download_url"`
|
||||||
|
SignatureURL string `json:"signature_url"`
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,7 +631,6 @@ func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request)
|
|||||||
|
|
||||||
h.jsonResponse(rw, map[string]interface{}{
|
h.jsonResponse(rw, map[string]interface{}{
|
||||||
"plugins": plugins,
|
"plugins": plugins,
|
||||||
"store_url": storeURL,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,8 +647,8 @@ func (h *APIHandler) handleStoreInstall(rw http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.PluginName == "" || req.DownloadURL == "" || req.ClientID == "" {
|
if req.PluginName == "" || req.DownloadURL == "" || req.ClientID == "" || req.SignatureURL == "" {
|
||||||
http.Error(rw, "plugin_name, download_url and client_id required", http.StatusBadRequest)
|
http.Error(rw, "plugin_name, download_url, signature_url and client_id required", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,10 +679,30 @@ func (h *APIHandler) handleStoreInstall(rw http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载签名文件
|
||||||
|
sigResp, err := client.Get(req.SignatureURL)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, "Failed to download signature: "+err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sigResp.Body.Close()
|
||||||
|
|
||||||
|
if sigResp.StatusCode != http.StatusOK {
|
||||||
|
http.Error(rw, "Signature download failed with status: "+sigResp.Status, http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := io.ReadAll(sigResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(rw, "Failed to read signature: "+err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 安装到客户端
|
// 安装到客户端
|
||||||
installReq := JSPluginInstallRequest{
|
installReq := JSPluginInstallRequest{
|
||||||
PluginName: req.PluginName,
|
PluginName: req.PluginName,
|
||||||
Source: string(source),
|
Source: string(source),
|
||||||
|
Signature: string(signature),
|
||||||
RuleName: req.PluginName,
|
RuleName: req.PluginName,
|
||||||
AutoStart: true,
|
AutoStart: true,
|
||||||
}
|
}
|
||||||
|
|||||||
154
pkg/plugin/audit/audit.go
Normal file
154
pkg/plugin/audit/audit.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package audit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventType 审计事件类型
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventPluginInstall EventType = "plugin_install"
|
||||||
|
EventPluginUninstall EventType = "plugin_uninstall"
|
||||||
|
EventPluginStart EventType = "plugin_start"
|
||||||
|
EventPluginStop EventType = "plugin_stop"
|
||||||
|
EventPluginVerify EventType = "plugin_verify"
|
||||||
|
EventPluginReject EventType = "plugin_reject"
|
||||||
|
EventConfigChange EventType = "config_change"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event 审计事件
|
||||||
|
type Event struct {
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Type EventType `json:"type"`
|
||||||
|
PluginName string `json:"plugin_name,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
ClientID string `json:"client_id,omitempty"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Details map[string]string `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger 审计日志记录器
|
||||||
|
type Logger struct {
|
||||||
|
path string
|
||||||
|
file *os.File
|
||||||
|
mu sync.Mutex
|
||||||
|
enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultLogger *Logger
|
||||||
|
loggerOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLogger 创建审计日志记录器
|
||||||
|
func NewLogger(dataDir string) (*Logger, error) {
|
||||||
|
path := filepath.Join(dataDir, "audit.log")
|
||||||
|
dir := filepath.Dir(path)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Logger{path: path, file: file, enabled: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitDefault 初始化默认日志记录器
|
||||||
|
func InitDefault(dataDir string) error {
|
||||||
|
var err error
|
||||||
|
loggerOnce.Do(func() {
|
||||||
|
defaultLogger, err = NewLogger(dataDir)
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log 记录审计事件
|
||||||
|
func (l *Logger) Log(event Event) {
|
||||||
|
if l == nil || !l.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.Timestamp = time.Now()
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[Audit] Marshal error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := l.file.Write(append(data, '\n')); err != nil {
|
||||||
|
log.Printf("[Audit] Write error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭日志文件
|
||||||
|
func (l *Logger) Close() error {
|
||||||
|
if l == nil || l.file == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return l.file.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogEvent 使用默认记录器记录事件
|
||||||
|
func LogEvent(event Event) {
|
||||||
|
if defaultLogger != nil {
|
||||||
|
defaultLogger.Log(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogPluginInstall 记录插件安装事件
|
||||||
|
func LogPluginInstall(pluginName, version, clientID string, success bool, msg string) {
|
||||||
|
LogEvent(Event{
|
||||||
|
Type: EventPluginInstall,
|
||||||
|
PluginName: pluginName,
|
||||||
|
Version: version,
|
||||||
|
ClientID: clientID,
|
||||||
|
Success: success,
|
||||||
|
Message: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogPluginVerify 记录插件验证事件
|
||||||
|
func LogPluginVerify(pluginName, version string, success bool, msg string) {
|
||||||
|
LogEvent(Event{
|
||||||
|
Type: EventPluginVerify,
|
||||||
|
PluginName: pluginName,
|
||||||
|
Version: version,
|
||||||
|
Success: success,
|
||||||
|
Message: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogPluginReject 记录插件拒绝事件
|
||||||
|
func LogPluginReject(pluginName, version, reason string) {
|
||||||
|
LogEvent(Event{
|
||||||
|
Type: EventPluginReject,
|
||||||
|
PluginName: pluginName,
|
||||||
|
Version: version,
|
||||||
|
Success: false,
|
||||||
|
Message: reason,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogWithDetails 记录带详情的事件
|
||||||
|
func LogWithDetails(eventType EventType, pluginName string, success bool, msg string, details map[string]string) {
|
||||||
|
LogEvent(Event{
|
||||||
|
Type: eventType,
|
||||||
|
PluginName: pluginName,
|
||||||
|
Success: success,
|
||||||
|
Message: msg,
|
||||||
|
Details: details,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -48,6 +48,11 @@ func (p *JSPlugin) SetSandbox(sandbox *Sandbox) {
|
|||||||
|
|
||||||
// init 初始化 JS 运行时
|
// init 初始化 JS 运行时
|
||||||
func (p *JSPlugin) init() error {
|
func (p *JSPlugin) init() error {
|
||||||
|
// 设置栈深度限制(防止递归攻击)
|
||||||
|
if p.sandbox.MaxStackDepth > 0 {
|
||||||
|
p.vm.SetMaxCallStackSize(p.sandbox.MaxStackDepth)
|
||||||
|
}
|
||||||
|
|
||||||
// 注入基础 API
|
// 注入基础 API
|
||||||
p.vm.Set("log", p.jsLog)
|
p.vm.Set("log", p.jsLog)
|
||||||
p.vm.Set("config", p.jsGetConfig)
|
p.vm.Set("config", p.jsGetConfig)
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ type Sandbox struct {
|
|||||||
MaxReadSize int64
|
MaxReadSize int64
|
||||||
// 最大文件写入大小 (bytes)
|
// 最大文件写入大小 (bytes)
|
||||||
MaxWriteSize int64
|
MaxWriteSize int64
|
||||||
|
// 最大内存使用量 (bytes),0 表示不限制
|
||||||
|
MaxMemory int64
|
||||||
|
// 最大调用栈深度
|
||||||
|
MaxStackDepth int
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultSandbox 返回默认沙箱配置(最小权限)
|
// DefaultSandbox 返回默认沙箱配置(最小权限)
|
||||||
@@ -32,6 +36,8 @@ func DefaultSandbox() *Sandbox {
|
|||||||
AllowNetwork: false,
|
AllowNetwork: false,
|
||||||
MaxReadSize: 10 * 1024 * 1024, // 10MB
|
MaxReadSize: 10 * 1024 * 1024, // 10MB
|
||||||
MaxWriteSize: 1 * 1024 * 1024, // 1MB
|
MaxWriteSize: 1 * 1024 * 1024, // 1MB
|
||||||
|
MaxMemory: 64 * 1024 * 1024, // 64MB
|
||||||
|
MaxStackDepth: 1000, // 最大调用栈深度
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,17 +2,21 @@ package sign
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyEntry 密钥条目
|
// KeyEntry 密钥条目
|
||||||
type KeyEntry struct {
|
type KeyEntry struct {
|
||||||
ID string // 密钥 ID
|
ID string `json:"id"`
|
||||||
PublicKey string // Base64 编码的公钥
|
PublicKey string `json:"public_key"`
|
||||||
ValidFrom time.Time // 生效时间
|
ValidFrom time.Time `json:"valid_from"`
|
||||||
RevokedAt time.Time // 吊销时间(零值表示未吊销)
|
RevokedAt time.Time `json:"revoked_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 官方公钥列表(支持密钥轮换)
|
// 官方公钥列表(支持密钥轮换)
|
||||||
@@ -28,8 +32,30 @@ var officialKeys = []KeyEntry{
|
|||||||
var (
|
var (
|
||||||
keyCache map[string]ed25519.PublicKey
|
keyCache map[string]ed25519.PublicKey
|
||||||
keyCacheOnce sync.Once
|
keyCacheOnce sync.Once
|
||||||
|
keyMu sync.RWMutex
|
||||||
|
remoteKeys []KeyEntry
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// KeyListConfig 远程公钥列表配置
|
||||||
|
type KeyListConfig struct {
|
||||||
|
RemoteURL string
|
||||||
|
FetchInterval time.Duration
|
||||||
|
RequestTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyListConfig = KeyListConfig{
|
||||||
|
RemoteURL: "",
|
||||||
|
FetchInterval: 24 * time.Hour,
|
||||||
|
RequestTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeyListConfig 设置远程公钥列表配置
|
||||||
|
func SetKeyListConfig(cfg KeyListConfig) {
|
||||||
|
keyMu.Lock()
|
||||||
|
defer keyMu.Unlock()
|
||||||
|
keyListConfig = cfg
|
||||||
|
}
|
||||||
|
|
||||||
// initKeyCache 初始化密钥缓存
|
// initKeyCache 初始化密钥缓存
|
||||||
func initKeyCache() {
|
func initKeyCache() {
|
||||||
keyCache = make(map[string]ed25519.PublicKey)
|
keyCache = make(map[string]ed25519.PublicKey)
|
||||||
@@ -49,7 +75,10 @@ func GetOfficialPublicKey() (ed25519.PublicKey, error) {
|
|||||||
func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
|
func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
|
||||||
keyCacheOnce.Do(initKeyCache)
|
keyCacheOnce.Do(initKeyCache)
|
||||||
|
|
||||||
|
keyMu.RLock()
|
||||||
pub, ok := keyCache[keyID]
|
pub, ok := keyCache[keyID]
|
||||||
|
keyMu.RUnlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unknown key ID: %s", keyID)
|
return nil, fmt.Errorf("unknown key ID: %s", keyID)
|
||||||
}
|
}
|
||||||
@@ -58,11 +87,20 @@ func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
|
|||||||
|
|
||||||
// IsKeyRevoked 检查密钥是否已吊销
|
// IsKeyRevoked 检查密钥是否已吊销
|
||||||
func IsKeyRevoked(keyID string) bool {
|
func IsKeyRevoked(keyID string) bool {
|
||||||
|
// 先检查内置密钥
|
||||||
for _, entry := range officialKeys {
|
for _, entry := range officialKeys {
|
||||||
if entry.ID == keyID {
|
if entry.ID == keyID {
|
||||||
return !entry.RevokedAt.IsZero()
|
return !entry.RevokedAt.IsZero()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 再检查远程密钥
|
||||||
|
keyMu.RLock()
|
||||||
|
defer keyMu.RUnlock()
|
||||||
|
for _, entry := range remoteKeys {
|
||||||
|
if entry.ID == keyID {
|
||||||
|
return !entry.RevokedAt.IsZero()
|
||||||
|
}
|
||||||
|
}
|
||||||
return true // 未知密钥视为已吊销
|
return true // 未知密钥视为已吊销
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,5 +111,108 @@ func GetKeyEntry(keyID string) *KeyEntry {
|
|||||||
return &officialKeys[i]
|
return &officialKeys[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
keyMu.RLock()
|
||||||
|
defer keyMu.RUnlock()
|
||||||
|
for i := range remoteKeys {
|
||||||
|
if remoteKeys[i].ID == keyID {
|
||||||
|
return &remoteKeys[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyList 远程公钥列表结构
|
||||||
|
type KeyList struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
Keys []KeyEntry `json:"keys"`
|
||||||
|
Signature string `json:"signature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchRemoteKeyList 从远程拉取公钥列表
|
||||||
|
func FetchRemoteKeyList() error {
|
||||||
|
keyMu.RLock()
|
||||||
|
cfg := keyListConfig
|
||||||
|
keyMu.RUnlock()
|
||||||
|
|
||||||
|
if cfg.RemoteURL == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: cfg.RequestTimeout}
|
||||||
|
resp, err := client.Get(cfg.RemoteURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetch key list: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("fetch key list: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read key list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var list KeyList
|
||||||
|
if err := json.Unmarshal(body, &list); err != nil {
|
||||||
|
return fmt.Errorf("parse key list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名(使用内置公钥验证)
|
||||||
|
if err := verifyKeyListSignature(&list); err != nil {
|
||||||
|
return fmt.Errorf("verify key list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateKeyCache(&list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyKeyListSignature 验证公钥列表签名
|
||||||
|
func verifyKeyListSignature(list *KeyList) error {
|
||||||
|
if list.Signature == "" {
|
||||||
|
return fmt.Errorf("missing signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用内置公钥验证(必须用内置密钥签名)
|
||||||
|
pubKey, err := GetOfficialPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
signData := struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
Keys []KeyEntry `json:"keys"`
|
||||||
|
}{
|
||||||
|
Version: list.Version,
|
||||||
|
UpdatedAt: list.UpdatedAt,
|
||||||
|
Keys: list.Keys,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(signData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return VerifyBase64(pubKey, data, list.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateKeyCache 更新公钥缓存
|
||||||
|
func updateKeyCache(list *KeyList) error {
|
||||||
|
keyMu.Lock()
|
||||||
|
defer keyMu.Unlock()
|
||||||
|
|
||||||
|
// 保存远程密钥列表
|
||||||
|
remoteKeys = list.Keys
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
for _, entry := range list.Keys {
|
||||||
|
if pub, err := DecodePublicKey(entry.PublicKey); err == nil {
|
||||||
|
keyCache[entry.ID] = pub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[KeyList] Updated with %d keys", len(list.Keys))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
package sign
|
package sign
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RevocationEntry 撤销条目
|
// RevocationEntry 撤销条目
|
||||||
@@ -20,7 +26,7 @@ type RevocationList struct {
|
|||||||
Signature string `json:"signature"` // 列表签名
|
Signature string `json:"signature"` // 列表签名
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内置撤销列表(编译时确定)
|
// 内置撤销列表(编译时确定,作为 fallback)
|
||||||
var builtinRevocations = []RevocationEntry{
|
var builtinRevocations = []RevocationEntry{
|
||||||
// 示例:{PluginName: "malicious-plugin", Reason: "security vulnerability"}
|
// 示例:{PluginName: "malicious-plugin", Reason: "security vulnerability"}
|
||||||
}
|
}
|
||||||
@@ -28,8 +34,42 @@ var builtinRevocations = []RevocationEntry{
|
|||||||
var (
|
var (
|
||||||
revocationCache map[string][]RevocationEntry // pluginName -> entries
|
revocationCache map[string][]RevocationEntry // pluginName -> entries
|
||||||
revocationCacheOnce sync.Once
|
revocationCacheOnce sync.Once
|
||||||
|
revocationMu sync.RWMutex
|
||||||
|
currentListVersion int
|
||||||
|
lastFetchTime time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RevocationConfig 远程撤销列表配置
|
||||||
|
type RevocationConfig struct {
|
||||||
|
RemoteURL string // 远程撤销列表 URL
|
||||||
|
FetchInterval time.Duration // 拉取间隔
|
||||||
|
RequestTimeout time.Duration // 请求超时
|
||||||
|
VerifySignature bool // 是否验证签名
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultRevocationConfig = RevocationConfig{
|
||||||
|
RemoteURL: "", // 默认为空,不启用远程拉取
|
||||||
|
FetchInterval: 1 * time.Hour,
|
||||||
|
RequestTimeout: 10 * time.Second,
|
||||||
|
VerifySignature: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var revocationConfig = defaultRevocationConfig
|
||||||
|
|
||||||
|
// SetRevocationConfig 设置远程撤销列表配置
|
||||||
|
func SetRevocationConfig(cfg RevocationConfig) {
|
||||||
|
revocationMu.Lock()
|
||||||
|
defer revocationMu.Unlock()
|
||||||
|
revocationConfig = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRevocationConfig 获取当前配置
|
||||||
|
func GetRevocationConfig() RevocationConfig {
|
||||||
|
revocationMu.RLock()
|
||||||
|
defer revocationMu.RUnlock()
|
||||||
|
return revocationConfig
|
||||||
|
}
|
||||||
|
|
||||||
// initRevocationCache 初始化撤销缓存
|
// initRevocationCache 初始化撤销缓存
|
||||||
func initRevocationCache() {
|
func initRevocationCache() {
|
||||||
revocationCache = make(map[string][]RevocationEntry)
|
revocationCache = make(map[string][]RevocationEntry)
|
||||||
@@ -43,6 +83,9 @@ func initRevocationCache() {
|
|||||||
func IsPluginRevoked(name, version string) (bool, string) {
|
func IsPluginRevoked(name, version string) (bool, string) {
|
||||||
revocationCacheOnce.Do(initRevocationCache)
|
revocationCacheOnce.Do(initRevocationCache)
|
||||||
|
|
||||||
|
revocationMu.RLock()
|
||||||
|
defer revocationMu.RUnlock()
|
||||||
|
|
||||||
entries, ok := revocationCache[name]
|
entries, ok := revocationCache[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return false, ""
|
return false, ""
|
||||||
@@ -56,3 +99,144 @@ func IsPluginRevoked(name, version string) (bool, string) {
|
|||||||
}
|
}
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchRemoteRevocationList 从远程拉取撤销列表
|
||||||
|
func FetchRemoteRevocationList() error {
|
||||||
|
cfg := GetRevocationConfig()
|
||||||
|
if cfg.RemoteURL == "" {
|
||||||
|
return nil // 未配置远程 URL,跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否需要刷新
|
||||||
|
revocationMu.RLock()
|
||||||
|
if time.Since(lastFetchTime) < cfg.FetchInterval {
|
||||||
|
revocationMu.RUnlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
revocationMu.RUnlock()
|
||||||
|
|
||||||
|
// 发起 HTTP 请求
|
||||||
|
client := &http.Client{Timeout: cfg.RequestTimeout}
|
||||||
|
resp, err := client.Get(cfg.RemoteURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetch revocation list: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("fetch revocation list: status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read revocation list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var list RevocationList
|
||||||
|
if err := json.Unmarshal(body, &list); err != nil {
|
||||||
|
return fmt.Errorf("parse revocation list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证签名
|
||||||
|
if cfg.VerifySignature {
|
||||||
|
if err := verifyRevocationListSignature(&list); err != nil {
|
||||||
|
return fmt.Errorf("verify revocation list: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
return updateRevocationCache(&list)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyRevocationListSignature 验证撤销列表签名
|
||||||
|
func verifyRevocationListSignature(list *RevocationList) error {
|
||||||
|
if list.Signature == "" {
|
||||||
|
return fmt.Errorf("missing signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取官方公钥
|
||||||
|
pubKey, err := GetOfficialPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造待签名数据(不含签名字段)
|
||||||
|
signData := struct {
|
||||||
|
Version int `json:"version"`
|
||||||
|
UpdatedAt int64 `json:"updated_at"`
|
||||||
|
Entries []RevocationEntry `json:"entries"`
|
||||||
|
}{
|
||||||
|
Version: list.Version,
|
||||||
|
UpdatedAt: list.UpdatedAt,
|
||||||
|
Entries: list.Entries,
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(signData)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal sign data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return VerifyBase64(pubKey, data, list.Signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateRevocationCache 更新撤销缓存
|
||||||
|
func updateRevocationCache(list *RevocationList) error {
|
||||||
|
revocationMu.Lock()
|
||||||
|
defer revocationMu.Unlock()
|
||||||
|
|
||||||
|
// 检查版本号,防止回滚攻击
|
||||||
|
if list.Version < currentListVersion {
|
||||||
|
return fmt.Errorf("revocation list version rollback: %d < %d", list.Version, currentListVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重建缓存:先加载内置列表,再合并远程列表
|
||||||
|
newCache := make(map[string][]RevocationEntry)
|
||||||
|
for _, entry := range builtinRevocations {
|
||||||
|
newCache[entry.PluginName] = append(newCache[entry.PluginName], entry)
|
||||||
|
}
|
||||||
|
for _, entry := range list.Entries {
|
||||||
|
newCache[entry.PluginName] = append(newCache[entry.PluginName], entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
revocationCache = newCache
|
||||||
|
currentListVersion = list.Version
|
||||||
|
lastFetchTime = time.Now()
|
||||||
|
|
||||||
|
log.Printf("[Revocation] Updated to version %d with %d entries", list.Version, len(list.Entries))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartRevocationRefresher 启动后台刷新协程
|
||||||
|
func StartRevocationRefresher(stopCh <-chan struct{}) {
|
||||||
|
cfg := GetRevocationConfig()
|
||||||
|
if cfg.RemoteURL == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即执行一次
|
||||||
|
if err := FetchRemoteRevocationList(); err != nil {
|
||||||
|
log.Printf("[Revocation] Initial fetch failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := time.NewTicker(cfg.FetchInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := FetchRemoteRevocationList(); err != nil {
|
||||||
|
log.Printf("[Revocation] Refresh failed: %v", err)
|
||||||
|
}
|
||||||
|
case <-stopCh:
|
||||||
|
log.Printf("[Revocation] Refresher stopped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRevocationListVersion 获取当前撤销列表版本
|
||||||
|
func GetRevocationListVersion() int {
|
||||||
|
revocationMu.RLock()
|
||||||
|
defer revocationMu.RUnlock()
|
||||||
|
return currentListVersion
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ export const enablePlugin = (name: string) => post(`/plugin/${name}/enable`)
|
|||||||
export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
|
export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
|
||||||
|
|
||||||
// 扩展商店
|
// 扩展商店
|
||||||
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[], store_url: string }>('/store/plugins')
|
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[] }>('/store/plugins')
|
||||||
export const installStorePlugin = (pluginName: string, downloadUrl: string, clientId: string) =>
|
export const installStorePlugin = (pluginName: string, downloadUrl: string, signatureUrl: string, clientId: string) =>
|
||||||
post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, client_id: clientId })
|
post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, signature_url: signatureUrl, client_id: clientId })
|
||||||
|
|
||||||
// 客户端插件配置
|
// 客户端插件配置
|
||||||
export const getClientPluginConfig = (clientId: string, pluginName: string) =>
|
export const getClientPluginConfig = (clientId: string, pluginName: string) =>
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ export interface StorePluginInfo {
|
|||||||
author: string
|
author: string
|
||||||
icon?: string
|
icon?: string
|
||||||
download_url?: string
|
download_url?: string
|
||||||
|
signature_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// JS 插件信息
|
// JS 插件信息
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const plugins = ref<PluginInfo[]>([])
|
|||||||
const storePlugins = ref<StorePluginInfo[]>([])
|
const storePlugins = ref<StorePluginInfo[]>([])
|
||||||
const jsPlugins = ref<JSPlugin[]>([])
|
const jsPlugins = ref<JSPlugin[]>([])
|
||||||
const clients = ref<ClientStatus[]>([])
|
const clients = ref<ClientStatus[]>([])
|
||||||
const storeUrl = ref('')
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const storeLoading = ref(false)
|
const storeLoading = ref(false)
|
||||||
const jsLoading = ref(false)
|
const jsLoading = ref(false)
|
||||||
@@ -41,7 +40,6 @@ const loadStorePlugins = async () => {
|
|||||||
try {
|
try {
|
||||||
const { data } = await getStorePlugins()
|
const { data } = await getStorePlugins()
|
||||||
storePlugins.value = data.plugins || []
|
storePlugins.value = data.plugins || []
|
||||||
storeUrl.value = data.store_url || ''
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load store plugins', e)
|
console.error('Failed to load store plugins', e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -165,11 +163,16 @@ const handleInstallStorePlugin = async () => {
|
|||||||
message.error('该插件没有下载地址')
|
message.error('该插件没有下载地址')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!selectedStorePlugin.value.signature_url) {
|
||||||
|
message.error('该插件没有签名文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
installing.value = true
|
installing.value = true
|
||||||
try {
|
try {
|
||||||
await installStorePlugin(
|
await installStorePlugin(
|
||||||
selectedStorePlugin.value.name,
|
selectedStorePlugin.value.name,
|
||||||
selectedStorePlugin.value.download_url,
|
selectedStorePlugin.value.download_url,
|
||||||
|
selectedStorePlugin.value.signature_url,
|
||||||
selectedClientId.value
|
selectedClientId.value
|
||||||
)
|
)
|
||||||
message.success(`已安装 ${selectedStorePlugin.value.name} 到客户端`)
|
message.success(`已安装 ${selectedStorePlugin.value.name} 到客户端`)
|
||||||
@@ -258,8 +261,7 @@ onMounted(() => {
|
|||||||
<!-- 扩展商店 -->
|
<!-- 扩展商店 -->
|
||||||
<n-tab-pane name="store" tab="扩展商店">
|
<n-tab-pane name="store" tab="扩展商店">
|
||||||
<n-spin :show="storeLoading">
|
<n-spin :show="storeLoading">
|
||||||
<n-empty v-if="!storeUrl" description="未配置扩展商店URL,请在配置文件中设置 plugin_store.url" />
|
<n-empty v-if="!storeLoading && storePlugins.length === 0" description="扩展商店暂无可用扩展" />
|
||||||
<n-empty v-else-if="!storeLoading && storePlugins.length === 0" description="扩展商店暂无可用扩展" />
|
|
||||||
|
|
||||||
<n-grid v-else :cols="3" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1" cols-m="2">
|
<n-grid v-else :cols="3" :x-gap="16" :y-gap="16" responsive="screen" cols-s="1" cols-m="2">
|
||||||
<n-gi v-for="plugin in storePlugins" :key="plugin.name">
|
<n-gi v-for="plugin in storePlugins" :key="plugin.name">
|
||||||
@@ -273,7 +275,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="plugin.download_url && onlineClients.length > 0"
|
v-if="plugin.download_url && plugin.signature_url && onlineClients.length > 0"
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="openInstallModal(plugin)"
|
@click="openInstallModal(plugin)"
|
||||||
|
|||||||
Reference in New Issue
Block a user