优化部分设置逻辑
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -46,7 +47,6 @@ func SetupRouter() *gin.Engine {
|
||||
// 前端入口
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.File("./static/index.html")
|
||||
c.String(200, "frpc-console 后端已启动 🚀")
|
||||
})
|
||||
|
||||
return r
|
||||
@@ -101,21 +101,29 @@ func updateConfigHandler(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("收到配置更新: %+v", cfg)
|
||||
|
||||
if err := UpdateGlobalConfig(&cfg); err != nil {
|
||||
log.Printf("❌ 更新数据库失败: %v", err) // 新增日志
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败"})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ 数据库更新成功")
|
||||
|
||||
// 自动生成配置文件并热加载
|
||||
if err := GenerateFrpcConfig(); err != nil {
|
||||
log.Printf("❌ 生成配置文件失败: %v", err) // 新增日志
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ 配置文件生成成功")
|
||||
|
||||
if err := ReloadFrpc(); err != nil {
|
||||
log.Printf("❌ 热加载失败: %v", err) // 新增日志
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ 热加载成功")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,13 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var jwtSecret = []byte("frpc-console-secret-key-2024")
|
||||
func getJwtSecret() []byte {
|
||||
secret, err := GetJwtSecret()
|
||||
if err != nil {
|
||||
log.Fatal("❌ 获取 JWT 密钥失败:", err)
|
||||
}
|
||||
return []byte(secret)
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
Username string `json:"username"`
|
||||
@@ -29,12 +35,12 @@ func GenerateJWT(username string) (string, error) {
|
||||
},
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
return token.SignedString(getJwtSecret())
|
||||
}
|
||||
|
||||
func ParseJWT(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
return getJwtSecret(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var DB *sql.DB
|
||||
var dbInitOnce sync.Once
|
||||
|
||||
// 全局配置表(A表)
|
||||
// ===== 全局配置表(A表)=====
|
||||
type GlobalConfig struct {
|
||||
ID int `json:"id"`
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
@@ -24,7 +28,7 @@ type GlobalConfig struct {
|
||||
PoolCount int `json:"poolCount"`
|
||||
}
|
||||
|
||||
// 隧道表(B表)
|
||||
// ===== 隧道表(B表)=====
|
||||
type Proxy struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -35,7 +39,7 @@ type Proxy struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// 用户表(C表)
|
||||
// ===== 用户表(C表)=====
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
@@ -43,6 +47,7 @@ type User struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// ===== 初始化数据库 =====
|
||||
func InitDB() error {
|
||||
var err error
|
||||
DB, err = sql.Open("sqlite", "./frpc-console.db")
|
||||
@@ -50,7 +55,7 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建用户表
|
||||
// 用户表(C表)
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -64,7 +69,7 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建全局配置表(只有一行数据)
|
||||
// 全局配置表(A表)
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS global_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
@@ -85,13 +90,15 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 确保全局配置表有一行数据
|
||||
// 确保全局配置有一行数据
|
||||
var count int
|
||||
DB.QueryRow("SELECT COUNT(*) FROM global_config").Scan(&count)
|
||||
if count == 0 {
|
||||
_, err = DB.Exec(`
|
||||
INSERT INTO global_config (id, server_addr, server_port, token, log_level, log_max_days, tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count)
|
||||
VALUES (1, 'frp.whitetop.xyz', 9358, 'Lxh10020328', 'info', 3, 1, 30, 15, 70, 8)
|
||||
INSERT INTO global_config (
|
||||
id, server_addr, server_port, token, log_level, log_max_days,
|
||||
tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count
|
||||
) VALUES (1, 'frp.whitetop.xyz', 9358, 'Lxh10020328', 'info', 3, 1, 30, 15, 70, 8)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -99,7 +106,7 @@ func InitDB() error {
|
||||
log.Println("✅ 全局配置初始化完成")
|
||||
}
|
||||
|
||||
// 创建隧道表
|
||||
// 隧道表(B表)
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS proxies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -117,36 +124,101 @@ func InitDB() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 🟢 新增:应用配置表(用于存储 JWT 密钥等全局配置)
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS app_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 🟢 确保 JWT 密钥存在
|
||||
if err := ensureJwtSecret(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("✅ 数据库初始化完成")
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== 全局配置 CRUD ==========
|
||||
// ===== 🟢 新增:JWT 密钥管理 =====
|
||||
|
||||
// ensureJwtSecret 检查 app_config 表里是否有 jwt_secret,没有则生成一个
|
||||
func ensureJwtSecret() error {
|
||||
var value string
|
||||
err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'jwt_secret'").Scan(&value)
|
||||
if err == nil && value != "" {
|
||||
// 密钥已存在
|
||||
return nil
|
||||
}
|
||||
|
||||
// 生成 32 字节随机密钥
|
||||
bytes := make([]byte, 32)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
secret := hex.EncodeToString(bytes)
|
||||
|
||||
// 存入数据库
|
||||
_, err = DB.Exec(`
|
||||
INSERT INTO app_config (key, value) VALUES ('jwt_secret', ?)
|
||||
`, secret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("✅ JWT 密钥已生成并保存到数据库")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetJwtSecret 从数据库读取 JWT 密钥
|
||||
func GetJwtSecret() (string, error) {
|
||||
var secret string
|
||||
err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'jwt_secret'").Scan(&secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// ===== 全局配置 CRUD =====
|
||||
func GetGlobalConfig() (*GlobalConfig, error) {
|
||||
var cfg GlobalConfig
|
||||
err := DB.QueryRow(`
|
||||
SELECT id, server_addr, server_port, token, log_level, log_max_days, tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count
|
||||
SELECT id, server_addr, server_port, token, log_level, log_max_days,
|
||||
tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count
|
||||
FROM global_config WHERE id = 1
|
||||
`).Scan(&cfg.ID, &cfg.ServerAddr, &cfg.ServerPort, &cfg.Token, &cfg.LogLevel, &cfg.LogMaxDays, &cfg.TcpMux, &cfg.TcpMuxKeepalive, &cfg.HeartbeatInterval, &cfg.HeartbeatTimeout, &cfg.PoolCount)
|
||||
`).Scan(
|
||||
&cfg.ID, &cfg.ServerAddr, &cfg.ServerPort, &cfg.Token,
|
||||
&cfg.LogLevel, &cfg.LogMaxDays, &cfg.TcpMux, &cfg.TcpMuxKeepalive,
|
||||
&cfg.HeartbeatInterval, &cfg.HeartbeatTimeout, &cfg.PoolCount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 🟢 强制开启 tcpMux,忽略数据库中存储的值
|
||||
cfg.TcpMux = true
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func UpdateGlobalConfig(cfg *GlobalConfig) error {
|
||||
// 忽略前端传过来的 TcpMux,强制保持为 true
|
||||
_, err := DB.Exec(`
|
||||
UPDATE global_config SET
|
||||
server_addr = ?, server_port = ?, token = ?, log_level = ?, log_max_days = ?,
|
||||
tcp_mux = ?, tcp_mux_keepalive = ?, heartbeat_interval = ?, heartbeat_timeout = ?, pool_count = ?,
|
||||
tcp_mux = 1, -- 🟢 强制写死为 1
|
||||
tcp_mux_keepalive = ?, heartbeat_interval = ?, heartbeat_timeout = ?, pool_count = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = 1
|
||||
`, cfg.ServerAddr, cfg.ServerPort, cfg.Token, cfg.LogLevel, cfg.LogMaxDays,
|
||||
cfg.TcpMux, cfg.TcpMuxKeepalive, cfg.HeartbeatInterval, cfg.HeartbeatTimeout, cfg.PoolCount)
|
||||
cfg.TcpMuxKeepalive, cfg.HeartbeatInterval, cfg.HeartbeatTimeout, cfg.PoolCount)
|
||||
return err
|
||||
}
|
||||
|
||||
// ========== 隧道 CRUD ==========
|
||||
// ===== 隧道 CRUD =====
|
||||
func GetProxies() ([]Proxy, error) {
|
||||
rows, err := DB.Query(`
|
||||
SELECT id, name, type, local_ip, local_port, remote_port, enabled
|
||||
@@ -182,11 +254,16 @@ func GetProxy(id int) (*Proxy, error) {
|
||||
}
|
||||
|
||||
func CreateProxy(p *Proxy) error {
|
||||
_, err := DB.Exec(`
|
||||
result, err := DB.Exec(`
|
||||
INSERT INTO proxies (name, type, local_ip, local_port, remote_port, enabled)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`, p.Name, p.Type, p.LocalIP, p.LocalPort, p.RemotePort, p.Enabled)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, _ := result.LastInsertId()
|
||||
p.ID = int(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateProxy(p *Proxy) error {
|
||||
@@ -204,7 +281,7 @@ func DeleteProxy(id int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ========== 用户 CRUD ==========
|
||||
// ===== 用户 CRUD =====
|
||||
func GetUserByUsername(username string) (*User, error) {
|
||||
var u User
|
||||
err := DB.QueryRow(`
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"text/template"
|
||||
)
|
||||
@@ -15,10 +16,30 @@ import (
|
||||
//go:embed bin/*
|
||||
var embeddedFrpc embed.FS
|
||||
|
||||
// 获取当前平台对应的 frpc 路径(从 embed 解压到临时目录)
|
||||
func getFrpcPath() (string, error) {
|
||||
var fileName string
|
||||
//go:embed frpc.tmpl
|
||||
var frpcTemplateContent string
|
||||
|
||||
var (
|
||||
cachedFrpcPath string
|
||||
frpcPathMutex sync.Mutex
|
||||
)
|
||||
|
||||
// getFrpcPath 获取当前平台对应的 frpc 路径(优先使用缓存)
|
||||
func getFrpcPath() (string, error) {
|
||||
frpcPathMutex.Lock()
|
||||
defer frpcPathMutex.Unlock()
|
||||
|
||||
// 如果缓存有效,直接返回
|
||||
if cachedFrpcPath != "" {
|
||||
if _, err := os.Stat(cachedFrpcPath); err == nil {
|
||||
return cachedFrpcPath, nil
|
||||
}
|
||||
// 缓存失效,清空
|
||||
cachedFrpcPath = ""
|
||||
}
|
||||
|
||||
// 确定文件名
|
||||
var fileName string
|
||||
switch {
|
||||
case runtime.GOOS == "windows" && runtime.GOARCH == "amd64":
|
||||
fileName = "frpc_windows_amd64.exe"
|
||||
@@ -27,20 +48,22 @@ func getFrpcPath() (string, error) {
|
||||
case runtime.GOOS == "linux" && runtime.GOARCH == "arm64":
|
||||
fileName = "frpc_linux_arm64"
|
||||
default:
|
||||
// 降级:尝试从 PATH 中查找 frpc
|
||||
// 降级:尝试从系统 PATH 查找 frpc
|
||||
path, err := exec.LookPath("frpc")
|
||||
if err == nil {
|
||||
cachedFrpcPath = path
|
||||
return path, nil
|
||||
}
|
||||
return "", fmt.Errorf("不支持的平台: %s/%s,且 PATH 中未找到 frpc", runtime.GOOS, runtime.GOARCH)
|
||||
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
// 从 embed 读取对应文件
|
||||
// 从 embed 读取
|
||||
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
||||
if err != nil {
|
||||
// 如果 embed 中没有,降级到 PATH
|
||||
// 降级到 PATH
|
||||
path, lookErr := exec.LookPath("frpc")
|
||||
if lookErr == nil {
|
||||
cachedFrpcPath = path
|
||||
return path, nil
|
||||
}
|
||||
return "", fmt.Errorf("未找到 frpc 文件 (embed 和 PATH 均无): %w", err)
|
||||
@@ -51,40 +74,21 @@ func getFrpcPath() (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
tmpPath += ".exe"
|
||||
}
|
||||
|
||||
// 尝试写入(如果文件已存在且正在使用,忽略错误,直接使用已有文件)
|
||||
if err := os.WriteFile(tmpPath, data, 0755); err != nil {
|
||||
// 如果写入失败,检查是否已有可用的 frpc
|
||||
if _, statErr := os.Stat(tmpPath); statErr == nil {
|
||||
cachedFrpcPath = tmpPath
|
||||
return tmpPath, nil
|
||||
}
|
||||
return "", fmt.Errorf("写入临时文件失败: %w", err)
|
||||
}
|
||||
|
||||
cachedFrpcPath = tmpPath
|
||||
return tmpPath, nil
|
||||
}
|
||||
|
||||
// frpc.toml 模板 - 完全复制你的配置文件结构
|
||||
const frpcTemplate = `serverAddr = "{{.ServerAddr}}"
|
||||
serverPort = {{.ServerPort}}
|
||||
|
||||
[auth]
|
||||
token = "{{.Token}}"
|
||||
|
||||
[log]
|
||||
to = "./frpc.log"
|
||||
level = "{{.LogLevel}}"
|
||||
maxDays = {{.LogMaxDays}}
|
||||
|
||||
[transport]
|
||||
tcpMux = {{.TcpMux}}
|
||||
tcpMuxKeepaliveInterval = {{.TcpMuxKeepalive}}
|
||||
heartbeatInterval = {{.HeartbeatInterval}}
|
||||
heartbeatTimeout = {{.HeartbeatTimeout}}
|
||||
poolCount = {{.PoolCount}}
|
||||
|
||||
{{range .Proxies}}
|
||||
[[proxies]]
|
||||
name = "{{.Name}}"
|
||||
type = "{{.Type}}"
|
||||
localIP = "{{.LocalIP}}"
|
||||
localPort = {{.LocalPort}}
|
||||
remotePort = {{.RemotePort}}
|
||||
{{end}}`
|
||||
|
||||
// GenerateFrpcConfig 生成 frpc.toml 配置文件
|
||||
func GenerateFrpcConfig() error {
|
||||
cfg, err := GetGlobalConfig()
|
||||
@@ -112,19 +116,35 @@ func GenerateFrpcConfig() error {
|
||||
Proxies: activeProxies,
|
||||
}
|
||||
|
||||
tmpl, err := template.New("frpc").Parse(frpcTemplate)
|
||||
// ===== 核心改动 =====
|
||||
// 优先使用外部模板文件 frpc.tmpl,如果不存在则使用 embed 中的模板
|
||||
var tmplContent string
|
||||
if _, err := os.Stat("frpc.tmpl"); err == nil {
|
||||
// 外部文件存在,读取它
|
||||
content, readErr := os.ReadFile("frpc.tmpl")
|
||||
if readErr == nil {
|
||||
tmplContent = string(content)
|
||||
} else {
|
||||
// 读取失败时回退到 embed
|
||||
tmplContent = frpcTemplateContent
|
||||
}
|
||||
} else {
|
||||
// 外部文件不存在,使用 embed 中的模板
|
||||
tmplContent = frpcTemplateContent
|
||||
}
|
||||
// ===== 核心改动结束 =====
|
||||
|
||||
tmpl, err := template.New("frpc").Parse(tmplContent)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析模板失败: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, data)
|
||||
if err != nil {
|
||||
if err := tmpl.Execute(&buf, data); err != nil {
|
||||
return fmt.Errorf("渲染模板失败: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile("./frpc.toml", buf.Bytes(), 0644)
|
||||
if err != nil {
|
||||
if err := os.WriteFile("./frpc.toml", buf.Bytes(), 0644); err != nil {
|
||||
return fmt.Errorf("写入配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -138,50 +158,47 @@ func StartFrpc() error {
|
||||
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查配置文件是否存在
|
||||
// 确保配置文件存在
|
||||
if _, err := os.Stat("./frpc.toml"); os.IsNotExist(err) {
|
||||
if err := GenerateFrpcConfig(); err != nil {
|
||||
return fmt.Errorf("生成配置文件失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 先检查是否已在运行
|
||||
// 检查是否已在运行
|
||||
running, _ := GetFrpcStatus()
|
||||
if running {
|
||||
return nil // 已经在运行,不重复启动
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
|
||||
// Windows 下需要隐藏窗口(可选)
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
}
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("启动 frpc 失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReloadFrpc 热加载 frpc 配置
|
||||
// ReloadFrpc 热加载 frpc 配置(仅发信号,不重复写入二进制)
|
||||
func ReloadFrpc() error {
|
||||
frpcPath, err := getFrpcPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查 frpc 是否在运行
|
||||
running, err := GetFrpcStatus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查 frpc 状态失败: %w", err)
|
||||
}
|
||||
if !running {
|
||||
// 没运行就直接启动
|
||||
return StartFrpc()
|
||||
}
|
||||
|
||||
// 执行 reload
|
||||
cmd := exec.Command(frpcPath, "reload", "-c", "./frpc.toml")
|
||||
// 确保缓存路径存在
|
||||
if cachedFrpcPath == "" {
|
||||
if _, err := getFrpcPath(); err != nil {
|
||||
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cmd := exec.Command(cachedFrpcPath, "reload", "-c", "./frpc.toml")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reload 失败: %s, %w", string(output), err)
|
||||
@@ -192,41 +209,33 @@ func ReloadFrpc() error {
|
||||
// StopFrpc 停止 frpc
|
||||
func StopFrpc() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows 用 taskkill
|
||||
cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe")
|
||||
err := cmd.Run()
|
||||
if err != nil && !strings.Contains(err.Error(), "not found") {
|
||||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") {
|
||||
return fmt.Errorf("停止 frpc 失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
// Linux/Unix 用 pkill
|
||||
}
|
||||
cmd := exec.Command("pkill", "-f", "frpc")
|
||||
err := cmd.Run()
|
||||
if err != nil && !strings.Contains(err.Error(), "no process") {
|
||||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") {
|
||||
return fmt.Errorf("停止 frpc 失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetFrpcStatus 检查 frpc 是否在运行
|
||||
func GetFrpcStatus() (bool, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows 用 tasklist 检查
|
||||
cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq frpc.exe")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return strings.Contains(string(output), "frpc.exe"), nil
|
||||
} else {
|
||||
// Linux/Unix 用 pgrep
|
||||
}
|
||||
cmd := exec.Command("pgrep", "-f", "frpc")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false, nil
|
||||
}
|
||||
return len(output) > 0, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
2026-07-23 19:03:20.982 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:03:20.991 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:03:21.150 [I] [client/service.go:328] [153a390fe71d72b9] login to server success, get run id [153a390fe71d72b9]
|
||||
2026-07-23 19:05:38.975 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:05:38.984 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:05:39.171 [I] [client/service.go:328] [7e21ed8b7758eb2e] login to server success, get run id [7e21ed8b7758eb2e]
|
||||
2026-07-23 19:08:07.129 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:08:07.142 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:08:07.302 [I] [client/service.go:328] [93745a8de846e221] login to server success, get run id [93745a8de846e221]
|
||||
2026-07-23 19:10:02.070 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:10:02.079 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:10:02.297 [I] [client/service.go:328] [a6203105fbd4c067] login to server success, get run id [a6203105fbd4c067]
|
||||
2026-07-23 19:19:21.669 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration
|
||||
2026-07-23 19:19:21.680 [I] [client/service.go:308] try to connect to server...
|
||||
2026-07-23 19:19:21.974 [I] [client/service.go:328] [5da2ae4457692b0e] login to server success, get run id [5da2ae4457692b0e]
|
||||
@@ -0,0 +1,29 @@
|
||||
serverAddr = "{{.ServerAddr}}"
|
||||
serverPort = {{.ServerPort}}
|
||||
|
||||
[auth]
|
||||
token = "{{.Token}}"
|
||||
|
||||
[log]
|
||||
to = "./frpc.log"
|
||||
level = "{{.LogLevel}}"
|
||||
maxDays = {{.LogMaxDays}}
|
||||
|
||||
[transport]
|
||||
tcpMux = {{.TcpMux}}
|
||||
tcpMuxKeepaliveInterval = {{.TcpMuxKeepalive}}
|
||||
heartbeatInterval = {{.HeartbeatInterval}}
|
||||
heartbeatTimeout = {{.HeartbeatTimeout}}
|
||||
poolCount = {{.PoolCount}}
|
||||
|
||||
[webServer]
|
||||
addr = "127.0.0.1:7400"
|
||||
|
||||
{{range .Proxies}}
|
||||
[[proxies]]
|
||||
name = "{{.Name}}"
|
||||
type = "{{.Type}}"
|
||||
localIP = "{{.LocalIP}}"
|
||||
localPort = {{.LocalPort}}
|
||||
remotePort = {{.RemotePort}}
|
||||
{{end}}
|
||||
@@ -1,18 +0,0 @@
|
||||
serverAddr = "frp.whitetop.xyz"
|
||||
serverPort = 9358
|
||||
|
||||
[auth]
|
||||
token = "Lxh10020328"
|
||||
|
||||
[log]
|
||||
to = "./frpc.log"
|
||||
level = "info"
|
||||
maxDays = 3
|
||||
|
||||
[transport]
|
||||
tcpMux = true
|
||||
tcpMuxKeepaliveInterval = 30
|
||||
heartbeatInterval = 15
|
||||
heartbeatTimeout = 70
|
||||
poolCount = 8
|
||||
|
||||
+29
-108
@@ -1,6 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frpc-console</title>
|
||||
@@ -8,17 +9,14 @@
|
||||
<!-- Vue 3 CDN -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<!-- Naive UI CDN -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/naive-ui@2/dist/styles.css"
|
||||
/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/naive-ui@2/dist/styles.css" />
|
||||
<!-- TOML 解析库(用于导入/导出) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/@iarna/toml@3.0.0/dist/toml.min.js"></script>
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 背景层:始终存在的磨砂玻璃质感 -->
|
||||
<div class="app-backdrop" :class="{ 'is-logged-in': loggedIn }"></div>
|
||||
@@ -35,21 +33,11 @@
|
||||
<div class="login-form">
|
||||
<div class="input-group">
|
||||
<label>user</label>
|
||||
<input
|
||||
v-model="loginForm.username"
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
@keydown.enter="doLogin"
|
||||
/>
|
||||
<input v-model="loginForm.username" type="text" placeholder="用户名" @keydown.enter="doLogin" />
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label>passwd</label>
|
||||
<input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
@keydown.enter="doLogin"
|
||||
/>
|
||||
<input v-model="loginForm.password" type="password" placeholder="密码" @keydown.enter="doLogin" />
|
||||
</div>
|
||||
<button class="login-btn" @click="doLogin" :disabled="loading">
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
@@ -65,9 +53,7 @@
|
||||
<div class="top-left">
|
||||
<span class="logo">🚀 frpc-console</span>
|
||||
<span class="status-dot" :class="{ active: frpcRunning }"></span>
|
||||
<span class="status-text"
|
||||
>{{ frpcRunning ? 'frpc 运行中' : 'frpc 已停止' }}</span
|
||||
>
|
||||
<span class="status-text">{{ frpcRunning ? 'frpc 运行中' : 'frpc 已停止' }}</span>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
<span class="user-name">{{ loginForm.username }}</span>
|
||||
@@ -77,18 +63,10 @@
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="tab-bar">
|
||||
<span
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'config' }"
|
||||
@click="activeTab = 'config'"
|
||||
>
|
||||
<span class="tab-item" :class="{ active: activeTab === 'config' }" @click="activeTab = 'config'">
|
||||
全局配置
|
||||
</span>
|
||||
<span
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 'proxies' }"
|
||||
@click="activeTab = 'proxies'"
|
||||
>
|
||||
<span class="tab-item" :class="{ active: activeTab === 'proxies' }" @click="activeTab = 'proxies'">
|
||||
隧道列表
|
||||
</span>
|
||||
</div>
|
||||
@@ -101,49 +79,29 @@
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">TCP Multiplexing</div>
|
||||
<div class="metric-value">
|
||||
<label class="switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="globalConfig.tcpMux"
|
||||
@change="saveConfig"
|
||||
/>
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span
|
||||
class="status-text"
|
||||
:class="{ active: globalConfig.tcpMux }"
|
||||
>
|
||||
{{ globalConfig.tcpMux ? '已启用' : '已禁用' }}
|
||||
</span>
|
||||
<span class="status-dot active"></span>
|
||||
<span class="status-text active">已启用(固定)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">连接池大小</div>
|
||||
<div class="metric-value">
|
||||
<span class="big-number"
|
||||
>{{ globalConfig.poolCount || 8 }}</span
|
||||
>
|
||||
<span class="big-number">{{ globalConfig.poolCount || 8 }}</span>
|
||||
<span class="unit">个</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">心跳间隔 / 超时</div>
|
||||
<div class="metric-value">
|
||||
<span class="digit"
|
||||
>{{ globalConfig.heartbeatInterval || 15 }}s</span
|
||||
>
|
||||
<span class="digit">{{ globalConfig.heartbeatInterval || 15 }}s</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="digit"
|
||||
>{{ globalConfig.heartbeatTimeout || 70 }}s</span
|
||||
>
|
||||
<span class="digit">{{ globalConfig.heartbeatTimeout || 70 }}s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-collapse" @click="showDetail = !showDetail">
|
||||
<span
|
||||
>{{ showDetail ? '收起全部配置 ▲' : '展开全部配置 ▼' }}</span
|
||||
>
|
||||
<span>{{ showDetail ? '收起全部配置 ▲' : '展开全部配置 ▼' }}</span>
|
||||
</div>
|
||||
<div v-show="showDetail" class="detail-panel">
|
||||
<div class="form-row">
|
||||
@@ -178,17 +136,11 @@
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Heartbeat Interval</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model="globalConfig.heartbeatInterval"
|
||||
/>
|
||||
<input type="number" v-model="globalConfig.heartbeatInterval" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Heartbeat Timeout</label>
|
||||
<input
|
||||
type="number"
|
||||
v-model="globalConfig.heartbeatTimeout"
|
||||
/>
|
||||
<input type="number" v-model="globalConfig.heartbeatTimeout" />
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>Pool Count</label>
|
||||
@@ -215,31 +167,18 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索隧道..."
|
||||
v-model="searchKeyword"
|
||||
/>
|
||||
<input class="search-input" placeholder="搜索隧道..." v-model="searchKeyword" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隧道卡片列表 -->
|
||||
<div class="proxy-list">
|
||||
<div
|
||||
v-for="p in filteredProxies"
|
||||
:key="p.id"
|
||||
class="proxy-card"
|
||||
>
|
||||
<div v-for="p in filteredProxies" :key="p.id" class="proxy-card">
|
||||
<div class="proxy-row">
|
||||
<div class="proxy-left">
|
||||
<span
|
||||
class="status-dot"
|
||||
:class="{ active: p.enabled }"
|
||||
></span>
|
||||
<span class="status-dot" :class="{ active: p.enabled }"></span>
|
||||
<span class="proxy-name">{{ p.name }}</span>
|
||||
<span class="proxy-addr"
|
||||
>{{ p.localIP }}:{{ p.localPort }}</span
|
||||
>
|
||||
<span class="proxy-addr">{{ p.localIP }}:{{ p.localPort }}</span>
|
||||
<span class="proxy-tag">{{ p.type }}</span>
|
||||
</div>
|
||||
<div class="proxy-right">
|
||||
@@ -247,26 +186,17 @@
|
||||
<button class="icon-btn" @click="openEditDialog(p)">
|
||||
✎
|
||||
</button>
|
||||
<button
|
||||
class="icon-btn danger"
|
||||
@click="deleteProxy(p.id)"
|
||||
>
|
||||
<button class="icon-btn danger" @click="deleteProxy(p.id)">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proxy-footer">
|
||||
<label class="switch small">
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="p.enabled"
|
||||
@change="toggleProxy(p)"
|
||||
/>
|
||||
<input type="checkbox" v-model="p.enabled" @change="toggleProxy(p)" />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
<span class="status-label"
|
||||
>{{ p.enabled ? '已启用' : '已禁用' }}</span
|
||||
>
|
||||
<span class="status-label">{{ p.enabled ? '已启用' : '已禁用' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredProxies.length === 0" class="empty-state">
|
||||
@@ -279,11 +209,7 @@
|
||||
</div>
|
||||
|
||||
<!-- ====== 新增/编辑弹窗 ====== -->
|
||||
<div
|
||||
v-if="dialogVisible"
|
||||
class="dialog-overlay"
|
||||
@click.self="dialogVisible = false"
|
||||
>
|
||||
<div v-if="dialogVisible" class="dialog-overlay" @click.self="dialogVisible = false">
|
||||
<div class="dialog-card">
|
||||
<h3>{{ dialogMode === 'add' ? '新增隧道' : '编辑隧道' }}</h3>
|
||||
<div class="dialog-form">
|
||||
@@ -323,16 +249,11 @@
|
||||
</div>
|
||||
|
||||
<!-- ====== 隐藏的文件输入(导入 TOML) ====== -->
|
||||
<input
|
||||
type="file"
|
||||
id="tomlFileInput"
|
||||
accept=".toml"
|
||||
style="display: none"
|
||||
@change="handleImport"
|
||||
/>
|
||||
<input type="file" id="tomlFileInput" accept=".toml" style="display: none" @change="handleImport" />
|
||||
</div>
|
||||
|
||||
<!-- 应用逻辑 -->
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user