Skip to main content

gix_protocol/handshake/
function.rs

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