111
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 29s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 48s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 37s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 36s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m6s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 35s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m0s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 52s
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 29s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 48s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 37s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 59s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 36s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 57s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m6s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 50s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 35s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m0s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 52s
This commit is contained in:
@@ -62,15 +62,22 @@ type ConfigField struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// RuleSchema 规则表单模式
|
||||
type RuleSchema struct {
|
||||
NeedsLocalAddr bool `json:"needs_local_addr"`
|
||||
ExtraFields []ConfigField `json:"extra_fields,omitempty"`
|
||||
}
|
||||
|
||||
// PluginInfo 插件信息
|
||||
type PluginInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RuleSchema *RuleSchema `json:"rule_schema,omitempty"`
|
||||
}
|
||||
|
||||
// AppInterface 应用接口
|
||||
|
||||
@@ -525,14 +525,35 @@ func (s *Server) GetPluginList() []router.PluginInfo {
|
||||
}
|
||||
|
||||
for _, info := range s.pluginRegistry.List() {
|
||||
result = append(result, router.PluginInfo{
|
||||
pi := router.PluginInfo{
|
||||
Name: info.Metadata.Name,
|
||||
Version: info.Metadata.Version,
|
||||
Type: string(info.Metadata.Type),
|
||||
Description: info.Metadata.Description,
|
||||
Source: string(info.Metadata.Source),
|
||||
Enabled: info.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
// 转换 RuleSchema
|
||||
if info.Metadata.RuleSchema != nil {
|
||||
rs := &router.RuleSchema{
|
||||
NeedsLocalAddr: info.Metadata.RuleSchema.NeedsLocalAddr,
|
||||
}
|
||||
for _, f := range info.Metadata.RuleSchema.ExtraFields {
|
||||
rs.ExtraFields = append(rs.ExtraFields, router.ConfigField{
|
||||
Key: f.Key,
|
||||
Label: f.Label,
|
||||
Type: string(f.Type),
|
||||
Default: f.Default,
|
||||
Required: f.Required,
|
||||
Options: f.Options,
|
||||
Description: f.Description,
|
||||
})
|
||||
}
|
||||
pi.RuleSchema = rs
|
||||
}
|
||||
|
||||
result = append(result, pi)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
103
pkg/plugin/api.go
Normal file
103
pkg/plugin/api.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 核心接口定义 - 按职责分离
|
||||
// =============================================================================
|
||||
|
||||
// Dialer 网络拨号接口(已在 types.go 中定义,此处为文档说明)
|
||||
// type Dialer interface {
|
||||
// Dial(network, address string) (net.Conn, error)
|
||||
// }
|
||||
|
||||
// PortManager 端口管理接口(仅服务端可用)
|
||||
type PortManager interface {
|
||||
// ReservePort 预留端口,返回错误如果端口已被占用
|
||||
ReservePort(port int) error
|
||||
// ReleasePort 释放端口
|
||||
ReleasePort(port int)
|
||||
// IsPortAvailable 检查端口是否可用
|
||||
IsPortAvailable(port int) bool
|
||||
}
|
||||
|
||||
// RuleManager 代理规则管理接口(仅服务端可用)
|
||||
type RuleManager interface {
|
||||
// CreateRule 创建代理规则
|
||||
CreateRule(rule *RuleConfig) error
|
||||
// DeleteRule 删除代理规则
|
||||
DeleteRule(clientID, ruleName string) error
|
||||
// GetRules 获取客户端的代理规则
|
||||
GetRules(clientID string) ([]RuleConfig, error)
|
||||
// UpdateRule 更新代理规则
|
||||
UpdateRule(clientID string, rule *RuleConfig) error
|
||||
}
|
||||
|
||||
// ClientManager 客户端管理接口(仅服务端可用)
|
||||
type ClientManager interface {
|
||||
// GetClientList 获取所有客户端列表
|
||||
GetClientList() ([]ClientInfo, error)
|
||||
// IsClientOnline 检查客户端是否在线
|
||||
IsClientOnline(clientID string) bool
|
||||
}
|
||||
|
||||
// Logger 日志接口
|
||||
type Logger interface {
|
||||
// Log 记录日志
|
||||
Log(level LogLevel, format string, args ...interface{})
|
||||
}
|
||||
|
||||
// ConfigStore 配置存储接口
|
||||
type ConfigStore interface {
|
||||
// GetConfig 获取配置值
|
||||
GetConfig(key string) string
|
||||
// SetConfig 设置配置值
|
||||
SetConfig(key, value string)
|
||||
}
|
||||
|
||||
// EventBus 事件总线接口
|
||||
type EventBus interface {
|
||||
// OnEvent 订阅事件
|
||||
OnEvent(eventType EventType, handler EventHandler)
|
||||
// EmitEvent 发送事件
|
||||
EmitEvent(event *Event)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 组合接口
|
||||
// =============================================================================
|
||||
|
||||
// PluginAPI 插件 API 主接口,组合所有子接口
|
||||
// 插件可以通过此接口访问 GoTunnel 的功能
|
||||
type PluginAPI interface {
|
||||
// 网络操作
|
||||
Dial(network, address string) (net.Conn, error)
|
||||
DialTimeout(network, address string, timeout time.Duration) (net.Conn, error)
|
||||
Listen(network, address string) (net.Listener, error)
|
||||
|
||||
// 端口管理(服务端)
|
||||
PortManager
|
||||
|
||||
// 规则管理(服务端)
|
||||
RuleManager
|
||||
|
||||
// 客户端管理(服务端)
|
||||
ClientManager
|
||||
|
||||
// 日志
|
||||
Logger
|
||||
|
||||
// 配置
|
||||
ConfigStore
|
||||
|
||||
// 事件
|
||||
EventBus
|
||||
|
||||
// 上下文
|
||||
GetContext() *Context
|
||||
GetClientID() string
|
||||
GetServerInfo() *ServerInfo
|
||||
}
|
||||
90
pkg/plugin/base.go
Normal file
90
pkg/plugin/base.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 基础实现 - 提取公共代码
|
||||
// =============================================================================
|
||||
|
||||
// baseAPI 包含服务端和客户端共享的基础功能
|
||||
type baseAPI struct {
|
||||
pluginName string
|
||||
config map[string]string
|
||||
configMu sync.RWMutex
|
||||
|
||||
eventHandlers map[EventType][]EventHandler
|
||||
eventMu sync.RWMutex
|
||||
}
|
||||
|
||||
// newBaseAPI 创建基础 API
|
||||
func newBaseAPI(pluginName string, config map[string]string) *baseAPI {
|
||||
cfg := config
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]string)
|
||||
}
|
||||
return &baseAPI{
|
||||
pluginName: pluginName,
|
||||
config: cfg,
|
||||
eventHandlers: make(map[EventType][]EventHandler),
|
||||
}
|
||||
}
|
||||
|
||||
// Log 记录日志
|
||||
func (b *baseAPI) Log(level LogLevel, format string, args ...interface{}) {
|
||||
prefix := fmt.Sprintf("[Plugin:%s] ", b.pluginName)
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
log.Printf("%s%s", prefix, msg)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置值
|
||||
func (b *baseAPI) GetConfig(key string) string {
|
||||
b.configMu.RLock()
|
||||
defer b.configMu.RUnlock()
|
||||
return b.config[key]
|
||||
}
|
||||
|
||||
// SetConfig 设置配置值
|
||||
func (b *baseAPI) SetConfig(key, value string) {
|
||||
b.configMu.Lock()
|
||||
defer b.configMu.Unlock()
|
||||
b.config[key] = value
|
||||
}
|
||||
|
||||
// OnEvent 订阅事件
|
||||
func (b *baseAPI) OnEvent(eventType EventType, handler EventHandler) {
|
||||
b.eventMu.Lock()
|
||||
defer b.eventMu.Unlock()
|
||||
b.eventHandlers[eventType] = append(b.eventHandlers[eventType], handler)
|
||||
}
|
||||
|
||||
// EmitEvent 发送事件(复制切片避免竞态条件)
|
||||
func (b *baseAPI) EmitEvent(event *Event) {
|
||||
b.eventMu.RLock()
|
||||
handlers := make([]EventHandler, len(b.eventHandlers[event.Type]))
|
||||
copy(handlers, b.eventHandlers[event.Type])
|
||||
b.eventMu.RUnlock()
|
||||
|
||||
for _, handler := range handlers {
|
||||
go handler(event)
|
||||
}
|
||||
}
|
||||
|
||||
// getPluginName 获取插件名称
|
||||
func (b *baseAPI) getPluginName() string {
|
||||
return b.pluginName
|
||||
}
|
||||
|
||||
// getConfigMap 获取配置副本
|
||||
func (b *baseAPI) getConfigMap() map[string]string {
|
||||
b.configMu.RLock()
|
||||
defer b.configMu.RUnlock()
|
||||
result := make(map[string]string, len(b.config))
|
||||
for k, v := range b.config {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -50,6 +50,9 @@ func (p *SOCKS5Plugin) Metadata() plugin.PluginMetadata {
|
||||
Capabilities: []string{
|
||||
"dial", "read", "write", "close",
|
||||
},
|
||||
RuleSchema: &plugin.RuleSchema{
|
||||
NeedsLocalAddr: false, // SOCKS5 不需要本地地址
|
||||
},
|
||||
ConfigSchema: []plugin.ConfigField{
|
||||
{
|
||||
Key: "auth",
|
||||
|
||||
@@ -34,6 +34,18 @@ func (p *VNCPlugin) Metadata() plugin.PluginMetadata {
|
||||
Capabilities: []string{
|
||||
"dial", "read", "write", "close",
|
||||
},
|
||||
RuleSchema: &plugin.RuleSchema{
|
||||
NeedsLocalAddr: false,
|
||||
ExtraFields: []plugin.ConfigField{
|
||||
{
|
||||
Key: "vnc_addr",
|
||||
Label: "VNC 地址",
|
||||
Type: plugin.ConfigFieldString,
|
||||
Default: "127.0.0.1:5900",
|
||||
Description: "客户端本地 VNC 服务地址",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
161
pkg/plugin/client_api.go
Normal file
161
pkg/plugin/client_api.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 客户端 API 实现
|
||||
// =============================================================================
|
||||
|
||||
// ClientAPI 客户端 PluginAPI 实现
|
||||
type ClientAPI struct {
|
||||
*baseAPI
|
||||
clientID string
|
||||
dialer Dialer
|
||||
}
|
||||
|
||||
// ClientAPIOption 客户端 API 配置选项
|
||||
type ClientAPIOption struct {
|
||||
PluginName string
|
||||
ClientID string
|
||||
Config map[string]string
|
||||
Dialer Dialer
|
||||
}
|
||||
|
||||
// NewClientAPI 创建客户端 API
|
||||
func NewClientAPI(opt ClientAPIOption) *ClientAPI {
|
||||
return &ClientAPI{
|
||||
baseAPI: newBaseAPI(opt.PluginName, opt.Config),
|
||||
clientID: opt.ClientID,
|
||||
dialer: opt.Dialer,
|
||||
}
|
||||
}
|
||||
|
||||
// --- 网络操作 ---
|
||||
|
||||
// Dial 通过隧道建立连接
|
||||
func (c *ClientAPI) Dial(network, address string) (net.Conn, error) {
|
||||
if c.dialer == nil {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
return c.dialer.Dial(network, address)
|
||||
}
|
||||
|
||||
// DialTimeout 带超时的连接(使用 context 避免 goroutine 泄漏)
|
||||
func (c *ClientAPI) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
if c.dialer == nil {
|
||||
return nil, ErrNotConnected
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
type result struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
ch := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := c.dialer.Dial(network, address)
|
||||
select {
|
||||
case ch <- result{conn, err}:
|
||||
case <-ctx.Done():
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case r := <-ch:
|
||||
return r.conn, r.err
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("dial timeout")
|
||||
}
|
||||
}
|
||||
|
||||
// Listen 客户端不支持监听
|
||||
func (c *ClientAPI) Listen(network, address string) (net.Listener, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// --- 端口管理(客户端不支持)---
|
||||
|
||||
// ReservePort 客户端不支持
|
||||
func (c *ClientAPI) ReservePort(port int) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
// ReleasePort 客户端不支持
|
||||
func (c *ClientAPI) ReleasePort(port int) {}
|
||||
|
||||
// IsPortAvailable 检查本地端口是否可用
|
||||
func (c *ClientAPI) IsPortAvailable(port int) bool {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ln.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
// --- 规则管理(客户端不支持)---
|
||||
|
||||
// CreateRule 客户端不支持
|
||||
func (c *ClientAPI) CreateRule(rule *RuleConfig) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
// DeleteRule 客户端不支持
|
||||
func (c *ClientAPI) DeleteRule(clientID, ruleName string) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
// GetRules 客户端不支持
|
||||
func (c *ClientAPI) GetRules(clientID string) ([]RuleConfig, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// UpdateRule 客户端不支持
|
||||
func (c *ClientAPI) UpdateRule(clientID string, rule *RuleConfig) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
// --- 客户端管理 ---
|
||||
|
||||
// GetClientID 获取当前客户端 ID
|
||||
func (c *ClientAPI) GetClientID() string {
|
||||
return c.clientID
|
||||
}
|
||||
|
||||
// GetClientList 客户端不支持
|
||||
func (c *ClientAPI) GetClientList() ([]ClientInfo, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// IsClientOnline 客户端不支持
|
||||
func (c *ClientAPI) IsClientOnline(clientID string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// --- 上下文 ---
|
||||
|
||||
// GetContext 获取当前上下文
|
||||
func (c *ClientAPI) GetContext() *Context {
|
||||
return &Context{
|
||||
PluginName: c.getPluginName(),
|
||||
Side: SideClient,
|
||||
ClientID: c.clientID,
|
||||
Config: c.getConfigMap(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetServerInfo 客户端不支持
|
||||
func (c *ClientAPI) GetServerInfo() *ServerInfo {
|
||||
return nil
|
||||
}
|
||||
180
pkg/plugin/server_api.go
Normal file
180
pkg/plugin/server_api.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// 服务端依赖接口(依赖注入)
|
||||
// =============================================================================
|
||||
|
||||
// PortStore 端口存储接口
|
||||
type PortStore interface {
|
||||
Reserve(port int, owner string) error
|
||||
Release(port int)
|
||||
IsAvailable(port int) bool
|
||||
}
|
||||
|
||||
// RuleStore 规则存储接口
|
||||
type RuleStore interface {
|
||||
GetAll(clientID string) ([]RuleConfig, error)
|
||||
Create(clientID string, rule *RuleConfig) error
|
||||
Update(clientID string, rule *RuleConfig) error
|
||||
Delete(clientID, ruleName string) error
|
||||
}
|
||||
|
||||
// ClientStore 客户端存储接口
|
||||
type ClientStore interface {
|
||||
GetAll() ([]ClientInfo, error)
|
||||
IsOnline(clientID string) bool
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 服务端 API 实现
|
||||
// =============================================================================
|
||||
|
||||
// ServerAPI 服务端 PluginAPI 实现
|
||||
type ServerAPI struct {
|
||||
*baseAPI
|
||||
portStore PortStore
|
||||
ruleStore RuleStore
|
||||
clientStore ClientStore
|
||||
serverInfo *ServerInfo
|
||||
}
|
||||
|
||||
// ServerAPIOption 服务端 API 配置选项
|
||||
type ServerAPIOption struct {
|
||||
PluginName string
|
||||
Config map[string]string
|
||||
PortStore PortStore
|
||||
RuleStore RuleStore
|
||||
ClientStore ClientStore
|
||||
ServerInfo *ServerInfo
|
||||
}
|
||||
|
||||
// NewServerAPI 创建服务端 API
|
||||
func NewServerAPI(opt ServerAPIOption) *ServerAPI {
|
||||
return &ServerAPI{
|
||||
baseAPI: newBaseAPI(opt.PluginName, opt.Config),
|
||||
portStore: opt.PortStore,
|
||||
ruleStore: opt.RuleStore,
|
||||
clientStore: opt.ClientStore,
|
||||
serverInfo: opt.ServerInfo,
|
||||
}
|
||||
}
|
||||
|
||||
// --- 网络操作 ---
|
||||
|
||||
// Dial 服务端不支持隧道拨号
|
||||
func (s *ServerAPI) Dial(network, address string) (net.Conn, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// DialTimeout 服务端不支持隧道拨号
|
||||
func (s *ServerAPI) DialTimeout(network, address string, timeout time.Duration) (net.Conn, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// Listen 在指定地址监听
|
||||
func (s *ServerAPI) Listen(network, address string) (net.Listener, error) {
|
||||
return net.Listen(network, address)
|
||||
}
|
||||
|
||||
// --- 端口管理 ---
|
||||
|
||||
// ReservePort 预留端口
|
||||
func (s *ServerAPI) ReservePort(port int) error {
|
||||
if s.portStore == nil {
|
||||
return ErrNotSupported
|
||||
}
|
||||
return s.portStore.Reserve(port, s.getPluginName())
|
||||
}
|
||||
|
||||
// ReleasePort 释放端口
|
||||
func (s *ServerAPI) ReleasePort(port int) {
|
||||
if s.portStore != nil {
|
||||
s.portStore.Release(port)
|
||||
}
|
||||
}
|
||||
|
||||
// IsPortAvailable 检查端口是否可用
|
||||
func (s *ServerAPI) IsPortAvailable(port int) bool {
|
||||
if s.portStore == nil {
|
||||
return false
|
||||
}
|
||||
return s.portStore.IsAvailable(port)
|
||||
}
|
||||
|
||||
// --- 规则管理 ---
|
||||
|
||||
// CreateRule 创建代理规则
|
||||
func (s *ServerAPI) CreateRule(rule *RuleConfig) error {
|
||||
if s.ruleStore == nil {
|
||||
return ErrNotSupported
|
||||
}
|
||||
return s.ruleStore.Create(rule.ClientID, rule)
|
||||
}
|
||||
|
||||
// DeleteRule 删除代理规则
|
||||
func (s *ServerAPI) DeleteRule(clientID, ruleName string) error {
|
||||
if s.ruleStore == nil {
|
||||
return ErrNotSupported
|
||||
}
|
||||
return s.ruleStore.Delete(clientID, ruleName)
|
||||
}
|
||||
|
||||
// GetRules 获取客户端的代理规则
|
||||
func (s *ServerAPI) GetRules(clientID string) ([]RuleConfig, error) {
|
||||
if s.ruleStore == nil {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
return s.ruleStore.GetAll(clientID)
|
||||
}
|
||||
|
||||
// UpdateRule 更新代理规则
|
||||
func (s *ServerAPI) UpdateRule(clientID string, rule *RuleConfig) error {
|
||||
if s.ruleStore == nil {
|
||||
return ErrNotSupported
|
||||
}
|
||||
return s.ruleStore.Update(clientID, rule)
|
||||
}
|
||||
|
||||
// --- 客户端管理 ---
|
||||
|
||||
// GetClientID 服务端返回空
|
||||
func (s *ServerAPI) GetClientID() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetClientList 获取所有客户端列表
|
||||
func (s *ServerAPI) GetClientList() ([]ClientInfo, error) {
|
||||
if s.clientStore == nil {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
return s.clientStore.GetAll()
|
||||
}
|
||||
|
||||
// IsClientOnline 检查客户端是否在线
|
||||
func (s *ServerAPI) IsClientOnline(clientID string) bool {
|
||||
if s.clientStore == nil {
|
||||
return false
|
||||
}
|
||||
return s.clientStore.IsOnline(clientID)
|
||||
}
|
||||
|
||||
// --- 上下文 ---
|
||||
|
||||
// GetContext 获取当前上下文
|
||||
func (s *ServerAPI) GetContext() *Context {
|
||||
return &Context{
|
||||
PluginName: s.getPluginName(),
|
||||
Side: SideServer,
|
||||
Config: s.getConfigMap(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetServerInfo 获取服务端信息
|
||||
func (s *ServerAPI) GetServerInfo() *ServerInfo {
|
||||
return s.serverInfo
|
||||
}
|
||||
@@ -45,6 +45,12 @@ type ConfigField struct {
|
||||
Description string `json:"description,omitempty"` // 字段描述
|
||||
}
|
||||
|
||||
// RuleSchema 规则表单模式定义
|
||||
type RuleSchema struct {
|
||||
NeedsLocalAddr bool `json:"needs_local_addr"` // 是否需要本地地址
|
||||
ExtraFields []ConfigField `json:"extra_fields,omitempty"` // 额外字段
|
||||
}
|
||||
|
||||
// PluginMetadata 描述一个 plugin
|
||||
type PluginMetadata struct {
|
||||
Name string `json:"name"` // 唯一标识符 (如 "socks5")
|
||||
@@ -57,7 +63,8 @@ type PluginMetadata struct {
|
||||
Checksum string `json:"checksum,omitempty"` // WASM 二进制的 SHA256
|
||||
Size int64 `json:"size,omitempty"` // WASM 二进制大小
|
||||
Capabilities []string `json:"capabilities,omitempty"` // 所需 host functions
|
||||
ConfigSchema []ConfigField `json:"config_schema,omitempty"`// 配置模式定义
|
||||
ConfigSchema []ConfigField `json:"config_schema,omitempty"`// 插件配置模式
|
||||
RuleSchema *RuleSchema `json:"rule_schema,omitempty"` // 规则表单模式
|
||||
}
|
||||
|
||||
// PluginInfo 组合元数据和运行时状态
|
||||
@@ -90,6 +97,15 @@ type ProxyHandler interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// ExtendedProxyHandler 扩展的代理处理器接口
|
||||
// 支持 PluginAPI 的插件应实现此接口
|
||||
type ExtendedProxyHandler interface {
|
||||
ProxyHandler
|
||||
|
||||
// SetAPI 设置 PluginAPI,允许插件调用系统功能
|
||||
SetAPI(api PluginAPI)
|
||||
}
|
||||
|
||||
// LogLevel 日志级别
|
||||
type LogLevel uint8
|
||||
|
||||
@@ -100,6 +116,101 @@ const (
|
||||
LogError
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// API 相关类型
|
||||
// =============================================================================
|
||||
|
||||
// Side 运行侧
|
||||
type Side string
|
||||
|
||||
const (
|
||||
SideServer Side = "server"
|
||||
SideClient Side = "client"
|
||||
)
|
||||
|
||||
// Context 插件运行上下文
|
||||
type Context struct {
|
||||
PluginName string
|
||||
Side Side
|
||||
ClientID string
|
||||
Config map[string]string
|
||||
}
|
||||
|
||||
// ServerInfo 服务端信息
|
||||
type ServerInfo struct {
|
||||
BindAddr string
|
||||
BindPort int
|
||||
Version string
|
||||
}
|
||||
|
||||
// RuleConfig 代理规则配置
|
||||
type RuleConfig struct {
|
||||
ClientID string `json:"client_id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
LocalIP string `json:"local_ip"`
|
||||
LocalPort int `json:"local_port"`
|
||||
RemotePort int `json:"remote_port"`
|
||||
Enabled bool `json:"enabled"`
|
||||
PluginName string `json:"plugin_name,omitempty"`
|
||||
PluginConfig map[string]string `json:"plugin_config,omitempty"`
|
||||
}
|
||||
|
||||
// ClientInfo 客户端信息
|
||||
type ClientInfo struct {
|
||||
ID string `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
Online bool `json:"online"`
|
||||
LastPing string `json:"last_ping,omitempty"`
|
||||
}
|
||||
|
||||
// EventType 事件类型
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventClientConnect EventType = "client_connect"
|
||||
EventClientDisconnect EventType = "client_disconnect"
|
||||
EventRuleCreated EventType = "rule_created"
|
||||
EventRuleDeleted EventType = "rule_deleted"
|
||||
EventProxyConnect EventType = "proxy_connect"
|
||||
EventProxyDisconnect EventType = "proxy_disconnect"
|
||||
)
|
||||
|
||||
// Event 事件
|
||||
type Event struct {
|
||||
Type EventType `json:"type"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// EventHandler 事件处理函数
|
||||
type EventHandler func(event *Event)
|
||||
|
||||
// =============================================================================
|
||||
// 错误定义
|
||||
// =============================================================================
|
||||
|
||||
// APIError API 错误
|
||||
type APIError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// 常见 API 错误
|
||||
var (
|
||||
ErrNotSupported = &APIError{Code: 1, Message: "operation not supported"}
|
||||
ErrClientNotFound = &APIError{Code: 2, Message: "client not found"}
|
||||
ErrPortOccupied = &APIError{Code: 3, Message: "port already occupied"}
|
||||
ErrRuleNotFound = &APIError{Code: 4, Message: "rule not found"}
|
||||
ErrRuleExists = &APIError{Code: 5, Message: "rule already exists"}
|
||||
ErrNotConnected = &APIError{Code: 6, Message: "not connected"}
|
||||
ErrInvalidConfig = &APIError{Code: 7, Message: "invalid configuration"}
|
||||
)
|
||||
|
||||
// ConnHandle WASM 连接句柄
|
||||
type ConnHandle uint32
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface ProxyRule {
|
||||
remote_port: number
|
||||
type?: string
|
||||
enabled?: boolean
|
||||
plugin_config?: Record<string, string>
|
||||
}
|
||||
|
||||
// 客户端已安装的插件
|
||||
@@ -27,6 +28,12 @@ export interface ConfigField {
|
||||
description?: string
|
||||
}
|
||||
|
||||
// 规则表单模式
|
||||
export interface RuleSchema {
|
||||
needs_local_addr: boolean
|
||||
extra_fields?: ConfigField[]
|
||||
}
|
||||
|
||||
// 插件配置响应
|
||||
export interface PluginConfigResponse {
|
||||
plugin_name: string
|
||||
@@ -89,6 +96,7 @@ export interface PluginInfo {
|
||||
source: string
|
||||
icon?: string
|
||||
enabled: boolean
|
||||
rule_schema?: RuleSchema
|
||||
}
|
||||
|
||||
// 扩展商店插件信息
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getClient, updateClient, deleteClient, pushConfigToClient, disconnectClient,
|
||||
getPlugins, installPluginsToClient, getClientPluginConfig, updateClientPluginConfig
|
||||
} from '../api'
|
||||
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField } from '../types'
|
||||
import type { ProxyRule, PluginInfo, ClientPlugin, ConfigField, RuleSchema } from '../types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -32,14 +32,35 @@ const editing = ref(false)
|
||||
const editNickname = ref('')
|
||||
const editRules = ref<ProxyRule[]>([])
|
||||
|
||||
const typeOptions = [
|
||||
// 内置类型
|
||||
const builtinTypes = [
|
||||
{ label: 'TCP', value: 'tcp' },
|
||||
{ label: 'UDP', value: 'udp' },
|
||||
{ label: 'HTTP', value: 'http' },
|
||||
{ label: 'HTTPS', value: 'https' },
|
||||
{ label: 'SOCKS5', value: 'socks5' }
|
||||
{ label: 'HTTPS', value: 'https' }
|
||||
]
|
||||
|
||||
// 规则类型选项(内置 + 插件)
|
||||
const typeOptions = ref([...builtinTypes])
|
||||
|
||||
// 插件 RuleSchema 映射
|
||||
const pluginRuleSchemas = ref<Record<string, RuleSchema>>({})
|
||||
|
||||
// 判断类型是否需要本地地址
|
||||
const needsLocalAddr = (type: string) => {
|
||||
// 内置类型
|
||||
if (['tcp', 'udp'].includes(type)) return true
|
||||
// 插件类型:查询 RuleSchema
|
||||
const schema = pluginRuleSchemas.value[type]
|
||||
return schema?.needs_local_addr ?? false
|
||||
}
|
||||
|
||||
// 获取类型的额外字段
|
||||
const getExtraFields = (type: string): ConfigField[] => {
|
||||
const schema = pluginRuleSchemas.value[type]
|
||||
return schema?.extra_fields || []
|
||||
}
|
||||
|
||||
// 插件安装相关
|
||||
const showInstallModal = ref(false)
|
||||
const availablePlugins = ref<PluginInfo[]>([])
|
||||
@@ -56,6 +77,21 @@ const loadPlugins = async () => {
|
||||
try {
|
||||
const { data } = await getPlugins()
|
||||
availablePlugins.value = (data || []).filter(p => p.enabled)
|
||||
|
||||
// 更新类型选项:内置类型 + proxy 类型插件
|
||||
const proxyPlugins = availablePlugins.value
|
||||
.filter(p => p.type === 'proxy')
|
||||
.map(p => ({ label: `${p.name.toUpperCase()} (插件)`, value: p.name }))
|
||||
typeOptions.value = [...builtinTypes, ...proxyPlugins]
|
||||
|
||||
// 保存插件的 RuleSchema
|
||||
const schemas: Record<string, RuleSchema> = {}
|
||||
for (const p of availablePlugins.value) {
|
||||
if (p.rule_schema) {
|
||||
schemas[p.name] = p.rule_schema
|
||||
}
|
||||
}
|
||||
pluginRuleSchemas.value = schemas
|
||||
} catch (e) {
|
||||
console.error('Failed to load plugins', e)
|
||||
}
|
||||
@@ -63,6 +99,9 @@ const loadPlugins = async () => {
|
||||
|
||||
const openInstallModal = async () => {
|
||||
await loadPlugins()
|
||||
// 过滤掉已安装的插件
|
||||
const installedNames = clientPlugins.value.map(p => p.name)
|
||||
availablePlugins.value = availablePlugins.value.filter(p => !installedNames.includes(p.name))
|
||||
selectedPlugins.value = []
|
||||
showInstallModal.value = true
|
||||
}
|
||||
@@ -85,7 +124,10 @@ const loadClient = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadClient)
|
||||
onMounted(() => {
|
||||
loadClient()
|
||||
loadPlugins()
|
||||
})
|
||||
|
||||
const startEdit = () => {
|
||||
editNickname.value = nickname.value
|
||||
@@ -316,9 +358,14 @@ const savePluginConfig = async () => {
|
||||
<tbody>
|
||||
<tr v-for="rule in rules" :key="rule.name">
|
||||
<td>{{ rule.name || '未命名' }}</td>
|
||||
<td>{{ rule.local_ip }}:{{ rule.local_port }}</td>
|
||||
<td>
|
||||
<template v-if="needsLocalAddr(rule.type || 'tcp')">
|
||||
{{ rule.local_ip }}:{{ rule.local_port }}
|
||||
</template>
|
||||
<span v-else style="color: #999;">-</span>
|
||||
</td>
|
||||
<td>{{ rule.remote_port }}</td>
|
||||
<td><n-tag size="small">{{ rule.type || 'tcp' }}</n-tag></td>
|
||||
<td><n-tag size="small">{{ (rule.type || 'tcp').toUpperCase() }}</n-tag></td>
|
||||
<td>
|
||||
<n-tag size="small" :type="rule.enabled !== false ? 'success' : 'default'">
|
||||
{{ rule.enabled !== false ? '启用' : '禁用' }}
|
||||
@@ -336,7 +383,7 @@ const savePluginConfig = async () => {
|
||||
<n-input v-model:value="editNickname" placeholder="给客户端起个名字(可选)" style="max-width: 300px;" />
|
||||
</n-form-item>
|
||||
<n-card v-for="(rule, i) in editRules" :key="i" size="small">
|
||||
<n-space align="center">
|
||||
<n-space align="center" wrap>
|
||||
<n-form-item label="启用" :show-feedback="false">
|
||||
<n-switch v-model:value="rule.enabled" />
|
||||
</n-form-item>
|
||||
@@ -344,17 +391,31 @@ const savePluginConfig = async () => {
|
||||
<n-input v-model:value="rule.name" placeholder="规则名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="类型" :show-feedback="false">
|
||||
<n-select v-model:value="rule.type" :options="typeOptions" style="width: 100px;" />
|
||||
</n-form-item>
|
||||
<n-form-item label="本地IP" :show-feedback="false">
|
||||
<n-input v-model:value="rule.local_ip" placeholder="127.0.0.1" />
|
||||
</n-form-item>
|
||||
<n-form-item label="本地端口" :show-feedback="false">
|
||||
<n-input-number v-model:value="rule.local_port" :show-button="false" />
|
||||
<n-select v-model:value="rule.type" :options="typeOptions" style="width: 140px;" />
|
||||
</n-form-item>
|
||||
<!-- 仅 tcp/udp 显示本地地址 -->
|
||||
<template v-if="needsLocalAddr(rule.type || 'tcp')">
|
||||
<n-form-item label="本地IP" :show-feedback="false">
|
||||
<n-input v-model:value="rule.local_ip" placeholder="127.0.0.1" />
|
||||
</n-form-item>
|
||||
<n-form-item label="本地端口" :show-feedback="false">
|
||||
<n-input-number v-model:value="rule.local_port" :show-button="false" />
|
||||
</n-form-item>
|
||||
</template>
|
||||
<n-form-item label="远程端口" :show-feedback="false">
|
||||
<n-input-number v-model:value="rule.remote_port" :show-button="false" />
|
||||
</n-form-item>
|
||||
<!-- 插件额外字段 -->
|
||||
<template v-for="field in getExtraFields(rule.type || '')" :key="field.key">
|
||||
<n-form-item :label="field.label" :show-feedback="false">
|
||||
<n-input
|
||||
v-if="field.type === 'string'"
|
||||
:value="rule.plugin_config?.[field.key] || field.default || ''"
|
||||
@update:value="(v: string) => { if (!rule.plugin_config) rule.plugin_config = {}; rule.plugin_config[field.key] = v }"
|
||||
:placeholder="field.description"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
<n-button v-if="editRules.length > 1" quaternary type="error" @click="removeRule(i)">
|
||||
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||
</n-button>
|
||||
|
||||
Reference in New Issue
Block a user