Merge branch 'jn/ssh-wrappers'

The ssh-variant 'simple' introduced earlier broke existing
installations by not passing --port/-4/-6 and not diagnosing an
attempt to pass these as an error.  Instead, default to
automatically detect how compatible the GIT_SSH/GIT_SSH_COMMAND is
to OpenSSH convention and then error out an invocation to make it
easier to diagnose connection errors.

* jn/ssh-wrappers:
  connect: correct style of C-style comment
  ssh: 'simple' variant does not support --port
  ssh: 'simple' variant does not support -4/-6
  ssh: 'auto' variant to select between 'ssh' and 'simple'
  connect: split ssh option computation to its own function
  connect: split ssh command line options into separate function
  connect: split git:// setup into a separate function
  connect: move no_fork fallback to git_tcp_connect
  ssh test: make copy_ssh_wrapper_as clean up after itself
This commit is contained in:
Junio C Hamano 2017-12-06 09:23:45 -08:00
commit ef47036444
4 changed files with 265 additions and 151 deletions

View file

@ -2108,16 +2108,22 @@ matched against are those given directly to Git commands. This means any URLs
visited as a result of a redirection do not participate in matching.
ssh.variant::
Depending on the value of the environment variables `GIT_SSH` or
`GIT_SSH_COMMAND`, or the config setting `core.sshCommand`, Git
auto-detects whether to adjust its command-line parameters for use
with ssh (OpenSSH), plink or tortoiseplink, as opposed to the default
(simple).
By default, Git determines the command line arguments to use
based on the basename of the configured SSH command (configured
using the environment variable `GIT_SSH` or `GIT_SSH_COMMAND` or
the config setting `core.sshCommand`). If the basename is
unrecognized, Git will attempt to detect support of OpenSSH
options by first invoking the configured SSH command with the
`-G` (print configuration) option and will subsequently use
OpenSSH options (if that is successful) or no options besides
the host and remote command (if it fails).
+
The config variable `ssh.variant` can be set to override this auto-detection;
valid values are `ssh`, `simple`, `plink`, `putty` or `tortoiseplink`. Any
other value will be treated as normal ssh. This setting can be overridden via
the environment variable `GIT_SSH_VARIANT`.
The config variable `ssh.variant` can be set to override this detection.
Valid values are `ssh` (to use OpenSSH options), `plink`, `putty`,
`tortoiseplink`, `simple` (no options except the host and remote command).
The default auto-detection can be explicitly requested using the value
`auto`. Any other value is treated as `ssh`. This setting can also be
overridden via the environment variable `GIT_SSH_VARIANT`.
+
The current command-line parameters used for each variant are as
follows:

320
connect.c
View file

@ -582,12 +582,25 @@ static int git_tcp_connect_sock(char *host, int flags)
#endif /* NO_IPV6 */
static void git_tcp_connect(int fd[2], char *host, int flags)
/*
* Dummy child_process returned by git_connect() if the transport protocol
* does not need fork(2).
*/
static struct child_process no_fork = CHILD_PROCESS_INIT;
int git_connection_is_socket(struct child_process *conn)
{
return conn == &no_fork;
}
static struct child_process *git_tcp_connect(int fd[2], char *host, int flags)
{
int sockfd = git_tcp_connect_sock(host, flags);
fd[0] = sockfd;
fd[1] = dup(sockfd);
return &no_fork;
}
@ -761,8 +774,6 @@ static enum protocol parse_connect_url(const char *url_orig, char **ret_host,
return protocol;
}
static struct child_process no_fork = CHILD_PROCESS_INIT;
static const char *get_ssh_command(void)
{
const char *ssh;
@ -777,6 +788,7 @@ static const char *get_ssh_command(void)
}
enum ssh_variant {
VARIANT_AUTO,
VARIANT_SIMPLE,
VARIANT_SSH,
VARIANT_PLINK,
@ -784,14 +796,16 @@ enum ssh_variant {
VARIANT_TORTOISEPLINK,
};
static int override_ssh_variant(enum ssh_variant *ssh_variant)
static void override_ssh_variant(enum ssh_variant *ssh_variant)
{
const char *variant = getenv("GIT_SSH_VARIANT");
if (!variant && git_config_get_string_const("ssh.variant", &variant))
return 0;
return;
if (!strcmp(variant, "plink"))
if (!strcmp(variant, "auto"))
*ssh_variant = VARIANT_AUTO;
else if (!strcmp(variant, "plink"))
*ssh_variant = VARIANT_PLINK;
else if (!strcmp(variant, "putty"))
*ssh_variant = VARIANT_PUTTY;
@ -801,18 +815,18 @@ static int override_ssh_variant(enum ssh_variant *ssh_variant)
*ssh_variant = VARIANT_SIMPLE;
else
*ssh_variant = VARIANT_SSH;
return 1;
}
static enum ssh_variant determine_ssh_variant(const char *ssh_command,
int is_cmdline)
{
enum ssh_variant ssh_variant = VARIANT_SIMPLE;
enum ssh_variant ssh_variant = VARIANT_AUTO;
const char *variant;
char *p = NULL;
if (override_ssh_variant(&ssh_variant))
override_ssh_variant(&ssh_variant);
if (ssh_variant != VARIANT_AUTO)
return ssh_variant;
if (!is_cmdline) {
@ -851,11 +865,181 @@ static enum ssh_variant determine_ssh_variant(const char *ssh_command,
}
/*
* This returns a dummy child_process if the transport protocol does not
* need fork(2), or a struct child_process object if it does. Once done,
* finish the connection with finish_connect() with the value returned from
* this function (it is safe to call finish_connect() with NULL to support
* the former case).
* Open a connection using Git's native protocol.
*
* The caller is responsible for freeing hostandport, but this function may
* modify it (for example, to truncate it to remove the port part).
*/
static struct child_process *git_connect_git(int fd[2], char *hostandport,
const char *path, const char *prog,
int flags)
{
struct child_process *conn;
struct strbuf request = STRBUF_INIT;
/*
* Set up virtual host information based on where we will
* connect, unless the user has overridden us in
* the environment.
*/
char *target_host = getenv("GIT_OVERRIDE_VIRTUAL_HOST");
if (target_host)
target_host = xstrdup(target_host);
else
target_host = xstrdup(hostandport);
transport_check_allowed("git");
/*
* These underlying connection commands die() if they
* cannot connect.
*/
if (git_use_proxy(hostandport))
conn = git_proxy_connect(fd, hostandport);
else
conn = git_tcp_connect(fd, hostandport, flags);
/*
* Separate original protocol components prog and path
* from extended host header with a NUL byte.
*
* Note: Do not add any other headers here! Doing so
* will cause older git-daemon servers to crash.
*/
strbuf_addf(&request,
"%s %s%chost=%s%c",
prog, path, 0,
target_host, 0);
/* If using a new version put that stuff here after a second null byte */
if (get_protocol_version_config() > 0) {
strbuf_addch(&request, '\0');
strbuf_addf(&request, "version=%d%c",
get_protocol_version_config(), '\0');
}
packet_write(fd[1], request.buf, request.len);
free(target_host);
strbuf_release(&request);
return conn;
}
/*
* Append the appropriate environment variables to `env` and options to
* `args` for running ssh in Git's SSH-tunneled transport.
*/
static void push_ssh_options(struct argv_array *args, struct argv_array *env,
enum ssh_variant variant, const char *port,
int flags)
{
if (variant == VARIANT_SSH &&
get_protocol_version_config() > 0) {
argv_array_push(args, "-o");
argv_array_push(args, "SendEnv=" GIT_PROTOCOL_ENVIRONMENT);
argv_array_pushf(env, GIT_PROTOCOL_ENVIRONMENT "=version=%d",
get_protocol_version_config());
}
if (flags & CONNECT_IPV4) {
switch (variant) {
case VARIANT_AUTO:
BUG("VARIANT_AUTO passed to push_ssh_options");
case VARIANT_SIMPLE:
die("ssh variant 'simple' does not support -4");
case VARIANT_SSH:
case VARIANT_PLINK:
case VARIANT_PUTTY:
case VARIANT_TORTOISEPLINK:
argv_array_push(args, "-4");
}
} else if (flags & CONNECT_IPV6) {
switch (variant) {
case VARIANT_AUTO:
BUG("VARIANT_AUTO passed to push_ssh_options");
case VARIANT_SIMPLE:
die("ssh variant 'simple' does not support -6");
case VARIANT_SSH:
case VARIANT_PLINK:
case VARIANT_PUTTY:
case VARIANT_TORTOISEPLINK:
argv_array_push(args, "-6");
}
}
if (variant == VARIANT_TORTOISEPLINK)
argv_array_push(args, "-batch");
if (port) {
switch (variant) {
case VARIANT_AUTO:
BUG("VARIANT_AUTO passed to push_ssh_options");
case VARIANT_SIMPLE:
die("ssh variant 'simple' does not support setting port");
case VARIANT_SSH:
argv_array_push(args, "-p");
break;
case VARIANT_PLINK:
case VARIANT_PUTTY:
case VARIANT_TORTOISEPLINK:
argv_array_push(args, "-P");
}
argv_array_push(args, port);
}
}
/* Prepare a child_process for use by Git's SSH-tunneled transport. */
static void fill_ssh_args(struct child_process *conn, const char *ssh_host,
const char *port, int flags)
{
const char *ssh;
enum ssh_variant variant;
if (looks_like_command_line_option(ssh_host))
die("strange hostname '%s' blocked", ssh_host);
ssh = get_ssh_command();
if (ssh) {
variant = determine_ssh_variant(ssh, 1);
} else {
/*
* GIT_SSH is the no-shell version of
* GIT_SSH_COMMAND (and must remain so for
* historical compatibility).
*/
conn->use_shell = 0;
ssh = getenv("GIT_SSH");
if (!ssh)
ssh = "ssh";
variant = determine_ssh_variant(ssh, 0);
}
if (variant == VARIANT_AUTO) {
struct child_process detect = CHILD_PROCESS_INIT;
detect.use_shell = conn->use_shell;
detect.no_stdin = detect.no_stdout = detect.no_stderr = 1;
argv_array_push(&detect.args, ssh);
argv_array_push(&detect.args, "-G");
push_ssh_options(&detect.args, &detect.env_array,
VARIANT_SSH, port, flags);
argv_array_push(&detect.args, ssh_host);
variant = run_command(&detect) ? VARIANT_SIMPLE : VARIANT_SSH;
}
argv_array_push(&conn->args, ssh);
push_ssh_options(&conn->args, &conn->env_array, variant, port, flags);
argv_array_push(&conn->args, ssh_host);
}
/*
* This returns the dummy child_process `no_fork` if the transport protocol
* does not need fork(2), or a struct child_process object if it does. Once
* done, finish the connection with finish_connect() with the value returned
* from this function (it is safe to call finish_connect() with NULL to
* support the former case).
*
* If it returns, the connect is successful; it just dies on errors (this
* will hopefully be changed in a libification effort, to return NULL when
@ -865,7 +1049,7 @@ struct child_process *git_connect(int fd[2], const char *url,
const char *prog, int flags)
{
char *hostandport, *path;
struct child_process *conn = &no_fork;
struct child_process *conn;
enum protocol protocol;
/* Without this we cannot rely on waitpid() to tell
@ -881,50 +1065,7 @@ struct child_process *git_connect(int fd[2], const char *url,
printf("Diag: path=%s\n", path ? path : "NULL");
conn = NULL;
} else if (protocol == PROTO_GIT) {
struct strbuf request = STRBUF_INIT;
/*
* Set up virtual host information based on where we will
* connect, unless the user has overridden us in
* the environment.
*/
char *target_host = getenv("GIT_OVERRIDE_VIRTUAL_HOST");
if (target_host)
target_host = xstrdup(target_host);
else
target_host = xstrdup(hostandport);
transport_check_allowed("git");
/* These underlying connection commands die() if they
* cannot connect.
*/
if (git_use_proxy(hostandport))
conn = git_proxy_connect(fd, hostandport);
else
git_tcp_connect(fd, hostandport, flags);
/*
* Separate original protocol components prog and path
* from extended host header with a NUL byte.
*
* Note: Do not add any other headers here! Doing so
* will cause older git-daemon servers to crash.
*/
strbuf_addf(&request,
"%s %s%chost=%s%c",
prog, path, 0,
target_host, 0);
/* If using a new version put that stuff here after a second null byte */
if (get_protocol_version_config() > 0) {
strbuf_addch(&request, '\0');
strbuf_addf(&request, "version=%d%c",
get_protocol_version_config(), '\0');
}
packet_write(fd[1], request.buf, request.len);
free(target_host);
strbuf_release(&request);
conn = git_connect_git(fd, hostandport, path, prog, flags);
} else {
struct strbuf cmd = STRBUF_INIT;
const char *const *var;
@ -946,8 +1087,6 @@ struct child_process *git_connect(int fd[2], const char *url,
conn->use_shell = 1;
conn->in = conn->out = -1;
if (protocol == PROTO_SSH) {
const char *ssh;
enum ssh_variant variant;
char *ssh_host = hostandport;
const char *port = NULL;
transport_check_allowed("ssh");
@ -969,57 +1108,7 @@ struct child_process *git_connect(int fd[2], const char *url,
strbuf_release(&cmd);
return NULL;
}
if (looks_like_command_line_option(ssh_host))
die("strange hostname '%s' blocked", ssh_host);
ssh = get_ssh_command();
if (ssh) {
variant = determine_ssh_variant(ssh, 1);
} else {
/*
* GIT_SSH is the no-shell version of
* GIT_SSH_COMMAND (and must remain so for
* historical compatibility).
*/
conn->use_shell = 0;
ssh = getenv("GIT_SSH");
if (!ssh)
ssh = "ssh";
variant = determine_ssh_variant(ssh, 0);
}
argv_array_push(&conn->args, ssh);
if (variant == VARIANT_SSH &&
get_protocol_version_config() > 0) {
argv_array_push(&conn->args, "-o");
argv_array_push(&conn->args, "SendEnv=" GIT_PROTOCOL_ENVIRONMENT);
argv_array_pushf(&conn->env_array, GIT_PROTOCOL_ENVIRONMENT "=version=%d",
get_protocol_version_config());
}
if (variant != VARIANT_SIMPLE) {
if (flags & CONNECT_IPV4)
argv_array_push(&conn->args, "-4");
else if (flags & CONNECT_IPV6)
argv_array_push(&conn->args, "-6");
}
if (variant == VARIANT_TORTOISEPLINK)
argv_array_push(&conn->args, "-batch");
if (port && variant != VARIANT_SIMPLE) {
if (variant == VARIANT_SSH)
argv_array_push(&conn->args, "-p");
else
argv_array_push(&conn->args, "-P");
argv_array_push(&conn->args, port);
}
argv_array_push(&conn->args, ssh_host);
fill_ssh_args(conn, ssh_host, port, flags);
} else {
transport_check_allowed("file");
if (get_protocol_version_config() > 0) {
@ -1041,11 +1130,6 @@ struct child_process *git_connect(int fd[2], const char *url,
return conn;
}
int git_connection_is_socket(struct child_process *conn)
{
return conn == &no_fork;
}
int finish_connect(struct child_process *conn)
{
int code;

View file

@ -306,23 +306,21 @@ test_expect_success 'clone checking out a tag' '
test_cmp fetch.expected fetch.actual
'
setup_ssh_wrapper () {
test_expect_success 'setup ssh wrapper' '
rm -f "$TRASH_DIRECTORY/ssh$X" &&
cp "$GIT_BUILD_DIR/t/helper/test-fake-ssh$X" \
"$TRASH_DIRECTORY/ssh$X" &&
GIT_SSH="$TRASH_DIRECTORY/ssh$X" &&
export GIT_SSH &&
export TRASH_DIRECTORY &&
>"$TRASH_DIRECTORY"/ssh-output
'
}
test_expect_success 'set up ssh wrapper' '
cp "$GIT_BUILD_DIR/t/helper/test-fake-ssh$X" \
"$TRASH_DIRECTORY/ssh$X" &&
GIT_SSH="$TRASH_DIRECTORY/ssh$X" &&
export GIT_SSH &&
export TRASH_DIRECTORY &&
>"$TRASH_DIRECTORY"/ssh-output
'
copy_ssh_wrapper_as () {
rm -f "${1%$X}$X" &&
cp "$TRASH_DIRECTORY/ssh$X" "${1%$X}$X" &&
test_when_finished "rm $(git rev-parse --sq-quote "${1%$X}$X")" &&
GIT_SSH="${1%$X}$X" &&
export GIT_SSH
test_when_finished "GIT_SSH=\"\$TRASH_DIRECTORY/ssh\$X\""
}
expect_ssh () {
@ -346,8 +344,6 @@ expect_ssh () {
(cd "$TRASH_DIRECTORY" && test_cmp ssh-expect ssh-output)
}
setup_ssh_wrapper
test_expect_success 'clone myhost:src uses ssh' '
git clone myhost:src ssh-clone &&
expect_ssh myhost src
@ -369,23 +365,50 @@ test_expect_success 'OpenSSH variant passes -4' '
expect_ssh "-4 -p 123" myhost src
'
test_expect_success 'variant can be overriden' '
git -c ssh.variant=simple clone -4 "[myhost:123]:src" ssh-simple-clone &&
expect_ssh myhost src
test_expect_success 'variant can be overridden' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/putty" &&
git -c ssh.variant=putty clone -4 "[myhost:123]:src" ssh-putty-clone &&
expect_ssh "-4 -P 123" myhost src
'
test_expect_success 'simple is treated as simple' '
test_expect_success 'variant=auto picks based on basename' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" &&
git -c ssh.variant=auto clone -4 "[myhost:123]:src" ssh-auto-clone &&
expect_ssh "-4 -P 123" myhost src
'
test_expect_success 'simple does not support -4/-6' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/simple" &&
git clone -4 "[myhost:123]:src" ssh-bracket-clone-simple &&
expect_ssh myhost src
test_must_fail git clone -4 "myhost:src" ssh-4-clone-simple
'
test_expect_success 'simple does not support port' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/simple" &&
test_must_fail git clone "[myhost:123]:src" ssh-bracket-clone-simple
'
test_expect_success 'uplink is treated as simple' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/uplink" &&
git clone "[myhost:123]:src" ssh-bracket-clone-uplink &&
test_must_fail git clone "[myhost:123]:src" ssh-bracket-clone-uplink &&
git clone "myhost:src" ssh-clone-uplink &&
expect_ssh myhost src
'
test_expect_success 'OpenSSH-like uplink is treated as ssh' '
write_script "$TRASH_DIRECTORY/uplink" <<-EOF &&
if test "\$1" = "-G"
then
exit 0
fi &&
exec "\$TRASH_DIRECTORY/ssh$X" "\$@"
EOF
test_when_finished "rm -f \"\$TRASH_DIRECTORY/uplink\"" &&
GIT_SSH="$TRASH_DIRECTORY/uplink" &&
test_when_finished "GIT_SSH=\"\$TRASH_DIRECTORY/ssh\$X\"" &&
git clone "[myhost:123]:src" ssh-bracket-clone-sshlike-uplink &&
expect_ssh "-p 123" myhost src
'
test_expect_success 'plink is treated specially (as putty)' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" &&
git clone "[myhost:123]:src" ssh-bracket-clone-plink-0 &&
@ -434,12 +457,14 @@ test_expect_success 'ssh.variant overrides plink detection' '
'
test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" &&
GIT_SSH_VARIANT=plink \
git clone "[myhost:123]:src" ssh-bracket-clone-variant-3 &&
expect_ssh "-P 123" myhost src
'
test_expect_success 'GIT_SSH_VARIANT overrides plink to tortoiseplink' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" &&
GIT_SSH_VARIANT=tortoiseplink \
git clone "[myhost:123]:src" ssh-bracket-clone-variant-4 &&
expect_ssh "-batch -P 123" myhost src
@ -451,9 +476,6 @@ test_expect_success 'clean failure on broken quoting' '
git clone "[myhost:123]:src" sq-failure
'
# Reset the GIT_SSH environment variable for clone tests.
setup_ssh_wrapper
counter=0
# $1 url
# $2 none|host

View file

@ -11,7 +11,9 @@ test_expect_success 'setup ssh wrapper' '
git upload-pack "$TRASH_DIRECTORY"
EOF
GIT_SSH="$TRASH_DIRECTORY/ssh-wrapper" &&
GIT_SSH_VARIANT=ssh &&
export GIT_SSH &&
export GIT_SSH_VARIANT &&
export TRASH_DIRECTORY
'