package sshd import ( "cocogo/pkg/model" "cocogo/pkg/proxy" "cocogo/pkg/transport" "cocogo/pkg/userhome" "context" "encoding/json" "fmt" "io" "strconv" "strings" "sync" "time" uuid "github.com/satori/go.uuid" "github.com/olekukonko/tablewriter" "github.com/xlab/treeprint" "github.com/gliderlabs/ssh" "golang.org/x/crypto/ssh/terminal" ) type HelpInfo struct { UserName string ColorCode string ColorEnd string Tab string EndLine string } func (d HelpInfo) displayHelpInfo(sess ssh.Session) { e := displayTemplate.Execute(sess, d) if e != nil { log.Warn("display help info failed") } } type sshInteractive struct { sess ssh.Session term *terminal.Terminal assetData *sync.Map user model.User helpInfo HelpInfo currentSearchAssets []model.Asset onceLoad sync.Once sync.RWMutex } func (s *sshInteractive) displayHelpInfo() { s.helpInfo.displayHelpInfo(s.sess) } func (s *sshInteractive) chooseSystemUser(systemUsers []model.SystemUser) model.SystemUser { table := tablewriter.NewWriter(s.sess) table.SetHeader([]string{"ID", "UserName"}) for i := 0; i < len(systemUsers); i++ { table.Append([]string{strconv.Itoa(i + 1), systemUsers[i].UserName}) } table.SetBorder(false) count := 0 term := terminal.NewTerminal(s.sess, "num:") for count < 3 { table.Render() line, err := term.ReadLine() if err != nil { continue } if num, err := strconv.Atoi(line); err == nil { if num > 0 && num <= len(systemUsers) { return systemUsers[num-1] } } count++ } return systemUsers[0] } // 当资产的数量为1的时候,就进行代理转化 func (s *sshInteractive) displayAssetsOrProxy(assets []model.Asset) { if len(assets) == 1 { var systemUser model.SystemUser switch len(assets[0].SystemUsers) { case 0: // 有授权的资产,但是资产用户信息,无法登陆 s.displayAssets(assets) return case 1: systemUser = assets[0].SystemUsers[0] default: systemUser = s.chooseSystemUser(assets[0].SystemUsers) } authInfo, err := appService.GetSystemUserAssetAuthInfo(systemUser.Id, assets[0].Id) if err != nil { return } if ok := appService.ValidateUserAssetPermission(s.user.Id, systemUser.Id, assets[0].Id); !ok { // 检查user 是否对该资产有权限 return } err = s.Proxy(assets[0], authInfo) if err != nil { log.Info(err) } return } else { s.displayAssets(assets) } } func (s *sshInteractive) displayAssets(assets []model.Asset) { if len(assets) == 0 { _, _ = io.WriteString(s.sess, "\r\n No Assets\r\n\r") } else { table := tablewriter.NewWriter(s.sess) table.SetHeader([]string{"ID", "Hostname", "IP", "LoginAs", "Comment"}) for index, assetItem := range assets { sysUserArray := make([]string, len(assetItem.SystemUsers)) for index, sysUser := range assetItem.SystemUsers { sysUserArray[index] = sysUser.Name } sysUsers := "[" + strings.Join(sysUserArray, " ") + "]" table.Append([]string{strconv.Itoa(index + 1), assetItem.Hostname, assetItem.Ip, sysUsers, assetItem.Comment}) } table.SetBorder(false) table.Render() } } func (s *sshInteractive) displayAssetNodes(nodes []model.AssetNode) { tree := ConstructAssetNodeTree(nodes) tipHeaderMsg := "\r\nNode: [ ID.Name(Asset amount) ]" tipEndMsg := "Tips: Enter g+NodeID to display the host under the node, such as g1\r\n\r" _, err := io.WriteString(s.sess, tipHeaderMsg) _, err = io.WriteString(s.sess, tree.String()) _, err = io.WriteString(s.sess, tipEndMsg) if err != nil { log.Info("displayAssetNodes err:", err) } } func (s *sshInteractive) refreshAssetsAndNodesData() { s.loadUserAssets() s.loadUserAssetNodes() _, err := io.WriteString(s.sess, "Refresh done\r\n") if err != nil { log.Error("refresh Assets Nodes err:", err) } } func (s *sshInteractive) loadUserAssets() { assets, err := appService.GetUserAssets(s.user.Id) if err != nil { log.Error("load Assets failed") return } log.Info("load Assets success") Cached.Store(s.user.Id, assets) s.assetData.Store(AssetsMapKey, assets) } func (s *sshInteractive) loadUserAssetNodes() { assetNodes, err := appService.GetUserAssetNodes(s.user.Id) if err != nil { log.Error("load Asset Nodes failed") return } log.Info("load Asset Nodes success") s.assetData.Store(AssetNodesMapKey, assetNodes) } func (s *sshInteractive) changeLanguage() { } func (s *sshInteractive) JoinShareRoom(roomID string) { sshConn := userhome.NewSSHConn(s.sess) ctx, cancelFuc := context.WithCancel(s.sess.Context()) _, winCh, _ := s.sess.Pty() go func() { for { select { case <-ctx.Done(): return case win, ok := <-winCh: if !ok { return } fmt.Println("join term change:", win) } } }() proxy.Manager.JoinShareRoom(roomID, sshConn) log.Info("exit room id:", roomID) cancelFuc() } func (s *sshInteractive) StartDispatch() { _, winCh, _ := s.sess.Pty() for { ctx, cancelFuc := context.WithCancel(s.sess.Context()) go func() { for { select { case <-ctx.Done(): log.Info("ctx done") return case win, ok := <-winCh: if !ok { return } log.Info("InteractiveHandler term change:", win) _ = s.term.SetSize(win.Width, win.Height) } } }() line, err := s.term.ReadLine() cancelFuc() if err != nil { log.Error("ReadLine done", err) break } if line == "" { continue } s.onceLoad.Do(func() { if _, ok := Cached.Load(s.user.Id); !ok { log.Info("first load this user asset data ") s.loadUserAssets() s.loadUserAssetNodes() } else { go func() { s.loadUserAssets() s.loadUserAssetNodes() }() } }) if len(line) == 1 { switch line { case "p", "P": if assets, ok := s.assetData.Load(AssetsMapKey); ok { s.displayAssets(assets.([]model.Asset)) s.currentSearchAssets = assets.([]model.Asset) } else if assets, ok := Cached.Load(s.user.Id); ok { s.displayAssets(assets.([]model.Asset)) s.currentSearchAssets = assets.([]model.Asset) } case "g", "G": if assetNodes, ok := s.assetData.Load(AssetNodesMapKey); ok { s.displayAssetNodes(assetNodes.([]model.AssetNode)) } else { s.displayAssetNodes([]model.AssetNode{}) } case "s", "S": s.changeLanguage() case "h", "H": s.displayHelpInfo() case "r", "R": s.refreshAssetsAndNodesData() case "q", "Q": log.Info("exit session") return default: assets := s.searchAsset(line) s.currentSearchAssets = assets s.displayAssetsOrProxy(assets) } continue } if strings.Index(line, "/") == 0 { searchWord := strings.TrimSpace(strings.TrimPrefix(line, "/")) assets := s.searchAsset(searchWord) s.currentSearchAssets = assets s.displayAssets(assets) continue } if strings.Index(line, "g") == 0 { searchWord := strings.TrimSpace(strings.TrimPrefix(line, "g")) if num, err := strconv.Atoi(searchWord); err == nil { if num >= 0 { assets := s.searchNodeAssets(num) s.displayAssets(assets) s.currentSearchAssets = assets continue } } } if strings.Index(line, "join") == 0 { roomID := strings.TrimSpace(strings.TrimPrefix(line, "join")) s.JoinShareRoom(roomID) continue } assets := s.searchAsset(line) s.currentSearchAssets = assets s.displayAssetsOrProxy(assets) } } func (s *sshInteractive) searchAsset(key string) (assets []model.Asset) { if indexNum, err := strconv.Atoi(key); err == nil { if indexNum > 0 && indexNum <= len(s.currentSearchAssets) { return []model.Asset{s.currentSearchAssets[indexNum-1]} } } if assetsData, ok := s.assetData.Load(AssetsMapKey); ok { for _, assetValue := range assetsData.([]model.Asset) { if isSubstring([]string{assetValue.Ip, assetValue.Hostname, assetValue.Comment}, key) { assets = append(assets, assetValue) } } } else { assetsData, _ := Cached.Load(s.user.Id) for _, assetValue := range assetsData.([]model.Asset) { if isSubstring([]string{assetValue.Ip, assetValue.Hostname, assetValue.Comment}, key) { assets = append(assets, assetValue) } } } return assets } func (s *sshInteractive) searchNodeAssets(num int) (assets []model.Asset) { var assetNodesData []model.AssetNode if assetNodes, ok := s.assetData.Load(AssetNodesMapKey); ok { assetNodesData = assetNodes.([]model.AssetNode) if num > len(assetNodesData) || num == 0 { return assets } return assetNodesData[num-1].AssetsGranted } return assets } func (s *sshInteractive) Proxy(asset model.Asset, systemUser model.SystemUserAuthInfo) error { /* 1. 创建SSHConn,符合core.Conn接口 2. 创建一个session Home 3. 创建一个NodeConn,及相关的channel 可以是MemoryChannel 或者是redisChannel 4. session Home 与 proxy channel 交换数据 */ ptyReq, winChan, _ := s.sess.Pty() sshConn := userhome.NewSSHConn(s.sess) serverAuth := transport.ServerAuth{ SessionID: uuid.NewV4().String(), IP: asset.Ip, Port: asset.Port, UserName: systemUser.UserName, Password: systemUser.Password, PublicKey: parsePrivateKey(systemUser.PrivateKey)} nodeConn, err := transport.NewNodeConn(s.sess.Context(), serverAuth, ptyReq, winChan) if err != nil { log.Error(err) return err } defer func() { nodeConn.Close() data := map[string]interface{}{ "id": nodeConn.SessionID, "user": s.user.UserName, "asset": asset.Hostname, "org_id": asset.OrgID, "system_user": systemUser.UserName, "login_from": "ST", "remote_addr": s.sess.RemoteAddr().String(), "is_finished": true, "date_start": nodeConn.StartTime.Format("2006-01-02 15:04:05 +0000"), "date_end": time.Now().UTC().Format("2006-01-02 15:04:05 +0000"), } postData, _ := json.Marshal(data) appService.FinishSession(nodeConn.SessionID, postData) appService.FinishReply(nodeConn.SessionID) }() data := map[string]interface{}{ "id": nodeConn.SessionID, "user": s.user.UserName, "asset": asset.Hostname, "org_id": asset.OrgID, "system_user": systemUser.UserName, "login_from": "ST", "remote_addr": s.sess.RemoteAddr().String(), "is_finished": false, "date_start": nodeConn.StartTime.Format("2006-01-02 15:04:05 +0000"), "date_end": nil, } postData, err := json.Marshal(data) if !appService.CreateSession(postData) { return err } memChan := transport.NewMemoryAgent(nodeConn) Home := userhome.NewUserSessionHome(sshConn) log.Info("session Home ID: ", Home.SessionID()) err = proxy.Manager.Switch(s.sess.Context(), Home, memChan) if err != nil { log.Error(err) } return err } func isSubstring(sArray []string, substr string) bool { for _, s := range sArray { if strings.Contains(s, substr) { return true } } return false } func ConstructAssetNodeTree(assetNodes []model.AssetNode) treeprint.Tree { model.SortAssetNodesByKey(assetNodes) var treeMap = map[string]treeprint.Tree{} tree := treeprint.New() for i := 0; i < len(assetNodes); i++ { r := strings.LastIndex(assetNodes[i].Key, ":") if r < 0 { subtree := tree.AddBranch(fmt.Sprintf("%s.%s(%s)", strconv.Itoa(i+1), assetNodes[i].Name, strconv.Itoa(assetNodes[i].AssetsAmount))) treeMap[assetNodes[i].Key] = subtree continue } if subtree, ok := treeMap[assetNodes[i].Key[:r]]; ok { nodeTree := subtree.AddBranch(fmt.Sprintf("%s.%s(%s)", strconv.Itoa(i+1), assetNodes[i].Name, strconv.Itoa(assetNodes[i].AssetsAmount))) treeMap[assetNodes[i].Key] = nodeTree } } return tree }