jsonrpc/http/
minreq_http.rs

1//! This module implements the [`crate::client::Transport`] trait using [`minreq`]
2//! as the underlying HTTP transport.
3//!
4//! [minreq]: <https://github.com/neonmoe/minreq>
5
6#[cfg(jsonrpc_fuzz)]
7use std::io::{self, Read, Write};
8#[cfg(jsonrpc_fuzz)]
9use std::sync::Mutex;
10use std::time::Duration;
11use std::{error, fmt};
12
13use crate::client::Transport;
14use crate::{Request, Response};
15
16const DEFAULT_URL: &str = "http://localhost";
17const DEFAULT_PORT: u16 = 8332; // the default RPC port for bitcoind.
18#[cfg(not(jsonrpc_fuzz))]
19const DEFAULT_TIMEOUT_SECONDS: u64 = 15;
20#[cfg(jsonrpc_fuzz)]
21const DEFAULT_TIMEOUT_SECONDS: u64 = 1;
22
23/// An HTTP transport that uses [`minreq`] and is useful for running a bitcoind RPC client.
24#[derive(Clone, Debug)]
25pub struct MinreqHttpTransport {
26    /// URL of the RPC server.
27    url: String,
28    /// timeout only supports second granularity.
29    timeout: Duration,
30    /// The value of the `Authorization` HTTP header, i.e., a base64 encoding of 'user:password'.
31    basic_auth: Option<String>,
32}
33
34impl Default for MinreqHttpTransport {
35    fn default() -> Self {
36        MinreqHttpTransport {
37            url: format!("{}:{}", DEFAULT_URL, DEFAULT_PORT),
38            timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECONDS),
39            basic_auth: None,
40        }
41    }
42}
43
44impl MinreqHttpTransport {
45    /// Constructs a new [`MinreqHttpTransport`] with default parameters.
46    pub fn new() -> Self {
47        MinreqHttpTransport::default()
48    }
49
50    /// Returns a builder for [`MinreqHttpTransport`].
51    pub fn builder() -> Builder {
52        Builder::new()
53    }
54
55    fn request<R>(&self, req: impl serde::Serialize) -> Result<R, Error>
56    where
57        R: for<'a> serde::de::Deserialize<'a>,
58    {
59        let req = match &self.basic_auth {
60            Some(auth) => minreq::Request::new(minreq::Method::Post, &self.url)
61                .with_timeout(self.timeout.as_secs())
62                .with_header("Authorization", auth)
63                .with_json(&req)?,
64            None => minreq::Request::new(minreq::Method::Post, &self.url)
65                .with_timeout(self.timeout.as_secs())
66                .with_json(&req)?,
67        };
68
69        // Send the request and parse the response. If the response is an error that does not
70        // contain valid JSON in its body (for instance if the bitcoind HTTP server work queue
71        // depth is exceeded), return the raw HTTP error so users can match against it.
72        let resp = req.send()?;
73        match resp.json() {
74            Ok(json) => Ok(json),
75            Err(minreq_err) => {
76                if resp.status_code != 200 {
77                    Err(Error::Http(HttpError {
78                        status_code: resp.status_code,
79                        body: resp.as_str().unwrap_or("").to_string(),
80                    }))
81                } else {
82                    Err(Error::Minreq(minreq_err))
83                }
84            }
85        }
86    }
87}
88
89impl Transport for MinreqHttpTransport {
90    fn send_request(&self, req: Request) -> Result<Response, crate::Error> {
91        Ok(self.request(req)?)
92    }
93
94    fn send_batch(&self, reqs: &[Request]) -> Result<Vec<Response>, crate::Error> {
95        Ok(self.request(reqs)?)
96    }
97
98    fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result {
99        write!(f, "{}", self.url)
100    }
101}
102
103/// Builder for simple bitcoind [`MinreqHttpTransport`].
104#[derive(Clone, Debug)]
105pub struct Builder {
106    tp: MinreqHttpTransport,
107}
108
109impl Builder {
110    /// Constructs a new [`Builder`] with default configuration and the URL to use.
111    pub fn new() -> Builder {
112        Builder {
113            tp: MinreqHttpTransport::new(),
114        }
115    }
116
117    /// Sets the timeout after which requests will abort if they aren't finished.
118    pub fn timeout(mut self, timeout: Duration) -> Self {
119        self.tp.timeout = timeout;
120        self
121    }
122
123    /// Sets the URL of the server to the transport.
124    pub fn url(mut self, url: &str) -> Result<Self, Error> {
125        self.tp.url = url.to_owned();
126        Ok(self)
127    }
128
129    /// Adds authentication information to the transport.
130    pub fn basic_auth(mut self, user: String, pass: Option<String>) -> Self {
131        let mut s = user;
132        s.push(':');
133        if let Some(ref pass) = pass {
134            s.push_str(pass.as_ref());
135        }
136        self.tp.basic_auth = Some(format!("Basic {}", &base64::encode(s.as_bytes())));
137        self
138    }
139
140    /// Adds authentication information to the transport using a cookie string ('user:pass').
141    ///
142    /// Does no checking on the format of the cookie string, just base64 encodes whatever is passed in.
143    ///
144    /// # Examples
145    ///
146    /// ```no_run
147    /// # use jsonrpc::minreq_http::MinreqHttpTransport;
148    /// # use std::fs::{self, File};
149    /// # use std::path::Path;
150    /// # let cookie_file = Path::new("~/.bitcoind/.cookie");
151    /// let mut file = File::open(cookie_file).expect("couldn't open cookie file");
152    /// let mut cookie = String::new();
153    /// fs::read_to_string(&mut cookie).expect("couldn't read cookie file");
154    /// let client = MinreqHttpTransport::builder().cookie_auth(cookie);
155    /// ```
156    pub fn cookie_auth<S: AsRef<str>>(mut self, cookie: S) -> Self {
157        self.tp.basic_auth = Some(format!("Basic {}", &base64::encode(cookie.as_ref().as_bytes())));
158        self
159    }
160
161    /// Builds the final [`MinreqHttpTransport`].
162    pub fn build(self) -> MinreqHttpTransport {
163        self.tp
164    }
165}
166
167impl Default for Builder {
168    fn default() -> Self {
169        Builder::new()
170    }
171}
172
173/// An HTTP error.
174#[derive(Debug)]
175pub struct HttpError {
176    /// Status code of the error response.
177    pub status_code: i32,
178    /// Raw body of the error response.
179    pub body: String,
180}
181
182impl fmt::Display for HttpError {
183    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
184        write!(f, "status: {}, body: {}", self.status_code, self.body)
185    }
186}
187
188impl error::Error for HttpError {}
189
190/// Error that can happen when sending requests. In case of error, a JSON error is returned if the
191/// body of the response could be parsed as such. Otherwise, an HTTP error is returned containing
192/// the status code and the raw body.
193#[non_exhaustive]
194#[derive(Debug)]
195pub enum Error {
196    /// JSON parsing error.
197    Json(serde_json::Error),
198    /// Minreq error.
199    Minreq(minreq::Error),
200    /// HTTP error that does not contain valid JSON as body.
201    Http(HttpError),
202}
203
204impl fmt::Display for Error {
205    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
206        match *self {
207            Error::Json(ref e) => write!(f, "parsing JSON failed: {}", e),
208            Error::Minreq(ref e) => write!(f, "minreq: {}", e),
209            Error::Http(ref e) => write!(f, "http ({})", e),
210        }
211    }
212}
213
214impl error::Error for Error {
215    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
216        use self::Error::*;
217
218        match *self {
219            Json(ref e) => Some(e),
220            Minreq(ref e) => Some(e),
221            Http(ref e) => Some(e),
222        }
223    }
224}
225
226impl From<serde_json::Error> for Error {
227    fn from(e: serde_json::Error) -> Self {
228        Error::Json(e)
229    }
230}
231
232impl From<minreq::Error> for Error {
233    fn from(e: minreq::Error) -> Self {
234        Error::Minreq(e)
235    }
236}
237
238impl From<Error> for crate::Error {
239    fn from(e: Error) -> crate::Error {
240        match e {
241            Error::Json(e) => crate::Error::Json(e),
242            e => crate::Error::Transport(Box::new(e)),
243        }
244    }
245}
246
247/// Global mutex used by the fuzzing harness to inject data into the read end of the TCP stream.
248#[cfg(jsonrpc_fuzz)]
249pub static FUZZ_TCP_SOCK: Mutex<Option<io::Cursor<Vec<u8>>>> = Mutex::new(None);
250
251#[cfg(jsonrpc_fuzz)]
252#[derive(Clone, Debug)]
253struct TcpStream;
254
255#[cfg(jsonrpc_fuzz)]
256mod impls {
257    use super::*;
258
259    impl Read for TcpStream {
260        fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
261            match *FUZZ_TCP_SOCK.lock().unwrap() {
262                Some(ref mut cursor) => io::Read::read(cursor, buf),
263                None => Ok(0),
264            }
265        }
266    }
267    impl Write for TcpStream {
268        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
269            io::sink().write(buf)
270        }
271        fn flush(&mut self) -> io::Result<()> {
272            Ok(())
273        }
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::Client;
281
282    #[test]
283    fn construct() {
284        let tp = Builder::new()
285            .timeout(Duration::from_millis(100))
286            .url("http://localhost:22")
287            .unwrap()
288            .basic_auth("user".to_string(), None)
289            .build();
290        let _ = Client::with_transport(tp);
291    }
292}