From 699a9e3b920a110a048dc1ab163b1ef49c110924 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Fri, 24 Jul 2026 20:13:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AD=A3=E5=9C=A8=E4=BC=98=E5=8C=96linux?= =?UTF-8?q?=E7=AB=AF=E7=9A=84=E8=BF=90=E8=A1=8C=E7=8A=B6=E6=80=81=E4=BA=8E?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.go | 35 +++++-- bin/{frp_linux_amd64 => frpc_linux_amd64} | Bin bin/{frp_linux_arm64 => frpc_linux_arm64} | Bin frp.go | 111 ++++++++++++---------- frp_other.go | 10 ++ go.mod | 1 + go.sum | 2 + {scripts => script}/build.go | 0 static/app.js | 1 + 9 files changed, 106 insertions(+), 54 deletions(-) rename bin/{frp_linux_amd64 => frpc_linux_amd64} (100%) rename bin/{frp_linux_arm64 => frpc_linux_arm64} (100%) create mode 100644 frp_other.go rename {scripts => script}/build.go (100%) diff --git a/api.go b/api.go index e74bf1d..9f07837 100644 --- a/api.go +++ b/api.go @@ -2,7 +2,9 @@ package main import ( "bytes" + "embed" "fmt" + "io/fs" "net/http" "strconv" "text/template" @@ -11,45 +13,66 @@ import ( "golang.org/x/crypto/bcrypt" ) +//go:embed static/* +var staticFS embed.FS + func SetupRouter() *gin.Engine { r := gin.Default() - r.Static("/static", "./static") + // 从 embed 读取前端静态文件 + // 注意:embed.FS 不支持直接作为 http.FileSystem 传参,需要用 http.FS 转换 + staticSubFS, _ := fs.Sub(staticFS, "static") + r.StaticFS("/static", http.FS(staticSubFS)) + + // 根路由:直接返回 index.html + 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 后端已启动 🎉") }) - r.GET("/", func(c *gin.Context) { - c.File("./static/index.html") - }) + // API 路由 api := r.Group("/api") { - // 公开接口 + // 登录(无需鉴权) api.GET("/check/users", checkUsersHandler) api.POST("/register", registerHandler) api.POST("/login", loginHandler) - // 需鉴权 + // 需要鉴权的路由 auth := api.Group("/") auth.Use(AuthMiddleware()) { + // 全局配置 auth.GET("/config", getConfigHandler) auth.PUT("/config", updateConfigHandler) + // 隧道管理 auth.GET("/proxies", getProxiesHandler) auth.GET("/proxy/:id", getProxyHandler) auth.POST("/proxy", createProxyHandler) auth.PUT("/proxy/:id", updateProxyHandler) auth.DELETE("/proxy/:id", deleteProxyHandler) + // frpc 控制 auth.POST("/frpc/reload", reloadFrpcHandler) auth.POST("/frpc/start", startFrpcHandler) auth.POST("/frpc/stop", stopFrpcHandler) auth.GET("/frpc/status", getFrpcStatusHandler) + // 导入/导出 TOML auth.POST("/import/toml", importTomlHandler) auth.GET("/export/toml", ExportTomlHandler) + // 修改密码 auth.PUT("/user/password", changePasswordHandler) } } diff --git a/bin/frp_linux_amd64 b/bin/frpc_linux_amd64 similarity index 100% rename from bin/frp_linux_amd64 rename to bin/frpc_linux_amd64 diff --git a/bin/frp_linux_arm64 b/bin/frpc_linux_arm64 similarity index 100% rename from bin/frp_linux_arm64 rename to bin/frpc_linux_arm64 diff --git a/frp.go b/frp.go index a538805..b7a6e6d 100644 --- a/frp.go +++ b/frp.go @@ -4,13 +4,17 @@ import ( "bytes" "embed" "fmt" + "io" "log" "os" "os/exec" + "path/filepath" "runtime" "strings" "sync" "text/template" + + "github.com/creack/pty" ) //go:embed bin/* @@ -24,7 +28,10 @@ var ( frpcPathMutex sync.Mutex ) -// getFrpcPath 获取当前平台对应的 frpc 路径(优先使用缓存) +// getFrpcPath 获取 frpc 路径,优先级: +// 1. 当前目录 ./bin/ (便于用户手动管理 frpc 版本) +// 2. embed 解压到 /tmp (便于开箱即用) +// 3. 系统 PATH 中的 frpc (降级方案) func getFrpcPath() (string, error) { frpcPathMutex.Lock() defer frpcPathMutex.Unlock() @@ -34,21 +41,24 @@ func getFrpcPath() (string, error) { 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 == "windows" && runtime.GOARCH == "386": + fileName = "frpc_windows_386.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_armv7" default: - // 降级:尝试从系统 PATH 查找 frpc + // 不支持的平台,尝试从 PATH 找 path, err := exec.LookPath("frpc") if err == nil { cachedFrpcPath = path @@ -57,36 +67,39 @@ func getFrpcPath() (string, error) { return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH) } - // 从 embed 读取 + // ===== 优先级 1:从当前目录 ./bin/ 查找 ===== + localPath := filepath.Join(".", "bin", fileName) + if _, err := os.Stat(localPath); err == nil { + cachedFrpcPath = localPath + return localPath, nil + } + + // ===== 优先级 2:从 embed 读取并解压到临时目录 ===== data, err := embeddedFrpc.ReadFile("bin/" + fileName) - if err != nil { - // 降级到 PATH - path, lookErr := exec.LookPath("frpc") - if lookErr == nil { - cachedFrpcPath = path - return path, nil + if err == nil { + tmpPath := filepath.Join(os.TempDir(), "frpc") + if runtime.GOOS == "windows" { + tmpPath += ".exe" } - return "", fmt.Errorf("未找到 frpc 文件 (embed 和 PATH 均无): %w", err) - } - - // 写入临时目录 - tmpPath := os.TempDir() + "/frpc" - if runtime.GOOS == "windows" { - tmpPath += ".exe" - } - - // 尝试写入(如果文件已存在且正在使用,忽略错误,直接使用已有文件) - if err := os.WriteFile(tmpPath, data, 0755); err != nil { - // 如果写入失败,检查是否已有可用的 frpc + 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 } - return "", fmt.Errorf("写入临时文件失败: %w", err) } - cachedFrpcPath = tmpPath - return tmpPath, nil + // ===== 优先级 3:降级到系统 PATH ===== + path, err := exec.LookPath("frpc") + if err == nil { + cachedFrpcPath = path + return path, nil + } + + return "", fmt.Errorf("未找到 frpc 文件 (本地 bin/、embed、PATH 均无)") } // GenerateFrpcConfig 生成 frpc.toml 配置文件 @@ -116,23 +129,18 @@ func GenerateFrpcConfig() error { Proxies: activeProxies, } - // ===== 核心改动 ===== - // 优先使用外部模板文件 frpc.tmpl,如果不存在则使用 embed 中的模板 + // 优先使用外部模板文件,不存在则用 embed var tmplContent string if _, err := os.Stat("frpc.tmpl"); err == nil { - // 外部文件存在,读取它 content, readErr := os.ReadFile("frpc.tmpl") if readErr == nil { tmplContent = string(content) } else { - // 读取失败时回退到 embed tmplContent = FrpcTemplateContent } } else { - // 外部文件不存在,使用 embed 中的模板 tmplContent = FrpcTemplateContent } - // ===== 核心改动结束 ===== tmpl, err := template.New("frpc").Parse(tmplContent) if err != nil { @@ -151,38 +159,46 @@ func GenerateFrpcConfig() error { return nil } -// StartFrpc 启动 frpc(后台运行) +// StartFrpc 启动 frpc + func StartFrpc() error { frpcPath, err := getFrpcPath() if err != nil { return fmt.Errorf("获取 frpc 路径失败: %w", err) } - // 确保配置文件存在 if _, err := os.Stat("./frpc.toml"); os.IsNotExist(err) { if err := GenerateFrpcConfig(); err != nil { return fmt.Errorf("生成配置文件失败: %w", err) } } - // 检查是否已在运行 running, _ := GetFrpcStatus() if running { return nil } cmd := exec.Command(frpcPath, "-c", "./frpc.toml") - if runtime.GOOS == "windows" { - setWindowHide(cmd) - } - if err := cmd.Start(); err != nil { - return fmt.Errorf("启动 frpc 失败: %w", err) + setWindowHide(cmd) + + // ✅ Linux 下分配伪终端,让 frpc 认为自己在真实终端中运行 + if runtime.GOOS != "windows" { + f, err := pty.Start(cmd) + if err != nil { + return fmt.Errorf("启动伪终端失败: %w", err) + } + // 可选:丢弃 pty 输出,或写入日志文件 + go io.Copy(io.Discard, f) + } else { + if err := cmd.Start(); err != nil { + return fmt.Errorf("启动 frpc 失败: %w", err) + } } + return nil } -// ReloadFrpc 热加载 frpc 配置(仅发信号,不重复写入二进制) -// ReloadFrpc 热加载 frpc 配置,失败时自动降级为重启 +// ReloadFrpc 热加载,失败时自动降级为重启 func ReloadFrpc() error { running, err := GetFrpcStatus() if err != nil { @@ -192,14 +208,13 @@ func ReloadFrpc() error { return StartFrpc() } - if cachedFrpcPath == "" { - if _, err := getFrpcPath(); err != nil { - return fmt.Errorf("获取 frpc 路径失败: %w", err) - } + frpcPath, err := getFrpcPath() + if err != nil { + return fmt.Errorf("获取 frpc 路径失败: %w", err) } - cmd := exec.Command(cachedFrpcPath, "reload", "-c", "./frpc.toml") - _, err = cmd.CombinedOutput() // ← 这里改了 + cmd := exec.Command(frpcPath, "reload", "-c", "./frpc.toml") + _, err = cmd.CombinedOutput() if err != nil { log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frpc", err) if stopErr := StopFrpc(); stopErr != nil { diff --git a/frp_other.go b/frp_other.go new file mode 100644 index 0000000..cc68b3b --- /dev/null +++ b/frp_other.go @@ -0,0 +1,10 @@ +// frp_other.go +//go:build !windows + +package main + +import "os/exec" + +func setWindowHide(cmd *exec.Cmd) { + // 非 Windows 平台什么都不做 +} diff --git a/go.mod b/go.mod index 4161340..ba3269b 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/creack/pty v1.1.24 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect diff --git a/go.sum b/go.sum index 935b577..bfa45f9 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/scripts/build.go b/script/build.go similarity index 100% rename from scripts/build.go rename to script/build.go diff --git a/static/app.js b/static/app.js index 29b147c..cf3ab0a 100644 --- a/static/app.js +++ b/static/app.js @@ -7,6 +7,7 @@ const { createApp, ref, reactive, computed, onMounted } = Vue; // ============================================================ const API_BASE = "/api"; + function getToken() { return localStorage.getItem("frpc_token") || ""; }