Files
1Panel/agent/app/service/ssh.go
ssongliu 7915230121 refactor: rebuild firewall management (#13628)
* 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
2026-08-24 12:51:34 +08:00

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
}