mattn / anko

Scriptable interpreter written in golang
http://play-anko.appspot.com/
MIT License
1.48k stars 118 forks source link

SSH library #72

Open k4ml opened 7 years ago

k4ml commented 7 years ago

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 commented 7 years ago

I try to add the ssh and ssh/agent library - https://github.com/k4ml/anko/commit/9f646360ab6152dc6490f192af8687638d6dde05

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 commented 7 years ago

please keep package namespaces as same as golang.

MichaelS11 commented 6 years ago

Do you think this can be closed?

MichaelS11 commented 6 years ago

@mattn any thoughts?

nickman commented 8 months ago

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
}