mirror of
				https://github.com/veops/oneterm.git
				synced 2025-10-31 19:02:39 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			555 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			555 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Package handler
 | |
| /**
 | |
| Copyright (c) The Authors.
 | |
| * @Author: feng.xiang
 | |
| * @Date: 2024/1/18 17:05
 | |
| * @Desc:
 | |
| */
 | |
| package handler
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"regexp"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 	"sync/atomic"
 | |
| 	"time"
 | |
| 	"unicode/utf8"
 | |
| 
 | |
| 	gossh "github.com/gliderlabs/ssh"
 | |
| 	"github.com/nicksnyder/go-i18n/v2/i18n"
 | |
| 
 | |
| 	myi18n "github.com/veops/oneterm/pkg/i18n"
 | |
| 	"github.com/veops/oneterm/pkg/logger"
 | |
| 	"github.com/veops/oneterm/pkg/proto/ssh/client"
 | |
| 	"github.com/veops/oneterm/pkg/proto/ssh/config"
 | |
| 	"github.com/veops/oneterm/pkg/server/model"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	IdleTimeout = time.Minute * 5
 | |
| 	ReturnKey   = []byte{0x0d}
 | |
| )
 | |
| 
 | |
| func (i *InteractiveHandler) Proxy(line string, accountId int) (step int, err error) {
 | |
| 	step = 1
 | |
| 	if accountId > 0 {
 | |
| 		var accountName string
 | |
| 		defer func() {
 | |
| 			if err != nil {
 | |
| 				i.PrintMessage(myi18n.MsgSshAccountLoginError, map[string]any{"User": accountName})
 | |
| 			}
 | |
| 		}()
 | |
| 		if i.SelectedAsset != nil && accountId < 0 {
 | |
| 			accountName = line
 | |
| 		}
 | |
| 		host, ok, er := i.Check(i.SelectedAsset.Id, nil)
 | |
| 		if er != nil {
 | |
| 			err = er
 | |
| 			return
 | |
| 		}
 | |
| 		if !ok {
 | |
| 			err = fmt.Errorf("current time is not allowed to access")
 | |
| 			return
 | |
| 		}
 | |
| 		i.SelectedAsset = host
 | |
| 		if _, ok := i.SelectedAsset.Authorization[accountId]; !ok {
 | |
| 			err = fmt.Errorf("you donnot have permission")
 | |
| 			return
 | |
| 		}
 | |
| 		account, er := i.Sshd.Core.Auth.AccountInfo(i.Session.Context().Value("cookie").(string), accountId, accountName)
 | |
| 		if er != nil {
 | |
| 			err = er
 | |
| 			return
 | |
| 		}
 | |
| 		accountName = account.Name
 | |
| 		var gateway *model.Gateway
 | |
| 		if i.SelectedAsset.GatewayId != 0 {
 | |
| 			gateway, err = i.Sshd.Core.Asset.Gateway(i.Session.Context().Value("cookie").(string), i.SelectedAsset.GatewayId)
 | |
| 			if err != nil {
 | |
| 				return
 | |
| 			}
 | |
| 		}
 | |
| 		conn, er := i.NewSession(account, gateway)
 | |
| 		if er != nil {
 | |
| 			err = er
 | |
| 			return
 | |
| 		}
 | |
| 		er = i.UpsertSession(conn, model.SESSIONSTATUS_ONLINE)
 | |
| 		if er != nil {
 | |
| 			logger.L.Warn(er.Error())
 | |
| 		}
 | |
| 		config.TotalHostSession.Store(conn.SessionId, conn)
 | |
| 
 | |
| 		if err := i.bind(i.Session, conn); err != nil {
 | |
| 			logger.L.Error(err.Error())
 | |
| 		}
 | |
| 		step = 1
 | |
| 	} else {
 | |
| 		if i.NeedAccount && i.SelectedAsset != nil {
 | |
| 			i.NeedAccount = false
 | |
| 			var account *model.Account
 | |
| 			accountIds := make([]int, 0)
 | |
| 			for aId := range i.SelectedAsset.Authorization {
 | |
| 				accountIds = append(accountIds, aId)
 | |
| 			}
 | |
| 			sort.Ints(accountIds)
 | |
| 			for _, aId := range accountIds {
 | |
| 				account := i.Accounts[aId]
 | |
| 				if config.SSHConfig.PlainMode {
 | |
| 					if account == nil || !strings.Contains(account.Name, line) {
 | |
| 						continue
 | |
| 					}
 | |
| 				} else {
 | |
| 					if account == nil || line != account.Name {
 | |
| 						continue
 | |
| 					}
 | |
| 				}
 | |
| 				return i.Proxy(line, account.Id)
 | |
| 			}
 | |
| 			if account == nil {
 | |
| 				i.PrintMessage(myi18n.MsgSshAccountLoginError, map[string]any{"User": line})
 | |
| 			}
 | |
| 			return
 | |
| 		} else if strings.TrimSpace(line) == "" {
 | |
| 			return
 | |
| 		}
 | |
| 		var (
 | |
| 			host *model.Asset
 | |
| 		)
 | |
| 		selectHosts, likeHosts, er := i.AcquireAndStoreAssets(line, 0)
 | |
| 		if er != nil {
 | |
| 			logger.L.Error(err.Error())
 | |
| 		}
 | |
| 		if len(selectHosts) == 0 && len(likeHosts) == 0 {
 | |
| 			i.PrintMessage(myi18n.MsgSshNoMatchingAsset, map[string]any{"Host": line})
 | |
| 			return
 | |
| 		}
 | |
| 
 | |
| 		switch len(selectHosts) {
 | |
| 		case 0:
 | |
| 			switch len(likeHosts) {
 | |
| 			case 0:
 | |
| 				return
 | |
| 			case 1:
 | |
| 				host = likeHosts[0]
 | |
| 			default:
 | |
| 				i.showResult(likeHosts)
 | |
| 				return
 | |
| 			}
 | |
| 		case 1:
 | |
| 			host = selectHosts[0]
 | |
| 		default:
 | |
| 			i.showResult(selectHosts)
 | |
| 			return
 | |
| 		}
 | |
| 		i.SelectedAsset = host
 | |
| 
 | |
| 		var sshPort string
 | |
| 		for _, v := range host.Protocols {
 | |
| 			if strings.HasPrefix(v, "ssh") {
 | |
| 				sshPort = getSshPort(v)
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		if sshPort == "" {
 | |
| 			i.PrintMessage(myi18n.MsgSshNoSshAccessMethod, map[string]any{"Host": line})
 | |
| 			return
 | |
| 		}
 | |
| 		i.SessionReq.Protocol = "ssh:" + sshPort
 | |
| 
 | |
| 		var hostAccountIds []int
 | |
| 		if len(i.Accounts) == 0 {
 | |
| 			accounts, er := i.AcquireAccounts()
 | |
| 			if er != nil {
 | |
| 				logger.L.Info(er.Error())
 | |
| 			} else {
 | |
| 				i.Accounts = accounts
 | |
| 			}
 | |
| 		}
 | |
| 		for k := range host.Authorization {
 | |
| 			if v, ok := i.Accounts[k]; ok {
 | |
| 				hostAccountIds = append(hostAccountIds, v.Id)
 | |
| 			}
 | |
| 		}
 | |
| 		sort.Ints(hostAccountIds)
 | |
| 		switch len(hostAccountIds) {
 | |
| 		case 0:
 | |
| 			i.PrintMessage(myi18n.MsgSshNoSshAccountForAsset, map[string]any{"Host": line})
 | |
| 			return
 | |
| 		case 1:
 | |
| 			return i.Proxy(line, hostAccountIds[0])
 | |
| 		default:
 | |
| 			var accounts []string
 | |
| 			for _, aId := range hostAccountIds {
 | |
| 				if account, ok := i.Accounts[aId]; ok {
 | |
| 					i.AccountsForSelect = append(i.AccountsForSelect, account)
 | |
| 					accounts = append(accounts, account.Name)
 | |
| 				}
 | |
| 			}
 | |
| 			i.NeedAccount = true
 | |
| 			if config.SSHConfig.PlainMode {
 | |
| 				i.PrintMessage(myi18n.MsgSshMultiSshAccountForAsset, map[string]any{"Accounts": i.tableData(accounts)})
 | |
| 			}
 | |
| 			step = 2
 | |
| 		}
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func getSshPort(protocol string) (sshPort string) {
 | |
| 	tmp := strings.Split(protocol, ":")
 | |
| 	if len(tmp) == 2 && tmp[0] == "ssh" {
 | |
| 		sshPort = tmp[1]
 | |
| 	}
 | |
| 	if strings.TrimSpace(sshPort) == "" {
 | |
| 		sshPort = "22"
 | |
| 	}
 | |
| 	return sshPort
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) handleUserInput(userConn gossh.Session, targetInChan chan<- []byte,
 | |
| 	done chan struct{}) {
 | |
| 	buffer := bytes.NewBuffer(make([]byte, 0, 1024*2))
 | |
| 	maxLen := 1024
 | |
| 	for {
 | |
| 		buf := make([]byte, maxLen)
 | |
| 		nr, err := userConn.Read(buf)
 | |
| 
 | |
| 		if nr > 0 {
 | |
| 			validBytes := buf[:nr]
 | |
| 			bufferLen := buffer.Len()
 | |
| 			if bufferLen > 0 || nr == maxLen {
 | |
| 				buffer.Write(buf[:nr])
 | |
| 				validBytes = validBytes[:0]
 | |
| 			}
 | |
| 			remainBytes := buffer.Bytes()
 | |
| 			for len(remainBytes) > 0 {
 | |
| 				r, size := utf8.DecodeRune(remainBytes)
 | |
| 				if r == utf8.RuneError {
 | |
| 					if len(remainBytes) <= 3 {
 | |
| 						break
 | |
| 					}
 | |
| 				}
 | |
| 				validBytes = append(validBytes, remainBytes[:size]...)
 | |
| 				remainBytes = remainBytes[size:]
 | |
| 			}
 | |
| 			buffer.Reset()
 | |
| 			if len(remainBytes) > 0 {
 | |
| 				buffer.Write(remainBytes)
 | |
| 			}
 | |
| 			select {
 | |
| 			case targetInChan <- validBytes:
 | |
| 			case <-done:
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		if err != nil {
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 	close(targetInChan)
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) handleHostOutput(hostConn *client.Connection, userConn gossh.Session,
 | |
| 	done chan struct{}, ticker *time.Ticker) {
 | |
| 
 | |
| 	for {
 | |
| 		buf := make([]byte, 1024)
 | |
| 		n, err := hostConn.Stdout.Read(buf)
 | |
| 		if err != nil {
 | |
| 			logger.L.Warn(err.Error())
 | |
| 		}
 | |
| 
 | |
| 		ticker.Reset(IdleTimeout)
 | |
| 		i.handleOutput(buf[:n], hostConn, userConn)
 | |
| 		if err == io.EOF {
 | |
| 			close(done)
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) bind(userConn gossh.Session, hostConn *client.Connection) error {
 | |
| 	maxIdleTimeout := time.Hour * 2
 | |
| 	mConfig, _ := i.AcquireConfig()
 | |
| 	if mConfig != nil && mConfig.Timeout > 0 {
 | |
| 		IdleTimeout = time.Second * time.Duration(mConfig.Timeout)
 | |
| 	}
 | |
| 	if IdleTimeout > maxIdleTimeout {
 | |
| 		IdleTimeout = maxIdleTimeout
 | |
| 	}
 | |
| 
 | |
| 	targetInChan := make(chan []byte, 1)
 | |
| 	done := make(chan struct{})
 | |
| 	var (
 | |
| 		exit             bool
 | |
| 		accessUpdateStep = time.Minute
 | |
| 		waitRead         atomic.Bool
 | |
| 	)
 | |
| 	waitRead.Store(true)
 | |
| 	i.wrapJsonResponse(hostConn.SessionId, 0, "success")
 | |
| 	tk, tkAccess, readReset := time.NewTicker(IdleTimeout), time.NewTicker(accessUpdateStep), time.NewTicker(time.Second)
 | |
| 	go i.handleUserInput(userConn, targetInChan, done)
 | |
| 	go i.handleHostOutput(hostConn, userConn, done, tk)
 | |
| 	for {
 | |
| 		select {
 | |
| 		case p, ok := <-targetInChan:
 | |
| 			if !ok {
 | |
| 				return nil
 | |
| 			}
 | |
| 			tk.Reset(IdleTimeout)
 | |
| 			//readL.WriteStdin(p)
 | |
| 			err, _ := i.HandleData(p, hostConn, userConn)
 | |
| 			if err != nil {
 | |
| 				logger.L.Error(err.Error())
 | |
| 			}
 | |
| 			readReset.Reset(time.Second)
 | |
| 		case <-done:
 | |
| 			exit = true
 | |
| 		case <-tk.C:
 | |
| 			_, err := userConn.Write([]byte(i.Message(myi18n.MsgSShHostIdleTimeout, map[string]any{"Idle": IdleTimeout})))
 | |
| 
 | |
| 			if err != nil {
 | |
| 				logger.L.Warn(err.Error())
 | |
| 			}
 | |
| 			exit = true
 | |
| 		case <-readReset.C:
 | |
| 			waitRead.Store(true)
 | |
| 		case <-hostConn.Exit:
 | |
| 			exit = true
 | |
| 		case <-i.Session.Context().Done():
 | |
| 			exit = true
 | |
| 		case <-tkAccess.C:
 | |
| 			commands, er := i.AcquireCommands()
 | |
| 			if er == nil {
 | |
| 				i.Commands = commands
 | |
| 			}
 | |
| 			asset, er := i.AcquireAssets("", hostConn.AssetId)
 | |
| 			if er != nil || len(asset) <= 0 || !i.Sshd.Core.Asset.HasPermission(asset[0].AccessAuth) {
 | |
| 				_, err := userConn.Write([]byte(i.Message(myi18n.MsgSshAccessRefusedInTimespan, nil)))
 | |
| 				if err != nil {
 | |
| 					logger.L.Error(err.Error())
 | |
| 				}
 | |
| 				exit = true
 | |
| 				break
 | |
| 			}
 | |
| 			i.SelectedAsset = asset[0]
 | |
| 			if _, ok := asset[0].Authorization[hostConn.AccountId]; !ok {
 | |
| 				_, err := userConn.Write([]byte(i.Message(myi18n.MsgSshNoAssetPermission, map[string]any{"Host": i.SelectedAsset.Name})))
 | |
| 				if err != nil {
 | |
| 					logger.L.Warn(err.Error())
 | |
| 				}
 | |
| 				exit = true
 | |
| 				break
 | |
| 			}
 | |
| 			account, er := i.AcquireAccountInfo(hostConn.AccountId, "")
 | |
| 			if er != nil || account == nil {
 | |
| 				_, err := userConn.Write([]byte(i.Message(myi18n.MsgSshNoAssetPermission, map[string]any{"Host": i.SelectedAsset.Name})))
 | |
| 				if err != nil {
 | |
| 					logger.L.Warn(err.Error())
 | |
| 				}
 | |
| 				exit = true
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		if exit {
 | |
| 			i.Exits(hostConn)
 | |
| 			return nil
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) handleOutput(data []byte, hostConn *client.Connection, userConn gossh.Session) {
 | |
| 	_, err := userConn.Write(data)
 | |
| 	if err != nil {
 | |
| 		logger.L.Info(err.Error())
 | |
| 	}
 | |
| 	if !hostConn.Parser.State(data) {
 | |
| 		i.Parser.OutputData = append(i.Parser.OutputData, data...)
 | |
| 	}
 | |
| 	err = hostConn.Record.Write(data)
 | |
| 	if err != nil {
 | |
| 		logger.L.Error(err.Error())
 | |
| 	}
 | |
| 	Monitor(hostConn.SessionId, data)
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) CommandLevel(cmd string) int {
 | |
| 	// TODO
 | |
| 	return 0
 | |
| }
 | |
| 
 | |
| func parseOutput(data []string) (output []string) {
 | |
| 	for _, line := range data {
 | |
| 		if strings.TrimSpace(line) != "" {
 | |
| 			output = append(output, line)
 | |
| 		}
 | |
| 	}
 | |
| 	return output
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) Output() string {
 | |
| 	i.Parser.Output.Feed(i.Parser.OutputData)
 | |
| 
 | |
| 	res := parseOutput(i.Parser.Output.Listener.Display())
 | |
| 	if len(res) == 0 {
 | |
| 		return ""
 | |
| 	}
 | |
| 	return res[len(res)-1]
 | |
| }
 | |
| func (i *InteractiveHandler) Command() string {
 | |
| 	s := i.Output()
 | |
| 	return strings.TrimPrefix(s, i.Parser.Ps1)
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) HandleData(data []byte, hostConn *client.Connection, userConn gossh.Session) (err error, exit bool) {
 | |
| 	if hostConn.Parser.State(data) {
 | |
| 		_, err = hostConn.Stdin.Write(data)
 | |
| 		if err != nil {
 | |
| 			logger.L.Error(err.Error())
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 	var write bool
 | |
| 
 | |
| 	if bytes.LastIndex(data, []byte{0x0d}) == -1 {
 | |
| 		_, err = hostConn.Stdin.Write(data)
 | |
| 		if err != nil {
 | |
| 			logger.L.Error(err.Error())
 | |
| 		}
 | |
| 
 | |
| 		write = true
 | |
| 	} else {
 | |
| 		if len(data) > 1 {
 | |
| 			var tmp []byte
 | |
| 			for _, d := range data {
 | |
| 				if d != 0x0d {
 | |
| 					tmp = append(tmp, d)
 | |
| 					continue
 | |
| 				}
 | |
| 				if len(tmp) > 0 {
 | |
| 					err1, stop := i.HandleData(tmp, hostConn, userConn)
 | |
| 					if err1 != nil {
 | |
| 						return err1, stop
 | |
| 					}
 | |
| 					if stop {
 | |
| 						break
 | |
| 					}
 | |
| 
 | |
| 				}
 | |
| 				err1, stop := i.HandleData(ReturnKey, hostConn, userConn)
 | |
| 				if err != nil {
 | |
| 					return err1, stop
 | |
| 				}
 | |
| 				if stop {
 | |
| 					break
 | |
| 				}
 | |
| 			}
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 	if bytes.LastIndex(data, ReturnKey) == 0 {
 | |
| 		time.Sleep(time.Millisecond * 100)
 | |
| 	}
 | |
| 
 | |
| 	if len(i.Parser.InputData) == 0 && i.Parser.Ps2 == "" {
 | |
| 		i.Parser.Ps1 = i.Output()
 | |
| 	}
 | |
| 	i.Parser.InputData = append(i.Parser.InputData, data...)
 | |
| 	if bytes.LastIndex(data, ReturnKey) == 0 {
 | |
| 		command := i.Command()
 | |
| 		i.Parser.Output.Listener.Reset()
 | |
| 		i.Parser.OutputData = nil
 | |
| 		i.Parser.InputData = nil
 | |
| 		i.Parser.Ps2 = ""
 | |
| 
 | |
| 		if _, valid := i.CommandCheck(command); valid {
 | |
| 			if strings.TrimSpace(command) != "" {
 | |
| 				go i.Sshd.Core.Audit.AddCommand(model.SessionCmd{Cmd: command, Level: i.CommandLevel(command), SessionId: hostConn.SessionId})
 | |
| 			}
 | |
| 			if !write {
 | |
| 				_, err = hostConn.Stdin.Write(data)
 | |
| 				if err != nil {
 | |
| 					logger.L.Warn(err.Error())
 | |
| 				}
 | |
| 			}
 | |
| 		} else {
 | |
| 			tips, _ := i.Localizer.Localize(&i18n.LocalizeConfig{
 | |
| 				DefaultMessage: myi18n.MsgSshCommandRefused,
 | |
| 				TemplateData:   map[string]string{"Command": command},
 | |
| 				PluralCount:    1,
 | |
| 			})
 | |
| 			_, err = hostConn.Stdin.Write([]byte{0x15})
 | |
| 			if err != nil {
 | |
| 				logger.L.Warn(err.Error())
 | |
| 			}
 | |
| 
 | |
| 			i.Parser.Ps2 = i.Parser.Ps1 + command
 | |
| 			i.handleOutput([]byte("\r\n"+tips+i.Parser.Ps2), hostConn, userConn)
 | |
| 			_, err = hostConn.Stdin.Write([]byte{0x15})
 | |
| 			return err, true
 | |
| 		}
 | |
| 	}
 | |
| 	return
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) Exits(conn *client.Connection) {
 | |
| 	if conn.GateWayCloseChan != nil {
 | |
| 		conn.GateWayCloseChan <- struct{}{}
 | |
| 	}
 | |
| 	_ = conn.Session.Close()
 | |
| 	conn.Record.Close()
 | |
| 
 | |
| 	config.TotalHostSession.Delete(conn.SessionId)
 | |
| 	config.TotalMonitors.Delete(conn.SessionId)
 | |
| 
 | |
| 	i.Locker.Lock()
 | |
| 	delete(i.SshSession, conn.SessionId)
 | |
| 	if len(i.SshSession) == 0 {
 | |
| 		_ = i.SshClient.Close()
 | |
| 		i.SshClient = nil
 | |
| 	}
 | |
| 	i.Locker.Unlock()
 | |
| 	err := i.UpsertSession(conn, model.SESSIONSTATUS_OFFLINE)
 | |
| 	if err != nil {
 | |
| 		logger.L.Error(err.Error())
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (i *InteractiveHandler) CommandCheck(command string) (string, bool) {
 | |
| 	for _, id := range i.SelectedAsset.CmdIds {
 | |
| 		cmd, ok := i.Commands[id]
 | |
| 		if !ok || !cmd.Enable {
 | |
| 			continue
 | |
| 		}
 | |
| 		for _, c := range cmd.Cmds {
 | |
| 			p, err := regexp.Compile(c)
 | |
| 			if err == nil {
 | |
| 				if p.Match([]byte(command)) {
 | |
| 					return c, false
 | |
| 				}
 | |
| 			} else {
 | |
| 				if c == command {
 | |
| 					return c, false
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return "", true
 | |
| }
 | |
| 
 | |
| //func (v *VirtualTermIn) Read(p []byte) (n int, err error) {
 | |
| //	return v.InChan.Read(p)
 | |
| //}
 | |
| //
 | |
| //func (v *VirtualTermIn) Close() error {
 | |
| //	return nil
 | |
| //}
 | |
| //
 | |
| //func (v *VirtualTermOut) Write(p []byte) (n int, err error) {
 | |
| //	return v.OutChan.Write(p)
 | |
| //}
 | 
