| //! A crate for using libcurl as a backend for HTTP git requests with git2-rs. |
| //! |
| //! This crate provides one public function, `register`, which will register |
| //! a custom HTTP transport with libcurl for any HTTP requests made by libgit2. |
| //! At this time the `register` function is unsafe for the same reasons that |
| //! `git2::transport::register` is also unsafe. |
| //! |
| //! It is not recommended to use this crate wherever possible. The current |
| //! libcurl backend used, `curl-rust`, only supports executing a request in one |
| //! method call implying no streaming support. This consequently means that |
| //! when a repository is cloned the entire contents of the repo are downloaded |
| //! into memory, and *then* written off to disk by libgit2 afterwards. It |
| //! should be possible to alleviate this problem in the future. |
| //! |
| //! > **NOTE**: At this time this crate likely does not support a `git push` |
| //! > operation, only clones. |
| |
| #![doc(html_root_url = "https://docs.rs/git2-curl/0.20")] |
| #![deny(missing_docs)] |
| #![warn(rust_2018_idioms)] |
| #![cfg_attr(test, deny(warnings))] |
| |
| use std::error; |
| use std::io::prelude::*; |
| use std::io::{self, Cursor}; |
| use std::str; |
| use std::sync::{Arc, Mutex, Once}; |
| |
| use curl::easy::{Easy, List}; |
| use git2::transport::SmartSubtransportStream; |
| use git2::transport::{Service, SmartSubtransport, Transport}; |
| use git2::Error; |
| use log::{debug, info}; |
| use url::Url; |
| |
| struct CurlTransport { |
| handle: Arc<Mutex<Easy>>, |
| /// The URL of the remote server, e.g. `https://github.com/user/repo` |
| /// |
| /// This is an empty string until the first action is performed. |
| /// If there is an HTTP redirect, this will be updated with the new URL. |
| base_url: Arc<Mutex<String>>, |
| } |
| |
| struct CurlSubtransport { |
| handle: Arc<Mutex<Easy>>, |
| service: &'static str, |
| url_path: &'static str, |
| base_url: Arc<Mutex<String>>, |
| method: &'static str, |
| reader: Option<Cursor<Vec<u8>>>, |
| sent_request: bool, |
| } |
| |
| /// Register the libcurl backend for HTTP requests made by libgit2. |
| /// |
| /// This function takes one parameter, a `handle`, which is used to perform all |
| /// future HTTP requests. The handle can be previously configured with |
| /// information such as proxies, SSL information, etc. |
| /// |
| /// This function is unsafe largely for the same reasons as |
| /// `git2::transport::register`: |
| /// |
| /// * The function needs to be synchronized against all other creations of |
| /// transport (any API calls to libgit2). |
| /// * The function will leak `handle` as once registered it is not currently |
| /// possible to unregister the backend. |
| /// |
| /// This function may be called concurrently, but only the first `handle` will |
| /// be used. All others will be discarded. |
| pub unsafe fn register(handle: Easy) { |
| static INIT: Once = Once::new(); |
| |
| let handle = Arc::new(Mutex::new(handle)); |
| let handle2 = handle.clone(); |
| INIT.call_once(move || { |
| git2::transport::register("http", move |remote| factory(remote, handle.clone())).unwrap(); |
| git2::transport::register("https", move |remote| factory(remote, handle2.clone())).unwrap(); |
| }); |
| } |
| |
| fn factory(remote: &git2::Remote<'_>, handle: Arc<Mutex<Easy>>) -> Result<Transport, Error> { |
| Transport::smart( |
| remote, |
| true, |
| CurlTransport { |
| handle: handle, |
| base_url: Arc::new(Mutex::new(String::new())), |
| }, |
| ) |
| } |
| |
| impl SmartSubtransport for CurlTransport { |
| fn action( |
| &self, |
| url: &str, |
| action: Service, |
| ) -> Result<Box<dyn SmartSubtransportStream>, Error> { |
| let mut base_url = self.base_url.lock().unwrap(); |
| if base_url.len() == 0 { |
| *base_url = url.to_string(); |
| } |
| let (service, path, method) = match action { |
| Service::UploadPackLs => ("upload-pack", "/info/refs?service=git-upload-pack", "GET"), |
| Service::UploadPack => ("upload-pack", "/git-upload-pack", "POST"), |
| Service::ReceivePackLs => { |
| ("receive-pack", "/info/refs?service=git-receive-pack", "GET") |
| } |
| Service::ReceivePack => ("receive-pack", "/git-receive-pack", "POST"), |
| }; |
| info!("action {} {}", service, path); |
| Ok(Box::new(CurlSubtransport { |
| handle: self.handle.clone(), |
| service: service, |
| url_path: path, |
| base_url: self.base_url.clone(), |
| method: method, |
| reader: None, |
| sent_request: false, |
| })) |
| } |
| |
| fn close(&self) -> Result<(), Error> { |
| Ok(()) // ... |
| } |
| } |
| |
| impl CurlSubtransport { |
| fn err<E: Into<Box<dyn error::Error + Send + Sync>>>(&self, err: E) -> io::Error { |
| io::Error::new(io::ErrorKind::Other, err) |
| } |
| |
| fn execute(&mut self, data: &[u8]) -> io::Result<()> { |
| if self.sent_request { |
| return Err(self.err("already sent HTTP request")); |
| } |
| let agent = format!("git/1.0 (git2-curl {})", env!("CARGO_PKG_VERSION")); |
| |
| // Parse our input URL to figure out the host |
| let url = format!("{}{}", self.base_url.lock().unwrap(), self.url_path); |
| let parsed = Url::parse(&url).map_err(|_| self.err("invalid url, failed to parse"))?; |
| let host = match parsed.host_str() { |
| Some(host) => host, |
| None => return Err(self.err("invalid url, did not have a host")), |
| }; |
| |
| // Prep the request |
| debug!("request to {}", url); |
| let mut h = self.handle.lock().unwrap(); |
| h.url(&url)?; |
| h.useragent(&agent)?; |
| h.follow_location(true)?; |
| match self.method { |
| "GET" => h.get(true)?, |
| "PUT" => h.put(true)?, |
| "POST" => h.post(true)?, |
| other => h.custom_request(other)?, |
| } |
| |
| let mut headers = List::new(); |
| headers.append(&format!("Host: {}", host))?; |
| if data.len() > 0 { |
| h.post_fields_copy(data)?; |
| headers.append(&format!( |
| "Accept: application/x-git-{}-result", |
| self.service |
| ))?; |
| headers.append(&format!( |
| "Content-Type: \ |
| application/x-git-{}-request", |
| self.service |
| ))?; |
| } else { |
| headers.append("Accept: */*")?; |
| } |
| headers.append("Expect:")?; |
| h.http_headers(headers)?; |
| |
| let mut content_type = None; |
| let mut data = Vec::new(); |
| { |
| let mut h = h.transfer(); |
| |
| // Look for the Content-Type header |
| h.header_function(|header| { |
| let header = match str::from_utf8(header) { |
| Ok(s) => s, |
| Err(..) => return true, |
| }; |
| let mut parts = header.splitn(2, ": "); |
| let name = parts.next().unwrap(); |
| let value = match parts.next() { |
| Some(value) => value, |
| None => return true, |
| }; |
| if name.eq_ignore_ascii_case("Content-Type") { |
| content_type = Some(value.trim().to_string()); |
| } |
| |
| true |
| })?; |
| |
| // Collect the request's response in-memory |
| h.write_function(|buf| { |
| data.extend_from_slice(buf); |
| Ok(buf.len()) |
| })?; |
| |
| // Send the request |
| h.perform()?; |
| } |
| |
| let code = h.response_code()?; |
| if code != 200 { |
| return Err(self.err( |
| &format!( |
| "failed to receive HTTP 200 response: \ |
| got {}", |
| code |
| )[..], |
| )); |
| } |
| |
| // Check returned headers |
| let expected = match self.method { |
| "GET" => format!("application/x-git-{}-advertisement", self.service), |
| _ => format!("application/x-git-{}-result", self.service), |
| }; |
| match content_type { |
| Some(ref content_type) if *content_type != expected => { |
| return Err(self.err( |
| &format!( |
| "expected a Content-Type header \ |
| with `{}` but found `{}`", |
| expected, content_type |
| )[..], |
| )) |
| } |
| Some(..) => {} |
| None => { |
| return Err(self.err( |
| &format!( |
| "expected a Content-Type header \ |
| with `{}` but didn't find one", |
| expected |
| )[..], |
| )) |
| } |
| } |
| |
| // Ok, time to read off some data. |
| let rdr = Cursor::new(data); |
| self.reader = Some(rdr); |
| |
| // If there was a redirect, update the `CurlTransport` with the new base. |
| if let Ok(Some(effective_url)) = h.effective_url() { |
| let new_base = if effective_url.ends_with(self.url_path) { |
| // Strip the action from the end. |
| &effective_url[..effective_url.len() - self.url_path.len()] |
| } else { |
| // I'm not sure if this code path makes sense, but it's what |
| // libgit does. |
| effective_url |
| }; |
| *self.base_url.lock().unwrap() = new_base.to_string(); |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl Read for CurlSubtransport { |
| fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { |
| if self.reader.is_none() { |
| self.execute(&[])?; |
| } |
| self.reader.as_mut().unwrap().read(buf) |
| } |
| } |
| |
| impl Write for CurlSubtransport { |
| fn write(&mut self, data: &[u8]) -> io::Result<usize> { |
| if self.reader.is_none() { |
| self.execute(data)?; |
| } |
| Ok(data.len()) |
| } |
| fn flush(&mut self) -> io::Result<()> { |
| Ok(()) |
| } |
| } |