This commit is contained in:
1 parent
2feecae70d
commit
c2dd826920
11 files changed
+773
-220
No files matched your search
+167
-33
@@ -1022,8 +1022,8 @@ impl ReqwestGitTransport {
|
||||
|
||||
fn response(response: reqwest::blocking::Response) -> Result<Vec<u8>, GitError> {
|
||||
let status = response.status();
|
||||
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
|
||||
return Err(GitError::AuthenticationFailed);
|
||||
if let Some(error) = classify_http_status(status) {
|
||||
return Err(error);
|
||||
}
|
||||
if !status.is_success() {
|
||||
return Err(GitError::InvalidRepository(format!(
|
||||
@@ -1273,13 +1273,103 @@ fn map_ambiguous_push_error(error: GitError) -> GitError {
|
||||
|
||||
fn map_reqwest_error(error: reqwest::Error) -> GitError {
|
||||
if error.is_builder() {
|
||||
return invalid(error);
|
||||
return generic_https_error(&error);
|
||||
}
|
||||
let message = error.to_string().to_ascii_lowercase();
|
||||
if message.contains("certificate") || message.contains("tls") {
|
||||
GitError::TlsFailed
|
||||
classify_structured_https_error(&error).unwrap_or_else(|| generic_https_error(&error))
|
||||
}
|
||||
|
||||
fn classify_http_status(status: reqwest::StatusCode) -> Option<GitError> {
|
||||
matches!(
|
||||
status,
|
||||
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
|
||||
)
|
||||
.then_some(GitError::AuthenticationFailed)
|
||||
}
|
||||
|
||||
fn classify_structured_https_error(error: &(dyn Error + 'static)) -> Option<GitError> {
|
||||
let mut current = Some(error);
|
||||
let mut tls = false;
|
||||
let mut network = false;
|
||||
while let Some(error) = current {
|
||||
if let Some(error) = error.downcast_ref::<reqwest::Error>() {
|
||||
if error.status().and_then(classify_http_status).is_some() {
|
||||
return Some(GitError::AuthenticationFailed);
|
||||
}
|
||||
network |= error.is_timeout() || error.is_connect();
|
||||
}
|
||||
if error.is::<rustls::Error>() {
|
||||
tls = true;
|
||||
}
|
||||
if let Some(error) = error.downcast_ref::<std::io::Error>() {
|
||||
if error.kind() == std::io::ErrorKind::PermissionDenied {
|
||||
return Some(GitError::AuthenticationFailed);
|
||||
}
|
||||
network |= matches!(
|
||||
error.kind(),
|
||||
std::io::ErrorKind::ConnectionRefused
|
||||
| std::io::ErrorKind::ConnectionReset
|
||||
| std::io::ErrorKind::ConnectionAborted
|
||||
| std::io::ErrorKind::NotConnected
|
||||
| std::io::ErrorKind::TimedOut
|
||||
| std::io::ErrorKind::AddrNotAvailable
|
||||
| std::io::ErrorKind::NetworkUnreachable
|
||||
| std::io::ErrorKind::HostUnreachable
|
||||
);
|
||||
}
|
||||
current = error.source();
|
||||
}
|
||||
if tls {
|
||||
Some(GitError::TlsFailed)
|
||||
} else if network {
|
||||
Some(GitError::NetworkUnavailable)
|
||||
} else {
|
||||
GitError::NetworkUnavailable
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn generic_https_error(error: &dyn fmt::Display) -> GitError {
|
||||
GitError::InvalidRepository(protocol_diagnostic(error.to_string().as_bytes()))
|
||||
}
|
||||
|
||||
fn map_https_connect_error(error: gix::remote::connect::Error) -> GitError {
|
||||
match error {
|
||||
error @ gix::remote::connect::Error::Connect(_) => {
|
||||
classify_structured_https_error(&error).unwrap_or_else(|| generic_https_error(&error))
|
||||
}
|
||||
error => generic_https_error(&error),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_https_prepare_error(error: gix::remote::fetch::prepare::Error) -> GitError {
|
||||
match error {
|
||||
error @ gix::remote::fetch::prepare::Error::RefMap(
|
||||
gix::remote::ref_map::Error::Transport(_) | gix::remote::ref_map::Error::Handshake(_),
|
||||
) => classify_structured_https_error(&error).unwrap_or_else(|| generic_https_error(&error)),
|
||||
error => generic_https_error(&error),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_https_fetch_error(
|
||||
error: gix::remote::fetch::Error,
|
||||
control: &GitOperationControl,
|
||||
) -> GitError {
|
||||
if control.is_cancelled() {
|
||||
return GitError::Cancelled;
|
||||
}
|
||||
match error {
|
||||
gix::remote::fetch::Error::Fetch(gix::protocol::fetch::Error::ConsumePack(_)) => {
|
||||
GitError::MalformedGitPack
|
||||
}
|
||||
gix::remote::fetch::Error::Fetch(
|
||||
gix::protocol::fetch::Error::FetchResponse(_)
|
||||
| gix::protocol::fetch::Error::Negotiate(_)
|
||||
| gix::protocol::fetch::Error::MissingServerFeature { .. },
|
||||
) => GitError::GitProtocolFailed,
|
||||
error @ (gix::remote::fetch::Error::Fetch(gix::protocol::fetch::Error::Client(_))
|
||||
| gix::remote::fetch::Error::Client(_)) => {
|
||||
classify_structured_https_error(&error).unwrap_or_else(|| generic_https_error(&error))
|
||||
}
|
||||
error => generic_https_error(&error),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1905,7 +1995,7 @@ impl GitRepository {
|
||||
.map_err(invalid)?;
|
||||
let mut connection = remote
|
||||
.connect(gix::remote::Direction::Fetch)
|
||||
.map_err(invalid)?;
|
||||
.map_err(map_https_connect_error)?;
|
||||
connection.set_credentials(move |action| match action {
|
||||
gix::credentials::helper::Action::Get(context) => {
|
||||
let same_origin = context
|
||||
@@ -1931,32 +2021,10 @@ impl GitRepository {
|
||||
});
|
||||
let prepared = connection
|
||||
.prepare_fetch(gix::progress::Discard, Default::default())
|
||||
.map_err(invalid)?;
|
||||
.map_err(map_https_prepare_error)?;
|
||||
let outcome = prepared
|
||||
.receive(gix::progress::Discard, control.cancelled.as_ref())
|
||||
.map_err(|error| {
|
||||
if control.is_cancelled() {
|
||||
return GitError::Cancelled;
|
||||
}
|
||||
let text = error.to_string();
|
||||
let lower = text.to_ascii_lowercase();
|
||||
if text.contains("401")
|
||||
|| text.contains("403")
|
||||
|| lower.contains("authentication")
|
||||
|| (lower.contains("credential") && lower.contains("not accepted"))
|
||||
{
|
||||
GitError::AuthenticationFailed
|
||||
} else if lower.contains("certificate") || lower.contains("tls") {
|
||||
GitError::TlsFailed
|
||||
} else if lower.contains("network")
|
||||
|| lower.contains("connect")
|
||||
|| lower.contains("dns")
|
||||
{
|
||||
GitError::NetworkUnavailable
|
||||
} else {
|
||||
invalid(error)
|
||||
}
|
||||
})?;
|
||||
.map_err(|error| map_https_fetch_error(error, control))?;
|
||||
Ok(matches!(
|
||||
outcome.status,
|
||||
gix::remote::fetch::Status::Change { .. }
|
||||
@@ -4331,8 +4399,12 @@ mod ssh_push_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{error::Error, fmt, io};
|
||||
|
||||
use super::{
|
||||
GitError, GitIdentity, GitRemoteCredentialOverride, GitRepository, SshPassphraseProvider,
|
||||
GitError, GitIdentity, GitOperationControl, GitRemoteCredentialOverride, GitRepository,
|
||||
SshPassphraseProvider, classify_http_status, classify_structured_https_error,
|
||||
generic_https_error, map_https_fetch_error,
|
||||
};
|
||||
use crate::{
|
||||
config::SshFingerprint,
|
||||
@@ -4341,6 +4413,17 @@ mod tests {
|
||||
|
||||
struct Passphrases;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Diagnostic(String);
|
||||
|
||||
impl fmt::Display for Diagnostic {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for Diagnostic {}
|
||||
|
||||
impl SshPassphraseProvider for Passphrases {
|
||||
fn ssh_key_passphrase(
|
||||
&self,
|
||||
@@ -4362,6 +4445,57 @@ mod tests {
|
||||
assert!(repository.repository.committer().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_failures_use_only_structured_status_and_sources() {
|
||||
assert_eq!(
|
||||
classify_http_status(reqwest::StatusCode::UNAUTHORIZED),
|
||||
Some(GitError::AuthenticationFailed)
|
||||
);
|
||||
assert_eq!(
|
||||
classify_http_status(reqwest::StatusCode::FORBIDDEN),
|
||||
Some(GitError::AuthenticationFailed)
|
||||
);
|
||||
assert_eq!(
|
||||
classify_structured_https_error(&io::Error::from(io::ErrorKind::ConnectionRefused)),
|
||||
Some(GitError::NetworkUnavailable)
|
||||
);
|
||||
assert_eq!(
|
||||
classify_structured_https_error(&io::Error::from(io::ErrorKind::TimedOut)),
|
||||
Some(GitError::NetworkUnavailable)
|
||||
);
|
||||
assert_eq!(
|
||||
classify_structured_https_error(&rustls::Error::General("opaque".to_owned())),
|
||||
Some(GitError::TlsFailed)
|
||||
);
|
||||
|
||||
let rendered_only = Diagnostic(
|
||||
"401 403 authentication credential certificate tls network connect dns".to_owned(),
|
||||
);
|
||||
assert_eq!(classify_structured_https_error(&rendered_only), None);
|
||||
assert!(matches!(
|
||||
generic_https_error(&rendered_only),
|
||||
GitError::InvalidRepository(_)
|
||||
));
|
||||
|
||||
let local_permission = gix::remote::fetch::Error::RemovePackKeepFile {
|
||||
path: "pack.keep".into(),
|
||||
source: io::Error::from(io::ErrorKind::PermissionDenied),
|
||||
};
|
||||
assert!(matches!(
|
||||
map_https_fetch_error(local_permission, &GitOperationControl::default()),
|
||||
GitError::InvalidRepository(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_https_diagnostic_is_bounded() {
|
||||
let error = generic_https_error(&Diagnostic("x".repeat(32 * 1024)));
|
||||
let GitError::InvalidRepository(diagnostic) = error else {
|
||||
panic!("unstructured HTTPS error must stay generic");
|
||||
};
|
||||
assert!(diagnostic.len() <= 8 * 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_ssh_passphrase_override_is_bound_to_its_fingerprint() {
|
||||
let selected = SshFingerprint::parse("SHA256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU")
|
||||
|
||||
Reference in new issue
Block a user