|
| 1 | +use std::env; |
1 | 2 | use std::fmt;
|
2 |
| -use std::path::{Path, PathBuf}; |
3 | 3 | use std::fs::{self, File};
|
| 4 | +use std::path::{Path, PathBuf}; |
4 | 5 |
|
5 | 6 | use rustc_serialize::{Encodable, Encoder};
|
6 | 7 | use url::Url;
|
@@ -358,66 +359,169 @@ impl<'a> GitCheckout<'a> {
|
358 | 359 | }
|
359 | 360 | }
|
360 | 361 |
|
| 362 | +/// Prepare the authentication callbacks for cloning a git repository. |
| 363 | +/// |
| 364 | +/// The main purpose of this function is to construct the "authentication |
| 365 | +/// callback" which is used to clone a repository. This callback will attempt to |
| 366 | +/// find the right authentication on the system (without user input) and will |
| 367 | +/// guide libgit2 in doing so. |
| 368 | +/// |
| 369 | +/// The callback is provided `allowed` types of credentials, and we try to do as |
| 370 | +/// much as possible based on that: |
| 371 | +/// |
| 372 | +/// * Prioritize SSH keys from the local ssh agent as they're likely the most |
| 373 | +/// reliable. The username here is prioritized from the credential |
| 374 | +/// callback, then from whatever is configured in git itself, and finally |
| 375 | +/// we fall back to the generic user of `git`. |
| 376 | +/// |
| 377 | +/// * If a username/password is allowed, then we fallback to git2-rs's |
| 378 | +/// implementation of the credential helper. This is what is configured |
| 379 | +/// with `credential.helper` in git, and is the interface for the OSX |
| 380 | +/// keychain, for example. |
| 381 | +/// |
| 382 | +/// * After the above two have failed, we just kinda grapple attempting to |
| 383 | +/// return *something*. |
| 384 | +/// |
| 385 | +/// If any form of authentication fails, libgit2 will repeatedly ask us for |
| 386 | +/// credentials until we give it a reason to not do so. To ensure we don't |
| 387 | +/// just sit here looping forever we keep track of authentications we've |
| 388 | +/// attempted and we don't try the same ones again. |
361 | 389 | fn with_authentication<T, F>(url: &str, cfg: &git2::Config, mut f: F)
|
362 | 390 | -> CargoResult<T>
|
363 | 391 | where F: FnMut(&mut git2::Credentials) -> CargoResult<T>
|
364 | 392 | {
|
365 |
| - // Prepare the authentication callbacks. |
366 |
| - // |
367 |
| - // We check the `allowed` types of credentials, and we try to do as much as |
368 |
| - // possible based on that: |
369 |
| - // |
370 |
| - // * Prioritize SSH keys from the local ssh agent as they're likely the most |
371 |
| - // reliable. The username here is prioritized from the credential |
372 |
| - // callback, then from whatever is configured in git itself, and finally |
373 |
| - // we fall back to the generic user of `git`. |
374 |
| - // |
375 |
| - // * If a username/password is allowed, then we fallback to git2-rs's |
376 |
| - // implementation of the credential helper. This is what is configured |
377 |
| - // with `credential.helper` in git, and is the interface for the OSX |
378 |
| - // keychain, for example. |
379 |
| - // |
380 |
| - // * After the above two have failed, we just kinda grapple attempting to |
381 |
| - // return *something*. |
382 |
| - // |
383 |
| - // Note that we keep track of the number of times we've called this callback |
384 |
| - // because libgit2 will repeatedly give us credentials until we give it a |
385 |
| - // reason to not do so. If we've been called once and our credentials failed |
386 |
| - // then we'll be called again, and in this case we assume that the reason |
387 |
| - // was because the credentials were wrong. |
388 | 393 | let mut cred_helper = git2::CredentialHelper::new(url);
|
389 | 394 | cred_helper.config(cfg);
|
390 |
| - let mut called = 0; |
| 395 | + |
| 396 | + let mut attempted = git2::CredentialType::empty(); |
| 397 | + let mut failed_cred_helper = false; |
| 398 | + |
| 399 | + // We try a couple of different user names when cloning via ssh as there's a |
| 400 | + // few possibilities if one isn't mentioned, and these are used to keep |
| 401 | + // track of that. |
| 402 | + enum UsernameAttempt { |
| 403 | + Arg, |
| 404 | + CredHelper, |
| 405 | + Local, |
| 406 | + Git, |
| 407 | + } |
| 408 | + let mut username_attempt = UsernameAttempt::Arg; |
| 409 | + let mut username_attempts = Vec::new(); |
| 410 | + |
391 | 411 | let res = f(&mut |url, username, allowed| {
|
392 |
| - called += 1; |
393 |
| - if called >= 2 { |
394 |
| - return Err(git2::Error::from_str("no authentication available")) |
| 412 | + let allowed = allowed & !attempted; |
| 413 | + |
| 414 | + // libgit2's "USERNAME" authentication actually means that it's just |
| 415 | + // asking us for a username to keep going. This is currently only really |
| 416 | + // used for SSH authentication and isn't really an authentication type. |
| 417 | + // The logic currently looks like: |
| 418 | + // |
| 419 | + // let user = ...; |
| 420 | + // if (user.is_null()) |
| 421 | + // user = callback(USERNAME, null, ...); |
| 422 | + // |
| 423 | + // callback(SSH_KEY, user, ...) |
| 424 | + // |
| 425 | + // So if we have a USERNAME request we just pass it either `username` or |
| 426 | + // a fallback of "git". We'll do some more principled attempts later on. |
| 427 | + if allowed.contains(git2::USERNAME) { |
| 428 | + attempted = attempted | git2::USERNAME; |
| 429 | + return git2::Cred::username(username.unwrap_or("git")) |
395 | 430 | }
|
396 |
| - if allowed.contains(git2::SSH_KEY) || |
397 |
| - allowed.contains(git2::USERNAME) { |
398 |
| - let user = username.map(|s| s.to_string()) |
399 |
| - .or_else(|| cred_helper.username.clone()) |
400 |
| - .unwrap_or("git".to_string()); |
401 |
| - if allowed.contains(git2::USERNAME) { |
402 |
| - git2::Cred::username(&user) |
403 |
| - } else { |
404 |
| - git2::Cred::ssh_key_from_agent(&user) |
| 431 | + |
| 432 | + // An "SSH_KEY" authentication indicates that we need some sort of SSH |
| 433 | + // authentication. This can currently either come from the ssh-agent |
| 434 | + // process or from a raw in-memory SSH key. Cargo only supports using |
| 435 | + // ssh-agent currently. |
| 436 | + // |
| 437 | + // We try a few different usernames here, including: |
| 438 | + // |
| 439 | + // 1. The `username` argument, if provided. This will cover cases where |
| 440 | + // the user was passed in the URL, for example. |
| 441 | + // 2. The global credential helper's username, if any is configured |
| 442 | + // 3. The local account's username (if present) |
| 443 | + // 4. Finally, "git" as it's a common fallback (e.g. with github) |
| 444 | + if allowed.contains(git2::SSH_KEY) { |
| 445 | + loop { |
| 446 | + let name = match username_attempt { |
| 447 | + UsernameAttempt::Arg => { |
| 448 | + username_attempt = UsernameAttempt::CredHelper; |
| 449 | + username.map(|s| s.to_string()) |
| 450 | + } |
| 451 | + UsernameAttempt::CredHelper => { |
| 452 | + username_attempt = UsernameAttempt::Local; |
| 453 | + cred_helper.username.clone() |
| 454 | + } |
| 455 | + UsernameAttempt::Local => { |
| 456 | + username_attempt = UsernameAttempt::Git; |
| 457 | + env::var("USER").or_else(|_| env::var("USERNAME")).ok() |
| 458 | + } |
| 459 | + UsernameAttempt::Git => { |
| 460 | + attempted = attempted | git2::SSH_KEY; |
| 461 | + Some("git".to_string()) |
| 462 | + } |
| 463 | + }; |
| 464 | + if let Some(name) = name { |
| 465 | + let ret = git2::Cred::ssh_key_from_agent(&name); |
| 466 | + username_attempts.push(name); |
| 467 | + return ret |
| 468 | + } |
405 | 469 | }
|
406 |
| - } else if allowed.contains(git2::USER_PASS_PLAINTEXT) { |
407 |
| - git2::Cred::credential_helper(cfg, url, username) |
408 |
| - } else if allowed.contains(git2::DEFAULT) { |
409 |
| - git2::Cred::default() |
410 |
| - } else { |
411 |
| - Err(git2::Error::from_str("no authentication available")) |
412 | 470 | }
|
| 471 | + |
| 472 | + // Sometimes libgit2 will ask for a username/password in plaintext. This |
| 473 | + // is where Cargo would have an interactive prompt if we supported it, |
| 474 | + // but we currently don't! Right now the only way we support fetching a |
| 475 | + // plaintext password is through the `credential.helper` support, so |
| 476 | + // fetch that here. |
| 477 | + if allowed.contains(git2::USER_PASS_PLAINTEXT) { |
| 478 | + attempted = attempted | git2::USER_PASS_PLAINTEXT; |
| 479 | + let r = git2::Cred::credential_helper(cfg, url, username); |
| 480 | + failed_cred_helper = r.is_err(); |
| 481 | + return r |
| 482 | + } |
| 483 | + |
| 484 | + // I'm... not sure what the DEFAULT kind of authentication is, but seems |
| 485 | + // easy to support? |
| 486 | + if allowed.contains(git2::DEFAULT) { |
| 487 | + attempted = attempted | git2::DEFAULT; |
| 488 | + return git2::Cred::default() |
| 489 | + } |
| 490 | + |
| 491 | + // Whelp, we tried our best |
| 492 | + Err(git2::Error::from_str("no authentication available")) |
413 | 493 | });
|
414 |
| - if called > 0 { |
415 |
| - res.chain_error(|| { |
416 |
| - human("failed to authenticate when downloading repository") |
417 |
| - }) |
418 |
| - } else { |
419 |
| - res |
| 494 | + |
| 495 | + if attempted.bits() == 0 || res.is_ok() { |
| 496 | + return res |
420 | 497 | }
|
| 498 | + |
| 499 | + // In the case of an authentication failure (where we tried something) then |
| 500 | + // we try to give a more helpful error message about precisely what we |
| 501 | + // tried. |
| 502 | + res.chain_error(|| { |
| 503 | + let mut msg = "failed to authenticate when downloading \ |
| 504 | + repository".to_string(); |
| 505 | + if attempted.contains(git2::SSH_KEY) { |
| 506 | + let names = username_attempts.iter() |
| 507 | + .map(|s| format!("`{}`", s)) |
| 508 | + .collect::<Vec<_>>() |
| 509 | + .join(", "); |
| 510 | + msg.push_str(&format!("\nattempted ssh-agent authentication, but \ |
| 511 | + none of the usernames {} succeeded", names)); |
| 512 | + } |
| 513 | + if attempted.contains(git2::USER_PASS_PLAINTEXT) { |
| 514 | + if failed_cred_helper { |
| 515 | + msg.push_str("\nattempted to find username/password via \ |
| 516 | + git's `credential.helper` support, but failed"); |
| 517 | + } else { |
| 518 | + msg.push_str("\nattempted to find username/password via \ |
| 519 | + `credential.helper`, but maybe the found \ |
| 520 | + credentials were incorrect"); |
| 521 | + } |
| 522 | + } |
| 523 | + human(msg) |
| 524 | + }) |
421 | 525 | }
|
422 | 526 |
|
423 | 527 | pub fn fetch(repo: &git2::Repository, url: &str,
|
|
0 commit comments