diff --git a/backend/app/api/v1/ssh.go b/backend/app/api/v1/ssh.go index 236c3b0a2..dfda7b09c 100644 --- a/backend/app/api/v1/ssh.go +++ b/backend/app/api/v1/ssh.go @@ -103,3 +103,30 @@ func (b *BaseApi) LoadSSHSecret(c *gin.Context) { } helper.SuccessWithData(c, data) } + +// @Tags SSH +// @Summary Load host ssh logs +// @Description 获取 ssh 登录日志 +// @Accept json +// @Param request body dto.SearchSSHLog true "request" +// @Success 200 {object} dto.SSHLog +// @Security ApiKeyAuth +// @Router /host/ssh/logs [post] +func (b *BaseApi) LoadSSHLogs(c *gin.Context) { + var req dto.SearchSSHLog + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + data, err := sshService.LoadLog(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} diff --git a/backend/app/dto/ssh.go b/backend/app/dto/ssh.go index 6191ccd34..a4429d7c3 100644 --- a/backend/app/dto/ssh.go +++ b/backend/app/dto/ssh.go @@ -1,5 +1,7 @@ package dto +import "time" + type SSHInfo struct { Port string `json:"port" validate:"required,number,max=65535,min=1"` ListenAddress string `json:"listenAddress"` @@ -17,3 +19,25 @@ type GenerateSSH struct { type GenerateLoad struct { EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"` } + +type SearchSSHLog struct { + PageInfo + Info string `json:"info"` + Status string `json:"Status" validate:"required,oneof=Success Failed All"` +} +type SSHLog struct { + Logs []SSHHistory `json:"logs"` + TotalCount int `json:"totalCount"` + SuccessfulCount int `json:"successfulCount"` + FailedCount int `json:"failedCount"` +} +type SSHHistory struct { + Date time.Time `json:"date"` + Belong string `json:"belong"` + User string `json:"user"` + AuthMode string `json:"authMode"` + Address string `json:"address"` + Port string `json:"port"` + Status string `json:"status"` + Message string `json:"message"` +} diff --git a/backend/app/service/ssh.go b/backend/app/service/ssh.go index ab0c45924..182d382b9 100644 --- a/backend/app/service/ssh.go +++ b/backend/app/service/ssh.go @@ -4,9 +4,14 @@ import ( "fmt" "os" "os/user" + "path" + "path/filepath" + "sort" "strings" + "time" "github.com/1Panel-dev/1Panel/backend/app/dto" + "github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/files" ) @@ -20,6 +25,7 @@ type ISSHService interface { Update(key, value string) error GenerateSSH(req dto.GenerateSSH) error LoadSSHSecret(mode string) (string, error) + LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) } func NewISSHService() ISSHService { @@ -141,6 +147,76 @@ func (u *SSHService) LoadSSHSecret(mode string) (string, error) { return string(file), err } +func (u *SSHService) LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error) { + var fileList []string + 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") { + if strings.HasSuffix(info.Name(), ".gz") { + if err := handleGunzip(pathItem); err == nil { + fileList = append(fileList, strings.ReplaceAll(pathItem, ".gz", "")) + } + } else { + fileList = append(fileList, pathItem) + } + } + return nil + }); err != nil { + return nil, err + } + + command := "" + if len(req.Info) != 0 { + command = fmt.Sprintf(" | grep '%s'", req.Info) + } + for i := 0; i < len(fileList); i++ { + if strings.HasPrefix(path.Base(fileList[i]), "secure") { + dataItem := loadFailedSecureDatas(fmt.Sprintf("cat %s | grep -a 'Failed password for' | grep -v 'invalid' %s", fileList[i], command)) + data.FailedCount += len(dataItem) + data.TotalCount += len(dataItem) + if req.Status != constant.StatusSuccess { + data.Logs = append(data.Logs, dataItem...) + } + } + if strings.HasPrefix(path.Base(fileList[i]), "auth.log") { + dataItem := loadFailedAuthDatas(fmt.Sprintf("cat %s | grep -a 'Connection closed by authenticating user' | grep -a 'preauth' %s", fileList[i], command)) + data.FailedCount += len(dataItem) + data.TotalCount += len(dataItem) + if req.Status != constant.StatusSuccess { + data.Logs = append(data.Logs, dataItem...) + } + } + dataItem := loadSuccessDatas(fmt.Sprintf("cat %s | grep Accepted %s", fileList[i], command)) + data.TotalCount += len(dataItem) + if req.Status != constant.StatusFailed { + data.Logs = append(data.Logs, dataItem...) + } + } + data.SuccessfulCount = data.TotalCount - data.FailedCount + + sort.Slice(data.Logs, func(i, j int) bool { + return data.Logs[i].Date.After(data.Logs[j].Date) + }) + + var itemDatas []dto.SSHHistory + total, start, end := len(data.Logs), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + itemDatas = make([]dto.SSHHistory, 0) + } else { + if end >= total { + end = total + } + itemDatas = data.Logs[start:end] + } + data.Logs = itemDatas + + return &data, nil +} + func updateSSHConf(oldFiles []string, param string, value interface{}) []string { hasKey := false var newFiles []string @@ -170,3 +246,103 @@ func updateSSHConf(oldFiles []string, param string, value interface{}) []string } return newFiles } + +func loadSuccessDatas(command string) []dto.SSHHistory { + var datas []dto.SSHHistory + timeNow := time.Now() + stdout2, err := cmd.Exec(command) + if err == nil { + lines := strings.Split(string(stdout2), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) != 14 { + continue + } + historyItem := dto.SSHHistory{ + Belong: parts[3], + AuthMode: parts[6], + User: parts[8], + Address: parts[10], + Port: parts[12], + Status: constant.StatusSuccess, + } + historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s %s %s", timeNow.Year(), parts[0], parts[1], parts[2])) + if historyItem.Date.After(timeNow) { + historyItem.Date = historyItem.Date.AddDate(-1, 0, 0) + } + datas = append(datas, historyItem) + } + } + return datas +} + +func loadFailedAuthDatas(command string) []dto.SSHHistory { + var datas []dto.SSHHistory + timeNow := time.Now() + stdout2, err := cmd.Exec(command) + if err == nil { + lines := strings.Split(string(stdout2), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) != 15 { + continue + } + historyItem := dto.SSHHistory{ + Belong: parts[3], + AuthMode: parts[8], + User: parts[10], + Address: parts[11], + Port: parts[13], + Status: constant.StatusFailed, + } + historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s %s %s", timeNow.Year(), parts[0], parts[1], parts[2])) + if historyItem.Date.After(timeNow) { + historyItem.Date = historyItem.Date.AddDate(-1, 0, 0) + } + if strings.Contains(line, ": ") { + historyItem.Message = strings.Split(line, ": ")[0] + } + datas = append(datas, historyItem) + } + } + return datas +} + +func loadFailedSecureDatas(command string) []dto.SSHHistory { + var datas []dto.SSHHistory + timeNow := time.Now() + stdout2, err := cmd.Exec(command) + if err == nil { + lines := strings.Split(string(stdout2), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) != 14 { + continue + } + historyItem := dto.SSHHistory{ + Belong: parts[3], + AuthMode: parts[6], + User: parts[8], + Address: parts[10], + Port: parts[12], + Status: constant.StatusFailed, + } + historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s %s %s", timeNow.Year(), parts[0], parts[1], parts[2])) + if historyItem.Date.After(timeNow) { + historyItem.Date = historyItem.Date.AddDate(-1, 0, 0) + } + if strings.Contains(line, ": ") { + historyItem.Message = strings.Split(line, ": ")[0] + } + datas = append(datas, historyItem) + } + } + return datas +} + +func handleGunzip(path string) error { + if _, err := cmd.Execf("gunzip %s", path); err != nil { + return err + } + return nil +} diff --git a/backend/app/service/ssh_test.go b/backend/app/service/ssh_test.go new file mode 100644 index 000000000..c37da5b85 --- /dev/null +++ b/backend/app/service/ssh_test.go @@ -0,0 +1,98 @@ +package service + +import ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/utils/cmd" +) + +func TestCa(t *testing.T) { + var ( + fileList []string + datas []history + successfulCount int + failedCount int + ) + baseDir := "/Users/slooop/Downloads" + 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") { + fileList = append(fileList, strings.ReplaceAll(pathItem, ".gz", "")) + } + return nil + }); err != nil { + fmt.Println(err) + } + for i := 0; i < len(fileList); i++ { + if strings.HasPrefix(path.Base(fileList[i]), "secure") { + dataItem := loadDatas2(fmt.Sprintf("cat %s | grep -a 'Failed password for' | grep -v 'invalid'", fileList[i]), 14, constant.StatusFailed) + failedCount += len(dataItem) + datas = append(datas, dataItem...) + } + if strings.HasPrefix(path.Base(fileList[i]), "auth.log") { + dataItem := loadDatas2(fmt.Sprintf("cat %s | grep -a 'Connection closed by authenticating user' | grep -a 'preauth'", fileList[i]), 15, constant.StatusFailed) + failedCount += len(dataItem) + datas = append(datas, dataItem...) + } + dataItem := loadDatas2(fmt.Sprintf("cat %s | grep Accepted", fileList[i]), 14, constant.StatusSuccess) + datas = append(datas, dataItem...) + } + successfulCount = len(datas) - failedCount + fmt.Println(len(datas), successfulCount, failedCount) +} + +func loadDatas2(command string, length int, status string) []history { + var datas []history + stdout2, err := cmd.Exec(command) + if err == nil { + lines := strings.Split(string(stdout2), "\n") + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) != length { + continue + } + historyItem := history{ + Belong: parts[3], + User: parts[8], + AuthMode: parts[6], + Address: parts[10], + Port: parts[12], + Status: status, + } + dateStr := fmt.Sprintf("%d %s %s %s", time.Now().Year(), parts[0], parts[1], parts[2]) + historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", dateStr) + // if err != nil { + // historyItem.Date, _ = time.Parse("2006 Jan 2 15:04:05", dateStr) + // } + fmt.Println(dateStr + "===>" + historyItem.Date.Format("2006.01.02 15:04:05")) + datas = append(datas, historyItem) + } + } + return datas +} + +func TestCas(t *testing.T) { + ss := "2023 May 9 14:48:28" + kk, err := time.Parse("2006 Jan 2 15:04:05", ss) + fmt.Println(kk, err) +} + +type history struct { + Date time.Time + Belong string + User string + AuthMode string + Address string + Port string + Status string + Message string +} diff --git a/backend/router/ro_host.go b/backend/router/ro_host.go index 51a4ea74d..ed576acb3 100644 --- a/backend/router/ro_host.go +++ b/backend/router/ro_host.go @@ -39,6 +39,7 @@ func (s *HostRouter) InitHostRouter(Router *gin.RouterGroup) { hostRouter.POST("/ssh/update", baseApi.UpdateSSH) hostRouter.POST("/ssh/generate", baseApi.GenerateSSH) hostRouter.POST("/ssh/secret", baseApi.LoadSSHSecret) + hostRouter.POST("/ssh/log", baseApi.LoadSSHLogs) hostRouter.GET("/command", baseApi.ListCommand) hostRouter.POST("/command", baseApi.CreateCommand) diff --git a/frontend/src/api/interface/host.ts b/frontend/src/api/interface/host.ts index 72031e5e9..38499cc73 100644 --- a/frontend/src/api/interface/host.ts +++ b/frontend/src/api/interface/host.ts @@ -118,4 +118,23 @@ export namespace Host { encryptionMode: string; password: string; } + export interface searchSSHLog extends ReqPage { + info: string; + status: string; + } + export interface sshLog { + logs: Array; + successfulCount: number; + failedCount: number; + } + export interface sshHistory { + date: Date; + belong: string; + user: string; + authMode: string; + address: string; + port: string; + status: string; + message: string; + } } diff --git a/frontend/src/api/modules/host.ts b/frontend/src/api/modules/host.ts index 10195a3cc..a043768bf 100644 --- a/frontend/src/api/modules/host.ts +++ b/frontend/src/api/modules/host.ts @@ -110,3 +110,6 @@ export const generateSecret = (params: Host.SSHGenerate) => { export const loadSecret = (mode: string) => { return http.post(`/hosts/ssh/secret`, { encryptionMode: mode }); }; +export const loadSSHLogs = (params: Host.searchSSHLog) => { + return http.post(`/hosts/ssh/log`, params); +}; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index f38a77c04..30e68fa6f 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -845,6 +845,11 @@ const message = { keyAuthHelper: '是否启用密钥认证,默认启用。', useDNS: '反向解析', dnsHelper: '控制 SSH 服务器是否启用 DNS 解析功能,从而验证连接方的身份。', + loginLogs: 'SSH 登录日志', + loginUser: '用户', + loginMode: '登录方式', + authenticating: '密钥', + password: '密码', }, setting: { all: '全部', diff --git a/frontend/src/routers/modules/host.ts b/frontend/src/routers/modules/host.ts index eda8edcec..649fcc969 100644 --- a/frontend/src/routers/modules/host.ts +++ b/frontend/src/routers/modules/host.ts @@ -51,15 +51,26 @@ const hostRouter = { }, }, { - path: '/hosts/ssh', + path: '/hosts/ssh/ssh', name: 'SSH', - component: () => import('@/views/host/ssh/index.vue'), + component: () => import('@/views/host/ssh/ssh/index.vue'), meta: { title: 'menu.ssh', + activeMenu: '/hosts/ssh/ssh', keepAlive: true, requiresAuth: false, }, }, + { + path: '/hosts/ssh/log', + name: 'SSHLog', + component: () => import('@/views/host/ssh/log/index.vue'), + hidden: true, + meta: { + activeMenu: '/hosts/ssh/ssh', + requiresAuth: false, + }, + }, { path: '/hosts/firewall/port', name: 'FirewallPort', diff --git a/frontend/src/views/host/ssh/index.vue b/frontend/src/views/host/ssh/index.vue index 902ab4bba..99f7a638c 100644 --- a/frontend/src/views/host/ssh/index.vue +++ b/frontend/src/views/host/ssh/index.vue @@ -1,230 +1,25 @@ diff --git a/frontend/src/views/host/ssh/log/index.vue b/frontend/src/views/host/ssh/log/index.vue new file mode 100644 index 000000000..92e49f89d --- /dev/null +++ b/frontend/src/views/host/ssh/log/index.vue @@ -0,0 +1,117 @@ + + + diff --git a/frontend/src/views/host/ssh/ssh/index.vue b/frontend/src/views/host/ssh/ssh/index.vue new file mode 100644 index 000000000..409c95729 --- /dev/null +++ b/frontend/src/views/host/ssh/ssh/index.vue @@ -0,0 +1,225 @@ + + + diff --git a/frontend/src/views/host/ssh/pubkey/index.vue b/frontend/src/views/host/ssh/ssh/pubkey/index.vue similarity index 100% rename from frontend/src/views/host/ssh/pubkey/index.vue rename to frontend/src/views/host/ssh/ssh/pubkey/index.vue