gix_protocol/handshake/
function.rs

1use gix_features::{progress, progress::Progress};
2use gix_transport::{client, client::SetServiceResponse, Service};
3use maybe_async::maybe_async;
4
5use super::{Error, Outcome};
6use crate::{credentials, handshake::refs};
7
8/// Perform a handshake with the server on the other side of `transport`, with `authenticate` being used if authentication
9/// turns out to be required. `extra_parameters` are the parameters `(name, optional value)` to add to the handshake,
10/// each time it is performed in case authentication is required.
11/// `progress` is used to inform about what's currently happening.
12#[allow(clippy::result_large_err)]
13#[maybe_async]
14pub async fn handshake<AuthFn, T>(
15    mut transport: T,
16    service: Service,
17    mut authenticate: AuthFn,
18    extra_parameters: Vec<(String, Option<String>)>,
19    progress: &mut impl Progress,
20) -> Result<Outcome, Error>
21where
22    AuthFn: FnMut(credentials::helper::Action) -> credentials::protocol::Result,
23    T: client::Transport,
24{
25    let _span = gix_features::trace::detail!("gix_protocol::handshake()", service = ?service, extra_parameters = ?extra_parameters);
26    let (server_protocol_version, refs, capabilities) = {
27        progress.init(None, progress::steps());
28        progress.set_name("handshake".into());
29        progress.step();
30
31        let extra_parameters: Vec<_> = extra_parameters
32            .iter()
33            .map(|(k, v)| (k.as_str(), v.as_deref()))
34            .collect();
35        let supported_versions: Vec<_> = transport.supported_protocol_versions().into();
36
37        let result = transport.handshake(service, &extra_parameters).await;
38        let SetServiceResponse {
39            actual_protocol,
40            capabilities,
41            refs,
42        } = match result {
43            Ok(v) => Ok(v),
44            Err(client::Error::Io(ref err)) if err.kind() == std::io::ErrorKind::PermissionDenied => {
45                drop(result); // needed to workaround this: https://github.com/rust-lang/rust/issues/76149
46                let url = transport.to_url().into_owned();
47                progress.set_name("authentication".into());
48                let credentials::protocol::Outcome { identity, next } =
49                    authenticate(credentials::helper::Action::get_for_url(url.clone()))?
50                        .ok_or(Error::EmptyCredentials)?;
51                transport.set_identity(identity)?;
52                progress.step();
53                progress.set_name("handshake (authenticated)".into());
54                match transport.handshake(service, &extra_parameters).await {
55                    Ok(v) => {
56                        authenticate(next.store())?;
57                        Ok(v)
58                    }
59                    // Still no permission? Reject the credentials.
60                    Err(client::Error::Io(err)) if err.kind() == std::io::ErrorKind::PermissionDenied => {
61                        authenticate(next.erase())?;
62                        return Err(Error::InvalidCredentials { url, source: err });
63                    }
64                    // Otherwise, do nothing, as we don't know if it actually got to try the credentials.
65                    // If they were previously stored, they remain. In the worst case, the user has to enter them again
66                    // next time they try.
67                    Err(err) => Err(err),
68                }
69            }
70            Err(err) => Err(err),
71        }?;
72
73        if !supported_versions.is_empty() && !supported_versions.contains(&actual_protocol) {
74            return Err(Error::TransportProtocolPolicyViolation {
75                actual_version: actual_protocol,
76            });
77        }
78
79        let parsed_refs = match refs {
80            Some(mut refs) => {
81                assert!(
82                    matches!(
83                        actual_protocol,
84                        gix_transport::Protocol::V0 | gix_transport::Protocol::V1
85                    ),
86                    "Only V(0|1) auto-responds with refs"
87                );
88                Some(
89                    refs::from_v1_refs_received_as_part_of_handshake_and_capabilities(&mut refs, capabilities.iter())
90                        .await?,
91                )
92            }
93            None => None,
94        };
95        (actual_protocol, parsed_refs, capabilities)
96    }; // this scope is needed, see https://github.com/rust-lang/rust/issues/76149
97
98    let (refs, v1_shallow_updates) = refs
99        .map(|(refs, shallow)| (Some(refs), Some(shallow)))
100        .unwrap_or_default();
101
102    Ok(Outcome {
103        server_protocol_version,
104        refs,
105        v1_shallow_updates,
106        capabilities,
107    })
108}