Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions agent/app/api/v2/setting.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package v2

import (
"encoding/json"
"os"
"os/user"
"path"

"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)

// @Tags System Setting
Expand Down Expand Up @@ -63,3 +70,75 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
func (b *BaseApi) LoadBaseDir(c *gin.Context) {
helper.SuccessWithData(c, global.Dir.DataDir)
}

func (b *BaseApi) CheckLocalConn(c *gin.Context) {
_, err := loadLocalConn()
helper.SuccessWithData(c, err == nil)
}

// @Tags System Setting
// @Summary Check local conn info
// @Success 200 {bool} isOk
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/ssh/check/info [post]
func (b *BaseApi) CheckLocalConnByInfo(c *gin.Context) {
var req dto.SSHConnData
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
helper.SuccessWithData(c, settingService.TestConnByInfo(req))
}

// @Tags System Setting
// @Summary Save local conn info
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/ssh [post]
func (b *BaseApi) SaveLocalConnInfo(c *gin.Context) {
var req dto.SSHConnData
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
helper.SuccessWithData(c, settingService.SaveConnInfo(req))
}

func loadLocalConn() (*ssh.SSHClient, error) {
itemPath := ""
currentInfo, _ := user.Current()
if len(currentInfo.HomeDir) == 0 {
itemPath = "/root/.ssh/id_ed25519_1panel"
} else {
itemPath = path.Join(currentInfo.HomeDir, ".ssh/id_ed25519_1panel")
}
if _, err := os.Stat(itemPath); err != nil {
_ = sshService.GenerateSSH(dto.GenerateSSH{EncryptionMode: "ed25519", Name: "_1panel"})
}

privateKey, _ := os.ReadFile(itemPath)
connWithKey := ssh.ConnInfo{
Addr: "127.0.0.1",
User: "root",
Port: 22,
AuthMode: "key",
PrivateKey: privateKey,
}
client, err := ssh.NewClient(connWithKey)
if err == nil {
return client, nil
}

connInfoInDB, err := settingService.GetSSHInfo()
if err != nil {
return nil, err
}
if len(connInfoInDB) == 0 {
return nil, errors.New("no such ssh conn info in db!")
}
var connInDB ssh.ConnInfo
if err := json.Unmarshal([]byte(connInfoInDB), &connInDB); err != nil {
return nil, err
}
return ssh.NewClient(connInDB)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code changes seem mostly correct, but there are a few areas where improvements can be made:

  1. Error Handling: In loadLocalConn, ensure that all errors are checked immediately after each operation to avoid cascading errors.

  2. Database Error Handling: Make sure database operations return appropriate errors and handle them appropriately.

  3. User Home Dir Check: Ensure that the directory exists before attempting to read from it, especially if it contains sensitive information like SSH keys.

  4. Code DRYness: Consider extracting common logic into utility functions to reduce redundancy and improve readability.

Here's an improved version of the function with some minor corrections:

func loadLocalConn() (*ssh.SSHClient, error) {
	currentInfo, err := user.Current()
	if err != nil {
		return(nil, fmt.Errorf("failed to get current user: %w", err))
	}

	itemPath := "/"
	if len(currentInfo.HomeDir) > 0 {
		itemPath = path.Join(currentInfo.HomeDir, ".ssh/")
	}
	itemPath += "id_ed25519_1panel"

	if _, err := os.Stat(itemPath); os.IsNotExist(err) {
		return generateNewSSHKey(itemPath)
	}

	file, err := os.OpenFile(itemPath, os.O_RDONLY|os.O_CREATE|os.O_EXCL, 0600)
	if err != nil {
		return(nil, fmt.Errorf("failed to open SSH key file: %w", err))
	}
	defer file.Close()

	privateKeyBytes, err := ioutil.ReadAll(file)
	if err != nil {
		return(nil, fmt.Errorf("failed to read SSH key: %w", err))
	}

	connWithKey := ssh.ConnInfo{
		Addr:       "127.0.0.1",
		User:       "root",
		Port:       22,
		AuthMode:   "key",
		PrivateKey: privateKeyBytes,
	}
	client, err := ssh.NewClient(connWithKey)
	if err == nil {
		return client, nil
	}

	connFromDB, err := settingService.GetSSHInfo()
	if err != nil {
		return(nil, fmt.Errorf("failed to get SSH connection info from DB: %w", err))
	}

	var connConfig ssh.ConnInfo
	err = json.Unmarshal([]byte(connFromDB), &connConfig)
	if err != nil {
		return(nil, fmt.Errorf("failed to unmarshal SSH connection info: %w", err))
	}

	return ssh.NewClient(connConfig)
}

func generateNewSSHKey(path string) (*ssh.SSHClient, error) {
	req := dto.GenerateSSH{
		EncryptionMode: "ed25519",
		Name:           "_1panel",
	}

	newPrivateKey, err := sshService.GenerateSSH(req)
	if err != nil {
		return(nil, fmt.Errorf("failed to generate new SSH key: %w", err))
	}

	err = ioutil.WriteFile(path, newPrivateKey.Bytes(), 0600)
	if err != nil {
		return(nil, fmt.Errorf("failed to write new SSH key: %w", err))
	}

	connWithKey := ssh.ConnInfo{
		Addr:       "127.0.0.1",
		User:       "root",
		Port:       22,
		AuthMode:   "key",
		PrivateKey: newPrivateKey.Bytes(),
	}
	client, err := ssh.NewClient(connWithKey)
	if err != nil {
		return	nil, fmt.Errorf("failed to create SSH client: %w", err)
	}

	return client, nil
}

Key Changes Made:

  • Improved error handling throughout the function.
  • Checked for non-existent files before assuming a directory structure.
  • Used context-aware operations for better resource management (defer).
  • Extracted error messages for clarity.
  • Ensured that paths were constructed correctly regardless of whether a home directory was present.

28 changes: 8 additions & 20 deletions agent/app/api/v2/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,21 @@ func (b *BaseApi) WsSSH(c *gin.Context) {
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
name, err := loadExecutor()
if wshandleError(wsConn, err) {
return
}
slave, err := terminal.NewCommand(name)
if wshandleError(wsConn, err) {

client, err := loadLocalConn()
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
defer slave.Close()

tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
defer client.Close()
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, "")
if wshandleError(wsConn, err) {
return
}
defer sws.Close()

quitChan := make(chan bool, 3)
tty.Start(quitChan)
go slave.Wait(quitChan)
sws.Start(quitChan)
go sws.Wait(quitChan)

<-quitChan

Expand Down Expand Up @@ -254,12 +251,3 @@ var upGrader = websocket.Upgrader{
return true
},
}

func loadExecutor() (string, error) {
std, err := cmd.RunDefaultWithStdoutBashC("echo $SHELL")
if err != nil {
return "", fmt.Errorf("load default executor failed, err: %s", std)
}

return strings.ReplaceAll(std, "\n", ""), nil
}
10 changes: 10 additions & 0 deletions agent/app/dto/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,13 @@ type Clean struct {
Name string `json:"name"`
Size uint64 `json:"size"`
}

type SSHConnData struct {
Addr string `json:"addr" validate:"required"`
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
User string `json:"user" validate:"required"`
AuthMode string `json:"authMode" validate:"oneof=password key"`
Password string `json:"password"`
PrivateKey string `json:"privateKey"`
PassPhrase string `json:"passPhrase"`
}
1 change: 1 addition & 0 deletions agent/app/dto/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type SSHInfo struct {
type GenerateSSH struct {
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
Password string `json:"password"`
Name string `json:"name"`
}

type GenerateLoad struct {
Expand Down
80 changes: 80 additions & 0 deletions agent/app/service/setting.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
package service

import (
"encoding/base64"
"encoding/json"
"time"

"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/jinzhu/copier"
)

type SettingService struct{}

type ISettingService interface {
GetSettingInfo() (*dto.SettingInfo, error)
Update(key, value string) error

GetSSHInfo() (string, error)
TestConnByInfo(req dto.SSHConnData) bool
SaveConnInfo(req dto.SSHConnData) error
}

func NewISettingService() ISettingService {
Expand Down Expand Up @@ -44,3 +52,75 @@ func (u *SettingService) GetSettingInfo() (*dto.SettingInfo, error) {
func (u *SettingService) Update(key, value string) error {
return settingRepo.UpdateOrCreate(key, value)
}

func (u *SettingService) GetSSHInfo() (string, error) {
conn, err := settingRepo.GetValueByKey("LocalSSHConn")
if err != nil || len(conn) == 0 {
return "", err
}
return encrypt.StringDecrypt(conn)
}

func (u *SettingService) TestConnByInfo(req dto.SSHConnData) bool {
if req.AuthMode == "password" && len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
return false
}
req.Password = string(password)
}
if req.AuthMode == "key" && len(req.PrivateKey) != 0 {
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
if err != nil {
return false
}
req.PrivateKey = string(privateKey)
}

var connInfo ssh.ConnInfo
_ = copier.Copy(&connInfo, &req)
connInfo.PrivateKey = []byte(req.PrivateKey)
if len(req.PassPhrase) != 0 {
connInfo.PassPhrase = []byte(req.PassPhrase)
}
client, err := ssh.NewClient(connInfo)
if err != nil {
return false
}
defer client.Close()
return true
}

func (u *SettingService) SaveConnInfo(req dto.SSHConnData) error {
if req.AuthMode == "password" && len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
return err
}
req.Password = string(password)
}
if req.AuthMode == "key" && len(req.PrivateKey) != 0 {
privateKey, err := base64.StdEncoding.DecodeString(req.PrivateKey)
if err != nil {
return err
}
req.PrivateKey = string(privateKey)
}

var connInfo ssh.ConnInfo
_ = copier.Copy(&connInfo, &req)
connInfo.PrivateKey = []byte(req.PrivateKey)
if len(req.PassPhrase) != 0 {
connInfo.PassPhrase = []byte(req.PassPhrase)
}
client, err := ssh.NewClient(connInfo)
if err != nil {
return err
}
defer client.Close()

localConn, _ := json.Marshal(&connInfo)
connAfterEncrypt, _ := encrypt.StringEncrypt(string(localConn))
_ = settingRepo.Update("LocalSSHConn", connAfterEncrypt)
return nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code provided contains several minor improvements and corrections to handle SSH connection encryption, decryption, serialization, and deserialization in addition to setting management:

Minor Changes and Corrections:

  1. Base64 Encoding and Decoding: Added calls to base64.StdEncoding.DecodeString for password and private key fields when updating them after retrieving from the repository.
  2. Error Handling for Base64 Decoding: Included checks for errors when decoding password and private key base64 strings before using them.
  3. Connection Testing Logic: The TestConnByInfo function now correctly handles authentication methods ("password" and "key") by decrypting passwords before attempting SSH connections.

Additional Features:

  • Encrypted Password Management: Ensures passwords are stored encrypted, preventing exposure of sensitive information.
  • SSH Client Configuration: The implementation includes creation and testing of an SSH client based on user-provided connection details.

Overall, these changes improve the security and functionality of the SSH settings management service within your application.

4 changes: 2 additions & 2 deletions agent/app/service/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,10 @@ func (u *SSHService) GenerateSSH(req dto.GenerateSSH) error {
}

fileOp := files.NewFileOp()
if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s", currentUser.HomeDir, req.EncryptionMode)); err != nil {
if err := fileOp.Rename(secretFile, fmt.Sprintf("%s/.ssh/id_%s%s", currentUser.HomeDir, req.EncryptionMode, req.Name)); err != nil {
return err
}
if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s.pub", currentUser.HomeDir, req.EncryptionMode)); err != nil {
if err := fileOp.Rename(secretPubFile, fmt.Sprintf("%s/.ssh/id_%s%s.pub", currentUser.HomeDir, req.EncryptionMode, req.Name)); err != nil {
return err
}

Expand Down
8 changes: 4 additions & 4 deletions agent/app/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,13 @@ func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, e
if taskID == "" {
taskID = uuid.New().String()
}
logDir := path.Join(global.Dir.LogDir, taskScope)
logDir := path.Join(global.Dir.TaskDir, taskScope)
if _, err := os.Stat(logDir); os.IsNotExist(err) {
if err = os.MkdirAll(logDir, constant.DirPerm); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
}
logPath := path.Join(global.Dir.LogDir, taskScope, taskID+".log")
logPath := path.Join(global.Dir.TaskDir, taskScope, taskID+".log")
file, err := os.OpenFile(logPath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, constant.FilePerm)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
Expand Down Expand Up @@ -267,7 +267,7 @@ func (t *Task) LogFailed(msg string) {
}

func (t *Task) LogFailedWithErr(msg string, err error) {
t.Logger.Println(fmt.Sprintf("%s %s : %s", msg, i18n.GetMsgByKey("Failed"), err.Error()))
t.Logger.Printf("%s %s : %s\n", msg, i18n.GetMsgByKey("Failed"), err.Error())
}

func (t *Task) LogSuccess(msg string) {
Expand All @@ -278,7 +278,7 @@ func (t *Task) LogSuccessF(format string, v ...any) {
}

func (t *Task) LogStart(msg string) {
t.Logger.Println(fmt.Sprintf("%s%s", i18n.GetMsgByKey("Start"), msg))
t.Logger.Printf("%s%s\n", i18n.GetMsgByKey("Start"), msg)
}

func (t *Task) LogWithOps(operate, msg string) {
Expand Down
1 change: 1 addition & 0 deletions agent/global/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type SystemDir struct {
BaseDir string
DbDir string
LogDir string
TaskDir string
DataDir string
TmpDir string
LocalBackupDir string
Expand Down
3 changes: 2 additions & 1 deletion agent/init/dir/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ func Init() {
global.Dir.BaseDir, _ = fileOp.CreateDirWithPath(true, baseDir)
global.Dir.DataDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel"))
global.Dir.DbDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/db"))
global.Dir.LogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/task"))
global.Dir.LogDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log"))
global.Dir.TaskDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/log/task"))
global.Dir.TmpDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/tmp"))

global.Dir.AppDir, _ = fileOp.CreateDirWithPath(true, path.Join(baseDir, "1panel/apps"))
Expand Down
1 change: 1 addition & 0 deletions agent/init/migration/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func InitAgentDB() {
migrations.UpdateSettingStatus,
migrations.InitDefault,
migrations.UpdateWebsiteExpireDate,
migrations.AddLocalSSHSetting,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
Expand Down
10 changes: 10 additions & 0 deletions agent/init/migration/migrations/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,13 @@ var UpdateWebsiteExpireDate = &gormigrate.Migration{
return nil
},
}

var AddLocalSSHSetting = &gormigrate.Migration{
ID: "20250417-add-local-ssh-setting",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "LocalSSHConn", Value: ""}).Error; err != nil {
return err
}
return nil
},
}
2 changes: 1 addition & 1 deletion agent/router/ro_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.GET("/tool/supervisor/process", baseApi.GetProcess)
hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile)

hostRouter.GET("/exec", baseApi.WsSSH)
hostRouter.GET("/terminal", baseApi.WsSSH)
}
}
4 changes: 4 additions & 0 deletions agent/router/ro_setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@ func (s *SettingRouter) InitRouter(Router *gin.RouterGroup) {
settingRouter.POST("/snapshot/description/update", baseApi.UpdateSnapDescription)

settingRouter.GET("/basedir", baseApi.LoadBaseDir)

settingRouter.POST("/ssh/check", baseApi.CheckLocalConn)
settingRouter.POST("/ssh", baseApi.SaveLocalConnInfo)
settingRouter.POST("/ssh/check/info", baseApi.CheckLocalConnByInfo)
}
}
Loading
Loading