139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
// Package custom 实现自定义协议插件:用户在平台注册命令钩子,
|
||
// 平台按统一 Protocol 契约调用(Create/Start/Stop/Status/Config)。
|
||
// 钩子通过 stdin 接收专线 Config JSON,约定 stdout 返回结果。
|
||
package custom
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os/exec"
|
||
"strings"
|
||
"time"
|
||
|
||
"vpn-platform/backend/internal/core"
|
||
"vpn-platform/backend/internal/model"
|
||
"vpn-platform/backend/internal/store"
|
||
)
|
||
|
||
// Service 以数据库中的 Plugin 定义为驱动的协议实现
|
||
type Service struct {
|
||
plugin *model.Plugin
|
||
}
|
||
|
||
func New(p *model.Plugin) *Service { return &Service{plugin: p} }
|
||
|
||
func (s *Service) Name() string { return "custom:" + s.plugin.Name }
|
||
func (s *Service) DisplayName() string { return s.plugin.DisplayName }
|
||
|
||
func (s *Service) Available() (bool, string) {
|
||
if s.plugin.StartHook == "" {
|
||
return false, "插件未配置 start 钩子"
|
||
}
|
||
return true, "插件钩子: " + s.plugin.StartHook
|
||
}
|
||
|
||
// runHook 执行钩子命令,stdin 传入专线上下文 JSON
|
||
func (s *Service) runHook(hook string, t *model.Tunnel) (string, error) {
|
||
if hook == "" {
|
||
return "", fmt.Errorf("插件 [%s] 未配置该动作的钩子命令", s.plugin.Name)
|
||
}
|
||
payload, _ := json.Marshal(map[string]interface{}{
|
||
"id": t.ID,
|
||
"name": t.Name,
|
||
"protocol": t.Protocol,
|
||
"config": json.RawMessage(t.Config),
|
||
})
|
||
ctx := exec.Command("sh", "-c", hook)
|
||
done := make(chan error, 1)
|
||
ctx.Stdin = bytes.NewReader(payload)
|
||
var out, errOut bytes.Buffer
|
||
ctx.Stdout = &out
|
||
ctx.Stderr = &errOut
|
||
if err := ctx.Start(); err != nil {
|
||
return "", err
|
||
}
|
||
go func() { done <- ctx.Wait() }()
|
||
select {
|
||
case err := <-done:
|
||
if err != nil {
|
||
return out.String(), fmt.Errorf("钩子执行失败: %v %s", err, errOut.String())
|
||
}
|
||
return strings.TrimSpace(out.String()), nil
|
||
case <-time.After(30 * time.Second):
|
||
_ = ctx.Process.Kill()
|
||
return "", fmt.Errorf("钩子执行超时(30s)")
|
||
}
|
||
}
|
||
|
||
func (s *Service) Validate(cfg map[string]interface{}) (map[string]interface{}, error) {
|
||
out := map[string]interface{}{}
|
||
for k, v := range cfg {
|
||
out[k] = v
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (s *Service) Create(t *model.Tunnel) error {
|
||
if s.plugin.CreateHook == "" {
|
||
return nil // 可选
|
||
}
|
||
_, err := s.runHook(s.plugin.CreateHook, t)
|
||
return err
|
||
}
|
||
|
||
func (s *Service) Start(t *model.Tunnel) error {
|
||
_, err := s.runHook(s.plugin.StartHook, t)
|
||
return err
|
||
}
|
||
|
||
func (s *Service) Stop(t *model.Tunnel) error {
|
||
if s.plugin.StopHook == "" {
|
||
return nil
|
||
}
|
||
_, err := s.runHook(s.plugin.StopHook, t)
|
||
return err
|
||
}
|
||
|
||
func (s *Service) Status(t *model.Tunnel) (core.Status, error) {
|
||
if s.plugin.StatusHook == "" {
|
||
return core.Status{State: "unknown", Detail: "插件未配置 status 钩子", UpdatedAt: timeNow()}, nil
|
||
}
|
||
out, err := s.runHook(s.plugin.StatusHook, t)
|
||
if err != nil {
|
||
return core.Status{Running: false, State: "error", Detail: err.Error(), UpdatedAt: timeNow()}, nil
|
||
}
|
||
// 约定:status 钩子 stdout 输出 running / stopped / 任意描述
|
||
running := strings.EqualFold(out, "running") || strings.EqualFold(out, "up") || out == "0"
|
||
state := "stopped"
|
||
if running {
|
||
state = "running"
|
||
}
|
||
return core.Status{Running: running, State: state, Detail: out, UpdatedAt: timeNow()}, nil
|
||
}
|
||
|
||
func timeNow() int64 { return time.Now().Unix() }
|
||
|
||
func (s *Service) GenerateClientConfig(t *model.Tunnel) (string, error) {
|
||
if s.plugin.ConfigHook == "" {
|
||
return "", fmt.Errorf("插件未配置 config 钩子,无法生成客户端配置")
|
||
}
|
||
return s.runHook(s.plugin.ConfigHook, t)
|
||
}
|
||
|
||
func (s *Service) Delete(t *model.Tunnel) error { return s.Stop(t) }
|
||
|
||
// LoadAll 从数据库加载全部启用的插件并注册到管理器
|
||
func LoadAll(m *core.Manager) {
|
||
if store.DB == nil {
|
||
return
|
||
}
|
||
var plugins []model.Plugin
|
||
if err := store.DB.Where("enabled = ?", true).Find(&plugins).Error; err != nil {
|
||
return
|
||
}
|
||
for i := range plugins {
|
||
m.Register(New(&plugins[i]))
|
||
}
|
||
}
|