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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
use bstr::BString;

use crate::Protocol;

/// The way to connect to a process speaking the `git` protocol.
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum ConnectMode {
    /// A git daemon.
    Daemon,
    /// A spawned `git` process to upload a pack to the client.
    Process,
}

/// A TCP connection to either a `git` daemon or a spawned `git` process.
///
/// When connecting to a daemon, additional context information is sent with the first line of the handshake. Otherwise that
/// context is passed using command line arguments to a [spawned `git` process][crate::client::file::SpawnProcessOnDemand].
pub struct Connection<R, W> {
    pub(in crate::client) writer: W,
    pub(in crate::client) line_provider: gix_packetline::StreamingPeekableIter<R>,
    pub(in crate::client) path: BString,
    pub(in crate::client) virtual_host: Option<(String, Option<u16>)>,
    pub(in crate::client) desired_version: Protocol,
    custom_url: Option<BString>,
    pub(in crate::client) mode: ConnectMode,
}

impl<R, W> Connection<R, W> {
    /// Return the inner reader and writer
    pub fn into_inner(self) -> (R, W) {
        (self.line_provider.into_inner(), self.writer)
    }

    /// Optionally set the URL to be returned when asked for it if `Some` or calculate a default for `None`.
    ///
    /// The URL is required as parameter for authentication helpers which are called in transports
    /// that support authentication. Even though plain git transports don't support that, this
    /// may well be the case in custom transports.
    pub fn custom_url(mut self, url: Option<BString>) -> Self {
        self.custom_url = url;
        self
    }
}

mod message {
    use bstr::{BString, ByteVec};

    use crate::{Protocol, Service};

    pub fn connect(
        service: Service,
        desired_version: Protocol,
        path: &[u8],
        virtual_host: Option<&(String, Option<u16>)>,
        extra_parameters: &[(&str, Option<&str>)],
    ) -> BString {
        let mut out = bstr::BString::from(service.as_str());
        out.push(b' ');
        let path = gix_url::expand_path::for_shell(path.into());
        out.extend_from_slice(&path);
        out.push(0);
        if let Some((host, port)) = virtual_host {
            out.push_str("host=");
            out.extend_from_slice(host.as_bytes());
            if let Some(port) = port {
                out.push_byte(b':');
                out.push_str(format!("{port}"));
            }
            out.push(0);
        }
        // We only send the version when needed, as otherwise a V2 server who is asked for V1 will respond with 'version 1'
        // as extra lines in the reply, which we don't want to handle. Especially since an old server will not respond with that
        // line (is what I assume, at least), so it's an optional part in the response to understand and handle. There is no value
        // in that, so let's help V2 servers to respond in a way that assumes V1.
        let extra_params_need_null_prefix = if desired_version != Protocol::V1 {
            out.push(0);
            out.push_str(format!("version={}", desired_version as usize));
            out.push(0);
            false
        } else {
            true
        };

        if !extra_parameters.is_empty() {
            if extra_params_need_null_prefix {
                out.push(0);
            }
            for (key, value) in extra_parameters {
                match value {
                    Some(value) => out.push_str(format!("{key}={value}")),
                    None => out.push_str(key),
                }
                out.push(0);
            }
        }
        out
    }
    #[cfg(test)]
    mod tests {
        use crate::{client::git, Protocol, Service};

        #[test]
        fn version_1_without_host_and_version() {
            assert_eq!(
                git::message::connect(Service::UploadPack, Protocol::V1, b"hello/world", None, &[]),
                "git-upload-pack hello/world\0"
            )
        }
        #[test]
        fn version_2_without_host_and_version() {
            assert_eq!(
                git::message::connect(Service::UploadPack, Protocol::V2, b"hello\\world", None, &[]),
                "git-upload-pack hello\\world\0\0version=2\0"
            )
        }
        #[test]
        fn version_2_without_host_and_version_and_exta_parameters() {
            assert_eq!(
                git::message::connect(
                    Service::UploadPack,
                    Protocol::V2,
                    b"/path/project.git",
                    None,
                    &[("key", Some("value")), ("value-only", None)]
                ),
                "git-upload-pack /path/project.git\0\0version=2\0key=value\0value-only\0"
            )
        }
        #[test]
        fn with_host_without_port() {
            assert_eq!(
                git::message::connect(
                    Service::UploadPack,
                    Protocol::V1,
                    b"hello\\world",
                    Some(&("host".into(), None)),
                    &[]
                ),
                "git-upload-pack hello\\world\0host=host\0"
            )
        }
        #[test]
        fn with_host_without_port_and_extra_parameters() {
            assert_eq!(
                git::message::connect(
                    Service::UploadPack,
                    Protocol::V1,
                    b"hello\\world",
                    Some(&("host".into(), None)),
                    &[("key", Some("value")), ("value-only", None)]
                ),
                "git-upload-pack hello\\world\0host=host\0\0key=value\0value-only\0"
            )
        }
        #[test]
        fn with_host_with_port() {
            assert_eq!(
                git::message::connect(
                    Service::UploadPack,
                    Protocol::V1,
                    b"hello\\world",
                    Some(&("host".into(), Some(404))),
                    &[]
                ),
                "git-upload-pack hello\\world\0host=host:404\0"
            )
        }

        #[test]
        fn with_strange_host_and_port() {
            assert_eq!(
                git::message::connect(
                    Service::UploadPack,
                    Protocol::V1,
                    b"--upload-pack=attack",
                    Some(&("--proxy=other-attack".into(), Some(404))),
                    &[]
                ),
                "git-upload-pack --upload-pack=attack\0host=--proxy=other-attack:404\0",
                "we explicitly allow possible `-arg` arguments to be passed to the git daemon - the remote must protect against exploitation, we don't want to prevent legitimate cases"
            )
        }
    }
}

#[cfg(feature = "async-client")]
mod async_io;

#[cfg(feature = "blocking-client")]
mod blocking_io;
#[cfg(feature = "blocking-client")]
pub use blocking_io::connect;