git2_ureq/
lib.rs

1use std::error;
2use std::io;
3use std::io::prelude::*;
4use std::sync::{Arc, Mutex, Once};
5use ureq::Agent;
6use ureq::AgentBuilder;
7use ureq::Proxy;
8use url::Url;
9
10use log::{debug, info};
11
12use git2::transport::{Service, SmartSubtransport, SmartSubtransportStream, Transport};
13use git2::Error;
14
15#[derive(Default)]
16struct UreqTransport {
17    /// The URL of the remote server, e.g. "https://github.com/user/repo"
18    ///
19    /// This is an empty string until the first action is performed.
20    /// If there is an HTTP redirect, this will be updated with the new URL.
21    base_url: Arc<Mutex<String>>,
22    proxy: Option<Proxy>,
23}
24
25struct UreqSubtransport {
26    service: &'static str,
27    url_path: &'static str,
28    base_url: Arc<Mutex<String>>,
29    method: &'static str,
30    reader: Option<Box<dyn Read + Send>>,
31    sent_request: bool,
32    client: Agent,
33}
34
35pub unsafe fn register(proxy: Option<String>) {
36    static INIT: Once = Once::new();
37
38    let proxy = proxy.map(|s| Proxy::new(s).ok()).flatten();
39    let p = proxy.clone();
40
41    INIT.call_once(move || {
42        git2::transport::register("http", move |remote| factory(remote, proxy.as_ref())).unwrap();
43        git2::transport::register("https", move |remote| factory(remote, p.as_ref())).unwrap();
44    });
45}
46
47fn factory(remote: &git2::Remote<'_>, proxy: Option<&Proxy>) -> Result<Transport, Error> {
48    Transport::smart(remote, true, UreqTransport::new(proxy.cloned()))
49}
50
51impl UreqTransport {
52    pub(crate) fn new(proxy: Option<Proxy>) -> Self {
53        Self {
54            proxy,
55            ..Default::default()
56        }
57    }
58}
59
60impl SmartSubtransport for UreqTransport {
61    fn action(
62        &self,
63        url: &str,
64        action: Service,
65    ) -> Result<Box<dyn SmartSubtransportStream>, Error> {
66        let mut base_url = self.base_url.lock().unwrap();
67        if base_url.len() == 0 {
68            *base_url = url.to_string();
69        }
70        let (service, path, method) = match action {
71            Service::UploadPackLs => ("upload-pack", "/info/refs?service=git-upload-pack", "GET"),
72            Service::UploadPack => ("upload-pack", "/git-upload-pack", "POST"),
73            Service::ReceivePackLs => {
74                ("receive-pack", "/info/refs?service=git-receive-pack", "GET")
75            }
76            Service::ReceivePack => ("receive-pack", "/git-receive-pack", "POST"),
77        };
78        info!("action {} {}", service, path);
79        Ok(Box::new(UreqSubtransport {
80            service,
81            url_path: path,
82            base_url: self.base_url.clone(),
83            method,
84            reader: None,
85            sent_request: false,
86            client: self
87                .proxy
88                .clone()
89                .map(|p| AgentBuilder::new().proxy(p))
90                .unwrap_or_else(AgentBuilder::new)
91                .build(),
92        }))
93    }
94
95    fn close(&self) -> Result<(), Error> {
96        Ok(())
97    }
98}
99
100impl UreqSubtransport {
101    fn err<E: Into<Box<dyn error::Error + Send + Sync>>>(&self, err: E) -> io::Error {
102        io::Error::new(io::ErrorKind::Other, err)
103    }
104
105    fn execute(&mut self, data: &[u8]) -> io::Result<()> {
106        if self.sent_request {
107            return Err(self.err("already sent HTTP request"));
108        }
109
110        let agent = format!("git/1.0 (git2-ureq {})", env!("CARGO_PKG_VERSION"));
111
112        // Parse our input URL to figure out the host
113        let url = format!("{}{}", self.base_url.lock().unwrap(), self.url_path);
114        let parsed = Url::parse(&url).map_err(|_| self.err("invalid url, failed to parse"))?;
115        let host = match parsed.host_str() {
116            Some(host) => host,
117            None => return Err(self.err("invalid url, did not have a host")),
118        };
119
120        // Prep the request
121        debug!("request to {}", url);
122        let request = self
123            .client
124            .request(self.method, &url)
125            .set("User-Agent", &agent)
126            .set("Host", &host)
127            .set("Expect", "");
128        let request = if data.is_empty() {
129            request.set("Accept", "*/*")
130        } else {
131            request
132                .set(
133                    "Accept",
134                    &format!("application/x-git-{}-result", self.service),
135                )
136                .set(
137                    "Conent-Type",
138                    &format!("application/x-git-{}-request", self.service),
139                )
140        };
141
142        let response = request.send(data).unwrap();
143        let content_type = response.header("Content-Type");
144
145        let code = response.status();
146        if code != 200 {
147            return Err(self.err(&format!("failed to receive HTTP 200 response: got {code}",)[..]));
148        }
149
150        // Check returned headers
151        let expected = match self.method {
152            "GET" => format!("application/x-git-{}-advertisement", self.service),
153            _ => format!("application/x-git-{}-result", self.service),
154        };
155
156        if let Some(content_type) = content_type {
157            if content_type != expected {
158                return Err(self.err(
159                    &format!(
160                        "expected a Content-Type header with `{expected}` but found `{content_type}`",
161                    )[..],
162                ));
163            }
164        } else {
165            return Err(
166                self.err(
167                    &format!(
168                        "expected a Content-Type header with `{expected}` but didn't find one"
169                    )[..],
170                ),
171            );
172        }
173
174        // preserve response body for reading afterwards
175        self.reader = Some(Box::new(response.into_reader()));
176
177        Ok(())
178    }
179}
180
181impl Read for UreqSubtransport {
182    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
183        if self.reader.is_none() {
184            self.execute(&[])?;
185        }
186
187        self.reader.as_mut().unwrap().read(buf)
188    }
189}
190
191impl Write for UreqSubtransport {
192    fn write(&mut self, data: &[u8]) -> io::Result<usize> {
193        if self.reader.is_none() {
194            self.execute(data)?;
195        }
196        Ok(data.len())
197    }
198
199    fn flush(&mut self) -> io::Result<()> {
200        Ok(())
201    }
202}