I have this piece of go code, mostly taken from here:
fmt.Println("Please enter your role: ")
fmt.Scanf("%s", &roleName)
flag.StringVar(&startURL, "start-url", "", "AWS SSO Start URL")
flag.StringVar(&accountID, "account-id", "", "AWS Account ID to fetch credentials for")
flag.Parse()
if startURL == "" || accountID == "" || roleName == "" {
flag.Usage()
os.Exit(1)
}
cfg := aws.Config{Region: "eu-west-1"}
// create sso oidc client to trigger login flow
ssooidcClient := ssooidc.NewFromConfig(cfg)
// register your client which is triggering the login flow
register, err := ssooidcClient.RegisterClient(context.TODO(), &ssooidc.RegisterClientInput{
ClientName: aws.String("sample-client-name"),
ClientType: aws.String("public"),
Scopes: []string{"sso-portal:*"},
})
if err != nil {
fmt.Println(err)
}
// authorize your device using the client registration response
deviceAuth, err := ssooidcClient.StartDeviceAuthorization(context.TODO(), &ssooidc.StartDeviceAuthorizationInput{
ClientId: register.ClientId,
ClientSecret: register.ClientSecret,
StartUrl: aws.String(startURL),
})
if err != nil {
fmt.Println(err)
}
// trigger OIDC login. open browser to login. close tab once login is done. press enter to continue
url := aws.ToString(deviceAuth.VerificationUriComplete)
fmt.Printf("If browser is not opened automatically, please open link:\n%v\n", url)
err = browser.OpenURL(url)
if err != nil {
fmt.Println(err)
}
fmt.Println("Press ENTER key once login is done")
// These lines get skipped on Windows
// also tried bufio.NewReader(os.Stdin).ReadBytes('\n')
// and fmt.Scanf("%s", &test)
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
if scanner.Err() != nil {
fmt.Println()
}
token, err := ssooidcClient.CreateToken(context.TODO(), &ssooidc.CreateTokenInput{
ClientId: register.ClientId,
ClientSecret: register.ClientSecret,
DeviceCode: deviceAuth.DeviceCode,
GrantType: aws.String("urn:ietf:params:oauth:grant-type:device_code"),
})
While on MacOS, the program waits for user input and hence the SSO login works perfectly, on Windows it gets skipped and the user does not have time to accept the login on AWS side, so the program fails. Moreover, the first prompt that asks for the users' role works correctly, so I really don't understand the second one just gets skipped ?
I use these commands to build the binary, from a MacOS machine:
GOOS=darwin go build
GOOS=windows go build
The call fmt.Scanf("%s", &roleName) returns after reading the first whitespace character after the token.
The line terminator on Windows is \r\n. The fmt.Scanf call returns after reading the \r. The \n remains in stdin. The later call to scanner.Scan() reads the remaining \n in stdin and returns immediately.
The line terminator on other systems is \n. The call fmt.Scanf returns after reading the entire line terminator. The call to scanner.Scan() waits for the user to type another line terminator.
One fix is to use the scanner for all input:
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("Please enter your role: ")
if !scanner.Scan() {
// handle EOF
}
roleName = strings.TrimSpace(scanner.Text())
…
fmt.Println("Press ENTER key once login is done")
scanner.Scan()
…
Related
I have a CLI written in Go using cobra where one of the commands is to remove a specific username from a list of usernames in a text file. For example, I have a list of usernames in a file,
user1
user2
user3
user4
user5
Here's my Go code inside my cobra command,
Run: func(cmd *cobra.Command, args []string) {
userBlacklist, _ := cmd.Flags().GetString("blacklist-file-path")
userName, _ := cmd.Flags().GetString("user-name")
if info, err := os.Stat(userBlacklist); err == nil && !info.IsDir() {
data, err := ioutil.ReadFile(userBlacklist)
if err != nil {
logger.Fatal("Failed to read in blacklist: %s", err)
}
lines := strings.Split(string(data), "\n")
file, err := os.Create(userBlacklist)
if err != nil {
logger.Fatal("Could not overwrite blacklist file %s\n", err)
}
defer file.Close()
for _, line := range lines {
if line != userName{
if _, err := file.WriteString(line + "\n"); err != nil {
logger.Fatal("Failed to write user to open blacklist file %s\n", err)
}
}
}
} else {
if err != nil {
logger.Fatal("Error stating blacklist %s\n", err )
} else {
logger.Fatal("--blacklist-file-path is a directory, not a file\n")
}
}
},
When I run it with, say, userName set to "user3" the resulting output file does indeed not have user3 in it. However it does have a blank line of "" at the end of the file. If I then run the command asking it to remove another user, it will remove that user too but the resulting file will now have two blank lines at the end, etc., etc.
I can prevent the blank lines by changing line 22 to be,
if !(line == userName || line == "") {
I don't understand how this is happening. Doesn't seem sensible? Where are the blank lines coming from?
I am on Ubuntu 20.04, Go version go1.16.5 linux/amd64, and github.com/spf13/cobra v1.5.0
Thanks in advance for any insight.
The file ends with a \n. The last element of strings.Split(string(data), "\n") is the empty string. The program adds the empty string to the end of the file when processing the last element.
Fix by using bufio.Scanner to parse the lines:
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := scanner.Text()
if line != userName {
if _, err := file.WriteString(line + "\n"); err != nil {
logger.Fatal("Failed to write user to open blacklist file %s\n", err)
}
}
}
When you write the line to the file, you are adding a newline character:
file.WriteString(line + "\n")
That's where the empty newlines at the end of the file are coming from.
My code is supposed to SSH to a remote-host (let’s say Routers) and run multiple commands on the remote-host and return the outputs.
The code attached is simplified and has three parts:
Main function: Reads list of commands and then by using the ExecCommands function dials/ssh to a remote-host to execute the commands.
ExecCommands function takes the remote-host IP, list of commands and SSH ClientConfig that is used for SSH. Then it dials to the IP and run the commands one-by-one. At the end, returns the output of all commands in only one string
InsecureClientConfig function that actually doesn’t do much except creating a SSH ClientConfig which is used for ExecCommands function
This program works well when I just want to apply some commands or config and save the wholes result. I mean ExecCommands takes the bunch of commands, push all of them to the remote-host and returns (or saves) the whole output of applied commands in one string as output.
Problem:
I cannot process the output of each command individually. For example, assume that I apply CMD1, CMD2, CMD3, … to the remote-host#1 by using ExecCommands function. Since it gives me back the whole output in one string, it is hard to find which output belongs to which CMD
Goal:
Modify or re-design ExecCommands function to the way that it provides separate output for each command it applies. It means if for remote-host#1 it applies 10 commands, I should have 10 separate strings as output.
Conditions/Restrictions:
I can not create any extra session for commands and must apply all commands in the first SSH session I created, i.e. cannot create multiple Sessions and use Run, Shell, Output, Start function in SSH package
No re-authentication is allowed. For example, I have only a single one-time-password that can be used for all remote-hosts.
Remote hosts don't support "echo" like commands similar to what you have in Linux
The remote-hosts dont’s support any type of APIs
Points:
Main focus is the function ExecCommands. I put a simplified version of the whole code to give an idea
I am using stdout, err := session.StdoutPipe() to run multiple commands which means -as pipe - it's Reader only is possible to be read when the job is done.
An option is to use Session.Stdout and Session.Stdin inside of the for loop in ExecCommands function. Tried but was not successful.
Code:
package main
import (
"errors"
"fmt"
"io/ioutil"
"log"
"time"
"golang.org/x/crypto/ssh"
)
func main() {
// List of the commands should be sent to the devices
listCMDs := []string{
"set cli op-command-xml-output on",
"test routing fib-lookup virtual-router default ip 1.1.1.1",
"test routing fib-lookup virtual-router default ip 2.2.2.2",
"show interface ethernet1/1",
"show interface ethernet1/2",
"test security-policy-match protocol 6 source 1.1.1.1 destination 2.2.2.2 destination-port 443 from ZONE1 to ZONE2",
"test security-policy-match protocol 6 source 10.0.0.1 destination 10.0.2.1 destination-port 443 from ZONE1 to ZONE2",
"exit",
}
sshconfig := InsecureClientConfig("admin", "admin")
s, err := ExecCommands("192.168.1.250", listCMDs, sshconfig)
fmt.Println(s, err)
}
// ExecCommands ...
func ExecCommands(ipAddr string, commands []string, sshconfig *ssh.ClientConfig) (string, error) {
// Gets IP, credentials and config/commands, SSH Config (Timeout, Ciphers, ...) and returns
// output of the device as "string" and an error. If error == nil, means program was able to SSH with no issue
// Creating outerr as Output Error.
outerr := errors.New("nil")
outerr = nil
// Creating Output as String
var outputStr string
// Dial to the remote-host
client, err := ssh.Dial("tcp", ipAddr+":22", sshconfig)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Create sesssion
session, err := client.NewSession()
if err != nil {
log.Fatal(err)
}
defer session.Close()
// StdinPipee() returns a pipe that will be connected to the remote command's standard input when the command starts.
// StdoutPipe() returns a pipe that will be connected to the remote command's standard output when the command starts.
stdin, err := session.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := session.StdoutPipe()
if err != nil {
log.Fatal(err)
}
// Start remote shell
err = session.Shell()
if err != nil {
log.Fatal(err)
}
// Send the commands to the remotehost one by one.
for _, cmd := range commands {
_, err := stdin.Write([]byte(cmd + "\n"))
if err != nil {
log.Fatal(err)
}
}
// Wait for session to finish
err = session.Wait()
if err != nil {
log.Fatal(err)
}
strByte, _ := ioutil.ReadAll(stdout)
outputStr = string(strByte)
return outputStr, outerr
}
// InsecureClientConfig ...
func InsecureClientConfig(userStr, passStr string) *ssh.ClientConfig {
SSHconfig := &ssh.ClientConfig{
User: userStr,
Timeout: 5 * time.Second,
Auth: []ssh.AuthMethod{ssh.Password(passStr)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Config: ssh.Config{
Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-cbc", "aes192-cbc",
"aes256-cbc", "3des-cbc", "des-cbc"},
KeyExchanges: []string{"diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha1",
"diffie-hellman-group14-sha1"},
},
}
return SSHconfig
}
This works properly:
package main
import (
"bufio"
"errors"
"fmt"
"log"
"time"
"golang.org/x/crypto/ssh"
)
func main() {
// List of the commands should be sent to the devices
listCMDs := []string{
"set cli op-command-xml-output on\n",
"test routing fib-lookup virtual-router default ip 1.1.1.1\n",
"test routing fib-lookup virtual-router default ip 2.2.2.2\n",
"show interface ethernet1/1\n",
"show interface ethernet1/2\n",
"test security-policy-match protocol 6 source 1.1.1.1 destination 2.2.2.2 destination-port 443 from ZONE1 to ZONE2\n",
"test security-policy-match protocol 6 source 10.0.0.1 destination 10.0.2.1 destination-port 443 from ZONE1 to ZONE2\n",
"exit",
}
sshconfig := InsecureClientConfig("admin", "Ghazanfar1!")
s, _ := ExecCommands("192.168.1.249", listCMDs, sshconfig)
for _, item := range s {
fmt.Println(item)
fmt.Println("-------------------------------")
}
}
// ExecCommands ...
func ExecCommands(ipAddr string, commands []string, sshconfig *ssh.ClientConfig) ([]string, error) {
// Gets IP, credentials and config/commands, SSH Config (Timeout, Ciphers, ...) and returns
// output of the device as "string" and an error. If error == nil, means program was able to SSH with no issue
// Creating outerr as Output Error.
outerr := errors.New("nil")
outerr = nil
// Creating Output as String
var outputStr []string
var strTmp string
// Dial to the remote-host
client, err := ssh.Dial("tcp", ipAddr+":22", sshconfig)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Create sesssion
session, err := client.NewSession()
if err != nil {
log.Fatal(err)
}
defer session.Close()
// StdinPipee() returns a pipe that will be connected to the remote command's standard input when the command starts.
// StdoutPipe() returns a pipe that will be connected to the remote command's standard output when the command starts.
stdin, err := session.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := session.StdoutPipe()
if err != nil {
log.Fatal(err)
}
// Start remote shell
err = session.Shell()
if err != nil {
log.Fatal(err)
}
stdinLines := make(chan string)
go func() {
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
stdinLines <- scanner.Text()
}
if err := scanner.Err(); err != nil {
log.Printf("scanner failed: %v", err)
}
close(stdinLines)
}()
// Send the commands to the remotehost one by one.
for i, cmd := range commands {
_, err := stdin.Write([]byte(cmd + "\n"))
if err != nil {
log.Fatal(err)
}
if i == len(commands)-1 {
_ = stdin.Close() // send eof
}
// wait for command to complete
// we'll assume the moment we've gone 1 secs w/o any output that our command is done
timer := time.NewTimer(0)
InputLoop:
for {
timer.Reset(time.Second)
select {
case line, ok := <-stdinLines:
if !ok {
log.Println("Finished processing")
break InputLoop
}
strTmp += line
strTmp += "\n"
case <-timer.C:
break InputLoop
}
}
outputStr = append(outputStr, strTmp)
//log.Printf("Finished processing %v\n", cmd)
strTmp = ""
}
// Wait for session to finish
err = session.Wait()
if err != nil {
log.Fatal(err)
}
return outputStr, outerr
}
// InsecureClientConfig ...
func InsecureClientConfig(userStr, passStr string) *ssh.ClientConfig {
SSHconfig := &ssh.ClientConfig{
User: userStr,
Timeout: 5 * time.Second,
Auth: []ssh.AuthMethod{ssh.Password(passStr)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Config: ssh.Config{
Ciphers: []string{"aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-cbc", "aes192-cbc",
"aes256-cbc", "3des-cbc", "des-cbc"},
KeyExchanges: []string{"diffie-hellman-group1-sha1",
"diffie-hellman-group-exchange-sha1",
"diffie-hellman-group14-sha1"},
},
}
return SSHconfig
}
Since you have limited number of commands to run on special hardwares and you know the pattern of the each command's output, you may use strings.Split or regexp to split the output.
And if you do not have echo command, but know any command with fast response with unique output pattern, then you may replace it with echo command in the following example (number 2).
Since a session only accepts one call to Run, Start, Shell, Output, or CombinedOutput, and you do not want to start a new session per command:
The key is to use a strings.Builder and empty it using sb.Reset() befor sending the command, and using io.Copy to copy concurrently the session's stdout into strings.Builder (assuming you do not need session's stderr):
sb := new(strings.Builder)
go io.Copy(sb, stdout)
This works if you know how much to wait for each command (tested):
sb := new(strings.Builder)
go io.Copy(sb, stdout)
commands := []string{"uname -a", "sleep 1", "pwd", "whoami", "exit"}
wait := []time.Duration{10, 1200, 20, 10, 10} // * time.Millisecond
ans := []string{}
time.Sleep(10 * time.Millisecond) // wait for the ssh greetings
// Send the commands to the remotehost one by one.
for i, cmd := range commands {
sb.Reset()
fmt.Println("*** command:\t", cmd)
_, err := stdin.Write([]byte(cmd + "\n"))
if err != nil {
log.Fatal(err)
}
time.Sleep(wait[i] * time.Millisecond) // wait for the command to finish
s := sb.String()
fmt.Println("*** response:\t", s)
ans = append(ans, s)
}
Using string delimiter and strings.Split (Note: You may replace echo with any fast command with known output pattern):
sb := new(strings.Builder)
go io.Copy(sb, stdout)
commands := []string{"uname -a", "sleep 1", "pwd", "whoami"}
delim := "********--------========12345678"
for _, cmd := range commands {
_, err = stdin.Write([]byte("echo " + delim + "\n"))
if err != nil {
log.Fatal(err)
}
_, err := stdin.Write([]byte(cmd + "\n"))
if err != nil {
log.Fatal(err)
}
}
_, err = stdin.Write([]byte("exit\n"))
if err != nil {
log.Fatal(err)
}
err = session.Wait() // Wait for session to exit
if err != nil {
log.Fatal(err)
}
ans := strings.Split(sb.String(), delim)
ans = ans[1:] // remove ssh greetings
Check this out: https://github.com/yahoo/vssh
You can set sessions to how many commands you need to run concurrently then send each command to remote host through run method and get the result individually!
I am writing a Go program to connect to a Cisco wireless controller to run multiple configuration commands on connected access points. I am able to connect to and send commands to the controller via Go's SSH package golang.com/x/crypto/ssh.
I need to a) only print the output of each command I send, not the controller's prompt or login prompts, and b) figure out how to send successive commands after a command whose output includes --More-- or (q)uit prompts.
Right now I have the os.Stdout assigned to the SSH session.Stdout, which is why I'm printing more than just the output of the commands I send. How would I manually control the output of session.Stdout? I know the session.Stdout is an io.Writer, but I don't know how to control what it writes to os.Stdout. Here is my code along with its output:
package main
import (
"bufio"
"fmt"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/terminal"
"log"
"os"
"time"
"io"
)
func SendCommand(in io.WriteCloser, cmd string) error {
if _, err := in.Write([]byte(cmd + "\n")); err != nil {
return err
}
return nil
}
func main() {
// Prompt for Username
fmt.Print("Username: ")
r := bufio.NewReader(os.Stdin)
username, err := r.ReadString('\n')
if err != nil {
log.Fatal(err)
}
// Prompt for password
fmt.Print("Password: ")
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
log.Fatal(err)
}
// Setup configuration for SSH client
config := &ssh.ClientConfig{
Timeout: time.Second * 5,
User: username,
Auth: []ssh.AuthMethod{
ssh.Password(string(password)),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
// Connect to the client
client, err := ssh.Dial("tcp", "host:22", config)
if err != nil {
log.Fatal(err)
}
defer client.Close()
// Create a session
session, err := client.NewSession()
if err != nil {
log.Fatal(err)
}
defer session.Close()
// Setup StdinPipe to send commands
stdin, err := session.StdinPipe()
if err != nil {
log.Fatal(err)
}
defer stdin.Close()
// Route session Stdout/Stderr to system Stdout/Stderr
session.Stdout = os.Stdout
session.Stderr = os.Stderr
// Start a shell
if err := session.Shell(); err != nil {
log.Fatal(err)
}
// Send username
if _, err := stdin.Write([]byte(username)); err != nil {
log.Fatal(err)
}
// Send password
SendCommand(stdin, string(password))
// Run configuration commands
SendCommand(stdin, "show ap summary")
SendCommand(stdin, "logout")
SendCommand(stdin, "N")
session.Wait()
}
Sample run:
$ go run main.go
Username: username
Password:
(Cisco Controller)
User: username
Password:****************
(Cisco Controller) >show ap summary
Number of APs.................................... 1
Global AP User Name.............................. its-wap
Global AP Dot1x User Name........................ Not Configured
AP Name Slots AP Model Ethernet MAC Location Country IP Address Clients DSE Location
------------------ ----- -------------------- ----------------- ---------------- ---------- --------------- -------- --------------
csc-ap3500 2 AIR-CAP3502I-A-K9 00:00:00:00:00:00 csc-TESTLAB-ap35 US 10.10.110.10 0 [0 ,0 ,0 ]
(Cisco Controller) >logout
The system has unsaved changes.
Would you like to save them now? (y/N) N
My next problem comes when the output of a command has one or more --More-- or (q)uit prompts and I try to send a successive command. For example, here is the output of my code when running show sysinfo followed by logout:
$ go run main.go
Username: username
Password:
(Cisco Controller)
User: username
Password:****************
(Cisco Controller) >show sysinfo
Manufacturer's Name.............................. Cisco Systems Inc.
Product Name..................................... Cisco Controller
Product Version.................................. 8.2.161.0
Bootloader Version............................... 1.0.20
Field Recovery Image Version..................... 7.6.101.1
Firmware Version................................. PIC 16.0
Build Type....................................... DATA + WPS
--More-- or (q)uit
Configured Country............................... US - United States
Operating Environment............................ Commercial (0 to 40 C)
Internal Temp Alarm Limits....................... 0 to 65 C
Internal Temperature............................. +23 C
External Temperature............................. +28 C
Fan Status....................................... 3900 rpm
# the "l" is missing in "logout"
(Cisco Controller) >ogout
Incorrect usage. Use the '?' or <TAB> key to list commands.
(Cisco Controller) >N
Incorrect usage. Use the '?' or <TAB> key to list commands.
# the program hangs here
(Cisco Controller) >
TL;DR How to manually control the output of an io.Writer and how to correctly handle sending commands in succession?
Any help is appreciated. Thanks in advance.
I need a way to read input from console twice (1 -from cat outupt, 2 - user inputs password), like this:
cat ./test_data/test.txt | app account import
My current code skips password input:
reader := bufio.NewReader(os.Stdin)
raw, err := ioutil.ReadAll(reader)
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
wlt, err := wallet.Deserialize(string(raw))
if err != nil {
return cli.NewExitError(err.Error(), 1)
}
fmt.Print("Enter password: ")
pass := ""
fmt.Fscanln(reader, &pass)
Also tried to read password with Scanln - doesn't works.
Note:
cat (and piping at all) can't be used with user input, as shell redirects inputs totally.
So the most simple solutions are:
to pass filename as argument
redirect manually app account import < ./test.txt
Read file and password separately (and don't show password), try this:
package main
import (
"fmt"
"io/ioutil"
"log"
"github.com/howeyc/gopass"
)
func main() {
b, err := ioutil.ReadFile("./test_data/test.txt") // just pass the file name
if err != nil {
fmt.Print(err)
}
fmt.Println(string(b))
fmt.Print("Password: ")
pass, err := gopass.GetPasswd()
if err != nil {
log.Fatalln(err)
}
fmt.Println(string(pass))
}
and go get github.com/howeyc/gopass first.
So I'm able to ssh into the machine, but i'm having trouble entering data into the prompt.
...
sshConfig := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.Password(password),
},
HostKeyCallback: KeyPrint,
}
connection, err := ssh.Dial("tcp", connStr, sshConfig)
if err != nil {
log.Fatalln(err)
}
session, err := connection.NewSession()
if err != nil {
log.Fatalln(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
}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
session.Close()
log.Fatalf("request for pseudo terminal failed: %s", err)
}
stdin, err := session.StdinPipe()
if err != nil {
log.Fatalf("Unable to setup stdin for session: %v", err)
}
go io.Copy(stdin, os.Stdin)
stdout, err := session.StdoutPipe()
if err != nil {
log.Fatalf("Unable to setup stdout for session: %v", err)
}
go io.Copy(os.Stdout, stdout)
stderr, err := session.StderrPipe()
if err != nil {
log.Fatalf("Unable to setup stderr for session: %v", err)
}
go io.Copy(os.Stderr, stderr)
// err = session.Run("1")
session.Run("") // running it allows me to interact with the remote machines terminal in my own terminal.. session.Start("") exits and session.Wait() doesn't display the Welcome screen that normally greats users, and the prompt doesn't appear.
stdin.Write([]byte("10000"))
os.Stdin.WriteString("110000")
// log.Fatalln(n, err)
// os.Stdin.WriteString("1")
// for {
// session.Run("1")
// go os.Stdin.WriteString("1")
// go stdin.Write([]byte("10000"))
// }
...
The above code snippet gets me into the machine and the machine's prompt is displayed on my screen as if I ssh'ed into manually. I can type in the shell... but i need to be able to have Go type in the shell for me. The prompt that I'm interacting with is a text based game so I can't just issue commands e.g no (ls, echo, grep, etc..) the only thing I'm allow to pass in are numbers. How do I send input to the ssh session? I've tried many ways and none of the input seems to be going through.
I'm also attaching a screenshot of the prompt, just incase the description above is confusion in trying to portray the type of session this is.
UPDATE:
I think I've found a way to send the data, at least once.
session, err := connection.NewSession()
if err != nil {
log.Fatalln(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
}
if err := session.RequestPty("xterm", 80, 40, modes); err != nil {
session.Close()
log.Fatalf("request for pseudo terminal failed: %s", err)
}
stdin, err := session.StdinPipe()
if err != nil {
log.Fatalf("Unable to setup stdin for session: %v", err)
}
go io.Copy(stdin, os.Stdin)
stdout, err := session.StdoutPipe()
if err != nil {
log.Fatalf("Unable to setup stdout for session: %v", err)
}
go io.Copy(os.Stdout, stdout)
stderr, err := session.StderrPipe()
if err != nil {
log.Fatalf("Unable to setup stderr for session: %v", err)
}
go io.Copy(os.Stderr, stderr)
go session.Start("")
for {
stdin.Write([]byte("10000000\n"))
break
}
session.Wait()
I start the session with go session.Start("") remember that there is no point is passing in command because all i'm doing is entering data in response to a prompt.
I then use session.Wait() at the end of a for loop...kinda like one does when using channels and waitgroups.. inside the for loop i send data with stdin.Write([]byte("10000000\n")) where the important thing to note is to add the \n delimiter to simulate hitting enter on the keyboard..
If there are any better ways to achieve what i'm trying to achieve please feel free. Next steps are to parse the stdout for a response and reply accordingly.
An empty Start will work, however within the ssh package, Start, Run, and Shell are all calls to, basically, the same thing. Start("cmd") executes a command within a shell, Run("cmd") is a simple call to Start("cmd") that then invokes Wait() for you (giving the feel of executing without concurrency), and Shell opens a shell (like Start), but without a command passed. It's six of one, half a dozen of the other, really, but using Shell() is probably the cleanest way to go about that.
Also, bear in mind that Start() and Shell() both leverage concurrency without the explicit invocation of "go". It might free an additional millisecond or so to invoke concurrency manually, but if that isn't of significance to you, then you should be able to drop that. The automatic concurrency of Start and Shell is the reason for the Wait() and Run("cmd") methods.
If you have no need to interpret your output (stdout/err), then you can map these without the pipe() call or io.Copy(), which is easier and more efficient. I did this in the example below, but bear in mind that if you do interpret the output, it's probably easier to work with the Pipe(). You can send multiple commands (or numbers) sequentially without reading for a prompt in most cases, but some things (like passwords prompts) clear the input buffer. If this happens for you, then you'll need to read the Stdout to find your prompt or leverage an expect tool like goexpect (https://github.com/google/goexpect). There are several expect-like packages for Go, but this one is from Google and (as of this posting) still fairly recently maintained.
StdinPipe() exposes a writeCloser that can be leveraged without io.Copy(), which should be more efficient.
Your for loop that writes to the StdinPipe() should allow you to enter several commands (or in your case, sets of numbers)... as an example, I have this reading commands (numbers, etc) from os.Args and iterating through them.
Lastly, you should probably add a session.Close()for healthy completion (you already have a call for errors). That said, this is what I would recommend (based on your last example):
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 {
session.Close()
log.Fatalf("request for pseudo terminal failed: %s", err)
}
defer session.Close()
stdin, err := session.StdinPipe()
if err != nil {
log.Fatalf("Unable to setup stdin for session: %v", err)
}
session.Stdout = os.Stdout
session.Stderr = os.Stderr
err = session.Shell()
if err != nil {
log.Fatalf("Unable to setup stdin for session: %v", err)
}
for _, cmd := range os.Args {
stdin.Write([]byte(cmd + "\n"))
}
session.Wait()
Oh, one more item to note is that the Wait() method relies on an unchecked channel that retrieves the exitStatus from your command, and this does hang on rare occasion (it is particularly problematic when connecting to cisco gear, but can happen with others as well). If you find that you encounter this (or you'd just like to be careful), you might want to wrap Wait() inside of some sort of timeout methodology such as by invoking Wait() with concurrency and reading the response through a channel that can be cased along with time.After() (I can provide an example, if that would be helpful).