TestSSHProxy_CommandQuoting verifies that the proxy preserves shell quoting when forwarding commands to the backend. This is critical for tools like Ansible that send commands such as: /bin/sh -c '( umask 77 && mkdir -p ... ) && sleep 0' The single quotes must be preserved so the backend shell re
(t *testing.T)
| 186 | // The single quotes must be preserved so the backend shell receives the |
| 187 | // subshell expression as a single argument to -c. |
| 188 | func TestSSHProxy_CommandQuoting(t *testing.T) { |
| 189 | if testing.Short() { |
| 190 | t.Skip("Skipping integration test in short mode") |
| 191 | } |
| 192 | |
| 193 | sshClient, cleanup := setupProxySSHClient(t) |
| 194 | defer cleanup() |
| 195 | |
| 196 | // These commands simulate what the SSH protocol delivers as exec payloads. |
| 197 | // When a user types: ssh host '/bin/sh -c "( echo hello )"' |
| 198 | // the local shell strips the outer single quotes, and the SSH exec request |
| 199 | // contains the raw string: /bin/sh -c "( echo hello )" |
| 200 | // |
| 201 | // The proxy must forward this string verbatim. Using session.Command() |
| 202 | // (shlex.Split + strings.Join) strips the inner double quotes, breaking |
| 203 | // the command on the backend. |
| 204 | tests := []struct { |
| 205 | name string |
| 206 | command string |
| 207 | expect string |
| 208 | }{ |
| 209 | { |
| 210 | name: "subshell_in_double_quotes", |
| 211 | command: `/bin/sh -c "( echo from-subshell ) && echo outer"`, |
| 212 | expect: "from-subshell\nouter\n", |
| 213 | }, |
| 214 | { |
| 215 | name: "printf_with_special_chars", |
| 216 | command: `/bin/sh -c "printf '%s\n' 'hello world'"`, |
| 217 | expect: "hello world\n", |
| 218 | }, |
| 219 | { |
| 220 | name: "nested_command_substitution", |
| 221 | command: `/bin/sh -c "echo $(echo nested)"`, |
| 222 | expect: "nested\n", |
| 223 | }, |
| 224 | } |
| 225 | |
| 226 | for _, tc := range tests { |
| 227 | t.Run(tc.name, func(t *testing.T) { |
| 228 | session, err := sshClient.NewSession() |
| 229 | require.NoError(t, err) |
| 230 | defer func() { _ = session.Close() }() |
| 231 | |
| 232 | var stderrBuf bytes.Buffer |
| 233 | session.Stderr = &stderrBuf |
| 234 | |
| 235 | outputCh := make(chan []byte, 1) |
| 236 | errCh := make(chan error, 1) |
| 237 | go func() { |
| 238 | output, err := session.Output(tc.command) |
| 239 | outputCh <- output |
| 240 | errCh <- err |
| 241 | }() |
| 242 | |
| 243 | select { |
| 244 | case output := <-outputCh: |
| 245 | err := <-errCh |