From 9118972a7a08b6ca15e81ebdbd21127a41f5e55b Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Thu, 23 Jul 2026 12:41:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E6=A1=86=E6=9E=B6=E5=85=88?= =?UTF-8?q?=E8=A1=8C=EF=BC=8C=E7=BC=96=E8=AF=91=E5=99=A8=E5=B7=B2=E6=97=A0?= =?UTF-8?q?=E6=8A=A5=E9=94=99=EF=BC=8C=E9=A1=B9=E7=9B=AE=E6=AD=A3=E5=BC=8F?= =?UTF-8?q?=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 0 api.go | 247 +++++++++++++++++++++++++++++++++++++++ auth.go | 138 ++++++++++++++++++++++ db.go | 231 ++++++++++++++++++++++++++++++++++++ frp.go | 146 +++++++++++++++++++++++ go.mod | 48 ++++++++ go.sum | 137 ++++++++++++++++++++++ main.go | 35 ++++++ static/app.js | 0 static/index.html | 22 ++++ static/style.css | 0 templates/frpc.toml.tmpl | 0 12 files changed, 1004 insertions(+) create mode 100644 Dockerfile create mode 100644 api.go create mode 100644 auth.go create mode 100644 db.go create mode 100644 frp.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 static/app.js create mode 100644 static/index.html create mode 100644 static/style.css create mode 100644 templates/frpc.toml.tmpl diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/api.go b/api.go new file mode 100644 index 0000000..549ef22 --- /dev/null +++ b/api.go @@ -0,0 +1,247 @@ +package main + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" +) + +func SetupRouter() *gin.Engine { + r := gin.Default() + + // 静态文件(前端) + r.Static("/static", "./static") + + // API 路由 + api := r.Group("/api") + { + // 登录(无需鉴权) + api.POST("/login", loginHandler) + + // 需要鉴权的路由 + auth := api.Group("/") + auth.Use(AuthMiddleware()) + { + // 全局配置 + auth.GET("/config", getConfigHandler) + auth.PUT("/config", updateConfigHandler) + + // 隧道管理 + auth.GET("/proxies", getProxiesHandler) + auth.GET("/proxy/:id", getProxyHandler) + auth.POST("/proxy", createProxyHandler) + auth.PUT("/proxy/:id", updateProxyHandler) + auth.DELETE("/proxy/:id", deleteProxyHandler) + + // frpc 控制 + auth.POST("/frpc/reload", reloadFrpcHandler) + auth.POST("/frpc/start", startFrpcHandler) + auth.POST("/frpc/stop", stopFrpcHandler) + auth.GET("/frpc/status", getFrpcStatusHandler) + } + } + + // 前端入口 + r.GET("/", func(c *gin.Context) { + c.File("./static/index.html") + }) + + return r +} + +// ========== 登录 ========== +func loginHandler(c *gin.Context) { + var req struct { + Username string `json:"username"` + Password string `json:"password"` + } + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) + return + } + + user, err := GetUserByUsername(req.Username) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"}) + return + } + + err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"}) + return + } + + token, err := GenerateJWT(user.Username) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "登录成功", "data": gin.H{"token": token}}) +} + +// ========== 全局配置 ========== +func getConfigHandler(c *gin.Context) { + cfg, err := GetGlobalConfig() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "data": cfg}) +} + +func updateConfigHandler(c *gin.Context) { + var cfg GlobalConfig + if err := c.ShouldBindJSON(&cfg); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) + return + } + + if err := UpdateGlobalConfig(&cfg); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败"}) + 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.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"}) +} + +// ========== 隧道管理 ========== +func getProxiesHandler(c *gin.Context) { + proxies, err := GetProxies() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道列表失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "data": proxies}) +} + +func getProxyHandler(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + p, err := GetProxy(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "隧道不存在"}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "data": p}) +} + +func createProxyHandler(c *gin.Context) { + var p Proxy + if err := c.ShouldBindJSON(&p); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) + return + } + p.Enabled = true // 默认启用 + + if err := CreateProxy(&p); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败"}) + return + } + + // 重新生成配置并热加载 + if err := generateAndReload(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道创建成功", "data": gin.H{"id": p.ID}}) +} + +func updateProxyHandler(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var p Proxy + if err := c.ShouldBindJSON(&p); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) + return + } + p.ID = id + + if err := UpdateProxy(&p); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败"}) + return + } + + // 重新生成配置并热加载 + if err := generateAndReload(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道更新成功"}) +} + +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": "删除隧道失败"}) + return + } + + // 重新生成配置并热加载 + if err := generateAndReload(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道删除成功"}) +} + +// ========== frpc 控制 ========== +func reloadFrpcHandler(c *gin.Context) { + 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.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "热加载成功"}) +} + +func startFrpcHandler(c *gin.Context) { + if err := StartFrpc(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "启动失败: " + err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 启动成功"}) +} + +func stopFrpcHandler(c *gin.Context) { + if err := StopFrpc(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "停止失败: " + err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 已停止"}) +} + +func getFrpcStatusHandler(c *gin.Context) { + running, err := GetFrpcStatus() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询状态失败"}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"running": running}}) +} + +// ========== 辅助函数 ========== +func generateAndReload() error { + if err := GenerateFrpcConfig(); err != nil { + return err + } + return ReloadFrpc() +} diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..b20ef50 --- /dev/null +++ b/auth.go @@ -0,0 +1,138 @@ +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +var jwtSecret = []byte("frpc-console-secret-key-2024") + +type Claims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + +func GenerateJWT(username string) (string, error) { + claims := Claims{ + Username: username, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)), + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtSecret) +} + +func ParseJWT(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return jwtSecret, nil + }) + if err != nil { + return nil, err + } + if claims, ok := token.Claims.(*Claims); ok && token.Valid { + return claims, nil + } + return nil, errors.New("invalid token") +} + +// 密码复杂度验证:至少3个大写 + 8个数字 + 1个特殊字符 +func ValidatePassword(pwd string) bool { + var upper, digit, special int + for _, ch := range pwd { + if ch >= 'A' && ch <= 'Z' { + upper++ + } + if ch >= '0' && ch <= '9' { + digit++ + } + if strings.ContainsAny(string(ch), "!@#$%^&*()_+-=[]{}|;:,.<>?") { + special++ + } + } + return upper >= 3 && digit >= 8 && special >= 1 && len(pwd) >= 12 +} + +// 首次启动强制设置管理员账户 +func InitAdminUser() { + count, err := CountUsers() + if err != nil { + log.Fatal("❌ 检查用户表失败:", err) + } + + if count > 0 { + log.Println("✅ 已存在管理员账户,跳过初始化") + return + } + + log.Println("⚠️ 首次启动,请设置管理员账户") + log.Print("用户名: ") + var username string + fmt.Scanln(&username) + + if username == "" { + username = "admin" + } + + for { + log.Print("密码 (至少3个大写+8个数字+1个特殊字符): ") + var password string + fmt.Scanln(&password) + + if ValidatePassword(password) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + log.Println("❌ 密码加密失败:", err) + continue + } + err = CreateUser(username, string(hash)) + if err != nil { + log.Println("❌ 创建用户失败:", err) + continue + } + log.Println("✅ 管理员账户创建成功!") + break + } else { + log.Println("❌ 密码不符合复杂度要求,请重新输入") + log.Println(" 要求: 至少3个大写字母 + 8个数字 + 1个特殊字符") + } + } +} + +// JWT 认证中间件 +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "未提供认证令牌"}) + c.Abort() + return + } + // 格式 "Bearer " + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "认证令牌格式错误"}) + c.Abort() + return + } + tokenString := parts[1] + claims, err := ParseJWT(tokenString) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "无效的认证令牌"}) + c.Abort() + return + } + // 将用户名存入上下文,方便后续使用 + c.Set("username", claims.Username) + c.Next() + } +} diff --git a/db.go b/db.go new file mode 100644 index 0000000..836d630 --- /dev/null +++ b/db.go @@ -0,0 +1,231 @@ +package main + +import ( + "database/sql" + "log" + + _ "modernc.org/sqlite" +) + +var DB *sql.DB + +// 全局配置表(A表) +type GlobalConfig struct { + ID int `json:"id"` + ServerAddr string `json:"serverAddr"` + ServerPort int `json:"serverPort"` + Token string `json:"token"` + LogLevel string `json:"logLevel"` + LogMaxDays int `json:"logMaxDays"` + TcpMux bool `json:"tcpMux"` + TcpMuxKeepalive int `json:"tcpMuxKeepalive"` + HeartbeatInterval int `json:"heartbeatInterval"` + HeartbeatTimeout int `json:"heartbeatTimeout"` + PoolCount int `json:"poolCount"` +} + +// 隧道表(B表) +type Proxy struct { + ID int `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + LocalIP string `json:"localIP"` + LocalPort int `json:"localPort"` + RemotePort int `json:"remotePort"` + Enabled bool `json:"enabled"` +} + +// 用户表(C表) +type User struct { + ID int `json:"id"` + Username string `json:"username"` + PasswordHash string `json:"-"` + CreatedAt string `json:"createdAt"` +} + +func InitDB() error { + var err error + DB, err = sql.Open("sqlite3", "./frpc-console.db") + if err != nil { + return err + } + + // 创建用户表 + _, err = DB.Exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + if err != nil { + return err + } + + // 创建全局配置表(只有一行数据) + _, err = DB.Exec(` + CREATE TABLE IF NOT EXISTS global_config ( + id INTEGER PRIMARY KEY CHECK (id = 1), + server_addr TEXT NOT NULL DEFAULT 'frp.whitetop.xyz', + server_port INTEGER NOT NULL DEFAULT 9358, + token TEXT NOT NULL DEFAULT 'Lxh10020328', + 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, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + if err != nil { + 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) + `) + if err != nil { + return err + } + log.Println("✅ 全局配置初始化完成") + } + + // 创建隧道表 + _, err = DB.Exec(` + CREATE TABLE IF NOT EXISTS proxies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + type TEXT NOT NULL DEFAULT 'tcp', + local_ip TEXT NOT NULL, + local_port INTEGER NOT NULL, + remote_port INTEGER NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + if err != nil { + return err + } + + log.Println("✅ 数据库初始化完成") + return 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 + 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) + if err != nil { + return nil, err + } + return &cfg, nil +} + +func UpdateGlobalConfig(cfg *GlobalConfig) error { + _, 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 = ?, + 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) + return err +} + +// ========== 隧道 CRUD ========== +func GetProxies() ([]Proxy, error) { + rows, err := DB.Query(` + SELECT id, name, type, local_ip, local_port, remote_port, enabled + FROM proxies ORDER BY id + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var proxies []Proxy + for rows.Next() { + var p Proxy + err := rows.Scan(&p.ID, &p.Name, &p.Type, &p.LocalIP, &p.LocalPort, &p.RemotePort, &p.Enabled) + if err != nil { + return nil, err + } + proxies = append(proxies, p) + } + return proxies, nil +} + +func GetProxy(id int) (*Proxy, error) { + var p Proxy + err := DB.QueryRow(` + SELECT id, name, type, local_ip, local_port, remote_port, enabled + FROM proxies WHERE id = ? + `, id).Scan(&p.ID, &p.Name, &p.Type, &p.LocalIP, &p.LocalPort, &p.RemotePort, &p.Enabled) + if err != nil { + return nil, err + } + return &p, nil +} + +func CreateProxy(p *Proxy) error { + _, 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) + return err +} + +func UpdateProxy(p *Proxy) error { + _, err := DB.Exec(` + UPDATE proxies SET + name = ?, type = ?, local_ip = ?, local_port = ?, remote_port = ?, enabled = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `, p.Name, p.Type, p.LocalIP, p.LocalPort, p.RemotePort, p.Enabled, p.ID) + return err +} + +func DeleteProxy(id int) error { + _, err := DB.Exec("DELETE FROM proxies WHERE id = ?", id) + return err +} + +// ========== 用户 CRUD ========== +func GetUserByUsername(username string) (*User, error) { + var u User + err := DB.QueryRow(` + SELECT id, username, password_hash, created_at + FROM users WHERE username = ? + `, username).Scan(&u.ID, &u.Username, &u.PasswordHash, &u.CreatedAt) + if err != nil { + return nil, err + } + return &u, nil +} + +func CreateUser(username, passwordHash string) error { + _, err := DB.Exec(` + INSERT INTO users (username, password_hash) VALUES (?, ?) + `, username, passwordHash) + return err +} + +func CountUsers() (int, error) { + var count int + err := DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) + return count, err +} diff --git a/frp.go b/frp.go new file mode 100644 index 0000000..813165d --- /dev/null +++ b/frp.go @@ -0,0 +1,146 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + "text/template" +) + +// 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}}` + +// 生成 frpc.toml 文件 +func GenerateFrpcConfig() error { + // 1. 读取全局配置 + cfg, err := GetGlobalConfig() + if err != nil { + return fmt.Errorf("读取全局配置失败: %w", err) + } + + // 2. 读取所有启用的隧道 + proxies, err := GetProxies() + if err != nil { + return fmt.Errorf("读取隧道列表失败: %w", err) + } + + var activeProxies []Proxy + for _, p := range proxies { + if p.Enabled { + activeProxies = append(activeProxies, p) + } + } + + // 3. 组合数据 + data := struct { + *GlobalConfig + Proxies []Proxy + }{ + GlobalConfig: cfg, + Proxies: activeProxies, + } + + // 4. 渲染模板 + tmpl, err := template.New("frpc").Parse(frpcTemplate) + if err != nil { + return fmt.Errorf("解析模板失败: %w", err) + } + + var buf bytes.Buffer + err = tmpl.Execute(&buf, data) + if err != nil { + return fmt.Errorf("渲染模板失败: %w", err) + } + + // 5. 写入文件 + err = os.WriteFile("./frpc.toml", buf.Bytes(), 0644) + if err != nil { + return fmt.Errorf("写入配置文件失败: %w", err) + } + + return nil +} + +// 热加载 frpc +func ReloadFrpc() error { + // 先检查 frpc 进程是否在运行 + cmd := exec.Command("pgrep", "-f", "frpc") + output, err := cmd.Output() + if err != nil || len(output) == 0 { + // frpc 没在运行,尝试启动 + return StartFrpc() + } + + // 执行 reload + reloadCmd := exec.Command("frpc", "reload", "-c", "./frpc.toml") + output, err = reloadCmd.CombinedOutput() + if err != nil { + return fmt.Errorf("reload 失败: %s, %w", string(output), err) + } + return nil +} + +// 启动 frpc +func StartFrpc() error { + // 先确保配置文件存在 + if _, err := os.Stat("./frpc.toml"); os.IsNotExist(err) { + if err := GenerateFrpcConfig(); err != nil { + return fmt.Errorf("生成配置文件失败: %w", err) + } + } + + cmd := exec.Command("frpc", "-c", "./frpc.toml") + // 后台运行,不阻塞 + err := cmd.Start() + if err != nil { + return fmt.Errorf("启动 frpc 失败: %w", err) + } + return nil +} + +// StopFrpc 停止 frpc +func StopFrpc() error { + cmd := exec.Command("pkill", "-f", "frpc") + err := cmd.Run() + if err != nil && !strings.Contains(err.Error(), "no process") { + return fmt.Errorf("停止 frpc 失败: %w", err) + } + return nil +} + +// 检查 frpc 状态 +func GetFrpcStatus() (bool, error) { + cmd := exec.Command("pgrep", "-f", "frpc") + output, err := cmd.Output() + if err != nil { + return false, nil + } + return len(output) > 0, nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4161340 --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module frpc-console + +go 1.26.5 + +require ( + github.com/gin-gonic/gin v1.12.0 + github.com/golang-jwt/jwt/v5 v5.3.1 + golang.org/x/crypto v0.54.0 + modernc.org/sqlite v1.54.0 +) + +require ( + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + 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/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 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.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect + google.golang.org/protobuf v1.36.10 // 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 new file mode 100644 index 0000000..935b577 --- /dev/null +++ b/go.sum @@ -0,0 +1,137 @@ +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +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-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= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +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 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= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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.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/main.go b/main.go new file mode 100644 index 0000000..742cd46 --- /dev/null +++ b/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" +) + +func main() { + // 1. 初始化数据库 + if err := InitDB(); err != nil { + log.Fatal("❌ 数据库初始化失败:", err) + } + + // 2. 首次启动设置管理员 + InitAdminUser() + + // 3. 启动时生成 frpc.toml + if err := GenerateFrpcConfig(); err != nil { + log.Println("⚠️ 生成配置文件失败:", err) + } + + // 4. 尝试启动 frpc + if err := StartFrpc(); err != nil { + log.Println("⚠️ 启动 frpc 失败:", err) + } + + // 5. 启动 Web 服务 + r := SetupRouter() + log.Println("🚀 frpc-console 启动成功!") + log.Println("📍 访问地址: http://localhost:8080") + log.Println("📍 API 地址: http://localhost:8080/api") + + if err := r.Run(":8080"); err != nil { + log.Fatal("❌ 服务启动失败:", err) + } +} diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..e69de29 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..b49a20b --- /dev/null +++ b/static/index.html @@ -0,0 +1,22 @@ + + + + + + frpc-console + + + +
+

🚀 frpc-console

+

后端已启动,正在等待前端接入...

+
✅ frpc 运行中
+
+ + \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..e69de29 diff --git a/templates/frpc.toml.tmpl b/templates/frpc.toml.tmpl new file mode 100644 index 0000000..e69de29