summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarlos Martín Nieto <cmn@dwim.me>2014-08-27 15:10:51 +0200
committerCarlos Martín Nieto <cmn@dwim.me>2014-08-27 15:10:51 +0200
commit15c7da3442843b8fd2cdb95af8716ec6b5c12a40 (patch)
treeb7a0dfb73f7311283e1469182c6aaeff48e00fb9
parentcb92467bc284b87c38cdf32f8803a528846d094b (diff)
parent7449c82ee713024f59badf6fef9cc0ac49ce9561 (diff)
downloadlibgit2-15c7da3442843b8fd2cdb95af8716ec6b5c12a40.tar.gz
Merge branch 'cmn/ssh-retry'
-rw-r--r--CHANGELOG.md6
-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.c174
-rw-r--r--tests/online/clone.c85
-rw-r--r--tests/online/push.c9
10 files changed, 343 insertions, 39 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93b5f20db..97c873dc2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,12 @@ v0.21 + 1
* The git_transport structure definition has moved into the sys/transport.h
file.
+* The ssh transport supports asking the remote host for accepted
+ credential types as well as multiple challeges using a single
+ connection. This requires to know which username you want to connect
+ as, so this introduces the USERNAME credential type which the ssh
+ transport will use to ask for the username.
+
* The git_transport_register function no longer takes a priority and takes
a URL scheme name (eg "http") instead of a prefix like "http://"
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 66341d296..7090698ac 100644
--- a/include/git2/transport.h
+++ b/include/git2/transport.h
@@ -36,6 +36,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 */
@@ -103,6 +111,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.
*
@@ -205,6 +219,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 5ba07460c..981f95b7e 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,7 +40,7 @@ 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 &&
rm -rf $HOME/_temp/test.git &&
git init --bare $HOME/_temp/test.git && # create an empty one
./libgit2_clar -sonline::clone::ssh_with_paths
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 85f620013..fff81661a 100644
--- a/src/transports/ssh.c
+++ b/src/transports/ssh.c
@@ -13,6 +13,7 @@
#include "buffer.h"
#include "netops.h"
#include "smart.h"
+#include "cred.h"
#ifdef GIT_SSH
@@ -41,6 +42,8 @@ typedef struct {
char *cmd_receivepack;
} 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;
@@ -354,6 +357,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;
@@ -362,6 +368,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)
@@ -402,8 +445,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;
@@ -414,56 +458,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;
}
@@ -474,6 +530,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);
@@ -490,6 +549,9 @@ on_error:
if (*stream)
ssh_stream_free(*stream);
+ if (cred)
+ cred->free(cred);
+
git__free(host);
git__free(port);
git__free(user);
@@ -498,7 +560,7 @@ on_error:
if (session)
libssh2_session_free(session);
- return -1;
+ return error;
}
static int ssh_uploadpack_ls(
@@ -508,10 +570,7 @@ static int ssh_uploadpack_ls(
{
const char *cmd = t->cmd_uploadpack ? t->cmd_uploadpack : cmd_uploadpack;
- if (_git_ssh_setup_conn(t, url, cmd, stream) < 0)
- return -1;
-
- return 0;
+ return _git_ssh_setup_conn(t, url, cmd, stream);
}
static int ssh_uploadpack(
@@ -606,6 +665,53 @@ static void _ssh_free(git_smart_subtransport *subtransport)
git__free(t->cmd_receivepack);
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 0cd0f3115..c9359655e 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;
@@ -222,8 +224,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);
}
int cred_default(
@@ -328,6 +363,36 @@ static int cred_cb(git_cred **cred, const char *url, const char *user_from_url,
return -1;
}
+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 custom_remote_ssh_with_paths(
git_remote **out,
git_repository *repo,
@@ -336,7 +401,6 @@ static int custom_remote_ssh_with_paths(
void *payload)
{
int error;
-
git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT;
if ((error = git_remote_create(out, repo, name, url)) < 0)
@@ -381,3 +445,18 @@ void test_online_clone__ssh_with_paths(void)
cl_git_pass(git_clone(&g_repo, remote_url, "./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 c351827d9..4d88bdf7f 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");