This commit is contained in:
Flik
2025-12-27 22:49:07 +08:00
parent 622f30b381
commit 4b71ff1604
10 changed files with 351 additions and 87 deletions

View File

@@ -10,8 +10,14 @@ import (
// ServerConfig 服务端配置
type ServerConfig struct {
Server ServerSettings `yaml:"server"`
Web WebSettings `yaml:"web"`
Server ServerSettings `yaml:"server"`
Web WebSettings `yaml:"web"`
PluginStore PluginStoreSettings `yaml:"plugin_store"`
}
// PluginStoreSettings 扩展商店设置
type PluginStoreSettings struct {
URL string `yaml:"url"` // 扩展商店URL例如 GitHub 仓库的 raw URL
}
// ServerSettings 服务端设置

View File

@@ -2,25 +2,34 @@ package db
import "github.com/gotunnel/pkg/protocol"
// ClientPlugin 客户端已安装的插件
type ClientPlugin struct {
Name string `json:"name"`
Version string `json:"version"`
Enabled bool `json:"enabled"`
}
// Client 客户端数据
type Client struct {
ID string `json:"id"`
Nickname string `json:"nickname,omitempty"`
Rules []protocol.ProxyRule `json:"rules"`
Plugins []ClientPlugin `json:"plugins,omitempty"` // 已安装的插件
}
// PluginData 插件数据
type PluginData struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Source string `json:"source"`
Description string `json:"description"`
Author string `json:"author"`
Checksum string `json:"checksum"`
Size int64 `json:"size"`
Enabled bool `json:"enabled"`
WASMData []byte `json:"-"`
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Source string `json:"source"`
Description string `json:"description"`
Author string `json:"author"`
Icon string `json:"icon"`
Checksum string `json:"checksum"`
Size int64 `json:"size"`
Enabled bool `json:"enabled"`
WASMData []byte `json:"-"`
}
// ClientStore 客户端存储接口

View File

@@ -39,7 +39,8 @@ func (s *SQLiteStore) init() error {
CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
nickname TEXT NOT NULL DEFAULT '',
rules TEXT NOT NULL DEFAULT '[]'
rules TEXT NOT NULL DEFAULT '[]',
plugins TEXT NOT NULL DEFAULT '[]'
)
`)
if err != nil {
@@ -48,6 +49,8 @@ func (s *SQLiteStore) init() error {
// 迁移:添加 nickname 列
s.db.Exec(`ALTER TABLE clients ADD COLUMN nickname TEXT NOT NULL DEFAULT ''`)
// 迁移:添加 plugins 列
s.db.Exec(`ALTER TABLE clients ADD COLUMN plugins TEXT NOT NULL DEFAULT '[]'`)
// 创建插件表
_, err = s.db.Exec(`
@@ -58,13 +61,21 @@ func (s *SQLiteStore) init() error {
source TEXT NOT NULL DEFAULT 'wasm',
description TEXT,
author TEXT,
icon TEXT,
checksum TEXT,
size INTEGER DEFAULT 0,
enabled INTEGER DEFAULT 1,
wasm_data BLOB
)
`)
return err
if err != nil {
return err
}
// 迁移:添加 icon 列
s.db.Exec(`ALTER TABLE plugins ADD COLUMN icon TEXT`)
return nil
}
// Close 关闭数据库连接
@@ -77,7 +88,7 @@ func (s *SQLiteStore) GetAllClients() ([]Client, error) {
s.mu.RLock()
defer s.mu.RUnlock()
rows, err := s.db.Query(`SELECT id, nickname, rules FROM clients`)
rows, err := s.db.Query(`SELECT id, nickname, rules, plugins FROM clients`)
if err != nil {
return nil, err
}
@@ -86,13 +97,16 @@ func (s *SQLiteStore) GetAllClients() ([]Client, error) {
var clients []Client
for rows.Next() {
var c Client
var rulesJSON string
if err := rows.Scan(&c.ID, &c.Nickname, &rulesJSON); err != nil {
var rulesJSON, pluginsJSON string
if err := rows.Scan(&c.ID, &c.Nickname, &rulesJSON, &pluginsJSON); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
c.Rules = []protocol.ProxyRule{}
}
if err := json.Unmarshal([]byte(pluginsJSON), &c.Plugins); err != nil {
c.Plugins = []ClientPlugin{}
}
clients = append(clients, c)
}
return clients, nil
@@ -104,14 +118,17 @@ func (s *SQLiteStore) GetClient(id string) (*Client, error) {
defer s.mu.RUnlock()
var c Client
var rulesJSON string
err := s.db.QueryRow(`SELECT id, nickname, rules FROM clients WHERE id = ?`, id).Scan(&c.ID, &c.Nickname, &rulesJSON)
var rulesJSON, pluginsJSON string
err := s.db.QueryRow(`SELECT id, nickname, rules, plugins FROM clients WHERE id = ?`, id).Scan(&c.ID, &c.Nickname, &rulesJSON, &pluginsJSON)
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
c.Rules = []protocol.ProxyRule{}
}
if err := json.Unmarshal([]byte(pluginsJSON), &c.Plugins); err != nil {
c.Plugins = []ClientPlugin{}
}
return &c, nil
}
@@ -124,7 +141,12 @@ func (s *SQLiteStore) CreateClient(c *Client) error {
if err != nil {
return err
}
_, err = s.db.Exec(`INSERT INTO clients (id, nickname, rules) VALUES (?, ?, ?)`, c.ID, c.Nickname, string(rulesJSON))
pluginsJSON, err := json.Marshal(c.Plugins)
if err != nil {
return err
}
_, err = s.db.Exec(`INSERT INTO clients (id, nickname, rules, plugins) VALUES (?, ?, ?, ?)`,
c.ID, c.Nickname, string(rulesJSON), string(pluginsJSON))
return err
}
@@ -137,7 +159,12 @@ func (s *SQLiteStore) UpdateClient(c *Client) error {
if err != nil {
return err
}
_, err = s.db.Exec(`UPDATE clients SET nickname = ?, rules = ? WHERE id = ?`, c.Nickname, string(rulesJSON), c.ID)
pluginsJSON, err := json.Marshal(c.Plugins)
if err != nil {
return err
}
_, err = s.db.Exec(`UPDATE clients SET nickname = ?, rules = ?, plugins = ? WHERE id = ?`,
c.Nickname, string(rulesJSON), string(pluginsJSON), c.ID)
return err
}
@@ -177,7 +204,7 @@ func (s *SQLiteStore) GetAllPlugins() ([]PluginData, error) {
defer s.mu.RUnlock()
rows, err := s.db.Query(`
SELECT name, version, type, source, description, author, checksum, size, enabled
SELECT name, version, type, source, description, author, icon, checksum, size, enabled
FROM plugins
`)
if err != nil {
@@ -189,12 +216,14 @@ func (s *SQLiteStore) GetAllPlugins() ([]PluginData, error) {
for rows.Next() {
var p PluginData
var enabled int
var icon sql.NullString
err := rows.Scan(&p.Name, &p.Version, &p.Type, &p.Source,
&p.Description, &p.Author, &p.Checksum, &p.Size, &enabled)
&p.Description, &p.Author, &icon, &p.Checksum, &p.Size, &enabled)
if err != nil {
return nil, err
}
p.Enabled = enabled == 1
p.Icon = icon.String
plugins = append(plugins, p)
}
return plugins, nil
@@ -207,15 +236,17 @@ func (s *SQLiteStore) GetPlugin(name string) (*PluginData, error) {
var p PluginData
var enabled int
var icon sql.NullString
err := s.db.QueryRow(`
SELECT name, version, type, source, description, author, checksum, size, enabled
SELECT name, version, type, source, description, author, icon, checksum, size, enabled
FROM plugins WHERE name = ?
`, name).Scan(&p.Name, &p.Version, &p.Type, &p.Source,
&p.Description, &p.Author, &p.Checksum, &p.Size, &enabled)
&p.Description, &p.Author, &icon, &p.Checksum, &p.Size, &enabled)
if err != nil {
return nil, err
}
p.Enabled = enabled == 1
p.Icon = icon.String
return &p, nil
}
@@ -230,10 +261,10 @@ func (s *SQLiteStore) SavePlugin(p *PluginData) error {
}
_, err := s.db.Exec(`
INSERT OR REPLACE INTO plugins
(name, version, type, source, description, author, checksum, size, enabled, wasm_data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
(name, version, type, source, description, author, icon, checksum, size, enabled, wasm_data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, p.Name, p.Version, p.Type, p.Source, p.Description, p.Author,
p.Checksum, p.Size, enabled, p.WASMData)
p.Icon, p.Checksum, p.Size, enabled, p.WASMData)
return err
}

View File

@@ -2,8 +2,10 @@ package router
import (
"encoding/json"
"io"
"net/http"
"regexp"
"time"
"github.com/gotunnel/internal/server/config"
"github.com/gotunnel/internal/server/db"
@@ -53,6 +55,7 @@ type PluginInfo struct {
Type string `json:"type"`
Description string `json:"description"`
Source string `json:"source"`
Icon string `json:"icon,omitempty"`
Enabled bool `json:"enabled"`
}
@@ -88,6 +91,7 @@ func RegisterRoutes(r *Router, app AppInterface) {
api.HandleFunc("/config/reload", h.handleReload)
api.HandleFunc("/plugins", h.handlePlugins)
api.HandleFunc("/plugin/", h.handlePlugin)
api.HandleFunc("/store/plugins", h.handleStorePlugins)
}
func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) {
@@ -233,7 +237,7 @@ func (h *APIHandler) getClient(rw http.ResponseWriter, clientID string) {
online, lastPing := h.server.GetClientStatus(clientID)
h.jsonResponse(rw, map[string]interface{}{
"id": client.ID, "nickname": client.Nickname, "rules": client.Rules,
"online": online, "last_ping": lastPing,
"plugins": client.Plugins, "online": online, "last_ping": lastPing,
})
}
@@ -241,6 +245,7 @@ func (h *APIHandler) updateClient(rw http.ResponseWriter, r *http.Request, clien
var req struct {
Nickname string `json:"nickname"`
Rules []protocol.ProxyRule `json:"rules"`
Plugins []db.ClientPlugin `json:"plugins"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
@@ -255,6 +260,9 @@ func (h *APIHandler) updateClient(rw http.ResponseWriter, r *http.Request, clien
client.Nickname = req.Nickname
client.Rules = req.Rules
if req.Plugins != nil {
client.Plugins = req.Plugins
}
if err := h.clientStore.UpdateClient(client); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
@@ -518,3 +526,63 @@ func (h *APIHandler) installPluginsToClient(rw http.ResponseWriter, r *http.Requ
}
h.jsonResponse(rw, map[string]string{"status": "ok"})
}
// 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"`
}
// handleStorePlugins 处理扩展商店插件列表
func (h *APIHandler) handleStorePlugins(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
cfg := h.app.GetConfig()
storeURL := cfg.PluginStore.URL
if storeURL == "" {
h.jsonResponse(rw, map[string]interface{}{
"plugins": []StorePluginInfo{},
"store_url": "",
})
return
}
// 从远程URL获取插件列表
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(storeURL)
if err != nil {
http.Error(rw, "Failed to fetch store: "+err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(rw, "Store returned error", http.StatusBadGateway)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(rw, "Failed to read response", http.StatusInternalServerError)
return
}
var plugins []StorePluginInfo
if err := json.Unmarshal(body, &plugins); err != nil {
http.Error(rw, "Invalid store format", http.StatusBadGateway)
return
}
h.jsonResponse(rw, map[string]interface{}{
"plugins": plugins,
"store_url": storeURL,
})
}