-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathssh.go
171 lines (140 loc) · 5.12 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
package codespaces
import (
"context"
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"github.com/cli/safeexec"
)
type printer interface {
Printf(fmt string, v ...interface{})
}
// Shell runs an interactive secure shell over an existing
// port-forwarding session. It runs until the shell is terminated
// (including by cancellation of the context).
func Shell(ctx context.Context, p printer, sshArgs []string, port int, destination string, usingCustomPort bool) error {
cmd, connArgs, err := newSSHCommand(ctx, port, destination, sshArgs)
if err != nil {
return fmt.Errorf("failed to create ssh command: %w", err)
}
if usingCustomPort {
p.Printf("Connection Details: ssh %s %s", destination, connArgs)
}
return cmd.Run()
}
// Copy runs an scp command over the specified port. scpArgs should contain both scp flags
// as well as the list of files to copy, with the flags first.
//
// Remote files indicated by a "remote:" prefix are resolved relative
// to the remote user's home directory, and are subject to shell expansion
// on the remote host; see https://lwn.net/Articles/835962/.
func Copy(ctx context.Context, scpArgs []string, port int, destination string) error {
cmd, err := newSCPCommand(ctx, port, destination, scpArgs)
if err != nil {
return fmt.Errorf("failed to create scp command: %w", err)
}
return cmd.Run()
}
// NewRemoteCommand returns an exec.Cmd that will securely run a shell
// command on the remote machine.
func NewRemoteCommand(ctx context.Context, tunnelPort int, destination string, sshArgs ...string) (*exec.Cmd, error) {
cmd, _, err := newSSHCommand(ctx, tunnelPort, destination, sshArgs)
return cmd, err
}
// newSSHCommand populates an exec.Cmd to run a command (or if blank,
// an interactive shell) over ssh.
func newSSHCommand(ctx context.Context, port int, dst string, cmdArgs []string) (*exec.Cmd, []string, error) {
connArgs := []string{
"-p", strconv.Itoa(port),
"-o", "NoHostAuthenticationForLocalhost=yes",
"-o", "PasswordAuthentication=no",
}
// The ssh command syntax is: ssh [flags] user@host command [args...]
// There is no way to specify the user@host destination as a flag.
// Unfortunately, that means we need to know which user-provided words are
// SSH flags and which are command arguments so that we can place
// them before or after the destination, and that means we need to know all
// the flags and their arities.
cmdArgs, command, err := parseSSHArgs(cmdArgs)
if err != nil {
return nil, nil, err
}
cmdArgs = append(cmdArgs, connArgs...)
cmdArgs = append(cmdArgs, "-C") // Compression
cmdArgs = append(cmdArgs, dst) // user@host
if command != nil {
cmdArgs = append(cmdArgs, command...)
}
exe, err := safeexec.LookPath("ssh")
if err != nil {
return nil, nil, fmt.Errorf("failed to execute ssh: %w", err)
}
cmd := exec.CommandContext(ctx, exe, cmdArgs...)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
return cmd, connArgs, nil
}
func parseSSHArgs(args []string) (cmdArgs, command []string, err error) {
return parseArgs(args, "bcDeFIiLlmOopRSWw")
}
// newSCPCommand populates an exec.Cmd to run an scp command for the files specified in cmdArgs.
// cmdArgs is parsed such that scp flags precede the files to copy in the command.
// For example: scp -F ./config local/file remote:file
func newSCPCommand(ctx context.Context, port int, dst string, cmdArgs []string) (*exec.Cmd, error) {
connArgs := []string{
"-P", strconv.Itoa(port),
"-o", "NoHostAuthenticationForLocalhost=yes",
"-o", "PasswordAuthentication=no",
"-C", // compression
}
cmdArgs, command, err := parseSCPArgs(cmdArgs)
if err != nil {
return nil, err
}
cmdArgs = append(cmdArgs, connArgs...)
for _, arg := range command {
// Replace "remote:" prefix with (e.g.) "root@localhost:".
if rest := strings.TrimPrefix(arg, "remote:"); rest != arg {
arg = dst + ":" + rest
}
cmdArgs = append(cmdArgs, arg)
}
exe, err := safeexec.LookPath("scp")
if err != nil {
return nil, fmt.Errorf("failed to execute scp: %w", err)
}
// Beware: invalid syntax causes scp to exit 1 with
// no error message, so don't let that happen.
cmd := exec.CommandContext(ctx, exe, cmdArgs...)
cmd.Stdin = nil
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
return cmd, nil
}
func parseSCPArgs(args []string) (cmdArgs, command []string, err error) {
return parseArgs(args, "cFiJloPS")
}
// parseArgs parses arguments into two distinct slices of flags and command. Parsing stops
// as soon as a non-flag argument is found assuming the remaining arguments are the command.
// It returns an error if a unary flag is provided without an argument.
func parseArgs(args []string, unaryFlags string) (cmdArgs, command []string, err error) {
for i := 0; i < len(args); i++ {
arg := args[i]
// if we've started parsing the command, set it to the rest of the args
if !strings.HasPrefix(arg, "-") {
command = args[i:]
break
}
cmdArgs = append(cmdArgs, arg)
if len(arg) == 2 && strings.Contains(unaryFlags, arg[1:2]) {
if i++; i == len(args) {
return nil, nil, fmt.Errorf("flag: %s requires an argument", arg)
}
cmdArgs = append(cmdArgs, args[i])
}
}
return cmdArgs, command, nil
}