diff options
author | Carlos Martín Nieto <cmn@dwim.me> | 2014-08-27 15:10:51 +0200 |
---|---|---|
committer | Carlos Martín Nieto <cmn@dwim.me> | 2014-08-27 15:10:51 +0200 |
commit | 15c7da3442843b8fd2cdb95af8716ec6b5c12a40 (patch) | |
tree | b7a0dfb73f7311283e1469182c6aaeff48e00fb9 | |
parent | cb92467bc284b87c38cdf32f8803a528846d094b (diff) | |
parent | 7449c82ee713024f59badf6fef9cc0ac49ce9561 (diff) | |
download | libgit2-15c7da3442843b8fd2cdb95af8716ec6b5c12a40.tar.gz |
Merge branch 'cmn/ssh-retry'
-rw-r--r-- | CHANGELOG.md | 6 | ||||
-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 | 174 | ||||
-rw-r--r-- | tests/online/clone.c | 85 | ||||
-rw-r--r-- | tests/online/push.c | 9 |
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"); |