package service import ( "fmt" "net" "os" "os/user" "path" "path/filepath" "sort" "strings" "time" "github.com/1Panel-dev/1Panel/backend/utils/geo" "github.com/gin-gonic/gin" "github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/systemctl" "github.com/pkg/errors" ) const sshPath = "/etc/ssh/sshd_config" type SSHService struct{} type ISSHService interface { GetSSHInfo() (*dto.SSHInfo, error) OperateSSH(operation string) error UpdateByFile(value string) error Update(req dto.SSHUpdate) error GenerateSSH(req dto.GenerateSSH) error LoadSSHSecret(mode string) (string, error) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog, error) LoadSSHConf() (string, error) } func NewISSHService() ISSHService { return &SSHService{} } func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) { data := dto.SSHInfo{ AutoStart: true, Status: constant.StatusEnable, Message: "", Port: "22", ListenAddress: "", PasswordAuthentication: "yes", PubkeyAuthentication: "yes", PermitRootLogin: "yes", UseDNS: "yes", } serviceName, err := loadServiceName() if err != nil { data.Status = constant.StatusDisable data.Message = err.Error() } isEnabled, _ := systemctl.IsEnable(serviceName) data.AutoStart = isEnabled isActive, err := systemctl.IsActive(serviceName) if isActive { data.Status = constant.StatusEnable } else { data.Status = constant.StatusDisable if err != nil { data.Message = err.Error() } } sshConf, err := os.ReadFile(sshPath) if err != nil { data.Message = err.Error() data.Status = constant.StatusDisable } lines := strings.Split(string(sshConf), "\n") for _, line := range lines { if strings.HasPrefix(line, "Port ") { data.Port = strings.ReplaceAll(line, "Port ", "") } if strings.HasPrefix(line, "ListenAddress ") { itemAddr := strings.ReplaceAll(line, "ListenAddress ", "") if len(data.ListenAddress) != 0 { data.ListenAddress += ("," + itemAddr) } else { data.ListenAddress = itemAddr } } if strings.HasPrefix(line, "PasswordAuthentication ") { data.PasswordAuthentication = strings.ReplaceAll(line, "PasswordAuthentication ", "") } if strings.HasPrefix(line, "PubkeyAuthentication ") { data.PubkeyAuthentication = strings.ReplaceAll(line, "PubkeyAuthentication ", "") } if strings.HasPrefix(line, "PermitRootLogin ") { data.PermitRootLogin = strings.ReplaceAll(strings.ReplaceAll(line, "PermitRootLogin ", ""), "prohibit-password", "without-password") } if strings.HasPrefix(line, "UseDNS ") { data.UseDNS = strings.ReplaceAll(line, "UseDNS ", "") } } return &data, nil } func (u *SSHService) OperateSSH(operation string) error { serviceName, err := loadServiceName() if err != nil { return err } switch operation { case "enable": return systemctl.Enable(serviceName) case "disable": return systemctl.Disable(serviceName) case "start": return systemctl.Start(serviceName) case "stop": isSocketActive, _ := systemctl.IsActive(serviceName + ".socket") if isSocketActive { if err := systemctl.Stop(serviceName + ".socket"); err != nil { global.LOG.Errorf("Failed to stop %s.socket: %v", serviceName, err) } } return systemctl.Stop(serviceName) case "restart": return systemctl.Restart(serviceName) } if result, err := systemctl.CustomAction(operation, serviceName); err != nil { return fmt.Errorf("failed to execute custom action: %v", result.Output) } return nil } func (u *SSHService) Update(req dto.SSHUpdate) error { serviceName, err := loadServiceName() if err != nil { return err } sshConf, err := os.ReadFile(sshPath) if err != nil { return err } lines := strings.Split(string(sshConf), "\n") newFiles := updateSSHConf(lines, req.Key, req.NewValue) file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, 0666) if err != nil { return err } defer file.Close() if _, err = file.WriteString(strings.Join(newFiles, "\n")); err != nil { return err } sudo := cmd.SudoHandleCmd() if req.Key == "Port" { stdout, _ := cmd.Execf("%s getenforce", sudo) if stdout == "Enforcing\n" { _, _ = cmd.Execf("%s semanage port -a -t ssh_port_t -p tcp %s", sudo, req.NewValue) } ruleItem := dto.PortRuleUpdate{ OldRule: dto.PortRuleOperate{ Operation: "remove", Port: req.OldValue, Protocol: "tcp", Strategy: "accept", }, NewRule: dto.PortRuleOperate{ Operation: "add", Port: req.NewValue, Protocol: "tcp", Strategy: "accept", }, } if err := NewIFirewallService().UpdatePortRule(ruleItem); err != nil { global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", req.OldValue, req.NewValue, err) } if err = NewIHostService().Update(1, map[string]interface{}{"port": req.NewValue}); err != nil { global.LOG.Errorf("reset host port %s -> %s failed, err: %v", req.OldValue, req.NewValue, err) } } err = systemctl.Restart(serviceName) if err != nil { global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err) } return nil } func (u *SSHService) UpdateByFile(value string) error { serviceName, err := loadServiceName() if err != nil { return err } file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, 0666) if err != nil { return err } defer file.Close() if _, err = file.WriteString(value); err != nil { return err } err = systemctl.Restart(serviceName) if err != nil { global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err) } return nil } func (u *SSHService) GenerateSSH(req dto.GenerateSSH) error { if cmd.CheckIllegal(req.EncryptionMode, req.Password) { return buserr.New(constant.ErrCmdIllegal) } currentUser, err := user.Current() if err != nil { return fmt.Errorf("load current user failed, err: %v", err) } secretFile := fmt.Sprintf("%s/.ssh/id_item_%s", currentUser.HomeDir, req.EncryptionMode) secretPubFile := fmt.Sprintf("%s/.ssh/id_item_%s.pub", currentUser.HomeDir, req.EncryptionMode) authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys" command := fmt.Sprintf("ssh-keygen -t %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, currentUser.HomeDir, req.EncryptionMode) if len(req.Password) != 0 { command = fmt.Sprintf("ssh-keygen -t %s -P %s -f %s/.ssh/id_item_%s | echo y", req.EncryptionMode, req.Password, currentUser.HomeDir, req.EncryptionMode) } stdout, err := cmd.Exec(command) if err != nil { return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout) } defer func() { _ = os.Remove(secretFile) }() defer func() { _ = os.Remove(secretPubFile) }() if _, err := os.Stat(authFilePath); err != nil && errors.Is(err, os.ErrNotExist) { authFile, err := os.Create(authFilePath) if err != nil { return err } defer authFile.Close() } stdout1, err := cmd.Execf("cat %s >> %s/.ssh/authorized_keys", secretPubFile, currentUser.HomeDir) if err != nil { return fmt.Errorf("generate failed, err: %v, message: %s", err, stdout1) } fileOp := files.NewFileOp() if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s", currentUser.HomeDir, req.EncryptionMode)); err != nil { return err } if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s.pub", currentUser.HomeDir, req.EncryptionMode)); err != nil { return err } return nil } func (u *SSHService) LoadSSHSecret(mode string) (string, error) { currentUser, err := user.Current() if err != nil { return "", fmt.Errorf("load current user failed, err: %v", err) } homeDir := currentUser.HomeDir if _, err := os.Stat(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)); err != nil { return "", nil } file, err := os.ReadFile(fmt.Sprintf("%s/.ssh/id_%s", homeDir, mode)) return string(file), err } type sshFileItem struct { Name string Year int } func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog, error) { var fileList []sshFileItem var data dto.SSHLog baseDir := "/var/log" if err := filepath.Walk(baseDir, func(pathItem string, info os.FileInfo, err error) error { if err != nil { return err } if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") || strings.HasPrefix(info.Name(), "messages")) { if !strings.HasSuffix(info.Name(), ".gz") { fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()}) return nil } itemFileName := strings.TrimSuffix(pathItem, ".gz") if _, err := os.Stat(itemFileName); err != nil && os.IsNotExist(err) { if err := handleGunzip(pathItem); err == nil { fileList = append(fileList, sshFileItem{Name: itemFileName, Year: info.ModTime().Year()}) } } } return nil }); err != nil { return nil, err } fileList = sortFileList(fileList) command := "" if len(req.Info) != 0 { command = fmt.Sprintf(" | grep '%s'", req.Info) } showCountFrom := (req.Page - 1) * req.PageSize showCountTo := req.Page * req.PageSize nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd()) for _, file := range fileList { commandItem := "" switch { case strings.HasPrefix(path.Base(file.Name), "secure"): switch req.Status { case constant.StatusSuccess: commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) case constant.StatusFailed: commandItem = fmt.Sprintf("cat %s | grep -a 'Failed password for' %s", file.Name, command) default: commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command) } case strings.HasPrefix(path.Base(file.Name), "messages"): switch req.Status { case constant.StatusSuccess: commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*Accepted (password|publickey)' %s", file.Name, command) case constant.StatusFailed: commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Failed password for|Connection closed by authenticating user)' %s", file.Name, command) default: commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Accepted|Failed password for|Connection closed)' %s", file.Name, command) } case strings.HasPrefix(path.Base(file.Name), "auth.log"): switch req.Status { case constant.StatusSuccess: commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) case constant.StatusFailed: commandItem = fmt.Sprintf("cat %s | grep -aE 'Failed password for|Connection closed by authenticating user' %s", file.Name, command) default: commandItem = fmt.Sprintf("cat %s | grep -aE \"(Failed password for|Connection closed by authenticating user|Accepted)\" %s", file.Name, command) } } dataItem, successCount, failedCount := loadSSHData(c, commandItem, showCountFrom, showCountTo, file.Year, nyc) data.FailedCount += failedCount data.TotalCount += successCount + failedCount showCountFrom = showCountFrom - (successCount + failedCount) showCountTo = showCountTo - (successCount + failedCount) data.Logs = append(data.Logs, dataItem...) } data.SuccessfulCount = data.TotalCount - data.FailedCount return &data, nil } func (u *SSHService) LoadSSHConf() (string, error) { if _, err := os.Stat("/etc/ssh/sshd_config"); err != nil { return "", buserr.New("ErrHttpReqNotFound") } content, err := os.ReadFile("/etc/ssh/sshd_config") if err != nil { return "", err } return string(content), nil } func sortFileList(fileNames []sshFileItem) []sshFileItem { if len(fileNames) < 2 { return fileNames } if strings.HasPrefix(path.Base(fileNames[0].Name), "secure") { var itemFile []sshFileItem sort.Slice(fileNames, func(i, j int) bool { return fileNames[i].Name > fileNames[j].Name }) itemFile = append(itemFile, fileNames[len(fileNames)-1]) itemFile = append(itemFile, fileNames[:len(fileNames)-1]...) return itemFile } sort.Slice(fileNames, func(i, j int) bool { return fileNames[i].Name < fileNames[j].Name }) return fileNames } func updateSSHConf(oldFiles []string, param string, value string) []string { var valueItems []string if param != "ListenAddress" { valueItems = append(valueItems, value) } else { if value != "" { valueItems = strings.Split(value, ",") } } var newFiles []string for _, line := range oldFiles { lineItem := strings.TrimSpace(line) if (strings.HasPrefix(lineItem, param) || strings.HasPrefix(lineItem, fmt.Sprintf("#%s", param))) && len(valueItems) != 0 { newFiles = append(newFiles, fmt.Sprintf("%s %s", param, valueItems[0])) valueItems = valueItems[1:] continue } if strings.HasPrefix(lineItem, param) && len(valueItems) == 0 { newFiles = append(newFiles, fmt.Sprintf("#%s", line)) continue } newFiles = append(newFiles, line) } if len(valueItems) != 0 { for _, item := range valueItems { newFiles = append(newFiles, fmt.Sprintf("%s %s", param, item)) } } return newFiles } func loadSSHData(c *gin.Context, command string, showCountFrom, showCountTo, currentYear int, nyc *time.Location) ([]dto.SSHHistory, int, int) { var ( datas []dto.SSHHistory successCount int failedCount int ) stdout2, err := cmd.Exec(command) if err != nil { return datas, 0, 0 } lines := strings.Split(string(stdout2), "\n") for i := len(lines) - 1; i >= 0; i-- { var itemData dto.SSHHistory line := strings.TrimSpace(lines[i]) if line == "" { continue } parts := strings.Fields(line) if len(parts) < 12 { continue } // 统一时间解析逻辑 var dateStr string var timeIndex int if strings.Contains(parts[0], "-") { // 处理RFC3339时间格式 t, err := time.Parse(time.RFC3339Nano, parts[0]) if err == nil { dateStr = t.Format("2006 Jan 2 15:04:05") timeIndex = 0 } } else { // 处理系统日志格式 dateParts := parts[:3] if len(dateParts) < 3 { continue } dateStr = strings.Join(dateParts, " ") timeIndex = 3 } // 根据日志类型解析内容 switch { case strings.Contains(line, "Failed password for"): itemData = parseFailedPasswordLog(parts, timeIndex, dateStr) case strings.Contains(line, "Connection closed by authenticating user"): itemData = parseConnectionClosedLog(parts, timeIndex, dateStr) case strings.Contains(line, "Accepted"): itemData = parseAcceptedLog(parts, timeIndex, dateStr) default: continue } if itemData.Address == "" { continue } total := successCount + failedCount if total >= showCountFrom && total < showCountTo { itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c)) itemData.Date = parseLogDate(currentYear, dateStr, nyc) datas = append(datas, itemData) } if itemData.Status == constant.StatusSuccess { successCount++ } else { failedCount++ } if total >= showCountTo { break } } return datas, successCount, failedCount } func parseFailedPasswordLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory { data := dto.SSHHistory{ DateStr: dateStr, Status: constant.StatusFailed, AuthMode: "publickey", } // 查找关键字段位置 for i := timeIndex; i < len(parts); i++ { switch parts[i] { case "for": if i+1 < len(parts) { data.User = parts[i+1] } case "from": if i+1 < len(parts) { data.Address = parts[i+1] } case "port": if i+1 < len(parts) { data.Port = parts[i+1] } case "password": data.AuthMode = "password" } } if strings.Contains(strings.Join(parts, " "), "invalid user") { data.Message = "invalid user attempt" } return data } func parseConnectionClosedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory { data := dto.SSHHistory{ DateStr: dateStr, Status: constant.StatusFailed, } // 解析Alpine格式的特殊字段 fieldStart := timeIndex + 5 // 跳过时间、主机、进程字段 for i := fieldStart; i < len(parts); i++ { switch { case parts[i] == "user": if i+1 < len(parts) { data.User = parts[i+1] } case parts[i] == "port": if i+1 < len(parts) { data.Port = parts[i+1] } case isIPAddress(parts[i]): data.Address = parts[i] } } return data } func parseAcceptedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory { data := dto.SSHHistory{ DateStr: dateStr, Status: constant.StatusSuccess, AuthMode: "password", // 默认值 } // 处理不同日志格式 fieldStart := timeIndex + 5 // 基础字段偏移 for i := fieldStart; i < len(parts); i++ { switch { case parts[i] == "for": if i+1 < len(parts) { data.User = parts[i+1] } case parts[i] == "from": if i+1 < len(parts) { data.Address = parts[i+1] } case parts[i] == "port": if i+1 < len(parts) { data.Port = parts[i+1] } case strings.Contains(parts[i], "ssh2:"): data.AuthMode = "publickey" case parts[i] == "publickey": data.AuthMode = "publickey" case parts[i] == "password": data.AuthMode = "password" } } return data } func handleGunzip(path string) error { if _, err := cmd.Execf("gunzip %s", path); err != nil { return err } return nil } func loadServiceName() (string, error) { serviceName, err := systemctl.GetServiceName("ssh") if err != nil { if errors.Is(err, systemctl.ErrServiceNotFound) { return "", fmt.Errorf("SSH service unavailable. Please ensure OpenSSH server is installed and configured") } return "", fmt.Errorf("failed to load SSH service name: %w", err) } return serviceName, nil } func parseLogDate(currentYear int, dateStr string, loc *time.Location) time.Time { // 处理带年份和不带年份的情况 formats := []string{ "2006 Jan 2 15:04:05", "Jan 2 15:04:05", } for _, format := range formats { t, err := time.ParseInLocation(format, dateStr, loc) if err == nil { if t.Year() == 0 { return time.Date(currentYear, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, loc) } return t } } return time.Now().In(loc) } func isIPAddress(s string) bool { return net.ParseIP(s) != nil }