2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

feat(frp): 添加运行用户设置功能 (#1193)

* 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>
This commit is contained in:
Copilot
2026-01-08 23:27:54 +08:00
committed by GitHub
parent 89ace2360c
commit cff71af26b
6 changed files with 171 additions and 9 deletions

7
go.sum
View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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 })
}

View File

@@ -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()
})
</script>
@@ -40,6 +57,21 @@ onMounted(() => {
<n-tab-pane name="frps" tab="Frps">
<n-flex vertical>
<service-status service="frps" />
<n-card :title="$gettext('Run User')">
<template #header-extra>
<n-button type="primary" @click="handleSaveUser('frps')">
{{ $gettext('Save') }}
</n-button>
</template>
<n-flex>
<n-form-item :label="$gettext('User')">
<n-input v-model:value="userInfo.frps.user" :placeholder="$gettext('User')" />
</n-form-item>
<n-form-item :label="$gettext('Group')">
<n-input v-model:value="userInfo.frps.group" :placeholder="$gettext('Group')" />
</n-form-item>
</n-flex>
</n-card>
<n-card :title="$gettext('Modify Configuration')">
<template #header-extra>
<n-button type="primary" @click="handleSaveConfig('frps')">
@@ -53,6 +85,21 @@ onMounted(() => {
<n-tab-pane name="frpc" tab="Frpc">
<n-flex vertical>
<service-status service="frpc" />
<n-card :title="$gettext('Run User')">
<template #header-extra>
<n-button type="primary" @click="handleSaveUser('frpc')">
{{ $gettext('Save') }}
</n-button>
</template>
<n-flex>
<n-form-item :label="$gettext('User')">
<n-input v-model:value="userInfo.frpc.user" :placeholder="$gettext('User')" />
</n-form-item>
<n-form-item :label="$gettext('Group')">
<n-input v-model:value="userInfo.frpc.group" :placeholder="$gettext('Group')" />
</n-form-item>
</n-flex>
</n-card>
<n-card :title="$gettext('Modify Configuration')">
<template #header-extra>
<n-button type="primary" @click="handleSaveConfig('frpc')">