// crimp - Higher-level Rust cURL API // // Copyright (C) 2019 Vincent Ambo // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see <http://www.gnu.org/licenses/>. //! # crimp //! //! This library provides a simplified API over the [cURL Rust //! bindings][] that resemble that of higher-level libraries such as //! [reqwest][]. All calls are synchronous. //! //! `crimp` is intended to be used in situations where HTTP client //! functionality is desired without adding a significant number of //! dependencies or sacrificing too much usability. //! //! Using `crimp` to make HTTP requests is done using a simple //! builder-pattern style API. For example, to make a `GET`-request //! and print the result to `stdout`: //! //! ```rust //! use crimp::Request; //! //! let response = Request::get("http://httpbin.org/get") //! .user_agent("crimp test suite") //! .unwrap() //! .send() //! .unwrap() //! .as_string() //! .unwrap(); //! //! println!("Status: {}\nBody: {}", response.status, response.body); //! # assert_eq!(response.status, 200); //! ``` //! //! If a feature from the underlying cURL library is missing, the //! `Request::raw` method can be used as an escape hatch to deal with //! the handle directly. Should you find yourself doing this, please //! [file an issue][]. //! //! `crimp` does not currently expose functionality for re-using a //! cURL Easy handle, meaning that keep-alive of HTTP connections and //! the like is not supported. //! //! ## Cargo features //! //! All optional features are enabled by default. //! //! * `json`: Adds `Request::json` and `Response::as_json` methods which can be used for convenient //! serialisation of request/response bodies using `serde_json`. This feature adds a dependency on //! the `serde` and `serde_json` crates. //! //! ## Initialisation //! //! It is recommended to call the underlying `curl::init` method //! (re-exported as `crimp::init`) when launching your application to //! initialise the cURL library. This is not required, but will //! otherwise occur the first time a request is made. //! //! [cURL Rust bindings]: https://docs.rs/curl //! [reqwest]: https://docs.rs/reqwest //! [file an issue]: https://github.com/tazjin/crimp/issues extern crate curl; #[cfg(feature = "json")] extern crate serde; #[cfg(feature = "json")] extern crate serde_json; pub use curl::init; use curl::easy::{Auth, Easy, Form, List, ReadError, Transfer, WriteError}; use std::collections::HashMap; use std::io::Write; use std::path::Path; use std::string::{FromUtf8Error, ToString}; use std::time::Duration; #[cfg(feature = "json")] use serde::de::DeserializeOwned; #[cfg(feature = "json")] use serde::Serialize; #[cfg(test)] mod tests; /// HTTP method to use for the request. enum Method { Get, Post, Put, Patch, Delete, } /// Certificate types for client-certificate key pairs. pub enum CertType { P12, PEM, DER, } /// Builder structure for an HTTP request. /// /// This is the primary API-type in `crimp`. After creating a new /// request its parameters are modified using the various builder /// methods until it is consumed by `send()`. pub struct Request<'a> { url: &'a str, method: Method, handle: Easy, headers: List, body: Body<'a>, } enum Body<'a> { NoBody, Form(Form), Bytes { content_type: &'a str, data: &'a [u8], }, #[cfg(feature = "json")] Json(Vec<u8>), } /// HTTP responses structure containing response data and headers. /// /// By default the `send()` function of the `Request` structure will /// return a `Response<Vec<u8>>`. Convenience helpers exist for /// decoding a string via `Response::as_string` or to a /// `serde`-compatible type with `Response::as_json` (if the /// `json`-feature is enabled). #[derive(Debug, PartialEq)] pub struct Response<T> { /// HTTP status code of the response. pub status: u32, /// HTTP headers returned from the remote. pub headers: HashMap<String, String>, /// Body data from the HTTP response. pub body: T, } impl<'a> Request<'a> { /// Initiate an HTTP request with the given method and URL. fn new(method: Method, url: &'a str) -> Self { Request { url, method, handle: Easy::new(), headers: List::new(), body: Body::NoBody, } } /// Initiate a GET request with the given URL. pub fn get(url: &'a str) -> Self { Request::new(Method::Get, url) } /// Initiate a POST request with the given URL. pub fn post(url: &'a str) -> Self { Request::new(Method::Post, url) } /// Initiate a PUT request with the given URL. pub fn put(url: &'a str) -> Self { Request::new(Method::Put, url) } /// Initiate a PATCH request with the given URL. pub fn patch(url: &'a str) -> Self { Request::new(Method::Patch, url) } /// Initiate a DELETE request with the given URL. pub fn delete(url: &'a str) -> Self { Request::new(Method::Delete, url) } /// Add an HTTP header to a request. pub fn header(mut self, k: &str, v: &str) -> Result<Self, curl::Error> { self.headers.append(&format!("{}: {}", k, v))?; Ok(self) } /// Set the `User-Agent` for this request. By default this will be /// set to cURL's standard user agent. pub fn user_agent(mut self, agent: &str) -> Result<Self, curl::Error> { self.handle.useragent(agent)?; Ok(self) } /// Set the `Authorization` header to a `Bearer` value with the /// supplied token. pub fn bearer_auth(mut self, token: &str) -> Result<Self, curl::Error> { self.headers .append(&format!("Authorization: Bearer {}", token))?; Ok(self) } /// Set the `Authorization` header to a basic authentication value /// from the supplied username and password. pub fn basic_auth(mut self, username: &str, password: &str) -> Result<Self, curl::Error> { let mut auth = Auth::new(); auth.basic(true); self.handle.username(username)?; self.handle.password(password)?; self.handle.http_auth(&auth)?; Ok(self) } /// Configure a TLS client certificate on the request. /// /// Depending on whether the certificate file contains the private /// key or not, calling `tls_client_key` may be required in /// addition. /// /// Consult the documentation for the `ssl_cert` and `ssl_key` /// functions in `curl::easy::Easy2` for details on supported /// formats and defaults. pub fn tls_client_cert<P: AsRef<Path>>( mut self, cert_type: CertType, cert: P, ) -> Result<Self, curl::Error> { self.handle.ssl_cert(cert)?; self.handle.ssl_cert_type(match cert_type { CertType::P12 => "P12", CertType::PEM => "PEM", CertType::DER => "DER", })?; Ok(self) } /// Configure a TLS client certificate key on the request. /// /// Note that this does **not** need to be called again for /// PKCS12-encoded key pairs which are set via `tls_client_cert`. /// /// Currently only PEM-encoded key files are supported. pub fn tls_client_key<P: AsRef<Path>>(mut self, key: P) -> Result<Self, curl::Error> { self.handle.ssl_key(key)?; Ok(self) } /// Configure an encryption password for a TLS client certificate /// key on the request. /// /// This is required in case of an encrypted private key that /// should be used. pub fn tls_key_password(mut self, password: &str) -> Result<Self, curl::Error> { self.handle.key_password(password)?; Ok(self) } /// Configure a timeout for the request after which the request /// will be aborted. pub fn timeout(mut self, timeout: Duration) -> Result<Self, curl::Error> { self.handle.timeout(timeout)?; Ok(self) } /// Set custom configuration on the cURL `Easy` handle. /// /// This function can be considered an "escape-hatch" from the /// high-level API which lets users access the internal /// `curl::easy::Easy` handle and configure options on it /// directly. /// /// ``` /// # use crimp::Request; /// let response = Request::get("https://httpbin.org/get") /// .with_handle(|mut handle| handle.referer("Example-Referer")) /// .unwrap() /// .send() /// .unwrap(); /// # /// # assert!(response.is_success()); /// ``` pub fn with_handle<F>(mut self, function: F) -> Result<Self, curl::Error> where F: FnOnce(&mut Easy) -> Result<(), curl::Error>, { function(&mut self.handle)?; Ok(self) } /// Add a byte-array body to a request using the specified /// `Content-Type`. pub fn body(mut self, content_type: &'a str, data: &'a [u8]) -> Self { self.body = Body::Bytes { data, content_type }; self } /// Add a form-encoded body to a request using the `curl::Form` /// type. /// /// ```rust /// # extern crate curl; /// # extern crate serde_json; /// # use crimp::*; /// # use serde_json::{Value, json}; /// use curl::easy::Form; /// /// let mut form = Form::new(); /// form.part("some-name") /// .contents("some-data".as_bytes()) /// .add() /// .unwrap(); /// /// let response = Request::post("https://httpbin.org/post") /// .user_agent("crimp test suite") /// .unwrap() /// .form(form) /// .send() /// .unwrap(); /// # /// # assert_eq!(200, response.status, "form POST should succeed"); /// # assert_eq!( /// # response.as_json::<Value>().unwrap().body.get("form").unwrap(), /// # &json!({"some-name": "some-data"}), /// # "posted form data should match", /// # ); /// ``` /// /// See the documentation of `curl::easy::Form` for details on how /// to construct a form body. pub fn form(mut self, form: Form) -> Self { self.body = Body::Form(form); self } /// Add a JSON-encoded body from a serializable type. #[cfg(feature = "json")] pub fn json<T: Serialize>(mut self, body: &T) -> Result<Self, serde_json::Error> { let json = serde_json::to_vec(body)?; self.body = Body::Json(json); Ok(self) } /// Send the HTTP request and return a response structure /// containing the raw body. pub fn send(mut self) -> Result<Response<Vec<u8>>, curl::Error> { // Configure request basics: self.handle.url(self.url)?; match self.method { Method::Get => self.handle.get(true)?, Method::Post => self.handle.post(true)?, Method::Put => self.handle.upload(true)?, Method::Patch => self.handle.custom_request("PATCH")?, Method::Delete => self.handle.custom_request("DELETE")?, } // Create structures in which to store the response data: let mut headers = HashMap::new(); let mut body = vec![]; // Submit a form value to cURL if it is set and proceed // pretending that there is no body, as the handling of this // type of body happens under-the-hood. if let Body::Form(form) = self.body { self.handle.httppost(form)?; self.body = Body::NoBody; } // Optionally set content type if a body payload is configured // and configure the expected body size (or form payload). match self.body { Body::Bytes { content_type, data } => { match self.method { Method::Put => self.handle.in_filesize(data.len() as u64)?, // TODO(sterni): this may still be wrong for some request types? _ => self.handle.post_field_size(data.len() as u64)?, }; self.headers .append(&format!("Content-Type: {}", content_type))?; } #[cfg(feature = "json")] Body::Json(ref data) => { match self.method { Method::Put => self.handle.in_filesize(data.len() as u64)?, // TODO(sterni): this may still be wrong for some request types? _ => self.handle.post_field_size(data.len() as u64)?, }; self.headers.append("Content-Type: application/json")?; } // Do not set content-type header at all if there is no // body, or if the form handler was invoked above. _ => (), }; // Configure headers on the request: self.handle.http_headers(self.headers)?; { // Take a scoped transfer from the Easy handle. This makes it // possible to write data into the above local buffers without // fighting the borrow-checker: let mut transfer = self.handle.transfer(); // Write the payload if it exists: match self.body { Body::Bytes { data, .. } => chunked_read_function(&mut transfer, data)?, #[cfg(feature = "json")] Body::Json(ref json) => chunked_read_function(&mut transfer, json)?, // Do nothing if there is no body or if the body is a // form. _ => (), }; // Read one header per invocation. Request processing is // terminated if any header is malformed: transfer.header_function(|header| { // Headers are expected to be valid UTF-8 data. If they // are not, the conversion is lossy. // // Technically it is legal for HTTP requests to use // different encodings, but we don't interface with such // services for hygienic reasons. let header = String::from_utf8_lossy(header); let split = header.splitn(2, ':').collect::<Vec<_>>(); // "Malformed" headers are skipped. In most cases this // will only be the HTTP version statement. if split.len() != 2 { return true; } headers.insert(split[0].trim().to_string(), split[1].trim().to_string()); true })?; // Read the body to the allocated buffer. transfer.write_function(|data| { let len = data.len(); body.write_all(data) .map(|_| len) .map_err(|_| WriteError::Pause) })?; transfer.perform()?; } Ok(Response { status: self.handle.response_code()?, headers, body, }) } } /// Provide a data chunk potentially larger than cURL's initial write /// buffer to the data reading callback by tracking the offset off /// already written data. /// /// As we manually set the expected upload size, cURL will call the /// read callback repeatedly until it has all the data it needs. fn chunked_read_function<'easy, 'data>( transfer: &mut Transfer<'easy, 'data>, data: &'data [u8], ) -> Result<(), curl::Error> { let mut data = data; transfer.read_function(move |mut into| { let written = into.write(data).map_err(|_| ReadError::Abort)?; data = &data[written..]; Ok(written) }) } impl<T> Response<T> { /// Check whether the status code of this HTTP response is a /// success (i.e. in the 200-299 range). pub fn is_success(&self) -> bool { self.status >= 200 && self.status < 300 } /// Check whether a request succeeded using `Request::is_success` /// and let users provide a closure that creates a custom error /// from the request if it did not. /// /// This function exists for convenience to avoid having to write /// repetitive `if !response.is_success() { ... }` blocks. pub fn error_for_status<F, E>(self, closure: F) -> Result<Self, E> where F: FnOnce(Self) -> E, { if !self.is_success() { return Err(closure(self)); } Ok(self) } } impl Response<Vec<u8>> { /// Attempt to parse the HTTP response body as a UTF-8 encoded /// string. pub fn as_string(self) -> Result<Response<String>, FromUtf8Error> { let body = String::from_utf8(self.body)?; Ok(Response { body, status: self.status, headers: self.headers, }) } /// Attempt to deserialize the HTTP response body from JSON. #[cfg(feature = "json")] pub fn as_json<T: DeserializeOwned>(self) -> Result<Response<T>, serde_json::Error> { let deserialized = serde_json::from_slice(&self.body)?; Ok(Response { body: deserialized, status: self.status, headers: self.headers, }) } }