blob: fdac608f541a9b50996a1d30ddf0a2e8ae38cb1e [file] [log] [blame]
use std::borrow::Cow;
pub use error::Error;
use crate::config::cache::util::ApplyLeniency;
use crate::{
bstr::{ByteSlice, ByteVec},
config::{
cache::util::IgnoreEmptyPath,
tree::{credential, gitoxide::Credentials, Core, Credential, Key},
Snapshot,
},
};
mod error {
use crate::bstr::BString;
/// The error returned by [`Snapshot::credential_helpers()`][super::Snapshot::credential_helpers()].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Could not parse 'useHttpPath' key in section {section}")]
InvalidUseHttpPath {
section: BString,
source: gix_config::value::Error,
},
#[error("core.askpass could not be read")]
CoreAskpass(#[from] gix_config::path::interpolate::Error),
#[error(transparent)]
BooleanConfig(#[from] crate::config::boolean::Error),
}
}
impl Snapshot<'_> {
/// Returns the configuration for all git-credential helpers from trusted configuration that apply
/// to the given `url` along with an action preconfigured to invoke the cascade with.
/// This includes `url` which may be altered to contain a user-name as configured.
///
/// These can be invoked to obtain credentials. Note that the `url` is expected to be the one used
/// to connect to a remote, and thus should already have passed the url-rewrite engine.
///
/// # Deviation
///
/// - Invalid urls can't be used to obtain credential helpers as they are rejected early when creating a valid `url` here.
/// - Parsed urls will automatically drop the port if it's the default, i.e. `http://host:80` becomes `http://host` when parsed.
/// This affects the prompt provided to the user, so that git will use the verbatim url, whereas we use `http://host`.
/// - Upper-case scheme and host will be lower-cased automatically when parsing into a url, so prompts differ compared to git.
/// - A **difference in prompt might affect the matching of getting existing stored credentials**, and it's a question of this being
/// a feature or a bug.
// TODO: when dealing with `http.*.*` configuration, generalize this algorithm as needed and support precedence.
pub fn credential_helpers(
&self,
mut url: gix_url::Url,
) -> Result<
(
gix_credentials::helper::Cascade,
gix_credentials::helper::Action,
gix_prompt::Options<'static>,
),
Error,
> {
let mut programs = Vec::new();
let mut use_http_path = false;
let url_had_user_initially = url.user().is_some();
normalize(&mut url);
if let Some(credential_sections) = self
.repo
.config
.resolved
.sections_by_name_and_filter("credential", &mut self.repo.filter_config_section())
{
for section in credential_sections {
let section = match section.header().subsection_name() {
Some(pattern) => gix_url::parse(pattern).ok().and_then(|mut pattern| {
normalize(&mut pattern);
let is_http = matches!(pattern.scheme, gix_url::Scheme::Https | gix_url::Scheme::Http);
let scheme = &pattern.scheme;
let host = pattern.host();
let ports = is_http
.then(|| (pattern.port_or_default(), url.port_or_default()))
.unwrap_or((pattern.port, url.port));
let path = (!(is_http && pattern.path_is_root())).then_some(&pattern.path);
if !path.map_or(true, |path| path == &url.path) {
return None;
}
if pattern.user().is_some() && pattern.user() != url.user() {
return None;
}
(scheme == &url.scheme && host_matches(host, url.host()) && ports.0 == ports.1).then_some((
section,
&credential::UrlParameter::HELPER,
&credential::UrlParameter::USERNAME,
&credential::UrlParameter::USE_HTTP_PATH,
))
}),
None => Some((
section,
&Credential::HELPER,
&Credential::USERNAME,
&Credential::USE_HTTP_PATH,
)),
};
if let Some((section, helper_key, username_key, use_http_path_key)) = section {
for value in section.values(helper_key.name) {
if value.trim().is_empty() {
programs.clear();
} else {
programs.push(gix_credentials::Program::from_custom_definition(value.into_owned()));
}
}
if let Some(Some(user)) = (!url_had_user_initially).then(|| {
section
.value(username_key.name)
.filter(|n| !n.trim().is_empty())
.and_then(|n| {
let n: Vec<_> = Cow::into_owned(n).into();
n.into_string().ok()
})
}) {
url.set_user(Some(user));
}
if let Some(toggle) = section
.value(use_http_path_key.name)
.map(|val| {
gix_config::Boolean::try_from(val)
.map_err(|err| Error::InvalidUseHttpPath {
source: err,
section: section.header().to_bstring(),
})
.map(|b| b.0)
})
.transpose()?
{
use_http_path = toggle;
}
}
}
}
let allow_git_env = self.repo.options.permissions.env.git_prefix.is_allowed();
let allow_ssh_env = self.repo.options.permissions.env.ssh_prefix.is_allowed();
let prompt_options = gix_prompt::Options {
askpass: self
.trusted_path(Core::ASKPASS.logical_name().as_str())
.transpose()
.ignore_empty()?
.map(|c| Cow::Owned(c.into_owned())),
mode: self
.try_boolean(Credentials::TERMINAL_PROMPT.logical_name().as_str())
.map(|val| Credentials::TERMINAL_PROMPT.enrich_error(val))
.transpose()
.with_leniency(self.repo.config.lenient_config)?
.and_then(|val| (!val).then_some(gix_prompt::Mode::Disable))
.unwrap_or_default(),
}
.apply_environment(allow_git_env, allow_ssh_env, false /* terminal prompt */);
Ok((
gix_credentials::helper::Cascade {
programs,
use_http_path,
// The default ssh implementation uses binaries that do their own auth, so our passwords aren't used.
query_user_only: url.scheme == gix_url::Scheme::Ssh,
stderr: self
.try_boolean(Credentials::HELPER_STDERR.logical_name().as_str())
.map(|val| Credentials::HELPER_STDERR.enrich_error(val))
.transpose()
.with_leniency(self.repo.options.lenient_config)?
.unwrap_or(true),
},
gix_credentials::helper::Action::get_for_url(url.to_bstring()),
prompt_options,
))
}
}
fn host_matches(pattern: Option<&str>, host: Option<&str>) -> bool {
match (pattern, host) {
(Some(pattern), Some(host)) => {
let lfields = pattern.split('.');
let rfields = host.split('.');
if lfields.clone().count() != rfields.clone().count() {
return false;
}
lfields
.zip(rfields)
.all(|(pat, value)| gix_glob::wildmatch(pat.into(), value.into(), gix_glob::wildmatch::Mode::empty()))
}
(None, None) => true,
(Some(_), None) | (None, Some(_)) => false,
}
}
fn normalize(url: &mut gix_url::Url) {
if !url.path_is_root() && url.path.ends_with(b"/") {
url.path.pop();
}
}