1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
16
"github.com/juju/errors"
17
"github.com/juju/utils"
18
"golang.org/x/crypto/ssh"
21
const sshDefaultPort = 22
23
// GoCryptoClient is an implementation of Client that
24
// uses the embedded go.crypto/ssh SSH client.
26
// GoCryptoClient is intentionally limited in the
27
// functionality that it enables, as it is currently
28
// intended to be used only for non-interactive command
30
type GoCryptoClient struct {
34
// NewGoCryptoClient creates a new GoCryptoClient.
36
// If no signers are specified, NewGoCryptoClient will
37
// use the private key generated by LoadClientKeys.
38
func NewGoCryptoClient(signers ...ssh.Signer) (*GoCryptoClient, error) {
39
return &GoCryptoClient{signers: signers}, nil
42
// Command implements Client.Command.
43
func (c *GoCryptoClient) Command(host string, command []string, options *Options) *Cmd {
44
shellCommand := utils.CommandString(command...)
46
if len(signers) == 0 {
47
signers = privateKeys()
49
user, host := splitUserHost(host)
50
port := sshDefaultPort
51
var proxyCommand []string
53
if options.port != 0 {
56
proxyCommand = options.proxyCommand
58
logger.Tracef(`running (equivalent of): ssh "%s@%s" -p %d '%s'`, user, host, port, shellCommand)
59
return &Cmd{impl: &goCryptoCommand{
62
addr: net.JoinHostPort(host, strconv.Itoa(port)),
63
command: shellCommand,
64
proxyCommand: proxyCommand,
68
// Copy implements Client.Copy.
70
// Copy is currently unimplemented, and will always return an error.
71
func (c *GoCryptoClient) Copy(args []string, options *Options) error {
72
return errors.Errorf("scp command is not implemented (OpenSSH scp not available in PATH)")
75
type goCryptoCommand struct {
88
var sshDial = ssh.Dial
90
var sshDialWithProxy = func(addr string, proxyCommand []string, config *ssh.ClientConfig) (*ssh.Client, error) {
91
if len(proxyCommand) == 0 {
92
return sshDial("tcp", addr, config)
94
// User has specified a proxy. Create a pipe and
95
// redirect the proxy command's stdin/stdout to it.
96
host, port, err := net.SplitHostPort(addr)
100
for i, arg := range proxyCommand {
101
arg = strings.Replace(arg, "%h", host, -1)
103
arg = strings.Replace(arg, "%p", port, -1)
105
arg = strings.Replace(arg, "%r", config.User, -1)
106
proxyCommand[i] = arg
108
client, server := net.Pipe()
109
logger.Tracef(`executing proxy command %q`, proxyCommand)
110
cmd := exec.Command(proxyCommand[0], proxyCommand[1:]...)
113
cmd.Stderr = os.Stderr
114
if err := cmd.Start(); err != nil {
117
conn, chans, reqs, err := ssh.NewClientConn(client, addr, config)
121
return ssh.NewClient(conn, chans, reqs), nil
124
func (c *goCryptoCommand) ensureSession() (*ssh.Session, error) {
128
if len(c.signers) == 0 {
129
return nil, errors.Errorf("no private keys available")
132
currentUser, err := user.Current()
134
return nil, errors.Errorf("getting current user: %v", err)
136
c.user = currentUser.Username
138
config := &ssh.ClientConfig{
140
Auth: []ssh.AuthMethod{
141
ssh.PublicKeysCallback(func() ([]ssh.Signer, error) {
142
return c.signers, nil
146
client, err := sshDialWithProxy(c.addr, c.proxyCommand, config)
150
sess, err := client.NewSession()
157
c.sess.Stdin = c.stdin
158
c.sess.Stdout = c.stdout
159
c.sess.Stderr = c.stderr
163
func (c *goCryptoCommand) Start() error {
164
sess, err := c.ensureSession()
171
return sess.Start(c.command)
174
func (c *goCryptoCommand) Close() error {
178
err0 := c.sess.Close()
179
err1 := c.client.Close()
188
func (c *goCryptoCommand) Wait() error {
190
return errors.Errorf("command has not been started")
197
func (c *goCryptoCommand) Kill() error {
199
return errors.Errorf("command has not been started")
201
return c.sess.Signal(ssh.SIGKILL)
204
func (c *goCryptoCommand) SetStdio(stdin io.Reader, stdout, stderr io.Writer) {
210
func (c *goCryptoCommand) StdinPipe() (io.WriteCloser, io.Reader, error) {
211
sess, err := c.ensureSession()
215
wc, err := sess.StdinPipe()
216
return wc, sess.Stdin, err
219
func (c *goCryptoCommand) StdoutPipe() (io.ReadCloser, io.Writer, error) {
220
sess, err := c.ensureSession()
224
wc, err := sess.StdoutPipe()
225
return ioutil.NopCloser(wc), sess.Stdout, err
228
func (c *goCryptoCommand) StderrPipe() (io.ReadCloser, io.Writer, error) {
229
sess, err := c.ensureSession()
233
wc, err := sess.StderrPipe()
234
return ioutil.NopCloser(wc), sess.Stderr, err
237
func splitUserHost(s string) (user, host string) {
238
userHost := strings.SplitN(s, "@", 2)
239
if len(userHost) == 2 {
240
return userHost[0], userHost[1]
242
return "", userHost[0]