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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use git_transport::client::Capabilities;

use crate::fetch::Ref;

/// The result of the [`handshake()`][super::handshake()] function.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
pub struct Outcome {
    /// The protocol version the server responded with. It might have downgraded the desired version.
    pub server_protocol_version: git_transport::Protocol,
    /// The references reported as part of the Protocol::V1 handshake, or `None` otherwise as V2 requires a separate request.
    pub refs: Option<Vec<Ref>>,
    /// The server capabilities.
    pub capabilities: Capabilities,
}

mod error {
    use git_transport::client;

    use crate::{credentials, fetch::refs};

    /// The error returned by [`handshake()`][crate::fetch::handshake()].
    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error(transparent)]
        Credentials(#[from] credentials::protocol::Error),
        #[error(transparent)]
        Transport(#[from] client::Error),
        #[error("The transport didn't accept the advertised server version {actual_version:?} and closed the connection client side")]
        TransportProtocolPolicyViolation { actual_version: git_transport::Protocol },
        #[error(transparent)]
        ParseRefs(#[from] refs::parse::Error),
    }
}
pub use error::Error;

pub(crate) mod function {
    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, fetch::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.
    #[maybe_async]
    pub async fn handshake<AuthFn, T>(
        mut transport: T,
        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 (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::UploadPack, &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();
                    progress.set_name("authentication");
                    let credentials::protocol::Outcome { identity, next } =
                        authenticate(credentials::helper::Action::get_for_url(url))?
                            .expect("FILL provides an identity or errors");
                    transport.set_identity(identity)?;
                    progress.step();
                    progress.set_name("handshake (authenticated)");
                    match transport.handshake(Service::UploadPack, &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())?;
                            Err(client::Error::Io { 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_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,
        })
    }
}