Start interactive ssh session using go

Viewed 781

I have this "sort of" working, but here is what is missing:

  1. control keys do not work. "ctrl-c" kills the entire go script, "ctrl-r" does not work as expected, etc.
  2. up and down arrows print ^[[A^[[D^[[C

The code I have below I got from this package: https://github.com/nanobox-io/golang-ssh I tested using that package separately and it works perfectly. I can't see what that package is doing that isn't in my script. I've also included the code I use to create a tunnel to the host, in case that is relevant.

Here is the example code. Please disregard the use of panic, it makes it easier for me to debug this new code while I am working on it. I would not intend this code in its current state to be used in a production environment.

func Start(hostName string) {
    var (
        termWidth, termHeight = 80, 24
    )

    jumpAddr := "..."
    hostAddr := "..."

    jumpConfig := ssh.ClientConfig{
        // ...
    }

    hostConfig := ssh.ClientConfig{
        // ...
    }

    jumpConn, err := ssh.Dial("tcp", jumpAddr+":22", &jumpConfig)
    ifpanic(err)

    hostConn, err := jumpConn.Dial("tcp", hostAddr+":22")
    ifpanic(err)

    hostSSHConn, chans, reqs, err := ssh.NewClientConn(hostConn, hostName+":22", &hostConfig)
    ifpanic(err)

    hostClient := ssh.NewClient(hostSSHConn, chans, reqs)
    session, err := hostClient.NewSession()
    ifpanic(err)

    session.Stdout = os.Stdout
    session.Stderr = os.Stderr
    session.Stdin = os.Stdin

    modes := ssh.TerminalModes{
        ssh.ECHO: 1,
    }

    fd := os.Stdin.Fd()

    oldState, err := term.MakeRaw(fd)
    ifpanic(err)

    defer ifpanic(term.RestoreTerminal(fd, oldState))

    winsize, err := term.GetWinsize(fd)
    if err == nil {
        termWidth = int(winsize.Width)
        termHeight = int(winsize.Height)
    }

    if err := session.RequestPty("xterm", termHeight, termWidth, modes); err != nil {
        panic(err)
    }

    if err := session.Shell(); err != nil {
        panic(err)
    }

    go monWinCh(session, os.Stdout.Fd())

    if err := session.Wait(); err != nil {
        panic(err)
    }
}

func ifpanic(err error) {
    if err != nil {
        panic(err)
    }
}

func monWinCh(session *ssh.Session, fd uintptr) {
    sigs := make(chan os.Signal, 1)

    signal.Notify(sigs, syscall.SIGWINCH)
    defer signal.Stop(sigs)

    for range sigs {
        if _, err := session.SendRequest("window-change", false, termSize(fd)); err != nil {
            panic(err)
        }
    }
}

func termSize(fd uintptr) []byte {
    size := make([]byte, 16)

    winsize, err := term.GetWinsize(fd)
    if err != nil {
        binary.BigEndian.PutUint32(size, uint32(80))
        binary.BigEndian.PutUint32(size[4:], uint32(24))
        return size
    }

    binary.BigEndian.PutUint32(size, uint32(winsize.Width))
    binary.BigEndian.PutUint32(size[4:], uint32(winsize.Height))

    return size
}

1 Answers

You can capture OS signals using a channel, then redirect them to the SSH session:

import (
    "os"
    "os/signal"
    "syscall"
)
//...

sigChannel := make(chan os.Signal)
signal.Notify(sigChannel, syscall.SIGINT, syscall.SIGTERM)
go func() {
    for {
        sig, ok := <-sigChannel
        if !ok {
            break
        }

        switch sig {
        case syscall.SIGINT:
            session.Signal(ssh.SIGINT)
        case syscall.SIGTERM:
            session.Signal(ssh.SIGTERM)
        }
    }
}()

Note: don't forget to close the signal channel properly so your goroutine can exit gracefully.

This code doesn't capture every signals, but you can find a list of available signals here, and pick the ones you want to handle.

Related