From d7c27c513c52f89f7accef66cd4201a22544aaa1 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Fri, 7 Aug 2026 21:18:28 +0800 Subject: [PATCH] =?UTF-8?q?2.6-Preview=E6=9C=80=E5=90=8E=E4=B8=80=E6=89=B9?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=B8=8A=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 14 +- go.sum | 58 ++++---- internal/api/handler.go | 5 + internal/api/static/app.js | 1 + internal/api/static/index.html | 4 + internal/db/db.go | 235 +++++++++++++++++++++++++-------- internal/db/models.go | 1 + internal/db/repository.go | 16 ++- internal/db/schema.go | 16 +++ internal/frp/config.go | 8 ++ internal/frp/frpc.tmpl | 2 +- internal/process/manager.go | 84 ++++++++---- 12 files changed, 322 insertions(+), 122 deletions(-) diff --git a/go.mod b/go.mod index 3592153..864447a 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.1 golang.org/x/crypto v0.54.0 golang.org/x/sys v0.47.0 - modernc.org/sqlite v1.29.9 + modernc.org/sqlite v1.54.0 ) require ( @@ -24,14 +24,13 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.1 // indirect @@ -43,10 +42,7 @@ require ( golang.org/x/net v0.56.0 // indirect golang.org/x/text v0.40.0 // indirect google.golang.org/protobuf v1.36.10 // indirect - modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect - modernc.org/libc v1.49.3 // indirect - modernc.org/mathutil v1.6.0 // indirect - modernc.org/memory v1.8.0 // indirect - modernc.org/strutil v1.2.0 // indirect - modernc.org/token v1.1.0 // indirect + modernc.org/libc v1.74.1 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index ef5afd9..b73ae87 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= -github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -53,8 +53,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= -github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -107,29 +107,31 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= -modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= -modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI= -modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= -modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= -modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= -modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= -modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= -modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= -modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= -modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.29.9 h1:9RhNMklxJs+1596GNuAX+O/6040bvOwacTxuFcRuQow= -modernc.org/sqlite v1.29.9/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= -modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= +modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= +modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI= +modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ= +modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog= +modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/api/handler.go b/internal/api/handler.go index b348c29..52727b3 100644 --- a/internal/api/handler.go +++ b/internal/api/handler.go @@ -188,6 +188,11 @@ func UpdateConfigHandler(c *gin.Context) { } cfg.TcpMux = true + // AdminPort 已从 JSON 绑定,直接使用 + if cfg.AdminPort <= 0 { + cfg.AdminPort = 7400 // 如果前端没传,默认 7400 + } + if err := db.UpdateGlobalConfig(&cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()}) return diff --git a/internal/api/static/app.js b/internal/api/static/app.js index 112e7fe..c00f851 100644 --- a/internal/api/static/app.js +++ b/internal/api/static/app.js @@ -95,6 +95,7 @@ const defaultConfig = { serverAddr: "frp.example.com", serverPort: 7000, token: "CHANGE_ME", + adminPort: 7400, logLevel: "info", logMaxDays: 3, tcpMux: true, diff --git a/internal/api/static/index.html b/internal/api/static/index.html index 9649269..278aa98 100644 --- a/internal/api/static/index.html +++ b/internal/api/static/index.html @@ -201,6 +201,10 @@ +
+ + +
diff --git a/internal/db/db.go b/internal/db/db.go index f3ddca8..4842684 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -17,7 +17,7 @@ import ( var DB *sql.DB -const SchemaVersion = "v2" +const SchemaVersion = "v3" // ================================================================ // 初始化 @@ -52,6 +52,7 @@ func InitDB() error { } func createTables() error { + // users 表 _, err := DB.Exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -65,12 +66,14 @@ func createTables() error { return err } + // global_config 表 _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS global_config ( id INTEGER PRIMARY KEY CHECK (id = 1), server_addr TEXT NOT NULL DEFAULT 'frp.example.com', server_port INTEGER NOT NULL DEFAULT 7000, token TEXT NOT NULL DEFAULT 'CHANGE_ME', + admin_port INTEGER NOT NULL DEFAULT 7400, log_level TEXT NOT NULL DEFAULT 'info', log_max_days INTEGER NOT NULL DEFAULT 3, tcp_mux INTEGER NOT NULL DEFAULT 1, @@ -86,6 +89,7 @@ func createTables() error { return err } + // proxies 表 _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS proxies ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -103,6 +107,7 @@ func createTables() error { return err } + // app_config 表 _, err = DB.Exec(` CREATE TABLE IF NOT EXISTS app_config ( key TEXT PRIMARY KEY, @@ -114,15 +119,16 @@ func createTables() error { return err } + // 初始化 global_config 默认值(新数据库) 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, + id, server_addr, server_port, token, admin_port, log_level, log_max_days, tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count, wire_protocol_v2 - ) VALUES (1, 'frp.example.com', 7000, 'CHANGE_ME', 'info', 3, 1, 30, 15, 70, 8, 0) + ) VALUES (1, 'frp.example.com', 7000, 'CHANGE_ME', 7400, 'info', 3, 1, 30, 15, 70, 8, 0) `) if err != nil { return err @@ -134,43 +140,113 @@ func createTables() error { } // ================================================================ -// JWT 密钥管理 +// v2 → v3 重型迁移:global_config 表添加 admin_port // ================================================================ -func ensureJwtSecret() error { - var value string - err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'jwt_secret'").Scan(&value) - if err == nil && value != "" { +func migrateGlobalConfigToV3() error { + log.Println(" 开始 global_config 表迁移 (v2→v3)") + + // 1. 检查 admin_port 列是否已存在 + var hasAdminPort bool + rows, err := DB.Query("PRAGMA table_info(global_config)") + if err != nil { + return fmt.Errorf("查询表结构失败: %w", err) + } + defer rows.Close() + + for rows.Next() { + var cid int + var name, ctype string + var notnull, pk int + var dflt sql.NullString + if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil { + return err + } + if name == "admin_port" { + hasAdminPort = true + break + } + } + + if hasAdminPort { + log.Println(" ✅ admin_port 列已存在,跳过迁移") return nil } - bytes := make([]byte, 32) - if _, err := rand.Read(bytes); err != nil { - return err - } - secret := hex.EncodeToString(bytes) + log.Println(" 创建 global_config_new 表...") _, err = DB.Exec(` - INSERT INTO app_config (key, value) VALUES ('jwt_secret', ?) - `, secret) + CREATE TABLE global_config_new ( + id INTEGER PRIMARY KEY CHECK (id = 1), + server_addr TEXT NOT NULL DEFAULT 'frp.example.com', + server_port INTEGER NOT NULL DEFAULT 7000, + token TEXT NOT NULL DEFAULT 'CHANGE_ME', + admin_port INTEGER NOT NULL DEFAULT 7400, + log_level TEXT NOT NULL DEFAULT 'info', + log_max_days INTEGER NOT NULL DEFAULT 3, + tcp_mux INTEGER NOT NULL DEFAULT 1, + tcp_mux_keepalive INTEGER NOT NULL DEFAULT 30, + heartbeat_interval INTEGER NOT NULL DEFAULT 15, + heartbeat_timeout INTEGER NOT NULL DEFAULT 70, + pool_count INTEGER NOT NULL DEFAULT 8, + wire_protocol_v2 INTEGER NOT NULL DEFAULT 0, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) if err != nil { - return err + return fmt.Errorf("创建 global_config_new 表失败: %w", err) } - log.Printf("✅ JWT 密钥已生成") + + log.Println(" 迁移数据 (admin_port = 7400)...") + + _, err = DB.Exec(` + INSERT INTO global_config_new ( + id, server_addr, server_port, token, admin_port, + log_level, log_max_days, tcp_mux, tcp_mux_keepalive, + heartbeat_interval, heartbeat_timeout, pool_count, + wire_protocol_v2, updated_at + ) + SELECT + id, server_addr, server_port, token, 7400, + log_level, log_max_days, tcp_mux, tcp_mux_keepalive, + heartbeat_interval, heartbeat_timeout, pool_count, + wire_protocol_v2, updated_at + FROM global_config + `) + if err != nil { + return fmt.Errorf("复制数据失败: %w", err) + } + + var oldCount, newCount int + DB.QueryRow("SELECT COUNT(*) FROM global_config").Scan(&oldCount) + DB.QueryRow("SELECT COUNT(*) FROM global_config_new").Scan(&newCount) + + if oldCount != newCount { + return fmt.Errorf("数据迁移不完整: 旧表 %d 行,新表 %d 行", oldCount, newCount) + } + log.Printf(" 数据迁移验证通过: %d 行", newCount) + + log.Println(" 交换表名...") + + if _, err := DB.Exec("ALTER TABLE global_config RENAME TO global_config_old"); err != nil { + return fmt.Errorf("重命名旧表失败: %w", err) + } + + if _, err := DB.Exec("ALTER TABLE global_config_new RENAME TO global_config"); err != nil { + DB.Exec("ALTER TABLE global_config_old RENAME TO global_config") + return fmt.Errorf("重命名新表失败: %w", err) + } + + if _, err := DB.Exec("DROP TABLE global_config_old"); err != nil { + log.Printf("⚠️ 删除旧表失败(不影响使用): %v", err) + } + + log.Println(" ✅ global_config 表迁移完成") return nil } -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 -} - // ================================================================ -// 迁移引擎 +// Schema 版本管理 // ================================================================ func getCurrentSchemaVersion() string { @@ -249,6 +325,10 @@ func restoreDatabase(backupPath string) error { return nil } +// ================================================================ +// runMigrations - 核心迁移入口 +// ================================================================ + func runMigrations() error { currentVer := getCurrentSchemaVersion() targetVer := SchemaVersion @@ -276,33 +356,37 @@ func runMigrations() error { log.Printf("📦 备份文件: %s", backupPath) } - currentSchema := getSchemaDef(currentVer) - targetSchema := getSchemaDef(targetVer) - - if targetSchema == nil { - return fmt.Errorf("目标 Schema 版本 %s 未定义", targetVer) + // ---- 阶段1: v1 → v2(proxies 表迁移) ---- + if currentVer == "v1" { + oldDef := getSchemaDef("v1") + newDef := getSchemaDef("v2") + if oldDef == nil || newDef == nil { + return fmt.Errorf("v1 或 v2 schema 定义不存在") + } + if !schemaVersionsEqual(oldDef, newDef) { + log.Println(" 阶段1: v1→v2 重型迁移(proxies 表结构变更)") + if err := heavyMigration(oldDef, newDef); err != nil { + if backupPath != "" { + restoreDatabase(backupPath) + } + return fmt.Errorf("v1→v2 迁移失败: %w", err) + } + } else { + log.Println(" 阶段1: v1→v2 轻量迁移(proxies 表结构无变更)") + } + currentVer = "v2" } - if currentSchema == nil || schemaVersionsEqual(currentSchema, targetSchema) { - log.Println(" 迁移类型: 轻量复制(Schema 无变更)") - var userCount int - err := DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&userCount) - if err != nil || userCount == 0 { - log.Println(" 数据库为空或无效,跳过迁移,直接初始化") - return nil - } - log.Println(" 数据库有效,继续使用") - } else { - log.Println(" 迁移类型: 重型迁移(Schema 有变更,新建表 + 搬数据)") - if err := heavyMigration(currentSchema, targetSchema); err != nil { + // ---- 阶段2: v2 → v3(global_config 表新增 admin_port) ---- + if currentVer == "v2" { + log.Println(" 阶段2: v2→v3 重型迁移(global_config 表新增 admin_port)") + if err := migrateGlobalConfigToV3(); err != nil { if backupPath != "" { - log.Printf("❌ 迁移失败,尝试恢复备份: %s", backupPath) - if restoreErr := restoreDatabase(backupPath); restoreErr != nil { - log.Printf("⚠️ 恢复备份失败: %v", restoreErr) - } + restoreDatabase(backupPath) } - return fmt.Errorf("重型迁移失败: %w", err) + return fmt.Errorf("v2→v3 迁移失败: %w", err) } + currentVer = "v3" } if err := setSchemaVersion(targetVer); err != nil { @@ -313,9 +397,13 @@ func runMigrations() error { return nil } +// ================================================================ +// 重型迁移引擎(用于 proxies 表) +// ================================================================ + func heavyMigration(oldDef, newDef *SchemaVersionDef) error { if oldDef == nil { - return fmt.Errorf("旧 Schema 定义为空,无法执行重型迁移") + return fmt.Errorf("旧 Schema 定义为空") } oldTable := oldDef.TableName @@ -364,7 +452,14 @@ func buildCreateTableSQL(tableName string, def *SchemaVersionDef) string { var cols []string var primaryKey string - for name, col := range def.Columns { + names := make([]string, 0, len(def.Columns)) + for name := range def.Columns { + names = append(names, name) + } + sort.Strings(names) + + for _, name := range names { + col := def.Columns[name] parts := []string{name, col.Type} if col.NotNull { parts = append(parts, "NOT NULL") @@ -387,7 +482,7 @@ func buildCreateTableSQL(tableName string, def *SchemaVersionDef) string { } func buildInsertSQL(oldTable, newTable string, oldDef, newDef *SchemaVersionDef) (string, error) { - var newCols []string + newCols := make([]string, 0, len(newDef.Columns)) for name := range newDef.Columns { newCols = append(newCols, name) } @@ -422,3 +517,39 @@ func buildInsertSQL(oldTable, newTable string, oldDef, newDef *SchemaVersionDef) oldTable, ), nil } + +// ================================================================ +// JWT 密钥管理 +// ================================================================ + +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 + } + + 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 +} + +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 +} diff --git a/internal/db/models.go b/internal/db/models.go index d7e7322..a10277f 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -6,6 +6,7 @@ type GlobalConfig struct { ServerAddr string `json:"serverAddr"` ServerPort int `json:"serverPort"` Token string `json:"token"` + AdminPort int `json:"adminPort"` // ← 新增 LogLevel string `json:"logLevel"` LogMaxDays int `json:"logMaxDays"` TcpMux bool `json:"tcpMux"` diff --git a/internal/db/repository.go b/internal/db/repository.go index 12b2af9..5d2c15c 100644 --- a/internal/db/repository.go +++ b/internal/db/repository.go @@ -4,16 +4,20 @@ package db // 全局配置 // ================================================================ +// internal/db/repository.go + func GetGlobalConfig() (*GlobalConfig, error) { var cfg GlobalConfig err := DB.QueryRow(` - SELECT id, server_addr, server_port, token, log_level, log_max_days, + SELECT id, server_addr, server_port, token, admin_port, + log_level, log_max_days, tcp_mux, tcp_mux_keepalive, heartbeat_interval, heartbeat_timeout, pool_count, wire_protocol_v2 FROM global_config WHERE id = 1 `).Scan( - &cfg.ID, &cfg.ServerAddr, &cfg.ServerPort, &cfg.Token, - &cfg.LogLevel, &cfg.LogMaxDays, &cfg.TcpMux, &cfg.TcpMuxKeepalive, + &cfg.ID, &cfg.ServerAddr, &cfg.ServerPort, &cfg.Token, &cfg.AdminPort, + &cfg.LogLevel, &cfg.LogMaxDays, + &cfg.TcpMux, &cfg.TcpMuxKeepalive, &cfg.HeartbeatInterval, &cfg.HeartbeatTimeout, &cfg.PoolCount, &cfg.WireProtocolV2, ) @@ -27,13 +31,15 @@ func GetGlobalConfig() (*GlobalConfig, error) { func UpdateGlobalConfig(cfg *GlobalConfig) error { _, err := DB.Exec(` UPDATE global_config SET - server_addr = ?, server_port = ?, token = ?, log_level = ?, log_max_days = ?, + server_addr = ?, server_port = ?, token = ?, admin_port = ?, + log_level = ?, log_max_days = ?, tcp_mux = 1, tcp_mux_keepalive = ?, heartbeat_interval = ?, heartbeat_timeout = ?, pool_count = ?, wire_protocol_v2 = ?, updated_at = CURRENT_TIMESTAMP WHERE id = 1 - `, cfg.ServerAddr, cfg.ServerPort, cfg.Token, cfg.LogLevel, cfg.LogMaxDays, + `, cfg.ServerAddr, cfg.ServerPort, cfg.Token, cfg.AdminPort, + cfg.LogLevel, cfg.LogMaxDays, cfg.TcpMuxKeepalive, cfg.HeartbeatInterval, cfg.HeartbeatTimeout, cfg.PoolCount, cfg.WireProtocolV2) return err diff --git a/internal/db/schema.go b/internal/db/schema.go index bf80ac1..57c68a3 100644 --- a/internal/db/schema.go +++ b/internal/db/schema.go @@ -50,6 +50,22 @@ var schemaHistory = []SchemaVersionDef{ "updated_at": {Type: "DATETIME", Default: "CURRENT_TIMESTAMP"}, }, }, + + { + Version: "v3", + TableName: "proxies", + Columns: map[string]ColumnDef{ + "id": {Type: "INTEGER", Primary: true}, + "name": {Type: "TEXT", NotNull: true}, + "type": {Type: "TEXT", NotNull: true, Default: "'tcp'"}, + "local_ip": {Type: "TEXT", NotNull: true}, + "local_port": {Type: "INTEGER", NotNull: true}, + "remote_port": {Type: "INTEGER", NotNull: true}, + "enabled": {Type: "INTEGER", NotNull: true, Default: "1"}, + "created_at": {Type: "DATETIME", Default: "CURRENT_TIMESTAMP"}, + "updated_at": {Type: "DATETIME", Default: "CURRENT_TIMESTAMP"}, + }, + }, } func getSchemaDef(version string) *SchemaVersionDef { diff --git a/internal/frp/config.go b/internal/frp/config.go index a9680c1..89839ff 100644 --- a/internal/frp/config.go +++ b/internal/frp/config.go @@ -19,6 +19,14 @@ type ConfigData struct { WireProtocolLine string } +// ConfigData 中已包含 *db.GlobalConfig,AdminPort 会自动传递到模板 +// 不需要额外修改,但模板 frpc.tmpl 需要支持 admin_port 输出 + +// 在 frpc.tmpl 中添加: +// {{- if .AdminPort }} +// admin_port = {{ .AdminPort }} +// {{- end }} + // GenerateConfig 生成 frpc.toml 配置文件 func GenerateConfig() error { cfg, err := db.GetGlobalConfig() diff --git a/internal/frp/frpc.tmpl b/internal/frp/frpc.tmpl index cc2e575..910ff3f 100644 --- a/internal/frp/frpc.tmpl +++ b/internal/frp/frpc.tmpl @@ -20,7 +20,7 @@ poolCount = {{.PoolCount}} {{end}} [webServer] -addr = "127.0.0.1:7400" +addr = "127.0.0.1:{{.AdminPort}}" {{range .Proxies}} [[proxies]] diff --git a/internal/process/manager.go b/internal/process/manager.go index 6f9b2fe..dbe7d3a 100644 --- a/internal/process/manager.go +++ b/internal/process/manager.go @@ -1,4 +1,4 @@ -// process/manager.go +// internal/process/manager.go // frpc-console 进程管理模块 // 2.6-preview: 端口检测 + 单实例锁定 + 状态自述 @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "io" + "log" "net" "net/http" "os" @@ -41,7 +42,6 @@ const ( // 数据结构 // ================================================================ -// PortStatus 端口检测结果 type PortStatus struct { Port int `json:"port"` Occupied bool `json:"occupied"` @@ -50,17 +50,15 @@ type PortStatus struct { ProcessCmd string `json:"process_cmd,omitempty"` } -// ProcessStatus frpc 进程状态 type ProcessStatus struct { - State string `json:"state"` // "running" | "stopped" | "unknown" | "conflict" - PID int `json:"pid"` // 进程 PID (如果运行中) - Port int `json:"port"` // 监听的端口 + State string `json:"state"` + PID int `json:"pid"` + Port int `json:"port"` Uptime string `json:"uptime,omitempty"` Version string `json:"version,omitempty"` Error string `json:"error,omitempty"` } -// FRPCStatus 来自 frpc admin API 的状态响应 type FRPCStatus struct { Version string `json:"version"` RunID string `json:"run_id"` @@ -91,7 +89,6 @@ var ( globalMu sync.Mutex ) -// NewManager 创建进程管理器 func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager { return &ProcessManager{ dataDir: dataDir, @@ -101,48 +98,62 @@ func NewManager(dataDir, configPath, frpcBinPath string) *ProcessManager { } } -// SetGlobalManager 设置全局管理器 func SetGlobalManager(pm *ProcessManager) { globalMu.Lock() defer globalMu.Unlock() globalManager = pm } -// GetGlobalManager 获取全局管理器 func GetGlobalManager() *ProcessManager { globalMu.Lock() defer globalMu.Unlock() return globalManager } -// AdminPort 获取 admin_port func (pm *ProcessManager) AdminPort() int { return pm.adminPort } // ================================================================ -// 配置读取 +// 配置读取 (带调试日志) // ================================================================ -// LoadConfig 从 frpc.toml 读取 admin_port 配置 -// 兼容 frp 0.52.0 前后的配置格式 func (pm *ProcessManager) LoadConfig() error { content, err := os.ReadFile(pm.configPath) if err != nil { return fmt.Errorf("读取配置文件失败: %w", err) } + log.Printf("[DEBUG] LoadConfig 读取到文件,长度: %d 字节", len(content)) + + preview := string(content) + if len(preview) > 600 { + preview = preview[:600] + "\n... (截断)" + } + log.Printf("[DEBUG] 文件内容预览:\n%s", preview) + if port := extractIntValue(string(content), "admin_port"); port > 0 { pm.adminPort = port + log.Printf("[DEBUG] ✅ 从 admin_port 解析到端口: %d", port) return nil } + log.Printf("[DEBUG] ❌ admin_port 未找到") if port := extractIntValueFromSection(string(content), "webServer", "port"); port > 0 { pm.adminPort = port + log.Printf("[DEBUG] ✅ 从 webServer.port 解析到端口: %d", port) return nil } + log.Printf("[DEBUG] ❌ webServer.port 未找到") - return fmt.Errorf("未找到 admin_port 或 webServer.port 配置") + if port := extractPortFromAddrSection(string(content), "webServer", "addr"); port > 0 { + pm.adminPort = port + log.Printf("[DEBUG] ✅ 从 webServer.addr 解析到端口: %d", port) + return nil + } + log.Printf("[DEBUG] ❌ webServer.addr 未找到或解析失败") + + return fmt.Errorf("未找到 admin_port 或 webServer.port/addr 配置") } func extractIntValue(content, key string) int { @@ -169,7 +180,8 @@ func extractIntValueFromSection(content, section, key string) int { for _, line := range lines { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") { - inSection = strings.Trim(trimmed, "[]") == section + sectionName := strings.TrimSpace(strings.Trim(trimmed, "[]")) + inSection = strings.EqualFold(sectionName, section) continue } if inSection && strings.HasPrefix(trimmed, key) { @@ -186,14 +198,40 @@ func extractIntValueFromSection(content, section, key string) int { return 0 } +func extractPortFromAddrSection(content, section, key string) int { + lines := strings.Split(content, "\n") + inSection := false + for _, line := range lines { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") { + sectionName := strings.TrimSpace(strings.Trim(trimmed, "[]")) + inSection = strings.EqualFold(sectionName, section) + continue + } + if inSection && strings.HasPrefix(trimmed, key) { + parts := strings.SplitN(trimmed, "=", 2) + if len(parts) == 2 { + val := strings.TrimSpace(parts[1]) + val = strings.Trim(val, `"`) + if idx := strings.LastIndex(val, ":"); idx != -1 { + portStr := val[idx+1:] + if port, err := strconv.Atoi(portStr); err == nil && port > 0 { + return port + } + } + } + } + } + return 0 +} + // ================================================================ // 端口检测 // ================================================================ -// CheckPort 检测端口是否被占用 func (pm *ProcessManager) CheckPort() (bool, error) { if pm.adminPort <= 0 { - return false, fmt.Errorf("admin_port 未配置") + return false, fmt.Errorf("admin_port 未配置 (当前值: %d)", pm.adminPort) } conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", pm.adminPort), PortCheckTimeout) if err != nil { @@ -203,7 +241,6 @@ func (pm *ProcessManager) CheckPort() (bool, error) { return true, nil } -// GetPortStatus 获取端口完整状态 (占用 + PID + 进程类型) func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) { status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false} occupied, err := pm.CheckPort() @@ -389,11 +426,10 @@ func (pm *ProcessManager) deletePIDFile() error { // 状态查询 // ================================================================ -// Status 获取 frpc 进程实时状态 func (pm *ProcessManager) Status() (*ProcessStatus, error) { status := &ProcessStatus{State: "unknown", PID: 0, Port: pm.adminPort} if pm.adminPort <= 0 { - status.Error = "admin_port 未配置" + status.Error = fmt.Sprintf("admin_port 未配置 (当前值: %d)", pm.adminPort) return status, nil } portStatus, err := pm.GetPortStatus() @@ -460,7 +496,6 @@ func (pm *ProcessManager) getFRPCStatus(pid int) *FRPCStatus { // 操作执行 (Start / Stop / Restart) // ================================================================ -// Start 启动 frpc (幂等) func (pm *ProcessManager) Start(ctx context.Context) error { if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) @@ -483,10 +518,7 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error { } cmd := exec.CommandContext(ctx, pm.frpcBinPath, "-c", pm.configPath) - - // 设置进程属性 (平台相关) setProcessAttributes(cmd) - cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -508,7 +540,6 @@ func (pm *ProcessManager) startLocked(ctx context.Context) error { return nil } -// Stop 停止 frpc (幂等) func (pm *ProcessManager) Stop(ctx context.Context) error { if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err) @@ -565,7 +596,6 @@ func (pm *ProcessManager) stopLocked(ctx context.Context) error { return nil } -// Restart 重启 frpc (原子操作) func (pm *ProcessManager) Restart(ctx context.Context) error { if err := pm.Lock(); err != nil { return fmt.Errorf("获取锁失败: %w", err)