Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Golang ssh - how to run multiple commands on the same session?

Tags:

ssh

go

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 )

like image 849
hey Avatar asked Jun 26 '14 21:06

hey


4 Answers

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.
like image 173
Shane Madden Avatar answered Nov 19 '22 23:11

Shane Madden


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+":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()
}

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

like image 35
egorka Avatar answered Nov 19 '22 22:11

egorka


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.

like image 11
OneOfOne Avatar answered Nov 19 '22 23:11

OneOfOne


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.

like image 5
creack Avatar answered Nov 19 '22 21:11

creack