From c004acbfa04d10c1e4107af4c201ab3ac92ac4cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Tue, 18 Jul 2023 00:42:37 +0800 Subject: [PATCH] feat: safe controller --- app/console/commands/panel.go | 4 +- .../plugins/mysql80/mysql80_controller.go | 31 +-- app/http/controllers/safe_controller.go | 245 ++++++++++++++++++ app/http/controllers/setting_controller.go | 131 ++++++++++ app/models/setting.go | 11 + 5 files changed, 405 insertions(+), 17 deletions(-) create mode 100644 app/http/controllers/safe_controller.go create mode 100644 app/http/controllers/setting_controller.go diff --git a/app/console/commands/panel.go b/app/console/commands/panel.go index 1ec17b88..51477cdd 100644 --- a/app/console/commands/panel.go +++ b/app/console/commands/panel.go @@ -51,7 +51,7 @@ func (receiver *Panel) Handle(ctx console.Context) error { return nil } - settings := []models.Setting{{Key: "name", Value: "耗子Linux面板"}, {Key: "monitor", Value: "1"}, {Key: "monitor_days", Value: "30"}, {Key: "backup_path", Value: "/www/backup"}, {Key: "website_path", Value: "/www/wwwroot"}, {Key: "panel_entrance", Value: "/"}} + settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyPanelEntrance, Value: "/"}} err = facades.Orm().Query().Create(&settings) if err != nil { color.Redln("初始化失败") @@ -176,7 +176,7 @@ func (receiver *Panel) Handle(ctx console.Context) error { var setting models.Setting err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{ - Key: "mysql_root_password", + Key: models.SettingKeyMysqlRootPassword, }, models.Setting{ Value: password, }) diff --git a/app/http/controllers/plugins/mysql80/mysql80_controller.go b/app/http/controllers/plugins/mysql80/mysql80_controller.go index d4565ff1..b5f2d5d0 100644 --- a/app/http/controllers/plugins/mysql80/mysql80_controller.go +++ b/app/http/controllers/plugins/mysql80/mysql80_controller.go @@ -12,6 +12,7 @@ import ( "github.com/goravel/framework/support/carbon" "github.com/spf13/cast" "golang.org/x/exp/slices" + "panel/app/models" "panel/app/http/controllers" "panel/app/http/controllers/plugins" @@ -175,7 +176,7 @@ func (r *Mysql80Controller) Load(ctx http.Context) { return } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) if len(rootPassword) == 0 { controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空") return @@ -293,7 +294,7 @@ func (r *Mysql80Controller) GetRootPassword(ctx http.Context) { return } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) if len(rootPassword) == 0 { controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空") return @@ -319,17 +320,17 @@ func (r *Mysql80Controller) SetRootPassword(ctx http.Context) { return } - rootPassword := ctx.Request().Input("mysql_root_password") + rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword) if len(rootPassword) == 0 { controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空") return } - oldRootPassword := r.setting.Get("mysql_root_password") + oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) if oldRootPassword != rootPassword { helper.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"") helper.ExecShell("mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"") - err := r.setting.Set("mysql_root_password", rootPassword) + err := r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword) if err != nil { helper.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"") helper.ExecShell("mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"") @@ -347,7 +348,7 @@ func (r *Mysql80Controller) DatabaseList(ctx http.Context) { return } - out := helper.ExecShell("mysql -uroot -p" + r.setting.Get("mysql_root_password") + " -e \"show databases;\"") + out := helper.ExecShell("mysql -uroot -p" + r.setting.Get(models.SettingKeyMysqlRootPassword) + " -e \"show databases;\"") databases := strings.Split(out, "\n") databases = databases[1 : len(databases)-1] @@ -392,7 +393,7 @@ func (r *Mysql80Controller) AddDatabase(ctx http.Context) { return } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) database := ctx.Request().Input("database") user := ctx.Request().Input("user") password := ctx.Request().Input("password") @@ -423,7 +424,7 @@ func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) { return } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) database := ctx.Request().Input("database") helper.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"") @@ -483,7 +484,7 @@ func (r *Mysql80Controller) CreateBackup(ctx http.Context) { } backupPath := "/www/backup/mysql" - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) database := ctx.Request().Input("database") backupFile := backupPath + "/" + database + "_" + carbon.Now().ToShortDateTimeString() + ".sql" if !helper.Exists(backupPath) { @@ -549,7 +550,7 @@ func (r *Mysql80Controller) RestoreBackup(ctx http.Context) { } backupPath := "/www/backup/mysql" - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) file := ctx.Request().Input("file") backupFile := backupPath + "/" + file if !helper.Exists(backupFile) { @@ -614,7 +615,7 @@ func (r *Mysql80Controller) UserList(ctx http.Context) { Privileges string `json:"privileges"` } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) out := helper.ExecShell("mysql -uroot -p" + rootPassword + " -e 'select user,host from mysql.user'") rawUsers := strings.Split(out, "\n") users := make([]User, 0) @@ -660,7 +661,7 @@ func (r *Mysql80Controller) AddUser(ctx http.Context) { return } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) user := ctx.Request().Input("user") password := ctx.Request().Input("password") database := ctx.Request().Input("database") @@ -689,7 +690,7 @@ func (r *Mysql80Controller) DeleteUser(ctx http.Context) { return } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) user := ctx.Request().Input("user") helper.ExecShell("mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"") @@ -715,7 +716,7 @@ func (r *Mysql80Controller) SetUserPassword(ctx http.Context) { return } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) user := ctx.Request().Input("user") password := ctx.Request().Input("password") helper.ExecShell("mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"") @@ -743,7 +744,7 @@ func (r *Mysql80Controller) SetUserPrivileges(ctx http.Context) { return } - rootPassword := r.setting.Get("mysql_root_password") + rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword) user := ctx.Request().Input("user") database := ctx.Request().Input("database") helper.ExecShell("mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"") diff --git a/app/http/controllers/safe_controller.go b/app/http/controllers/safe_controller.go new file mode 100644 index 00000000..89f0c848 --- /dev/null +++ b/app/http/controllers/safe_controller.go @@ -0,0 +1,245 @@ +package controllers + +import ( + "regexp" + "strings" + + "github.com/goravel/framework/contracts/http" + "github.com/spf13/cast" + + "panel/packages/helper" +) + +type SafeController struct { + // Dependent services +} + +func NewSafeController() *SafeController { + return &SafeController{ + // Inject services + } +} + +func (r *SafeController) GetFirewallStatus(ctx http.Context) { + Success(ctx, r.firewallStatus()) +} + +func (r *SafeController) SetFirewallStatus(ctx http.Context) { + var out string + if ctx.Request().QueryBool("status") { + if helper.IsRHEL() { + out = helper.ExecShell("systemctl start firewalld") + } else { + out = helper.ExecShell("echo y | ufw enable") + } + } else { + if helper.IsRHEL() { + out = helper.ExecShell("systemctl stop firewalld") + } else { + out = helper.ExecShell("ufw disable") + } + } + + Success(ctx, out) +} + +func (r *SafeController) GetFirewallRules(ctx http.Context) { + if !r.firewallStatus() { + Error(ctx, http.StatusBadRequest, "防火墙未启动") + return + } + + if helper.IsRHEL() { + out := helper.ExecShell("firewall-cmd --list-all 2>&1") + match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out) + if len(match) == 0 { + Success(ctx, nil) + return + } + ports := strings.Split(match[1], " ") + var rules []map[string]string + for _, port := range ports { + rule := strings.Split(port, "/") + rules = append(rules, map[string]string{ + "port": rule[0], + "protocol": rule[1], + }) + } + + Success(ctx, rules) + } else { + out := helper.ExecShell("ufw status numbered | grep ALLOW | awk '{print $2}'") + if len(out) == 0 { + Success(ctx, nil) + return + } + var rules []map[string]string + for _, port := range strings.Split(out, "\n") { + if strings.Contains(port, "]") { + continue + } + rule := strings.Split(port, "/") + rules = append(rules, map[string]string{ + "port": rule[0], + "protocol": rule[1], + }) + } + + Success(ctx, rules) + } +} + +func (r *SafeController) AddFirewallRule(ctx http.Context) { + if !r.firewallStatus() { + Error(ctx, http.StatusBadRequest, "防火墙未启动") + return + } + + port := ctx.Request().InputInt("port", 0) + protocol := ctx.Request().Input("protocol", "") + if port == 0 || protocol == "" { + Error(ctx, http.StatusBadRequest, "参数错误") + return + } + + if helper.IsRHEL() { + helper.ExecShell("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1") + helper.ExecShell("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1") + helper.ExecShell("firewall-cmd --reload") + } else { + helper.ExecShell("ufw delete allow " + cast.ToString(port) + "/" + protocol) + helper.ExecShell("ufw allow " + cast.ToString(port) + "/" + protocol) + helper.ExecShell("ufw reload") + } + + Success(ctx, nil) +} + +func (r *SafeController) DeleteFirewallRule(ctx http.Context) { + if !r.firewallStatus() { + Error(ctx, http.StatusBadRequest, "防火墙未启动") + return + } + + port := ctx.Request().InputInt("port", 0) + protocol := ctx.Request().Input("protocol", "") + if port == 0 || protocol == "" { + Error(ctx, http.StatusBadRequest, "参数错误") + return + } + + if helper.IsRHEL() { + helper.ExecShell("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1") + helper.ExecShell("firewall-cmd --reload") + } else { + helper.ExecShell("ufw delete allow " + cast.ToString(port) + "/" + protocol) + helper.ExecShell("ufw reload") + } + + Success(ctx, nil) +} + +func (r *SafeController) firewallStatus() bool { + var out string + var running bool + if helper.IsRHEL() { + out = helper.ExecShell("systemctl status firewalld | grep Active | awk '{print $3}'") + if out == "(running)" { + running = true + } else { + running = false + } + } else { + out = helper.ExecShell("ufw status | grep Status | awk '{print $2}'") + if out == "active" { + running = true + } else { + running = false + } + } + + return running +} + +func (r *SafeController) GetSshStatus(ctx http.Context) { + out := helper.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'") + running := false + if out == "(running)" { + running = true + } + + Success(ctx, running) +} + +func (r *SafeController) SetSshStatus(ctx http.Context) { + if ctx.Request().QueryBool("status") { + helper.ExecShell("systemctl enable sshd") + helper.ExecShell("systemctl start sshd") + } else { + helper.ExecShell("systemctl stop sshd") + helper.ExecShell("systemctl disable sshd") + } + + Success(ctx, nil) +} + +func (r *SafeController) GetSshPort(ctx http.Context) { + out := helper.ExecShell("cat /etc/ssh/sshd_config | grep Port | awk '{print $2}'") + Success(ctx, out) +} + +func (r *SafeController) SetSshPort(ctx http.Context) { + port := ctx.Request().InputInt("port", 0) + if port == 0 { + Error(ctx, http.StatusBadRequest, "参数错误") + return + } + + oldPort := helper.ExecShell("cat /etc/ssh/sshd_config | grep Port | awk '{print $2}'") + helper.ExecShell("sed -i 's/#Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config") + helper.ExecShell("sed -i 's/Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config") + + if status := helper.ExecShell("systemctl status sshd | grep Active | awk '{print $3}'"); status == "(running)" { + helper.ExecShell("systemctl restart sshd") + } + + Success(ctx, nil) +} + +func (r *SafeController) GetPingStatus(ctx http.Context) { + if helper.IsRHEL() { + out := helper.ExecShell("firewall-cmd --query-rich-rule='rule protocol value=icmp drop' 2>&1") + if out == "no" { + Success(ctx, true) + } else { + Success(ctx, false) + } + } else { + config := helper.ReadFile("/etc/ufw/before.rules") + if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") { + Success(ctx, true) + } else { + Success(ctx, false) + } + } +} + +func (r *SafeController) SetPingStatus(ctx http.Context) { + if helper.IsRHEL() { + if ctx.Request().QueryBool("status") { + helper.ExecShell("firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'") + } else { + helper.ExecShell("firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'") + } + helper.ExecShell("firewall-cmd --reload") + } else { + if ctx.Request().QueryBool("status") { + helper.ExecShell("sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules") + } else { + helper.ExecShell("sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules") + } + helper.ExecShell("ufw reload") + } + + Success(ctx, nil) +} diff --git a/app/http/controllers/setting_controller.go b/app/http/controllers/setting_controller.go new file mode 100644 index 00000000..a85b5cb9 --- /dev/null +++ b/app/http/controllers/setting_controller.go @@ -0,0 +1,131 @@ +package controllers + +import ( + "github.com/goravel/framework/contracts/http" + "github.com/goravel/framework/facades" + + "panel/app/models" + "panel/app/services" +) + +type SettingController struct { + setting services.Setting +} + +func NewSettingController() *SettingController { + return &SettingController{ + setting: services.NewSettingImpl(), + } +} + +func (r *SettingController) List(ctx http.Context) { + var settings []models.Setting + err := facades.Orm().Query().Get(&settings) + if err != nil { + facades.Log().Error("[面板][SettingController] 查询设置列表失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + + var result = make(map[string]string) + for _, setting := range settings { + if setting.Key == models.SettingKeyMysqlRootPassword { + continue + } + + result[setting.Key] = setting.Value + } + + var user models.User + err = facades.Auth().User(ctx, &user) + if err != nil { + facades.Log().Error("[面板][SettingController] 获取用户失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + + result["username"] = user.Username + result["email"] = user.Email + + Success(ctx, result) +} + +func (r *SettingController) Save(ctx http.Context) { + name := ctx.Request().Input("name") + port := ctx.Request().Input("port") + backupPath := ctx.Request().Input("backup_path") + websitePath := ctx.Request().Input("website_path") + panelEntrance := ctx.Request().Input("panel_entrance") + username := ctx.Request().Input("username") + email := ctx.Request().Input("email") + password := ctx.Request().Input("password") + + err := r.setting.Set(models.SettingKeyName, name) + if err != nil { + facades.Log().Error("[面板][SettingController] 保存设置失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + err = r.setting.Set(models.SettingKeyPort, port) + if err != nil { + facades.Log().Error("[面板][SettingController] 保存设置失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + err = r.setting.Set(models.SettingKeyBackupPath, backupPath) + if err != nil { + facades.Log().Error("[面板][SettingController] 保存设置失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + err = r.setting.Set(models.SettingKeyWebsitePath, websitePath) + if err != nil { + facades.Log().Error("[面板][SettingController] 保存设置失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + err = r.setting.Set(models.SettingKeyPanelEntrance, panelEntrance) + if err != nil { + facades.Log().Error("[面板][SettingController] 保存设置失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + + var user models.User + err = facades.Auth().User(ctx, &user) + if err != nil { + facades.Log().Error("[面板][SettingController] 获取用户失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + + user.Username = username + user.Email = email + if len(password) > 0 { + hash, err := facades.Hash().Make(password) + if err != nil { + facades.Log().Error("[面板][SettingController] 保存设置失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + user.Password = hash + } + + if err = facades.Orm().Query().Save(&user); err != nil { + facades.Log().Error("[面板][SettingController] 保存设置失败 ", err) + Error(ctx, http.StatusInternalServerError, "系统内部错误") + + return + } + + Success(ctx, nil) +} diff --git a/app/models/setting.go b/app/models/setting.go index 31f06550..92c70d73 100644 --- a/app/models/setting.go +++ b/app/models/setting.go @@ -2,6 +2,17 @@ package models import "github.com/goravel/framework/support/carbon" +const ( + SettingKeyName = "name" + SettingKeyPort = "port" + SettingKeyMonitor = "monitor" + SettingKeyMonitorDays = "monitor_days" + SettingKeyBackupPath = "backup_path" + SettingKeyWebsitePath = "website_path" + SettingKeyPanelEntrance = "panel_entrance" + SettingKeyMysqlRootPassword = "mysql_root_password" +) + type Setting struct { ID uint `gorm:"primaryKey" json:"id"` Key string `gorm:"unique;not null" json:"key"`