Crypto ssh how to read the stdout pipe end?

package main

import (
	"flag"
	"fmt"
	"golang.org/x/crypto/ssh"
	"io"
	"log"
	"strings"
	"time"
)

func readBuffForString(whattoexpect string, sshOut io.Reader, buffRead chan<- string) {
	buf := make([]byte, 1000)
	waitingString := ""
	for {
		n, err := sshOut.Read(buf) //this reads the ssh terminal
		if err != nil && err != io.EOF{
			fmt.Println(err)
			break
		}
		if err == io.EOF || n == 0 {
			break
		}
		fmt.Println(string(buf[:n]))
		waitingString += string(buf[:n])
	}
	//n, err := sshOut.Read(buf) //this reads the ssh terminal
	//waitingString := ""
	//if err == nil {
	//	waitingString = string(buf[:n])
	//}
	//for (err == nil) && (!strings.Contains(waitingString, whattoexpect)) {
	//	n, err = sshOut.Read(buf)
	//	waitingString += string(buf[:n])
	//	//fmt.Println(waitingString) //uncommenting this might help you debug if you are coming into errors with timeouts when correct details entered

	//}
	buffRead <- waitingString
}
func readBuff(whattoexpect string, sshOut io.Reader, timeoutSeconds int) string {
	ch := make(chan string)
	go func(whattoexpect string, sshOut io.Reader) {
		buffRead := make(chan string)
		go readBuffForString(whattoexpect, sshOut, buffRead)
		select {
		case ret := <-buffRead:
			ch <- ret
		case <-time.After(time.Duration(timeoutSeconds) * time.Second):
			handleError(fmt.Errorf("%d", timeoutSeconds), true, "Waiting for \""+whattoexpect+"\" took longer than %s seconds, perhaps you've entered incorrect details?")
		}
	}(whattoexpect, sshOut)
	return <-ch
}
func writeBuff(command string, sshIn io.WriteCloser) (int, error) {
	returnCode, err := sshIn.Write([]byte(command + "\r"))
	return returnCode, err
}
func handleError(e error, fatal bool, customMessage ...string) {
	var errorMessage string
	if e != nil {
		if len(customMessage) > 0 {
			errorMessage = strings.Join(customMessage, " ")
		} else {
			errorMessage = "%s"
		}
		if fatal == true {
			log.Fatalf(errorMessage, e)
		} else {
			log.Print(errorMessage, e)
		}
	}
}
func main() {
	flag.Parse()

	sshConfig := &ssh.ClientConfig{
		User: "admin",
		Auth: []ssh.AuthMethod{
			ssh.Password("r00ttest"),
		},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
	}

	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
	}
	connection, err := ssh.Dial("tcp", "172.16.240.189:22", sshConfig)
	if err != nil {
		log.Fatalf("Failed to dial: %s", err)
	}
	session, err := connection.NewSession()
	handleError(err, true, "Failed to create session: %s")
	sshOut, err := session.StdoutPipe()
	handleError(err, true, "Unable to setup stdin for session: %v")
	sshIn, err := session.StdinPipe()
	handleError(err, true, "Unable to setup stdout for session: %v")
	//sshErr, err := session.StderrPipe()
	//handleError(err, true, "Unable to setup stderr for session: %v")
	if err := session.RequestPty("xterm", 0, 200, modes); err != nil {
		session.Close()
		handleError(err, true, "request for pseudo terminal failed: %s")
	}
	if err := session.Shell(); err != nil {
		session.Close()
		handleError(err, true, "request for shell failed: %s")
	}
	fmt.Print(readBuff(">", sshOut, 2))
	if _, err := writeBuff("configure", sshIn); err != nil {
		handleError(err, true, "Failed to run: %s")
	}
	fmt.Println(readBuff("#", sshOut, 2))
	if _, err := writeBuff("set system host-name fuckme", sshIn); err != nil {
		handleError(err, true, "Failed to run: %s")
	}
	fmt.Println(readBuff("#", sshOut, 2))
	//fmt.Print(readBuff("", sshErr, 2))
	if _, err := writeBuff("commit", sshIn); err != nil {
		handleError(err, true, "Failed to run: %s")
	}
	fmt.Println(readBuff("#", sshOut, 20))
	if _, err := writeBuff("show system", sshIn); err != nil {
		handleError(err, true, "Failed to run: %s")
	}
	fmt.Println(readBuff("[edit]", sshOut, 20))
	session.Close()
}

in the func readBuffForString, how to get the buffer end like io.EOF ?
the commented code is not a elegant way to do that

strings.Contains(waitingString, whattoexpect)
1 Like

I am a bit confused on what you are trying to achieve, but if stdout is not writable, you get an error when you write to it. (Not EOF, as that happens only when reading.)

In other words, check the return from your fmt.Println.

Also, I think you can use ioutil.ReadAll somewhere there, but again, I didn’t try to follow the code carefully.

i am using crypto/ssh to do interactive things. Those things like change the host-name of juniper network devices always need serval commands. first, you need enter into configuration mode, then set the host name you want in the same session. So, session.Run(cmd) didn’t work for those things.

I did some google, session.Shell() would be the appropriate way to do that.
But, for each command, i need to read the stdout separately. That’s why i need to know how to check the stdout end.

1 Like

“End” in Go terms is defined as the remote side closing the connection.

What you want to do is consume output line by line until you receive what looks like a prompt.

Something like bufio.Reader.ReadString, or bufio.Scanner.Scan.

1 Like

But there is no certain pattern for the prompt. Something else in the output could fit with the prompt pattern.

1 Like

minimal code for this question

package main

import (
	"fmt"
	cssh "golang.org/x/crypto/ssh"
	"io"
	"time"
)

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

func readBuffForString(sshOut io.Reader) string {
	buf := make([]byte, 1000)
	n, err := sshOut.Read(buf) //this reads the ssh terminal
	waitingString := ""
	if err == nil {
		for _, v := range buf[:n] {
			fmt.Printf("%c", v)
		}
		waitingString = string(buf[:n])
	}
	for err == nil {
		// this loop will not end!!
		n, err = sshOut.Read(buf)
		waitingString += string(buf[:n])
		for _, v := range buf[:n] {
			fmt.Printf("%c", v)
		}
		if err != nil {
			fmt.Println(err)
		}
	}
	return waitingString
}

func write(cmd string, sshIn io.WriteCloser) {
	_, err := sshIn.Write([]byte(cmd + "\r"))
	handleError(err)
}

func main() {
	// create a new connection
	conn, err := cssh.Dial("tcp", "172.16.240.189:22", &cssh.ClientConfig{
		User:            "admin",
		Auth:            []cssh.AuthMethod{cssh.Password("r00ttest")},
		HostKeyCallback: cssh.InsecureIgnoreHostKey(),
		Timeout:         5 * time.Second,
	})

	if err != nil {
		fmt.Println(err)
	}
	session, err := conn.NewSession()
	handleError(err)
	sshOut, err := session.StdoutPipe()
	handleError(err)
	sshIn, err := session.StdinPipe()

	err = session.Shell()
	handleError(err)

	write("configure", sshIn)
	readBuffForString(sshOut)

	session.Close()
	conn.Close()
}
1 Like

Yup, them’s the breaks of writing expect(1)

More seriously, don’t get distracted by trying to make your program act like a human typing on a keyboard. Instead, make your program send the input that you cisco router expects.

Sorry, what’s that mean?

Yup, them’s the breaks of writing expect(1)

More seriously, don’t get distracted by trying to make your program act like a human typing on a keyboard. Instead, make your program send the input that you cisco router expects.

I’m trying to. But i need run several commands in a single session and get the output separately.

StdinPipe.Write(cmd1)
StdoutPipe.Read()
StdinPipe.Write(cmd2)
StdoutPipe.Read()
.......

Maybe ssh shell is built for human and i not friendly with a program, i think

You’re trying to write a tool to scrape network switches, right?

Yep.
I found a way to deal with it.

var (
    // admin@localhost# $
    // admin@localhost> $
    // localhost> $
    // localhost# $
    // the $ means the end of line
    prompt = regexp.MustCompile(".*@?.*(#|>) $")
)

// check if the string is a prompt
func check(s string) bool {
    m := prompt.FindStringSubmatch(s)
    // return true if it is
    return m != nil
}

func readBuffForString(sshOut io.Reader, buffRead chan string) {
    buf := make([]byte, 1000)
    waitingString := ""
    for {
        n, err := sshOut.Read(buf) //this reads the ssh terminal
        if err != nil {
            // someting wrong
            break
        }
        // for every line
        current := string(buf[:n])
        if check(current) {
            // ignore prompt and break
            fmt.Print(current)
            break
        }
        // add current line to result string
        waitingString += current

    }
    fmt.Println(waitingString)
    buffRead <- waitingString
}

But it’s not perfect. If the line which contains normal output match the prompt pattern, it will get wrong result.

1 Like

Here’s what you need to do

  1. You’re using a single ssh connection to handle multiple requests and responses. This means you have decided not to use io.EOF as a signal that the remove end has finished sending your data. Instead you need to decide on your framing.
  2. The most reasonable framing will be to scan the output from the remote end for something that looks like a prompt. This is what humans do when they interact with a CLI, we type a command, read some output and then when we decide the program has finished, we type some more. Usually this is because somethings that looks like our prompt is output after the program has completed. (There are other ways to test, but none of them apply for the faux environment of a script).
    2a. Really important you cannot use time for this. You cannot say “i’ll wait for n seconds for the response”, sometimes things will be slow and you’ll get truncated output, other times things will be fast, but your program will wait pessimistically.
  3. This is going to be a messy problem because it looks like you don’t control what the prompt is – but as this is a shell you can redefine the prompt to be something unique; google the $PS1 variable.
1 Like

Thank you Dave. You helped me a lot!:grinning:
I will try $PS1 variable.

Also, as this is a shell, you can run a shell script and get all the output you need in one go (assuming that you don’t need the output of one command as the input of another).

Got it. I could combine the commands with “\r” and write to StdinPipe for once. But it’s not my case, i need to check the output of each command for fast fail

If you write them inside a shell script, use -e for fast fail, then pipe that script to bash on the remote side you won’t need a remote shell.

As a side effect you know when a command has failed because the remote command will cause ssh.Run to return an error.

Badly, the commands is not bash/sh commands. They are running in network device configure mode, maybe a program.

Then you’re stuck parsing the output one command at a time, sorry.

Yep :slight_smile: