package main import ( "bytes" "embed" "encoding/json" "io" "io/fs" // 新增 "net/http" "os" "strings" "text/template" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" ) //go:embed static/* var staticFS embed.FS func SetupRouter() *gin.Engine { r := gin.Default() staticSubFS, _ := fs.Sub(staticFS, "static") r.StaticFS("/static", http.FS(staticSubFS)) r.GET("/", func(c *gin.Context) { content, err := staticFS.ReadFile("static/index.html") if err != nil { c.String(500, "加载前端页面失败") return } c.Data(http.StatusOK, "text/html; charset=utf-8", content) }) r.GET("/ping", func(c *gin.Context) { c.String(200, "frps-console 后端已启动 🎉") }) api := r.Group("/api") { api.GET("/check/users", checkUsersHandler) api.POST("/register", registerHandler) api.POST("/login", loginHandler) auth := api.Group("/") auth.Use(AuthMiddleware()) { auth.GET("/config", getConfigHandler) auth.PUT("/config", updateConfigHandler) auth.GET("/frps/log", getFrpsLogHandler) // frps 特有:客户端列表(从 Dashboard API 读取) auth.GET("/clients", getClientsHandler) auth.GET("/proxies", getProxiesHandler) auth.POST("/frps/reload", reloadFrpsHandler) auth.POST("/frps/start", startFrpsHandler) auth.POST("/frps/stop", stopFrpsHandler) auth.GET("/frps/status", getFrpsStatusHandler) auth.POST("/import/toml", importTomlHandler) auth.GET("/export/toml", ExportTomlHandler) auth.PUT("/user/password", changePasswordHandler) } } return r } // ============================================================ // 认证 Handler(与 frpc-console 相同,略) // ============================================================ func checkUsersHandler(c *gin.Context) { count, err := 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 := 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 !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 := CreateUser(req.Username, string(hash)); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建用户失败: " + err.Error()}) return } token, err := 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 := 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 := 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, exists := c.Get("username") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "未登录"}) return } user, err := GetUserByUsername(username.(string)) 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 !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 := UpdatePassword(username.(string), 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 := 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": "更新配置失败: " + err.Error()}) return } if err := GenerateFrpsConfig(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()}) return } if err := ReloadFrps(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"}) } // ============================================================ // 客户端/代理列表(从 frps Dashboard API 读取) // ============================================================ // FrpsDashboardClient frps Dashboard API 返回的客户端信息 type FrpsDashboardClient struct { ID int `json:"id"` Name string `json:"name"` OS string `json:"os"` Arch string `json:"arch"` Version string `json:"version"` Status string `json:"status"` ConnTime string `json:"connTime"` } // FrpsDashboardProxy frps Dashboard API 返回的代理信息 type FrpsDashboardProxy struct { Name string `json:"name"` Type string `json:"type"` Status string `json:"status"` LocalAddr string `json:"localAddr"` RemoteAddr string `json:"remoteAddr"` } func getClientsHandler(c *gin.Context) { clients, err := fetchClientsFromDashboard() if err != nil { c.JSON(http.StatusOK, gin.H{"code": 0, "data": []interface{}{}, "msg": "无法连接 frps Dashboard: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "data": clients}) } func getProxiesHandler(c *gin.Context) { proxies, err := fetchProxiesFromDashboard() if err != nil { c.JSON(http.StatusOK, gin.H{"code": 0, "data": []interface{}{}, "msg": "无法连接 frps Dashboard: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "data": proxies}) } // fetchClientsFromDashboard 从 frps Dashboard API 读取客户端列表 func fetchClientsFromDashboard() ([]FrpsDashboardClient, error) { // frps Dashboard 默认在 127.0.0.1:7500,后续可配置 resp, err := http.Get("http://127.0.0.1:7500/api/v2/clients") if err != nil { return nil, err } defer resp.Body.Close() var result struct { Data []FrpsDashboardClient `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return result.Data, nil } // fetchProxiesFromDashboard 从 frps Dashboard API 读取代理列表 func fetchProxiesFromDashboard() ([]FrpsDashboardProxy, error) { resp, err := http.Get("http://127.0.0.1:7500/api/v2/proxies") if err != nil { return nil, err } defer resp.Body.Close() var result struct { Data []FrpsDashboardProxy `json:"data"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, err } return result.Data, nil } // ============================================================ // frps 进程管理 Handler // ============================================================ func reloadFrpsHandler(c *gin.Context) { if err := GenerateFrpsConfig(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()}) return } if err := ReloadFrps(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "热加载成功"}) } func startFrpsHandler(c *gin.Context) { if err := StartFrps(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "启动失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frps 启动成功"}) } func stopFrpsHandler(c *gin.Context) { if err := StopFrps(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "停止失败: " + err.Error()}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frps 已停止"}) } func getFrpsStatusHandler(c *gin.Context) { running, err := GetFrpsStatus() 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}}) } // ============================================================ // 导入/导出 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 } cfg := parsed.ToGlobalConfig() if err := UpdateGlobalConfig(cfg); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()}) return } if err := GenerateFrpsConfig(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()}) return } if err := ReloadFrps(); err != nil { c.JSON(http.StatusOK, gin.H{ "code": 0, "msg": "导入成功,但热加载失败: " + err.Error(), }) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "导入成功"}) } func ExportTomlHandler(c *gin.Context) { cfg, err := GetGlobalConfig() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败: " + err.Error()}) return } tmpl, err := template.New("frps").Parse(FrpsTemplateContent) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "解析模板失败: " + err.Error()}) return } var buf bytes.Buffer if err := tmpl.Execute(&buf, cfg); 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=frps.toml") c.String(http.StatusOK, buf.String()) } // ============================================================ // 日志读取 // ============================================================ func getFrpsLogHandler(c *gin.Context) { lines, err := readTailLog("./frps.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), }, }) } // readTailLog 读取文件末尾 n 行 func readTailLog(filePath string, n int) ([]string, error) { file, err := os.Open(filePath) if err != nil { return nil, err } defer file.Close() info, err := file.Stat() if err != nil { return nil, err } fileSize := info.Size() if fileSize == 0 { return []string{}, nil } const chunkSize = 4096 var lines []string var leftover []byte offset := fileSize for len(lines) < n && offset > 0 { readSize := chunkSize if offset < int64(chunkSize) { readSize = int(offset) } offset -= int64(readSize) buf := make([]byte, readSize) _, err := file.ReadAt(buf, offset) if err != nil && err != io.EOF { return nil, err } data := append(buf, leftover...) leftover = nil start := 0 for i := len(data) - 1; i >= 0; i-- { if data[i] == '\n' { if i+1 < len(data) { line := string(data[i+1:]) if line != "" { lines = append([]string{line}, lines...) if len(lines) >= n { break } } } start = i } } if len(lines) < n && start > 0 { leftover = data[:start] } } if len(lines) < n && len(leftover) > 0 { parts := strings.Split(string(leftover), "\n") for i := len(parts) - 1; i >= 0; i-- { if parts[i] != "" { lines = append([]string{parts[i]}, lines...) if len(lines) >= n { break } } } } return lines, nil }