forked from mutagen-io/mutagen
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ssh.go
198 lines (163 loc) · 6.47 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
package ssh
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/pkg/errors"
"github.com/havoc-io/mutagen/pkg/url"
)
const (
connectTimeoutSeconds = 5
)
// compressionArgument returns a flag that can be passed to scp or ssh to enable
// compression. Note that while SSH does have a CompressionLevel configuration
// option, this only applies to SSHv1. SSHv2 defaults to a DEFLATE level of 6,
// which is what we want anyway.
func compressionArgument() string {
return "-C"
}
// timeoutArgument returns a option flag that can be passed to scp or ssh to
// limit connection time (though not transfer time or process lifetime). It is
// currently a fixed value, but in the future we might want to make this
// configurable for people with poor connections.
func timeoutArgument() string {
return fmt.Sprintf("-oConnectTimeout=%d", connectTimeoutSeconds)
}
// Copy copies a local file (which MUST be an absolute path) to a remote
// destination. If a prompter is provided, this method will attempt to use it
// for authentication if necessary.
func Copy(prompter, local string, remote *url.URL) error {
// Validate the URL protocol.
if remote.Protocol != url.Protocol_SSH {
return errors.New("non-SSH URL provided")
}
// Locate the SCP command.
scp, err := scpCommand()
if err != nil {
return errors.Wrap(err, "unable to identify SCP executable")
}
// HACK: On Windows, we attempt to use SCP executables that might not
// understand Windows paths because they're designed to run inside a POSIX-
// style environment (e.g. MSYS or Cygwin). To work around this, we run them
// in the same directory as the source and just pass them the source base
// name. This works fine on other systems as well. Unfortunately this means
// that we need to use absolute paths, but we do that anyway.
if !filepath.IsAbs(local) {
return errors.New("scp source path must be absolute")
}
workingDirectory, sourceBase := filepath.Split(local)
// Compute the destination URL.
destinationURL := fmt.Sprintf("%s:%s", remote.Hostname, remote.Path)
if remote.Username != "" {
destinationURL = fmt.Sprintf("%s@%s", remote.Username, destinationURL)
}
// Set up arguments.
var scpArguments []string
scpArguments = append(scpArguments, compressionArgument())
scpArguments = append(scpArguments, timeoutArgument())
if remote.Port != 0 {
scpArguments = append(scpArguments, "-P", fmt.Sprintf("%d", remote.Port))
}
scpArguments = append(scpArguments, sourceBase, destinationURL)
// Create the process.
scpProcess := exec.Command(scp, scpArguments...)
// Set the working directory.
scpProcess.Dir = workingDirectory
// Force it to run detached.
scpProcess.SysProcAttr = processAttributes()
// Create a copy of the current environment.
environment := os.Environ()
// Add locale environment variables.
environment = addLocaleVariables(environment)
// Set prompting environment variables
environment, err = setPrompterVariables(environment, prompter)
if err != nil {
return errors.Wrap(err, "unable to create prompter environment")
}
// Set the environment.
scpProcess.Env = environment
// Run the operation.
if err = scpProcess.Run(); err != nil {
return errors.Wrap(err, "unable to run SCP process")
}
// Success.
return nil
}
// Command create an SSH process set to connect to the specified remote and
// invoke the specified command. This function does not start the process. If a
// prompter is provided, the process will be directed to use it on startup if
// necessary. The command string is interpreted as literal input to the remote
// shell, so its contents are more flexible than just an executable name or
// path. The path component of the remote URL is NOT used as a working directory
// and is simply ignored - the command will execute in whatever default
// directory the server chooses.
func Command(prompter string, remote *url.URL, command string) (*exec.Cmd, error) {
// Validate the URL protocol.
if remote.Protocol != url.Protocol_SSH {
return nil, errors.New("non-SSH URL provided")
}
// Locate the SSH command.
ssh, err := sshCommand()
if err != nil {
return nil, errors.Wrap(err, "unable to identify SSH executable")
}
// Compute the target.
target := remote.Hostname
if remote.Username != "" {
target = fmt.Sprintf("%s@%s", remote.Username, remote.Hostname)
}
// Set up arguments. We intentionally don't use compression on SSH commands
// since the agent stream uses the FLATE algorithm internally and it's much
// more efficient to compress at that layer, even with the slower Go
// implementation.
var sshArguments []string
sshArguments = append(sshArguments, timeoutArgument())
if remote.Port != 0 {
sshArguments = append(sshArguments, "-p", fmt.Sprintf("%d", remote.Port))
}
sshArguments = append(sshArguments, target, command)
// Create the process.
sshProcess := exec.Command(ssh, sshArguments...)
// Force it to run detached.
sshProcess.SysProcAttr = processAttributes()
// Create a copy of the current environment.
environment := os.Environ()
// Add locale environment variables.
environment = addLocaleVariables(environment)
// Set prompting environment variables
environment, err = setPrompterVariables(environment, prompter)
if err != nil {
return nil, errors.Wrap(err, "unable to create prompter environment")
}
// Set the environment.
sshProcess.Env = environment
// Done.
return sshProcess, nil
}
// Run creates an SSH command by forwarding its arguments to Command and then
// returning the result of its Run method. If there is an error creating the
// command, it will be returned, but otherwise the result of the Run method will
// be returned un-wrapped, so it can be treated as an os/exec.ExitError.
func Run(prompter string, remote *url.URL, command string) error {
// Create the process.
process, err := Command(prompter, remote, command)
if err != nil {
return errors.Wrap(err, "unable to create command")
}
// Run the process.
return process.Run()
}
// Output creates an SSH command by forwarding its arguments to Command and then
// returning the results of its Output method. If there is an error creating the
// command, it will be returned, but otherwise the result of the Run method will
// be returned un-wrapped, so it can be treated as an os/exec.ExitError.
func Output(prompter string, remote *url.URL, command string) ([]byte, error) {
// Create the process.
process, err := Command(prompter, remote, command)
if err != nil {
return nil, errors.Wrap(err, "unable to create command")
}
// Run the process.
return process.Output()
}