1Panel/backend/app/service/ssh.go

456 lines
13 KiB
Go

package service
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/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/qqwry"
)
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(key, value string) error
GenerateSSH(req dto.GenerateSSH) error
LoadSSHSecret(mode string) (string, error)
LoadLog(req dto.SearchSSHLog) (*dto.SSHLog, error)
}
func NewISSHService() ISSHService {
return &SSHService{}
}
func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
data := dto.SSHInfo{
Status: constant.StatusEnable,
Message: "",
Port: "22",
ListenAddress: "0.0.0.0",
PasswordAuthentication: "yes",
PubkeyAuthentication: "yes",
PermitRootLogin: "yes",
UseDNS: "yes",
}
sudo := cmd.SudoHandleCmd()
stdout, err := cmd.Execf("%s systemctl status sshd", sudo)
if err != nil {
data.Message = stdout
data.Status = constant.StatusDisable
}
stdLines := strings.Split(stdout, "\n")
for _, stdline := range stdLines {
if strings.Contains(stdline, "active (running)") {
data.Status = constant.StatusEnable
break
}
}
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 ") {
data.ListenAddress = strings.ReplaceAll(line, "ListenAddress ", "")
}
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(line, "PermitRootLogin ", "")
}
if strings.HasPrefix(line, "UseDNS ") {
data.UseDNS = strings.ReplaceAll(line, "UseDNS ", "")
}
}
return &data, nil
}
func (u *SSHService) OperateSSH(operation string) error {
if operation == "start" || operation == "stop" || operation == "restart" {
sudo := cmd.SudoHandleCmd()
stdout, err := cmd.Execf("%s systemctl %s sshd", sudo, operation)
if err != nil {
return fmt.Errorf("%s sshd failed, stdout: %s, err: %v", operation, stdout, err)
}
return nil
}
return fmt.Errorf("not support such operation: %s", operation)
}
func (u *SSHService) Update(key, value string) error {
sshConf, err := os.ReadFile(sshPath)
if err != nil {
return err
}
lines := strings.Split(string(sshConf), "\n")
newFiles := updateSSHConf(lines, key, value)
if err := settingRepo.Update(key, value); 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(strings.Join(newFiles, "\n")); err != nil {
return err
}
sudo := cmd.SudoHandleCmd()
if 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, value)
}
}
_, _ = cmd.Execf("%s systemctl restart sshd", sudo)
return nil
}
func (u *SSHService) UpdateByFile(value string) error {
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
}
sudo := cmd.SudoHandleCmd()
_, _ = cmd.Execf("%s systemctl restart sshd", sudo)
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)
authFile := 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(authFile); err != nil {
_, _ = os.Create(authFile)
}
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
}
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
}
fileList = sortFileList(fileList)
command := ""
if len(req.Info) != 0 {
command = fmt.Sprintf(" | grep '%s'", req.Info)
}
for i := 0; i < len(fileList); i++ {
withAppend := len(data.Logs) < req.Page*req.PageSize
if req.Status != constant.StatusSuccess {
if strings.HasPrefix(path.Base(fileList[i]), "secure") {
commandItem := fmt.Sprintf("cat %s | grep -a 'Failed password for' | grep -v 'invalid' %s", fileList[i], command)
dataItem, itemTotal := loadFailedSecureDatas(commandItem, withAppend)
data.FailedCount += itemTotal
data.TotalCount += itemTotal
data.Logs = append(data.Logs, dataItem...)
}
if strings.HasPrefix(path.Base(fileList[i]), "auth.log") {
commandItem := fmt.Sprintf("cat %s | grep -a 'Connection closed by authenticating user' | grep -a 'preauth' %s", fileList[i], command)
dataItem, itemTotal := loadFailedAuthDatas(commandItem, withAppend)
data.FailedCount += itemTotal
data.TotalCount += itemTotal
data.Logs = append(data.Logs, dataItem...)
}
}
if req.Status != constant.StatusFailed {
commandItem := fmt.Sprintf("cat %s | grep -a Accepted %s", fileList[i], command)
dataItem, itemTotal := loadSuccessDatas(commandItem, withAppend)
data.TotalCount += itemTotal
data.Logs = append(data.Logs, dataItem...)
}
}
data.SuccessfulCount = data.TotalCount - data.FailedCount
if len(data.Logs) < 1 {
return nil, nil
}
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
timeNow := time.Now()
nyc, _ := time.LoadLocation(common.LoadTimeZone())
qqWry, err := qqwry.NewQQwry()
if err != nil {
global.LOG.Errorf("load qqwry datas failed: %s", err)
}
var itemLogs []dto.SSHHistory
for i := 0; i < len(data.Logs); i++ {
data.Logs[i].Area = qqWry.Find(data.Logs[i].Address).Area
data.Logs[i].Date, _ = time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", timeNow.Year(), data.Logs[i].DateStr), nyc)
itemLogs = append(itemLogs, data.Logs[i])
}
data.Logs = itemLogs
return &data, nil
}
func sortFileList(fileNames []string) []string {
if len(fileNames) < 2 {
return fileNames
}
if strings.HasPrefix(path.Base(fileNames[0]), "secure") {
var itemFile []string
sort.Slice(fileNames, func(i, j int) bool {
return fileNames[i] > fileNames[j]
})
itemFile = append(itemFile, fileNames[len(fileNames)-1])
itemFile = append(itemFile, fileNames[:len(fileNames)-2]...)
return itemFile
}
sort.Slice(fileNames, func(i, j int) bool {
return fileNames[i] < fileNames[j]
})
return fileNames
}
func updateSSHConf(oldFiles []string, param string, value interface{}) []string {
hasKey := false
var newFiles []string
for _, line := range oldFiles {
if strings.HasPrefix(line, param+" ") {
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value))
hasKey = true
continue
}
newFiles = append(newFiles, line)
}
if !hasKey {
newFiles = []string{}
for _, line := range oldFiles {
if strings.HasPrefix(line, fmt.Sprintf("#%s ", param)) && !hasKey {
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value))
hasKey = true
continue
}
newFiles = append(newFiles, line)
}
}
if !hasKey {
newFiles = []string{}
newFiles = append(newFiles, oldFiles...)
newFiles = append(newFiles, fmt.Sprintf("%s %v", param, value))
}
return newFiles
}
func loadSuccessDatas(command string, withAppend bool) ([]dto.SSHHistory, int) {
var (
datas []dto.SSHHistory
totalNum int
)
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
if len(lines) == 0 {
return datas, 0
}
for i := len(lines) - 1; i >= 0; i-- {
parts := strings.Fields(lines[i])
if len(parts) < 14 {
continue
}
totalNum++
if withAppend {
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[6],
User: parts[8],
Address: parts[10],
Port: parts[12],
Status: constant.StatusSuccess,
}
datas = append(datas, historyItem)
}
}
}
return datas, totalNum
}
func loadFailedAuthDatas(command string, withAppend bool) ([]dto.SSHHistory, int) {
var (
datas []dto.SSHHistory
totalNum int
)
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
if len(lines) == 0 {
return datas, 0
}
for i := len(lines) - 1; i >= 0; i-- {
parts := strings.Fields(lines[i])
if len(parts) < 14 {
continue
}
totalNum++
if withAppend {
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[8],
User: parts[10],
Address: parts[11],
Port: parts[13],
Status: constant.StatusFailed,
}
if strings.Contains(lines[i], ": ") {
historyItem.Message = strings.Split(lines[i], ": ")[1]
}
datas = append(datas, historyItem)
}
}
}
return datas, totalNum
}
func loadFailedSecureDatas(command string, withAppend bool) ([]dto.SSHHistory, int) {
var (
datas []dto.SSHHistory
totalNum int
)
stdout2, err := cmd.Exec(command)
if err == nil {
lines := strings.Split(string(stdout2), "\n")
if len(lines) == 0 {
return datas, 0
}
for i := len(lines) - 1; i >= 0; i-- {
parts := strings.Fields(lines[i])
if len(parts) < 14 {
continue
}
totalNum++
if withAppend {
historyItem := dto.SSHHistory{
DateStr: fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]),
AuthMode: parts[6],
User: parts[8],
Address: parts[10],
Port: parts[12],
Status: constant.StatusFailed,
}
if strings.Contains(lines[i], ": ") {
historyItem.Message = strings.Split(lines[i], ": ")[1]
}
datas = append(datas, historyItem)
}
}
}
return datas, totalNum
}
func handleGunzip(path string) error {
if _, err := cmd.Execf("gunzip %s", path); err != nil {
return err
}
return nil
}