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:
7
go.sum
7
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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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')">
|
||||
|
||||
Reference in New Issue
Block a user