use git_features::{progress, progress::Progress}; use git_transport::{client, client::SetServiceResponse, Service}; use maybe_async::maybe_async; use super::{Error, Outcome}; use crate::{credentials, handshake::refs}; /// Perform a handshake with the server on the other side of `transport`, with `authenticate` being used if authentication /// turns out to be required. `extra_parameters` are the parameters `(name, optional value)` to add to the handshake, /// each time it is performed in case authentication is required. /// `progress` is used to inform about what's currently happening. #[allow(clippy::result_large_err)] #[maybe_async] pub async fn handshake( mut transport: T, service: Service, mut authenticate: AuthFn, extra_parameters: Vec<(String, Option)>, progress: &mut impl Progress, ) -> Result where AuthFn: FnMut(credentials::helper::Action) -> credentials::protocol::Result, T: client::Transport, { let (server_protocol_version, refs, capabilities) = { progress.init(None, progress::steps()); progress.set_name("handshake"); progress.step(); let extra_parameters: Vec<_> = extra_parameters .iter() .map(|(k, v)| (k.as_str(), v.as_ref().map(|s| s.as_str()))) .collect(); let supported_versions: Vec<_> = transport.supported_protocol_versions().into(); let result = transport.handshake(service, &extra_parameters).await; let SetServiceResponse { actual_protocol, capabilities, refs, } = match result { Ok(v) => Ok(v), Err(client::Error::Io(ref err)) if err.kind() == std::io::ErrorKind::PermissionDenied => { drop(result); // needed to workaround this: https://github.com/rust-lang/rust/issues/76149 let url = transport.to_url().into_owned(); progress.set_name("authentication"); let credentials::protocol::Outcome { identity, next } = authenticate(credentials::helper::Action::get_for_url(url.clone()))? .expect("FILL provides an identity or errors"); transport.set_identity(identity)?; progress.step(); progress.set_name("handshake (authenticated)"); match transport.handshake(service, &extra_parameters).await { Ok(v) => { authenticate(next.store())?; Ok(v) } // Still no permission? Reject the credentials. Err(client::Error::Io(err)) if err.kind() == std::io::ErrorKind::PermissionDenied => { authenticate(next.erase())?; return Err(Error::InvalidCredentials { url }); } // Otherwise, do nothing, as we don't know if it actually got to try the credentials. // If they were previously stored, they remain. In the worst case, the user has to enter them again // next time they try. Err(err) => Err(err), } } Err(err) => Err(err), }?; if !supported_versions.is_empty() && !supported_versions.contains(&actual_protocol) { return Err(Error::TransportProtocolPolicyViolation { actual_version: actual_protocol, }); } let parsed_refs = match refs { Some(mut refs) => { assert_eq!( actual_protocol, git_transport::Protocol::V1, "Only V1 auto-responds with refs" ); Some( refs::from_v1_refs_received_as_part_of_handshake_and_capabilities(&mut refs, capabilities.iter()) .await?, ) } None => None, }; (actual_protocol, parsed_refs, capabilities) }; // this scope is needed, see https://github.com/rust-lang/rust/issues/76149 Ok(Outcome { server_protocol_version, refs, capabilities, }) }