【问题标题】:Golang ssh - how to run multiple commands on the same session?Golang ssh - 如何在同一个会话上运行多个命令?
【发布时间】:2020-11-20 20:52:19
【问题描述】:

我试图通过ssh 运行多个命令,但似乎Session.Run 每个会话只允许一个命令(除非我错了)。我想知道如何绕过这个限制并重用会话或发送一系列命令。 原因是我需要在同一个会话中使用下一个命令( sh /usr/bin/myscript.sh )运行sudo su

【问题讨论】:

  • sudo 需要输入密码吗?如果不是,你可以使用sudo /usr/bin/myscript.sh(当然是chmod +x /usr/bin/myscript.sh)。
  • 谢谢!我不需要 sudo 的密码,所以我的问题得到了解决。不过我认为在某些情况下您可能需要按顺序运行命令。

标签: ssh go


【解决方案1】:

Session.Shell 允许运行多个命令,方法是通过 session.StdinPipe() 传递您的命令。

请注意,使用这种方法会使您的生活更加复杂;您需要管理输入缓冲区(不要忘记命令末尾的\n),而不是运行命令并在完成后收集输出的一次性函数调用,等待输出真正从 SSH 服务器返回,然后适当地处理该输出(如果您有多个正在运行的命令并且想知道哪个输出属于哪个输入,您需要制定一个计划来解决这个问题)。

stdinBuf, _ := session.StdinPipe()
err := session.Shell()
stdinBuf.Write([]byte("cd /\n"))
// The command has been sent to the device, but you haven't gotten output back yet.
// Not that you can't send more commands immediately.
stdinBuf.Write([]byte("ls\n"))
// Then you'll want to wait for the response, and watch the stdout buffer for output.

【讨论】:

    【解决方案2】:

    虽然针对您的具体问题,您可以轻松运行sudo /path/to/script.sh,但令我震惊的是,没有一种简单的方法可以在同一会话中运行多个命令,所以我想出了一点hack, YMMV:

    func MuxShell(w io.Writer, r io.Reader) (chan<- string, <-chan string) {
        in := make(chan string, 1)
        out := make(chan string, 1)
        var wg sync.WaitGroup
        wg.Add(1) //for the shell itself
        go func() {
            for cmd := range in {
                wg.Add(1)
                w.Write([]byte(cmd + "\n"))
                wg.Wait()
            }
        }()
        go func() {
            var (
                buf [65 * 1024]byte
                t   int
            )
            for {
                n, err := r.Read(buf[t:])
                if err != nil {
                    close(in)
                    close(out)
                    return
                }
                t += n
                if buf[t-2] == '$' { //assuming the $PS1 == 'sh-4.3$ '
                    out <- string(buf[:t])
                    t = 0
                    wg.Done()
                }
            }
        }()
        return in, out
    }
    
    func main() {
        config := &ssh.ClientConfig{
            User: "kf5",
            Auth: []ssh.AuthMethod{
                ssh.Password("kf5"),
            },
        }
        client, err := ssh.Dial("tcp", "127.0.0.1:22", config)
        if err != nil {
            panic(err)
        }
    
        defer client.Close()
        session, err := client.NewSession()
    
        if err != nil {
            log.Fatalf("unable to create session: %s", err)
        }
        defer session.Close()
    
        modes := ssh.TerminalModes{
            ssh.ECHO:          0,     // disable echoing
            ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
            ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
        }
    
        if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
            log.Fatal(err)
        }
    
        w, err := session.StdinPipe()
        if err != nil {
            panic(err)
        }
        r, err := session.StdoutPipe()
        if err != nil {
            panic(err)
        }
        in, out := MuxShell(w, r)
        if err := session.Start("/bin/sh"); err != nil {
            log.Fatal(err)
        }
        <-out //ignore the shell output
        in <- "ls -lhav"
        fmt.Printf("ls output: %s\n", <-out)
    
        in <- "whoami"
        fmt.Printf("whoami: %s\n", <-out)
    
        in <- "exit"
        session.Wait()
    }
    

    如果你的 shell 提示符不是以 $ 结尾($ 后跟一个空格),这将死锁,因此它是一个 hack。

    【讨论】:

    • 这不足为奇。 SSH 是一个远程“shell”,您要么运行一个命令,要么请求一个交互式 shell/pty。
    • 好点,我只是希望 ssh 包有某种功能,就像我做的 hack。
    【解决方案3】:

    NewSession 是一种连接方法。您无需每次都创建新连接。 Session 似乎是这个库为客户端调用的通道,许多通道在单个连接中多路复用。因此:

    func executeCmd(cmd []string, hostname string, config *ssh.ClientConfig) string {
        conn, err := ssh.Dial("tcp", hostname+":8022", config)
    
        if err != nil {
            log.Fatal(err)
        }
        defer conn.Close()
    
        var stdoutBuf bytes.Buffer
    
        for _, command := range cmd {
    
            session, err := conn.NewSession()
    
            if err != nil {
                log.Fatal(err)
            }
            defer session.Close()
    
            session.Stdout = &stdoutBuf
            session.Run(command)
        }
    
        return hostname + ": " + stdoutBuf.String()
    }
    

    因此,您打开一个新会话(通道)并在现有 ssh 连接中运行命令,但每次都使用一个新会话(通道)。

    【讨论】:

    • 这在您需要在命令之间保留上下文的情况下不起作用。
    【解决方案4】:

    你可以使用一个小技巧:sh -c 'cmd1&amp;&amp;cmd2&amp;&amp;cmd3&amp;&amp;cmd4&amp;&amp;etc..'

    这是一个单独的命令,实际的命令作为参数传递给将执行它们的 shell。这就是 Docker 处理多个命令的方式。

    【讨论】:

      【解决方案5】:

      这对我有用。

      package main
      
      import (
          "fmt"
          "golang.org/x/crypto/ssh"
          // "io"
          "log"
          "os"
          // Uncomment to store output in variable
          //"bytes"
      )
      
      type MachineDetails struct {
          username, password, hostname, port string
      }
      
      func main() {
      
          h1 := MachineDetails{"root", "xxxxx", "x.x.x.x", "22"}
      
          // Uncomment to store output in variable
          //var b bytes.Buffer
          //sess.Stdout = &amp;b
          //sess.Stderr = &amp;b
      
          commands := []string{
              "pwd",
              "whoami",
              "echo 'bye'",
              "exit",
          }
      
          connectHost(h1, commands)
      
          // Uncomment to store in variable
          //fmt.Println(b.String())
      
      }
      
      func connectHost(hostParams MachineDetails, commands []string) {
      
          // SSH client config
          config := &ssh.ClientConfig{
              User: hostParams.username,
              Auth: []ssh.AuthMethod{
                  ssh.Password(hostParams.password),
              },
              // Non-production only
              HostKeyCallback: ssh.InsecureIgnoreHostKey(),
          }
      
          // Connect to host
          client, err := ssh.Dial("tcp", hostParams.hostname+":"+hostParams.port, config)
          if err != nil {
              log.Fatal(err)
          }
          defer client.Close()
      
          // Create sesssion
          sess, err := client.NewSession()
          if err != nil {
              log.Fatal("Failed to create session: ", err)
          }
          defer sess.Close()
      
          // Enable system stdout
          // Comment these if you uncomment to store in variable
          sess.Stdout = os.Stdout
          sess.Stderr = os.Stderr
      
          // StdinPipe for commands
          stdin, err := sess.StdinPipe()
          if err != nil {
              log.Fatal(err)
          }
      
          // Start remote shell
          err = sess.Shell()
          if err != nil {
              log.Fatal(err)
          }
      
          // send the commands
      
          for _, cmd := range commands {
              _, err = fmt.Fprintf(stdin, "%s\n", cmd)
              if err != nil {
                  log.Fatal(err)
              }
          }
      
          // Wait for sess to finish
          err = sess.Wait()
          if err != nil {
              log.Fatal(err)
          }
      
          // return sess, stdin, err
      }
      
      func createSession() {
      
      }
      

      【讨论】:

        【解决方案6】:

        真的很喜欢 OneOfOne 的回答,它启发了我一个更通用的解决方案,即采用一个可以匹配读取字节尾部并打破阻塞读取的变量(也不需要分叉两个额外的线程来阻塞读取和写入)。已知的限制是(与原始解决方案一样)如果匹配字符串出现在 64 * 1024 字节之后,那么 this code 将永远旋转。

        package main
        
        import (
            "fmt"
            "golang.org/x/crypto/ssh"
            "io"
            "log"
        )
        
        var escapePrompt = []byte{'$', ' '}
        
        func main() {
            config := &ssh.ClientConfig{
                User: "dummy",
                Auth: []ssh.AuthMethod{
                    ssh.Password("dummy"),
                },
                HostKeyCallback: ssh.InsecureIgnoreHostKey(),
            }
            client, err := ssh.Dial("tcp", "127.0.0.1:22", config)
            if err != nil {
                panic(err)
            }
        
            defer client.Close()
            session, err := client.NewSession()
        
            if err != nil {
                log.Fatalf("unable to create session: %s", err)
            }
            defer session.Close()
        
            modes := ssh.TerminalModes{
                ssh.ECHO:          0,     // disable echoing
                ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
                ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
            }
        
            if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
                log.Fatal(err)
            }
        
            w, err := session.StdinPipe()
            if err != nil {
                panic(err)
            }
            r, err := session.StdoutPipe()
            if err != nil {
                panic(err)
            }
            if err := session.Start("/bin/sh"); err != nil {
                log.Fatal(err)
            }
            readUntil(r, escapePrompt) //ignore the shell output
        
            write(w, "ls -lhav")
        
            out, err := readUntil(r, escapePrompt)
        
            fmt.Printf("ls output: %s\n", *out)
        
            write(w, "whoami")
        
            out, err = readUntil(r, escapePrompt)
        
            fmt.Printf("whoami: %s\n", *out)
        
            write(w, "exit")
        
            session.Wait()
        }
        
        func write(w io.WriteCloser, command string) error {
            _, err := w.Write([]byte(command + "\n"))
            return err
        }
        
        func readUntil(r io.Reader, matchingByte []byte) (*string, error) {
            var buf [64 * 1024]byte
            var t int
            for {
                n, err := r.Read(buf[t:])
                if err != nil {
                    return nil, err
                }
                t += n
                if isMatch(buf[:t], t, matchingByte) {
                    stringResult := string(buf[:t])
                    return &stringResult, nil
                }
            }
        }
        
        func isMatch(bytes []byte, t int, matchingBytes []byte) bool {
            if t >= len(matchingBytes) {
                for i := 0; i < len(matchingBytes); i++ {
                    if bytes[t - len(matchingBytes) + i] != matchingBytes[i] {
                        return false
                    }
                }
                return true
            }
            return false
        }
        

        【讨论】:

          【解决方案7】:

          get inspiration from this

          我花了几天时间,这个答案激励我尝试使用 sdtin 运行多个命令,终于成功了。我想说我根本不知道golang,因此它可能是多余的,但代码有效。

          if _, err := w.Write([]byte("sys\r")); err != nil {
              panic("Failed to run: " + err.Error())
          }
          if _, err := w.Write([]byte("wlan\r")); err != nil {
              panic("Failed to run: " + err.Error())
          }
          if _, err := w.Write([]byte("ap-id 2099\r")); err != nil {
              panic("Failed to run: " + err.Error())
          }
          
          if _, err := w.Write([]byte("ap-group xuebao-free\r")); err != nil {
              panic("Failed to run: " + err.Error())
          }
          if _, err := w.Write([]byte("y\r")); err != nil {
              panic("Failed to run: " + err.Error())
          }
          

          功能同terminal operation

          这是完整的代码:

          /* switch ssh
           */
          package main
          
          import (
              "flag"
              "fmt"
              "io"
              "log"
              "net"
              "os"
              "strings"
              "sync"
          )
          
          import (
              "golang.org/x/crypto/ssh"
          )
          
          func main() {
              //go run ./testConfig.go --username="aaa" --passwd='aaa' --ip_port="192.168.6.87" --cmd='display version'
              username := flag.String("username", "aaa", "username")
              passwd := flag.String("passwd", "aaa", "password")
              ip_port := flag.String("ip_port", "1.1.1.1:22", "ip and port")
              cmdstring := flag.String("cmd", "display arp statistics all", "cmdstring")
              flag.Parse()
              fmt.Println("username:", *username)
              fmt.Println("passwd:", *passwd)
              fmt.Println("ip_port:", *ip_port)
              fmt.Println("cmdstring:", *cmdstring)
              config := &ssh.ClientConfig{
                  User: *username,
                  Auth: []ssh.AuthMethod{
                      ssh.Password(*passwd),
                  },
                  Config: ssh.Config{
                      Ciphers: []string{"aes128-cbc", "aes128-ctr"},
                  },
                  HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
                      return nil
                  },
              }
              // config.Config.Ciphers = append(config.Config.Ciphers, "aes128-cbc")
              clinet, err := ssh.Dial("tcp", *ip_port, config)
              checkError(err, "connet "+*ip_port)
          
              session, err := clinet.NewSession()
              defer session.Close()
              checkError(err, "creae shell")
          
              modes := ssh.TerminalModes{
                  ssh.ECHO:          1,     // disable echoing
                  ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
                  ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
              }
          
              if err := session.RequestPty("vt100", 80, 40, modes); err != nil {
                  log.Fatal(err)
              }
          
              w, err := session.StdinPipe()
              if err != nil {
                  panic(err)
              }
              r, err := session.StdoutPipe()
              if err != nil {
                  panic(err)
              }
              e, err := session.StderrPipe()
              if err != nil {
                  panic(err)
              }
          
              in, out := MuxShell(w, r, e)
              if err := session.Shell(); err != nil {
                  log.Fatal(err)
              }
              <-out //ignore the shell output
              in <- *cmdstring
              fmt.Printf("%s\n", <-out)
          
          
              if _, err := w.Write([]byte("sys\r")); err != nil {
                  panic("Failed to run: " + err.Error())
              }
          if _, err := w.Write([]byte("wlan\r")); err != nil {
                  panic("Failed to run: " + err.Error())
              }
          if _, err := w.Write([]byte("ap-id 2099\r")); err != nil {
                  panic("Failed to run: " + err.Error())
              }
          
          if _, err := w.Write([]byte("ap-group xuebao-free\r")); err != nil {
                  panic("Failed to run: " + err.Error())
              }
          if _, err := w.Write([]byte("y\r")); err != nil {
                  panic("Failed to run: " + err.Error())
              }
          
              in <- "quit"
              _ = <-out
              session.Wait()
          }
          
          func checkError(err error, info string) {
              if err != nil {
                  fmt.Printf("%s. error: %s\n", info, err)
                  os.Exit(1)
              }
          }
          
          func MuxShell(w io.Writer, r, e io.Reader) (chan<- string, <-chan string) {
              in := make(chan string, 5)
              out := make(chan string, 5)
              var wg sync.WaitGroup
              wg.Add(1) //for the shell itself
              go func() {
                  for cmd := range in {
                      wg.Add(1)
                      w.Write([]byte(cmd + "\n"))
                      wg.Wait()
                  }
              }()
          
              go func() {
                  var (
                      buf [1024 * 1024]byte
                      t   int
                  )
                  for {
                      n, err := r.Read(buf[t:])
                      if err != nil {
                          fmt.Println(err.Error())
                          close(in)
                          close(out)
                          return
                      }
                      t += n
                      result := string(buf[:t])
                      if strings.Contains(string(buf[t-n:t]), "More") {
                          w.Write([]byte("\n"))
                      }
                      if strings.Contains(result, "username:") ||
                          strings.Contains(result, "password:") ||
                          strings.Contains(result, ">") {
                          out <- string(buf[:t])
                          t = 0
                          wg.Done()
                      }
                  }
              }()
              return in, out
          }
          

          【讨论】:

            【解决方案8】:

            以下代码适用于我。

            func main() {
                key, err := ioutil.ReadFile("path to your key file")
                if err != nil {
                    panic(err)
                }
                signer, err := ssh.ParsePrivateKey([]byte(key))
                if err != nil {
                    panic(err)
                }
                config := &ssh.ClientConfig{
                    User: "ubuntu",
                    Auth: []ssh.AuthMethod{
                        ssh.PublicKeys(signer),
                    },
                }
                client, err := ssh.Dial("tcp", "52.91.35.179:22", config)
                if err != nil {
                    panic(err)
                }
                session, err := client.NewSession()
                if err != nil {
                    panic(err)
                }
                defer session.Close()
                session.Stdout = os.Stdout
                session.Stderr = os.Stderr
                session.Stdin = os.Stdin
                session.Shell()
                session.Wait()
            }
            

            【讨论】:

            • 你能否也解释一下代码是如何解决问题的,以便将来的读者受益#
            • 不回答问题。
            猜你喜欢
            • 2013-11-24
            • 2015-09-18
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2015-05-11
            相关资源
            最近更新 更多