GoLang SSH Commands with slashes seem to instantly fail - go

I'm currently writing an app that given a parameter, will run a command on a remote server. I'm using the /x/crypto/ssh package, everything seems to go smoothly if I use one liner commands like "who" or "ls", however, if I run a more complex command such as:
"grep SOMEDATA /var/log/logfile.log"
the program immediately exits and the command execution line with nothing more than "process exited with status 1", and I don't get anything else back.
If I check the history of the user I'm having it SSH into the remote system as, I do not see the command running at all.
Has anyone else run into this type of issue before? Here's a snippet of the code I'm using to execute this (sensitive data removed of course):
func returnData(w http.ResponseWriter, r *http.Request) {
var b bytes.Buffer
hostKey, err := getHostKey("SERVERNAME")
if err != nil {
log.Fatalln(err)
}
err = r.ParseForm()
if err != nil {
log.Fatalln(err)
}
config := &ssh.ClientConfig{
User: "USERNAME",
Auth: []ssh.AuthMethod{
ssh.Password("TESTPASS"),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
}
client, err := ssh.Dial("tcp", "SERVERNAME:22", config)
if err != nil {
log.Fatalln("Creating Client Failed: ", err)
}
session, err := client.NewSession()
if err != nil {
log.Fatalln("Creating new Session Failed: ", err)
}
session.Stdout = &b
inputData := r.Form["fname"][0]
cmdExecute := fmt.Sprintf(`sudo grep %v /var/log/logfile.log`, inputData)
log.Println(cmdExecute)
if err := session.Run(cmdExecute); err != nil {
log.Fatalln("Getting Data From session Failed: ", err)
log.Fatalln(b.String())
}
//log.Println(hostKey)
defer session.Close()

Related

Interact with a shell script running remotely via SSH

I am working an application written in golang,for which one of the capability will be to SSH into a device and execute a shell script there. With my current implementation,a normal script is getting executed.The problem lies in the execution of a script that requires user input: for e.g the script asks the user, can we proceed with the installation: and the user has to type in Y or N. This is the part where it is failing. Here is my implementation:
func main() {
cmdOutput,err := ExecuteScriptSSH()
if err != nil{
fmt.Printf("Error is %s",err)
}
fmt.Printf("Command Output is %s", cmdOutput)
}
func ExecuteScriptSSH() (string, error) {
script := "#!/bin/sh\n\n ls -l \n\n date"
params := make(map[string]interface{})
params["Username"] = "uname"
params["Password"] = "pwd"
params["IPAddress"] = "ip"
params["Port"] = "22"
connection := NewConnection(params)
client, err := connection.ConnectNonSecure()
if err != nil {
fmt.Printf("unable to Connect:%v ", err)
}
defer client.Close()
ss, err := client.NewSession()
if err != nil {
fmt.Printf("unable to create SSH session:%v ", err)
}
//Converting the Script to string to a shell script file by writing to it.
d1 := []byte(script)
err = os.WriteFile("/tmp/script.sh", d1, 0777)
if err != nil {
fmt.Printf("Error constructing Script file %v", err)
}
// opening the script file
scriptFile, err2 := os.OpenFile("/tmp/script.sh", os.O_RDWR|os.O_CREATE, 0777)
if err2 != nil {
fmt.Printf("Error opening Script file is %s", err)
}
cmdOutput := &bytes.Buffer{}
ss.Stdout = cmdOutput
ss.Stdin = scriptFile
interpreter := "sh"
err = ss.Run(interpreter)
if err != nil {
fmt.Printf("Error in Executing Script file %v", err.Error())
}
return cmdOutput.String(), err
}
type Params struct {
Username string
Password string
IPAddress string
Port string
}
func NewConnection(data map[string]interface{}) *Params {
var params Params
fmt.Printf("Data is %v",data)
err := mapstructure.Decode(data,&params)
if err != nil {
panic(err)
}
fmt.Printf("Mapped Connection details %v",params)
return &params
}
func (p *Params) ConnectNonSecure() (*ssh.Client, error){
config := &ssh.ClientConfig{
User: p.Username,
Auth: []ssh.AuthMethod{
ssh.Password(p.Password),
},
// Non-Production-only
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
fmt.Printf("Connection details %v",p)
// Connect to the remote server and perform a handshake
client, err := ssh.Dial("tcp", p.IPAddress+":"+p.Port, config)
if err != nil {
log.Fatalf("unable to connect to: %v %s", err,p.IPAddress)
}
return client, err
}
for simplicity sake I have added a very simple script,but actually the script is huge and requires user confirmation.Any idea on how to solve this issue?

Is the output from ssh guaranteed to be retrieved?

I use the ssh package to connect to a Linux server and retrieve the output of commands. The helper function I wrote for that is below:
func sshRunCommand(host string, command string) (output string, err error) {
keyData, err := ioutil.ReadFile("srv.private.openssh")
if err != nil {
return "", err
}
key, err := ssh.ParsePrivateKey(keyData)
if err != nil {
return "", err
}
// Authentication
config := &ssh.ClientConfig{
User: "root",
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
},
}
// Connect
client, err := ssh.Dial("tcp", net.JoinHostPort(host, "22"), config)
if err != nil {
return "", err
}
// Create a session. It is one session per command.
session, err := client.NewSession()
if err != nil {
return "", err
}
defer session.Close()
var b bytes.Buffer
session.Stdout = &b // 👈 this is the place I am concerned with
// Finally, run the command
err = session.Run(command)
return b.String(), err
}
This command usually works fine: it always connects but randomly does not return the output.
Before going further in my investigations, I wanted to make sure that the output buffer is flushed before returning the output. Is this the case?
In this use-case/issue, you can see stdout and stderr being linked to the same buffer.
See if that helps in your case.
sess, err := client.NewSession()
if err != nil {
log.Fatal("Failed to create session: ", err)
}
defer sess.Close()
stdin, err := sess.StdinPipe()
if err != nil {
log.Fatal(err)
}
var b bytes.Buffer
sess.Stdout = &b
sess.Stderr = &b
err = sess.Shell()
if err != nil {
log.Fatal(err)
}
...
_, err = fmt.Fprintf(stdin, "%s\n", cmd)

writing twice to the same sub process golang

I have a simple scp function that is just a wrapper over the scp cli tool.
type credential struct {
username string
password string
host string
port string
}
func scpFile(filepath, destpath string, c *credential) error {
cmd := exec.Command("scp", filepath, c.username+"#"+c.host+":"+destpath)
if err := cmd.Run(); err != nil {
return err
}
fmt.Println("done")
return nil
}
This works just fine now I want to add the capability of putting in a password the SSH if scp needs it. This is what I came up with
func scpFile(filepath, destpath string, c *credential) error {
cmd := exec.Command("scp", filepath, c.username+"#"+c.host+":"+destpath)
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
defer stdin.Close()
if err := cmd.Start(); err != nil {
return err
}
io.WriteString(stdin, c.password+"\n")
cmd.Wait()
fmt.Println("done")
return nil
}
This does not work as the password prompt just hangs there. I tried adding a 1 second sleep before I re write to stdin thinking maybe I was writing the password to fast but did not make a difference.
So I was able to find a work around by instead of trying to send the password to stdin I create a ssh session and scp a file through the ssh session. Here is the new scpFile function:
func scpFile(filePath, destinationPath string, session *ssh.Session) error {
defer session.Close()
f, err := os.Open(filePath)
if err != nil {
return err
}
defer f.Close()
s, err := f.Stat()
if err != nil {
return err
}
go func() {
w, _ := session.StdinPipe()
defer w.Close()
fmt.Fprintf(w, "C%#o %d %s\n", s.Mode().Perm(), s.Size(), path.Base(filePath))
io.Copy(w, f)
fmt.Fprint(w, "\x00")
}()
cmd := fmt.Sprintf("scp -t %s", destinationPath)
if err := session.Run(cmd); err != nil {
return err
}
return nil
}
This could probably be made better but the main idea is there

Interactive secure shell in Golang not capturing all keyboard

I am trying to start an interactive SSH session with a remote computer using Golang. I was able to get that without any problems, but the pseudo terminal doesn't seem to be capturing all of the keyboard i/o correctly.
For example, if I run a regular SSH command like,
ssh -i ~/.ssh/some-key.pem username#1.1.1.1
I can exit with a simple Ctrl+d, but for some reason when I run the interactive shell started with Golang it's not working and only prints the actual key characters ^D. Same goes for trying to use the arrow keys. If I run a Ctrl+c it exits the original Golang process and kills the interactive shell rather than executing on the remote machine.
Below is my code for setting up the shell,
func StartInteractiveShell(sshConfig *ssh.ClientConfig, network string, host string, port string) error {
var (
session *ssh.Session
conn *ssh.Client
err error
)
if conn, err = getSshConnection(sshConfig, network, host, port); err != nil {
fmt.Printf("Failed to dial: %s", err)
return err
}
if session, err = getSshSession(conn); err != nil {
fmt.Printf("Failed to create session: %s", err)
return err
}
defer session.Close()
if err = setupPty(session); err != nil {
fmt.Printf("Failed to set up pseudo terminal: %s", err)
return err
}
session.Stdout = os.Stdout
session.Stdin = os.Stdin
session.Stderr = os.Stderr
if err = session.Shell(); err != nil {
fmt.Printf("Failed to start interactive shell: %s", err)
return err
}
return session.Wait()
}
func getSshConnection(config *ssh.ClientConfig, network string, host string, port string) (*ssh.Client, error) {
addr := host + ":" + port
return ssh.Dial(network, addr, config)
}
func getSshSession(clientConnection *ssh.Client) (*ssh.Session, error) {
return clientConnection.NewSession()
}
// pty = pseudo terminal
func setupPty(session *ssh.Session) error {
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()
fmt.Printf("request for pseudo terminal failed: %s", err)
return err
}
return nil
}
Am I missing something there?

ssh executing nsenter as remote command with interactive shell in golang to debug docker container

I am trying to automate debugging of docker containers on coreos. I want to have a script that connects to a host via ssh and exectues nsenter. That would be very convenient to jump directly into a container from my OSX box without doing a lot of stuff manually. I know that entering containers that way can be nasty, but if things are getting tough I would like to use such a tool. So here is what I have so far in golang.
I am able to create a interactive shell. Here I have the problem that things like reverse searching bash history using ctrl+R breaks the session. That code is commented below, thus not executed.
However, I am also able to execute a single command, here nsenter, but I receive the error stdin: is not a tty and nothing more happens. I am interested to know why stdin in my programm is not a tty and how I can achieve this.
Thanks
package main
import (
"code.google.com/p/go.crypto/ssh"
"io/ioutil"
"log"
"os"
)
func privateKey() ssh.Signer {
buf, err := ioutil.ReadFile("./id_rsa")
if err != nil {
panic(err)
}
key, err := ssh.ParsePrivateKey(buf)
if err != nil {
panic(err)
}
return key
}
func main() {
privateKey := privateKey()
// Create client config
config := &ssh.ClientConfig{
User: "core",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(privateKey),
},
}
// Connect to ssh server
conn, err := ssh.Dial("tcp", "myhost.com:22", config)
if err != nil {
log.Fatalf("unable to connect: %s", err)
}
defer conn.Close()
// Create a session
session, err := conn.NewSession()
if err != nil {
log.Fatalf("unable to create session: %s", err)
}
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin // How can session.Stdin be a tty?
//////////////////////////////////////////////////////////////////////
// Stuff for interactive shell
// Set up terminal modes
//modes := ssh.TerminalModes{
// ssh.ECHO: 1, // enable 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-256color", 80, 40, modes); err != nil {
// log.Fatalf("request for pseudo terminal failed: %s", err)
//}
// Start remote shell
//if err := session.Shell(); err != nil {
// log.Fatalf("failed to start shell: %s", err)
//}
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
// Stuff for executing remote command
// 2202 in my example is actually the pid of a running container
if err := session.Run("sudo nsenter --target 2202 --mount --uts --ipc --net --pid"); err != nil {
panic("Failed to run: " + err.Error())
}
//////////////////////////////////////////////////////////////////////
session.Wait()
}
Super cool, I got it working, but there is still a magic I cannot comprehend. However, I changed my code as followed. The basic change leading to the correct pty behaviour, was the usage of the package "code.google.com/p/go.crypto/ssh/terminal". Using its MakeRaw(fd) seems to lead to side effects that enable the correct pty behaviour. Also thanks to the fleet project where I found the working example https://github.com/coreos/fleet/blob/master/ssh/ssh.go.
// The following two lines makes the terminal work properly because of
// side-effects I don't understand.
fd := int(os.Stdin.Fd())
oldState, err := terminal.MakeRaw(fd)
if err != nil {
panic(err)
}
session.Stdout = os.Stdout
session.Stderr = os.Stderr
session.Stdin = os.Stdin
termWidth, termHeight, err := terminal.GetSize(fd)
if err != nil {
panic(err)
}
// Set up terminal modes
modes := ssh.TerminalModes{
ssh.ECHO: 1, // enable 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-256color", termHeight, termWidth, modes); err != nil {
log.Fatalf("request for pseudo terminal failed: %s", err)
}
if err := session.Run("sudo nsenter --target 2202 --mount --uts --ipc --net --pid"); err != nil {
// if the session terminated normally, err should be ExitError; in that
// case, return nil error and actual exit status of command
if exitErr, ok := err.(*ssh.ExitError); ok {
fmt.Printf("exit code: %#v\n", exitErr.ExitStatus())
} else {
panic("Failed to run: " + err.Error())
}
}
session.Close()
terminal.Restore(fd, oldState)

Resources