/
ssh.go
325 lines (279 loc) · 9.33 KB
/
ssh.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// Copyright 2013 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
// Package ssh contains utilities for dealing with SSH connections,
// key management, and so on. All SSH-based command executions in
// Juju should use the Command/ScpCommand functions in this package.
//
package ssh
import (
"bytes"
"io"
"os/exec"
"syscall"
"github.com/juju/cmd"
"github.com/juju/errors"
)
// StrictHostChecksOption defines the possible values taken by
// Option.SetStrictHostKeyChecking().
type StrictHostChecksOption int
const (
// StrictHostChecksDefault configures the default,
// implementation-specific, behaviour.
//
// For the OpenSSH implementation, this elides the
// StrictHostKeyChecking option, which means the
// user's personal configuration will be used.
//
// For the go.crypto implementation, the default is
// the equivalent of "ask".
StrictHostChecksDefault StrictHostChecksOption = iota
// StrictHostChecksNo disables strict host key checking.
StrictHostChecksNo
// StrictHostChecksYes enabled strict host key checking
// enabled. Target hosts must appear in known_hosts file or
// connections will fail.
StrictHostChecksYes
// StrictHostChecksAsk will cause openssh to ask the user about
// hosts that don't appear in known_hosts file.
StrictHostChecksAsk
)
// Options is a client-implementation independent SSH options set.
type Options struct {
// proxyCommand specifies the command to
// execute to proxy SSH traffic through.
proxyCommand []string
// ssh server port; zero means use the default (22)
port int
// no PTY forced by default
allocatePTY bool
// password authentication is disallowed by default
passwordAuthAllowed bool
// identities is a sequence of paths to private key/identity files
// to use when attempting to login. A client implementaton may attempt
// with additional identities, but must give preference to these
identities []string
// knownHostsFile is a path to a file in which to save the host's
// fingerprint.
knownHostsFile string
// strictHostKeyChecking sets that the host being connected to must
// exist in the known_hosts file, and with a matching public key.
strictHostKeyChecking StrictHostChecksOption
// hostKeyAlgorithms sets the host key types that the client will
// accept from the server, in order of preference. By default the
// client implementation will specify a set of reasonable types.
hostKeyAlgorithms []string
}
// SetProxyCommand sets a command to execute to proxy traffic through.
func (o *Options) SetProxyCommand(command ...string) {
o.proxyCommand = append([]string{}, command...)
}
// SetPort sets the SSH server port to connect to.
func (o *Options) SetPort(port int) {
o.port = port
}
// EnablePTY forces the allocation of a pseudo-TTY.
//
// Forcing a pseudo-TTY is required, for example, for sudo
// prompts on the target host.
func (o *Options) EnablePTY() {
o.allocatePTY = true
}
// SetKnownHostsFile sets the host's fingerprint to be saved in the given file.
//
// Host fingerprints are saved in ~/.ssh/known_hosts by default.
func (o *Options) SetKnownHostsFile(file string) {
o.knownHostsFile = file
}
// SetStrictHostKeyChecking sets the desired host key checking
// behaviour. It takes one of the StrictHostChecksOption constants.
// See also EnableStrictHostKeyChecking.
func (o *Options) SetStrictHostKeyChecking(value StrictHostChecksOption) {
o.strictHostKeyChecking = value
}
// AllowPasswordAuthentication allows the SSH
// client to prompt the user for a password.
//
// Password authentication is disallowed by default.
func (o *Options) AllowPasswordAuthentication() {
o.passwordAuthAllowed = true
}
// SetIdentities sets a sequence of paths to private key/identity files
// to use when attempting login. Client implementations may attempt to
// use additional identities, but must give preference to the ones
// specified here.
func (o *Options) SetIdentities(identityFiles ...string) {
o.identities = append([]string{}, identityFiles...)
}
// SetHostKeyAlgorithms sets the host key types that the client will
// accept from the server, in order of preference. If not specified,
// the client implementation may choose its own defaults.
func (o *Options) SetHostKeyAlgorithms(algos ...string) {
o.hostKeyAlgorithms = algos
}
// Client is an interface for SSH clients to implement
type Client interface {
// Command returns a Command for executing a command
// on the specified host. Each Command is executed
// within its own SSH session.
//
// Host is specified in the format [user@]host.
Command(host string, command []string, options *Options) *Cmd
// Copy copies file(s) between local and remote host(s).
// Paths are specified in the scp format, [[user@]host:]path. If
// any extra arguments are specified in extraArgs, they are passed
// verbatim.
Copy(args []string, options *Options) error
}
// Cmd represents a command to be (or being) executed
// on a remote host.
type Cmd struct {
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
impl command
}
func newCmd(impl command) *Cmd {
return &Cmd{impl: impl}
}
// CombinedOutput runs the command, and returns the
// combined stdout/stderr output and result of
// executing the command.
func (c *Cmd) CombinedOutput() ([]byte, error) {
if c.Stdout != nil {
return nil, errors.New("ssh: Stdout already set")
}
if c.Stderr != nil {
return nil, errors.New("ssh: Stderr already set")
}
var b bytes.Buffer
c.Stdout = &b
c.Stderr = &b
err := c.Run()
return b.Bytes(), err
}
// Output runs the command, and returns the stdout
// output and result of executing the command.
func (c *Cmd) Output() ([]byte, error) {
if c.Stdout != nil {
return nil, errors.New("ssh: Stdout already set")
}
var b bytes.Buffer
c.Stdout = &b
err := c.Run()
return b.Bytes(), err
}
// Run runs the command, and returns the result as an error.
func (c *Cmd) Run() error {
if err := c.Start(); err != nil {
return err
}
err := c.Wait()
if exitError, ok := err.(*exec.ExitError); ok && exitError != nil {
status := exitError.ProcessState.Sys().(syscall.WaitStatus)
if status.Exited() {
return cmd.NewRcPassthroughError(status.ExitStatus())
}
}
return err
}
// Start starts the command running, but does not wait for
// it to complete. If the command could not be started, an
// error is returned.
func (c *Cmd) Start() error {
c.impl.SetStdio(c.Stdin, c.Stdout, c.Stderr)
return c.impl.Start()
}
// Wait waits for the started command to complete,
// and returns the result as an error.
func (c *Cmd) Wait() error {
return c.impl.Wait()
}
// Kill kills the started command.
func (c *Cmd) Kill() error {
return c.impl.Kill()
}
// StdinPipe creates a pipe and connects it to
// the command's stdin. The read end of the pipe
// is assigned to c.Stdin.
func (c *Cmd) StdinPipe() (io.WriteCloser, error) {
wc, r, err := c.impl.StdinPipe()
if err != nil {
return nil, err
}
c.Stdin = r
return wc, nil
}
// StdoutPipe creates a pipe and connects it to
// the command's stdout. The write end of the pipe
// is assigned to c.Stdout.
func (c *Cmd) StdoutPipe() (io.ReadCloser, error) {
rc, w, err := c.impl.StdoutPipe()
if err != nil {
return nil, err
}
c.Stdout = w
return rc, nil
}
// StderrPipe creates a pipe and connects it to
// the command's stderr. The write end of the pipe
// is assigned to c.Stderr.
func (c *Cmd) StderrPipe() (io.ReadCloser, error) {
rc, w, err := c.impl.StderrPipe()
if err != nil {
return nil, err
}
c.Stderr = w
return rc, nil
}
// command is an implementation-specific representation of a
// command prepared to execute against a specific host.
type command interface {
Start() error
Wait() error
Kill() error
SetStdio(stdin io.Reader, stdout, stderr io.Writer)
StdinPipe() (io.WriteCloser, io.Reader, error)
StdoutPipe() (io.ReadCloser, io.Writer, error)
StderrPipe() (io.ReadCloser, io.Writer, error)
}
// DefaultClient is the default SSH client for the process.
//
// If the OpenSSH client is found in $PATH, then it will be
// used for DefaultClient; otherwise, DefaultClient will use
// an embedded client based on go.crypto/ssh.
var DefaultClient Client
// chosenClient holds the type of SSH client created for
// DefaultClient, so that we can log it in Command or Copy.
var chosenClient string
func init() {
initDefaultClient()
}
func initDefaultClient() {
if client, err := NewOpenSSHClient(); err == nil {
DefaultClient = client
chosenClient = "OpenSSH"
} else if client, err := NewGoCryptoClient(); err == nil {
DefaultClient = client
chosenClient = "go.crypto (embedded)"
}
}
// Command is a short-cut for DefaultClient.Command.
func Command(host string, command []string, options *Options) *Cmd {
logger.Debugf("using %s ssh client", chosenClient)
return DefaultClient.Command(host, command, options)
}
// Copy is a short-cut for DefaultClient.Copy.
func Copy(args []string, options *Options) error {
logger.Debugf("using %s ssh client", chosenClient)
return DefaultClient.Copy(args, options)
}
// CopyReader sends the reader's data to a file on the remote host over SSH.
func CopyReader(host, filename string, r io.Reader, options *Options) error {
logger.Debugf("using %s ssh client", chosenClient)
return copyReader(DefaultClient, host, filename, r, options)
}
func copyReader(client Client, host, filename string, r io.Reader, options *Options) error {
cmd := client.Command(host, []string{"cat - > " + filename}, options)
cmd.Stdin = r
return errors.Trace(cmd.Run())
}