reesericci
reesericci

Reputation: 132

Wait for command completion and capture output in interactive shell with Go crypto/ssh

After piping in a command to stdin after running session.RequestPty() and session.Shell(), there's seemingly no way to tell when that command is either done executing so I can try to parse stdout myself, or any way to directly get the output from the command. Additionally, I would like to be able to tell if a command is waiting for an interaction like pushing Y or entering a password.

Relevant code:

func connectSSH(username, host, identity, proxy string, resolve js.Value, reject js.Value) error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    log.Println("Dialing WebSocket...")
    dialed, _, err := websocket.Dial(ctx, proxy, nil)
    if err != nil {
        log.Printf("Failed to dial WebSocket: %v", err)
        return err
    }

    dialed.Ping(ctx)
    log.Println("WebSocket connection dialed")

    c := websocket.NetConn(context.Background(), dialed, websocket.MessageBinary)
    log.Println("NetConn created")

    signer, err := ssh.ParsePrivateKey([]byte(identity))
    if err != nil {
        reject.Invoke(err.Error())
        return err
    }

    ncc, chans, reqs, err := ssh.NewClientConn(c, host, &ssh.ClientConfig{
        User: username,
        Auth: []ssh.AuthMethod{
            ssh.PublicKeys(signer),
        },
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    })
    if err != nil {
        reject.Invoke(err.Error())
        return err
    }

    client := ssh.NewClient(ncc, chans, reqs)

    session, err := client.NewSession()
    if err != nil {
        reject.Invoke(err.Error())
        return err
    }

    stdin, err := session.StdinPipe()
    if err != nil {
        reject.Invoke(err.Error())
        return err
    }

    var stdoutBuf bytes.Buffer
    session.Stdout = &stdoutBuf

    stderr, err := session.StderrPipe()
    if err != nil {
        reject.Invoke(err.Error())
        return err
    }

    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
    }
    // Request pseudo terminal
    if err := session.RequestPty("xterm", 40, 80, modes); err != nil {
        log.Fatal("request for pseudo terminal failed: ", err)
        reject.Invoke(err.Error())
        return err
    }

    // Execute the user's login shell
    if err := session.Shell(); err != nil {
        reject.Invoke(err.Error())
        return err
    }

    activeConnection = Connection{
        nc:      c,
        client:  client,
        wc:      dialed,
        session: session,
        stdin:   &stdin,
        stdout:  &stdoutBuf,
        stderr:  &stderr,
    }

    resolve.Invoke()
    return nil
}

func stdout(resolve, reject js.Value) {
    if activeConnection == (Connection{}) {
            reject.Invoke("no active connection")
            return
    }

    resolve.Invoke(stripansi.Strip(activeConnection.stdout.String()))
}

func execute(command string, resolve, reject js.Value) {
    if activeConnection == (Connection{}) {
        reject.Invoke("no active connection")
        return
    }

    n, err := (*activeConnection.stdin).Write([]byte(command + "\n"))

    if err != nil {
        log.Printf("Error writing command to stdin: %v", err)
        reject.Invoke(err.Error())
        return
    }

    log.Printf("Wrote %d bytes to stdin", n)

    if closer, ok := (*activeConnection.stdin).(io.Closer); ok {
        err = closer.Close()
        if err != nil {
            log.Printf("Error flushing stdin: %v", err)
            reject.Invoke(err.Error())
            return
        }
    }
    resolve.Invoke(n)
}

Upvotes: 0

Views: 90

Answers (0)

Related Questions