diff options
| author | Vicent Marti <vicent@github.com> | 2014-08-26 17:48:06 +0200 |
|---|---|---|
| committer | Vicent Marti <vicent@github.com> | 2014-08-26 17:48:06 +0200 |
| commit | 94f74ad28f191e7dc17ae874c00e3ecd6a80fcf4 (patch) | |
| tree | 3128f3eac456486814fdbc5529e44e127316c519 | |
| parent | 1589aa0c4d48fb130d8a5db28c45cd3d173cde6d (diff) | |
| parent | 8733993599d9d9a8fe1b042a1e18d6ecb9b0a0b2 (diff) | |
| download | libgit2-94f74ad28f191e7dc17ae874c00e3ecd6a80fcf4.tar.gz | |
Merge pull request #2422 from libgit2/cmn/ssh-retry
Enable retries for SSH
| -rw-r--r-- | include/git2/errors.h | 1 | ||||
| -rw-r--r-- | include/git2/transport.h | 22 | ||||
| -rwxr-xr-x | script/cibuild.sh | 10 | ||||
| -rw-r--r-- | src/transports/cred.c | 58 | ||||
| -rw-r--r-- | src/transports/cred.h | 14 | ||||
| -rw-r--r-- | src/transports/cred_helpers.c | 3 | ||||
| -rw-r--r-- | src/transports/ssh.c | 175 | ||||
| -rw-r--r-- | tests/online/clone.c | 80 | ||||
| -rw-r--r-- | tests/online/push.c | 9 |
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"); |
