#[macro_use]
extern crate pest_derive;
pub use git2;
mod ssh_config;
#[cfg(feature = "ui4dialoguer")]
pub mod ui4dialoguer;
use std::error::Error;
pub struct CredentialHandler {
username_attempts_count: usize,
username_candidates: Vec<String>,
ssh_attempts_count: usize,
ssh_key_candidates: Vec<std::path::PathBuf>,
cred_helper_bad: Option<bool>,
cfg: git2::Config,
ui: Box<dyn CredentialUI>,
}
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 {
CredentialHandler {
username_attempts_count: 0,
username_candidates: vec![],
ssh_attempts_count: 0,
ssh_key_candidates: vec![],
cred_helper_bad: None,
cfg,
ui,
}
}
pub fn try_next_credential(
&mut self,
url: &str,
username: Option<&str>,
allowed: git2::CredentialType,
) -> Result<git2::Cred, git2::Error> {
if allowed.contains(git2::CredentialType::USERNAME) {
let idx = self.username_attempts_count;
self.username_attempts_count += 1;
if idx == 0 {
let maybe_host = extract_host(url)?;
self.username_candidates =
ssh_config::find_username_candidates(maybe_host.as_deref())?;
}
return match self.username_candidates.get(idx).map(|s| &s[..]) {
Some(s) => git2::Cred::username(s),
_ => Err(git2::Error::from_str("no more username to try")),
};
}
if allowed.contains(git2::CredentialType::SSH_KEY) {
self.ssh_attempts_count += 1;
let u = username.unwrap_or("git");
return if self.ssh_attempts_count == 1 {
git2::Cred::ssh_key_from_agent(u)
} else {
if self.ssh_attempts_count == 2 {
let maybe_host = extract_host(url)?;
self.ssh_key_candidates =
ssh_config::find_ssh_key_candidates(maybe_host.as_deref())?;
}
let candidate_idx = self.ssh_attempts_count - 2;
if candidate_idx < self.ssh_key_candidates.len() {
self.cred_from_ssh_config(candidate_idx, u)
} else {
Err(git2::Error::from_str("try with an other username"))
}
};
}
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() {
if let Ok((user, password)) = self.ui.ask_user_password(username.unwrap_or("")) {
return git2::Cred::userpass_plaintext(&user, &password);
}
}
return r;
}
if allowed.contains(git2::CredentialType::DEFAULT) {
return git2::Cred::default();
}
Err(git2::Error::from_str("no valid authentication available"))
}
fn cred_from_ssh_config(
&self,
candidate_idx: usize,
username: &str,
) -> Result<git2::Cred, git2::Error> {
let key = ssh_config::get_ssh_key(&self.ssh_key_candidates, candidate_idx)?;
match key {
Some(k) => git2::Cred::ssh_key(username, None, &k, None).or_else(|_| {
let passphrase = self
.ui
.ask_ssh_passphrase(&format!(
"Enter passphrase for key '{}'",
k.to_string_lossy()
))
.ok()
.filter(|v| !v.is_empty());
git2::Cred::ssh_key(username, None, &k, passphrase.as_deref())
}),
None => Err(git2::Error::from_str(
"failed authentication for repository",
)),
}
}
}
fn extract_host(url: &str) -> Result<Option<String>, git2::Error> {
let url_re = regex::Regex::new(
r"^(https?|ssh)://([[:alnum:]:\._-]+@)?(?P<host>[[:alnum:]\._-]+)(:\d+)?/(?P<path>[[:alnum:]\._\-/]+).git$",
).map_err(|source| git2::Error::from_str(&format!("failed to parse url '{}': {:#?}", url, source)))?;
let url_re2 = regex::Regex::new(
r"^(https?|ssh)://([[:alnum:]:\._-]+@)?(?P<host>[[:alnum:]\._-]+)(:\d+)?/(?P<path>[[:alnum:]\._\-/]+)$",
).map_err(|source| git2::Error::from_str(&format!("failed to parse url '{}': {:#?}", url, source)))?;
let git_re = regex::Regex::new(
r"^([[:alnum:]:\._-]+@)?(?P<host>[[:alnum:]\._-]+):(?P<path>[[:alnum:]\._\-/]+).git$",
)
.map_err(|source| {
git2::Error::from_str(&format!("failed to parse url '{}': {:#?}", url, source))
})?;
let git_re2 = regex::Regex::new(
r"^([[:alnum:]:\._-]+@)?(?P<host>[[:alnum:]\._-]+):(?P<path>[[:alnum:]\._\-/]+)$",
)
.map_err(|source| {
git2::Error::from_str(&format!("failed to parse url '{}': {:#?}", url, source))
})?;
Ok(url_re
.captures(url)
.or_else(|| url_re2.captures(url))
.or_else(|| git_re.captures(url))
.or_else(|| git_re2.captures(url))
.map(|caps| caps["host"].to_string()))
}
pub trait CredentialUI {
fn ask_user_password(&self, username: &str) -> Result<(String, String), Box<dyn Error>>;
fn ask_ssh_passphrase(&self, passphrase_prompt: &str) -> Result<String, Box<dyn Error>>;
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_extract_host() -> Result<(), Box<dyn Error>> {
assert_eq!(
extract_host("git@github.com:davidB/git2_credentials.git"),
Ok(Some("github.com".to_string()))
);
assert_eq!(
extract_host("https://github.com/davidB/git2_credentials.git"),
Ok(Some("github.com".to_string()))
);
assert_eq!(
extract_host("ssh://aur@aur.archlinux.org/souko.git"),
Ok(Some("aur.archlinux.org".to_string()))
);
assert_eq!(
extract_host("aur@aur.archlinux.org:souko.git"),
Ok(Some("aur.archlinux.org".to_string()))
);
assert_eq!(
extract_host("aur.archlinux.org:souko.git"),
Ok(Some("aur.archlinux.org".to_string()))
);
Ok(())
}
}