Skip to content
This repository was archived by the owner on Jan 17, 2021. It is now read-only.

Commit 3794755

Browse files
committed
restructure SSH master code, apply requested fixes
1 parent eee34f5 commit 3794755

File tree

2 files changed

+100
-82
lines changed

2 files changed

+100
-82
lines changed

main.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,11 @@ func (c *rootCmd) Run(fl *flag.FlagSet) {
7878
}
7979

8080
err := sshCode(host, dir, options{
81-
skipSync: c.skipSync,
82-
sshFlags: c.sshFlags,
83-
bindAddr: c.bindAddr,
84-
syncBack: c.syncBack,
85-
noReuseConnection: c.noReuseConnection,
81+
skipSync: c.skipSync,
82+
sshFlags: c.sshFlags,
83+
bindAddr: c.bindAddr,
84+
syncBack: c.syncBack,
85+
reuseConnection: !c.noReuseConnection,
8686
})
8787

8888
if err != nil {

sshcode.go

+95-77
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,21 @@ import (
2121
)
2222

2323
const codeServerPath = "~/.cache/sshcode/sshcode-server"
24-
const sshDirectory = "~/.ssh"
25-
const sshDirectoryUnsafeModeMask = 0022
26-
const sshControlPath = sshDirectory + "/control-%h-%p-%r"
24+
25+
const (
26+
sshDirectory = "~/.ssh"
27+
sshDirectoryUnsafeModeMask = 0022
28+
sshControlPath = sshDirectory + "/control-%h-%p-%r"
29+
)
2730

2831
type options struct {
29-
skipSync bool
30-
syncBack bool
31-
noOpen bool
32-
noReuseConnection bool
33-
bindAddr string
34-
remotePort string
35-
sshFlags string
32+
skipSync bool
33+
syncBack bool
34+
noOpen bool
35+
reuseConnection bool
36+
bindAddr string
37+
remotePort string
38+
sshFlags string
3639
}
3740

3841
func sshCode(host, dir string, o options) error {
@@ -57,68 +60,19 @@ func sshCode(host, dir string, o options) error {
5760
}
5861

5962
// Check the SSH directory's permissions and warn the user if it is not safe.
60-
sshDirectoryMode, err := os.Lstat(expandPath(sshDirectory))
61-
if err != nil {
62-
if !o.noReuseConnection {
63-
flog.Info("failed to stat %v directory, disabling connection reuse feature: %v", sshDirectory, err)
64-
o.noReuseConnection = true
65-
}
66-
} else {
67-
if !sshDirectoryMode.IsDir() {
68-
if !o.noReuseConnection {
69-
flog.Info("%v is not a directory, disabling connection reuse feature", sshDirectory)
70-
o.noReuseConnection = true
71-
} else {
72-
flog.Info("warning: %v is not a directory", sshDirectory)
73-
}
74-
}
75-
if sshDirectoryMode.Mode().Perm()&sshDirectoryUnsafeModeMask != 0 {
76-
flog.Info("warning: the %v directory has unsafe permissions, they should only be writable by "+
77-
"the owner (and files inside should be set to 0600)", sshDirectory)
78-
}
79-
}
63+
o.reuseConnection = checkSSHDirectory(sshDirectory, o.reuseConnection)
8064

8165
// Start SSH master connection socket. This prevents multiple password prompts from appearing as authentication
8266
// only happens on the initial connection.
83-
if !o.noReuseConnection {
84-
newSSHFlags := fmt.Sprintf(`%v -o "ControlPath=%v"`, o.sshFlags, sshControlPath)
85-
86-
// -MN means "start a master socket and don't open a session, just connect".
87-
sshCmdStr := fmt.Sprintf(`exec ssh %v -MN %v`, newSSHFlags, host)
88-
sshMasterCmd := exec.Command("sh", "-c", sshCmdStr)
89-
sshMasterCmd.Stdin = os.Stdin
90-
sshMasterCmd.Stdout = os.Stdout
91-
sshMasterCmd.Stderr = os.Stderr
92-
stopSSHMaster := func() {
93-
if sshMasterCmd.Process != nil {
94-
err := sshMasterCmd.Process.Signal(syscall.Signal(0))
95-
if err != nil {
96-
return
97-
}
98-
err = sshMasterCmd.Process.Signal(syscall.SIGTERM)
99-
if err != nil {
100-
flog.Error("failed to send SIGTERM to SSH master process: %v", err)
101-
}
102-
}
103-
}
104-
defer stopSSHMaster()
105-
106-
err = sshMasterCmd.Start()
107-
go sshMasterCmd.Wait()
67+
if o.reuseConnection {
68+
flog.Info("starting SSH master connection...")
69+
newSSHFlags, cancel, err := startSSHMaster(o.sshFlags, sshControlPath, host)
70+
defer cancel()
10871
if err != nil {
109-
flog.Error("failed to start SSH master connection, disabling connection reuse feature: %v", err)
110-
o.noReuseConnection = true
111-
stopSSHMaster()
72+
flog.Error("failed to start SSH master connection: %v", err)
73+
o.reuseConnection = false
11274
} else {
113-
err = checkSSHMaster(sshMasterCmd, newSSHFlags, host)
114-
if err != nil {
115-
flog.Error("SSH master failed to be ready in time, disabling connection reuse feature: %v", err)
116-
o.noReuseConnection = true
117-
stopSSHMaster()
118-
} else {
119-
sshMasterCmd.Stdin = nil
120-
o.sshFlags = newSSHFlags
121-
}
75+
o.sshFlags = newSSHFlags
12276
}
12377
}
12478

@@ -226,12 +180,12 @@ func sshCode(host, dir string, o options) error {
226180

227181
err = syncExtensions(o.sshFlags, host, true)
228182
if err != nil {
229-
return xerrors.Errorf("failed to sync extensions back: %v", err)
183+
return xerrors.Errorf("failed to sync extensions back: %w", err)
230184
}
231185

232186
err = syncUserSettings(o.sshFlags, host, true)
233187
if err != nil {
234-
return xerrors.Errorf("failed to sync user settings settings back: %v", err)
188+
return xerrors.Errorf("failed to sync user settings settings back: %w", err)
235189
}
236190

237191
return nil
@@ -350,6 +304,74 @@ func randomPort() (string, error) {
350304
return "", xerrors.Errorf("max number of tries exceeded: %d", maxTries)
351305
}
352306

307+
// checkSSHDirectory performs sanity and safety checks on sshDirectory, and
308+
// returns a new value for o.reuseConnection depending on the checks.
309+
func checkSSHDirectory(sshDirectory string, reuseConnection bool) bool {
310+
sshDirectoryMode, err := os.Lstat(expandPath(sshDirectory))
311+
if err != nil {
312+
if reuseConnection {
313+
flog.Info("failed to stat %v directory, disabling connection reuse feature: %v", sshDirectory, err)
314+
}
315+
reuseConnection = false
316+
} else {
317+
if !sshDirectoryMode.IsDir() {
318+
if reuseConnection {
319+
flog.Info("%v is not a directory, disabling connection reuse feature", sshDirectory)
320+
} else {
321+
flog.Info("warning: %v is not a directory", sshDirectory)
322+
}
323+
reuseConnection = false
324+
}
325+
if sshDirectoryMode.Mode().Perm()&sshDirectoryUnsafeModeMask != 0 {
326+
flog.Info("warning: the %v directory has unsafe permissions, they should only be writable by "+
327+
"the owner (and files inside should be set to 0600)", sshDirectory)
328+
}
329+
}
330+
return reuseConnection
331+
}
332+
333+
// startSSHMaster starts an SSH master connection and waits for it to be ready.
334+
// It returns a new set of SSH flags for child SSH processes to use.
335+
func startSSHMaster(sshFlags string, sshControlPath string, host string) (string, func(), error) {
336+
ctx, cancel := context.WithCancel(context.Background())
337+
338+
newSSHFlags := fmt.Sprintf(`%v -o "ControlPath=%v"`, sshFlags, sshControlPath)
339+
340+
// -MN means "start a master socket and don't open a session, just connect".
341+
sshCmdStr := fmt.Sprintf(`exec ssh %v -MNq %v`, newSSHFlags, host)
342+
sshMasterCmd := exec.CommandContext(ctx, "sh", "-c", sshCmdStr)
343+
sshMasterCmd.Stdin = os.Stdin
344+
sshMasterCmd.Stderr = os.Stderr
345+
346+
// Gracefully stop the SSH master.
347+
stopSSHMaster := func() {
348+
if sshMasterCmd.Process != nil {
349+
if sshMasterCmd.ProcessState != nil && sshMasterCmd.ProcessState.Exited() {
350+
return
351+
}
352+
err := sshMasterCmd.Process.Signal(syscall.SIGTERM)
353+
if err != nil {
354+
flog.Error("failed to send SIGTERM to SSH master process: %v", err)
355+
}
356+
}
357+
cancel()
358+
}
359+
360+
// Start ssh master and wait. Waiting prevents the process from becoming a zombie process if it dies before
361+
// sshcode does, and allows sshMasterCmd.ProcessState to be populated.
362+
err := sshMasterCmd.Start()
363+
go sshMasterCmd.Wait()
364+
if err != nil {
365+
return "", stopSSHMaster, err
366+
}
367+
err = checkSSHMaster(sshMasterCmd, newSSHFlags, host)
368+
if err != nil {
369+
stopSSHMaster()
370+
return "", stopSSHMaster, xerrors.Errorf("SSH master wasn't ready on time: %w", err)
371+
}
372+
return newSSHFlags, stopSSHMaster, nil
373+
}
374+
353375
// checkSSHMaster polls every second for 30 seconds to check if the SSH master
354376
// is ready.
355377
func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error {
@@ -359,16 +381,12 @@ func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error
359381
err error
360382
)
361383
for i := 0; i < maxTries; i++ {
362-
// Check if the master is running
363-
if sshMasterCmd.Process == nil {
364-
return xerrors.Errorf("SSH master process not running")
365-
}
366-
err = sshMasterCmd.Process.Signal(syscall.Signal(0))
367-
if err != nil {
368-
return xerrors.Errorf("failed to check if SSH master process was alive: %v", err)
384+
// Check if the master is running.
385+
if sshMasterCmd.Process == nil || (sshMasterCmd.ProcessState != nil && sshMasterCmd.ProcessState.Exited()) {
386+
return xerrors.Errorf("SSH master process is not running")
369387
}
370388

371-
// Check if it's ready
389+
// Check if it's ready.
372390
sshCmdStr := fmt.Sprintf(`ssh %v -O check %v`, sshFlags, host)
373391
sshCmd := exec.Command("sh", "-c", sshCmdStr)
374392
err = sshCmd.Run()

0 commit comments

Comments
 (0)