diff --git a/api.go b/api.go index a5d76dc..44b6cdc 100644 --- a/api.go +++ b/api.go @@ -1,7 +1,8 @@ package main import ( - "log" + "bytes" + "fmt" "net/http" "strconv" @@ -9,13 +10,24 @@ import ( "golang.org/x/crypto/bcrypt" ) +// SetupRouter 配置所有路由 func SetupRouter() *gin.Engine { r := gin.Default() // 静态文件(前端) r.Static("/static", "./static") - // API 路由 + // 健康检查 + r.GET("/ping", func(c *gin.Context) { + c.String(200, "frpc-console 后端已启动 🎉") + }) + + // 前端入口 + r.GET("/", func(c *gin.Context) { + c.File("./static/index.html") + }) + + // API 路由组 api := r.Group("/api") { // 登录(无需鉴权) @@ -41,14 +53,12 @@ func SetupRouter() *gin.Engine { auth.POST("/frpc/start", startFrpcHandler) auth.POST("/frpc/stop", stopFrpcHandler) auth.GET("/frpc/status", getFrpcStatusHandler) + + // 🟢 导入 TOML(新增) + auth.POST("/import/toml", importTomlHandler) } } - // 前端入口 - r.GET("/", func(c *gin.Context) { - c.File("./static/index.html") - }) - return r } @@ -69,8 +79,7 @@ func loginHandler(c *gin.Context) { return } - err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) - if err != nil { + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"}) return } @@ -81,7 +90,11 @@ func loginHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "登录成功", "data": gin.H{"token": token}}) + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": "登录成功", + "data": gin.H{"token": token}, + }) } // ========== 全局配置 ========== @@ -101,29 +114,23 @@ func updateConfigHandler(c *gin.Context) { return } - log.Printf("收到配置更新: %+v", cfg) + // 强制 tcpMux 为 true + cfg.TcpMux = true if err := UpdateGlobalConfig(&cfg); err != nil { - log.Printf("❌ 更新数据库失败: %v", err) // 新增日志 - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()}) 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": "配置更新成功"}) } @@ -139,7 +146,11 @@ func getProxiesHandler(c *gin.Context) { } func getProxyHandler(c *gin.Context) { - id, _ := strconv.Atoi(c.Param("id")) + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"}) + return + } p, err := GetProxy(id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "隧道不存在"}) @@ -154,14 +165,13 @@ func createProxyHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) return } - p.Enabled = true // 默认启用 + p.Enabled = true if err := CreateProxy(&p); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败: " + err.Error()}) return } - // 重新生成配置并热加载 if err := generateAndReload(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()}) return @@ -171,7 +181,12 @@ func createProxyHandler(c *gin.Context) { } func updateProxyHandler(c *gin.Context) { - id, _ := strconv.Atoi(c.Param("id")) + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"}) + return + } + var p Proxy if err := c.ShouldBindJSON(&p); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) @@ -180,11 +195,10 @@ func updateProxyHandler(c *gin.Context) { p.ID = id if err := UpdateProxy(&p); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败"}) + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败: " + err.Error()}) return } - // 重新生成配置并热加载 if err := generateAndReload(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()}) return @@ -194,13 +208,17 @@ func updateProxyHandler(c *gin.Context) { } func deleteProxyHandler(c *gin.Context) { - id, _ := strconv.Atoi(c.Param("id")) - if err := DeleteProxy(id); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "删除隧道失败"}) + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"}) + return + } + + if err := DeleteProxy(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "删除隧道失败: " + err.Error()}) return } - // 重新生成配置并热加载 if err := generateAndReload(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()}) return @@ -247,6 +265,80 @@ func getFrpcStatusHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"running": running}}) } +// ========== 🟢 导入 TOML(新增) ========== +func importTomlHandler(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请选择文件"}) + return + } + + f, err := file.Open() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取文件失败"}) + return + } + defer f.Close() + + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(f); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取文件失败"}) + return + } + + parsed, err := ParseToml(buf.String()) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "解析 TOML 失败: " + err.Error()}) + return + } + + // 更新全局配置(保持当前 tcpMux 值) + cfg := parsed.ToGlobalConfig() + currentCfg, _ := GetGlobalConfig() + if currentCfg != nil { + cfg.TcpMux = currentCfg.TcpMux // 保持当前值不变 + } + cfg.TcpMux = true // 强制开启 + + if err := UpdateGlobalConfig(cfg); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()}) + return + } + + // 清空并导入隧道 + if _, err := DB.Exec("DELETE FROM proxies"); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "清空隧道失败"}) + return + } + + proxies := parsed.ToProxies() + for _, p := range proxies { + if err := CreateProxy(&p); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "导入隧道失败: " + err.Error()}) + return + } + } + + // 生成配置并热加载 + if err := GenerateFrpcConfig(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()}) + return + } + if err := ReloadFrpc(); err != nil { + // 导入成功但热加载失败,不影响整体结果 + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": fmt.Sprintf("导入成功!共 %d 条隧道,但热加载失败: %s", len(proxies), err.Error()), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "msg": fmt.Sprintf("导入成功!共 %d 条隧道", len(proxies)), + }) +} + // ========== 辅助函数 ========== func generateAndReload() error { if err := GenerateFrpcConfig(); err != nil { diff --git a/frp.go b/frp.go index ccfa53d..68fdd57 100644 --- a/frp.go +++ b/frp.go @@ -4,6 +4,7 @@ import ( "bytes" "embed" "fmt" + "log" "os" "os/exec" "runtime" @@ -182,6 +183,7 @@ func StartFrpc() error { } // ReloadFrpc 热加载 frpc 配置(仅发信号,不重复写入二进制) +// ReloadFrpc 热加载 frpc 配置,失败时自动降级为重启 func ReloadFrpc() error { running, err := GetFrpcStatus() if err != nil { @@ -191,7 +193,6 @@ func ReloadFrpc() error { return StartFrpc() } - // 确保缓存路径存在 if cachedFrpcPath == "" { if _, err := getFrpcPath(); err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) @@ -199,9 +200,16 @@ func ReloadFrpc() error { } cmd := exec.Command(cachedFrpcPath, "reload", "-c", "./frpc.toml") - output, err := cmd.CombinedOutput() + _, err = cmd.CombinedOutput() // ← 这里改了 if err != nil { - return fmt.Errorf("reload 失败: %s, %w", string(output), err) + log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frpc", err) + if stopErr := StopFrpc(); stopErr != nil { + return fmt.Errorf("停止 frpc 失败: %w", stopErr) + } + if startErr := StartFrpc(); startErr != nil { + return fmt.Errorf("启动 frpc 失败: %w", startErr) + } + return nil } return nil } diff --git a/frpc-console.db b/frpc-console.db new file mode 100644 index 0000000..ec717b8 Binary files /dev/null and b/frpc-console.db differ diff --git a/frpc.log b/frpc.log index eb21a48..130a726 100644 --- a/frpc.log +++ b/frpc.log @@ -13,3 +13,53 @@ 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] +2026-07-23 19:23:33.949 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 19:23:33.958 [I] [client/service.go:308] try to connect to server... +2026-07-23 19:23:34.135 [I] [client/service.go:328] [3d8ddea11ad24348] login to server success, get run id [3d8ddea11ad24348] +2026-07-23 19:32:51.328 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 19:32:51.339 [I] [client/service.go:308] try to connect to server... +2026-07-23 19:32:51.523 [I] [client/service.go:328] [03c218c64c950a94] login to server success, get run id [03c218c64c950a94] +2026-07-23 19:51:09.560 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 19:51:09.568 [I] [client/service.go:308] try to connect to server... +2026-07-23 19:51:09.748 [I] [client/service.go:328] [ad7003cd99cd5042] login to server success, get run id [ad7003cd99cd5042] +2026-07-23 19:51:56.516 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 19:51:56.526 [I] [client/service.go:308] try to connect to server... +2026-07-23 19:51:56.683 [I] [client/service.go:328] [74fb0125aa25f80a] login to server success, get run id [74fb0125aa25f80a] +2026-07-23 20:13:36.963 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 20:13:36.972 [I] [client/service.go:308] try to connect to server... +2026-07-23 20:13:37.156 [I] [client/service.go:328] [eccab666ed041f5d] login to server success, get run id [eccab666ed041f5d] +2026-07-23 20:14:01.389 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 20:14:01.398 [I] [client/service.go:308] try to connect to server... +2026-07-23 20:14:01.558 [I] [client/service.go:328] [ba1fe4056d350068] login to server success, get run id [ba1fe4056d350068] +2026-07-23 20:14:01.558 [I] [proxy/proxy_manager.go:180] [ba1fe4056d350068] proxy added: [1panel-1 Gitea Grafana Halo SMB SSH immich immich-APP napcat napcat-2 navidrome openlist podux sun-panel] +2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [1panel-1] start error: proxy [1panel-1] already exists +2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [SMB] start error: proxy [SMB] already exists +2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [Grafana] start error: proxy [Grafana] already exists +2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [Halo] start error: proxy [Halo] already exists +2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [Gitea] start error: proxy [Gitea] already exists +2026-07-23 20:14:01.626 [W] [client/control.go:163] [ba1fe4056d350068] [immich-APP] start error: proxy [immich-APP] already exists +2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [immich] start error: proxy [immich] already exists +2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [napcat] start error: proxy [napcat] already exists +2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [napcat-2] start error: proxy [napcat-2] already exists +2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [navidrome] start error: proxy [navidrome] already exists +2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [SSH] start error: proxy [SSH] already exists +2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [openlist] start error: proxy [openlist] already exists +2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [podux] start error: proxy [podux] already exists +2026-07-23 20:14:01.661 [W] [client/control.go:163] [ba1fe4056d350068] [sun-panel] start error: proxy [sun-panel] already exists +2026-07-23 20:14:23.803 [I] [sub/root.go:201] start frpc service for config file [./frpc.toml] with aggregated configuration +2026-07-23 20:14:23.811 [I] [client/service.go:308] try to connect to server... +2026-07-23 20:14:23.983 [I] [client/service.go:328] [83ea7405ffa3e73d] login to server success, get run id [83ea7405ffa3e73d] +2026-07-23 20:14:23.983 [I] [proxy/proxy_manager.go:180] [83ea7405ffa3e73d] proxy added: [1panel-1 Gitea Grafana Halo SMB SSH immich-APP napcat napcat-2 navidrome openlist podux sun-panel] +2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [Gitea] start error: proxy [Gitea] already exists +2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [SMB] start error: proxy [SMB] already exists +2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [Grafana] start error: proxy [Grafana] already exists +2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [SSH] start error: proxy [SSH] already exists +2026-07-23 20:14:24.051 [W] [client/control.go:163] [83ea7405ffa3e73d] [napcat-2] start error: proxy [napcat-2] already exists +2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [immich-APP] start error: proxy [immich-APP] already exists +2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [napcat] start error: proxy [napcat] already exists +2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [navidrome] start error: proxy [navidrome] already exists +2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [openlist] start error: proxy [openlist] already exists +2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [Halo] start error: proxy [Halo] already exists +2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [podux] start error: proxy [podux] already exists +2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [sun-panel] start error: proxy [sun-panel] already exists +2026-07-23 20:14:24.096 [W] [client/control.go:163] [83ea7405ffa3e73d] [1panel-1] start error: proxy [1panel-1] already exists diff --git a/frpc.toml b/frpc.toml new file mode 100644 index 0000000..1d7c4a4 --- /dev/null +++ b/frpc.toml @@ -0,0 +1,112 @@ +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 + +[webServer] +addr = "127.0.0.1:7400" + + +[[proxies]] +name = "sun-panel" +type = "tcp" +localIP = "192.168.3.16" +localPort = 3002 +remotePort = 30005 + +[[proxies]] +name = "1panel-1" +type = "tcp" +localIP = "192.168.3.16" +localPort = 10086 +remotePort = 20003 + +[[proxies]] +name = "navidrome" +type = "tcp" +localIP = "192.168.3.16" +localPort = 4533 +remotePort = 20022 + +[[proxies]] +name = "openlist" +type = "tcp" +localIP = "192.168.3.16" +localPort = 5244 +remotePort = 20005 + +[[proxies]] +name = "SSH" +type = "tcp" +localIP = "192.168.3.16" +localPort = 22 +remotePort = 20001 + +[[proxies]] +name = "SMB" +type = "tcp" +localIP = "192.168.3.16" +localPort = 445 +remotePort = 20008 + +[[proxies]] +name = "napcat" +type = "tcp" +localIP = "192.168.3.16" +localPort = 6099 +remotePort = 20007 + +[[proxies]] +name = "Gitea" +type = "tcp" +localIP = "192.168.3.16" +localPort = 3800 +remotePort = 20017 + +[[proxies]] +name = "immich-APP" +type = "tcp" +localIP = "192.168.3.16" +localPort = 2283 +remotePort = 20020 + +[[proxies]] +name = "Halo" +type = "tcp" +localIP = "192.168.3.16" +localPort = 65535 +remotePort = 65535 + +[[proxies]] +name = "napcat-2" +type = "tcp" +localIP = "192.168.3.16" +localPort = 6100 +remotePort = 20012 + +[[proxies]] +name = "podux" +type = "tcp" +localIP = "192.168.3.16" +localPort = 8090 +remotePort = 20010 + +[[proxies]] +name = "Grafana" +type = "tcp" +localIP = "192.168.3.16" +localPort = 3030 +remotePort = 20014 diff --git a/static/app.js b/static/app.js index dd9060c..15481e0 100644 --- a/static/app.js +++ b/static/app.js @@ -17,7 +17,7 @@ const app = createApp({ const token = ref(""); // ===== Tab ===== - const activeTab = ref("config"); + const activeTab = ref("proxies"); // ===== 全局配置 ===== const globalConfig = reactive({ @@ -296,51 +296,30 @@ const app = createApp({ const handleImport = async (e) => { const file = e.target.files[0]; if (!file) return; - try { - const text = await file.text(); - const parsed = TOML.parse(text); - if (!parsed.proxies || !Array.isArray(parsed.proxies)) { - alert("无效的 TOML 文件:缺少 proxies 数组"); - return; - } - // 过滤出有效的隧道条目 - const list = parsed.proxies.filter( - (p) => p.name && p.localIP && p.localPort && p.remotePort, - ); - if (list.length === 0) { - alert("未解析到有效的隧道条目"); - return; - } - if ( - !confirm(`解析到 ${list.length} 条隧道,将覆盖当前配置,确认导入?`) - ) - return; - // 批量导入:先清空,再逐条插入(后端未提供批量接口,用循环) - // 简单做法:先删除所有,再逐个创建 - for (const p of proxies.value) { - await apiFetch("/api/proxy/" + p.id, { method: "DELETE" }); + const formData = new FormData(); + formData.append('file', file); + + try { + const res = await fetch('/api/import/toml', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token.value, + // 不要设置 Content-Type,浏览器会自动加上 boundary + }, + body: formData, + }); + const data = await res.json(); + if (data.code === 0) { + alert(data.msg); + await loadAllData(); + } else { + alert('导入失败: ' + data.msg); } - for (const p of list) { - await apiFetch("/api/proxy", { - method: "POST", - body: JSON.stringify({ - name: p.name, - type: p.type || "tcp", - localIP: p.localIP, - localPort: p.localPort, - remotePort: p.remotePort, - enabled: p.enabled !== undefined ? p.enabled : true, - }), - }); - } - await loadProxies(); - await loadFrpcStatus(); - alert("导入成功!"); } catch (err) { - alert("解析 TOML 失败: " + err.message); + alert('网络错误: ' + err.message); } - e.target.value = ""; + e.target.value = ''; }; // ===== 导出 TOML ===== diff --git a/static/index.html b/static/index.html index 14db023..5a8b286 100644 --- a/static/index.html +++ b/static/index.html @@ -63,12 +63,12 @@
diff --git a/toml_parser.go b/toml_parser.go new file mode 100644 index 0000000..f39f44f --- /dev/null +++ b/toml_parser.go @@ -0,0 +1,181 @@ +package main + +import ( + "fmt" + "strconv" + "strings" +) + +// FrpcToml 对应 frpc.toml 的完整结构 +type FrpcToml struct { + ServerAddr string `json:"serverAddr"` + ServerPort int `json:"serverPort"` + Auth struct { + Token string `json:"token"` + } `json:"auth"` + Log struct { + To string `json:"to"` + Level string `json:"level"` + MaxDays int `json:"maxDays"` + } `json:"log"` + Transport struct { + TcpMux bool `json:"tcpMux"` + TcpMuxKeepalive int `json:"tcpMuxKeepalive"` + HeartbeatInterval int `json:"heartbeatInterval"` + HeartbeatTimeout int `json:"heartbeatTimeout"` + PoolCount int `json:"poolCount"` + } `json:"transport"` + Proxies []TomlProxy `json:"proxies"` +} + +// TomlProxy 对应 [[proxies]] 条目 +type TomlProxy struct { + Name string `json:"name"` + Type string `json:"type"` + LocalIP string `json:"localIP"` + LocalPort int `json:"localPort"` + RemotePort int `json:"remotePort"` + Enabled bool `json:"enabled"` // 导入时默认 true +} + +// ParseToml 解析 frpc.toml 内容 +func ParseToml(content string) (*FrpcToml, error) { + lines := strings.Split(content, "\n") + result := &FrpcToml{ + Proxies: []TomlProxy{}, + Transport: struct { + TcpMux bool `json:"tcpMux"` + TcpMuxKeepalive int `json:"tcpMuxKeepalive"` + HeartbeatInterval int `json:"heartbeatInterval"` + HeartbeatTimeout int `json:"heartbeatTimeout"` + PoolCount int `json:"poolCount"` + }{ + TcpMux: true, // 默认值 + TcpMuxKeepalive: 30, + HeartbeatInterval: 15, + HeartbeatTimeout: 70, + PoolCount: 8, + }, + } + + // 简单状态机解析 + var currentProxy *TomlProxy + inProxies := false + + for _, rawLine := range lines { + line := strings.TrimSpace(rawLine) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // 检测 [[proxies]] 段开始 + if strings.HasPrefix(line, "[[proxies]]") { + inProxies = true + currentProxy = &TomlProxy{ + Type: "tcp", + Enabled: true, + } + result.Proxies = append(result.Proxies, *currentProxy) + // 注意:由于值传递,需要取最后一个元素的指针 + currentProxy = &result.Proxies[len(result.Proxies)-1] + continue + } + + // 检测其他段头(忽略,我们的解析器只关心具体键值对) + if strings.HasPrefix(line, "[") { + inProxies = false + currentProxy = nil + continue + } + + // 解析键值对 + if strings.Contains(line, "=") { + parts := strings.SplitN(line, "=", 2) + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + // 去掉引号 + value = strings.Trim(value, `"`) + + if inProxies && currentProxy != nil { + // 解析隧道字段 + switch key { + case "name": + currentProxy.Name = value + case "type": + currentProxy.Type = value + case "localIP": + currentProxy.LocalIP = value + case "localPort": + currentProxy.LocalPort, _ = strconv.Atoi(value) + case "remotePort": + currentProxy.RemotePort, _ = strconv.Atoi(value) + } + } else { + // 解析全局字段 + switch key { + case "serverAddr": + result.ServerAddr = value + case "serverPort": + result.ServerPort, _ = strconv.Atoi(value) + case "token": + result.Auth.Token = value + case "level": + result.Log.Level = value + case "maxDays": + result.Log.MaxDays, _ = strconv.Atoi(value) + case "tcpMux": + result.Transport.TcpMux = value == "true" + case "tcpMuxKeepaliveInterval": + result.Transport.TcpMuxKeepalive, _ = strconv.Atoi(value) + case "heartbeatInterval": + result.Transport.HeartbeatInterval, _ = strconv.Atoi(value) + case "heartbeatTimeout": + result.Transport.HeartbeatTimeout, _ = strconv.Atoi(value) + case "poolCount": + result.Transport.PoolCount, _ = strconv.Atoi(value) + } + } + } + } + + if result.ServerAddr == "" { + return nil, fmt.Errorf("未找到 serverAddr 字段") + } + if len(result.Proxies) == 0 { + return nil, fmt.Errorf("未找到任何隧道条目") + } + + return result, nil +} + +// ToGlobalConfig 将解析结果转换为 GlobalConfig +func (f *FrpcToml) ToGlobalConfig() *GlobalConfig { + return &GlobalConfig{ + ServerAddr: f.ServerAddr, + ServerPort: f.ServerPort, + Token: f.Auth.Token, + LogLevel: f.Log.Level, + LogMaxDays: f.Log.MaxDays, + TcpMux: f.Transport.TcpMux, + TcpMuxKeepalive: f.Transport.TcpMuxKeepalive, + HeartbeatInterval: f.Transport.HeartbeatInterval, + HeartbeatTimeout: f.Transport.HeartbeatTimeout, + PoolCount: f.Transport.PoolCount, + } +} + +// ToProxies 将解析结果转换为 Proxy 列表 +func (f *FrpcToml) ToProxies() []Proxy { + var proxies []Proxy + for _, p := range f.Proxies { + proxies = append(proxies, Proxy{ + Name: p.Name, + Type: p.Type, + LocalIP: p.LocalIP, + LocalPort: p.LocalPort, + RemotePort: p.RemotePort, + Enabled: true, // 导入默认启用 + }) + } + return proxies +}