Commit 0dd284d9dd
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
cmd/gitbay/local.go +1 −5
| @@ -187,11 +187,7 @@ func cmdInit(args []string) int { | ||
| 187 | 187 | |
| 188 | 188 | // captureSSH runs a server command and returns its stdout. |
| 189 | 189 | func captureSSH(t target, serverArgv []string) (string, int) { |
| 190 | args := []string{} | |
| 191 | if t.inst.Port != 0 && t.inst.Port != 22 { | |
| 192 | args = append(args, "-p", fmt.Sprint(t.inst.Port)) | |
| 193 | } | |
| 194 | args = append(args, t.inst.SSHOptions...) | |
| 190 | args := sshArgs(t.inst) | |
| 195 | 191 | quoted := quoteAll(serverArgv) |
| 196 | 192 | args = append(args, t.inst.SSHUser()+"@"+t.inst.Host, "--", strings.Join(quoted, " ")) |
| 197 | 193 | cmd := exec.Command("ssh", args...) |
cmd/gitbay/ssh.go +29 −10
| @@ -6,6 +6,7 @@ import ( | ||
| 6 | 6 | "io" |
| 7 | 7 | "os" |
| 8 | 8 | "os/exec" |
| 9 | "path/filepath" | |
| 9 | 10 | "regexp" |
| 10 | 11 | "strconv" |
| 11 | 12 | "strings" |
| @@ -78,14 +79,36 @@ func shellQuote(arg string) string { | ||
| 78 | 79 | return "'" + strings.ReplaceAll(arg, "'", `'\''`) + "'" |
| 79 | 80 | } |
| 80 | 81 | |
| 82 | // sshArgs is every argument before the destination: the port, connection | |
| 83 | // multiplexing, and the profile's own options last so they win. | |
| 84 | // | |
| 85 | // Multiplexing is what makes a CLI over SSH usable: without it every | |
| 86 | // command pays a full handshake, seconds on a distant instance, and with | |
| 87 | // it the second command in five minutes rides the first's connection | |
| 88 | // (#94). The control socket lives under ~/.ssh, which ssh requires to be | |
| 89 | // private; a profile can set no_multiplex = true to opt out. | |
| 90 | func sshArgs(inst cliconfig.Instance) []string { | |
| 91 | args := []string{} | |
| 92 | if inst.Port != 0 && inst.Port != 22 { | |
| 93 | args = append(args, "-p", strconv.Itoa(inst.Port)) | |
| 94 | } | |
| 95 | if !inst.NoMultiplex { | |
| 96 | if home, err := os.UserHomeDir(); err == nil { | |
| 97 | if st, err := os.Stat(filepath.Join(home, ".ssh")); err == nil && st.IsDir() { | |
| 98 | args = append(args, | |
| 99 | "-o", "ControlMaster=auto", | |
| 100 | "-o", "ControlPath="+filepath.Join(home, ".ssh", "gitbay-%C"), | |
| 101 | "-o", "ControlPersist=300") | |
| 102 | } | |
| 103 | } | |
| 104 | } | |
| 105 | return append(args, inst.SSHOptions...) | |
| 106 | } | |
| 107 | ||
| 81 | 108 | // runSSH executes the server command over the system ssh binary, wiring |
| 82 | 109 | // stdio through. It returns the remote exit code. |
| 83 | 110 | func runSSH(t target, serverArgv []string, stdin io.Reader) int { |
| 84 | args := []string{} | |
| 85 | if t.inst.Port != 0 && t.inst.Port != 22 { | |
| 86 | args = append(args, "-p", strconv.Itoa(t.inst.Port)) | |
| 87 | } | |
| 88 | args = append(args, t.inst.SSHOptions...) | |
| 111 | args := sshArgs(t.inst) | |
| 89 | 112 | quoted := make([]string, len(serverArgv)) |
| 90 | 113 | for i, a := range serverArgv { |
| 91 | 114 | quoted[i] = shellQuote(a) |
| @@ -114,11 +137,7 @@ func runSSH(t target, serverArgv []string, stdin io.Reader) int { | ||
| 114 | 137 | // sshCapture runs a server command and returns its stdout, discarding |
| 115 | 138 | // stderr. Used for quiet metadata fetches like issue templates. |
| 116 | 139 | func sshCapture(t target, serverArgv []string) (string, int) { |
| 117 | args := []string{} | |
| 118 | if t.inst.Port != 0 && t.inst.Port != 22 { | |
| 119 | args = append(args, "-p", strconv.Itoa(t.inst.Port)) | |
| 120 | } | |
| 121 | args = append(args, t.inst.SSHOptions...) | |
| 140 | args := sshArgs(t.inst) | |
| 122 | 141 | quoted := make([]string, len(serverArgv)) |
| 123 | 142 | for i, a := range serverArgv { |
| 124 | 143 | quoted[i] = shellQuote(a) |
cmd/gitbay/sshargs_test.go added +43
| @@ -0,0 +1,43 @@ | ||
| 1 | package main | |
| 2 | ||
| 3 | import ( | |
| 4 | "os" | |
| 5 | "path/filepath" | |
| 6 | "slices" | |
| 7 | "strings" | |
| 8 | "testing" | |
| 9 | ||
| 10 | "gitbay.org/gitbay/internal/cliconfig" | |
| 11 | ) | |
| 12 | ||
| 13 | // Every ssh the CLI spawns shares one connection per instance, so a | |
| 14 | // command costs a round trip rather than a handshake (#94). The socket | |
| 15 | // sits under ~/.ssh; with no such directory, or no_multiplex set, the | |
| 16 | // arguments are the plain ones. | |
| 17 | func TestSSHArgsMultiplex(t *testing.T) { | |
| 18 | home := t.TempDir() | |
| 19 | t.Setenv("HOME", home) | |
| 20 | inst := cliconfig.Instance{Host: "forge.test", Port: 2222, SSHOptions: []string{"-i", "k"}} | |
| 21 | ||
| 22 | if args := sshArgs(inst); slices.Contains(args, "ControlMaster=auto") { | |
| 23 | t.Errorf("multiplexing without ~/.ssh: %v", args) | |
| 24 | } | |
| 25 | os.Mkdir(filepath.Join(home, ".ssh"), 0o700) | |
| 26 | ||
| 27 | args := sshArgs(inst) | |
| 28 | joined := strings.Join(args, " ") | |
| 29 | for _, want := range []string{"-p 2222", "ControlMaster=auto", "ControlPersist=300", | |
| 30 | "ControlPath=" + filepath.Join(home, ".ssh", "gitbay-%C")} { | |
| 31 | if !strings.Contains(joined, want) { | |
| 32 | t.Errorf("missing %q in %v", want, args) | |
| 33 | } | |
| 34 | } | |
| 35 | if args[len(args)-2] != "-i" || args[len(args)-1] != "k" { | |
| 36 | t.Errorf("profile options are not last: %v", args) | |
| 37 | } | |
| 38 | ||
| 39 | inst.NoMultiplex = true | |
| 40 | if args := sshArgs(inst); slices.Contains(args, "ControlMaster=auto") { | |
| 41 | t.Errorf("no_multiplex ignored: %v", args) | |
| 42 | } | |
| 43 | } | |
internal/cliconfig/cliconfig.go +4
| @@ -21,6 +21,10 @@ type Instance struct { | ||
| 21 | 21 | // e.g. ["-i", "~/.ssh/forge_ed25519"]. Most setups need none: the |
| 22 | 22 | // system ssh already honors ~/.ssh/config and the agent. |
| 23 | 23 | SSHOptions []string `toml:"ssh_options,omitempty"` |
| 24 | // NoMultiplex turns off SSH connection sharing for this instance. On | |
| 25 | // by default: one handshake per five minutes instead of one per | |
| 26 | // command. | |
| 27 | NoMultiplex bool `toml:"no_multiplex,omitempty"` | |
| 24 | 28 | } |
| 25 | 29 | |
| 26 | 30 | func (i Instance) SSHUser() string { |