Golang ssh - how to run multiple commands on the s

2019-03-11 08:58发布

问题:

I'm trying to run multiple commands through ssh but seems that Session.Run allows only one command per session ( unless I'm wrong). I'm wondering how can I bypass this limitation and reuse the session or send a sequence of commands. The reason is that I need to run sudo su within the same session with the next command ( sh /usr/bin/myscript.sh )

回答1:

While for your specific problem, you can easily run sudo /path/to/script.sh, it shock me that there wasn't a simple way to run multiple commands on the same session, so I came up with a bit of a 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()
}

If your shell prompt doesn't end with $ ($ followed by a space), this will deadlock, hence why it's a hack.



回答2:

NewSession is a method of a connection. You don't need to create a new connection each time. A Session seems to be what this library calls a channel for the client, and many channels are multiplexed in a single connection. Hence:

func executeCmd(cmd []string, hostname string, config *ssh.ClientConfig) string {
conn, err := ssh.Dial("tcp", hostname+":22", 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()

}

So you open a new session(channel) and you run command within the existing ssh connection but with a new session(channel) each time.



回答3:

Session.Shell allows for more than one command to be run, by passing your commands in via session.StdinPipe().

Be aware that using this approach will make your life more complicated; instead of having a one-shot function call that runs the command and collects the output once it's complete, you'll need to manage your input buffer (don't forget a \n at the end of a command), wait for output to actually come back from the SSH server, then deal with that output appropriately (if you had multiple commands in flight and want to know what output belongs to what input, you'll need to have a plan to figure that out).

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.


回答4:

You can use a small trick: sh -c 'cmd1&&cmd2&&cmd3&&cmd4&&etc..'

This is a single command, the actual commands are passed as argument to the shell which will execute them. This is how Docker handles multiple commands.



回答5:

This works for me.

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:

Really liked OneOfOne's answer which inspired me with a more generalized solution to taken a variable that could match the tail of the read bytes and break the blocking read (also no need to fork two extra threads for blocking read and writes). The known limitation is (as in the original solution) if the matching string comes after 64 * 1024 bytes, then this code will spin forever.

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

i spent several days and that answer inspires me to try about using sdtin to run multiple commands, finally succeed. and i want to say i dont know golang at all , hence it may be redundant ,but the code works.

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())
}

its function is the same asterminal operation

here is the whole code:

/* 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:

The following code works for me.

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()
}


标签: ssh go