mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 16:13:59 +08:00
* refactor(firewall): rebuild rule management foundation * refactor(firewall): streamline rule checks and inventory * feat(firewall): improve native rule inventory * refactor(firewall): refine rule management * feat: add Docker port guard * feat(firewall): support native nftables * feat(firewall): add configurable firewall selection * feat(firewall): support nftables docker port guard * refactor(firewall): complete v2 rule management and migration * refactor(firewall): align state and API contracts * refactor(firewall): unify rule management operations * feat: refine firewall v2 rules and forwarding * fix(firewall): harden dual-stack rule management * refactor(firewall): consolidate rule validation and persistence
1730 lines
46 KiB
Go
1730 lines
46 KiB
Go
package service
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/utils/controller"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
|
csvexport "github.com/1Panel-dev/1Panel/agent/utils/csv_export"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/geo"
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const sshPath = "/etc/ssh/sshd_config"
|
|
const defaultSSHPort = "22"
|
|
const sshManagedMarker = "# config by 1panel"
|
|
|
|
type SSHService struct{}
|
|
|
|
type ISSHService interface {
|
|
GetSSHInfo() (*dto.SSHInfo, error)
|
|
OperateSSH(operation string) error
|
|
Update(req dto.SSHUpdate) error
|
|
LoadSSHFile(name string) (string, error)
|
|
UpdateByFile(req dto.SSHConfUpdate) error
|
|
|
|
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error)
|
|
ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error)
|
|
CleanLog() error
|
|
|
|
SyncRootCert() error
|
|
CreateRootCert(req dto.RootCertOperate) error
|
|
EditRootCert(req dto.RootCertOperate) error
|
|
SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error)
|
|
DeleteRootCerts(req dto.ForceDelete) error
|
|
}
|
|
|
|
func NewISSHService() ISSHService {
|
|
return &SSHService{}
|
|
}
|
|
|
|
type sshDirective struct {
|
|
Key string
|
|
Value string
|
|
File string
|
|
Line int
|
|
InMatch bool
|
|
}
|
|
|
|
type sshConfigFile struct {
|
|
Path string `json:"path"`
|
|
Priority int `json:"priority"`
|
|
}
|
|
|
|
type sshManagedBlock struct {
|
|
Start int
|
|
End int
|
|
}
|
|
|
|
func (b sshManagedBlock) contains(line int) bool {
|
|
return line >= b.Start && line < b.End
|
|
}
|
|
|
|
func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
|
|
data := dto.SSHInfo{
|
|
AutoStart: true,
|
|
IsExist: true,
|
|
IsActive: true,
|
|
Message: "",
|
|
Port: "22",
|
|
ListenAddress: "",
|
|
PasswordAuthentication: "yes",
|
|
PubkeyAuthentication: "yes",
|
|
PermitRootLogin: "yes",
|
|
UseDNS: "yes",
|
|
}
|
|
loadSSHServiceStatus(&data)
|
|
loadSSHConfigInfo(&data)
|
|
data.CurrentUser = loadCurrentUserName()
|
|
return &data, nil
|
|
}
|
|
|
|
func (u *SSHService) OperateSSH(operation string) error {
|
|
serviceName, err := loadServiceName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch operation {
|
|
case "start", "restart":
|
|
if err := stopSSHSocketIfActive(serviceName); err != nil {
|
|
return err
|
|
}
|
|
case "stop":
|
|
if err := stopSSHSocketIfActive(serviceName); err != nil {
|
|
return err
|
|
}
|
|
case "enable", "disable":
|
|
if operation == "disable" {
|
|
if err := disableSSHSocket(serviceName, false); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := disableSSHSocket(serviceName, true); err != nil {
|
|
return err
|
|
}
|
|
if err := controller.Handle("enable", serviceName+".service"); err != nil {
|
|
return fmt.Errorf("enable %s.service failed, err: %v", serviceName, err)
|
|
}
|
|
return nil
|
|
}
|
|
serviceName += ".service"
|
|
}
|
|
|
|
if err := controller.Handle(operation, serviceName); err != nil {
|
|
return fmt.Errorf("%s %s failed, err: %v", operation, serviceName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func restartSSHService(serviceName string) error {
|
|
if err := stopSSHSocketIfActive(serviceName); err != nil {
|
|
return err
|
|
}
|
|
if err := controller.HandleRestart(serviceName); err != nil {
|
|
return fmt.Errorf("restart %s failed, err: %v", serviceName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func stopSSHSocketIfActive(serviceName string) error {
|
|
for _, socketName := range loadSSHSocketNames(serviceName) {
|
|
active, _ := controller.CheckActive(socketName)
|
|
if !active {
|
|
continue
|
|
}
|
|
if err := controller.HandleStop(socketName); err != nil {
|
|
return fmt.Errorf("stop %s failed, err: %v", socketName, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func disableSSHSocket(serviceName string, stopActive bool) error {
|
|
if stopActive {
|
|
if err := stopSSHSocketIfActive(serviceName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, socketName := range loadSSHSocketNames(serviceName) {
|
|
if err := controller.Handle("disable", socketName); err != nil {
|
|
return fmt.Errorf("disable %s failed, err: %v", socketName, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadSSHSocketNames(serviceName string) []string {
|
|
baseName := strings.TrimSuffix(serviceName, ".service")
|
|
candidates := []string{baseName + ".socket"}
|
|
switch baseName {
|
|
case "ssh":
|
|
candidates = append(candidates, "sshd.socket")
|
|
case "sshd":
|
|
candidates = append(candidates, "ssh.socket")
|
|
}
|
|
|
|
seen := map[string]struct{}{}
|
|
var sockets []string
|
|
for _, candidate := range candidates {
|
|
if _, ok := seen[candidate]; ok {
|
|
continue
|
|
}
|
|
seen[candidate] = struct{}{}
|
|
exist, _ := controller.CheckExist(candidate)
|
|
if exist {
|
|
sockets = append(sockets, candidate)
|
|
}
|
|
}
|
|
return sockets
|
|
}
|
|
|
|
func (u *SSHService) Update(req dto.SSHUpdate) error {
|
|
serviceName, err := loadServiceName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
directives, _, err := parseSSHConfigTree(sshPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldPortValue := strings.Join(loadSSHPortValues(directives), ",")
|
|
if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil {
|
|
return err
|
|
}
|
|
if req.Key == "Port" {
|
|
handleSSHPortUpdate(oldPortValue, req.NewValue)
|
|
}
|
|
|
|
return restartSSHService(serviceName)
|
|
}
|
|
|
|
func loadSSHServiceStatus(data *dto.SSHInfo) {
|
|
serviceName, err := loadServiceName()
|
|
if err != nil {
|
|
data.IsExist = false
|
|
data.Message = err.Error()
|
|
return
|
|
}
|
|
|
|
active, err := controller.CheckActive(serviceName)
|
|
data.IsActive = active || hasActiveSSHSocket(serviceName)
|
|
if !active && err != nil {
|
|
data.Message = err.Error()
|
|
}
|
|
|
|
enable, err := controller.CheckEnable(serviceName)
|
|
if err != nil {
|
|
data.AutoStart = hasEnabledSSHSocket(serviceName)
|
|
return
|
|
}
|
|
data.AutoStart = enable || hasEnabledSSHSocket(serviceName)
|
|
}
|
|
|
|
func hasActiveSSHSocket(serviceName string) bool {
|
|
for _, socketName := range loadSSHSocketNames(serviceName) {
|
|
active, _ := controller.CheckActive(socketName)
|
|
if active {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasEnabledSSHSocket(serviceName string) bool {
|
|
for _, socketName := range loadSSHSocketNames(serviceName) {
|
|
enable, _ := controller.CheckEnable(socketName)
|
|
if enable {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func loadSSHConfigInfo(data *dto.SSHInfo) {
|
|
directives, _, err := parseSSHConfigTree(sshPath)
|
|
if err != nil {
|
|
data.Message = err.Error()
|
|
data.IsActive = false
|
|
return
|
|
}
|
|
|
|
if port := strings.Join(loadSSHPortValues(directives), ","); port != "" {
|
|
data.Port = port
|
|
}
|
|
data.ListenAddress = strings.Join(loadSSHDirectiveValues(directives, "ListenAddress"), ",")
|
|
if value, ok := loadFirstSSHDirectiveValue(directives, "PasswordAuthentication"); ok {
|
|
data.PasswordAuthentication = value
|
|
}
|
|
if value, ok := loadFirstSSHDirectiveValue(directives, "PubkeyAuthentication"); ok {
|
|
data.PubkeyAuthentication = value
|
|
}
|
|
if value, ok := loadFirstSSHDirectiveValue(directives, "PermitRootLogin"); ok {
|
|
data.PermitRootLogin = strings.ReplaceAll(value, "prohibit-password", "without-password")
|
|
}
|
|
if value, ok := loadFirstSSHDirectiveValue(directives, "UseDNS"); ok {
|
|
data.UseDNS = value
|
|
}
|
|
}
|
|
|
|
func loadCurrentUserName() string {
|
|
currentUser, err := user.Current()
|
|
if err != nil || currentUser.Name == "" {
|
|
return "root"
|
|
}
|
|
return currentUser.Name
|
|
}
|
|
|
|
func handleSSHPortUpdate(oldValue, newValue string) {
|
|
sudo := cmd.SudoHandleCmd()
|
|
newPorts := splitSSHPorts(newValue)
|
|
oldPorts := splitSSHPorts(oldValue)
|
|
|
|
stdout, _ := runWithOptionalSudo(sudo, "getenforce")
|
|
if stdout == "Enforcing\n" {
|
|
for _, port := range diffSSHPorts(oldPorts, newPorts) {
|
|
if _, err := runWithOptionalSudo(sudo, "semanage", "port", "-d", "-t", "ssh_port_t", "-p", "tcp", port); err != nil {
|
|
global.LOG.Warnf("remove selinux ssh port %s failed, err: %v", port, err)
|
|
}
|
|
}
|
|
for _, port := range newPorts {
|
|
_, _ = runWithOptionalSudo(sudo, "semanage", "port", "-a", "-t", "ssh_port_t", "-p", "tcp", port)
|
|
}
|
|
}
|
|
|
|
removedPorts, err := parseSSHPortsToInts(diffSSHPorts(oldPorts, newPorts))
|
|
if err != nil {
|
|
global.LOG.Errorf("parse removed ssh ports failed, err: %v", err)
|
|
} else {
|
|
addedPorts, err := parseSSHPortsToInts(diffSSHPorts(newPorts, oldPorts))
|
|
if err != nil {
|
|
global.LOG.Errorf("parse added ssh ports failed, err: %v", err)
|
|
} else if err := OperateFirewallPort(removedPorts, addedPorts); err != nil {
|
|
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", oldValue, newValue, err)
|
|
}
|
|
}
|
|
|
|
primaryPort, err := loadPrimarySSHPort(newValue)
|
|
if err != nil {
|
|
global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err)
|
|
return
|
|
}
|
|
if err := updateLocalConn(uint(primaryPort)); err != nil {
|
|
global.LOG.Errorf("update local conn for terminal failed, err: %v", err)
|
|
}
|
|
if err := updateSSHSocketFile(strconv.Itoa(primaryPort)); err != nil {
|
|
global.LOG.Errorf("update port for ssh.socket failed, err: %v", err)
|
|
}
|
|
}
|
|
|
|
func splitSSHPorts(value string) []string {
|
|
var ports []string
|
|
for _, item := range strings.Split(value, ",") {
|
|
port := strings.TrimSpace(item)
|
|
if port != "" {
|
|
ports = append(ports, port)
|
|
}
|
|
}
|
|
return ports
|
|
}
|
|
|
|
func diffSSHPorts(left, right []string) []string {
|
|
rightSet := make(map[string]struct{}, len(right))
|
|
for _, item := range right {
|
|
rightSet[item] = struct{}{}
|
|
}
|
|
var diff []string
|
|
for _, item := range left {
|
|
if _, ok := rightSet[item]; ok {
|
|
continue
|
|
}
|
|
diff = append(diff, item)
|
|
}
|
|
return diff
|
|
}
|
|
|
|
func loadPrimarySSHPort(value string) (int, error) {
|
|
ports := splitSSHPorts(value)
|
|
if len(ports) == 0 {
|
|
return 0, fmt.Errorf("ssh port is empty")
|
|
}
|
|
return strconv.Atoi(ports[0])
|
|
}
|
|
|
|
func parseSSHPortsToInts(ports []string) ([]int, error) {
|
|
var values []int
|
|
for _, port := range ports {
|
|
value, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values = append(values, value)
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func runWithOptionalSudo(sudo, name string, args ...string) (string, error) {
|
|
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
|
|
if sudo != "" {
|
|
commandArgs := append([]string{name}, args...)
|
|
return cmdMgr.RunWithStdout("sudo", commandArgs...)
|
|
}
|
|
return cmdMgr.RunWithStdout(name, args...)
|
|
}
|
|
|
|
func (u *SSHService) SyncRootCert() error {
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("load current user failed, err: %v", err)
|
|
}
|
|
sshDir := fmt.Sprintf("%s/.ssh", currentUser.HomeDir)
|
|
|
|
fileList, err := os.ReadDir(sshDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var rootCerts []model.RootCert
|
|
fileMap := make(map[string]bool)
|
|
for _, item := range fileList {
|
|
if !item.IsDir() {
|
|
fileMap[item.Name()] = true
|
|
}
|
|
}
|
|
for item := range fileMap {
|
|
if !strings.HasSuffix(item, ".pub") {
|
|
continue
|
|
}
|
|
if !fileMap[strings.TrimSuffix(item, ".pub")] {
|
|
continue
|
|
}
|
|
cert := model.RootCert{Name: strings.TrimSuffix(item, ".pub"), PublicKeyPath: path.Join(sshDir, item), PrivateKeyPath: path.Join(sshDir, strings.TrimSuffix(item, ".pub"))}
|
|
pubItem, err := os.ReadFile(path.Join(sshDir, item))
|
|
if err != nil {
|
|
global.LOG.Errorf("read pubic key of %s for sync failed, err: %v", item, err)
|
|
continue
|
|
}
|
|
cert.EncryptionMode = loadEncryptioMode(string(pubItem))
|
|
rootCerts = append(rootCerts, cert)
|
|
}
|
|
return hostRepo.SyncCert(rootCerts)
|
|
}
|
|
|
|
func (u *SSHService) CreateRootCert(req dto.RootCertOperate) error {
|
|
if cmd.CheckIllegal(req.EncryptionMode, req.PassPhrase) {
|
|
return buserr.New("ErrCmdIllegal")
|
|
}
|
|
certItem, _ := hostRepo.GetCert(repo.WithByName(req.Name))
|
|
if certItem.ID != 0 {
|
|
return buserr.New("ErrRecordExist")
|
|
}
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("load current user failed, err: %v", err)
|
|
}
|
|
var cert model.RootCert
|
|
if err := copier.Copy(&cert, req); err != nil {
|
|
return err
|
|
}
|
|
privatePath := fmt.Sprintf("%s/.ssh/%s", currentUser.HomeDir, req.Name)
|
|
publicPath := fmt.Sprintf("%s/.ssh/%s.pub", currentUser.HomeDir, req.Name)
|
|
authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys"
|
|
|
|
if info, err := os.Stat(authFilePath); err == nil && info.Size() > 0 {
|
|
f, err := os.Open(authFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
if _, err := f.Seek(-1, 2); err == nil {
|
|
buf := make([]byte, 1)
|
|
if _, err := f.Read(buf); err == nil && buf[0] != '\n' {
|
|
appendFile, err := os.OpenFile(authFilePath, os.O_APPEND|os.O_WRONLY, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := appendFile.Write([]byte("\n")); err != nil {
|
|
_ = appendFile.Close()
|
|
return err
|
|
}
|
|
_ = appendFile.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
if req.Mode == "input" || req.Mode == "import" {
|
|
if err := os.WriteFile(privatePath, []byte(req.PrivateKey), constant.FilePerm); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(publicPath, []byte(req.PublicKey), constant.FilePerm); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
tmpPrivatePath := privatePath + ".tmp"
|
|
tmpPublicPath := privatePath + ".tmp.pub"
|
|
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
|
|
args := []string{
|
|
"-t", req.EncryptionMode,
|
|
"-f", tmpPrivatePath,
|
|
"-N", req.PassPhrase,
|
|
"-q",
|
|
}
|
|
|
|
if err := cmdMgr.Run("ssh-keygen", args...); err != nil {
|
|
_ = os.Remove(tmpPrivatePath)
|
|
_ = os.Remove(tmpPublicPath)
|
|
return fmt.Errorf("generate failed, %v", err)
|
|
}
|
|
if err := os.Rename(tmpPrivatePath, privatePath); err != nil {
|
|
return fmt.Errorf("replace private key failed, %v", err)
|
|
}
|
|
if err := os.Rename(tmpPublicPath, publicPath); err != nil {
|
|
return fmt.Errorf("replace public key failed, %v", err)
|
|
}
|
|
}
|
|
|
|
publicKeyBytes, err := os.ReadFile(publicPath)
|
|
if err != nil {
|
|
return fmt.Errorf("read public key failed, %v", err)
|
|
}
|
|
|
|
cleanKey := strings.TrimRight(string(publicKeyBytes), "\n") + "\n"
|
|
authFile, err := os.OpenFile(authFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("open authorized_keys failed, %v", err)
|
|
}
|
|
defer func() { _ = authFile.Close() }()
|
|
|
|
if _, err := authFile.Write([]byte(cleanKey)); err != nil {
|
|
return fmt.Errorf("append authorized_keys failed, %v", err)
|
|
}
|
|
|
|
cert.PrivateKeyPath = privatePath
|
|
cert.PublicKeyPath = publicPath
|
|
if len(cert.PassPhrase) != 0 {
|
|
cert.PassPhrase, _ = encrypt.StringEncrypt(cert.PassPhrase)
|
|
}
|
|
return hostRepo.SaveCert(&cert)
|
|
}
|
|
|
|
func (u *SSHService) EditRootCert(req dto.RootCertOperate) error {
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("load current user failed, err: %v", err)
|
|
}
|
|
certItem, _ := hostRepo.GetCert(repo.WithByID(req.ID))
|
|
if certItem.ID == 0 {
|
|
return buserr.New("ErrRecordNotFound")
|
|
}
|
|
oldPublicItem, err := os.ReadFile(certItem.PublicKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var cert model.RootCert
|
|
if err := copier.Copy(&cert, req); err != nil {
|
|
return err
|
|
}
|
|
cert.PrivateKeyPath = fmt.Sprintf("%s/.ssh/%s", currentUser.HomeDir, req.Name)
|
|
cert.PublicKeyPath = fmt.Sprintf("%s/.ssh/%s.pub", currentUser.HomeDir, req.Name)
|
|
if err := os.WriteFile(cert.PrivateKeyPath, []byte(req.PrivateKey), constant.FilePerm); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(cert.PublicKeyPath, []byte(req.PublicKey), constant.FilePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys"
|
|
authItem, err := os.ReadFile(authFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
oldPublic := strings.ReplaceAll(string(oldPublicItem), "\n", "")
|
|
newPublic := strings.ReplaceAll(string(req.PublicKey), "\n", "")
|
|
lines := strings.Split(string(authItem), "\n")
|
|
var newFiles []string
|
|
for i := 0; i < len(lines); i++ {
|
|
if len(lines[i]) != 0 && lines[i] != oldPublic && lines[i] != newPublic {
|
|
newFiles = append(newFiles, lines[i])
|
|
}
|
|
}
|
|
newFiles = append(newFiles, newPublic)
|
|
if err := os.WriteFile(authFilePath, []byte(strings.Join(newFiles, "\n")), constant.FilePerm); err != nil {
|
|
return fmt.Errorf("refresh authorized_keys failed, err: %v", err)
|
|
}
|
|
if len(cert.PassPhrase) != 0 {
|
|
cert.PassPhrase, _ = encrypt.StringEncrypt(cert.PassPhrase)
|
|
}
|
|
return hostRepo.SaveCert(&cert)
|
|
}
|
|
|
|
func (u *SSHService) SearchRootCerts(req dto.SearchWithPage) (int64, interface{}, error) {
|
|
total, records, err := hostRepo.PageCert(req.Page, req.PageSize)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
var datas []dto.RootCert
|
|
for i := 0; i < len(records); i++ {
|
|
publicItem, err := os.ReadFile(records[i].PublicKeyPath)
|
|
var publicBase64 string
|
|
if err == nil && len(publicItem) != 0 {
|
|
publicBase64 = base64.StdEncoding.EncodeToString(publicItem)
|
|
}
|
|
privateItem, _ := os.ReadFile(records[i].PrivateKeyPath)
|
|
var privateBase64 string
|
|
if err == nil && len(publicItem) != 0 {
|
|
privateBase64 = base64.StdEncoding.EncodeToString(privateItem)
|
|
}
|
|
passPhrase, _ := encrypt.StringDecryptWithBase64(records[i].PassPhrase)
|
|
datas = append(datas, dto.RootCert{
|
|
ID: records[i].ID,
|
|
CreatedAt: records[i].CreatedAt,
|
|
Name: records[i].Name,
|
|
EncryptionMode: records[i].EncryptionMode,
|
|
PassPhrase: passPhrase,
|
|
PublicKey: publicBase64,
|
|
PrivateKey: privateBase64,
|
|
Description: records[i].Description,
|
|
})
|
|
}
|
|
return total, datas, err
|
|
}
|
|
|
|
func (u *SSHService) DeleteRootCerts(req dto.ForceDelete) error {
|
|
currentUser, err := user.Current()
|
|
if err != nil && !req.ForceDelete {
|
|
return fmt.Errorf("load current user failed, err: %v", err)
|
|
}
|
|
authFilePath := currentUser.HomeDir + "/.ssh/authorized_keys"
|
|
authItem, err := os.ReadFile(authFilePath)
|
|
if err != nil && !req.ForceDelete {
|
|
return err
|
|
}
|
|
for _, id := range req.IDs {
|
|
cert, _ := hostRepo.GetCert(repo.WithByID(id))
|
|
if cert.ID == 0 {
|
|
if !req.ForceDelete {
|
|
return buserr.New("ErrRecordNotFound")
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
publicItem, err := os.ReadFile(cert.PublicKeyPath)
|
|
if err != nil && !req.ForceDelete {
|
|
return err
|
|
}
|
|
newFile := bytes.ReplaceAll(authItem, publicItem, nil)
|
|
if err := os.WriteFile(authFilePath, newFile, constant.FilePerm); err != nil && !req.ForceDelete {
|
|
return fmt.Errorf("refresh authorized_keys failed, err: %v", err)
|
|
}
|
|
_ = os.Remove(cert.PublicKeyPath)
|
|
_ = os.Remove(cert.PrivateKeyPath)
|
|
if err := hostRepo.DeleteCert(repo.WithByID(id)); err != nil && !req.ForceDelete {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type sshFileItem struct {
|
|
Name string
|
|
Year int
|
|
}
|
|
|
|
func isSSHLogFileName(name string) bool {
|
|
for _, baseName := range []string{"auth.log", "secure"} {
|
|
if name == baseName || strings.HasPrefix(name, baseName+".") || strings.HasPrefix(name, baseName+"-") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (u *SSHService) LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error) {
|
|
var fileList []sshFileItem
|
|
var data []dto.SSHHistory
|
|
baseDir := "/var/log"
|
|
fileItems, err := os.ReadDir(baseDir)
|
|
if err != nil {
|
|
return 0, data, err
|
|
}
|
|
for _, item := range fileItems {
|
|
if item.IsDir() || !isSSHLogFileName(item.Name()) {
|
|
continue
|
|
}
|
|
info, err := item.Info()
|
|
if err != nil {
|
|
return 0, data, err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
continue
|
|
}
|
|
itemPath := path.Join(baseDir, info.Name())
|
|
if strings.HasSuffix(itemPath, ".gz") {
|
|
if _, err := os.Stat(strings.TrimSuffix(itemPath, ".gz")); err == nil {
|
|
continue
|
|
}
|
|
}
|
|
fileList = append(fileList, sshFileItem{Name: itemPath, Year: info.ModTime().Year()})
|
|
}
|
|
fileList = sortFileList(fileList)
|
|
|
|
filter := ""
|
|
if len(req.Info) != 0 {
|
|
if cmd.CheckIllegal(req.Info) {
|
|
return 0, data, buserr.New("ErrCmdIllegal")
|
|
}
|
|
filter = req.Info
|
|
}
|
|
|
|
showCountFrom := (req.Page - 1) * req.PageSize
|
|
showCountTo := req.Page * req.PageSize
|
|
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
|
|
itemFailed, itemTotal := 0, 0
|
|
for _, file := range fileList {
|
|
dataItem, successCount, failedCount := loadSSHData(
|
|
ctx,
|
|
file.Name,
|
|
req.Status,
|
|
filter,
|
|
req.StartTime,
|
|
req.EndTime,
|
|
showCountFrom,
|
|
showCountTo,
|
|
file.Year,
|
|
nyc,
|
|
)
|
|
itemFailed += failedCount
|
|
itemTotal += successCount + failedCount
|
|
showCountFrom = showCountFrom - (successCount + failedCount)
|
|
if showCountTo != -1 {
|
|
showCountTo = showCountTo - (successCount + failedCount)
|
|
}
|
|
data = append(data, dataItem...)
|
|
}
|
|
|
|
total := itemTotal
|
|
if req.Status == constant.StatusFailed {
|
|
total = itemFailed
|
|
}
|
|
if req.Status == constant.StatusSuccess {
|
|
total = itemTotal - itemFailed
|
|
}
|
|
return int64(total), data, nil
|
|
}
|
|
|
|
func (u *SSHService) CleanLog() error {
|
|
return cleanSSHLogFiles("/var/log")
|
|
}
|
|
|
|
func cleanSSHLogFiles(baseDir string) error {
|
|
fileItems, err := os.ReadDir(baseDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, item := range fileItems {
|
|
if item.IsDir() || !isSSHLogFileName(item.Name()) {
|
|
continue
|
|
}
|
|
info, err := item.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
continue
|
|
}
|
|
|
|
itemPath := path.Join(baseDir, item.Name())
|
|
if item.Name() == "auth.log" || item.Name() == "secure" {
|
|
if err := os.Truncate(itemPath, 0); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if err := os.Remove(itemPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (u *SSHService) ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error) {
|
|
_, logs, err := u.LoadLog(ctx, req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(logs) == 0 {
|
|
return "", buserr.New("ErrRecordNotFound")
|
|
}
|
|
tmpFileName := path.Join(global.Dir.TmpDir, "export/ssh-log", fmt.Sprintf("1panel-ssh-log-%s.csv", time.Now().Format(constant.DateTimeSlimLayout)))
|
|
if _, err := os.Stat(path.Dir(tmpFileName)); err != nil {
|
|
_ = os.MkdirAll(path.Dir(tmpFileName), constant.DirPerm)
|
|
}
|
|
if err := csvexport.ExportSSHLogs(tmpFileName, logs); err != nil {
|
|
return "", err
|
|
}
|
|
return tmpFileName, nil
|
|
}
|
|
|
|
func (u *SSHService) LoadSSHFile(name string) (string, error) {
|
|
var fileName string
|
|
switch name {
|
|
case "authKeys":
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return "", fmt.Errorf("load current user failed, err: %v", err)
|
|
}
|
|
fileName = currentUser.HomeDir + "/.ssh/authorized_keys"
|
|
case "sshdConf":
|
|
fileName = "/etc/ssh/sshd_config"
|
|
case "sshdConfOptions":
|
|
_, fileList, err := parseSSHConfigTree(sshPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
content, err := json.Marshal(fileList)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(content), nil
|
|
default:
|
|
if strings.HasPrefix(name, "sshdConfPath:") {
|
|
fileName = strings.TrimPrefix(name, "sshdConfPath:")
|
|
if !isSSHConfigPathAllowed(fileName) {
|
|
return "", buserr.WithName("ErrNotSupportType", name)
|
|
}
|
|
} else {
|
|
return "", buserr.WithName("ErrNotSupportType", name)
|
|
}
|
|
}
|
|
if _, err := os.Stat(fileName); err != nil {
|
|
return "", buserr.WithErr("ErrHttpReqNotFound", err)
|
|
}
|
|
content, err := os.ReadFile(fileName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(content), nil
|
|
}
|
|
|
|
func (u *SSHService) UpdateByFile(req dto.SSHConfUpdate) error {
|
|
var fileName string
|
|
switch req.Key {
|
|
case "authKeys":
|
|
currentUser, err := user.Current()
|
|
if err != nil {
|
|
return fmt.Errorf("load current user failed, err: %v", err)
|
|
}
|
|
fileName = currentUser.HomeDir + "/.ssh/authorized_keys"
|
|
case "sshdConf":
|
|
fileName = "/etc/ssh/sshd_config"
|
|
case "sshdConfPath":
|
|
fileName = req.Path
|
|
if !isSSHConfigPathAllowed(fileName) {
|
|
return buserr.WithName("ErrNotSupportType", req.Key)
|
|
}
|
|
default:
|
|
return buserr.WithName("ErrNotSupportType", req.Key)
|
|
}
|
|
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
if _, err = file.WriteString(req.Value); err != nil {
|
|
return err
|
|
}
|
|
if req.Key == "authKeys" {
|
|
return nil
|
|
}
|
|
serviceName, err := loadServiceName()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return restartSSHService(serviceName)
|
|
}
|
|
|
|
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 parseSSHConfigTree(root string) ([]sshDirective, []sshConfigFile, error) {
|
|
rootPath, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var (
|
|
directives []sshDirective
|
|
fileList []sshConfigFile
|
|
order int
|
|
priority int
|
|
)
|
|
visited := make(map[string]bool)
|
|
if err := parseSSHConfigFile(rootPath, false, visited, &directives, &fileList, &order, &priority); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return directives, fileList, nil
|
|
}
|
|
|
|
func parseSSHConfigFile(
|
|
fileName string,
|
|
inMatch bool,
|
|
visited map[string]bool,
|
|
directives *[]sshDirective,
|
|
fileList *[]sshConfigFile,
|
|
order *int,
|
|
priority *int,
|
|
) error {
|
|
absolutePath, err := filepath.Abs(fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if visited[absolutePath] {
|
|
return nil
|
|
}
|
|
visited[absolutePath] = true
|
|
|
|
content, err := os.ReadFile(absolutePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
lines := strings.Split(string(content), "\n")
|
|
currentMatch := inMatch
|
|
fileRegistered := false
|
|
for index, rawLine := range lines {
|
|
line := strings.TrimSpace(rawLine)
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key, value, ok := parseSSHConfigLine(line)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if strings.EqualFold(key, "Match") {
|
|
currentMatch = true
|
|
continue
|
|
}
|
|
if strings.EqualFold(key, "Include") {
|
|
for _, includePath := range expandSSHIncludePaths(absolutePath, value) {
|
|
if err := parseSSHConfigFile(includePath, currentMatch, visited, directives, fileList, order, priority); err != nil {
|
|
global.LOG.Warnf("parse ssh include %s failed, err: %v", includePath, err)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if !fileRegistered {
|
|
registerSSHConfigFile(absolutePath, fileList, priority)
|
|
fileRegistered = true
|
|
}
|
|
*order++
|
|
*directives = append(*directives, sshDirective{
|
|
Key: key,
|
|
Value: normalizeSSHDirectiveValue(key, value),
|
|
File: absolutePath,
|
|
Line: index,
|
|
InMatch: currentMatch,
|
|
})
|
|
}
|
|
if !fileRegistered {
|
|
registerSSHConfigFile(absolutePath, fileList, priority)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func registerSSHConfigFile(fileName string, fileList *[]sshConfigFile, priority *int) {
|
|
for _, item := range *fileList {
|
|
if item.Path == fileName {
|
|
return
|
|
}
|
|
}
|
|
*priority++
|
|
*fileList = append(*fileList, sshConfigFile{Path: fileName, Priority: *priority})
|
|
}
|
|
|
|
func parseSSHConfigLine(line string) (string, string, bool) {
|
|
line = trimSSHInlineComment(line)
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 2 {
|
|
return "", "", false
|
|
}
|
|
return fields[0], strings.Join(fields[1:], " "), true
|
|
}
|
|
|
|
func trimSSHInlineComment(line string) string {
|
|
inQuote := false
|
|
for i := 0; i < len(line); i++ {
|
|
switch line[i] {
|
|
case '"':
|
|
inQuote = !inQuote
|
|
case '#':
|
|
if !inQuote && (i == 0 || line[i-1] == ' ' || line[i-1] == '\t') {
|
|
return strings.TrimSpace(line[:i])
|
|
}
|
|
}
|
|
}
|
|
return strings.TrimSpace(line)
|
|
}
|
|
|
|
func expandSSHIncludePaths(baseFile, value string) []string {
|
|
var includeFiles []string
|
|
for _, item := range strings.Fields(strings.ReplaceAll(value, "\"", "")) {
|
|
pattern := item
|
|
if !filepath.IsAbs(pattern) {
|
|
pattern = filepath.Join(filepath.Dir(baseFile), pattern)
|
|
}
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sort.Strings(matches)
|
|
includeFiles = append(includeFiles, matches...)
|
|
}
|
|
return includeFiles
|
|
}
|
|
|
|
func normalizeSSHDirectiveValue(key, value string) string {
|
|
if strings.EqualFold(key, "PermitRootLogin") && value == "prohibit-password" {
|
|
return "without-password"
|
|
}
|
|
return value
|
|
}
|
|
|
|
func loadFirstSSHDirectiveValue(directives []sshDirective, key string) (string, bool) {
|
|
for _, item := range directives {
|
|
if item.InMatch || !strings.EqualFold(item.Key, key) {
|
|
continue
|
|
}
|
|
return item.Value, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func loadSSHDirectiveValues(directives []sshDirective, key string) []string {
|
|
var values []string
|
|
for _, item := range directives {
|
|
if item.InMatch || !strings.EqualFold(item.Key, key) {
|
|
continue
|
|
}
|
|
values = append(values, item.Value)
|
|
}
|
|
return values
|
|
}
|
|
|
|
func loadSSHPortValues(directives []sshDirective) []string {
|
|
values := loadSSHDirectiveValues(directives, "Port")
|
|
if len(values) == 0 {
|
|
return []string{defaultSSHPort}
|
|
}
|
|
return values
|
|
}
|
|
|
|
func updateSSHDirectiveValue(key, value string, directives []sshDirective) error {
|
|
if key == "Port" || key == "ListenAddress" {
|
|
return rewriteSSHMultiValueDirective(key, value, directives)
|
|
}
|
|
return updateSSHSingleDirectiveValue(key, value, directives)
|
|
}
|
|
|
|
func updateSSHSingleDirectiveValue(key, value string, directives []sshDirective) error {
|
|
block, err := loadSSHManagedBlockFromFile(sshPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, item := range directives {
|
|
if item.InMatch || !strings.EqualFold(item.Key, key) {
|
|
continue
|
|
}
|
|
if item.File == sshPath && block.contains(item.Line) {
|
|
continue
|
|
}
|
|
if err := commentSSHConfigLines(item.File, []int{item.Line}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return rewriteSSHManagedDirectives(sshPath, key, []string{fmt.Sprintf("%s %s", key, value)})
|
|
}
|
|
|
|
func rewriteSSHMultiValueDirective(key, value string, directives []sshDirective) error {
|
|
block, err := loadSSHManagedBlockFromFile(sshPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
targetFiles := make(map[string][]int)
|
|
for _, item := range directives {
|
|
if item.InMatch || !strings.EqualFold(item.Key, key) {
|
|
continue
|
|
}
|
|
if item.File == sshPath && block.contains(item.Line) {
|
|
continue
|
|
}
|
|
targetFiles[item.File] = append(targetFiles[item.File], item.Line)
|
|
}
|
|
for fileName, lines := range targetFiles {
|
|
if err := commentSSHConfigLines(fileName, lines); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return rewriteSSHManagedDirectives(sshPath, key, buildSSHDirectiveLines(key, value))
|
|
}
|
|
|
|
func buildSSHDirectiveLines(key, value string) []string {
|
|
var directives []string
|
|
for _, item := range strings.Split(value, ",") {
|
|
if item != "" {
|
|
directives = append(directives, fmt.Sprintf("%s %s", key, item))
|
|
}
|
|
}
|
|
return directives
|
|
}
|
|
|
|
func rewriteSSHManagedDirectives(fileName, key string, newDirectives []string) error {
|
|
content, err := os.ReadFile(fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lines := strings.Split(string(content), "\n")
|
|
lines, insertAt := ensureSSHManagedMarker(lines)
|
|
|
|
block, ok := loadSSHManagedBlock(lines)
|
|
if ok {
|
|
var filtered []string
|
|
for index := block.Start; index < block.End; index++ {
|
|
line := strings.TrimSpace(lines[index])
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
filtered = append(filtered, lines[index])
|
|
continue
|
|
}
|
|
itemKey, _, ok := parseSSHConfigLine(line)
|
|
if ok && strings.EqualFold(itemKey, key) {
|
|
continue
|
|
}
|
|
filtered = append(filtered, lines[index])
|
|
}
|
|
lines = append(append(lines[:block.Start], filtered...), lines[block.End:]...)
|
|
insertAt = block.Start
|
|
}
|
|
|
|
if len(newDirectives) != 0 {
|
|
lines = insertSSHDirectivesAt(lines, insertAt, newDirectives)
|
|
}
|
|
return os.WriteFile(fileName, []byte(strings.Join(lines, "\n")), constant.FilePerm)
|
|
}
|
|
|
|
func commentSSHConfigLines(fileName string, targetLines []int) error {
|
|
content, err := os.ReadFile(fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lines := strings.Split(string(content), "\n")
|
|
targetSet := make(map[int]struct{}, len(targetLines))
|
|
for _, line := range targetLines {
|
|
targetSet[line] = struct{}{}
|
|
}
|
|
for index := range lines {
|
|
if _, ok := targetSet[index]; !ok {
|
|
continue
|
|
}
|
|
trimmed := strings.TrimSpace(lines[index])
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
|
continue
|
|
}
|
|
lines[index] = "#" + lines[index]
|
|
}
|
|
return os.WriteFile(fileName, []byte(strings.Join(lines, "\n")), constant.FilePerm)
|
|
}
|
|
|
|
func loadSSHInsertIndex(lines []string) int {
|
|
for index, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(strings.ToLower(trimmed), "match ") {
|
|
return index
|
|
}
|
|
}
|
|
return len(lines)
|
|
}
|
|
|
|
func ensureSSHManagedMarker(lines []string) ([]string, int) {
|
|
for index, line := range lines {
|
|
if strings.TrimSpace(line) == sshManagedMarker {
|
|
return lines, index + 1
|
|
}
|
|
}
|
|
insertAt := loadSSHInsertIndex(lines)
|
|
lines = insertSSHDirectivesAt(lines, insertAt, []string{sshManagedMarker})
|
|
return lines, insertAt + 1
|
|
}
|
|
|
|
func loadSSHManagedBlock(lines []string) (sshManagedBlock, bool) {
|
|
for index, line := range lines {
|
|
if strings.TrimSpace(line) != sshManagedMarker {
|
|
continue
|
|
}
|
|
end := len(lines)
|
|
for next := index + 1; next < len(lines); next++ {
|
|
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(lines[next])), "match ") {
|
|
end = next
|
|
break
|
|
}
|
|
}
|
|
return sshManagedBlock{Start: index + 1, End: end}, true
|
|
}
|
|
return sshManagedBlock{}, false
|
|
}
|
|
|
|
func loadSSHManagedBlockFromFile(fileName string) (sshManagedBlock, error) {
|
|
content, err := os.ReadFile(fileName)
|
|
if err != nil {
|
|
return sshManagedBlock{}, err
|
|
}
|
|
block, ok := loadSSHManagedBlock(strings.Split(string(content), "\n"))
|
|
if !ok {
|
|
return sshManagedBlock{}, nil
|
|
}
|
|
return block, nil
|
|
}
|
|
|
|
func insertSSHDirectivesAt(lines []string, insertAt int, directives []string) []string {
|
|
newLines := make([]string, 0, len(lines)+len(directives))
|
|
newLines = append(newLines, lines[:insertAt]...)
|
|
newLines = append(newLines, directives...)
|
|
newLines = append(newLines, lines[insertAt:]...)
|
|
return newLines
|
|
}
|
|
|
|
func isSSHConfigPathAllowed(fileName string) bool {
|
|
if fileName == "" {
|
|
return false
|
|
}
|
|
absolutePath, err := filepath.Abs(fileName)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if absolutePath == sshPath {
|
|
return true
|
|
}
|
|
_, fileList, err := parseSSHConfigTree(sshPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, item := range fileList {
|
|
if item.Path == absolutePath {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type sshLogHeader struct {
|
|
DateStr string
|
|
Process string
|
|
PID string
|
|
Message string
|
|
}
|
|
|
|
type sshParsedLog struct {
|
|
History dto.SSHHistory
|
|
SessionKey string
|
|
Raw string
|
|
Search string
|
|
Index int
|
|
}
|
|
|
|
func loadSSHData(
|
|
ctx *gin.Context,
|
|
filePath, status, filter string,
|
|
startTime, endTime time.Time,
|
|
showCountFrom, showCountTo, currentYear int,
|
|
nyc *time.Location,
|
|
) ([]dto.SSHHistory, int, int) {
|
|
var (
|
|
datas []dto.SSHHistory
|
|
successCount int
|
|
failedCount int
|
|
)
|
|
getLoc, err := geo.NewGeo()
|
|
if err != nil {
|
|
return datas, 0, 0
|
|
}
|
|
lines, err := loadSSHLogLines(filePath)
|
|
if err != nil {
|
|
return datas, 0, 0
|
|
}
|
|
items := collectSSHLogItems(lines, filter, status)
|
|
for i := len(items) - 1; i >= 0; i-- {
|
|
itemData := items[i].History
|
|
if !matchSSHLogStatus(status, itemData.Status) || !checkIsStandard(itemData) {
|
|
continue
|
|
}
|
|
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
|
|
if !isSSHLogWithinTimeRange(itemData.Date, startTime, endTime) {
|
|
continue
|
|
}
|
|
if successCount+failedCount >= showCountFrom && (showCountTo == -1 || successCount+failedCount < showCountTo) {
|
|
itemData.Area, _ = geo.GetIPLocation(getLoc, itemData.Address, common.GetLang(ctx))
|
|
datas = append(datas, itemData)
|
|
}
|
|
if itemData.Status == constant.StatusSuccess {
|
|
successCount++
|
|
} else {
|
|
failedCount++
|
|
}
|
|
}
|
|
return datas, successCount, failedCount
|
|
}
|
|
|
|
func isSSHLogWithinTimeRange(itemTime, startTime, endTime time.Time) bool {
|
|
if startTime.IsZero() || endTime.IsZero() {
|
|
return true
|
|
}
|
|
return itemTime.After(startTime) && itemTime.Before(endTime)
|
|
}
|
|
|
|
func collectSSHLogItems(lines []string, filter, status string) []sshParsedLog {
|
|
var items []sshParsedLog
|
|
auxiliaryIndex := make(map[string]int)
|
|
sessionHasAuthEvent := make(map[string]bool)
|
|
authenticatedEndpoints := make(map[string]bool)
|
|
matchedTerminalSessions := make(map[string]bool)
|
|
for lineIndex, line := range lines {
|
|
if !shouldParseSSHLogLine(line, status, filter) {
|
|
continue
|
|
}
|
|
item, ok := parseSSHLogLine(line)
|
|
if !ok || !checkIsStandard(item.History) {
|
|
continue
|
|
}
|
|
item.Index = lineIndex
|
|
item.Search = line
|
|
endpointKey := loadSSHLogEndpointKey(item.History)
|
|
if isSSHAuthEvent(item) && item.SessionKey != "" {
|
|
sessionHasAuthEvent[item.SessionKey] = true
|
|
delete(matchedTerminalSessions, item.SessionKey)
|
|
if endpointKey != "" {
|
|
if item.History.Status == constant.StatusSuccess {
|
|
authenticatedEndpoints[endpointKey] = true
|
|
} else {
|
|
delete(authenticatedEndpoints, endpointKey)
|
|
}
|
|
}
|
|
items = append(items, item)
|
|
continue
|
|
}
|
|
if endpointKey != "" && isSSHTerminalEvent(item) && authenticatedEndpoints[endpointKey] {
|
|
// A successful authentication and its terminal log can be emitted by
|
|
// different sshd processes. Associate only this active connection by
|
|
// endpoint, then clear it so a reused client port starts a new session.
|
|
delete(authenticatedEndpoints, endpointKey)
|
|
matchedTerminalSessions[item.SessionKey] = true
|
|
continue
|
|
}
|
|
if isSSHTerminalEvent(item) && matchedTerminalSessions[item.SessionKey] {
|
|
continue
|
|
}
|
|
delete(matchedTerminalSessions, item.SessionKey)
|
|
if index, ok := auxiliaryIndex[item.SessionKey]; ok {
|
|
items[index].Search += "\n" + line
|
|
continue
|
|
}
|
|
auxiliaryIndex[item.SessionKey] = len(items)
|
|
items = append(items, item)
|
|
}
|
|
items = filterRedundantSSHSessionItems(items, sessionHasAuthEvent)
|
|
if filter != "" {
|
|
filteredItems := make([]sshParsedLog, 0, len(items))
|
|
for _, item := range items {
|
|
if strings.Contains(item.Search, filter) {
|
|
filteredItems = append(filteredItems, item)
|
|
}
|
|
}
|
|
items = filteredItems
|
|
}
|
|
sort.SliceStable(items, func(i, j int) bool {
|
|
return items[i].Index < items[j].Index
|
|
})
|
|
return items
|
|
}
|
|
|
|
func filterRedundantSSHSessionItems(items []sshParsedLog, sessionHasAuthEvent map[string]bool) []sshParsedLog {
|
|
filteredItems := make([]sshParsedLog, 0, len(items))
|
|
for _, item := range items {
|
|
if !isSSHAuthEvent(item) && item.SessionKey != "" && sessionHasAuthEvent[item.SessionKey] {
|
|
continue
|
|
}
|
|
filteredItems = append(filteredItems, item)
|
|
}
|
|
return filteredItems
|
|
}
|
|
|
|
func isSSHAuthEvent(item sshParsedLog) bool {
|
|
return item.History.Status == constant.StatusSuccess || item.History.AuthMode != ""
|
|
}
|
|
|
|
func isSSHTerminalEvent(item sshParsedLog) bool {
|
|
message := item.History.Message
|
|
return strings.HasPrefix(message, "Connection closed by ") ||
|
|
strings.HasPrefix(message, "Disconnected from ") ||
|
|
strings.HasPrefix(message, "Received disconnect from ")
|
|
}
|
|
|
|
func shouldParseSSHLogLine(line, status, filter string) bool {
|
|
if !strings.Contains(line, "sshd") {
|
|
return false
|
|
}
|
|
if filter != "" {
|
|
return containsKnownSSHLogMessage(line)
|
|
}
|
|
switch status {
|
|
case constant.StatusSuccess:
|
|
return strings.Contains(line, "Accepted ")
|
|
case constant.StatusFailed:
|
|
// Accepted lines are still needed here to suppress redundant session failure records from the same SSH session.
|
|
return containsFailedSSHLogMessage(line) || strings.Contains(line, "Accepted ")
|
|
default:
|
|
return containsKnownSSHLogMessage(line)
|
|
}
|
|
}
|
|
|
|
func containsKnownSSHLogMessage(line string) bool {
|
|
return strings.Contains(line, "Accepted ") || containsFailedSSHLogMessage(line)
|
|
}
|
|
|
|
func containsFailedSSHLogMessage(line string) bool {
|
|
return strings.Contains(line, "Failed ") ||
|
|
strings.Contains(line, "Invalid user ") ||
|
|
strings.Contains(line, "Connection closed by ") ||
|
|
strings.Contains(line, "Disconnected from ") ||
|
|
strings.Contains(line, "Received disconnect from ") ||
|
|
strings.Contains(line, "maximum authentication attempts exceeded") ||
|
|
strings.Contains(line, " not allowed")
|
|
}
|
|
|
|
func loadSSHLogLines(filePath string) ([]string, error) {
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
var reader io.Reader = file
|
|
if strings.HasSuffix(filePath, ".gz") {
|
|
gzipReader, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gzipReader.Close()
|
|
reader = gzipReader
|
|
}
|
|
|
|
var lines []string
|
|
scanner := bufio.NewScanner(reader)
|
|
buf := make([]byte, 0, 64*1024)
|
|
scanner.Buffer(buf, 1024*1024)
|
|
for scanner.Scan() {
|
|
lines = append(lines, scanner.Text())
|
|
}
|
|
return lines, scanner.Err()
|
|
}
|
|
|
|
func matchSSHLogStatus(requestStatus, itemStatus string) bool {
|
|
switch requestStatus {
|
|
case constant.StatusSuccess:
|
|
return itemStatus == constant.StatusSuccess
|
|
case constant.StatusFailed:
|
|
return itemStatus == constant.StatusFailed
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
func parseSSHLogLine(line string) (sshParsedLog, bool) {
|
|
header, ok := parseSSHLogHeader(line)
|
|
if !ok {
|
|
return sshParsedLog{}, false
|
|
}
|
|
data, ok := parseSSHLogMessage(header.Message)
|
|
if !ok {
|
|
return sshParsedLog{}, false
|
|
}
|
|
data.DateStr = header.DateStr
|
|
data.Message = header.Message
|
|
sessionKey := loadSSHLogSessionKey(header, data, line)
|
|
return sshParsedLog{History: data, SessionKey: sessionKey, Raw: line}, true
|
|
}
|
|
|
|
func parseSSHLogHeader(line string) (sshLogHeader, bool) {
|
|
for _, pattern := range []string{re.SSHRFC3339LinePattern, re.SSHDateTimeLinePattern, re.SSHSyslogLinePattern} {
|
|
matches := re.GetRegex(pattern).FindStringSubmatch(line)
|
|
if len(matches) != 5 {
|
|
continue
|
|
}
|
|
dateStr := normalizeSSHLogDate(matches[1])
|
|
if dateStr == "" {
|
|
return sshLogHeader{}, false
|
|
}
|
|
return sshLogHeader{DateStr: dateStr, Process: matches[2], PID: matches[3], Message: matches[4]}, true
|
|
}
|
|
return sshLogHeader{}, false
|
|
}
|
|
|
|
func loadSSHLogSessionKey(header sshLogHeader, data dto.SSHHistory, line string) string {
|
|
if header.Process == "sshd-session" {
|
|
if endpointKey := loadSSHLogEndpointKey(data); endpointKey != "" {
|
|
return endpointKey
|
|
}
|
|
}
|
|
if header.PID != "" {
|
|
return "pid:" + header.PID
|
|
}
|
|
if endpointKey := loadSSHLogEndpointKey(data); endpointKey != "" {
|
|
return endpointKey
|
|
}
|
|
return line
|
|
}
|
|
|
|
func loadSSHLogEndpointKey(data dto.SSHHistory) string {
|
|
if data.Address == "" || data.Port == "" {
|
|
return ""
|
|
}
|
|
return net.JoinHostPort(data.Address, data.Port)
|
|
}
|
|
|
|
func normalizeSSHLogDate(dateStr string) string {
|
|
if t, err := time.Parse(time.RFC3339Nano, dateStr); err == nil {
|
|
return t.Format("2006 Jan 2 15:04:05")
|
|
}
|
|
if t, err := time.Parse(constant.DateTimeLayout, dateStr); err == nil {
|
|
return t.Format("2006 Jan 2 15:04:05")
|
|
}
|
|
if _, err := time.Parse("Jan 2 15:04:05", dateStr); err == nil {
|
|
return dateStr
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func parseSSHLogMessage(message string) (dto.SSHHistory, bool) {
|
|
if matches := re.GetRegex(re.SSHAcceptedPattern).FindStringSubmatch(message); len(matches) == 5 {
|
|
return dto.SSHHistory{
|
|
AuthMode: matches[1],
|
|
User: matches[2],
|
|
Address: matches[3],
|
|
Port: matches[4],
|
|
Status: constant.StatusSuccess,
|
|
}, true
|
|
}
|
|
if matches := re.GetRegex(re.SSHFailedPattern).FindStringSubmatch(message); len(matches) == 6 {
|
|
return dto.SSHHistory{
|
|
AuthMode: matches[1],
|
|
User: matches[3],
|
|
Address: matches[4],
|
|
Port: matches[5],
|
|
Status: constant.StatusFailed,
|
|
}, true
|
|
}
|
|
if matches := re.GetRegex(re.SSHInvalidUserPattern).FindStringSubmatch(message); len(matches) == 4 {
|
|
return dto.SSHHistory{
|
|
User: matches[1],
|
|
Address: matches[2],
|
|
Port: matches[3],
|
|
Status: constant.StatusFailed,
|
|
}, true
|
|
}
|
|
if matches := re.GetRegex(re.SSHClosedPattern).FindStringSubmatch(message); len(matches) == 4 {
|
|
return dto.SSHHistory{
|
|
User: matches[1],
|
|
Address: matches[2],
|
|
Port: matches[3],
|
|
Status: constant.StatusFailed,
|
|
}, true
|
|
}
|
|
if matches := re.GetRegex(re.SSHDisconnectedPattern).FindStringSubmatch(message); len(matches) == 4 {
|
|
return dto.SSHHistory{
|
|
User: matches[1],
|
|
Address: matches[2],
|
|
Port: matches[3],
|
|
Status: constant.StatusFailed,
|
|
}, true
|
|
}
|
|
if matches := re.GetRegex(re.SSHDisconnectPattern).FindStringSubmatch(message); len(matches) == 3 {
|
|
return dto.SSHHistory{
|
|
Address: matches[1],
|
|
Port: matches[2],
|
|
Status: constant.StatusFailed,
|
|
}, true
|
|
}
|
|
if matches := re.GetRegex(re.SSHMaxAuthPattern).FindStringSubmatch(message); len(matches) == 4 {
|
|
return dto.SSHHistory{
|
|
User: matches[1],
|
|
Address: matches[2],
|
|
Port: matches[3],
|
|
Status: constant.StatusFailed,
|
|
}, true
|
|
}
|
|
if matches := re.GetRegex(re.SSHNotAllowedPattern).FindStringSubmatch(message); len(matches) == 3 {
|
|
return dto.SSHHistory{
|
|
User: matches[1],
|
|
Address: matches[2],
|
|
Status: constant.StatusFailed,
|
|
}, true
|
|
}
|
|
return dto.SSHHistory{}, false
|
|
}
|
|
|
|
func checkIsStandard(item dto.SSHHistory) bool {
|
|
if len(item.Address) == 0 || net.ParseIP(item.Address) == nil {
|
|
return false
|
|
}
|
|
if item.Port == "" {
|
|
return true
|
|
}
|
|
portItem, _ := strconv.Atoi(item.Port)
|
|
return portItem > 0 && portItem < 65536
|
|
}
|
|
|
|
func handleGunzip(path string) error {
|
|
cmdMgr := cmd.NewCommandMgr()
|
|
if err := cmdMgr.Run("gunzip", path); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadServiceName() (string, error) {
|
|
if exist, _ := controller.CheckExist("sshd"); exist {
|
|
return "sshd", nil
|
|
} else if exist, _ := controller.CheckExist("ssh"); exist {
|
|
return "ssh", nil
|
|
}
|
|
return "", errors.New("The ssh or sshd service is unavailable")
|
|
}
|
|
|
|
func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
|
|
itemDate, err := time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", currentYear, DateStr), nyc)
|
|
if err != nil {
|
|
itemDate, _ = time.ParseInLocation("2006 Jan 2 15:04:05", DateStr, nyc)
|
|
}
|
|
return itemDate
|
|
}
|
|
|
|
func loadEncryptioMode(content string) string {
|
|
if strings.HasPrefix(content, "ssh-rsa") {
|
|
return "rsa"
|
|
}
|
|
if strings.HasPrefix(content, "ssh-ed25519") {
|
|
return "ed25519"
|
|
}
|
|
if strings.HasPrefix(content, "ssh-ecdsa") {
|
|
return "ecdsa"
|
|
}
|
|
if strings.HasPrefix(content, "ssh-dsa") {
|
|
return "dsa"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func updateLocalConn(newPort uint) error {
|
|
conn, _ := settingRepo.GetValueByKey("LocalSSHConn")
|
|
if len(conn) == 0 {
|
|
return nil
|
|
}
|
|
connItem, err := encrypt.StringDecrypt(conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var data dto.SSHConnData
|
|
if err := json.Unmarshal([]byte(connItem), &data); err != nil {
|
|
return err
|
|
}
|
|
data.Port = newPort
|
|
connNew, err := json.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
connNewItem, err := encrypt.StringEncrypt(string(connNew))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return settingRepo.Update("LocalSSHConn", connNewItem)
|
|
}
|
|
|
|
func updateSSHSocketFile(newPort string) error {
|
|
active, _ := controller.CheckActive("ssh.socket")
|
|
if !active {
|
|
return nil
|
|
}
|
|
filepath := "/usr/lib/systemd/system/ssh.socket"
|
|
file, err := os.ReadFile(filepath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lines := strings.Split(string(file), "\n")
|
|
for i := 0; i < len(lines); i++ {
|
|
if strings.HasPrefix(lines[i], "ListenStream=") {
|
|
parts := strings.Split(lines[i], ":")
|
|
if len(parts) > 1 {
|
|
lines[i] = strings.ReplaceAll(lines[i], parts[len(parts)-1], newPort)
|
|
continue
|
|
}
|
|
parts = strings.Split(lines[i], "=")
|
|
if len(parts) > 1 {
|
|
lines[i] = strings.ReplaceAll(lines[i], parts[len(parts)-1], newPort)
|
|
}
|
|
}
|
|
}
|
|
fileItem, err := os.OpenFile(filepath, os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fileItem.Close()
|
|
if _, err = fileItem.WriteString(strings.Join(lines, "\n")); err != nil {
|
|
return err
|
|
}
|
|
_ = controller.Reload()
|
|
_ = controller.HandleRestart("ssh.socket")
|
|
return nil
|
|
}
|