use std::{collections::VecDeque, env};
type Username = String;
const USERNAME_EMPTY: &str = "";
const USERNAME_GIT: &str = "git";
pub struct AuthHandler {
config: git2::Config,
usernames: VecDeque<Username>,
ssh_trial_methods: VecDeque<SSHTrialMethod>,
tried_plain_user_pass: bool,
pub callback_username: Option<Username>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SSHTrialMethod {
Agent,
}
impl AuthHandler {
pub fn new(
config: git2::Config,
usernames: VecDeque<Username>,
ssh_trial_methods: VecDeque<SSHTrialMethod>,
tried_plain_user_pass: bool,
callback_username: Option<String>,
) -> Self {
Self {
config,
usernames,
ssh_trial_methods,
tried_plain_user_pass,
callback_username,
}
}
pub fn default_with_config(config: git2::Config) -> Self {
let mut usernames = VecDeque::with_capacity(3);
usernames.push_back(USERNAME_EMPTY.to_string());
usernames.push_back(USERNAME_GIT.to_string());
if let Ok(env_username) = env::var("USER") {
usernames.push_back(env_username);
}
let mut ssh_trial_method = VecDeque::default();
ssh_trial_method.push_back(SSHTrialMethod::Agent);
let callback_username = None;
let tried_plain_user_pass = false;
Self::new(
config,
usernames,
ssh_trial_method,
tried_plain_user_pass,
callback_username,
)
}
pub fn handle_callback(
&mut self,
url: &str,
username: Option<&str>,
allowed: git2::CredentialType,
) -> Result<git2::Cred, git2::Error> {
self.callback_username = username.map(|st| st.to_string());
if allowed.contains(git2::CredentialType::USERNAME) {
return self.handle_username_callback();
}
if allowed.contains(git2::CredentialType::SSH_KEY) && !self.ssh_trial_methods.is_empty() {
return self.handle_ssh_callback();
}
if allowed.contains(git2::CredentialType::USER_PASS_PLAINTEXT) {
return git2::Cred::credential_helper(&self.config, url, username);
}
if allowed.contains(git2::CredentialType::DEFAULT) && !self.tried_plain_user_pass {
self.tried_plain_user_pass = true;
return git2::Cred::default();
}
Err(git2::Error::from_str(
"tried all possible credential types for authentication",
))
}
pub fn get_next_username(&mut self) -> Option<Username> {
let usernames = &mut self.usernames;
usernames.pop_front()
}
pub fn get_next_ssh_trial_method(&mut self) -> Option<SSHTrialMethod> {
let methods = &mut self.ssh_trial_methods;
methods.pop_front()
}
pub(crate) fn handle_username_callback(&mut self) -> Result<git2::Cred, git2::Error> {
let username = self.get_next_username().ok_or_else(|| {
git2::Error::from_str("tried all possible usernames for the callback")
})?;
git2::Cred::username(&username)
}
pub(crate) fn handle_ssh_callback(&mut self) -> Result<git2::Cred, git2::Error> {
let ssh_trial_method = self
.get_next_ssh_trial_method()
.ok_or_else(|| git2::Error::from_str("no ssh handler present for authentication"))?;
ssh_trial_method.handle_callback(self.callback_username.as_ref())
}
}
impl SSHTrialMethod {
pub(crate) fn handle_callback(
&self,
callback_username: Option<&Username>,
) -> Result<git2::Cred, git2::Error> {
match self {
SSHTrialMethod::Agent => {
let username = callback_username.ok_or_else(|| {
git2::Error::from_str("username must be provided with SSH_KEY callback")
})?;
git2::Cred::ssh_key_from_agent(username)
}
}
}
}