Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSH library #72

Open
k4ml opened this issue Apr 1, 2017 · 5 comments
Open

SSH library #72

k4ml opened this issue Apr 1, 2017 · 5 comments

Comments

@k4ml
Copy link

k4ml commented Apr 1, 2017

I wonder if it possible to add ssh library. Currently making ssh connection and run remote command is too much boilerplate in Go - https://gist.github.com/erikdubbelboer/f62a109d8e8798a11eb89ed494491953.

If we can simplify this in anko, it can be a practical alternative for some scripting work in Go (anko).

@k4ml
Copy link
Author

k4ml commented Apr 1, 2017

I try to add the ssh and ssh/agent library - k4ml@9f64636

Now I'm stuck with this error on go build:-

# github.com/mattn/anko/builtins/ssh
builtins/ssh/ssh.go:13: cannot make type ssh.ClientConfig

I'm trying to make this anko script to work:-

var ssh, agent = import("ssh"), import("ssh/agent")
var ioutil, net, os = import("io/ioutil"), import("net"), import("os")

func privateKeyPath() {
    return os.Getenv("HOME") + "/.ssh/kamalkey.pem"
}

func parsePrivateKey(keyPath) {
    buff, _ = ioutil.ReadFile(keyPath)
    return ssh.ParsePrivateKey(buff)
}

func makeSshConfig(user) {
    socket = os.Getenv("SSH_AUTH_SOCK")
    conn, err = net.Dial("unix", socket)

    agentClient = agent.NewClient(conn)

    config = make(ssh.ClientConfig)
    config.Set("user", user)

    #config = make(ssh.ClientConfig{
    #    User: user,
    #    Auth: []ssh.AuthMethod{
    #        ssh.PublicKeysCallBack(agentClient.Signers),
    #    },
    #})

    return config, nil
}

func main() {
    config, err = makeSshConfig("kamal")

    client, err = ssh.Dial("tcp", "myserver:22", config)
}

main()

@mattn
Copy link
Owner

mattn commented Apr 2, 2017

please keep package namespaces as same as golang.

@MichaelS11
Copy link
Contributor

Do you think this can be closed?

@MichaelS11
Copy link
Contributor

@mattn any thoughts?

@nickman
Copy link

nickman commented Mar 23, 2024

I have achieved some decent mileage implementing Anko extensions using factory methods and registering them in the Anko env.
e.g. for SSH commands (code below), the factory method is:

func NewSSHCommandClient(host, userName string) *SSHCommandClient

in the package sshcommand.
So I register this in the env like this:

_ = e.Define("sshcommand", sshcommand.NewSSHCommandClient)

My script then looks like this:

sshConn = sshcommand("my-host", "my-user").Connect()
printf("SSH Connected: %s\n", sshConn)
shell = sshConn.Shell()
output = shell.WriteCommandWithRead("pwd")
printf("PWD: %s\n", output)

It's a bit rough but it works. I put more cleanliness into extensions I use more, like Redis/DynamoDB etc.

SSHCommand Code:

package sshcommand

import (
	"bytes"
	"fmt"
	"github.com/elliotchance/sshtunnel"
	"golang.org/x/crypto/ssh"
	"io"
	"log"
	"net"
	"pql/util"
	"strings"
	"time"
)

var (
	modes = ssh.TerminalModes{
		ssh.ECHO:  0, // Disable echoing
		ssh.IGNCR: 1, // Ignore CR on input.
	}
)

type SSHCommandClient struct {
	host         string
	port         int
	userName     string
	userPassword string
	privateKey   string
	sshConfig    *ssh.ClientConfig
	connection   *ssh.Client
	timeout      time.Duration
}

func NewSSHCommandClient(host, userName string) *SSHCommandClient {
	return &SSHCommandClient{
		host:     host,
		port:     22,
		userName: userName,
		timeout:  10 * time.Second,
	}
}

func (s *SSHCommandClient) String() string {
	return fmt.Sprintf("%s@%s:%d?connected=%t", s.userName, s.host, s.port, s.connection != nil)
}

func (s *SSHCommandClient) Connect() *SSHCommandClient {
	s.init()
	if client, err := ssh.Dial("tcp", net.JoinHostPort(s.host, fmt.Sprintf("%d", s.port)), s.sshConfig); err != nil {
		panic(err)
	} else {
		s.connection = client
	}
	return s
}

func (s *SSHCommandClient) init() {
	// Authentication
	config := &ssh.ClientConfig{
		User:            s.userName,
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Timeout:         s.timeout,
		BannerCallback: func(message string) error {
			log.Printf("Banner: %s\n", message)
			return nil
		},
	}
	if s.userPassword != "" {
		config.Auth = []ssh.AuthMethod{
			ssh.Password(s.userPassword),
		}
	} else if s.privateKey != "" {
		bts := []byte(util.StringFromFile(s.privateKey))
		if key, err := ssh.ParsePrivateKey(bts); err != nil {
			panic(err)
		} else {
			config.Auth = []ssh.AuthMethod{
				ssh.PublicKeys(key),
			}
		}
	} else {
		config.Auth = []ssh.AuthMethod{
			sshtunnel.SSHAgent(),
		}
	}
	s.sshConfig = config
}

func (s *SSHCommandClient) WithPort(p int) *SSHCommandClient {
	s.port = p
	return s
}

func (s *SSHCommandClient) WithTimeout(t string) *SSHCommandClient {
	if to, err := time.ParseDuration(t); err == nil {
		s.timeout = to
	}
	return s
}

func (s *SSHCommandClient) WithPrivateKey(key string) *SSHCommandClient {
	s.privateKey = key
	return s
}

func (s *SSHCommandClient) WithPassword(pass string) *SSHCommandClient {
	s.privateKey = pass
	return s
}

func (s *SSHCommandClient) Exec(command string) string {
	// Create a session. It is one session per command.
	if session, err := s.connection.NewSession(); err != nil {
		panic(err)
	} else {
		defer session.Close()
		if bs, err := session.CombinedOutput(command); err != nil {
			panic(err)
		} else {
			return string(bs)
		}
	}
}

func (s *SSHCommandClient) Close() {
	s.connection.Close()
}

type SSHShell struct {
	client    *SSHCommandClient
	session   *ssh.Session
	stdIn     io.WriteCloser
	stdOutErr io.Reader
	ps1       string
}

func (h *SSHShell) Prompt() string {
	return h.ps1
}

func (h *SSHShell) Close() {
	h.session.Close()
}

func (h *SSHShell) WriteCommandWithRead(cmd string) string {
	h.WriteCommand(cmd)
	return h.ReadOutput()
}

func (h *SSHShell) WriteCommand(cmd string) {
	if _, err := h.stdIn.Write([]byte(cmd)); err != nil {
		panic(err)
	}
}

func (h *SSHShell) ReadOutput() string {
	var b strings.Builder
	for {
		buff := make([]byte, 1024, 1024)
		if n, err := h.stdOutErr.Read(buff); err != nil {
			if n > 0 {
				b.Write(buff[:n])
			}
			if err == io.EOF {
				break
			} else {
				panic(err)
			}
		} else {
			if n > 0 {
				s := string(buff[:n])
				b.WriteString(s)
				if strings.Contains(strings.TrimSpace(s), h.ps1) {
					break
				}
			}
		}
	}
	return b.String()
}

func (s *SSHCommandClient) Shell() *SSHShell {
	// Create a session. It is one session per command.
	if session, err := s.connection.NewSession(); err != nil {
		panic(err)
	} else {
		if writer, err := session.StdinPipe(); err != nil {
			panic(err)
		} else {
			if stdOut, err := session.StdoutPipe(); err != nil {
				panic(err)
			} else {
				if stdErr, err := session.StderrPipe(); err != nil {
					panic(err)
				} else {
					if err := session.RequestPty("vt100", 80, 120, modes); err != nil {
						panic(err)
					} else {
						if err := session.Shell(); err != nil {
							panic(err)
						} else {
							return &SSHShell{
								ps1:       "$",
								client:    s,
								session:   session,
								stdIn:     writer,
								stdOutErr: io.MultiReader(stdOut, stdErr),
							}
						}
					}
				}
			}
		}
	}
}

func (s *SSHCommandClient) Start(command string) string {
	// Create a session. It is one session per command.
	if session, err := s.connection.NewSession(); err != nil {
		panic(err)
	} else {
		defer session.Close()
		var b bytes.Buffer
		session.Stdout = &b
		session.Stderr = &b

		if err := session.Start(command); err != nil {
			panic(err)
		} else {
			return string(b.String())
		}
	}
}

func remoteRun(user string, addr string, privateKey string, cmd string) (string, error) {
	// privateKey could be read from a file, or retrieved from another storage
	// source, such as the Secret Service / GNOME Keyring
	key, err := ssh.ParsePrivateKey([]byte(privateKey))
	if err != nil {
		return "", err
	}
	// Authentication
	config := &ssh.ClientConfig{
		User: user,
		// https://github.com/golang/go/issues/19767
		// as clientConfig is non-permissive by default
		// you can set ssh.InsercureIgnoreHostKey to allow any host
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Auth: []ssh.AuthMethod{
			ssh.PublicKeys(key),
		},
		//alternatively, you could use a password
		/*
		   Auth: []ssh.AuthMethod{
		       ssh.Password("PASSWORD"),
		   },
		*/
	}
	// Connect
	client, err := ssh.Dial("tcp", net.JoinHostPort(addr, "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  // import "bytes"
	session.Stdout = &b // get output
	// you can also pass what gets input to the stdin, allowing you to pipe
	// content from client to server
	//      session.Stdin = bytes.NewBufferString("My input")

	// Shell starts a login shell on the remote host. A Session only
	// accepts one call to Run, Start, Shell, Output, or CombinedOutput.

	// Start runs cmd on the remote host. Typically, the remote
	// server passes cmd to the shell for interpretation.
	// A Session only accepts one call to Run, Start or Shell.

	// CombinedOutput runs cmd on the remote host and returns its combined
	// standard output and standard error.

	// Finally, run the command
	bs, err := session.CombinedOutput(cmd)

	return string(bs), err
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants