diff --git a/Dockerfile.txt b/Dockerfile.txt deleted file mode 100644 index 2b2d186..0000000 --- a/Dockerfile.txt +++ /dev/null @@ -1,30 +0,0 @@ -# 多阶段构建 -FROM golang:alpine AS builder - -WORKDIR /app - -COPY go.mod go.sum ./ - -RUN sed -i 's/go 1.2[6-9].*/go 1.21/' go.mod && go mod tidy -RUN go env -w GOPROXY=https://goproxy.cn,direct -RUN go mod download - -COPY . . - -# 编译时不注入任何版本信息(纯净二进制) -RUN CGO_ENABLED=0 GOOS=linux go build \ - -ldflags="-s -w" \ - -o frpc-console . - -FROM alpine:latest - -RUN apk --no-cache add ca-certificates tzdata sqlite - -WORKDIR /app - -COPY --from=builder /app/frpc-console /app/frpc-console -COPY static/ /app/static/ - -EXPOSE 9300 - -ENTRYPOINT ["/app/frpc-console"] \ No newline at end of file diff --git a/api.go b/api.go deleted file mode 100644 index 9ba667c..0000000 --- a/api.go +++ /dev/null @@ -1,575 +0,0 @@ -package main - -import ( - "bytes" - "embed" - "fmt" - "io/fs" - "net" - "net/http" - "strconv" - "text/template" - "time" - - "github.com/gin-gonic/gin" - "golang.org/x/crypto/bcrypt" -) - -//go:embed static/* -var staticFS embed.FS - -func SetupRouter() *gin.Engine { - r := gin.Default() - - // 从 embed 读取前端静态文件 - 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, "frpc-console 后端已启动 🎉") - }) - - api := r.Group("/api") - { - // ---- 公开路由(不需要认证) ---- - api.GET("/check/users", checkUsersHandler) - api.POST("/register", registerHandler) - api.POST("/login", loginHandler) - api.GET("/ping", pingHandler) - - // ---- 需要认证的路由 ---- - 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) - - auth.POST("/frpc/reload", reloadFrpcHandler) - auth.POST("/frpc/start", startFrpcHandler) - auth.POST("/frpc/stop", stopFrpcHandler) - auth.GET("/frpc/status", getFrpcStatusHandler) - auth.GET("/frpc/log", getFrpcLogHandler) - - auth.POST("/import/toml", importTomlHandler) - auth.GET("/export/toml", ExportTomlHandler) - - auth.PUT("/user/password", changePasswordHandler) - } - } - - return r -} - -// ========== 认证 Handler ========== - -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 - } - cfg.TcpMux = true - - if err := UpdateGlobalConfig(&cfg); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()}) - return - } - - if err := GenerateFrpcConfig(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()}) - return - } - - if err := ReloadFrpc(); err != nil { - c.JSON(http.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 := 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 := 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": "创建隧道失败: " + 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 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": "更新隧道失败: " + 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 := 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 := 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}}) -} - -// ========== 日志 Handler ========== - -func getFrpcLogHandler(c *gin.Context) { - // readTailLog 在 frp.go 中定义,读取 ./data/frpc.log - lines, err := 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), - }, - }) -} - -// ========== 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 := 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}) -} - -// ========== 导入/导出 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() - cfg.TcpMux = true - if err := UpdateGlobalConfig(cfg); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()}) - return - } - - if _, err := DB.Exec("DELETE FROM proxies"); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "清空隧道失败"}) - return - } - - proxies := parsed.ToProxies() - for _, p := range proxies { - if err := CreateProxy(&p); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "导入隧道失败: " + err.Error()}) - return - } - } - - if err := GenerateFrpcConfig(); err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()}) - return - } - - if err := ReloadFrpc(); err != nil { - c.JSON(http.StatusOK, gin.H{ - "code": 0, - "msg": fmt.Sprintf("导入成功!共 %d 条隧道,但热加载失败: %s", len(proxies), err.Error()), - }) - return - } - - c.JSON(http.StatusOK, gin.H{ - "code": 0, - "msg": fmt.Sprintf("导入成功!共 %d 条隧道", len(proxies)), - }) -} - -func ExportTomlHandler(c *gin.Context) { - cfg, err := GetGlobalConfig() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败: " + err.Error()}) - return - } - - proxies, err := GetProxies() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道失败: " + err.Error()}) - return - } - - var activeProxies []Proxy - for _, p := range proxies { - if p.Enabled { - activeProxies = append(activeProxies, p) - } - } - - data := struct { - *GlobalConfig - Proxies []Proxy - WireProtocolLine string - }{ - GlobalConfig: cfg, - Proxies: activeProxies, - } - - if cfg.WireProtocolV2 { - data.WireProtocolLine = `wireProtocol = "v2"` - } else { - data.WireProtocolLine = "" - } - - tmpl, err := template.New("frpc").Parse(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()) -} - -func generateAndReload() error { - if err := GenerateFrpcConfig(); err != nil { - return err - } - return ReloadFrpc() -} diff --git a/auth.go b/auth.go deleted file mode 100644 index 5b46007..0000000 --- a/auth.go +++ /dev/null @@ -1,142 +0,0 @@ -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 jwtSecretCache []byte - -func getJwtSecret() []byte { - if len(jwtSecretCache) > 0 { - return jwtSecretCache - } - secret, err := GetJwtSecret() - if err != nil { - log.Fatal("❌ 获取 JWT 密钥失败:", err) - } - jwtSecretCache = []byte(secret) - return jwtSecretCache -} - -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(getJwtSecret()) -} - -func ParseJWT(tokenString string) (*Claims, error) { - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { - return getJwtSecret(), 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") -} - -func ValidatePassword(pwd string) bool { - if len(pwd) < 8 { - return false - } - var hasUpper, hasLower, hasDigit, hasSpecial bool - for _, ch := range pwd { - if ch >= 'A' && ch <= 'Z' { - hasUpper = true - } else if ch >= 'a' && ch <= 'z' { - hasLower = true - } else if ch >= '0' && ch <= '9' { - hasDigit = true - } else if strings.ContainsAny(string(ch), "!@#$%^&*()_+-=[]{}|;:,.<>?") { - hasSpecial = true - } - } - return hasUpper && hasLower && hasDigit && hasSpecial -} - -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("密码 (至少8位,含大小写、数字、特殊字符): ") - 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(" 要求: 至少8位,包含大小写字母、数字和特殊字符") - } - } -} - -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 - } - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) != 2 || parts[0] != "Bearer" { - c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "认证令牌格式错误"}) - c.Abort() - return - } - claims, err := ParseJWT(parts[1]) - 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/frp-unix.go b/frp-unix.go deleted file mode 100644 index ebbe825..0000000 --- a/frp-unix.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build linux || darwin || freebsd || netbsd || openbsd || solaris - -package main - -import ( - "os/exec" - "syscall" -) - -// setSysProcAttr 为 Unix 系统设置 Setsid,让 frpc 进程脱离父进程独立运行 -func setSysProcAttr(cmd *exec.Cmd) { - if cmd.SysProcAttr == nil { - cmd.SysProcAttr = &syscall.SysProcAttr{} - } - cmd.SysProcAttr.Setsid = true -} diff --git a/frp.go b/frp.go deleted file mode 100644 index b5c2066..0000000 --- a/frp.go +++ /dev/null @@ -1,457 +0,0 @@ -package main - -import ( - "bytes" - "embed" - "fmt" - "io" - "log" - "os" - "os/exec" - "path/filepath" - "runtime" - "strconv" - "strings" - "sync" - "syscall" - "text/template" -) - -//go:embed bin/* -var embeddedFrpc embed.FS - -//go:embed frpc.tmpl -var FrpcTemplateContent string - -var ( - cachedFrpcPath string - frpcPathMutex sync.Mutex -) - -// ================================================================ -// frpc 二进制提取 -// ================================================================ - -func getFrpcPath() (string, error) { - frpcPathMutex.Lock() - defer frpcPathMutex.Unlock() - - if cachedFrpcPath != "" { - if _, err := os.Stat(cachedFrpcPath); err == nil { - return cachedFrpcPath, nil - } - cachedFrpcPath = "" - } - - var fileName string - switch { - case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": - fileName = "frpc_windows_amd64.exe" - case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": - fileName = "frpc_linux_amd64" - case runtime.GOOS == "linux" && runtime.GOARCH == "arm64": - fileName = "frpc_linux_arm64" - case runtime.GOOS == "linux" && runtime.GOARCH == "arm": - fileName = "frpc_linux_arm_hf" - default: - path, err := exec.LookPath("frpc") - if err == nil { - cachedFrpcPath = path - return path, nil - } - return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH) - } - - localPath := filepath.Join(".", "bin", fileName) - if _, err := os.Stat(localPath); err == nil { - cachedFrpcPath = localPath - return localPath, nil - } - - data, err := embeddedFrpc.ReadFile("bin/" + fileName) - if err == nil { - tmpPath := filepath.Join(os.TempDir(), "frpc") - if runtime.GOOS == "windows" { - tmpPath += ".exe" - } - if err := os.WriteFile(tmpPath, data, 0755); err == nil { - cachedFrpcPath = tmpPath - return tmpPath, nil - } - if _, statErr := os.Stat(tmpPath); statErr == nil { - cachedFrpcPath = tmpPath - return tmpPath, nil - } - } - - path, err := exec.LookPath("frpc") - if err == nil { - cachedFrpcPath = path - return path, nil - } - - return "", fmt.Errorf("未找到 frpc 文件") -} - -// ================================================================ -// 配置生成 -// ================================================================ - -func GenerateFrpcConfig() error { - cfg, err := GetGlobalConfig() - if err != nil { - return fmt.Errorf("读取全局配置失败: %w", err) - } - - 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) - } - } - - data := struct { - *GlobalConfig - Proxies []Proxy - WireProtocolLine string - }{ - GlobalConfig: cfg, - Proxies: activeProxies, - } - - if cfg.WireProtocolV2 { - data.WireProtocolLine = `wireProtocol = "v2"` - } else { - data.WireProtocolLine = "" - } - - var tmplContent string - if _, err := os.Stat("frpc.tmpl"); err == nil { - content, readErr := os.ReadFile("frpc.tmpl") - if readErr == nil { - tmplContent = string(content) - } else { - tmplContent = FrpcTemplateContent - } - } else { - tmplContent = FrpcTemplateContent - } - - tmpl, err := template.New("frpc").Parse(tmplContent) - if err != nil { - return fmt.Errorf("解析模板失败: %w", err) - } - - var buf bytes.Buffer - if err := tmpl.Execute(&buf, data); err != nil { - return fmt.Errorf("渲染模板失败: %w", err) - } - - if err := os.MkdirAll("./data", 0755); err != nil { - return fmt.Errorf("创建 data 目录失败: %w", err) - } - - if err := os.WriteFile("./data/frpc.toml", buf.Bytes(), 0644); err != nil { - return fmt.Errorf("写入配置文件失败: %w", err) - } - - return nil -} - -// ================================================================ -// 进程管理兼容层 -// ================================================================ - -func isFrpcRunning() bool { - if processManager != nil { - status, err := processManager.Status() - if err != nil { - log.Printf("[WARN] ProcessManager.Status() 失败: %v,降级到 PID 文件", err) - return isFrpcRunningLegacy() - } - return status.State == "running" - } - return isFrpcRunningLegacy() -} - -func isFrpcRunningLegacy() bool { - pidData, err := os.ReadFile("./data/frpc.pid") - if err != nil { - return false - } - pid, err := strconv.Atoi(strings.TrimSpace(string(pidData))) - if err != nil { - return false - } - if runtime.GOOS == "windows" { - cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid)) - output, err := cmd.CombinedOutput() - if err != nil { - return false - } - return strings.Contains(string(output), strconv.Itoa(pid)) - } - process, err := os.FindProcess(pid) - if err != nil { - return false - } - return process.Signal(syscall.Signal(0)) == nil -} - -func StartFrpc() error { - if processManager != nil { - log.Println("[INFO] 使用 ProcessManager 启动 frpc") - return processManager.Start(nil) - } - log.Println("[WARN] ProcessManager 未初始化,使用兼容模式启动 frpc") - return startFrpcLegacy() -} - -func startFrpcLegacy() error { - frpcPath, err := getFrpcPath() - if err != nil { - return fmt.Errorf("获取 frpc 路径失败: %w", err) - } - if err := os.MkdirAll("./data", 0755); err != nil { - return fmt.Errorf("创建 data 目录失败: %w", err) - } - if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) { - if err := GenerateFrpcConfig(); err != nil { - return fmt.Errorf("生成配置文件失败: %w", err) - } - } - if isFrpcRunningLegacy() { - return nil - } - os.Remove("./data/frpc.pid") - cmd := exec.Command(frpcPath, "-c", "./data/frpc.toml") - setWindowHide(cmd) - setSysProcAttr(cmd) - logFile, err := os.OpenFile("./data/frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) - if err != nil { - return fmt.Errorf("打开日志文件失败: %w", err) - } - cmd.Stdout = logFile - cmd.Stderr = logFile - if err := cmd.Start(); err != nil { - return fmt.Errorf("启动 frpc 失败: %w", err) - } - go func() { - if err := cmd.Wait(); err != nil { - log.Printf("frpc 子进程退出: %v", err) - } - os.Remove("./data/frpc.pid") - }() - if err := os.WriteFile("./data/frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil { - return fmt.Errorf("保存 PID 失败: %w", err) - } - return nil -} - -func StopFrpc() error { - if processManager != nil { - log.Println("[INFO] 使用 ProcessManager 停止 frpc") - return processManager.Stop(nil) - } - log.Println("[WARN] ProcessManager 未初始化,使用兼容模式停止 frpc") - return stopFrpcLegacy() -} - -func stopFrpcLegacy() error { - if runtime.GOOS == "windows" { - cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe") - if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") { - return fmt.Errorf("停止 frpc 失败: %w", err) - } - os.Remove("./data/frpc.pid") - return nil - } - pidData, err := os.ReadFile("./data/frpc.pid") - if err != nil { - cmd := exec.Command("pkill", "-f", "frpc") - if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") { - return fmt.Errorf("停止 frpc 失败: %w", err) - } - return nil - } - pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData))) - process, err := os.FindProcess(pid) - if err != nil { - os.Remove("./data/frpc.pid") - return nil - } - if err := process.Kill(); err != nil { - return fmt.Errorf("杀死进程失败: %w", err) - } - os.Remove("./data/frpc.pid") - return nil -} - -func RestartFrpc() error { - if processManager != nil { - log.Println("[INFO] 使用 ProcessManager 重启 frpc") - return processManager.Restart(nil) - } - log.Println("[WARN] ProcessManager 未初始化,使用兼容模式重启 frpc") - if err := stopFrpcLegacy(); err != nil { - return err - } - return startFrpcLegacy() -} - -func GetFrpcStatus() (map[string]interface{}, error) { - if processManager != nil { - status, err := processManager.Status() - if err != nil { - return nil, err - } - return map[string]interface{}{ - "state": status.State, - "pid": status.PID, - "port": status.Port, - }, nil - } - running := isFrpcRunningLegacy() - return map[string]interface{}{ - "state": map[bool]string{true: "running", false: "stopped"}[running], - "pid": 0, - "port": 0, - "legacy": true, - }, nil -} - -func ReloadFrpc() error { - if processManager == nil { - return reloadFrpcLegacy() - } - status, err := processManager.Status() - if err != nil { - return fmt.Errorf("获取 frpc 状态失败: %w", err) - } - if status.State != "running" { - return processManager.Start(nil) - } - frpcPath, err := getFrpcPath() - if err != nil { - return fmt.Errorf("获取 frpc 路径失败: %w", err) - } - cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") - output, err := cmd.CombinedOutput() - if err != nil { - log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) - log.Printf(" reload 输出: %s", string(output)) - if err := processManager.Restart(nil); err != nil { - return fmt.Errorf("重启 frpc 失败: %w", err) - } - return nil - } - log.Printf("✅ frpc 热加载成功: %s", string(output)) - return nil -} - -func reloadFrpcLegacy() error { - if !isFrpcRunningLegacy() { - return startFrpcLegacy() - } - frpcPath, err := getFrpcPath() - if err != nil { - return fmt.Errorf("获取 frpc 路径失败: %w", err) - } - cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") - output, err := cmd.CombinedOutput() - if err != nil { - log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) - log.Printf(" reload 输出: %s", string(output)) - if stopErr := stopFrpcLegacy(); stopErr != nil { - return fmt.Errorf("停止 frpc 失败: %w", stopErr) - } - if startErr := startFrpcLegacy(); startErr != nil { - return fmt.Errorf("启动 frpc 失败: %w", startErr) - } - return nil - } - log.Printf("✅ frpc 热加载成功 (兼容模式): %s", string(output)) - return nil -} - -// ================================================================ -// 日志读取 -// ================================================================ - -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 -} diff --git a/frp_other.go b/frp_other.go deleted file mode 100644 index 068ef7f..0000000 --- a/frp_other.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !windows - -package main - -import "os/exec" - -func setWindowHide(cmd *exec.Cmd) { - // 非 Windows 平台什么都不做 -} diff --git a/frp_windows.go b/frp_windows.go deleted file mode 100644 index 51a045d..0000000 --- a/frp_windows.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build windows - -package main - -import ( - "os/exec" - "syscall" -) - -func setWindowHide(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{ - HideWindow: true, - } -} - -func setSysProcAttr(cmd *exec.Cmd) { - // Windows 不支持 Setpgid,不需要额外设置 -} diff --git a/internal/auth/auth.go b/internal/auth/auth.go new file mode 100644 index 0000000..4b8bd05 --- /dev/null +++ b/internal/auth/auth.go @@ -0,0 +1,234 @@ +package auth + +import ( + "errors" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" + + "frpc-console/internal/db" +) + +var jwtSecretCache []byte + +// getJwtSecret 从数据库获取 JWT 密钥 +func getJwtSecret() []byte { + if len(jwtSecretCache) > 0 { + return jwtSecretCache + } + secret, err := db.GetJwtSecret() + if err != nil { + // 如果数据库还没有密钥,生成一个默认的(仅用于开发) + // 生产环境应该通过数据库初始化时生成 + return []byte("frpc-console-default-secret-key-2024") + } + jwtSecretCache = []byte(secret) + return jwtSecretCache +} + +// Claims JWT 声明 +type Claims struct { + Username string `json:"username"` + jwt.RegisteredClaims +} + +// GenerateJWT 生成 JWT +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(getJwtSecret()) +} + +// ParseJWT 解析 JWT +func ParseJWT(tokenString string) (*Claims, error) { + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return getJwtSecret(), 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") +} + +// ValidatePassword 校验密码复杂度 +// 要求: 至少8位,包含大小写字母、数字、特殊字符 +func ValidatePassword(pwd string) bool { + if len(pwd) < 8 { + return false + } + var hasUpper, hasLower, hasDigit, hasSpecial bool + for _, ch := range pwd { + switch { + case ch >= 'A' && ch <= 'Z': + hasUpper = true + case ch >= 'a' && ch <= 'z': + hasLower = true + case ch >= '0' && ch <= '9': + hasDigit = true + case strings.ContainsAny(string(ch), "!@#$%^&*()_+-=[]{}|;:,.<>?"): + hasSpecial = true + } + } + return hasUpper && hasLower && hasDigit && hasSpecial +} + +// HashPassword 加密密码 +func HashPassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +// CheckPassword 验证密码 +func CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// ================================================================ +// 登录 / 注册 / 修改密码 的业务逻辑 (供 handler 调用) +// ================================================================ + +// LoginRequest 登录请求 +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// LoginResponse 登录响应 +type LoginResponse struct { + Token string `json:"token"` +} + +// Login 登录业务逻辑 +func Login(username, password string) (string, error) { + user, err := db.GetUserByUsername(username) + if err != nil { + return "", errors.New("用户不存在") + } + if !CheckPassword(password, user.PasswordHash) { + return "", errors.New("密码错误") + } + return GenerateJWT(username) +} + +// RegisterRequest 注册请求 +type RegisterRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +// Register 注册业务逻辑 +func Register(username, password string) (string, error) { + // 检查用户名是否已存在 + exists, err := db.UserExists(username) + if err != nil { + return "", err + } + if exists { + return "", errors.New("用户名已存在") + } + // 密码复杂度校验 + if !ValidatePassword(password) { + return "", errors.New("密码不符合复杂度要求") + } + // 加密密码 + hash, err := HashPassword(password) + if err != nil { + return "", err + } + // 创建用户 + if err := db.CreateUser(username, hash); err != nil { + return "", err + } + // 生成 JWT + return GenerateJWT(username) +} + +// ChangePasswordRequest 修改密码请求 +type ChangePasswordRequest struct { + OldPassword string `json:"oldPassword"` + NewPassword string `json:"newPassword"` +} + +// ChangePassword 修改密码业务逻辑 +func ChangePassword(username, oldPassword, newPassword string) error { + user, err := db.GetUserByUsername(username) + if err != nil { + return errors.New("用户不存在") + } + if !CheckPassword(oldPassword, user.PasswordHash) { + return errors.New("原密码错误") + } + if !ValidatePassword(newPassword) { + return errors.New("新密码不符合复杂度要求") + } + hash, err := HashPassword(newPassword) + if err != nil { + return err + } + return db.UpdateUserPassword(username, hash) +} + +// HasUsers 检查是否存在用户 +func HasUsers() (bool, error) { + count, err := db.CountUsers() + if err != nil { + return false, err + } + return count > 0, nil +} + +// ================================================================ +// Gin 中间件 +// ================================================================ + +// AuthMiddleware 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 + } + parts := strings.SplitN(authHeader, " ", 2) + if len(parts) != 2 || parts[0] != "Bearer" { + c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "认证令牌格式错误"}) + c.Abort() + return + } + claims, err := ParseJWT(parts[1]) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "无效的认证令牌"}) + c.Abort() + return + } + c.Set("username", claims.Username) + c.Next() + } +} + +// GetUsernameFromContext 从 gin.Context 获取当前用户名 +func GetUsernameFromContext(c *gin.Context) string { + username, exists := c.Get("username") + if !exists { + return "" + } + return username.(string) +} diff --git a/db.go b/internal/db/db.go similarity index 67% rename from db.go rename to internal/db/db.go index 178abdb..f3ddca8 100644 --- a/db.go +++ b/internal/db/db.go @@ -1,4 +1,4 @@ -package main +package db import ( "crypto/rand" @@ -19,40 +19,11 @@ var DB *sql.DB const SchemaVersion = "v2" -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"` - WireProtocolV2 bool `json:"wireProtocolV2"` -} - -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"` -} - -type User struct { - ID int `json:"id"` - Username string `json:"username"` - PasswordHash string `json:"-"` - CreatedAt string `json:"createdAt"` -} +// ================================================================ +// 初始化 +// ================================================================ func InitDB() error { - // 确保 data 目录存在 if err := os.MkdirAll("./data", 0755); err != nil { return fmt.Errorf("创建数据目录失败: %w", err) } @@ -162,6 +133,46 @@ func createTables() error { return 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 +} + +// ================================================================ +// 迁移引擎 +// ================================================================ + func getCurrentSchemaVersion() string { var version string err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'schema_version'").Scan(&version) @@ -174,7 +185,7 @@ func getCurrentSchemaVersion() string { } return SchemaVersion } - log.Printf("⚠️ 读取 Schema 版本失败: %v", err) + log.Printf("⚠️ 读取 Schema 版本失败: %v", err) return "v1" } return version @@ -269,7 +280,7 @@ func runMigrations() error { targetSchema := getSchemaDef(targetVer) if targetSchema == nil { - return fmt.Errorf("目标 Schema 版本 %s 未在 schemaHistory 中定义", targetVer) + return fmt.Errorf("目标 Schema 版本 %s 未定义", targetVer) } if currentSchema == nil || schemaVersionsEqual(currentSchema, targetSchema) { @@ -287,7 +298,7 @@ func runMigrations() error { if backupPath != "" { log.Printf("❌ 迁移失败,尝试恢复备份: %s", backupPath) if restoreErr := restoreDatabase(backupPath); restoreErr != nil { - log.Printf("⚠️ 恢复备份失败: %v", restoreErr) + log.Printf("⚠️ 恢复备份失败: %v", restoreErr) } } return fmt.Errorf("重型迁移失败: %w", err) @@ -342,7 +353,7 @@ func heavyMigration(oldDef, newDef *SchemaVersionDef) error { return fmt.Errorf("重命名新表失败: %w", err) } if _, err := DB.Exec(fmt.Sprintf("DROP TABLE %s", tempTable)); err != nil { - log.Printf("⚠️ 删除临时表失败(不影响使用): %v", err) + log.Printf("⚠️ 删除临时表失败(不影响使用): %v", err) } log.Printf(" 表交换完成: %s (新表已生效)", oldTable) @@ -353,14 +364,7 @@ func buildCreateTableSQL(tableName string, def *SchemaVersionDef) string { var cols []string var primaryKey string - 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] + for name, col := range def.Columns { parts := []string{name, col.Type} if col.NotNull { parts = append(parts, "NOT NULL") @@ -383,7 +387,7 @@ func buildCreateTableSQL(tableName string, def *SchemaVersionDef) string { } func buildInsertSQL(oldTable, newTable string, oldDef, newDef *SchemaVersionDef) (string, error) { - newCols := make([]string, 0, len(newDef.Columns)) + var newCols []string for name := range newDef.Columns { newCols = append(newCols, name) } @@ -418,165 +422,3 @@ func buildInsertSQL(oldTable, newTable string, oldDef, newDef *SchemaVersionDef) oldTable, ), nil } - -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 -} - -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, - 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.HeartbeatInterval, &cfg.HeartbeatTimeout, &cfg.PoolCount, - &cfg.WireProtocolV2, - ) - if err != nil { - return nil, err - } - cfg.TcpMux = true - 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 = 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.TcpMuxKeepalive, cfg.HeartbeatInterval, cfg.HeartbeatTimeout, cfg.PoolCount, - cfg.WireProtocolV2) - return err -} - -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, rows.Err() -} - -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 { - result, 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) - if err != nil { - return err - } - id, _ := result.LastInsertId() - p.ID = int(id) - return nil -} - -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 -} - -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 UpdatePassword(username, passwordHash string) error { - _, err := DB.Exec(` - UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP - WHERE username = ? - `, passwordHash, username) - return err -} - -func CountUsers() (int, error) { - var count int - err := DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) - return count, err -} diff --git a/internal/db/models.go b/internal/db/models.go new file mode 100644 index 0000000..d7e7322 --- /dev/null +++ b/internal/db/models.go @@ -0,0 +1,36 @@ +package db + +// GlobalConfig 全局配置 +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"` + WireProtocolV2 bool `json:"wireProtocolV2"` +} + +// Proxy 隧道配置 +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"` +} + +// User 用户 +type User struct { + ID int `json:"id"` + Username string `json:"username"` + PasswordHash string `json:"-"` + CreatedAt string `json:"createdAt"` +} diff --git a/internal/db/repository.go b/internal/db/repository.go new file mode 100644 index 0000000..12b2af9 --- /dev/null +++ b/internal/db/repository.go @@ -0,0 +1,149 @@ +package db + +// ================================================================ +// 全局配置 +// ================================================================ + +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, + 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.HeartbeatInterval, &cfg.HeartbeatTimeout, &cfg.PoolCount, + &cfg.WireProtocolV2, + ) + if err != nil { + return nil, err + } + cfg.TcpMux = true + 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 = 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.TcpMuxKeepalive, cfg.HeartbeatInterval, cfg.HeartbeatTimeout, cfg.PoolCount, + cfg.WireProtocolV2) + return err +} + +// ================================================================ +// 隧道管理 +// ================================================================ + +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, rows.Err() +} + +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 { + result, 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) + if err != nil { + return err + } + id, _ := result.LastInsertId() + p.ID = int(id) + return nil +} + +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 +} + +// ================================================================ +// 用户管理 +// ================================================================ + +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 UserExists(username string) (bool, error) { + var count int + err := DB.QueryRow("SELECT COUNT(*) FROM users WHERE username = ?", username).Scan(&count) + return count > 0, err +} + +func CreateUser(username, passwordHash string) error { + _, err := DB.Exec(` + INSERT INTO users (username, password_hash) VALUES (?, ?) + `, username, passwordHash) + return err +} + +func UpdateUserPassword(username, passwordHash string) error { + _, err := DB.Exec(` + UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP + WHERE username = ? + `, passwordHash, username) + return err +} + +func CountUsers() (int, error) { + var count int + err := DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count) + return count, err +} diff --git a/db-history.go b/internal/db/schema.go similarity index 89% rename from db-history.go rename to internal/db/schema.go index 62417b9..bf80ac1 100644 --- a/db-history.go +++ b/internal/db/schema.go @@ -1,8 +1,8 @@ -package main +package db -// ============================================================ -// db-history.go - Schema 版本声明与字段映射 -// ============================================================ +// ================================================================ +// Schema 版本声明 +// ================================================================ type SchemaVersionDef struct { Version string @@ -18,7 +18,7 @@ type ColumnDef struct { } var schemaHistory = []SchemaVersionDef{ - // v1:初始版本 + // v1: 初始版本 { Version: "v1", TableName: "proxies", @@ -34,7 +34,7 @@ var schemaHistory = []SchemaVersionDef{ "updated_at": {Type: "DATETIME", Default: "CURRENT_TIMESTAMP"}, }, }, - // v2:当前版本(frpc-console 2.0 LTS) + // v2: 当前版本 { Version: "v2", TableName: "proxies", diff --git a/internal/frp/binary.go b/internal/frp/binary.go new file mode 100644 index 0000000..5435937 --- /dev/null +++ b/internal/frp/binary.go @@ -0,0 +1,85 @@ +package frp + +import ( + "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "sync" +) + +//go:embed bin/* +var embeddedFrpc embed.FS + +var ( + cachedFrpcPath string + frpcPathMutex sync.Mutex +) + +// GetFrpcPath 获取 frpc 二进制路径 +// 优先级: 本地缓存 > 内嵌二进制 > 系统 PATH +func GetFrpcPath() (string, error) { + frpcPathMutex.Lock() + defer frpcPathMutex.Unlock() + + if cachedFrpcPath != "" { + if _, err := os.Stat(cachedFrpcPath); err == nil { + return cachedFrpcPath, nil + } + cachedFrpcPath = "" + } + + var fileName string + switch { + case runtime.GOOS == "windows" && runtime.GOARCH == "amd64": + fileName = "frpc_windows_amd64.exe" + case runtime.GOOS == "linux" && runtime.GOARCH == "amd64": + fileName = "frpc_linux_amd64" + case runtime.GOOS == "linux" && runtime.GOARCH == "arm64": + fileName = "frpc_linux_arm64" + case runtime.GOOS == "linux" && runtime.GOARCH == "arm": + fileName = "frpc_linux_arm_hf" + default: + path, err := exec.LookPath("frpc") + if err == nil { + cachedFrpcPath = path + return path, nil + } + return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH) + } + + // 尝试从本地 bin 目录加载 + localPath := filepath.Join(".", "bin", fileName) + if _, err := os.Stat(localPath); err == nil { + cachedFrpcPath = localPath + return localPath, nil + } + + // 尝试从 embed 提取到临时目录 + data, err := embeddedFrpc.ReadFile("bin/" + fileName) + if err == nil { + tmpPath := filepath.Join(os.TempDir(), "frpc") + if runtime.GOOS == "windows" { + tmpPath += ".exe" + } + if err := os.WriteFile(tmpPath, data, 0755); err == nil { + cachedFrpcPath = tmpPath + return tmpPath, nil + } + if _, statErr := os.Stat(tmpPath); statErr == nil { + cachedFrpcPath = tmpPath + return tmpPath, nil + } + } + + // 最后尝试从系统 PATH 查找 + path, err := exec.LookPath("frpc") + if err == nil { + cachedFrpcPath = path + return path, nil + } + + return "", fmt.Errorf("未找到 frpc 文件") +} diff --git a/internal/frp/legacy.go b/internal/frp/legacy.go new file mode 100644 index 0000000..1a83e15 --- /dev/null +++ b/internal/frp/legacy.go @@ -0,0 +1,285 @@ +package frp + +import ( + "context" + "log" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "syscall" + + "frpc-console/internal/process" +) + +// ================================================================ +// 兼容层:保持对外接口不变 +// 这些函数供 api 和外部调用,实际委托给 process.Manager +// ================================================================ + +// IsRunning 检查 frpc 是否在运行 +func IsRunning() bool { + pm := process.GetGlobalManager() + if pm != nil { + status, err := pm.Status() + if err != nil { + log.Printf("[WARN] ProcessManager.Status() 失败: %v,降级到 PID 文件", err) + return isRunningLegacy() + } + return status.State == "running" + } + return isRunningLegacy() +} + +// Start 启动 frpc (幂等) +func Start() error { + pm := process.GetGlobalManager() + if pm != nil { + log.Println("[INFO] 使用 ProcessManager 启动 frpc") + return pm.Start(context.Background()) + } + log.Println("[WARN] ProcessManager 未初始化,使用兼容模式启动 frpc") + return startLegacy() +} + +// Stop 停止 frpc (幂等) +func Stop() error { + pm := process.GetGlobalManager() + if pm != nil { + log.Println("[INFO] 使用 ProcessManager 停止 frpc") + return pm.Stop(context.Background()) + } + log.Println("[WARN] ProcessManager 未初始化,使用兼容模式停止 frpc") + return stopLegacy() +} + +// Restart 重启 frpc (原子操作) +func Restart() error { + pm := process.GetGlobalManager() + if pm != nil { + log.Println("[INFO] 使用 ProcessManager 重启 frpc") + return pm.Restart(context.Background()) + } + log.Println("[WARN] ProcessManager 未初始化,使用兼容模式重启 frpc") + if err := stopLegacy(); err != nil { + return err + } + return startLegacy() +} + +// GetStatus 获取 frpc 详细状态 (供 API 调用) +func GetStatus() (map[string]interface{}, error) { + pm := process.GetGlobalManager() + if pm != nil { + status, err := pm.Status() + if err != nil { + return nil, err + } + return map[string]interface{}{ + "state": status.State, + "pid": status.PID, + "port": status.Port, + }, nil + } + + running := isRunningLegacy() + return map[string]interface{}{ + "state": map[bool]string{true: "running", false: "stopped"}[running], + "pid": 0, + "port": 0, + "legacy": true, + }, nil +} + +// Reload 热加载 frpc 配置 +func Reload() error { + pm := process.GetGlobalManager() + if pm == nil { + return reloadLegacy() + } + + status, err := pm.Status() + if err != nil { + return err + } + + if status.State != "running" { + return pm.Start(context.Background()) + } + + frpcPath, err := GetFrpcPath() + if err != nil { + return err + } + + cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) + log.Printf(" reload 输出: %s", string(output)) + if err := pm.Restart(context.Background()); err != nil { + return err + } + return nil + } + + log.Printf("✅ frpc 热加载成功: %s", string(output)) + return nil +} + +// ================================================================ +// 旧版实现 (降级方案) +// ================================================================ + +func isRunningLegacy() bool { + pidData, err := os.ReadFile("./data/frpc.pid") + if err != nil { + return false + } + pid, err := strconv.Atoi(strings.TrimSpace(string(pidData))) + if err != nil { + return false + } + + if runtime.GOOS == "windows" { + cmd := exec.Command("tasklist", "/FI", "PID eq", strconv.Itoa(pid)) + output, err := cmd.CombinedOutput() + if err != nil { + return false + } + return strings.Contains(string(output), strconv.Itoa(pid)) + } + + proc, err := os.FindProcess(pid) + if err != nil { + return false + } + return proc.Signal(syscall.Signal(0)) == nil +} + +func startLegacy() error { + frpcPath, err := GetFrpcPath() + if err != nil { + return err + } + + if err := os.MkdirAll("./data", 0755); err != nil { + return err + } + + if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) { + if err := GenerateConfig(); err != nil { + return err + } + } + + if isRunningLegacy() { + return nil + } + + os.Remove("./data/frpc.pid") + + cmd := exec.Command(frpcPath, "-c", "./data/frpc.toml") + setWindowHide(cmd) + setSysProcAttr(cmd) + + logFile, err := os.OpenFile("./data/frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) + if err != nil { + return err + } + cmd.Stdout = logFile + cmd.Stderr = logFile + + if err := cmd.Start(); err != nil { + return err + } + + go func() { + if err := cmd.Wait(); err != nil { + log.Printf("frpc 子进程退出: %v", err) + } + os.Remove("./data/frpc.pid") + }() + + return os.WriteFile("./data/frpc.pid", []byte(strconv.Itoa(cmd.Process.Pid)), 0644) +} + +func stopLegacy() error { + if runtime.GOOS == "windows" { + cmd := exec.Command("taskkill", "/F", "/IM", "frpc.exe") + if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") { + return err + } + os.Remove("./data/frpc.pid") + return nil + } + + pidData, err := os.ReadFile("./data/frpc.pid") + if err != nil { + cmd := exec.Command("pkill", "-f", "frpc") + if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") { + return err + } + return nil + } + + pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData))) + proc, err := os.FindProcess(pid) + if err != nil { + os.Remove("./data/frpc.pid") + return nil + } + + if err := proc.Kill(); err != nil { + return err + } + + os.Remove("./data/frpc.pid") + return nil +} + +func reloadLegacy() error { + if !isRunningLegacy() { + return startLegacy() + } + + frpcPath, err := GetFrpcPath() + if err != nil { + return err + } + + cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml") + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("⚠️ 热加载失败 (%v),降级为重启 frpc", err) + log.Printf(" reload 输出: %s", string(output)) + if stopErr := stopLegacy(); stopErr != nil { + return stopErr + } + return startLegacy() + } + + log.Printf("✅ frpc 热加载成功 (兼容模式): %s", string(output)) + return nil +} + +// ================================================================ +// 辅助函数 (平台相关) +// ================================================================ + +func setWindowHide(cmd *exec.Cmd) { + if runtime.GOOS == "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + } + } +} + +func setSysProcAttr(cmd *exec.Cmd) { + if runtime.GOOS != "windows" { + cmd.SysProcAttr = &syscall.SysProcAttr{ + Setpgid: true, + } + } +} diff --git a/main.go b/main.go index e5970ce..f5e7afe 100644 --- a/main.go +++ b/main.go @@ -6,20 +6,26 @@ import ( "os" "strings" "time" -) -var ( - // 全局进程管理器 - processManager *ProcessManager + "frpc-console/internal/api" + "frpc-console/internal/db" + "frpc-console/internal/frp" + "frpc-console/internal/process" ) func main() { - // 确保 data 目录存在 + log.SetFlags(log.LstdFlags | log.Lshortfile) + + // ============================================================ + // 1. 初始化数据目录 + // ============================================================ if err := os.MkdirAll("./data", 0755); err != nil { - log.Printf("⚠️ 创建 data 目录失败: %v", err) + log.Printf("⚠️ 创建 data 目录失败: %v", err) } - // 读取 version.ini 显示版本 + // ============================================================ + // 2. 显示版本信息 + // ============================================================ if data, err := os.ReadFile("./data/version.ini"); err == nil { version := strings.TrimSpace(string(data)) log.Printf("📌 版本: %s", version) @@ -27,37 +33,39 @@ func main() { log.Printf("📌 版本: (未记录)") } - if err := InitDB(); err != nil { + // ============================================================ + // 3. 初始化数据库 + // ============================================================ + if err := db.InitDB(); err != nil { log.Fatal("❌ 数据库初始化失败:", err) } - if err := GenerateFrpcConfig(); err != nil { - log.Println("⚠️ 生成配置文件失败:", err) + // ============================================================ + // 4. 生成 frpc 配置文件 + // ============================================================ + if err := frp.GenerateConfig(); err != nil { + log.Println("⚠️ 生成配置文件失败:", err) } // ============================================================ - // 2.6-preview: 使用 ProcessManager 管理 frpc 进程 + // 5. 创建进程管理器并启动 frpc // ============================================================ - processManager = NewProcessManager( - "./data", // 数据目录 - "./data/frpc.toml", // 配置文件路径 - "./data/frpc", // frpc 二进制路径 - ) + pm := process.NewManager("./data", "./data/frpc.toml") + process.SetGlobalManager(pm) // 加载 admin_port 配置 - if err := processManager.LoadConfig(); err != nil { - log.Printf("⚠️ 加载 admin_port 配置失败: %v (将使用 PID 文件模式)", err) + if err := pm.LoadConfig(); err != nil { + log.Printf("⚠️ 加载 admin_port 配置失败: %v (将使用 PID 文件模式)", err) } else { - log.Printf("📌 admin_port: %d", processManager.adminPort) + log.Printf("📌 admin_port: %d", pm.AdminPort()) } // 启动 frpc (幂等) ctx := context.Background() - if err := processManager.Start(ctx); err != nil { - log.Printf("⚠️ 启动 frpc 失败: %v", err) + if err := pm.Start(ctx); err != nil { + log.Printf("⚠️ 启动 frpc 失败: %v", err) } else { - // 查询并显示启动状态 - if status, err := processManager.Status(); err == nil { + if status, err := pm.Status(); err == nil { log.Printf("✅ frpc 状态: %s", status.State) if status.PID > 0 { log.Printf(" PID: %d, 端口: %d", status.PID, status.Port) @@ -66,40 +74,42 @@ func main() { } // ============================================================ - // 启动看门狗 (使用 ProcessManager 检测状态) + // 6. 启动看门狗 (自动恢复) // ============================================================ - go startWatchdog() + go startWatchdog(pm) - r := SetupRouter() + // ============================================================ + // 7. 启动 HTTP 服务 + // ============================================================ + router := api.SetupRouter() log.Println("🚀 frpc-console 启动成功!") log.Println("📍 访问地址: http://localhost:9300") log.Println("📍 API 地址: http://localhost:9300/api") - if err := r.Run(":9300"); err != nil { + if err := router.Run(":9300"); err != nil { log.Fatal("❌ 服务启动失败:", err) } } // ================================================================ -// 看门狗 (使用 ProcessManager) +// 看门狗 // ================================================================ -func startWatchdog() { +func startWatchdog(pm *process.Manager) { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for range ticker.C { - // 使用 ProcessManager 检查状态 - status, err := processManager.Status() + status, err := pm.Status() if err != nil { - log.Printf("⚠️ 检查 frpc 状态失败: %v", err) + log.Printf("⚠️ 检查 frpc 状态失败: %v", err) continue } if status.State != "running" { - log.Printf("⚠️ frpc 进程已停止 (状态: %s),自动重启...", status.State) + log.Printf("⚠️ frpc 进程已停止 (状态: %s),自动重启...", status.State) ctx := context.Background() - if err := processManager.Start(ctx); err != nil { + if err := pm.Start(ctx); err != nil { log.Printf("❌ 自动重启 frpc 失败: %v", err) } else { log.Println("✅ frpc 已自动重启") diff --git a/process_manager.go b/process_manager.go deleted file mode 100644 index aa6dd9a..0000000 --- a/process_manager.go +++ /dev/null @@ -1,620 +0,0 @@ -// process_manager.go -// frpc-console 进程管理模块 -// 2.6-preview: 端口检测 + 单实例锁定 + 状态自述 - -package main - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "sync" - "syscall" - "time" - - "golang.org/x/sys/unix" -) - -// ================================================================ -// 常量定义 -// ================================================================ - -const ( - LockFileName = ".frpc.lock" - PortCheckTimeout = 500 * time.Millisecond - StartWaitTime = 500 * time.Millisecond - StopMaxWaitTime = 5 * time.Second - LockAcquireTimeout = 30 * time.Second - LockRetryInterval = 100 * time.Millisecond - APITimeout = 2 * time.Second -) - -// ================================================================ -// 数据结构 -// ================================================================ - -type PortStatus struct { - Port int `json:"port"` - Occupied bool `json:"occupied"` - PID int `json:"pid"` - IsFRPC bool `json:"is_frpc"` - ProcessCmd string `json:"process_cmd,omitempty"` -} - -type ProcessStatus struct { - 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"` -} - -type FRPCStatus struct { - Version string `json:"version"` - RunID string `json:"run_id"` - Proxies []struct { - Name string `json:"name"` - Type string `json:"type"` - Status string `json:"status"` - LocalAddr string `json:"local_addr"` - } `json:"proxies"` -} - -// ================================================================ -// ProcessManager 主结构 -// ================================================================ - -type ProcessManager struct { - mu sync.Mutex - dataDir string - configPath string - frpcBinPath string - adminPort int - lockFile *os.File - locked bool -} - -func NewProcessManager(dataDir, configPath, frpcBinPath string) *ProcessManager { - return &ProcessManager{ - dataDir: dataDir, - configPath: configPath, - frpcBinPath: frpcBinPath, - adminPort: 0, - } -} - -// ================================================================ -// 配置读取 -// ================================================================ - -func (pm *ProcessManager) LoadConfig() error { - content, err := os.ReadFile(pm.configPath) - if err != nil { - return fmt.Errorf("读取配置文件失败: %w", err) - } - if port := extractIntValue(string(content), "admin_port"); port > 0 { - pm.adminPort = port - return nil - } - if port := extractIntValueFromSection(string(content), "webServer", "port"); port > 0 { - pm.adminPort = port - return nil - } - return fmt.Errorf("未找到 admin_port 或 webServer.port 配置") -} - -func extractIntValue(content, key string) int { - lines := strings.Split(content, "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, key) { - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) == 2 { - val := strings.TrimSpace(parts[1]) - val = strings.Trim(val, `"`) - if port, err := strconv.Atoi(val); err == nil && port > 0 { - return port - } - } - } - } - return 0 -} - -func extractIntValueFromSection(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, "]") { - inSection = strings.Trim(trimmed, "[]") == 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 port, err := strconv.Atoi(val); err == nil && port > 0 { - return port - } - } - } - } - return 0 -} - -// ================================================================ -// 端口检测 -// ================================================================ - -func (pm *ProcessManager) CheckPort() (bool, error) { - if pm.adminPort <= 0 { - return false, fmt.Errorf("admin_port 未配置") - } - conn, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", pm.adminPort), PortCheckTimeout) - if err != nil { - return false, nil - } - conn.Close() - return true, nil -} - -func (pm *ProcessManager) GetPortStatus() (*PortStatus, error) { - status := &PortStatus{Port: pm.adminPort, Occupied: false, PID: 0, IsFRPC: false} - occupied, err := pm.CheckPort() - if err != nil { - return status, err - } - status.Occupied = occupied - if !occupied { - return status, nil - } - - pid, err := pm.getPIDByPort(pm.adminPort) - if err != nil { - if pidFromFile := pm.readPIDFile(); pidFromFile > 0 { - if pm.isProcessListeningOnPort(pidFromFile, pm.adminPort) { - status.PID = pidFromFile - } - } - } else { - status.PID = pid - } - if status.PID == 0 { - return status, nil - } - isFRPC, cmd := pm.isFRPCProcess(status.PID) - status.IsFRPC = isFRPC - status.ProcessCmd = cmd - return status, nil -} - -func (pm *ProcessManager) getPIDByPort(port int) (int, error) { - if pid, err := pm.getPIDBySS(port); err == nil && pid > 0 { - return pid, nil - } - if pid, err := pm.getPIDByNetstat(port); err == nil && pid > 0 { - return pid, nil - } - if pid, err := pm.getPIDByLsof(port); err == nil && pid > 0 { - return pid, nil - } - return 0, fmt.Errorf("无法通过端口反查 PID") -} - -func (pm *ProcessManager) getPIDBySS(port int) (int, error) { - cmd := exec.Command("ss", "-lpn", "state", "listening") - out, err := cmd.Output() - if err != nil { - return 0, err - } - scanner := bufio.NewScanner(bytes.NewReader(out)) - for scanner.Scan() { - line := scanner.Text() - if !strings.Contains(line, fmt.Sprintf(":%d", port)) { - continue - } - if idx := strings.Index(line, "pid="); idx != -1 { - end := strings.Index(line[idx:], ",") - if end == -1 { - end = strings.Index(line[idx:], ")") - } - if end == -1 { - continue - } - pidStr := line[idx+4 : idx+end] - pidStr = strings.TrimSpace(pidStr) - if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 { - return pid, nil - } - } - } - return 0, fmt.Errorf("未找到监听端口 %d 的进程", port) -} - -func (pm *ProcessManager) getPIDByNetstat(port int) (int, error) { - cmd := exec.Command("netstat", "-tulpn") - out, err := cmd.Output() - if err != nil { - return 0, err - } - scanner := bufio.NewScanner(bytes.NewReader(out)) - for scanner.Scan() { - line := scanner.Text() - if !strings.Contains(line, fmt.Sprintf(":%d", port)) { - continue - } - parts := strings.Fields(line) - if len(parts) < 7 { - continue - } - last := parts[len(parts)-1] - if idx := strings.Index(last, "/"); idx != -1 { - pidStr := last[:idx] - if pid, err := strconv.Atoi(pidStr); err == nil && pid > 0 { - return pid, nil - } - } - } - return 0, fmt.Errorf("未找到监听端口 %d 的进程", port) -} - -func (pm *ProcessManager) getPIDByLsof(port int) (int, error) { - cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN") - out, err := cmd.Output() - if err != nil { - return 0, err - } - scanner := bufio.NewScanner(bytes.NewReader(out)) - for scanner.Scan() { - line := scanner.Text() - if strings.Contains(line, "frpc") { - parts := strings.Fields(line) - if len(parts) >= 2 { - if pid, err := strconv.Atoi(parts[1]); err == nil && pid > 0 { - return pid, nil - } - } - } - } - return 0, fmt.Errorf("未找到监听端口 %d 的 frpc 进程", port) -} - -func (pm *ProcessManager) isProcessListeningOnPort(pid, port int) bool { - cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-a", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN") - out, err := cmd.Output() - if err != nil { - return false - } - return strings.Contains(string(out), "LISTEN") -} - -func (pm *ProcessManager) isFRPCProcess(pid int) (bool, string) { - cmdlinePath := fmt.Sprintf("/proc/%d/cmdline", pid) - if data, err := os.ReadFile(cmdlinePath); err == nil { - cmd := strings.ReplaceAll(string(data), "\x00", " ") - if strings.Contains(cmd, "frpc") { - return true, cmd - } - } - cmd := exec.Command("ps", "-p", strconv.Itoa(pid), "-o", "args=") - out, err := cmd.Output() - if err == nil { - args := strings.TrimSpace(string(out)) - if strings.Contains(args, "frpc") { - return true, args - } - } - return false, "" -} - -// ================================================================ -// 互斥锁 (flock) -// ================================================================ - -func (pm *ProcessManager) Lock() error { - pm.mu.Lock() - defer pm.mu.Unlock() - if pm.locked { - return nil - } - lockPath := filepath.Join(pm.dataDir, LockFileName) - if err := os.MkdirAll(pm.dataDir, 0755); err != nil { - return fmt.Errorf("创建数据目录失败: %w", err) - } - file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644) - if err != nil { - return fmt.Errorf("打开锁文件失败: %w", err) - } - start := time.Now() - for { - err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB) - if err == nil { - pm.lockFile = file - pm.locked = true - return nil - } - if err != syscall.EWOULDBLOCK { - file.Close() - return fmt.Errorf("获取锁失败: %w", err) - } - if time.Since(start) > LockAcquireTimeout { - file.Close() - return fmt.Errorf("获取锁超时 (超过 %v)", LockAcquireTimeout) - } - time.Sleep(LockRetryInterval) - } -} - -func (pm *ProcessManager) Unlock() error { - pm.mu.Lock() - defer pm.mu.Unlock() - if !pm.locked { - return nil - } - if pm.lockFile != nil { - unix.Flock(int(pm.lockFile.Fd()), unix.LOCK_UN) - pm.lockFile.Close() - pm.lockFile = nil - } - pm.locked = false - return nil -} - -// ================================================================ -// PID 文件操作 -// ================================================================ - -func (pm *ProcessManager) pidFilePath() string { - return filepath.Join(pm.dataDir, "frpc.pid") -} - -func (pm *ProcessManager) readPIDFile() int { - data, err := os.ReadFile(pm.pidFilePath()) - if err != nil { - return 0 - } - pid, err := strconv.Atoi(strings.TrimSpace(string(data))) - if err != nil || pid <= 0 { - return 0 - } - return pid -} - -func (pm *ProcessManager) writePIDFile(pid int) error { - return os.WriteFile(pm.pidFilePath(), []byte(strconv.Itoa(pid)), 0644) -} - -func (pm *ProcessManager) deletePIDFile() error { - err := os.Remove(pm.pidFilePath()) - if os.IsNotExist(err) { - return nil - } - return err -} - -// ================================================================ -// 状态查询 -// ================================================================ - -func (pm *ProcessManager) Status() (*ProcessStatus, error) { - status := &ProcessStatus{State: "unknown", PID: 0, Port: pm.adminPort} - if pm.adminPort <= 0 { - status.Error = "admin_port 未配置" - return status, nil - } - portStatus, err := pm.GetPortStatus() - if err != nil { - status.Error = err.Error() - return status, nil - } - if !portStatus.Occupied { - pm.deletePIDFile() - status.State = "stopped" - return status, nil - } - if !portStatus.IsFRPC { - status.State = "conflict" - status.PID = portStatus.PID - status.Error = fmt.Sprintf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID) - return status, nil - } - status.State = "running" - status.PID = portStatus.PID - pm.writePIDFile(portStatus.PID) - if info := pm.getFRPCStatus(portStatus.PID); info != nil { - status.Version = info.Version - } - return status, nil -} - -// ================================================================ -// API 回源 (2.7-preview 预留) -// ================================================================ - -func (pm *ProcessManager) getFRPCStatus(pid int) *FRPCStatus { - if pid <= 0 || pm.adminPort <= 0 { - return nil - } - url := fmt.Sprintf("http://127.0.0.1:%d/api/status", pm.adminPort) - ctx, cancel := context.WithTimeout(context.Background(), APITimeout) - defer cancel() - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil - } - client := &http.Client{Timeout: APITimeout} - resp, err := client.Do(req) - if err != nil { - return nil - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - return nil - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil - } - var status FRPCStatus - if err := json.Unmarshal(body, &status); err != nil { - return nil - } - return &status -} - -// ================================================================ -// 操作执行 (Start / Stop / Restart) -// ================================================================ - -func (pm *ProcessManager) Start(ctx context.Context) error { - if err := pm.Lock(); err != nil { - return fmt.Errorf("获取锁失败: %w", err) - } - defer pm.Unlock() - return pm.startLocked(ctx) -} - -func (pm *ProcessManager) startLocked(ctx context.Context) error { - portStatus, err := pm.GetPortStatus() - if err != nil { - return fmt.Errorf("检测端口状态失败: %w", err) - } - if portStatus.Occupied { - if portStatus.IsFRPC { - pm.writePIDFile(portStatus.PID) - return nil - } - return fmt.Errorf("端口 %d 被非 frpc 进程占用 (PID: %d)", pm.adminPort, portStatus.PID) - } - - cmd := exec.CommandContext(ctx, pm.frpcBinPath, "-c", pm.configPath) - - // 根据平台设置 SysProcAttr - setProcessAttributes(cmd) - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Start(); err != nil { - return fmt.Errorf("启动 frpc 失败: %w", err) - } - time.Sleep(StartWaitTime) - occupied, err := pm.CheckPort() - if err != nil { - return fmt.Errorf("验证启动状态失败: %w", err) - } - if !occupied { - return fmt.Errorf("frpc 启动失败: 端口未监听") - } - pid, _ := pm.getPIDByPort(pm.adminPort) - if pid > 0 { - pm.writePIDFile(pid) - } - return nil -} - -func (pm *ProcessManager) Stop(ctx context.Context) error { - if err := pm.Lock(); err != nil { - return fmt.Errorf("获取锁失败: %w", err) - } - defer pm.Unlock() - return pm.stopLocked(ctx) -} - -func (pm *ProcessManager) stopLocked(ctx context.Context) error { - portStatus, err := pm.GetPortStatus() - if err != nil { - return fmt.Errorf("检测端口状态失败: %w", err) - } - if !portStatus.Occupied { - pm.deletePIDFile() - return nil - } - var pid int - if portStatus.IsFRPC { - pid = portStatus.PID - } else { - return fmt.Errorf("端口 %d 被非 frpc 进程占用, 无法安全停止", pm.adminPort) - } - if pid <= 0 { - pid = pm.readPIDFile() - if pid <= 0 { - return fmt.Errorf("无法确定 frpc 进程 PID") - } - } - proc, err := os.FindProcess(pid) - if err != nil { - pm.deletePIDFile() - return nil - } - if err := proc.Signal(syscall.SIGTERM); err != nil { - pm.deletePIDFile() - return nil - } - start := time.Now() - for time.Since(start) < StopMaxWaitTime { - occupied, _ := pm.CheckPort() - if !occupied { - pm.deletePIDFile() - return nil - } - time.Sleep(200 * time.Millisecond) - } - proc.Kill() - time.Sleep(500 * time.Millisecond) - if occupied, _ := pm.CheckPort(); occupied { - return fmt.Errorf("强制停止失败: 端口仍被占用") - } - pm.deletePIDFile() - return nil -} - -func (pm *ProcessManager) Restart(ctx context.Context) error { - if err := pm.Lock(); err != nil { - return fmt.Errorf("获取锁失败: %w", err) - } - defer pm.Unlock() - if err := pm.stopLocked(ctx); err != nil { - return fmt.Errorf("停止失败: %w", err) - } - if err := pm.startLocked(ctx); err != nil { - return fmt.Errorf("启动失败: %w", err) - } - return nil -} - -// ================================================================ -// 健康检查 -// ================================================================ - -func (pm *ProcessManager) HealthCheck() map[string]interface{} { - result := map[string]interface{}{"admin_port": pm.adminPort} - status, err := pm.Status() - if err != nil { - result["state"] = "error" - result["error"] = err.Error() - return result - } - result["state"] = status.State - result["pid"] = status.PID - if status.Version != "" { - result["version"] = status.Version - } - if status.Error != "" { - result["error"] = status.Error - } - return result -} diff --git a/process_manager_linux.go b/process_manager_linux.go deleted file mode 100644 index b5c8d4e..0000000 --- a/process_manager_linux.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build linux - -package main - -import ( - "os/exec" - "syscall" -) - -// setProcessAttributes 设置 Linux 进程属性 (Setpgid) -func setProcessAttributes(cmd *exec.Cmd) { - cmd.SysProcAttr = &syscall.SysProcAttr{ - Setpgid: true, - } -} diff --git a/process_manager_lock_linux.go b/process_manager_lock_linux.go deleted file mode 100644 index 99ac018..0000000 --- a/process_manager_lock_linux.go +++ /dev/null @@ -1,65 +0,0 @@ -//go:build linux - -package main - -import ( - "fmt" - "os" - "path/filepath" - "time" - - "golang.org/x/sys/unix" -) - -func (pm *ProcessManager) Lock() error { - pm.mu.Lock() - defer pm.mu.Unlock() - - if pm.locked { - return nil - } - - lockPath := filepath.Join(pm.dataDir, LockFileName) - if err := os.MkdirAll(pm.dataDir, 0755); err != nil { - return fmt.Errorf("创建数据目录失败: %w", err) - } - file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0644) - if err != nil { - return fmt.Errorf("打开锁文件失败: %w", err) - } - - start := time.Now() - for { - err := unix.Flock(int(file.Fd()), unix.LOCK_EX|unix.LOCK_NB) - if err == nil { - pm.lockFile = file - pm.locked = true - return nil - } - if err != unix.EWOULDBLOCK { - file.Close() - return fmt.Errorf("获取锁失败: %w", err) - } - if time.Since(start) > LockAcquireTimeout { - file.Close() - return fmt.Errorf("获取锁超时 (超过 %v)", LockAcquireTimeout) - } - time.Sleep(LockRetryInterval) - } -} - -func (pm *ProcessManager) Unlock() error { - pm.mu.Lock() - defer pm.mu.Unlock() - - if !pm.locked { - return nil - } - if pm.lockFile != nil { - unix.Flock(int(pm.lockFile.Fd()), unix.LOCK_UN) - pm.lockFile.Close() - pm.lockFile = nil - } - pm.locked = false - return nil -} diff --git a/process_manager_lock_windows.go b/process_manager_lock_windows.go deleted file mode 100644 index fe4bd99..0000000 --- a/process_manager_lock_windows.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build windows - -package main - -func (pm *ProcessManager) Lock() error { - pm.mu.Lock() - defer pm.mu.Unlock() - - if pm.locked { - return nil - } - - // Windows 上暂用内存锁模拟(进程间不互斥,仅同一进程内互斥) - // 如需真正的进程间锁,后续可改用 Windows Named Mutex - pm.locked = true - return nil -} - -func (pm *ProcessManager) Unlock() error { - pm.mu.Lock() - defer pm.mu.Unlock() - - if !pm.locked { - return nil - } - pm.locked = false - return nil -} diff --git a/process_manager_other.go b/process_manager_other.go deleted file mode 100644 index 8117554..0000000 --- a/process_manager_other.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !linux - -package main - -import ( - "os/exec" -) - -// setProcessAttributes 非 Linux 平台 (Windows/macOS) 不做特殊设置 -func setProcessAttributes(cmd *exec.Cmd) { - // 非 Linux 平台不需要 Setpgid -} diff --git a/toml_parser.go b/toml_parser.go deleted file mode 100644 index f39f44f..0000000 --- a/toml_parser.go +++ /dev/null @@ -1,181 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "strings" -) - -// FrpcToml 对应 frpc.toml 的完整结构 -type FrpcToml struct { - ServerAddr string `json:"serverAddr"` - ServerPort int `json:"serverPort"` - Auth struct { - Token string `json:"token"` - } `json:"auth"` - Log struct { - To string `json:"to"` - Level string `json:"level"` - MaxDays int `json:"maxDays"` - } `json:"log"` - Transport struct { - TcpMux bool `json:"tcpMux"` - TcpMuxKeepalive int `json:"tcpMuxKeepalive"` - HeartbeatInterval int `json:"heartbeatInterval"` - HeartbeatTimeout int `json:"heartbeatTimeout"` - PoolCount int `json:"poolCount"` - } `json:"transport"` - Proxies []TomlProxy `json:"proxies"` -} - -// TomlProxy 对应 [[proxies]] 条目 -type TomlProxy struct { - Name string `json:"name"` - Type string `json:"type"` - LocalIP string `json:"localIP"` - LocalPort int `json:"localPort"` - RemotePort int `json:"remotePort"` - Enabled bool `json:"enabled"` // 导入时默认 true -} - -// ParseToml 解析 frpc.toml 内容 -func ParseToml(content string) (*FrpcToml, error) { - lines := strings.Split(content, "\n") - result := &FrpcToml{ - Proxies: []TomlProxy{}, - Transport: struct { - TcpMux bool `json:"tcpMux"` - TcpMuxKeepalive int `json:"tcpMuxKeepalive"` - HeartbeatInterval int `json:"heartbeatInterval"` - HeartbeatTimeout int `json:"heartbeatTimeout"` - PoolCount int `json:"poolCount"` - }{ - TcpMux: true, // 默认值 - TcpMuxKeepalive: 30, - HeartbeatInterval: 15, - HeartbeatTimeout: 70, - PoolCount: 8, - }, - } - - // 简单状态机解析 - var currentProxy *TomlProxy - inProxies := false - - for _, rawLine := range lines { - line := strings.TrimSpace(rawLine) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - // 检测 [[proxies]] 段开始 - if strings.HasPrefix(line, "[[proxies]]") { - inProxies = true - currentProxy = &TomlProxy{ - Type: "tcp", - Enabled: true, - } - result.Proxies = append(result.Proxies, *currentProxy) - // 注意:由于值传递,需要取最后一个元素的指针 - currentProxy = &result.Proxies[len(result.Proxies)-1] - continue - } - - // 检测其他段头(忽略,我们的解析器只关心具体键值对) - if strings.HasPrefix(line, "[") { - inProxies = false - currentProxy = nil - continue - } - - // 解析键值对 - if strings.Contains(line, "=") { - parts := strings.SplitN(line, "=", 2) - key := strings.TrimSpace(parts[0]) - value := strings.TrimSpace(parts[1]) - // 去掉引号 - value = strings.Trim(value, `"`) - - if inProxies && currentProxy != nil { - // 解析隧道字段 - switch key { - case "name": - currentProxy.Name = value - case "type": - currentProxy.Type = value - case "localIP": - currentProxy.LocalIP = value - case "localPort": - currentProxy.LocalPort, _ = strconv.Atoi(value) - case "remotePort": - currentProxy.RemotePort, _ = strconv.Atoi(value) - } - } else { - // 解析全局字段 - switch key { - case "serverAddr": - result.ServerAddr = value - case "serverPort": - result.ServerPort, _ = strconv.Atoi(value) - case "token": - result.Auth.Token = value - case "level": - result.Log.Level = value - case "maxDays": - result.Log.MaxDays, _ = strconv.Atoi(value) - case "tcpMux": - result.Transport.TcpMux = value == "true" - case "tcpMuxKeepaliveInterval": - result.Transport.TcpMuxKeepalive, _ = strconv.Atoi(value) - case "heartbeatInterval": - result.Transport.HeartbeatInterval, _ = strconv.Atoi(value) - case "heartbeatTimeout": - result.Transport.HeartbeatTimeout, _ = strconv.Atoi(value) - case "poolCount": - result.Transport.PoolCount, _ = strconv.Atoi(value) - } - } - } - } - - if result.ServerAddr == "" { - return nil, fmt.Errorf("未找到 serverAddr 字段") - } - if len(result.Proxies) == 0 { - return nil, fmt.Errorf("未找到任何隧道条目") - } - - return result, nil -} - -// ToGlobalConfig 将解析结果转换为 GlobalConfig -func (f *FrpcToml) ToGlobalConfig() *GlobalConfig { - return &GlobalConfig{ - ServerAddr: f.ServerAddr, - ServerPort: f.ServerPort, - Token: f.Auth.Token, - LogLevel: f.Log.Level, - LogMaxDays: f.Log.MaxDays, - TcpMux: f.Transport.TcpMux, - TcpMuxKeepalive: f.Transport.TcpMuxKeepalive, - HeartbeatInterval: f.Transport.HeartbeatInterval, - HeartbeatTimeout: f.Transport.HeartbeatTimeout, - PoolCount: f.Transport.PoolCount, - } -} - -// ToProxies 将解析结果转换为 Proxy 列表 -func (f *FrpcToml) ToProxies() []Proxy { - var proxies []Proxy - for _, p := range f.Proxies { - proxies = append(proxies, Proxy{ - Name: p.Name, - Type: p.Type, - LocalIP: p.LocalIP, - LocalPort: p.LocalPort, - RemotePort: p.RemotePort, - Enabled: true, // 导入默认启用 - }) - } - return proxies -}