1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
use gix_features::{progress, progress::Progress};
use gix_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<AuthFn, T>(
    mut transport: T,
    service: Service,
    mut authenticate: AuthFn,
    extra_parameters: Vec<(String, Option<String>)>,
    progress: &mut impl Progress,
) -> Result<Outcome, Error>
where
    AuthFn: FnMut(credentials::helper::Action) -> credentials::protocol::Result,
    T: client::Transport,
{
    let _span = gix_features::trace::detail!("gix_protocol::handshake()", service = ?service, extra_parameters = ?extra_parameters);
    let (server_protocol_version, refs, capabilities) = {
        progress.init(None, progress::steps());
        progress.set_name("handshake".into());
        progress.step();

        let extra_parameters: Vec<_> = extra_parameters
            .iter()
            .map(|(k, v)| (k.as_str(), v.as_deref()))
            .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".into());
                let credentials::protocol::Outcome { identity, next } =
                    authenticate(credentials::helper::Action::get_for_url(url.clone()))?
                        .ok_or(Error::EmptyCredentials)?;
                transport.set_identity(identity)?;
                progress.step();
                progress.set_name("handshake (authenticated)".into());
                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, source: err });
                    }
                    // 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!(
                    matches!(
                        actual_protocol,
                        gix_transport::Protocol::V0 | gix_transport::Protocol::V1
                    ),
                    "Only V(0|1) 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,
    })
}