git2_curl/
lib.rs

1//! A crate for using libcurl as a backend for HTTP git requests with git2-rs.
2//!
3//! This crate provides one public function, `register`, which will register
4//! a custom HTTP transport with libcurl for any HTTP requests made by libgit2.
5//! At this time the `register` function is unsafe for the same reasons that
6//! `git2::transport::register` is also unsafe.
7//!
8//! It is not recommended to use this crate wherever possible. The current
9//! libcurl backend used, `curl-rust`, only supports executing a request in one
10//! method call implying no streaming support. This consequently means that
11//! when a repository is cloned the entire contents of the repo are downloaded
12//! into memory, and *then* written off to disk by libgit2 afterwards. It
13//! should be possible to alleviate this problem in the future.
14//!
15//! > **NOTE**: At this time this crate likely does not support a `git push`
16//! >           operation, only clones.
17
18#![doc(html_root_url = "https://docs.rs/git2-curl/0.21")]
19#![deny(missing_docs)]
20#![warn(rust_2018_idioms)]
21#![cfg_attr(test, deny(warnings))]
22
23use std::error;
24use std::io::prelude::*;
25use std::io::{self, Cursor};
26use std::str;
27use std::sync::{Arc, Mutex, Once};
28
29use curl::easy::{Easy, List};
30use git2::transport::SmartSubtransportStream;
31use git2::transport::{Service, SmartSubtransport, Transport};
32use git2::Error;
33use log::{debug, info};
34use url::Url;
35
36struct CurlTransport {
37    handle: Arc<Mutex<Easy>>,
38    /// The URL of the remote server, e.g. `https://github.com/user/repo`
39    ///
40    /// This is an empty string until the first action is performed.
41    /// If there is an HTTP redirect, this will be updated with the new URL.
42    base_url: Arc<Mutex<String>>,
43}
44
45struct CurlSubtransport {
46    handle: Arc<Mutex<Easy>>,
47    service: &'static str,
48    url_path: &'static str,
49    base_url: Arc<Mutex<String>>,
50    method: &'static str,
51    reader: Option<Cursor<Vec<u8>>>,
52    sent_request: bool,
53}
54
55/// Register the libcurl backend for HTTP requests made by libgit2.
56///
57/// This function takes one parameter, a `handle`, which is used to perform all
58/// future HTTP requests. The handle can be previously configured with
59/// information such as proxies, SSL information, etc.
60///
61/// This function is unsafe largely for the same reasons as
62/// `git2::transport::register`:
63///
64/// * The function needs to be synchronized against all other creations of
65///   transport (any API calls to libgit2).
66/// * The function will leak `handle` as once registered it is not currently
67///   possible to unregister the backend.
68///
69/// This function may be called concurrently, but only the first `handle` will
70/// be used. All others will be discarded.
71pub unsafe fn register(handle: Easy) {
72    static INIT: Once = Once::new();
73
74    let handle = Arc::new(Mutex::new(handle));
75    let handle2 = handle.clone();
76    INIT.call_once(move || {
77        git2::transport::register("http", move |remote| factory(remote, handle.clone())).unwrap();
78        git2::transport::register("https", move |remote| factory(remote, handle2.clone())).unwrap();
79    });
80}
81
82fn factory(remote: &git2::Remote<'_>, handle: Arc<Mutex<Easy>>) -> Result<Transport, Error> {
83    Transport::smart(
84        remote,
85        true,
86        CurlTransport {
87            handle: handle,
88            base_url: Arc::new(Mutex::new(String::new())),
89        },
90    )
91}
92
93impl SmartSubtransport for CurlTransport {
94    fn action(
95        &self,
96        url: &str,
97        action: Service,
98    ) -> Result<Box<dyn SmartSubtransportStream>, Error> {
99        let mut base_url = self.base_url.lock().unwrap();
100        if base_url.len() == 0 {
101            *base_url = url.to_string();
102        }
103        let (service, path, method) = match action {
104            Service::UploadPackLs => ("upload-pack", "/info/refs?service=git-upload-pack", "GET"),
105            Service::UploadPack => ("upload-pack", "/git-upload-pack", "POST"),
106            Service::ReceivePackLs => {
107                ("receive-pack", "/info/refs?service=git-receive-pack", "GET")
108            }
109            Service::ReceivePack => ("receive-pack", "/git-receive-pack", "POST"),
110        };
111        info!("action {} {}", service, path);
112        Ok(Box::new(CurlSubtransport {
113            handle: self.handle.clone(),
114            service: service,
115            url_path: path,
116            base_url: self.base_url.clone(),
117            method: method,
118            reader: None,
119            sent_request: false,
120        }))
121    }
122
123    fn close(&self) -> Result<(), Error> {
124        Ok(()) // ...
125    }
126}
127
128impl CurlSubtransport {
129    fn err<E: Into<Box<dyn error::Error + Send + Sync>>>(&self, err: E) -> io::Error {
130        io::Error::new(io::ErrorKind::Other, err)
131    }
132
133    fn execute(&mut self, data: &[u8]) -> io::Result<()> {
134        if self.sent_request {
135            return Err(self.err("already sent HTTP request"));
136        }
137        let agent = format!("git/1.0 (git2-curl {})", env!("CARGO_PKG_VERSION"));
138
139        // Parse our input URL to figure out the host
140        let url = format!("{}{}", self.base_url.lock().unwrap(), self.url_path);
141        let parsed = Url::parse(&url).map_err(|_| self.err("invalid url, failed to parse"))?;
142        let host = match parsed.host_str() {
143            Some(host) => host,
144            None => return Err(self.err("invalid url, did not have a host")),
145        };
146
147        // Prep the request
148        debug!("request to {}", url);
149        let mut h = self.handle.lock().unwrap();
150        h.url(&url)?;
151        h.useragent(&agent)?;
152        h.follow_location(true)?;
153        match self.method {
154            "GET" => h.get(true)?,
155            "PUT" => h.put(true)?,
156            "POST" => h.post(true)?,
157            other => h.custom_request(other)?,
158        }
159
160        let mut headers = List::new();
161        headers.append(&format!("Host: {}", host))?;
162        if data.len() > 0 {
163            h.post_fields_copy(data)?;
164            headers.append(&format!(
165                "Accept: application/x-git-{}-result",
166                self.service
167            ))?;
168            headers.append(&format!(
169                "Content-Type: \
170                 application/x-git-{}-request",
171                self.service
172            ))?;
173        } else {
174            headers.append("Accept: */*")?;
175        }
176        headers.append("Expect:")?;
177        h.http_headers(headers)?;
178
179        let mut content_type = None;
180        let mut data = Vec::new();
181        {
182            let mut h = h.transfer();
183
184            // Look for the Content-Type header
185            h.header_function(|header| {
186                let header = match str::from_utf8(header) {
187                    Ok(s) => s,
188                    Err(..) => return true,
189                };
190                let mut parts = header.splitn(2, ": ");
191                let name = parts.next().unwrap();
192                let value = match parts.next() {
193                    Some(value) => value,
194                    None => return true,
195                };
196                if name.eq_ignore_ascii_case("Content-Type") {
197                    content_type = Some(value.trim().to_string());
198                }
199
200                true
201            })?;
202
203            // Collect the request's response in-memory
204            h.write_function(|buf| {
205                data.extend_from_slice(buf);
206                Ok(buf.len())
207            })?;
208
209            // Send the request
210            h.perform()?;
211        }
212
213        let code = h.response_code()?;
214        if code != 200 {
215            return Err(self.err(
216                &format!(
217                    "failed to receive HTTP 200 response: \
218                     got {}",
219                    code
220                )[..],
221            ));
222        }
223
224        // Check returned headers
225        let expected = match self.method {
226            "GET" => format!("application/x-git-{}-advertisement", self.service),
227            _ => format!("application/x-git-{}-result", self.service),
228        };
229        match content_type {
230            Some(ref content_type) if *content_type != expected => {
231                return Err(self.err(
232                    &format!(
233                        "expected a Content-Type header \
234                         with `{}` but found `{}`",
235                        expected, content_type
236                    )[..],
237                ))
238            }
239            Some(..) => {}
240            None => {
241                return Err(self.err(
242                    &format!(
243                        "expected a Content-Type header \
244                         with `{}` but didn't find one",
245                        expected
246                    )[..],
247                ))
248            }
249        }
250
251        // Ok, time to read off some data.
252        let rdr = Cursor::new(data);
253        self.reader = Some(rdr);
254
255        // If there was a redirect, update the `CurlTransport` with the new base.
256        if let Ok(Some(effective_url)) = h.effective_url() {
257            let new_base = if effective_url.ends_with(self.url_path) {
258                // Strip the action from the end.
259                &effective_url[..effective_url.len() - self.url_path.len()]
260            } else {
261                // I'm not sure if this code path makes sense, but it's what
262                // libgit does.
263                effective_url
264            };
265            *self.base_url.lock().unwrap() = new_base.to_string();
266        }
267
268        Ok(())
269    }
270}
271
272impl Read for CurlSubtransport {
273    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
274        if self.reader.is_none() {
275            self.execute(&[])?;
276        }
277        self.reader.as_mut().unwrap().read(buf)
278    }
279}
280
281impl Write for CurlSubtransport {
282    fn write(&mut self, data: &[u8]) -> io::Result<usize> {
283        if self.reader.is_none() {
284            self.execute(data)?;
285        }
286        Ok(data.len())
287    }
288    fn flush(&mut self) -> io::Result<()> {
289        Ok(())
290    }
291}