重构进行中,preview通道尚未完全恢复全部功能

This commit is contained in:
2026-08-07 19:16:08 +08:00
parent 5bce105bd6
commit 3a381c85ed
21 changed files with 889 additions and 2416 deletions
-30
View File
@@ -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"]
-575
View File
@@ -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()
}
-142
View File
@@ -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()
}
}
-16
View File
@@ -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
}
-457
View File
@@ -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
}
-9
View File
@@ -1,9 +0,0 @@
//go:build !windows
package main
import "os/exec"
func setWindowHide(cmd *exec.Cmd) {
// 非 Windows 平台什么都不做
}
-18
View File
@@ -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,不需要额外设置
}
+234
View File
@@ -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)
}
+50 -208
View File
@@ -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
}
+36
View File
@@ -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"`
}
+149
View File
@@ -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
}
+6 -6
View File
@@ -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",
+85
View File
@@ -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 文件")
}
+285
View File
@@ -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,
}
}
}
+44 -34
View File
@@ -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 已自动重启")
-620
View File
@@ -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
}
-15
View File
@@ -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,
}
}
-65
View File
@@ -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
}
-28
View File
@@ -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
}
-12
View File
@@ -1,12 +0,0 @@
//go:build !linux
package main
import (
"os/exec"
)
// setProcessAttributes 非 Linux 平台 (Windows/macOS) 不做特殊设置
func setProcessAttributes(cmd *exec.Cmd) {
// 非 Linux 平台不需要 Setpgid
}
-181
View File
@@ -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
}