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. visited as a result of a redirection do not participate in matching.
ssh.variant:: ssh.variant::
Depending on the value of the environment variables `GIT_SSH` or By default, Git determines the command line arguments to use
`GIT_SSH_COMMAND`, or the config setting `core.sshCommand`, Git based on the basename of the configured SSH command (configured
auto-detects whether to adjust its command-line parameters for use using the environment variable `GIT_SSH` or `GIT_SSH_COMMAND` or
with ssh (OpenSSH), plink or tortoiseplink, as opposed to the default the config setting `core.sshCommand`). If the basename is
(simple). 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; The config variable `ssh.variant` can be set to override this detection.
valid values are `ssh`, `simple`, `plink`, `putty` or `tortoiseplink`. Any Valid values are `ssh` (to use OpenSSH options), `plink`, `putty`,
other value will be treated as normal ssh. This setting can be overridden via `tortoiseplink`, `simple` (no options except the host and remote command).
the environment variable `GIT_SSH_VARIANT`. 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 The current command-line parameters used for each variant are as
follows: follows:

320
connect.c
View file

@ -582,12 +582,25 @@ static int git_tcp_connect_sock(char *host, int flags)
#endif /* NO_IPV6 */ #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); int sockfd = git_tcp_connect_sock(host, flags);
fd[0] = sockfd; fd[0] = sockfd;
fd[1] = dup(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; return protocol;
} }
static struct child_process no_fork = CHILD_PROCESS_INIT;
static const char *get_ssh_command(void) static const char *get_ssh_command(void)
{ {
const char *ssh; const char *ssh;
@ -777,6 +788,7 @@ static const char *get_ssh_command(void)
} }
enum ssh_variant { enum ssh_variant {
VARIANT_AUTO,
VARIANT_SIMPLE, VARIANT_SIMPLE,
VARIANT_SSH, VARIANT_SSH,
VARIANT_PLINK, VARIANT_PLINK,
@ -784,14 +796,16 @@ enum ssh_variant {
VARIANT_TORTOISEPLINK, 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"); const char *variant = getenv("GIT_SSH_VARIANT");
if (!variant && git_config_get_string_const("ssh.variant", &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; *ssh_variant = VARIANT_PLINK;
else if (!strcmp(variant, "putty")) else if (!strcmp(variant, "putty"))
*ssh_variant = VARIANT_PUTTY; *ssh_variant = VARIANT_PUTTY;
@ -801,18 +815,18 @@ static int override_ssh_variant(enum ssh_variant *ssh_variant)
*ssh_variant = VARIANT_SIMPLE; *ssh_variant = VARIANT_SIMPLE;
else else
*ssh_variant = VARIANT_SSH; *ssh_variant = VARIANT_SSH;
return 1;
} }
static enum ssh_variant determine_ssh_variant(const char *ssh_command, static enum ssh_variant determine_ssh_variant(const char *ssh_command,
int is_cmdline) int is_cmdline)
{ {
enum ssh_variant ssh_variant = VARIANT_SIMPLE; enum ssh_variant ssh_variant = VARIANT_AUTO;
const char *variant; const char *variant;
char *p = NULL; char *p = NULL;
if (override_ssh_variant(&ssh_variant)) override_ssh_variant(&ssh_variant);
if (ssh_variant != VARIANT_AUTO)
return ssh_variant; return ssh_variant;
if (!is_cmdline) { 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 * Open a connection using Git's native protocol.
* 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 * The caller is responsible for freeing hostandport, but this function may
* this function (it is safe to call finish_connect() with NULL to support * modify it (for example, to truncate it to remove the port part).
* the former case). */
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 * 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 * 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) const char *prog, int flags)
{ {
char *hostandport, *path; char *hostandport, *path;
struct child_process *conn = &no_fork; struct child_process *conn;
enum protocol protocol; enum protocol protocol;
/* Without this we cannot rely on waitpid() to tell /* 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"); printf("Diag: path=%s\n", path ? path : "NULL");
conn = NULL; conn = NULL;
} else if (protocol == PROTO_GIT) { } else if (protocol == PROTO_GIT) {
struct strbuf request = STRBUF_INIT; conn = git_connect_git(fd, hostandport, path, prog, flags);
/*
* 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);
} else { } else {
struct strbuf cmd = STRBUF_INIT; struct strbuf cmd = STRBUF_INIT;
const char *const *var; const char *const *var;
@ -946,8 +1087,6 @@ struct child_process *git_connect(int fd[2], const char *url,
conn->use_shell = 1; conn->use_shell = 1;
conn->in = conn->out = -1; conn->in = conn->out = -1;
if (protocol == PROTO_SSH) { if (protocol == PROTO_SSH) {
const char *ssh;
enum ssh_variant variant;
char *ssh_host = hostandport; char *ssh_host = hostandport;
const char *port = NULL; const char *port = NULL;
transport_check_allowed("ssh"); transport_check_allowed("ssh");
@ -969,57 +1108,7 @@ struct child_process *git_connect(int fd[2], const char *url,
strbuf_release(&cmd); strbuf_release(&cmd);
return NULL; return NULL;
} }
fill_ssh_args(conn, ssh_host, port, flags);
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);
} else { } else {
transport_check_allowed("file"); transport_check_allowed("file");
if (get_protocol_version_config() > 0) { if (get_protocol_version_config() > 0) {
@ -1041,11 +1130,6 @@ struct child_process *git_connect(int fd[2], const char *url,
return conn; return conn;
} }
int git_connection_is_socket(struct child_process *conn)
{
return conn == &no_fork;
}
int finish_connect(struct child_process *conn) int finish_connect(struct child_process *conn)
{ {
int code; int code;

View file

@ -306,23 +306,21 @@ test_expect_success 'clone checking out a tag' '
test_cmp fetch.expected fetch.actual test_cmp fetch.expected fetch.actual
' '
setup_ssh_wrapper () { test_expect_success 'set up ssh wrapper' '
test_expect_success 'setup ssh wrapper' ' cp "$GIT_BUILD_DIR/t/helper/test-fake-ssh$X" \
rm -f "$TRASH_DIRECTORY/ssh$X" && "$TRASH_DIRECTORY/ssh$X" &&
cp "$GIT_BUILD_DIR/t/helper/test-fake-ssh$X" \ GIT_SSH="$TRASH_DIRECTORY/ssh$X" &&
"$TRASH_DIRECTORY/ssh$X" && export GIT_SSH &&
GIT_SSH="$TRASH_DIRECTORY/ssh$X" && export TRASH_DIRECTORY &&
export GIT_SSH && >"$TRASH_DIRECTORY"/ssh-output
export TRASH_DIRECTORY && '
>"$TRASH_DIRECTORY"/ssh-output
'
}
copy_ssh_wrapper_as () { copy_ssh_wrapper_as () {
rm -f "${1%$X}$X" && rm -f "${1%$X}$X" &&
cp "$TRASH_DIRECTORY/ssh$X" "${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" && GIT_SSH="${1%$X}$X" &&
export GIT_SSH test_when_finished "GIT_SSH=\"\$TRASH_DIRECTORY/ssh\$X\""
} }
expect_ssh () { expect_ssh () {
@ -346,8 +344,6 @@ expect_ssh () {
(cd "$TRASH_DIRECTORY" && test_cmp ssh-expect ssh-output) (cd "$TRASH_DIRECTORY" && test_cmp ssh-expect ssh-output)
} }
setup_ssh_wrapper
test_expect_success 'clone myhost:src uses ssh' ' test_expect_success 'clone myhost:src uses ssh' '
git clone myhost:src ssh-clone && git clone myhost:src ssh-clone &&
expect_ssh myhost src expect_ssh myhost src
@ -369,23 +365,50 @@ test_expect_success 'OpenSSH variant passes -4' '
expect_ssh "-4 -p 123" myhost src expect_ssh "-4 -p 123" myhost src
' '
test_expect_success 'variant can be overriden' ' test_expect_success 'variant can be overridden' '
git -c ssh.variant=simple clone -4 "[myhost:123]:src" ssh-simple-clone && copy_ssh_wrapper_as "$TRASH_DIRECTORY/putty" &&
expect_ssh myhost src 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" && copy_ssh_wrapper_as "$TRASH_DIRECTORY/simple" &&
git clone -4 "[myhost:123]:src" ssh-bracket-clone-simple && test_must_fail git clone -4 "myhost:src" ssh-4-clone-simple
expect_ssh myhost src '
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' ' test_expect_success 'uplink is treated as simple' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/uplink" && 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 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)' ' test_expect_success 'plink is treated specially (as putty)' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" && copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" &&
git clone "[myhost:123]:src" ssh-bracket-clone-plink-0 && 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' ' test_expect_success 'GIT_SSH_VARIANT overrides plink detection to plink' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" &&
GIT_SSH_VARIANT=plink \ GIT_SSH_VARIANT=plink \
git clone "[myhost:123]:src" ssh-bracket-clone-variant-3 && git clone "[myhost:123]:src" ssh-bracket-clone-variant-3 &&
expect_ssh "-P 123" myhost src expect_ssh "-P 123" myhost src
' '
test_expect_success 'GIT_SSH_VARIANT overrides plink to tortoiseplink' ' test_expect_success 'GIT_SSH_VARIANT overrides plink to tortoiseplink' '
copy_ssh_wrapper_as "$TRASH_DIRECTORY/plink" &&
GIT_SSH_VARIANT=tortoiseplink \ GIT_SSH_VARIANT=tortoiseplink \
git clone "[myhost:123]:src" ssh-bracket-clone-variant-4 && git clone "[myhost:123]:src" ssh-bracket-clone-variant-4 &&
expect_ssh "-batch -P 123" myhost src 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 git clone "[myhost:123]:src" sq-failure
' '
# Reset the GIT_SSH environment variable for clone tests.
setup_ssh_wrapper
counter=0 counter=0
# $1 url # $1 url
# $2 none|host # $2 none|host

View file

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