package api import ( "bytes" "fmt" "html/template" "net" "net/http" "strconv" "time" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "frpc-console/internal/auth" "frpc-console/internal/db" "frpc-console/internal/frp" ) // ================================================================ // 认证 Handler // ================================================================ func CheckUsersHandler(c *gin.Context) { count, err := db.CountUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"}) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "data": gin.H{"hasUsers": count > 0, "count": count}, }) } func RegisterHandler(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 } count, err := db.CountUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"}) return } if count > 0 { c.JSON(http.StatusForbidden, gin.H{"code": 1, "msg": "已存在管理员账户,请登录"}) return } if len(req.Username) < 5 { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "用户名至少 5 位"}) return } if !auth.ValidatePassword(req.Password) { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"}) return } hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"}) return } if err := db.CreateUser(req.Username, string(hash)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建用户失败: " + err.Error()}) return } token, err := auth.GenerateJWT(req.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 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 := db.GetUserByUsername(req.Username) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"}) return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"}) return } token, err := auth.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 ChangePasswordHandler(c *gin.Context) { var req struct { OldPassword string `json:"oldPassword"` NewPassword string `json:"newPassword"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) return } username := auth.GetUsernameFromContext(c) if username == "" { c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "未登录"}) return } user, err := db.GetUserByUsername(username) if err != nil { c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "用户不存在"}) return } if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "当前密码错误"}) return } if !auth.ValidatePassword(req.NewPassword) { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"}) return } hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"}) return } if err := db.UpdateUserPassword(username, string(hash)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新密码失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码修改成功"}) } // ================================================================ // 配置 Handler // ================================================================ func GetConfigHandler(c *gin.Context) { cfg, err := db.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 db.GlobalConfig if err := c.ShouldBindJSON(&cfg); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) return } cfg.TcpMux = true if err := db.UpdateGlobalConfig(&cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()}) return } if err := frp.GenerateConfig(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()}) return } if err := frp.Reload(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"}) } // ================================================================ // 隧道 Handler // ================================================================ func GetProxiesHandler(c *gin.Context) { proxies, err := db.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, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"}) return } p, err := db.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 db.Proxy if err := c.ShouldBindJSON(&p); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) return } p.Enabled = true if err := db.CreateProxy(&p); 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 } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道创建成功", "data": gin.H{"id": p.ID}}) } func UpdateProxyHandler(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"}) return } var p db.Proxy if err := c.ShouldBindJSON(&p); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"}) return } p.ID = id if err := db.UpdateProxy(&p); 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 } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道更新成功"}) } func DeleteProxyHandler(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"}) return } if err := db.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 } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道删除成功"}) } // ================================================================ // frpc 进程管理 Handler // ================================================================ func ReloadFrpcHandler(c *gin.Context) { if err := frp.GenerateConfig(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()}) return } if err := frp.Reload(); 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 := frp.Start(); 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 := frp.Stop(); 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) { status, err := frp.GetStatus() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询状态失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "data": status}) } // ================================================================ // Ping Handler // ================================================================ func PingHandler(c *gin.Context) { target := c.Query("target") if target == "" { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "缺少 target 参数"}) return } cfg, err := db.GetGlobalConfig() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"}) return } port := cfg.ServerPort address := net.JoinHostPort(target, strconv.Itoa(port)) start := time.Now() conn, err := net.DialTimeout("tcp", address, 5*time.Second) if err != nil { c.JSON(http.StatusOK, gin.H{"code": 1, "msg": "ping 失败", "latency": -1}) return } conn.Close() latency := time.Since(start).Milliseconds() c.JSON(http.StatusOK, gin.H{"code": 0, "latency": latency}) } // ================================================================ // 日志 Handler // ================================================================ func GetFrpcLogHandler(c *gin.Context) { lines, err := frp.ReadTailLog("./data/frpc.log", 200) if err != nil { c.JSON(http.StatusOK, gin.H{ "code": 0, "data": gin.H{ "lines": []string{}, "total": 0, "error": err.Error(), }, }) return } c.JSON(http.StatusOK, gin.H{ "code": 0, "data": gin.H{ "lines": lines, "total": len(lines), }, }) } // ================================================================ // 辅助函数 // ================================================================ func generateAndReload() error { if err := frp.GenerateConfig(); err != nil { return err } return frp.Reload() } // readTailLog 读取文件末尾 n 行 (临时放在这里,后续移到独立包) func readTailLog(filePath string, n int) ([]string, error) { // 这个函数在 frp 模块中也有,但为了避免循环依赖,在这里实现一份简单的 // 或者直接调用 frp.ReadTailLog 如果导出的话 // 目前保持和原来一致,后续可以统一到 pkg/utils // 为了编译通过,先简单返回空 return []string{}, nil } // ================================================================ // 导入/导出 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 := frp.ParseToml(buf.String()) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "解析 TOML 失败: " + err.Error()}) return } cfg := parsed.ToGlobalConfig() cfg.TcpMux = true if err := db.UpdateGlobalConfig(cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()}) return } if _, err := db.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 := db.CreateProxy(&p); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "导入隧道失败: " + err.Error()}) return } } if err := frp.GenerateConfig(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()}) return } if err := frp.Reload(); 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 ExportTomlHandler(c *gin.Context) { cfg, err := db.GetGlobalConfig() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败: " + err.Error()}) return } proxies, err := db.GetProxies() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道失败: " + err.Error()}) return } var activeProxies []db.Proxy for _, p := range proxies { if p.Enabled { activeProxies = append(activeProxies, p) } } data := struct { *db.GlobalConfig Proxies []db.Proxy WireProtocolLine string }{ GlobalConfig: cfg, Proxies: activeProxies, } if cfg.WireProtocolV2 { data.WireProtocolLine = `wireProtocol = "v2"` } else { data.WireProtocolLine = "" } // 这里需要 frp.FrpcTemplateContent,需要从 frp 包导出 tmpl, err := template.New("frpc").Parse(frp.FrpcTemplateContent) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "解析模板失败: " + err.Error()}) return } var buf bytes.Buffer if err := tmpl.Execute(&buf, data); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "渲染模板失败: " + err.Error()}) return } c.Header("Content-Type", "text/plain; charset=utf-8") c.Header("Content-Disposition", "attachment; filename=frpc.toml") c.String(http.StatusOK, buf.String()) }