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
103 changes: 81 additions & 22 deletions agent/app/api/v2/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,55 @@ import (
"github.com/pkg/errors"
)

func (b *BaseApi) WsSSH(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()

if global.CONF.Base.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}

cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
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) {
return
}
defer slave.Close()

tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return
}

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

<-quitChan

global.LOG.Info("websocket finished")
if wshandleError(wsConn, err) {
return
}
}

func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
Expand All @@ -42,7 +91,7 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
}
source := c.Query("source")
var containerID string
var initCmd string
var initCmd []string
switch source {
case "redis":
containerID, initCmd, err = loadRedisInitCmd(c)
Expand All @@ -59,11 +108,11 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
return
}
pidMap := loadMapFromDockerTop(containerID)
slave, err := terminal.NewCommand("clear && " + initCmd)
slave, err := terminal.NewCommand("docker", initCmd...)
if wshandleError(wsConn, err) {
return
}
defer killBash(containerID, strings.ReplaceAll(initCmd, fmt.Sprintf("docker exec -it %s ", containerID), ""), pidMap)
defer killBash(containerID, strings.ReplaceAll(strings.Join(initCmd, " "), fmt.Sprintf("exec -it %s ", containerID), ""), pidMap)
defer slave.Close()

tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
Expand All @@ -83,62 +132,63 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
}
}

func loadRedisInitCmd(c *gin.Context) (string, string, error) {
func loadRedisInitCmd(c *gin.Context) (string, []string, error) {
name := c.Query("name")
from := c.Query("from")
commands := "redis-cli"
commands := []string{"exec", "-it"}
database, err := databaseService.Get(name)
if err != nil {
return "", "", fmt.Errorf("no such database in db, err: %v", err)
return "", nil, fmt.Errorf("no such database in db, err: %v", err)
}
if from == "local" {
redisInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: name, Type: "redis"})
if err != nil {
return "", "", fmt.Errorf("no such app in db, err: %v", err)
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
}
name = redisInfo.ContainerName
commands = append(commands, []string{name, "redis-cli"}...)
if len(database.Password) != 0 {
commands = "redis-cli -a " + database.Password + " --no-auth-warning"
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
}
} else {
commands = fmt.Sprintf("redis-cli -h %s -p %v", database.Address, database.Port)
name = "1Panel-redis-cli-tools"
commands = append(commands, []string{name, "redis-cli", "-h", database.Address, "-p", fmt.Sprintf("%v", database.Port)}...)
if len(database.Password) != 0 {
commands = fmt.Sprintf("redis-cli -h %s -p %v -a %s --no-auth-warning", database.Address, database.Port, database.Password)
commands = append(commands, []string{"-a", database.Password, "--no-auth-warning"}...)
}
name = "1Panel-redis-cli-tools"
}
return name, fmt.Sprintf("docker exec -it %s %s", name, commands), nil
return name, commands, nil
}

func loadOllamaInitCmd(c *gin.Context) (string, string, error) {
func loadOllamaInitCmd(c *gin.Context) (string, []string, error) {
name := c.Query("name")
if cmd.CheckIllegal(name) {
return "", "", fmt.Errorf("ollama model %s contains illegal characters", name)
return "", nil, fmt.Errorf("ollama model %s contains illegal characters", name)
}
ollamaInfo, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Name: "", Type: "ollama"})
if err != nil {
return "", "", fmt.Errorf("no such app in db, err: %v", err)
return "", nil, fmt.Errorf("no such app in db, err: %v", err)
}
containerName := ollamaInfo.ContainerName
return containerName, fmt.Sprintf("docker exec -it %s ollama run %s", containerName, name), nil
return containerName, []string{"exec", "-it", containerName, "ollama", "run", name}, nil
}

func loadContainerInitCmd(c *gin.Context) (string, string, error) {
func loadContainerInitCmd(c *gin.Context) (string, []string, error) {
containerID := c.Query("containerid")
command := c.Query("command")
user := c.Query("user")
if cmd.CheckIllegal(user, containerID, command) {
return "", "", fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
return "", nil, fmt.Errorf("the command contains illegal characters. command: %s, user: %s, containerID: %s", command, user, containerID)
}
if len(command) == 0 || len(containerID) == 0 {
return "", "", fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
return "", nil, fmt.Errorf("error param of command: %s or containerID: %s", command, containerID)
}
command = fmt.Sprintf("docker exec -it %s %s", containerID, command)
commands := []string{"exec", "-it", containerID, command}
if len(user) != 0 {
command = fmt.Sprintf("docker exec -it -u %s %s %s", user, containerID, command)
commands = []string{"exec", "-it", "-u", user, containerID, command}
}

return containerID, command, nil
return containerID, commands, nil
}

func wshandleError(ws *websocket.Conn, err error) bool {
Expand Down Expand Up @@ -204,3 +254,12 @@ 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
}
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 looks generally clean and well-structured. However, there are a few improvements that can be made for readability and maintainability:

  1. Variable Naming: Some variable names could be more descriptive to improve clarity.

  2. Error Handling: The wshandleError function should log an error message in addition to returning true. This will help with debugging if something goes wrong later on.

  3. Concurrency Consideration: The way processes like containers and terminals are managed is relatively straightforward but consider how concurrent access might impact performance or behavior.

  4. Comments: Adding comments about certain logic decisions would make it easier for others (or future you) to understand the purpose of different parts of the code.

Updated Code

package api

import (
	"context"
	"fmt"

	"log"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"github.com/your-module-name/app/modules/dto"
	"github.com/your-module-name/cmd"
	"github.com/your-module-name/common/global"
	"github.com/your-module-name/lib/databaseService"
	"github.com/your-module-name/lib/killBash"
	"github.com/your-module-name/lib/loadMapFromDockerTop"
	"github.com/your-module-name/lib/loadRedisInitCmd"
	"github.com/your-module-name/lib/loadOllamaInitCmd"
	"github.com/your-module-name/lib/loadContainerInitCmd"
)

var upGrader = websocket.Upgrader{}

// BaseApi represents the API base struct.
type BaseApi struct {
}

const defaultCols = 80
const defaultRows = 40

// WebSsh handles WebSocket connections for SSH sessions using bash shell.
func (b *BaseApi) WsSSH(c *gin.Context) {
	wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		global.LOG.Errorf("Failed to upgrade gin context HTTP handler: err: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"err": "failed to upgrade connection"})
		return
	}
	defer wsConn.Close()

	if global.CONF.Base.IsDemo {
		log.Println("WebSocket connection rejected due to demo mode.")
		wssHandleError(wsConn, errors.New("demo server prohibits this operation!"))
		return
	}

	colStr := c.DefaultQuery("cols", strconv.Itoa(defaultCols))
	rowStr := c.DefaultQuery("rows", strconv.Itoa(defaultRows))

	if col, err := strconv.Atoi(colStr); err != nil {
		wssHandleError(wsConn, fmt.Errorf("invalid param 'cols' in request: %w", err))
		return
	}
	if row, err := strconv.Atoi(rowStr); err != nil {
		wssHandleError(wsConn, fmt.Errorf("invalid param 'rows' in request: %w", err))
		return
	}

	executor, err := loadExecutor()
	if err != nil {
		wssHandleError(wsConn, fmt.Errorf("unable to determine executable: %w", err))
		return
	}

	slave, err := terminal.NewCommand(executor)
	if err != nil {
		wssHandleError(wsConn, fmt.Errorf("failed to create command executor: %w", err))
		return
	}
	defer slave.Close()

	tty, quitChannel, err := newLocalWsSession(cols, rows, wsConn, slave, false, shutdownContext(context.Background()))
	if err != nil {
		wssHandleError(wsConn, err)
		return
	}
	defer tty.Stop()
	defer close(quitChannel)

	go masterThread(tty, slave)

	select {
	case <-quitChannel:
		log.Printf("WebSocket session terminated for %d cols x %d rows\n", cols, rows)
	default:
	}
}

// ContainerWsSSH handles WebSocket connections for SSH sessions within Docker containers.
func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
	// Similar setup as BaseApi.WsSSH but adapted for container usage.
}

func wssHandleError(ws *websocket.Conn, err error, additionalLogMessage ...any) {
	if len(additionalLogMessage) > 0 {
		for _, msg := range additionalLogMessage {
			logger.Infof(msg)
		}
	}
	errMsg := fmt.Sprintf("WSS Error: %s", err.Error())
	global.LOG.Errorf(errMsg)
	c.JSON(http.StatusInternalServerError, gin.H{
		"err": errMsg,
	})
	fmt.Fprintf(ws, "%s \n", errMsg)
	return true
}

func loadExecutor() (string, error) {
	if cmd.Which("bash") {
		return "bash", nil
	} else if cmd.Which("sh") {
		return "sh", nil // Using sh instead of dash as some distributions prefer.
	} else if cmd.Which("zsh") {
		return "dash", nil // dash is usually available and used less often than other shells.
	}
	return "", fmt.Errorf("unsupported shell found")
}

Key Changes:

  • Added type declarations for variables where they were ambiguous (int, string).
  • Moved logging into the wssHandleError function, including calling it twice: once with additional log messages and again directly.
  • Simplified the handling of loadExecutor() return value by checking which shell works first before proceeding with fallback.
  • Used close(quitChannel) for proper channel closure and added comments for better understanding.

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.

Code Differences and Analysis:

1. Added Function WsSSH in BaseApi struct:

  • Introduced a new endpoint /ws/ssh for WebSocket connections using SSH.
  • Used `upGrader to upgrade request context to WebSocket connection.
  • Included checks for whether the instance is running in a demo mode and handled errors accordingly.
  • Retrieved columns (cols) and rows (rows) from query parameters, with defaults of 80 and 40 respectively.
  • Loaded an executor shell based on current configuration and attempted to start a local WebSockets session.
  • Managed goroutines to handle shutdown signals and deferred closing of relevant objects.

Potential Issues: The function lacks proper error handling if creating the local WebSockets session fails.

Optimization Suggestions:

  • Implement more detailed logging and use custom error messages for better debugging capability.
  • Ensure that resources (like quitChan, slave, and tty) are properly cleaned up before returning.

2. Changes in ContainerWsSSH method:

  • Replaced usage of strings concatenation with slice expansion when constructing Redis initial commands.
  • Simplified logic by passing multiple arguments directly to NewCommand.
  • Enhanced robustness by adding validation and return errors immediately upon catching them early in the flow.

Potential Issues: The functions related to initializing Ollama and regular containers might lead to repeated parsing or invalid data access since the loadOllamaInitCmd does not validate the existence of the container name.

Optimization Suggestions:

  • Validate presence of all necessary components required for initialization (database connection information, container name). If missing, return appropriate error codes.
  • Consolidate similar logic across these methods into utility functions or structs (e.g., initializeCommands) to avoid redundancy.

3. Additional Function wshandleError:

  • Designed as a helper function to manage WebSocket-related error reporting within API handlers cleanly.
  • Improved readability and maintainability of log entries regarding WebSocket events without repeating boilerplate code.

Potential Issues: Not designed to catch other types of HTTP-related errors like bad requests, which would need additional setup.

Optimization Suggestions:

  • Add explicit logging around common API errors such as incorrect query parameter formats, ensuring consistency in how logs report failures.

4. Updated LoadExecutor Function:

  • Utilized Bash script functionality via the cmd module to fetch the default Shell path.
  • Employed slicing techniques to strip trailing newline characters from the output string.

Potential Issues: Error handling was enhanced here, but previous versions did not include basic sanity checks (e.g., checking if the command returns success).

Optimization Suggestions:

  • Implement basic input sanitation during execution by removing any unwanted patterns from fetched commands (though this is generally mitigated by escaping special characters).
  • Consider moving some core command processing capabilities out of the controller layer into service classes or dedicated modules to improve separation of concerns.

2 changes: 2 additions & 0 deletions agent/router/ro_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,7 @@ func (s *HostRouter) InitRouter(Router *gin.RouterGroup) {
hostRouter.POST("/tool/supervisor/process", baseApi.OperateProcess)
hostRouter.GET("/tool/supervisor/process", baseApi.GetProcess)
hostRouter.POST("/tool/supervisor/process/file", baseApi.GetProcessFile)

hostRouter.GET("/exec", baseApi.WsSSH)
}
}
11 changes: 5 additions & 6 deletions agent/utils/terminal/local_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,20 @@ type LocalCommand struct {
pty *os.File
}

func NewCommand(initCmd string) (*LocalCommand, error) {
cmd := exec.Command("bash")
func NewCommand(name string, arg ...string) (*LocalCommand, error) {
cmd := exec.Command(name, arg...)
if term := os.Getenv("TERM"); term != "" {
cmd.Env = append(os.Environ(), "TERM="+term)
} else {
cmd.Env = append(os.Environ(), "TERM=xterm")
}
homeDir, _ := os.UserHomeDir()
cmd.Dir = homeDir

pty, err := pty.Start(cmd)
if err != nil {
return nil, errors.Wrapf(err, "failed to start command")
}
if len(initCmd) != 0 {
time.Sleep(100 * time.Millisecond)
_, _ = pty.Write([]byte(initCmd + "\n"))
}

lcmd := &LocalCommand{
closeSignal: DefaultCloseSignal,
Expand Down Expand Up @@ -99,4 +97,5 @@ func (lcmd *LocalCommand) Wait(quitChan chan bool) {
global.LOG.Errorf("ssh session wait failed, err: %v", err)
setQuit(quitChan)
}
setQuit(quitChan)
}
25 changes: 4 additions & 21 deletions core/app/service/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,9 @@ func (u *HostService) TestLocalConn(id uint) bool {
host model.Host
err error
)
if id == 0 {
host, err = hostRepo.Get(hostRepo.WithByAddr("127.0.0.1"))
if err != nil {
return false
}
} else {
host, err = hostRepo.Get(repo.WithByID(id))
if err != nil {
return false
}
host, err = hostRepo.Get(repo.WithByID(id))
if err != nil {
return false
}
var connInfo ssh.ConnInfo
if err := copier.Copy(&connInfo, &host); err != nil {
Expand Down Expand Up @@ -206,11 +199,7 @@ func (u *HostService) SearchForTree(search dto.SearchForTree) ([]dto.HostTree, e
func (u *HostService) GetHostByID(id uint) (*dto.HostInfo, error) {
var item dto.HostInfo
var host model.Host
if id == 0 {
host, _ = hostRepo.Get(repo.WithByName("local"))
} else {
host, _ = hostRepo.Get(repo.WithByID(id))
}
host, _ = hostRepo.Get(repo.WithByID(id))
if host.ID == 0 {
return nil, buserr.New("ErrRecordNotFound")
}
Expand Down Expand Up @@ -246,9 +235,6 @@ func (u *HostService) GetHostByID(id uint) (*dto.HostInfo, error) {
}

func (u *HostService) Create(req dto.HostOperate) (*dto.HostInfo, error) {
if req.Name == "local" {
return nil, buserr.New("ErrRecordExist")
}
hostItem, _ := hostRepo.Get(hostRepo.WithByAddr(req.Addr), hostRepo.WithByUser(req.User), hostRepo.WithByPort(req.Port))
if hostItem.ID != 0 {
return nil, buserr.New("ErrRecordExist")
Expand Down Expand Up @@ -305,9 +291,6 @@ func (u *HostService) Delete(ids []uint) error {
if host.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if host.Name == "local" {
return errors.New("the local connection information cannot be deleted!")
}
if err := hostRepo.Delete(repo.WithByID(id)); err != nil {
return err
}
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lang/modules/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,6 @@ const message = {
terminal: {
local: 'Local',
localHelper: 'The `local` name is used only for system local identification',
connLocalErr: 'Unable to automatically authenticate, please fill in the local server login information!',
testConn: 'Test connection',
saveAndConn: 'Save and Connect',
connTestOk: 'Connection information available',
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lang/modules/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,6 @@ const message = {
terminal: {
local: 'ローカル',
localHelper: 'ローカル名はシステムのローカル識別にのみ使用されます。',
connLocalErr: '自動的に認証できない場合は、ローカルサーバーのログイン情報を入力してください。',
testConn: 'テスト接続',
saveAndConn: '保存して接続します',
connTestOk: '利用可能な接続情報',
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lang/modules/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,6 @@ const message = {
terminal: {
local: '로컬',
localHelper: '로컬 이름은 시스템 로컬 식별에만 사용됩니다.',
connLocalErr: '자동 인증에 실패했습니다. 로컬 서버 로그인 정보를 입력해주세요.',
testConn: '연결 테스트',
saveAndConn: '저장 후 연결',
connTestOk: '연결 정보가 유효합니다.',
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lang/modules/ms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1079,7 +1079,6 @@ const message = {
terminal: {
local: 'Tempatan',
localHelper: 'Nama tempatan hanya digunakan untuk pengenalan sistem tempatan.',
connLocalErr: 'Tidak dapat mengesahkan secara automatik, sila isi maklumat log masuk pelayan tempatan.',
testConn: 'Uji sambungan',
saveAndConn: 'Simpan dan sambung',
connTestOk: 'Maklumat sambungan tersedia',
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/lang/modules/pt-br.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,8 +1069,6 @@ const message = {
terminal: {
local: 'Local',
localHelper: 'O nome local é usado apenas para identificação local do sistema.',
connLocalErr:
'Não foi possível autenticar automaticamente, por favor, preencha as informações de login do servidor local.',
testConn: 'Testar conexão',
saveAndConn: 'Salvar e conectar',
connTestOk: 'Informações de conexão disponíveis',
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/lang/modules/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,8 +1074,6 @@ const message = {
terminal: {
local: 'Локальный',
localHelper: 'Локальное имя используется только для локальной идентификации системы.',
connLocalErr:
'Невозможно автоматически аутентифицироваться, пожалуйста, заполните информацию для входа на локальный сервер.',
testConn: 'Проверить подключение',
saveAndConn: 'Сохранить и подключиться',
connTestOk: 'Информация о подключении доступна',
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lang/modules/zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,6 @@ const message = {
terminal: {
local: '本機',
localHelper: 'local 名稱僅用於系統本機標識',
connLocalErr: '無法自動認證,請填寫本地服務器的登錄信息!',
testConn: '連接測試',
saveAndConn: '保存並連接',
connTestOk: '連接信息可用',
Expand Down
1 change: 0 additions & 1 deletion frontend/src/lang/modules/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,6 @@ const message = {
terminal: {
local: '本机',
localHelper: 'local 名称仅用于系统本机标识',
connLocalErr: '无法自动认证,请填写本地服务器的登录信息!',
testConn: '连接测试',
saveAndConn: '保存并连接',
connTestOk: '连接信息可用',
Expand Down
8 changes: 1 addition & 7 deletions frontend/src/views/terminal/host/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
:data="data"
@search="search"
>
<el-table-column type="selection" :selectable="selectable" fix />
<el-table-column type="selection" fix />
<el-table-column :label="$t('terminal.ip')" prop="addr" fix />
<el-table-column :label="$t('commons.login.username')" show-overflow-tooltip prop="user" />
<el-table-column :label="$t('commons.table.port')" prop="port" />
Expand Down Expand Up @@ -106,9 +106,6 @@ const acceptParams = () => {
search();
};

function selectable(row) {
return row.name !== 'local';
}
const dialogRef = ref();
const onOpenDialog = async (
title: string,
Expand Down Expand Up @@ -174,9 +171,6 @@ const buttons = [
},
{
label: i18n.global.t('commons.button.delete'),
disabled: (row: any) => {
return row.name === 'local';
},
click: (row: Host.Host) => {
onBatchDelete(row);
},
Expand Down
Loading
Loading