Merge branch 'dev'
Some checks failed
Build Multi-Platform Binaries / build-frontend (push) Failing after 1m4s
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 1m4s
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
This commit is contained in:
@@ -164,16 +164,6 @@ func verifyPluginSignature(name, source, signature string) error {
|
||||
return fmt.Errorf("decode signature: %w", err)
|
||||
}
|
||||
|
||||
// 检查插件是否被撤销
|
||||
if revoked, reason := sign.IsPluginRevoked(name, signed.Payload.Version); revoked {
|
||||
return fmt.Errorf("plugin revoked: %s", reason)
|
||||
}
|
||||
|
||||
// 检查密钥是否已吊销
|
||||
if sign.IsKeyRevoked(signed.Payload.KeyID) {
|
||||
return fmt.Errorf("signing key revoked: %s", signed.Payload.KeyID)
|
||||
}
|
||||
|
||||
// 获取公钥
|
||||
pubKey, err := sign.GetPublicKeyByID(signed.Payload.KeyID)
|
||||
if err != nil {
|
||||
|
||||
@@ -578,17 +578,6 @@ func (c *Client) verifyJSPluginSignature(pluginName, source, signature string) e
|
||||
return fmt.Errorf("decode signature: %w", err)
|
||||
}
|
||||
|
||||
// 检查插件是否被撤销
|
||||
if revoked, reason := sign.IsPluginRevoked(pluginName, signed.Payload.Version); revoked {
|
||||
return fmt.Errorf("plugin %s v%s has been revoked: %s",
|
||||
pluginName, signed.Payload.Version, reason)
|
||||
}
|
||||
|
||||
// 检查密钥是否已吊销
|
||||
if sign.IsKeyRevoked(signed.Payload.KeyID) {
|
||||
return fmt.Errorf("signing key %s has been revoked", signed.Payload.KeyID)
|
||||
}
|
||||
|
||||
// 根据 KeyID 获取对应公钥
|
||||
pubKey, err := sign.GetPublicKeyByID(signed.Payload.KeyID)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// versionStoreData 版本存储数据结构(带 HMAC)
|
||||
type versionStoreData struct {
|
||||
Versions map[string]string `json:"versions"`
|
||||
HMAC string `json:"hmac"`
|
||||
}
|
||||
|
||||
// PluginVersionStore 插件版本存储
|
||||
type PluginVersionStore struct {
|
||||
path string
|
||||
hmacKey []byte
|
||||
versions map[string]string // pluginName -> version
|
||||
mu sync.RWMutex
|
||||
}
|
||||
@@ -18,6 +29,7 @@ type PluginVersionStore struct {
|
||||
func NewPluginVersionStore(dataDir string) (*PluginVersionStore, error) {
|
||||
store := &PluginVersionStore{
|
||||
path: filepath.Join(dataDir, "plugin_versions.json"),
|
||||
hmacKey: deriveHMACKey(dataDir),
|
||||
versions: make(map[string]string),
|
||||
}
|
||||
if err := store.load(); err != nil {
|
||||
@@ -26,6 +38,15 @@ func NewPluginVersionStore(dataDir string) (*PluginVersionStore, error) {
|
||||
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 从文件加载版本信息
|
||||
func (s *PluginVersionStore) load() error {
|
||||
data, err := os.ReadFile(s.path)
|
||||
@@ -35,7 +56,26 @@ func (s *PluginVersionStore) load() error {
|
||||
if err != nil {
|
||||
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 保存版本信息到文件
|
||||
@@ -44,7 +84,16 @@ func (s *PluginVersionStore) save() error {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -65,3 +114,17 @@ func (s *PluginVersionStore) SetVersion(name, version string) error {
|
||||
s.versions[name] = version
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -26,13 +26,21 @@ type JSPluginConfig struct {
|
||||
AutoStart bool `yaml:"auto_start,omitempty"` // 是否自动启动
|
||||
}
|
||||
|
||||
// PluginStoreSettings 扩展商店设置
|
||||
// PluginStoreSettings 插件仓库设置
|
||||
type PluginStoreSettings struct {
|
||||
// 保留结构体以便未来扩展,但不暴露 URL 配置
|
||||
URL string `yaml:"url"` // 插件仓库 URL,为空则使用默认值
|
||||
}
|
||||
|
||||
// 官方插件商店(不可配置)
|
||||
const OfficialPluginStoreURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/store.json"
|
||||
// 默认插件仓库 URL
|
||||
const DefaultPluginStoreURL = "https://git.92coco.cn:8443/flik/GoTunnel-Plugins/raw/branch/main/store.json"
|
||||
|
||||
// GetPluginStoreURL 获取插件仓库 URL
|
||||
func (s *PluginStoreSettings) GetPluginStoreURL() string {
|
||||
if s.URL != "" {
|
||||
return s.URL
|
||||
}
|
||||
return DefaultPluginStoreURL
|
||||
}
|
||||
|
||||
// ServerSettings 服务端设置
|
||||
type ServerSettings struct {
|
||||
|
||||
@@ -574,20 +574,22 @@ func (h *APIHandler) installPluginsToClient(rw http.ResponseWriter, r *http.Requ
|
||||
|
||||
// StorePluginInfo 扩展商店插件信息
|
||||
type StorePluginInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Author string `json:"author"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
SignatureURL string `json:"signature_url,omitempty"`
|
||||
}
|
||||
|
||||
// StorePluginInstallRequest 从商店安装插件的请求
|
||||
type StorePluginInstallRequest struct {
|
||||
PluginName string `json:"plugin_name"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
PluginName string `json:"plugin_name"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
SignatureURL string `json:"signature_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
}
|
||||
|
||||
// handleStorePlugins 处理扩展商店插件列表
|
||||
@@ -598,8 +600,7 @@ func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
cfg := h.app.GetConfig()
|
||||
storeURL := config.OfficialPluginStoreURL
|
||||
_ = cfg // 保留以便未来扩展
|
||||
storeURL := cfg.PluginStore.GetPluginStoreURL()
|
||||
|
||||
// 从远程URL获取插件列表
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
@@ -628,8 +629,7 @@ func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
h.jsonResponse(rw, map[string]interface{}{
|
||||
"plugins": plugins,
|
||||
"store_url": storeURL,
|
||||
"plugins": plugins,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -646,8 +646,8 @@ func (h *APIHandler) handleStoreInstall(rw http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
if req.PluginName == "" || req.DownloadURL == "" || req.ClientID == "" {
|
||||
http.Error(rw, "plugin_name, download_url and client_id required", http.StatusBadRequest)
|
||||
if req.PluginName == "" || req.DownloadURL == "" || req.ClientID == "" || req.SignatureURL == "" {
|
||||
http.Error(rw, "plugin_name, download_url, signature_url and client_id required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -678,10 +678,30 @@ func (h *APIHandler) handleStoreInstall(rw http.ResponseWriter, r *http.Request)
|
||||
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{
|
||||
PluginName: req.PluginName,
|
||||
Source: string(source),
|
||||
Signature: string(signature),
|
||||
RuleName: req.PluginName,
|
||||
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 运行时
|
||||
func (p *JSPlugin) init() error {
|
||||
// 设置栈深度限制(防止递归攻击)
|
||||
if p.sandbox.MaxStackDepth > 0 {
|
||||
p.vm.SetMaxCallStackSize(p.sandbox.MaxStackDepth)
|
||||
}
|
||||
|
||||
// 注入基础 API
|
||||
p.vm.Set("log", p.jsLog)
|
||||
p.vm.Set("config", p.jsGetConfig)
|
||||
|
||||
@@ -21,6 +21,10 @@ type Sandbox struct {
|
||||
MaxReadSize int64
|
||||
// 最大文件写入大小 (bytes)
|
||||
MaxWriteSize int64
|
||||
// 最大内存使用量 (bytes),0 表示不限制
|
||||
MaxMemory int64
|
||||
// 最大调用栈深度
|
||||
MaxStackDepth int
|
||||
}
|
||||
|
||||
// DefaultSandbox 返回默认沙箱配置(最小权限)
|
||||
@@ -30,8 +34,10 @@ func DefaultSandbox() *Sandbox {
|
||||
WritablePaths: []string{},
|
||||
DeniedPaths: defaultDeniedPaths(),
|
||||
AllowNetwork: false,
|
||||
MaxReadSize: 10 * 1024 * 1024, // 10MB
|
||||
MaxWriteSize: 1 * 1024 * 1024, // 1MB
|
||||
MaxReadSize: 10 * 1024 * 1024, // 10MB
|
||||
MaxWriteSize: 1 * 1024 * 1024, // 1MB
|
||||
MaxMemory: 64 * 1024 * 1024, // 64MB
|
||||
MaxStackDepth: 1000, // 最大调用栈深度
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,76 +2,30 @@ package sign
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// KeyEntry 密钥条目
|
||||
type KeyEntry struct {
|
||||
ID string // 密钥 ID
|
||||
PublicKey string // Base64 编码的公钥
|
||||
ValidFrom time.Time // 生效时间
|
||||
RevokedAt time.Time // 吊销时间(零值表示未吊销)
|
||||
}
|
||||
|
||||
// 官方公钥列表(支持密钥轮换)
|
||||
var officialKeys = []KeyEntry{
|
||||
{
|
||||
ID: "official-v1",
|
||||
PublicKey: "0A0xRthj0wgPg8X8GJZ6/EnNpAUw5v7O//XLty+P5Yw=",
|
||||
ValidFrom: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
// 添加新密钥时,在此处追加
|
||||
}
|
||||
// 官方固定公钥(客户端内置)
|
||||
const OfficialPublicKeyBase64 = "0A0xRthj0wgPg8X8GJZ6/EnNpAUw5v7O//XLty+P5Yw="
|
||||
|
||||
var (
|
||||
keyCache map[string]ed25519.PublicKey
|
||||
keyCacheOnce sync.Once
|
||||
officialPubKey ed25519.PublicKey
|
||||
officialPubKeyOnce sync.Once
|
||||
officialPubKeyErr error
|
||||
)
|
||||
|
||||
// initKeyCache 初始化密钥缓存
|
||||
func initKeyCache() {
|
||||
keyCache = make(map[string]ed25519.PublicKey)
|
||||
for _, entry := range officialKeys {
|
||||
if pub, err := DecodePublicKey(entry.PublicKey); err == nil {
|
||||
keyCache[entry.ID] = pub
|
||||
}
|
||||
}
|
||||
// initOfficialKey 初始化官方公钥
|
||||
func initOfficialKey() {
|
||||
officialPubKey, officialPubKeyErr = DecodePublicKey(OfficialPublicKeyBase64)
|
||||
}
|
||||
|
||||
// GetOfficialPublicKey 获取默认官方公钥(兼容旧接口)
|
||||
// GetOfficialPublicKey 获取官方公钥
|
||||
func GetOfficialPublicKey() (ed25519.PublicKey, error) {
|
||||
return GetPublicKeyByID("official-v1")
|
||||
officialPubKeyOnce.Do(initOfficialKey)
|
||||
return officialPubKey, officialPubKeyErr
|
||||
}
|
||||
|
||||
// GetPublicKeyByID 根据 ID 获取公钥
|
||||
// GetPublicKeyByID 根据 ID 获取公钥(兼容旧接口,忽略 keyID)
|
||||
func GetPublicKeyByID(keyID string) (ed25519.PublicKey, error) {
|
||||
keyCacheOnce.Do(initKeyCache)
|
||||
|
||||
pub, ok := keyCache[keyID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown key ID: %s", keyID)
|
||||
}
|
||||
return pub, nil
|
||||
}
|
||||
|
||||
// IsKeyRevoked 检查密钥是否已吊销
|
||||
func IsKeyRevoked(keyID string) bool {
|
||||
for _, entry := range officialKeys {
|
||||
if entry.ID == keyID {
|
||||
return !entry.RevokedAt.IsZero()
|
||||
}
|
||||
}
|
||||
return true // 未知密钥视为已吊销
|
||||
}
|
||||
|
||||
// GetKeyEntry 获取密钥条目
|
||||
func GetKeyEntry(keyID string) *KeyEntry {
|
||||
for i := range officialKeys {
|
||||
if officialKeys[i].ID == keyID {
|
||||
return &officialKeys[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return GetOfficialPublicKey()
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
package sign
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// RevocationEntry 撤销条目
|
||||
type RevocationEntry struct {
|
||||
PluginName string `json:"plugin_name"` // 插件名称
|
||||
Version string `json:"version,omitempty"` // 特定版本(空表示所有版本)
|
||||
Reason string `json:"reason"` // 撤销原因
|
||||
RevokedAt int64 `json:"revoked_at"` // 撤销时间戳
|
||||
}
|
||||
|
||||
// RevocationList 撤销列表
|
||||
type RevocationList struct {
|
||||
Version int `json:"version"` // 列表版本
|
||||
UpdatedAt int64 `json:"updated_at"` // 更新时间
|
||||
Entries []RevocationEntry `json:"entries"` // 撤销条目
|
||||
Signature string `json:"signature"` // 列表签名
|
||||
}
|
||||
|
||||
// 内置撤销列表(编译时确定)
|
||||
var builtinRevocations = []RevocationEntry{
|
||||
// 示例:{PluginName: "malicious-plugin", Reason: "security vulnerability"}
|
||||
}
|
||||
|
||||
var (
|
||||
revocationCache map[string][]RevocationEntry // pluginName -> entries
|
||||
revocationCacheOnce sync.Once
|
||||
)
|
||||
|
||||
// initRevocationCache 初始化撤销缓存
|
||||
func initRevocationCache() {
|
||||
revocationCache = make(map[string][]RevocationEntry)
|
||||
for _, entry := range builtinRevocations {
|
||||
revocationCache[entry.PluginName] = append(
|
||||
revocationCache[entry.PluginName], entry)
|
||||
}
|
||||
}
|
||||
|
||||
// IsPluginRevoked 检查插件是否被撤销
|
||||
func IsPluginRevoked(name, version string) (bool, string) {
|
||||
revocationCacheOnce.Do(initRevocationCache)
|
||||
|
||||
entries, ok := revocationCache[name]
|
||||
if !ok {
|
||||
return false, ""
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
// 空版本表示所有版本都被撤销
|
||||
if entry.Version == "" || entry.Version == version {
|
||||
return true, entry.Reason
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
@@ -32,9 +32,9 @@ export const enablePlugin = (name: string) => post(`/plugin/${name}/enable`)
|
||||
export const disablePlugin = (name: string) => post(`/plugin/${name}/disable`)
|
||||
|
||||
// 扩展商店
|
||||
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[], store_url: string }>('/store/plugins')
|
||||
export const installStorePlugin = (pluginName: string, downloadUrl: string, clientId: string) =>
|
||||
post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, client_id: clientId })
|
||||
export const getStorePlugins = () => get<{ plugins: StorePluginInfo[] }>('/store/plugins')
|
||||
export const installStorePlugin = (pluginName: string, downloadUrl: string, signatureUrl: string, clientId: string) =>
|
||||
post('/store/install', { plugin_name: pluginName, download_url: downloadUrl, signature_url: signatureUrl, client_id: clientId })
|
||||
|
||||
// 客户端插件配置
|
||||
export const getClientPluginConfig = (clientId: string, pluginName: string) =>
|
||||
|
||||
@@ -110,6 +110,7 @@ export interface StorePluginInfo {
|
||||
author: string
|
||||
icon?: string
|
||||
download_url?: string
|
||||
signature_url?: string
|
||||
}
|
||||
|
||||
// JS 插件信息
|
||||
|
||||
@@ -19,7 +19,6 @@ const plugins = ref<PluginInfo[]>([])
|
||||
const storePlugins = ref<StorePluginInfo[]>([])
|
||||
const jsPlugins = ref<JSPlugin[]>([])
|
||||
const clients = ref<ClientStatus[]>([])
|
||||
const storeUrl = ref('')
|
||||
const loading = ref(true)
|
||||
const storeLoading = ref(false)
|
||||
const jsLoading = ref(false)
|
||||
@@ -41,7 +40,6 @@ const loadStorePlugins = async () => {
|
||||
try {
|
||||
const { data } = await getStorePlugins()
|
||||
storePlugins.value = data.plugins || []
|
||||
storeUrl.value = data.store_url || ''
|
||||
} catch (e) {
|
||||
console.error('Failed to load store plugins', e)
|
||||
} finally {
|
||||
@@ -165,11 +163,16 @@ const handleInstallStorePlugin = async () => {
|
||||
message.error('该插件没有下载地址')
|
||||
return
|
||||
}
|
||||
if (!selectedStorePlugin.value.signature_url) {
|
||||
message.error('该插件没有签名文件')
|
||||
return
|
||||
}
|
||||
installing.value = true
|
||||
try {
|
||||
await installStorePlugin(
|
||||
selectedStorePlugin.value.name,
|
||||
selectedStorePlugin.value.download_url,
|
||||
selectedStorePlugin.value.signature_url,
|
||||
selectedClientId.value
|
||||
)
|
||||
message.success(`已安装 ${selectedStorePlugin.value.name} 到客户端`)
|
||||
@@ -258,8 +261,7 @@ onMounted(() => {
|
||||
<!-- 扩展商店 -->
|
||||
<n-tab-pane name="store" tab="扩展商店">
|
||||
<n-spin :show="storeLoading">
|
||||
<n-empty v-if="!storeUrl" description="未配置扩展商店URL,请在配置文件中设置 plugin_store.url" />
|
||||
<n-empty v-else-if="!storeLoading && storePlugins.length === 0" description="扩展商店暂无可用扩展" />
|
||||
<n-empty v-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-gi v-for="plugin in storePlugins" :key="plugin.name">
|
||||
@@ -273,7 +275,7 @@ onMounted(() => {
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
v-if="plugin.download_url && onlineClients.length > 0"
|
||||
v-if="plugin.download_url && plugin.signature_url && onlineClients.length > 0"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="openInstallModal(plugin)"
|
||||
|
||||
Reference in New Issue
Block a user