Files

139 lines
3.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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]))
}
}