mirror of https://github.com/1Panel-dev/1Panel
appstorecrontabdatabasedockerdocker-composedocker-containerdocker-imagedocker-uifilemanagerlamplnmppanel
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
451 lines
13 KiB
451 lines
13 KiB
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/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 { |
|
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 |
|
}
|
|
|