Skip to content

Commit

Permalink
Correctly attempt multiple usernames with libgit2
Browse files Browse the repository at this point in the history
This commit corrects the logic for attempting multiple usernames with libgit2.
There is a restriction that for each authentication seession you can only
authenticate with one ssh username, but we were attempting multiple as part of
the same sesssion. Instead restart authentication whenever we try a new username.

Closes rust-lang#2399
  • Loading branch information
alexcrichton committed Apr 18, 2016
1 parent 88e3081 commit 537f718
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 82 deletions.
159 changes: 94 additions & 65 deletions src/cargo/sources/git/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,29 +388,18 @@ impl<'a> GitCheckout<'a> {
/// attempted and we don't try the same ones again.
fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
-> CargoResult<T>
where F: FnMut(&mut git2::Credentials) -> CargoResult<T>
where F: FnMut(&mut git2::Credentials) -> Result<T, git2::Error>
{
let mut cred_helper = git2::CredentialHelper::new(url);
cred_helper.config(cfg);

let mut attempted = git2::CredentialType::empty();
let mut failed_cred_helper = false;

// We try a couple of different user names when cloning via ssh as there's a
// few possibilities if one isn't mentioned, and these are used to keep
// track of that.
enum UsernameAttempt {
Arg,
CredHelper,
Local,
Git,
}
let mut username_attempt = UsernameAttempt::Arg;
let mut username_attempts = Vec::new();

let res = f(&mut |url, username, allowed| {
let allowed = allowed & !attempted;
let mut ssh_username_requested = false;
let mut cred_helper_bad = None;
let mut ssh_agent_attempts = Vec::new();
let mut any_attempts = false;

let mut res = f(&mut |url, username, allowed| {
any_attempts = true;
// libgit2's "USERNAME" authentication actually means that it's just
// asking us for a username to keep going. This is currently only really
// used for SSH authentication and isn't really an authentication type.
Expand All @@ -422,51 +411,32 @@ fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
//
// callback(SSH_KEY, user, ...)
//
// So if we have a USERNAME request we just pass it either `username` or
// a fallback of "git". We'll do some more principled attempts later on.
// So if we're being called here then we know that (a) we're using ssh
// authentication and (b) no username was specified in the URL that
// we're trying to clone. We need to guess an appropriate username here,
// but that may involve a few attempts. Unfortunately we can't switch
// usernames during one authentication session with libgit2, so to
// handle this we bail out of this authentication session after setting
// the flag `ssh_username_requested`, and then we handle this below.
if allowed.contains(git2::USERNAME) {
attempted = attempted | git2::USERNAME;
return git2::Cred::username(username.unwrap_or("git"))
debug_assert!(username.is_none());
ssh_username_requested = true;
return Err(git2::Error::from_str("gonna try usernames later"))
}

// An "SSH_KEY" authentication indicates that we need some sort of SSH
// authentication. This can currently either come from the ssh-agent
// process or from a raw in-memory SSH key. Cargo only supports using
// ssh-agent currently.
//
// We try a few different usernames here, including:
//
// 1. The `username` argument, if provided. This will cover cases where
// the user was passed in the URL, for example.
// 2. The global credential helper's username, if any is configured
// 3. The local account's username (if present)
// 4. Finally, "git" as it's a common fallback (e.g. with github)
// If we get called with this then the only way that should be possible
// is if a username is specified in the URL itself (e.g. `username` is
// Some), hence the unwrap() here. We try custom usernames down below.
if allowed.contains(git2::SSH_KEY) {
loop {
let name = match username_attempt {
UsernameAttempt::Arg => {
username_attempt = UsernameAttempt::CredHelper;
username.map(|s| s.to_string())
}
UsernameAttempt::CredHelper => {
username_attempt = UsernameAttempt::Local;
cred_helper.username.clone()
}
UsernameAttempt::Local => {
username_attempt = UsernameAttempt::Git;
env::var("USER").or_else(|_| env::var("USERNAME")).ok()
}
UsernameAttempt::Git => {
attempted = attempted | git2::SSH_KEY;
Some("git".to_string())
}
};
if let Some(name) = name {
let ret = git2::Cred::ssh_key_from_agent(&name);
username_attempts.push(name);
return ret
}
}
let username = username.unwrap();
debug_assert!(!ssh_username_requested);
ssh_agent_attempts.push(username.to_string());
return git2::Cred::ssh_key_from_agent(&username)
}

// Sometimes libgit2 will ask for a username/password in plaintext. This
Expand All @@ -475,25 +445,84 @@ fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
// plaintext password is through the `credential.helper` support, so
// fetch that here.
if allowed.contains(git2::USER_PASS_PLAINTEXT) {
attempted = attempted | git2::USER_PASS_PLAINTEXT;
let r = git2::Cred::credential_helper(cfg, url, username);
failed_cred_helper = r.is_err();
cred_helper_bad = Some(r.is_err());
return r
}

// I'm... not sure what the DEFAULT kind of authentication is, but seems
// easy to support?
if allowed.contains(git2::DEFAULT) {
attempted = attempted | git2::DEFAULT;
return git2::Cred::default()
}

// Whelp, we tried our best
Err(git2::Error::from_str("no authentication available"))
});

if attempted.bits() == 0 || res.is_ok() {
return res
// Ok, so if it looks like we're going to be doing ssh authentication, we
// want to try a few different usernames as one wasn't specified in the URL
// for us to use. In order, we'll try:
//
// * A credential helper's username for this URL, if available.
// * This account's username.
// * "git"
//
// We have to restart the authentication session each time (due to
// constraints in libssh2 I guess? maybe this is inherent to ssh?), so we
// call our callback, `f`, in a loop here.
if ssh_username_requested {
debug_assert!(res.is_err());
let mut attempts = Vec::new();
attempts.push("git".to_string());
if let Ok(s) = env::var("USER").or_else(|_| env::var("USERNAME")) {
attempts.push(s);
}
if let Some(ref s) = cred_helper.username {
attempts.push(s.clone());
}

while let Some(s) = attempts.pop() {
// We should get `USERNAME` first, where we just return our attempt,
// and then after that we should get `SSH_KEY`. If the first attempt
// fails we'll get called again, but we don't have another option so
// we bail out.
let mut attempts = 0;
res = f(&mut |_url, username, allowed| {
if allowed.contains(git2::USERNAME) {
return git2::Cred::username(&s);
}
if allowed.contains(git2::SSH_KEY) {
debug_assert_eq!(Some(&s[..]), username);
attempts += 1;
if attempts == 1 {
ssh_agent_attempts.push(s.to_string());
return git2::Cred::ssh_key_from_agent(&s)
}
}
Err(git2::Error::from_str("no authentication available"))
});

// If we made two attempts then that means:
//
// 1. A username was requested, we returned `s`.
// 2. An ssh key was requested, we returned to look up `s` in the
// ssh agent.
// 3. For whatever reason that lookup failed, so we were asked again
// for another mode of authentication.
//
// Essentially, if `attempts == 2` then in theory the only error was
// that this username failed to authenticate (e.g. no other network
// errors happened). Otherwise something else is funny so we bail
// out.
if attempts != 2 {
break
}
}
}

if res.is_ok() || !any_attempts {
return res.map_err(From::from)
}

// In the case of an authentication failure (where we tried something) then
Expand All @@ -502,15 +531,15 @@ fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
res.chain_error(|| {
let mut msg = "failed to authenticate when downloading \
repository".to_string();
if attempted.contains(git2::SSH_KEY) {
let names = username_attempts.iter()
.map(|s| format!("`{}`", s))
.collect::<Vec<_>>()
.join(", ");
if ssh_agent_attempts.len() > 0 {
let names = ssh_agent_attempts.iter()
.map(|s| format!("`{}`", s))
.collect::<Vec<_>>()
.join(", ");
msg.push_str(&format!("\nattempted ssh-agent authentication, but \
none of the usernames {} succeeded", names));
}
if attempted.contains(git2::USER_PASS_PLAINTEXT) {
if let Some(failed_cred_helper) = cred_helper_bad {
if failed_cred_helper {
msg.push_str("\nattempted to find username/password via \
git's `credential.helper` support, but failed");
Expand Down
20 changes: 3 additions & 17 deletions tests/test_cargo_build_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,17 +145,10 @@ test!(https_something_happens {
updating = UPDATING,
addr = addr,
))
.with_stderr(&format!("\
{error} Unable to update https://{addr}/foo/bar
Caused by:
failed to clone into: [..]
.with_stderr_contains(&format!("\
Caused by:
{errmsg}
",
addr = addr,
error = ERROR,
errmsg = if cfg!(windows) {
"[[..]] failed to send request: [..]\n"
} else if cfg!(target_os = "macos") {
Expand Down Expand Up @@ -197,16 +190,9 @@ test!(ssh_something_happens {
updating = UPDATING,
addr = addr,
))
.with_stderr(&format!("\
{error} Unable to update ssh://{addr}/foo/bar
Caused by:
failed to clone into: [..]
.with_stderr_contains("\
Caused by:
[[..]] Failed to start SSH session: Failed getting banner
",
addr = addr,
error = ERROR)));
"));
t.join().ok().unwrap();
});

0 comments on commit 537f718

Please sign in to comment.