summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVicent Marti <vicent@github.com>2014-08-26 17:48:06 +0200
committerVicent Marti <vicent@github.com>2014-08-26 17:48:06 +0200
commit94f74ad28f191e7dc17ae874c00e3ecd6a80fcf4 (patch)
tree3128f3eac456486814fdbc5529e44e127316c519
parent1589aa0c4d48fb130d8a5db28c45cd3d173cde6d (diff)
parent8733993599d9d9a8fe1b042a1e18d6ecb9b0a0b2 (diff)
downloadlibgit2-94f74ad28f191e7dc17ae874c00e3ecd6a80fcf4.tar.gz
Merge pull request #2422 from libgit2/cmn/ssh-retry
Enable retries for SSH
-rw-r--r--include/git2/errors.h1
-rw-r--r--include/git2/transport.h22
-rwxr-xr-xscript/cibuild.sh10
-rw-r--r--src/transports/cred.c58
-rw-r--r--src/transports/cred.h14
-rw-r--r--src/transports/cred_helpers.c3
-rw-r--r--src/transports/ssh.c175
-rw-r--r--tests/online/clone.c80
-rw-r--r--tests/online/push.c9
9 files changed, 333 insertions, 39 deletions
diff --git a/include/git2/errors.h b/include/git2/errors.h
index c914653fc..b91560631 100644
--- a/include/git2/errors.h
+++ b/include/git2/errors.h
@@ -41,6 +41,7 @@ typedef enum {
GIT_EMERGECONFLICT = -13, /**< Merge conflicts prevented operation */
GIT_ELOCKED = -14, /**< Lock file prevented operation */
GIT_EMODIFIED = -15, /**< Reference value does not match expected */
+ GIT_EAUTH = -16, /**< Authentication error */
GIT_PASSTHROUGH = -30, /**< Internal only */
GIT_ITEROVER = -31, /**< Signals end of iteration with iterator */
diff --git a/include/git2/transport.h b/include/git2/transport.h
index af7812b5d..b57d1dd7f 100644
--- a/include/git2/transport.h
+++ b/include/git2/transport.h
@@ -44,6 +44,14 @@ typedef enum {
/* git_cred_ssh_interactive */
GIT_CREDTYPE_SSH_INTERACTIVE = (1u << 4),
+
+ /**
+ * Username-only information
+ *
+ * If the SSH transport does not know which username to use,
+ * it will ask via this credential type.
+ */
+ GIT_CREDTYPE_USERNAME = (1u << 5),
} git_credtype_t;
/* The base structure for all credential types */
@@ -105,6 +113,12 @@ typedef struct git_cred_ssh_custom {
/** A key for NTLM/Kerberos "default" credentials */
typedef struct git_cred git_cred_default;
+/** Username-only credential information */
+typedef struct git_cred_username {
+ git_cred parent;
+ char username[1];
+} git_cred_username;
+
/**
* Check whether a credential object contains username information.
*
@@ -207,6 +221,14 @@ GIT_EXTERN(int) git_cred_ssh_custom_new(
GIT_EXTERN(int) git_cred_default_new(git_cred **out);
/**
+ * Create a credential to specify a username.
+ *
+ * This is used with ssh authentication to query for the username if
+ * none is specified in the url.
+ */
+GIT_EXTERN(int) git_cred_username_new(git_cred **cred, const char *username);
+
+/**
* Signature of a function which acquires a credential object.
*
* - cred: The newly created credential object.
diff --git a/script/cibuild.sh b/script/cibuild.sh
index 699404bd2..ef2ac6e8e 100755
--- a/script/cibuild.sh
+++ b/script/cibuild.sh
@@ -22,7 +22,13 @@ ctest -V . || exit $?
# can do the push tests over it
killall git-daemon
-sudo start ssh
+
+if [ "$TRAVIS_OS_NAME" = "osx" ]; then
+ echo 'PasswordAuthentication yes' | sudo tee -a /etc/sshd_config
+else
+ sudo start ssh
+fi
+
ssh-keygen -t rsa -f ~/.ssh/id_rsa -N "" -q
cat ~/.ssh/id_rsa.pub >>~/.ssh/authorized_keys
ssh-keyscan -t rsa localhost >>~/.ssh/known_hosts
@@ -34,5 +40,5 @@ export GITTEST_REMOTE_SSH_PUBKEY="$HOME/.ssh/id_rsa.pub"
export GITTEST_REMOTE_SSH_PASSPHRASE=""
if [ -e ./libgit2_clar ]; then
- ./libgit2_clar -sonline::push -sonline::clone::cred_callback_failure
+ ./libgit2_clar -sonline::push -sonline::clone::cred_callback
fi
diff --git a/src/transports/cred.c b/src/transports/cred.c
index 913ec36cc..1b4d29c0a 100644
--- a/src/transports/cred.c
+++ b/src/transports/cred.c
@@ -17,6 +17,40 @@ int git_cred_has_username(git_cred *cred)
return 1;
}
+const char *git_cred__username(git_cred *cred)
+{
+ switch (cred->credtype) {
+ case GIT_CREDTYPE_USERNAME:
+ {
+ git_cred_username *c = (git_cred_username *) cred;
+ return c->username;
+ }
+ case GIT_CREDTYPE_USERPASS_PLAINTEXT:
+ {
+ git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *) cred;
+ return c->username;
+ }
+ case GIT_CREDTYPE_SSH_KEY:
+ {
+ git_cred_ssh_key *c = (git_cred_ssh_key *) cred;
+ return c->username;
+ }
+ case GIT_CREDTYPE_SSH_CUSTOM:
+ {
+ git_cred_ssh_custom *c = (git_cred_ssh_custom *) cred;
+ return c->username;
+ }
+ case GIT_CREDTYPE_SSH_INTERACTIVE:
+ {
+ git_cred_ssh_interactive *c = (git_cred_ssh_interactive *) cred;
+ return c->username;
+ }
+
+ default:
+ return NULL;
+ }
+}
+
static void plaintext_free(struct git_cred *cred)
{
git_cred_userpass_plaintext *c = (git_cred_userpass_plaintext *)cred;
@@ -129,6 +163,11 @@ static void default_free(struct git_cred *cred)
git__free(c);
}
+static void username_free(struct git_cred *cred)
+{
+ git__free(cred);
+}
+
int git_cred_ssh_key_new(
git_cred **cred,
const char *username,
@@ -263,3 +302,22 @@ int git_cred_default_new(git_cred **cred)
*cred = c;
return 0;
}
+
+int git_cred_username_new(git_cred **cred, const char *username)
+{
+ git_cred_username *c;
+ size_t len;
+
+ assert(cred);
+
+ len = strlen(username);
+ c = git__malloc(sizeof(git_cred_username) + len + 1);
+ GITERR_CHECK_ALLOC(c);
+
+ c->parent.credtype = GIT_CREDTYPE_USERNAME;
+ c->parent.free = username_free;
+ memcpy(c->username, username, len + 1);
+
+ *cred = (git_cred *) c;
+ return 0;
+}
diff --git a/src/transports/cred.h b/src/transports/cred.h
new file mode 100644
index 000000000..2de8deee8
--- /dev/null
+++ b/src/transports/cred.h
@@ -0,0 +1,14 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+#ifndef INCLUDE_git_cred_h__
+#define INCLUDE_git_cred_h__
+
+#include "git2/transport.h"
+
+const char *git_cred__username(git_cred *cred);
+
+#endif
diff --git a/src/transports/cred_helpers.c b/src/transports/cred_helpers.c
index d420e3e3c..5cc9b0869 100644
--- a/src/transports/cred_helpers.c
+++ b/src/transports/cred_helpers.c
@@ -41,6 +41,9 @@ int git_cred_userpass(
else
return -1;
+ if (GIT_CREDTYPE_USERNAME & allowed_types)
+ return git_cred_username_new(cred, effective_username);
+
if ((GIT_CREDTYPE_USERPASS_PLAINTEXT & allowed_types) == 0 ||
git_cred_userpass_plaintext_new(cred, effective_username, userpass->password) < 0)
return -1;
diff --git a/src/transports/ssh.c b/src/transports/ssh.c
index b403727c9..6a7f67e99 100644
--- a/src/transports/ssh.c
+++ b/src/transports/ssh.c
@@ -9,6 +9,7 @@
#include "buffer.h"
#include "netops.h"
#include "smart.h"
+#include "cred.h"
#ifdef GIT_SSH
@@ -34,9 +35,10 @@ typedef struct {
git_smart_subtransport parent;
transport_smart *owner;
ssh_stream *current_stream;
- git_cred *cred;
} ssh_subtransport;
+static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username);
+
static void ssh_error(LIBSSH2_SESSION *session, const char *errmsg)
{
char *ssherr;
@@ -339,6 +341,9 @@ static int _git_ssh_authenticate_session(
}
} while (LIBSSH2_ERROR_EAGAIN == rc || LIBSSH2_ERROR_TIMEOUT == rc);
+ if (rc == LIBSSH2_ERROR_PASSWORD_EXPIRED || rc == LIBSSH2_ERROR_AUTHENTICATION_FAILED)
+ return GIT_EAUTH;
+
if (rc != LIBSSH2_ERROR_NONE) {
ssh_error(session, "Failed to authenticate SSH session");
return -1;
@@ -347,6 +352,43 @@ static int _git_ssh_authenticate_session(
return 0;
}
+static int request_creds(git_cred **out, ssh_subtransport *t, const char *user, int auth_methods)
+{
+ int error, no_callback = 0;
+ git_cred *cred = NULL;
+
+ if (!t->owner->cred_acquire_cb) {
+ no_callback = 1;
+ } else {
+ error = t->owner->cred_acquire_cb(&cred, t->owner->url, user, auth_methods,
+ t->owner->cred_acquire_payload);
+
+ if (error == GIT_PASSTHROUGH)
+ no_callback = 1;
+ else if (error < 0)
+ return error;
+ else if (!cred) {
+ giterr_set(GITERR_SSH, "Callback failed to initialize SSH credentials");
+ return -1;
+ }
+ }
+
+ if (no_callback) {
+ giterr_set(GITERR_SSH, "authentication required but no callback set");
+ return -1;
+ }
+
+ if (!(cred->credtype & auth_methods)) {
+ cred->free(cred);
+ giterr_set(GITERR_SSH, "callback returned unsupported credentials type");
+ return -1;
+ }
+
+ *out = cred;
+
+ return 0;
+}
+
static int _git_ssh_session_create(
LIBSSH2_SESSION** session,
gitno_socket socket)
@@ -387,8 +429,9 @@ static int _git_ssh_setup_conn(
{
char *host=NULL, *port=NULL, *path=NULL, *user=NULL, *pass=NULL;
const char *default_port="22";
- int no_callback = 0;
+ int auth_methods, error = 0;
ssh_stream *s;
+ git_cred *cred = NULL;
LIBSSH2_SESSION* session=NULL;
LIBSSH2_CHANNEL* channel=NULL;
@@ -399,56 +442,68 @@ static int _git_ssh_setup_conn(
s = (ssh_stream *)*stream;
if (!git__prefixcmp(url, prefix_ssh)) {
- if (gitno_extract_url_parts(&host, &port, &path, &user, &pass, url, default_port) < 0)
+ if ((error = gitno_extract_url_parts(&host, &port, &path, &user, &pass, url, default_port)) < 0)
goto on_error;
} else {
- if (git_ssh_extract_url_parts(&host, &user, url) < 0)
+ if ((error = git_ssh_extract_url_parts(&host, &user, url)) < 0)
goto on_error;
port = git__strdup(default_port);
GITERR_CHECK_ALLOC(port);
}
- if (gitno_connect(&s->socket, host, port, 0) < 0)
- goto on_error;
-
- if (user && pass) {
- if (git_cred_userpass_plaintext_new(&t->cred, user, pass) < 0)
+ /* we need the username to ask for auth methods */
+ if (!user) {
+ if ((error = request_creds(&cred, t, NULL, GIT_CREDTYPE_USERNAME)) < 0)
goto on_error;
- } else if (!t->owner->cred_acquire_cb) {
- no_callback = 1;
- } else {
- int error;
- error = t->owner->cred_acquire_cb(&t->cred, t->owner->url, user,
- GIT_CREDTYPE_USERPASS_PLAINTEXT |
- GIT_CREDTYPE_SSH_KEY | GIT_CREDTYPE_SSH_CUSTOM |
- GIT_CREDTYPE_SSH_INTERACTIVE,
- t->owner->cred_acquire_payload);
- if (error == GIT_PASSTHROUGH)
- no_callback = 1;
- else if (error < 0)
+ user = git__strdup(((git_cred_username *) cred)->username);
+ cred->free(cred);
+ cred = NULL;
+ if (!user)
goto on_error;
- else if (!t->cred) {
- giterr_set(GITERR_SSH, "Callback failed to initialize SSH credentials");
+ } else if (user && pass) {
+ if ((error = git_cred_userpass_plaintext_new(&cred, user, pass)) < 0)
goto on_error;
- }
}
- if (no_callback) {
- giterr_set(GITERR_SSH, "authentication required but no callback set");
+ if ((error = gitno_connect(&s->socket, host, port, 0)) < 0)
goto on_error;
- }
- assert(t->cred);
+ if ((error = _git_ssh_session_create(&session, s->socket)) < 0)
+ goto on_error;
- if (_git_ssh_session_create(&session, s->socket) < 0)
+ if ((error = list_auth_methods(&auth_methods, session, user)) < 0)
goto on_error;
- if (_git_ssh_authenticate_session(session, t->cred) < 0)
+ error = GIT_EAUTH;
+ /* if we already have something to try */
+ if (cred && auth_methods & cred->credtype)
+ error = _git_ssh_authenticate_session(session, cred);
+
+ while (error == GIT_EAUTH) {
+ if (cred) {
+ cred->free(cred);
+ cred = NULL;
+ }
+
+ if ((error = request_creds(&cred, t, user, auth_methods)) < 0)
+ goto on_error;
+
+ if (strcmp(user, git_cred__username(cred))) {
+ giterr_set(GITERR_SSH, "username does not match previous request");
+ error = -1;
+ goto on_error;
+ }
+
+ error = _git_ssh_authenticate_session(session, cred);
+ }
+
+ if (error < 0)
goto on_error;
channel = libssh2_channel_open_session(session);
if (!channel) {
+ error = -1;
ssh_error(session, "Failed to open SSH channel");
goto on_error;
}
@@ -459,6 +514,9 @@ static int _git_ssh_setup_conn(
s->channel = channel;
t->current_stream = s;
+ if (cred)
+ cred->free(cred);
+
git__free(host);
git__free(port);
git__free(path);
@@ -475,6 +533,9 @@ on_error:
if (*stream)
ssh_stream_free(*stream);
+ if (cred)
+ cred->free(cred);
+
git__free(host);
git__free(port);
git__free(user);
@@ -483,7 +544,7 @@ on_error:
if (session)
libssh2_session_free(session);
- return -1;
+ return error;
}
static int ssh_uploadpack_ls(
@@ -491,10 +552,7 @@ static int ssh_uploadpack_ls(
const char *url,
git_smart_subtransport_stream **stream)
{
- if (_git_ssh_setup_conn(t, url, cmd_uploadpack, stream) < 0)
- return -1;
-
- return 0;
+ return _git_ssh_setup_conn(t, url, cmd_uploadpack, stream);
}
static int ssh_uploadpack(
@@ -585,6 +643,53 @@ static void _ssh_free(git_smart_subtransport *subtransport)
git__free(t);
}
+
+#define SSH_AUTH_PUBLICKEY "publickey"
+#define SSH_AUTH_PASSWORD "password"
+#define SSH_AUTH_KEYBOARD_INTERACTIVE "keyboard-interactive"
+
+static int list_auth_methods(int *out, LIBSSH2_SESSION *session, const char *username)
+{
+ const char *list, *ptr;
+
+ *out = 0;
+
+ list = libssh2_userauth_list(session, username, strlen(username));
+
+ /* either error, or the remote accepts NONE auth, which is bizarre, let's punt */
+ if (list == NULL && !libssh2_userauth_authenticated(session))
+ return -1;
+
+ ptr = list;
+ while (ptr) {
+ if (*ptr == ',')
+ ptr++;
+
+ if (!git__prefixcmp(ptr, SSH_AUTH_PUBLICKEY)) {
+ *out |= GIT_CREDTYPE_SSH_KEY;
+ *out |= GIT_CREDTYPE_SSH_CUSTOM;
+ ptr += strlen(SSH_AUTH_PUBLICKEY);
+ continue;
+ }
+
+ if (!git__prefixcmp(ptr, SSH_AUTH_PASSWORD)) {
+ *out |= GIT_CREDTYPE_USERPASS_PLAINTEXT;
+ ptr += strlen(SSH_AUTH_PASSWORD);
+ continue;
+ }
+
+ if (!git__prefixcmp(ptr, SSH_AUTH_KEYBOARD_INTERACTIVE)) {
+ *out |= GIT_CREDTYPE_SSH_INTERACTIVE;
+ ptr += strlen(SSH_AUTH_KEYBOARD_INTERACTIVE);
+ continue;
+ }
+
+ /* Skipt it if we don't know it */
+ ptr = strchr(ptr, ',');
+ }
+
+ return 0;
+}
#endif
int git_smart_subtransport_ssh(
diff --git a/tests/online/clone.c b/tests/online/clone.c
index 4f4312a8c..cb541acf1 100644
--- a/tests/online/clone.c
+++ b/tests/online/clone.c
@@ -12,6 +12,8 @@
#define BB_REPO_URL_WITH_PASS "https://libgit3:libgit3@bitbucket.org/libgit2/testgitrepository.git"
#define BB_REPO_URL_WITH_WRONG_PASS "https://libgit3:wrong@bitbucket.org/libgit2/testgitrepository.git"
+#define SSH_REPO_URL "ssh://github.com/libgit2/TestGitRepository"
+
static git_repository *g_repo;
static git_clone_options g_options;
@@ -240,8 +242,41 @@ void test_online_clone__cred_callback_failure_return_code_is_tunnelled(void)
g_options.remote_callbacks.credentials = cred_failure_cb;
- /* TODO: this should expect -172. */
- cl_git_fail_with(git_clone(&g_repo, remote_url, "./foo", &g_options), -1);
+ cl_git_fail_with(-172, git_clone(&g_repo, remote_url, "./foo", &g_options));
+}
+
+static int cred_count_calls_cb(git_cred **cred, const char *url, const char *user,
+ unsigned int allowed_types, void *data)
+{
+ size_t *counter = (size_t *) data;
+
+ GIT_UNUSED(url); GIT_UNUSED(user); GIT_UNUSED(allowed_types);
+
+ if (allowed_types == GIT_CREDTYPE_USERNAME)
+ return git_cred_username_new(cred, "foo");
+
+ (*counter)++;
+
+ if (*counter == 3)
+ return GIT_EUSER;
+
+ return git_cred_userpass_plaintext_new(cred, "foo", "bar");
+}
+
+void test_online_clone__cred_callback_called_again_on_auth_failure(void)
+{
+ const char *remote_url = cl_getenv("GITTEST_REMOTE_URL");
+ const char *remote_user = cl_getenv("GITTEST_REMOTE_USER");
+ size_t counter = 0;
+
+ if (!remote_url || !remote_user)
+ clar__skip();
+
+ g_options.remote_callbacks.credentials = cred_count_calls_cb;
+ g_options.remote_callbacks.payload = &counter;
+
+ cl_git_fail_with(GIT_EUSER, git_clone(&g_repo, remote_url, "./foo", &g_options));
+ cl_assert_equal_i(3, counter);
}
void test_online_clone__credentials(void)
@@ -307,7 +342,48 @@ void test_online_clone__can_cancel(void)
}
+static int check_ssh_auth_methods(git_cred **cred, const char *url, const char *username_from_url,
+ unsigned int allowed_types, void *data)
+{
+ int *with_user = (int *) data;
+ GIT_UNUSED(cred); GIT_UNUSED(url); GIT_UNUSED(username_from_url); GIT_UNUSED(data);
+ if (!*with_user)
+ cl_assert_equal_i(GIT_CREDTYPE_USERNAME, allowed_types);
+ else
+ cl_assert(!(allowed_types & GIT_CREDTYPE_USERNAME));
+ return GIT_EUSER;
+}
+void test_online_clone__ssh_auth_methods(void)
+{
+ int with_user;
+
+ g_options.remote_callbacks.credentials = check_ssh_auth_methods;
+ g_options.remote_callbacks.payload = &with_user;
+
+ with_user = 0;
+ cl_git_fail_with(GIT_EUSER,
+ git_clone(&g_repo, SSH_REPO_URL, "./foo", &g_options));
+
+ with_user = 1;
+ cl_git_fail_with(GIT_EUSER,
+ git_clone(&g_repo, "ssh://git@github.com/libgit2/TestGitRepository", "./foo", &g_options));
+}
+static int cred_foo_bar(git_cred **cred, const char *url, const char *username_from_url,
+ unsigned int allowed_types, void *data)
+
+{
+ GIT_UNUSED(url); GIT_UNUSED(username_from_url); GIT_UNUSED(allowed_types); GIT_UNUSED(data);
+
+ return git_cred_userpass_plaintext_new(cred, "foo", "bar");
+}
+
+void test_online_clone__ssh_cannot_change_username(void)
+{
+ g_options.remote_callbacks.credentials = cred_foo_bar;
+
+ cl_git_fail(git_clone(&g_repo, "ssh://git@github.com/libgit2/TestGitRepository", "./foo", &g_options));
+}
diff --git a/tests/online/push.c b/tests/online/push.c
index 6da27bb96..50419efd4 100644
--- a/tests/online/push.c
+++ b/tests/online/push.c
@@ -50,6 +50,15 @@ static int cred_acquire_cb(
GIT_UNUSED(user_from_url);
GIT_UNUSED(payload);
+ if (GIT_CREDTYPE_USERNAME & allowed_types) {
+ if (!_remote_user) {
+ printf("GITTEST_REMOTE_USER must be set\n");
+ return -1;
+ }
+
+ return git_cred_username_new(cred, _remote_user);
+ }
+
if (GIT_CREDTYPE_DEFAULT & allowed_types) {
if (!_remote_default) {
printf("GITTEST_REMOTE_DEFAULT must be set to use NTLM/Negotiate credentials\n");