重构进行中,preview通道尚未完全恢复全部功能
This commit is contained in:
@@ -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"]
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func setWindowHide(cmd *exec.Cmd) {
|
||||
// 非 Windows 平台什么都不做
|
||||
}
|
||||
@@ -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,不需要额外设置
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
@@ -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 文件")
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 已自动重启")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// setProcessAttributes 非 Linux 平台 (Windows/macOS) 不做特殊设置
|
||||
func setProcessAttributes(cmd *exec.Cmd) {
|
||||
// 非 Linux 平台不需要 Setpgid
|
||||
}
|
||||
-181
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user