diff --git a/cmd/ace/wire_gen.go b/cmd/ace/wire_gen.go
index 64e72e1a..b83d2901 100644
--- a/cmd/ace/wire_gen.go
+++ b/cmd/ace/wire_gen.go
@@ -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)
diff --git a/go.sum b/go.sum
index 2b43e86e..72e7e4b4 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/internal/http/request/toolbox_ssh.go b/internal/http/request/toolbox_ssh.go
new file mode 100644
index 00000000..1ec9456b
--- /dev/null
+++ b/internal/http/request/toolbox_ssh.go
@@ -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"`
+}
diff --git a/internal/route/http.go b/internal/route/http.go
index 49831ff1..b4b21103 100644
--- a/internal/route/http.go
+++ b/internal/route/http.go
@@ -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)
diff --git a/internal/service/service.go b/internal/service/service.go
index 50ac3f25..756e74c9 100644
--- a/internal/service/service.go
+++ b/internal/service/service.go
@@ -37,5 +37,6 @@ var ProviderSet = wire.NewSet(
NewWebsiteService,
NewToolboxSystemService,
NewToolboxBenchmarkService,
+ NewToolboxSSHService,
NewWsService,
)
diff --git a/internal/service/toolbox_ssh.go b/internal/service/toolbox_ssh.go
new file mode 100644
index 00000000..7440d7c5
--- /dev/null
+++ b/internal/service/toolbox_ssh.go
@@ -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
+}
diff --git a/internal/service/toolbox_system.go b/internal/service/toolbox_system.go
index f50ad006..db96e5e6 100644
--- a/internal/service/toolbox_system.go
+++ b/internal/service/toolbox_system.go
@@ -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)
-}
diff --git a/web/src/api/panel/toolbox-ssh/index.ts b/web/src/api/panel/toolbox-ssh/index.ts
new file mode 100644
index 00000000..7db5fae3
--- /dev/null
+++ b/web/src/api/panel/toolbox-ssh/index.ts
@@ -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')
+}
diff --git a/web/src/views/toolbox/IndexView.vue b/web/src/views/toolbox/IndexView.vue
index 806d6635..ca016a10 100644
--- a/web/src/views/toolbox/IndexView.vue
+++ b/web/src/views/toolbox/IndexView.vue
@@ -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')
+
@@ -26,6 +28,7 @@ const current = ref('process')
+
diff --git a/web/src/views/toolbox/SSHView.vue b/web/src/views/toolbox/SSHView.vue
new file mode 100644
index 00000000..276c3a52
--- /dev/null
+++ b/web/src/views/toolbox/SSHView.vue
@@ -0,0 +1,393 @@
+
+
+
+
+
+
+
+
+ {{ $gettext('SSH Service Status') }}
+
+
+ {{ $gettext('Restart') }}
+
+
+
+
+
+
+
+
+
+
+ {{ $gettext('SSH Password Login') }}
+
+
+ {{ $gettext('Allow password authentication for SSH login') }}
+
+
+
+
+
+ {{ $gettext('SSH Key Login') }}
+
+
+ {{
+ $gettext('Allow public key authentication for SSH login')
+ }}
+
+
+
+
+
+ {{ $gettext('SSH Port') }}
+
+
+
+
+
+
+
+ {{ $gettext('Save') }}
+
+
+ {{ $gettext('Current SSH port, default is 22') }}
+
+
+
+
+
+
+
+
+
+ {{ $gettext('Root Password Login Setting') }}
+
+
+
+
+
+ {{ $gettext('Root Password') }}
+
+
+
+
+
+
+
+
+ {{ $gettext('Reset') }}
+
+
+
+ {{
+ $gettext(
+ 'It is recommended to use a complex password. Save after modification. Refresh will clear the password field.'
+ )
+ }}
+
+
+
+
+
+
+ {{ $gettext('Root Key') }}
+
+ {{ $gettext('View Key') }}
+
+
+ {{ $gettext('Download') }}
+
+
+
+ {{
+ $gettext('Recommended to use key login with password disabled for higher security')
+ }}
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ $gettext(
+ 'This is the private key of the root user. Keep it safe and use it to login to this server.'
+ )
+ }}
+
+
+
+
+ {{ $gettext('Regenerate') }}
+
+
+ {{ $gettext('Download Private Key') }}
+
+
+
+
+
+
+
diff --git a/web/src/views/toolbox/SystemView.vue b/web/src/views/toolbox/SystemView.vue
index d29c28ea..cb07f7a1 100644
--- a/web/src/views/toolbox/SystemView.vue
+++ b/web/src/views/toolbox/SystemView.vue
@@ -21,7 +21,6 @@ const hosts = ref('')
const timezone = ref('')
const timezones = ref([])
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 = () => {
-
-
-
-
-
-
-
- {{ $gettext('Save') }}
-
-