From cff71af26b96b22b907c10c115b9ba5f0a03a835 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 23:27:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(frp):=20=E6=B7=BB=E5=8A=A0=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E7=94=A8=E6=88=B7=E8=AE=BE=E7=BD=AE=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=20(#1193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * feat(frp): 添加运行用户设置功能 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * refactor(frp): 优化正则表达式和用户/组更新逻辑 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * 完成 FRP 运行用户设置功能 Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> --- go.sum | 7 -- internal/apps/frp/app.go | 105 +++++++++++++++++++++++++++ internal/apps/frp/request.go | 6 ++ pkg/systemctl/service.go | 6 ++ web/src/api/apps/frp/index.ts | 7 +- web/src/views/apps/frp/IndexView.vue | 49 ++++++++++++- 6 files changed, 171 insertions(+), 9 deletions(-) diff --git a/go.sum b/go.sum index 72e7e4b4..2b43e86e 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,6 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= -github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -269,7 +267,6 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= @@ -378,8 +375,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -452,8 +447,6 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= diff --git a/internal/apps/frp/app.go b/internal/apps/frp/app.go index 54f3f920..9dcc636e 100644 --- a/internal/apps/frp/app.go +++ b/internal/apps/frp/app.go @@ -3,6 +3,7 @@ package frp import ( "fmt" "net/http" + "regexp" "github.com/go-chi/chi/v5" @@ -12,6 +13,15 @@ import ( "github.com/acepanel/panel/pkg/systemctl" ) +// 预编译正则表达式 +var ( + userCaptureRegex = regexp.MustCompile(`(?m)^User=(.*)$`) + groupCaptureRegex = regexp.MustCompile(`(?m)^Group=(.*)$`) + userRegex = regexp.MustCompile(`(?m)^User=.*$`) + groupRegex = regexp.MustCompile(`(?m)^Group=.*$`) + serviceRegex = regexp.MustCompile(`(?m)^\[Service\]$`) +) + type App struct{} func NewApp() *App { @@ -21,6 +31,8 @@ func NewApp() *App { func (s *App) Route(r chi.Router) { r.Get("/config", s.GetConfig) r.Post("/config", s.UpdateConfig) + r.Get("/user", s.GetUser) + r.Post("/user", s.UpdateUser) } func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) { @@ -58,3 +70,96 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) { service.Success(w, nil) } + +// UserInfo 运行用户信息 +type UserInfo struct { + User string `json:"user"` + Group string `json:"group"` +} + +// GetUser 获取服务的运行用户 +func (s *App) GetUser(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[Name](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", req.Name) + content, err := io.Read(servicePath) + if err != nil { + service.Error(w, http.StatusInternalServerError, "%v", err) + return + } + + userInfo := UserInfo{ + User: "", + Group: "", + } + + // 解析 User 和 Group + if matches := userCaptureRegex.FindStringSubmatch(content); len(matches) > 1 { + userInfo.User = matches[1] + } + if matches := groupCaptureRegex.FindStringSubmatch(content); len(matches) > 1 { + userInfo.Group = matches[1] + } + + service.Success(w, userInfo) +} + +// UpdateUser 更新服务的运行用户 +func (s *App) UpdateUser(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdateUser](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", req.Name) + content, err := io.Read(servicePath) + if err != nil { + service.Error(w, http.StatusInternalServerError, "%v", err) + return + } + + // 检查 User 和 Group 是否存在 + hasUser := userRegex.MatchString(content) + hasGroup := groupRegex.MatchString(content) + + // 替换或添加 User 和 Group 配置 + if hasUser && hasGroup { + // 两者都存在,分别替换 + content = userRegex.ReplaceAllString(content, fmt.Sprintf("User=%s", req.User)) + content = groupRegex.ReplaceAllString(content, fmt.Sprintf("Group=%s", req.Group)) + } else if hasUser && !hasGroup { + // 只有 User,替换 User 并添加 Group + content = userRegex.ReplaceAllString(content, fmt.Sprintf("User=%s\nGroup=%s", req.User, req.Group)) + } else if !hasUser && hasGroup { + // 只有 Group,添加 User 并替换 Group + content = serviceRegex.ReplaceAllString(content, fmt.Sprintf("[Service]\nUser=%s", req.User)) + content = groupRegex.ReplaceAllString(content, fmt.Sprintf("Group=%s", req.Group)) + } else { + // 两者都不存在,在 [Service] 后添加两者 + content = serviceRegex.ReplaceAllString(content, fmt.Sprintf("[Service]\nUser=%s\nGroup=%s", req.User, req.Group)) + } + + if err = io.Write(servicePath, content, 0644); err != nil { + service.Error(w, http.StatusInternalServerError, "%v", err) + return + } + + // 重载 systemd 配置 + if err = systemctl.DaemonReload(); err != nil { + service.Error(w, http.StatusInternalServerError, "%v", err) + return + } + + // 重启服务以应用更改 + if err = systemctl.Restart(req.Name); err != nil { + service.Error(w, http.StatusInternalServerError, "%v", err) + return + } + + service.Success(w, nil) +} diff --git a/internal/apps/frp/request.go b/internal/apps/frp/request.go index ddf76990..8d66f519 100644 --- a/internal/apps/frp/request.go +++ b/internal/apps/frp/request.go @@ -8,3 +8,9 @@ type UpdateConfig struct { Name string `form:"name" json:"name" validate:"required"` Config string `form:"config" json:"config" validate:"required"` } + +type UpdateUser struct { + Name string `form:"name" json:"name" validate:"required"` + User string `form:"user" json:"user" validate:"required"` + Group string `form:"group" json:"group" validate:"required"` +} diff --git a/pkg/systemctl/service.go b/pkg/systemctl/service.go index 8a325e62..ac3bf792 100644 --- a/pkg/systemctl/service.go +++ b/pkg/systemctl/service.go @@ -84,3 +84,9 @@ func LogClear(name string) error { _, err := shell.Execf("journalctl --vacuum-time=1s -u '%s'", name) return err } + +// DaemonReload 重载 systemd 服务配置 +func DaemonReload() error { + _, err := shell.ExecfWithTimeout(2*time.Minute, "systemctl daemon-reload") + return err +} diff --git a/web/src/api/apps/frp/index.ts b/web/src/api/apps/frp/index.ts index 5a3227c5..58567f4d 100644 --- a/web/src/api/apps/frp/index.ts +++ b/web/src/api/apps/frp/index.ts @@ -4,5 +4,10 @@ export default { // 获取配置 config: (name: string): any => http.Get('/apps/frp/config', { params: { name } }), // 保存配置 - saveConfig: (name: string, config: string): any => http.Post('/apps/frp/config', { name, config }) + saveConfig: (name: string, config: string): any => http.Post('/apps/frp/config', { name, config }), + // 获取运行用户 + user: (name: string): any => http.Get('/apps/frp/user', { params: { name } }), + // 设置运行用户 + saveUser: (name: string, user: string, group: string): any => + http.Post('/apps/frp/user', { name, user, group }) } diff --git a/web/src/views/apps/frp/IndexView.vue b/web/src/views/apps/frp/IndexView.vue index c9c7d44c..41a0d125 100644 --- a/web/src/views/apps/frp/IndexView.vue +++ b/web/src/views/apps/frp/IndexView.vue @@ -3,7 +3,7 @@ defineOptions({ name: 'apps-frp-index' }) -import { NButton } from 'naive-ui' +import { NButton, NFormItem, NInput } from 'naive-ui' import { useGettext } from 'vue3-gettext' import frp from '@/api/apps/frp' @@ -15,12 +15,21 @@ const config = ref({ frpc: '', frps: '' }) +const userInfo = ref({ + frpc: { user: '', group: '' }, + frps: { user: '', group: '' } +}) const getConfig = async () => { config.value.frps = await frp.config('frps') config.value.frpc = await frp.config('frpc') } +const getUser = async () => { + userInfo.value.frps = await frp.user('frps') + userInfo.value.frpc = await frp.user('frpc') +} + const handleSaveConfig = (service: string) => { useRequest(frp.saveConfig(service, config.value[service as keyof typeof config.value])).onSuccess( () => { @@ -29,8 +38,16 @@ const handleSaveConfig = (service: string) => { ) } +const handleSaveUser = (service: string) => { + const info = userInfo.value[service as keyof typeof userInfo.value] + useRequest(frp.saveUser(service, info.user, info.group)).onSuccess(() => { + window.$message.success($gettext('Saved successfully')) + }) +} + onMounted(() => { getConfig() + getUser() }) @@ -40,6 +57,21 @@ onMounted(() => { + + + + + + + + + + +