1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
//! Provide credential function to used with [git2](https://crates.io/crates/git2)::[RemoteCallbacks.credentials](https://docs.rs/git2/0.8.0/git2/struct.RemoteCallbacks.html#method.credentials) //! //! Usage: //! ```rust //! use git2; //! use git2_credentials::CredentialHandler; //! use tempfile; //! //! let mut cb = git2::RemoteCallbacks::new(); //! let git_config = git2::Config::open_default().unwrap(); //! let mut ch = CredentialHandler::new(git_config); //! cb.credentials(move |url, username, allowed| ch.try_next_credential(url, username, allowed)); //! //! // let mut fo = git2::FetchOptions::new(); //! // fo.remote_callbacks(cb) //! // .download_tags(git2::AutotagOption::All) //! // .update_fetchhead(true); //! // let dst = tempfile::tempdir().unwrap(); //! // std::fs::create_dir_all(&dst.as_ref()).unwrap(); //! // git2::build::RepoBuilder::new() //! // .branch("master") //! // .fetch_options(fo) //! // .clone("git@github.com:davidB/git2_credentials.git", dst.as_ref()).unwrap(); //! ``` mod ssh_config; #[cfg(feature = "ui4dialoguer")] pub mod ui4dialoguer; use failure::Error; use git2; pub struct CredentialHandler { usernames: Vec<String>, ssh_attempts_count: usize, username_attempts_count: usize, cred_helper_bad: Option<bool>, cfg: git2::Config, ui: Box<dyn CredentialUI>, } // implemention based on code & comment from cargo // https://github.com/rust-lang/cargo/blob/master/src/cargo/sources/git/utils.rs#L415-L628 // License APACHE // but adapted to not use wrapper over function like withXxx(FnMut), a more OO approach impl CredentialHandler { #[cfg(feature = "ui4dialoguer")] pub fn new(cfg: git2::Config) -> Self { use ui4dialoguer::CredentialUI4Dialoguer; Self::new_with_ui(cfg, Box::new(CredentialUI4Dialoguer{})) } pub fn new_with_ui(cfg: git2::Config, ui: Box<dyn CredentialUI>) -> Self { let mut usernames = Vec::new(); usernames.push("".to_string()); //default usernames.push("git".to_string()); if let Ok(s) = std::env::var("USER").or_else(|_| std::env::var("USERNAME")) { usernames.push(s); } // let mut cred_helper = git2::CredentialHelper::new(url); // cred_helper.config(cfg); // if let Some(ref s) = cred_helper.username { // usernames.push(s.clone()); // } CredentialHandler { usernames, ssh_attempts_count: 2, username_attempts_count: 0, cred_helper_bad: None, cfg, ui, } } /// Prepare the authentication callbacks for cloning a git repository. /// /// The main purpose of this function is to construct the "authentication /// callback" which is used to clone a repository. This callback will attempt to /// find the right authentication on the system (maybe with user input) and will /// guide libgit2 in doing so. /// /// The callback is provided `allowed` types of credentials, and we try to do as /// much as possible based on that: /// /// - Prioritize SSH keys from the local ssh agent as they're likely the most /// reliable. The username here is prioritized from the credential /// callback, then from whatever is configured in git itself, and finally /// we fall back to the generic user of `git`. If no ssh agent try to use /// the default key ($HOME/.ssh/id_rsa, $HOME/.ssh/id_ed25519) /// /// - If a username/password is allowed, then we fallback to git2-rs's /// implementation of the credential helper. This is what is configured /// with `credential.helper` in git, and is the interface for the macOS /// keychain, for example. Else ask (on ui) the for username and password. /// /// - After the above two have failed, we just kinda grapple attempting to /// return *something*. /// /// If any form of authentication fails, libgit2 will repeatedly ask us for /// credentials until we give it a reason to not do so. To ensure we don't /// just sit here looping forever we keep track of authentications we've /// attempted and we don't try the same ones again. pub fn try_next_credential( &mut self, url: &str, username: Option<&str>, allowed: git2::CredentialType, ) -> Result<git2::Cred, git2::Error> { // dbg!(allowed); // 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. // The logic currently looks like: // // let user = ...; // if (user.is_null()) // user = callback(USERNAME, null, ...); // // callback(SSH_KEY, user, ...) // // 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. // (FIXME) 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::CredentialType::USERNAME) { // debug_assert!(username.is_none()); let idx = self.username_attempts_count; self.username_attempts_count += 1; return match self.usernames.get(idx).map(|s| &s[..]) { Some("") if username.is_none() => { Err(git2::Error::from_str("gonna try usernames later")) } Some("") => git2::Cred::username(&username.unwrap_or("")), Some(s) => git2::Cred::username(&s), _ => Err(git2::Error::from_str("no more username to try")), }; } // 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. // // 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::CredentialType::SSH_KEY) { // If ssh-agent authentication fails, libgit2 will keep // calling this callback asking for other authentication // methods to try. Make sure we only try ssh-agent once. self.ssh_attempts_count = (self.ssh_attempts_count + 1) % 3; // dbg!(self.ssh_attempts_count); let u = username.unwrap_or("git"); return match self.ssh_attempts_count { 0 => git2::Cred::ssh_key_from_agent(&u), 1 => self.cred_from_ssh_config(&u), _ => Err(git2::Error::from_str("try with an other username")), }; } // Sometimes libgit2 will ask for a username/password in plaintext. // // If ssh-agent authentication fails, libgit2 will keep calling this // callback asking for other authentication methods to try. Check // cred_helper_bad to make sure we only try the git credentail helper // once, to avoid looping forever. if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) && self.cred_helper_bad.is_none() { let r = git2::Cred::credential_helper(&self.cfg, url, username); self.cred_helper_bad = Some(r.is_err()); if r.is_err() { match self.ui.ask_user_password(username.unwrap_or("")) { Ok((user, password)) => { return git2::Cred::userpass_plaintext(&user, &password) } Err(_) => (), //FIXME give a feeback instead of ignore } } return r; } // I'm... not sure what the DEFAULT kind of authentication is, but seems // easy to support? if allowed.contains(git2::CredentialType::DEFAULT) { return git2::Cred::default(); } // Stop trying Err(git2::Error::from_str("no valid authentication available")) } fn cred_from_ssh_config(&self, username: &str) -> Result<git2::Cred, git2::Error> { let (key, passphrase) = ssh_config::get_ssh_key_and_passphrase(self.ui.as_ref()); return match key { Some(k) => { git2::Cred::ssh_key(username, None, &k, passphrase.as_ref().map(String::as_str)) } None => Err(git2::Error::from_str(&format!( "failed authentication for repository" ))), }; } } pub trait CredentialUI { fn ask_user_password(&self, username: &str) -> Result<(String, String), Error>; fn ask_ssh_passphrase(&self, passphrase_prompt: &str) -> Result<String, Error>; }