gix_transport/client/blocking_io/http/curl/
mod.rs

1use std::{
2    sync::mpsc::{Receiver, SyncSender},
3    thread,
4};
5
6use gix_features::io;
7
8use crate::client::blocking_io::http::{self, traits::PostBodyDataKind};
9
10mod remote;
11
12/// Options to configure the `curl` HTTP handler.
13#[derive(Default)]
14pub struct Options {
15    /// If `true` and runtime configuration is possible for `curl` backends, certificates revocation will be checked.
16    ///
17    /// This only works on windows apparently. Ignored if `None`.
18    pub schannel_check_revoke: Option<bool>,
19}
20
21/// The error returned by the 'remote' helper, a purely internal construct to perform http requests.
22///
23/// It can be used for downcasting errors, which are boxed to hide the actual implementation.
24#[derive(Debug, thiserror::Error)]
25#[allow(missing_docs)]
26pub enum Error {
27    #[error(transparent)]
28    Curl(#[from] curl::Error),
29    #[error(transparent)]
30    Redirect(#[from] http::redirect::Error),
31    #[error("Could not finish reading all data to post to the remote")]
32    ReadPostBody(#[from] std::io::Error),
33    #[error(transparent)]
34    Authenticate(#[from] gix_credentials::protocol::Error),
35}
36
37impl crate::IsSpuriousError for Error {
38    fn is_spurious(&self) -> bool {
39        match self {
40            Error::Curl(err) => curl_is_spurious(err),
41            _ => false,
42        }
43    }
44}
45
46pub(crate) fn curl_is_spurious(err: &curl::Error) -> bool {
47    err.is_couldnt_connect()
48        || err.is_couldnt_resolve_proxy()
49        || err.is_couldnt_resolve_host()
50        || err.is_operation_timedout()
51        || err.is_recv_error()
52        || err.is_send_error()
53        || err.is_http2_error()
54        || err.is_http2_stream_error()
55        || err.is_ssl_connect_error()
56        || err.is_partial_file()
57}
58
59/// A utility to abstract interactions with curl handles.
60pub struct Curl {
61    req: SyncSender<remote::Request>,
62    res: Receiver<remote::Response>,
63    handle: Option<thread::JoinHandle<Result<(), Error>>>,
64    config: http::Options,
65}
66
67impl Curl {
68    fn restore_thread_after_failure(&mut self) -> http::Error {
69        let err_that_brought_thread_down = self
70            .handle
71            .take()
72            .expect("thread handle present")
73            .join()
74            .expect("handler thread should never panic")
75            .expect_err("something should have gone wrong with curl (we join on error only)");
76        let (handle, req, res) = remote::new();
77        self.handle = Some(handle);
78        self.req = req;
79        self.res = res;
80        err_that_brought_thread_down.into()
81    }
82
83    fn make_request(
84        &mut self,
85        url: &str,
86        base_url: &str,
87        headers: impl IntoIterator<Item = impl AsRef<str>>,
88        upload_body_kind: Option<PostBodyDataKind>,
89    ) -> Result<http::PostResponse<io::pipe::Reader, io::pipe::Reader, io::pipe::Writer>, http::Error> {
90        let mut list = curl::easy::List::new();
91        for header in headers {
92            list.append(header.as_ref())?;
93        }
94        if self
95            .req
96            .send(remote::Request {
97                url: url.to_owned(),
98                base_url: base_url.to_owned(),
99                headers: list,
100                upload_body_kind,
101                config: self.config.clone(),
102            })
103            .is_err()
104        {
105            return Err(self.restore_thread_after_failure());
106        }
107        let remote::Response {
108            headers,
109            body,
110            upload_body,
111        } = match self.res.recv() {
112            Ok(res) => res,
113            Err(_) => return Err(self.restore_thread_after_failure()),
114        };
115        Ok(http::PostResponse {
116            post_body: upload_body,
117            headers,
118            body,
119        })
120    }
121}
122
123impl Default for Curl {
124    fn default() -> Self {
125        let (handle, req, res) = remote::new();
126        Curl {
127            handle: Some(handle),
128            req,
129            res,
130            config: http::Options::default(),
131        }
132    }
133}
134
135#[allow(clippy::type_complexity)]
136impl http::Http for Curl {
137    type Headers = io::pipe::Reader;
138    type ResponseBody = io::pipe::Reader;
139    type PostBody = io::pipe::Writer;
140
141    fn get(
142        &mut self,
143        url: &str,
144        base_url: &str,
145        headers: impl IntoIterator<Item = impl AsRef<str>>,
146    ) -> Result<http::GetResponse<Self::Headers, Self::ResponseBody>, http::Error> {
147        self.make_request(url, base_url, headers, None).map(Into::into)
148    }
149
150    fn post(
151        &mut self,
152        url: &str,
153        base_url: &str,
154        headers: impl IntoIterator<Item = impl AsRef<str>>,
155        body: PostBodyDataKind,
156    ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> {
157        self.make_request(url, base_url, headers, Some(body))
158    }
159
160    fn configure(
161        &mut self,
162        config: &dyn std::any::Any,
163    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
164        if let Some(config) = config.downcast_ref::<http::Options>() {
165            self.config = config.clone();
166        }
167        Ok(())
168    }
169}