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

feat: ssh管理, close #350

This commit is contained in:
2026-01-09 00:46:40 +08:00
parent 8e31361eaf
commit 7d5a0ac1c0
11 changed files with 816 additions and 36 deletions

View File

@@ -118,6 +118,7 @@ func initWeb() (*app.Web, error) {
systemctlService := service.NewSystemctlService(locale)
toolboxSystemService := service.NewToolboxSystemService(locale)
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
toolboxSSHService := service.NewToolboxSSHService(locale)
webHookRepo := data.NewWebHookRepo(locale, db)
webHookService := service.NewWebHookService(webHookRepo)
codeserverApp := codeserver.NewApp()
@@ -141,7 +142,7 @@ func initWeb() (*app.Web, error) {
s3fsApp := s3fs.NewApp(locale)
supervisorApp := supervisor.NewApp(locale)
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, webHookService, loader)
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, webHookService, loader)
wsService := service.NewWsService(locale, config, logger, sshRepo)
ws := route.NewWs(wsService)
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)

7
go.sum
View File

@@ -118,6 +118,8 @@ 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=
@@ -267,6 +269,7 @@ 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=
@@ -375,6 +378,8 @@ 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=
@@ -447,6 +452,8 @@ 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

@@ -0,0 +1,26 @@
package request
// ToolboxSSHPort SSH 端口设置
type ToolboxSSHPort struct {
Port uint `form:"port" json:"port" validate:"required|min:1|max:65535"`
}
// ToolboxSSHPasswordAuth SSH 密码认证设置
type ToolboxSSHPasswordAuth struct {
Enabled bool `form:"enabled" json:"enabled"`
}
// ToolboxSSHPubKeyAuth SSH 密钥认证设置
type ToolboxSSHPubKeyAuth struct {
Enabled bool `form:"enabled" json:"enabled"`
}
// ToolboxSSHRootLogin Root 登录设置
type ToolboxSSHRootLogin struct {
Mode string `form:"mode" json:"mode" validate:"required|in:yes,no,without-password,prohibit-password"`
}
// ToolboxSSHRootPassword Root 密码设置
type ToolboxSSHRootPassword struct {
Password string `form:"password" json:"password" validate:"required|password"`
}

View File

@@ -48,6 +48,7 @@ type Http struct {
systemctl *service.SystemctlService
toolboxSystem *service.ToolboxSystemService
toolboxBenchmark *service.ToolboxBenchmarkService
toolboxSSH *service.ToolboxSSHService
webhook *service.WebHookService
apps *apploader.Loader
}
@@ -85,6 +86,7 @@ func NewHttp(
systemctl *service.SystemctlService,
toolboxSystem *service.ToolboxSystemService,
toolboxBenchmark *service.ToolboxBenchmarkService,
toolboxSSH *service.ToolboxSSHService,
webhook *service.WebHookService,
apps *apploader.Loader,
) *Http {
@@ -121,6 +123,7 @@ func NewHttp(
systemctl: systemctl,
toolboxSystem: toolboxSystem,
toolboxBenchmark: toolboxBenchmark,
toolboxSSH: toolboxSSH,
webhook: webhook,
apps: apps,
}
@@ -438,13 +441,26 @@ func (route *Http) Register(r *chi.Mux) {
r.Post("/hostname", route.toolboxSystem.UpdateHostname)
r.Get("/hosts", route.toolboxSystem.GetHosts)
r.Post("/hosts", route.toolboxSystem.UpdateHosts)
r.Post("/root_password", route.toolboxSystem.UpdateRootPassword)
})
r.Route("/toolbox_benchmark", func(r chi.Router) {
r.Post("/test", route.toolboxBenchmark.Test)
})
r.Route("/toolbox_ssh", func(r chi.Router) {
r.Get("/info", route.toolboxSSH.GetInfo)
r.Post("/start", route.toolboxSSH.Start)
r.Post("/stop", route.toolboxSSH.Stop)
r.Post("/restart", route.toolboxSSH.Restart)
r.Post("/port", route.toolboxSSH.UpdatePort)
r.Post("/password_auth", route.toolboxSSH.UpdatePasswordAuth)
r.Post("/pubkey_auth", route.toolboxSSH.UpdatePubKeyAuth)
r.Post("/root_login", route.toolboxSSH.UpdateRootLogin)
r.Post("/root_password", route.toolboxSSH.UpdateRootPassword)
r.Get("/root_key", route.toolboxSSH.GetRootKey)
r.Post("/root_key", route.toolboxSSH.GenerateRootKey)
})
r.Route("/webhook", func(r chi.Router) {
r.Get("/", route.webhook.List)
r.Post("/", route.webhook.Create)

View File

@@ -37,5 +37,6 @@ var ProviderSet = wire.NewSet(
NewWebsiteService,
NewToolboxSystemService,
NewToolboxBenchmarkService,
NewToolboxSSHService,
NewWsService,
)

View File

@@ -0,0 +1,339 @@
package service
import (
"net/http"
"regexp"
"strings"
"github.com/leonelquinteros/gotext"
"github.com/libtnb/chix"
"github.com/spf13/cast"
"github.com/acepanel/panel/internal/http/request"
"github.com/acepanel/panel/pkg/io"
"github.com/acepanel/panel/pkg/shell"
"github.com/acepanel/panel/pkg/systemctl"
)
type ToolboxSSHService struct {
t *gotext.Locale
}
func NewToolboxSSHService(t *gotext.Locale) *ToolboxSSHService {
return &ToolboxSSHService{
t: t,
}
}
// GetInfo 获取 SSH 信息
func (s *ToolboxSSHService) GetInfo(w http.ResponseWriter, r *http.Request) {
// 读取 sshd_config
sshdConfig, err := io.Read("/etc/ssh/sshd_config")
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to read sshd_config: %v", err))
return
}
// 获取 SSH 服务状态
status, err := systemctl.Status("sshd")
if err != nil {
// 尝试 ssh 服务名
status, err = systemctl.Status("ssh")
if err != nil {
status = false
}
}
// 解析端口
port := 22
portMatch := regexp.MustCompile(`(?m)^Port\s+(\d+)`).FindStringSubmatch(sshdConfig)
if len(portMatch) >= 2 {
port = cast.ToInt(portMatch[1])
}
// 解析密码认证
passwordAuth := true
passwordAuthMatch := regexp.MustCompile(`(?m)^PasswordAuthentication\s+(\S+)`).FindStringSubmatch(sshdConfig)
if len(passwordAuthMatch) >= 2 {
passwordAuth = strings.ToLower(passwordAuthMatch[1]) == "yes"
}
// 解析密钥认证
pubKeyAuth := true
pubKeyAuthMatch := regexp.MustCompile(`(?m)^PubkeyAuthentication\s+(\S+)`).FindStringSubmatch(sshdConfig)
if len(pubKeyAuthMatch) >= 2 {
pubKeyAuth = strings.ToLower(pubKeyAuthMatch[1]) == "yes"
}
// 解析 Root 登录设置
rootLogin := "yes"
rootLoginMatch := regexp.MustCompile(`(?m)^PermitRootLogin\s+(\S+)`).FindStringSubmatch(sshdConfig)
if len(rootLoginMatch) >= 2 {
rootLogin = strings.ToLower(rootLoginMatch[1])
}
Success(w, chix.M{
"status": status,
"port": port,
"password_auth": passwordAuth,
"pubkey_auth": pubKeyAuth,
"root_login": rootLogin,
})
}
// Start 启动 SSH 服务
func (s *ToolboxSSHService) Start(w http.ResponseWriter, r *http.Request) {
err := systemctl.Start("sshd")
if err != nil {
err = systemctl.Start("ssh")
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to start SSH service: %v", err))
return
}
}
Success(w, nil)
}
// Stop 停止 SSH 服务
func (s *ToolboxSSHService) Stop(w http.ResponseWriter, r *http.Request) {
err := systemctl.Stop("sshd")
if err != nil {
err = systemctl.Stop("ssh")
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to stop SSH service: %v", err))
return
}
}
Success(w, nil)
}
// Restart 重启 SSH 服务
func (s *ToolboxSSHService) Restart(w http.ResponseWriter, r *http.Request) {
err := systemctl.Restart("sshd")
if err != nil {
err = systemctl.Restart("ssh")
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
return
}
}
Success(w, nil)
}
// UpdatePort 修改 SSH 端口
func (s *ToolboxSSHService) UpdatePort(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxSSHPort](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.updateSSHConfig("Port", cast.ToString(req.Port)); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to update SSH port: %v", err))
return
}
// 重启 SSH 服务
if err = s.restartSSH(); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
return
}
Success(w, nil)
}
// UpdatePasswordAuth 设置密码认证
func (s *ToolboxSSHService) UpdatePasswordAuth(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxSSHPasswordAuth](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
value := "no"
if req.Enabled {
value = "yes"
}
if err = s.updateSSHConfig("PasswordAuthentication", value); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to update password authentication: %v", err))
return
}
if err = s.restartSSH(); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
return
}
Success(w, nil)
}
// UpdatePubKeyAuth 设置密钥认证
func (s *ToolboxSSHService) UpdatePubKeyAuth(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxSSHPubKeyAuth](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
value := "no"
if req.Enabled {
value = "yes"
}
if err = s.updateSSHConfig("PubkeyAuthentication", value); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to update pubkey authentication: %v", err))
return
}
if err = s.restartSSH(); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
return
}
Success(w, nil)
}
// UpdateRootLogin 设置 Root 登录
func (s *ToolboxSSHService) UpdateRootLogin(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxSSHRootLogin](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = s.updateSSHConfig("PermitRootLogin", req.Mode); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to update root login setting: %v", err))
return
}
if err = s.restartSSH(); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to restart SSH service: %v", err))
return
}
Success(w, nil)
}
// UpdateRootPassword 修改 Root 密码
func (s *ToolboxSSHService) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxSSHRootPassword](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
password := strings.ReplaceAll(req.Password, `'`, `\'`)
if _, err = shell.Execf(`yes '%s' | passwd root`, password); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to update root password: %v", err))
return
}
Success(w, nil)
}
// GetRootKey 获取 Root 私钥
func (s *ToolboxSSHService) GetRootKey(w http.ResponseWriter, r *http.Request) {
var privateKey string
// 优先尝试 ed25519 密钥
if io.Exists("/root/.ssh/id_ed25519") {
privateKey, _ = io.Read("/root/.ssh/id_ed25519")
} else if io.Exists("/root/.ssh/id_rsa") {
privateKey, _ = io.Read("/root/.ssh/id_rsa")
}
Success(w, strings.TrimSpace(privateKey))
}
// GenerateRootKey 生成 Root 密钥对
func (s *ToolboxSSHService) GenerateRootKey(w http.ResponseWriter, r *http.Request) {
// 确保 .ssh 目录存在
if _, err := shell.Execf("mkdir -p /root/.ssh && chmod 700 /root/.ssh"); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to create .ssh directory: %v", err))
return
}
// 优先生成 ED25519 密钥对
keyType := "ed25519"
if _, err := shell.Execf(`yes 'y' | ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""`); err != nil {
// 不行再生成 RSA 密钥
keyType = "rsa"
if _, err = shell.Execf(`yes 'y' | ssh-keygen -t rsa -b 4096 -f /root/.ssh/id_rsa -N ""`); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to generate SSH key: %v", err))
return
}
}
// 读取生成的密钥
var pubKey, privateKey string
var err error
if keyType == "ed25519" {
pubKey, err = io.Read("/root/.ssh/id_ed25519.pub")
if err == nil {
privateKey, _ = io.Read("/root/.ssh/id_ed25519")
}
} else {
pubKey, err = io.Read("/root/.ssh/id_rsa.pub")
if err == nil {
privateKey, _ = io.Read("/root/.ssh/id_rsa")
}
}
if err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to read generated key: %v", err))
return
}
// 将公钥添加到 authorized_keys
pubKey = strings.TrimSpace(pubKey)
privateKey = strings.TrimSpace(privateKey)
authorizedKeysPath := "/root/.ssh/authorized_keys"
authorizedKeys, _ := io.Read(authorizedKeysPath)
// 检查公钥是否已存在
if !strings.Contains(authorizedKeys, pubKey) {
if authorizedKeys != "" && !strings.HasSuffix(authorizedKeys, "\n") {
authorizedKeys += "\n"
}
authorizedKeys += pubKey + "\n"
if err = io.Write(authorizedKeysPath, authorizedKeys, 0600); err != nil {
Error(w, http.StatusInternalServerError, s.t.Get("failed to update authorized_keys: %v", err))
return
}
}
_ = s.restartSSH()
Success(w, privateKey)
}
// updateSSHConfig 更新 SSH 配置项
func (s *ToolboxSSHService) updateSSHConfig(key, value string) error {
sshdConfig, err := io.Read("/etc/ssh/sshd_config")
if err != nil {
return err
}
// 检查配置项是否存在(包括注释的)
configRegex := regexp.MustCompile(`(?m)^#?\s*` + key + `\s+.*$`)
if configRegex.MatchString(sshdConfig) {
// 替换现有配置
sshdConfig = configRegex.ReplaceAllString(sshdConfig, key+" "+value)
} else {
// 添加新配置
sshdConfig = sshdConfig + "\n" + key + " " + value + "\n"
}
return io.Write("/etc/ssh/sshd_config", sshdConfig, 0600)
}
// restartSSH 重启 SSH 服务
func (s *ToolboxSSHService) restartSSH() error {
err := systemctl.Restart("sshd")
if err != nil {
err = systemctl.Restart("ssh")
}
return err
}

View File

@@ -305,20 +305,3 @@ func (s *ToolboxSystemService) UpdateHosts(w http.ResponseWriter, r *http.Reques
Success(w, nil)
}
// UpdateRootPassword 设置 root 密码
func (s *ToolboxSystemService) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
req, err := Bind[request.ToolboxSystemPassword](r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
Error(w, http.StatusInternalServerError, "%v", s.t.Get("failed to set root password: %v", err))
return
}
Success(w, nil)
}

View File

@@ -0,0 +1,28 @@
import { http } from '@/utils'
export default {
// 获取 SSH 信息
info: (): any => http.Get('/toolbox_ssh/info'),
// 启动 SSH 服务
start: (): any => http.Post('/toolbox_ssh/start'),
// 停止 SSH 服务
stop: (): any => http.Post('/toolbox_ssh/stop'),
// 重启 SSH 服务
restart: (): any => http.Post('/toolbox_ssh/restart'),
// 设置 SSH 端口
updatePort: (port: number): any => http.Post('/toolbox_ssh/port', { port }),
// 设置密码认证
updatePasswordAuth: (enabled: boolean): any =>
http.Post('/toolbox_ssh/password_auth', { enabled }),
// 设置密钥认证
updatePubkeyAuth: (enabled: boolean): any => http.Post('/toolbox_ssh/pubkey_auth', { enabled }),
// 设置 Root 登录
updateRootLogin: (mode: string): any => http.Post('/toolbox_ssh/root_login', { mode }),
// 设置 Root 密码
updateRootPassword: (password: string): any =>
http.Post('/toolbox_ssh/root_password', { password }),
// 获取 Root 公钥
rootKey: (): any => http.Get('/toolbox_ssh/root_key'),
// 生成 Root 密钥对
generateRootKey: (): any => http.Post('/toolbox_ssh/root_key')
}

View File

@@ -5,6 +5,7 @@ defineOptions({
import BenchmarkView from '@/views/toolbox/BenchmarkView.vue'
import ProcessView from '@/views/toolbox/ProcessView.vue'
import SSHView from '@/views/toolbox/SSHView.vue'
import SystemView from '@/views/toolbox/SystemView.vue'
import WebHookView from '@/views/toolbox/WebHookView.vue'
import { useGettext } from 'vue3-gettext'
@@ -19,6 +20,7 @@ const current = ref('process')
<n-tabs v-model:value="current" animated>
<n-tab name="process" :tab="$gettext('Process')" />
<n-tab name="system" :tab="$gettext('System')" />
<n-tab name="ssh" tab="SSH" />
<n-tab name="webhook" :tab="$gettext('WebHook')" />
<n-tab name="benchmark" :tab="$gettext('Benchmark')" />
</n-tabs>
@@ -26,6 +28,7 @@ const current = ref('process')
<n-flex vertical>
<process-view v-if="current === 'process'" />
<system-view v-if="current === 'system'" />
<s-s-h-view v-if="current === 'ssh'" />
<web-hook-view v-if="current === 'webhook'" />
<benchmark-view v-if="current === 'benchmark'" />
</n-flex>

View File

@@ -0,0 +1,393 @@
<script setup lang="ts">
defineOptions({
name: 'toolbox-ssh'
})
import toolboxSSH from '@/api/panel/toolbox-ssh'
import TheIcon from '@/components/custom/TheIcon.vue'
import { generateRandomString } from '@/utils'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
// SSH 基础设置
const sshStatus = ref(false)
const sshPort = ref(22)
const passwordAuth = ref(false)
const pubkeyAuth = ref(true)
// Root 设置
const rootLogin = ref('without-password')
const rootPassword = ref('')
const rootKey = ref('')
// 加载状态
const loading = ref(false)
const portLoading = ref(false)
const passwordLoading = ref(false)
const pubkeyLoading = ref(false)
const rootLoginLoading = ref(false)
const rootPasswordLoading = ref(false)
const keyLoading = ref(false)
// Root 登录选项
const rootLoginOptions = [
{ label: 'yes - ' + $gettext('Allow password and key login'), value: 'yes' },
{ label: 'no - ' + $gettext('Disable root login'), value: 'no' },
{
label: 'prohibit-password - ' + $gettext('Only allow key login (recommended)'),
value: 'prohibit-password'
},
{
label: 'forced-commands-only - ' + $gettext('Only allow key login with forced commands'),
value: 'forced-commands-only'
}
]
// 加载数据
const loadData = async () => {
loading.value = true
try {
const info = await toolboxSSH.info()
sshStatus.value = info.status
sshPort.value = info.port
passwordAuth.value = info.password_auth
pubkeyAuth.value = info.pubkey_auth
rootLogin.value = info.root_login
// 加载 root 私钥
const key = await toolboxSSH.rootKey()
rootKey.value = key || ''
} finally {
loading.value = false
}
}
// 切换 SSH 服务状态
const handleToggleSSH = async () => {
loading.value = true
try {
if (sshStatus.value) {
await toolboxSSH.stop()
window.$message.success($gettext('SSH service stopped'))
} else {
await toolboxSSH.start()
window.$message.success($gettext('SSH service started'))
}
sshStatus.value = !sshStatus.value
} finally {
loading.value = false
}
}
// 重启 SSH 服务
const handleRestartSSH = async () => {
loading.value = true
try {
await toolboxSSH.restart()
window.$message.success($gettext('SSH service restarted'))
} finally {
loading.value = false
}
}
// 更新端口
const handleUpdatePort = async () => {
portLoading.value = true
try {
await toolboxSSH.updatePort(sshPort.value)
window.$message.success($gettext('SSH port updated'))
} finally {
portLoading.value = false
}
}
// 生成随机端口
const handleRandomPort = () => {
// 生成 10000-65535 之间的随机端口
sshPort.value = Math.floor(Math.random() * (65535 - 10000 + 1)) + 10000
}
// 切换密码认证
const handleTogglePasswordAuth = async () => {
passwordLoading.value = true
try {
await toolboxSSH.updatePasswordAuth(!passwordAuth.value)
passwordAuth.value = !passwordAuth.value
window.$message.success($gettext('Password authentication updated'))
} finally {
passwordLoading.value = false
}
}
// 切换密钥认证
const handleTogglePubkeyAuth = async () => {
pubkeyLoading.value = true
try {
await toolboxSSH.updatePubkeyAuth(!pubkeyAuth.value)
pubkeyAuth.value = !pubkeyAuth.value
window.$message.success($gettext('Key authentication updated'))
} finally {
pubkeyLoading.value = false
}
}
// 更新 Root 登录设置
const handleUpdateRootLogin = async (value: string) => {
rootLoginLoading.value = true
try {
await toolboxSSH.updateRootLogin(value)
rootLogin.value = value
window.$message.success($gettext('Root login setting updated'))
} finally {
rootLoginLoading.value = false
}
}
// 更新 Root 密码
const handleUpdateRootPassword = async () => {
if (!rootPassword.value) {
window.$message.warning($gettext('Please enter a password'))
return
}
rootPasswordLoading.value = true
try {
await toolboxSSH.updateRootPassword(rootPassword.value)
rootPassword.value = ''
window.$message.success($gettext('Root password updated'))
} finally {
rootPasswordLoading.value = false
}
}
// 生成随机 Root 密码
const handleGeneratePassword = () => {
rootPassword.value = generateRandomString(16)
}
// 查看密钥
const showKeyModal = ref(false)
const handleViewKey = async () => {
if (!rootKey.value) {
// 没有密钥,先生成一个
keyLoading.value = true
try {
const key = await toolboxSSH.generateRootKey()
rootKey.value = key
window.$message.success($gettext('SSH key generated'))
} finally {
keyLoading.value = false
}
}
showKeyModal.value = true
}
// 生成密钥
const handleGenerateKey = async () => {
keyLoading.value = true
try {
const key = await toolboxSSH.generateRootKey()
rootKey.value = key
window.$message.success($gettext('SSH key generated'))
} finally {
keyLoading.value = false
}
}
// 下载私钥
const handleDownloadKey = () => {
if (!rootKey.value) {
window.$message.warning($gettext('No SSH key found'))
return
}
const blob = new Blob([rootKey.value], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 根据私钥内容判断文件名
if (rootKey.value.includes('OPENSSH PRIVATE KEY')) {
link.download = 'id_ed25519'
} else if (rootKey.value.includes('RSA PRIVATE KEY')) {
link.download = 'id_rsa'
} else {
link.download = 'id_key'
}
link.click()
URL.revokeObjectURL(url)
}
onMounted(() => {
loadData()
})
</script>
<template>
<n-spin :show="loading">
<n-flex vertical :size="24">
<!-- SSH 服务状态 -->
<n-card :title="$gettext('SSH Service')">
<n-flex align="center" :size="12">
<n-text strong>{{ $gettext('SSH Service Status') }}</n-text>
<n-switch :value="sshStatus" :loading="loading" @update:value="handleToggleSSH" />
<n-button :loading="loading" @click="handleRestartSSH">
{{ $gettext('Restart') }}
</n-button>
</n-flex>
</n-card>
<!-- SSH 基础设置 -->
<n-card :title="$gettext('SSH Basic Settings')">
<n-flex vertical :size="16">
<!-- SSH 密码登录 -->
<n-flex vertical :size="4">
<n-flex align="center" :size="12">
<n-text strong>{{ $gettext('SSH Password Login') }}</n-text>
<n-switch
:value="passwordAuth"
:loading="passwordLoading"
@update:value="handleTogglePasswordAuth"
/>
</n-flex>
<n-text depth="3">{{ $gettext('Allow password authentication for SSH login') }}</n-text>
</n-flex>
<!-- SSH 密钥登录 -->
<n-flex vertical :size="4">
<n-flex align="center" :size="12">
<n-text strong>{{ $gettext('SSH Key Login') }}</n-text>
<n-switch
:value="pubkeyAuth"
:loading="pubkeyLoading"
@update:value="handleTogglePubkeyAuth"
/>
</n-flex>
<n-text depth="3">{{
$gettext('Allow public key authentication for SSH login')
}}</n-text>
</n-flex>
<!-- SSH 端口 -->
<n-flex vertical :size="4">
<n-flex align="center" :size="12">
<n-text strong>{{ $gettext('SSH Port') }}</n-text>
<n-input-number v-model:value="sshPort" :min="1" :max="65535" style="width: 120px" />
<n-button @click="handleRandomPort">
<template #icon>
<the-icon :size="16" icon="mdi:refresh" />
</template>
</n-button>
<n-button type="primary" :loading="portLoading" @click="handleUpdatePort">
{{ $gettext('Save') }}
</n-button>
</n-flex>
<n-text depth="3">{{ $gettext('Current SSH port, default is 22') }}</n-text>
</n-flex>
</n-flex>
</n-card>
<!-- Root 设置 -->
<n-card :title="$gettext('Root Settings')">
<n-flex vertical :size="16">
<!-- Root 密码登录设置 -->
<n-flex vertical :size="8">
<n-text strong>{{ $gettext('Root Password Login Setting') }}</n-text>
<n-select
:value="rootLogin"
:options="rootLoginOptions"
:loading="rootLoginLoading"
style="max-width: 400px"
@update:value="handleUpdateRootLogin"
/>
</n-flex>
<!-- Root 密码 -->
<n-flex vertical :size="8">
<n-text strong>{{ $gettext('Root Password') }}</n-text>
<n-flex align="center" :size="12">
<n-input
v-model:value="rootPassword"
type="password"
show-password-on="click"
:placeholder="$gettext('Enter new password')"
style="max-width: 300px"
/>
<n-button @click="handleGeneratePassword">
<template #icon>
<the-icon :size="16" icon="mdi:refresh" />
</template>
</n-button>
<n-button
type="warning"
:loading="rootPasswordLoading"
@click="handleUpdateRootPassword"
>
{{ $gettext('Reset') }}
</n-button>
</n-flex>
<n-text depth="3">
{{
$gettext(
'It is recommended to use a complex password. Save after modification. Refresh will clear the password field.'
)
}}
</n-text>
</n-flex>
<!-- Root 密钥 -->
<n-flex vertical :size="4">
<n-flex align="center" :size="12">
<n-text strong>{{ $gettext('Root Key') }}</n-text>
<n-button type="primary" :loading="keyLoading" @click="handleViewKey">
{{ $gettext('View Key') }}
</n-button>
<n-button :loading="keyLoading" @click="handleDownloadKey">
{{ $gettext('Download') }}
</n-button>
</n-flex>
<n-text depth="3">
{{
$gettext('Recommended to use key login with password disabled for higher security')
}}
</n-text>
</n-flex>
</n-flex>
</n-card>
</n-flex>
</n-spin>
<!-- 查看私钥弹窗 -->
<n-modal
v-model:show="showKeyModal"
preset="card"
:title="$gettext('Root Private Key')"
style="width: 60vw"
:bordered="false"
>
<n-flex vertical :size="16">
<n-alert type="warning">
{{
$gettext(
'This is the private key of the root user. Keep it safe and use it to login to this server.'
)
}}
</n-alert>
<n-input
:value="rootKey"
type="textarea"
:rows="10"
readonly
:placeholder="$gettext('No private key generated')"
/>
<n-flex justify="end" :size="12">
<n-button :loading="keyLoading" @click="handleGenerateKey">
{{ $gettext('Regenerate') }}
</n-button>
<n-button type="primary" @click="handleDownloadKey">
{{ $gettext('Download Private Key') }}
</n-button>
</n-flex>
</n-flex>
</n-modal>
</template>
<style scoped lang="scss"></style>

View File

@@ -21,7 +21,6 @@ const hosts = ref('')
const timezone = ref('')
const timezones = ref<any[]>([])
const time = ref(DateTime.now().toMillis())
const rootPassword = ref('')
useRequest(system.dns()).onSuccess(({ data }) => {
dns1.value = data[0]
@@ -65,12 +64,6 @@ const handleUpdateHost = async () => {
})
}
const handleUpdateRootPassword = () => {
useRequest(system.updateRootPassword(rootPassword.value)).onSuccess(() => {
window.$message.success($gettext('Saved successfully'))
})
}
const handleUpdateTime = async () => {
await Promise.all([
useRequest(system.updateTime(String(DateTime.fromMillis(time.value).toISO()))),
@@ -180,15 +173,5 @@ const handleSyncTime = () => {
</n-flex>
</n-flex>
</n-tab-pane>
<n-tab-pane name="root-password" :tab="$gettext('Root Password')">
<n-form>
<n-form-item :label="$gettext('Root Password')">
<n-input v-model:value="rootPassword" type="password" show-password-on="click" />
</n-form-item>
</n-form>
<n-button type="primary" @click="handleUpdateRootPassword">
{{ $gettext('Save') }}
</n-button>
</n-tab-pane>
</n-tabs>
</template>