use std::fmt; use std::sync::{Arc, Mutex, RwLock}; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use hyper::client::IntoUrl; use hyper::header::{Headers, ContentType, Location, Referer, UserAgent, Accept, Encoding, AcceptEncoding, Range, qitem}; use hyper::method::Method; use hyper::status::StatusCode; use hyper::version::HttpVersion; use hyper::{Url}; use serde::Serialize; use serde_json; use serde_urlencoded; use ::body::{self, Body}; use ::redirect::{self, RedirectPolicy, check_redirect, remove_sensitive_headers}; use ::response::Response; static DEFAULT_USER_AGENT: &'static str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); /// A `Client` to make Requests with. /// /// The Client has various configuration values to tweak, but the defaults /// are set to what is usually the most commonly desired value. /// /// The `Client` holds a connection pool internally, so it is advised that /// you create one and reuse it. /// /// # Examples /// /// ``` /// # use reqwest::{Error, Client}; /// # /// # fn run() -> Result<(), Error> { /// let client = Client::new()?; /// let resp = client.get("http://httpbin.org/").send()?; /// # drop(resp); /// # Ok(()) /// # } /// /// ``` #[derive(Clone)] pub struct Client { inner: Arc, } impl Client { /// Constructs a new `Client`. pub fn new() -> ::Result { let mut client = try_!(new_hyper_client()); client.set_redirect_policy(::hyper::client::RedirectPolicy::FollowNone); Ok(Client { inner: Arc::new(ClientRef { hyper: RwLock::new(client), redirect_policy: Mutex::new(RedirectPolicy::default()), auto_referer: AtomicBool::new(true), auto_ungzip: AtomicBool::new(true), }), }) } /// Enable auto gzip decompression by checking the ContentEncoding response header. /// /// Default is enabled. pub fn gzip(&mut self, enable: bool) { self.inner.auto_ungzip.store(enable, Ordering::Relaxed); } /// Set a `RedirectPolicy` for this client. /// /// Default will follow redirects up to a maximum of 10. pub fn redirect(&mut self, policy: RedirectPolicy) { *self.inner.redirect_policy.lock().unwrap() = policy; } /// Enable or disable automatic setting of the `Referer` header. /// /// Default is `true`. pub fn referer(&mut self, enable: bool) { self.inner.auto_referer.store(enable, Ordering::Relaxed); } /// Set a timeout for both the read and write operations of a client. pub fn timeout(&mut self, timeout: Duration) { let mut client = self.inner.hyper.write().unwrap(); client.set_read_timeout(Some(timeout)); client.set_write_timeout(Some(timeout)); } /// Convenience method to make a `GET` request to a URL. pub fn get(&self, url: U) -> RequestBuilder { self.request(Method::Get, url) } /// Convenience method to make a `POST` request to a URL. pub fn post(&self, url: U) -> RequestBuilder { self.request(Method::Post, url) } /// Convenience method to make a `PUT` request to a URL. pub fn put(&self, url: U) -> RequestBuilder { self.request(Method::Put, url) } /// Convenience method to make a `PATCH` request to a URL. pub fn patch(&self, url: U) -> RequestBuilder { self.request(Method::Patch, url) } /// Convenience method to make a `DELETE` request to a URL. pub fn delete(&self, url: U) -> RequestBuilder { self.request(Method::Delete, url) } /// Convenience method to make a `HEAD` request to a URL. pub fn head(&self, url: U) -> RequestBuilder { self.request(Method::Head, url) } /// Start building a `Request` with the `Method` and `Url`. /// /// Returns a `RequestBuilder`, which will allow setting headers and /// request body before sending. pub fn request(&self, method: Method, url: U) -> RequestBuilder { let url = url.into_url(); RequestBuilder { client: self.inner.clone(), method: method, url: url, _version: HttpVersion::Http11, headers: Headers::new(), body: None, } } } impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Client") .field("redirect_policy", &self.inner.redirect_policy) .field("referer", &self.inner.auto_referer) .field("auto_ungzip", &self.inner.auto_ungzip) .finish() } } struct ClientRef { hyper: RwLock<::hyper::Client>, redirect_policy: Mutex, auto_referer: AtomicBool, auto_ungzip: AtomicBool, } fn new_hyper_client() -> ::Result<::hyper::Client> { use hyper_native_tls::NativeTlsClient; Ok(::hyper::Client::with_connector( ::hyper::client::Pool::with_connector( Default::default(), ::hyper::net::HttpsConnector::new( try_!(NativeTlsClient::new() .map_err(|e| ::hyper::Error::Ssl(Box::new(e))))) ) )) } /// A builder to construct the properties of a `Request`. pub struct RequestBuilder { client: Arc, method: Method, url: Result, _version: HttpVersion, headers: Headers, body: Option<::Result>, } impl RequestBuilder { /// Add a `Header` to this Request. /// /// ```no_run /// use reqwest::header::UserAgent; /// let client = reqwest::Client::new().expect("client failed to construct"); /// /// let res = client.get("https://www.rust-lang.org") /// .header(UserAgent("foo".to_string())) /// .send(); /// ``` pub fn header(mut self, header: H) -> RequestBuilder { self.headers.set(header); self } /// Add a set of Headers to the existing ones on this Request. /// /// The headers will be merged in to any already set. pub fn headers(mut self, headers: ::header::Headers) -> RequestBuilder { self.headers.extend(headers.iter()); self } /// Enable HTTP basic authentication. pub fn basic_auth(self, username: String, password: Option) -> RequestBuilder { self.header(::header::Authorization(::header::Basic{ username: username, password: password, })) } /// Set the request body. pub fn body>(mut self, body: T) -> RequestBuilder { self.body = Some(Ok(body.into())); self } /// Send a form body. /// /// Sets the body to the url encoded serialization of the passed value, /// and also sets the `Content-Type: application/www-form-url-encoded` /// header. /// /// ```no_run /// # use std::collections::HashMap; /// let mut params = HashMap::new(); /// params.insert("lang", "rust"); /// /// let client = reqwest::Client::new().unwrap(); /// let res = client.post("http://httpbin.org") /// .form(¶ms) /// .send(); /// ``` pub fn form(mut self, form: &T) -> RequestBuilder { let body = serde_urlencoded::to_string(form).map_err(::error::from); self.headers.set(ContentType::form_url_encoded()); self.body = Some(body.map(|b| b.into())); self } /// Send a JSON body. /// /// Sets the body to the JSON serialization of the passed value, and /// also sets the `Content-Type: application/json` header. /// /// ```no_run /// # use std::collections::HashMap; /// let mut map = HashMap::new(); /// map.insert("lang", "rust"); /// /// let client = reqwest::Client::new().unwrap(); /// let res = client.post("http://httpbin.org") /// .json(&map) /// .send(); /// ``` pub fn json(mut self, json: &T) -> RequestBuilder { let body = serde_json::to_vec(json).expect("serde to_vec cannot fail"); self.headers.set(ContentType::json()); self.body = Some(Ok(body.into())); self } /// Constructs the Request and sends it the target URL, returning a Response. pub fn send(mut self) -> ::Result { if !self.headers.has::() { self.headers.set(UserAgent(DEFAULT_USER_AGENT.to_owned())); } if !self.headers.has::() { self.headers.set(Accept::star()); } if self.client.auto_ungzip.load(Ordering::Relaxed) && !self.headers.has::() && !self.headers.has::() { self.headers.set(AcceptEncoding(vec![qitem(Encoding::Gzip)])); } let client = self.client; let mut method = self.method; let mut url = try_!(self.url); let mut headers = self.headers; let mut body = match self.body { Some(b) => Some(try_!(b)), None => None, }; let mut urls = Vec::new(); loop { let res = { info!("Request: {:?} {}", method, url); let c = client.hyper.read().unwrap(); let mut req = c.request(method.clone(), url.clone()) .headers(headers.clone()); if let Some(ref mut b) = body { let body = body::as_hyper_body(b); req = req.body(body); } try_!(req.send(), &url) }; let should_redirect = match res.status { StatusCode::MovedPermanently | StatusCode::Found | StatusCode::SeeOther => { body = None; match method { Method::Get | Method::Head => {}, _ => { method = Method::Get; } } true }, StatusCode::TemporaryRedirect | StatusCode::PermanentRedirect => { if let Some(ref body) = body { body::can_reset(body) } else { true } }, _ => false, }; if should_redirect { let loc = { let loc = res.headers.get::().map(|loc| url.join(loc)); if let Some(loc) = loc { loc } else { return Ok(::response::new(res, client.auto_ungzip.load(Ordering::Relaxed))); } }; url = match loc { Ok(loc) => { if client.auto_referer.load(Ordering::Relaxed) { if let Some(referer) = make_referer(&loc, &url) { headers.set(referer); } } urls.push(url); let action = check_redirect(&client.redirect_policy.lock().unwrap(), &loc, &urls); match action { redirect::Action::Follow => loc, redirect::Action::Stop => { debug!("redirect_policy disallowed redirection to '{}'", loc); return Ok(::response::new(res, client.auto_ungzip.load(Ordering::Relaxed))); }, redirect::Action::LoopDetected => { return Err(::error::loop_detected(res.url.clone())); }, redirect::Action::TooManyRedirects => { return Err(::error::too_many_redirects(res.url.clone())); } } }, Err(e) => { debug!("Location header had invalid URI: {:?}", e); return Ok(::response::new(res, client.auto_ungzip.load(Ordering::Relaxed))) } }; remove_sensitive_headers(&mut headers, &url, &urls); debug!("redirecting to {:?} '{}'", method, url); } else { return Ok(::response::new(res, client.auto_ungzip.load(Ordering::Relaxed))) } } } } impl fmt::Debug for RequestBuilder { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("RequestBuilder") .field("method", &self.method) .field("url", &self.url) .field("headers", &self.headers) .finish() } } fn make_referer(next: &Url, previous: &Url) -> Option { if next.scheme() == "http" && previous.scheme() == "https" { return None; } let mut referer = previous.clone(); let _ = referer.set_username(""); let _ = referer.set_password(None); referer.set_fragment(None); Some(Referer(referer.into_string())) } #[cfg(test)] mod tests { use super::*; use ::body; use hyper::method::Method; use hyper::Url; use hyper::header::{Host, Headers, ContentType}; use std::collections::HashMap; use serde_urlencoded; use serde_json; #[test] fn basic_get_request() { let client = Client::new().unwrap(); let some_url = "https://google.com/"; let r = client.get(some_url); assert_eq!(r.method, Method::Get); assert_eq!(r.url, Url::parse(some_url)); } #[test] fn basic_head_request() { let client = Client::new().unwrap(); let some_url = "https://google.com/"; let r = client.head(some_url); assert_eq!(r.method, Method::Head); assert_eq!(r.url, Url::parse(some_url)); } #[test] fn basic_post_request() { let client = Client::new().unwrap(); let some_url = "https://google.com/"; let r = client.post(some_url); assert_eq!(r.method, Method::Post); assert_eq!(r.url, Url::parse(some_url)); } #[test] fn basic_put_request() { let client = Client::new().unwrap(); let some_url = "https://google.com"; let r = client.put(some_url); assert_eq!(r.method, Method::Put); assert_eq!(r.url, Url::parse(some_url)); } #[test] fn basic_patch_request() { let client = Client::new().unwrap(); let some_url = "https://google.com"; let r = client.patch(some_url); assert_eq!(r.method, Method::Patch); assert_eq!(r.url, Url::parse(some_url)); } #[test] fn basic_delete_request() { let client = Client::new().unwrap(); let some_url = "https://google.com"; let r = client.delete(some_url); assert_eq!(r.method, Method::Delete); assert_eq!(r.url, Url::parse(some_url)); } #[test] fn add_header() { let client = Client::new().unwrap(); let some_url = "https://google.com/"; let mut r = client.post(some_url); let header = Host { hostname: "google.com".to_string(), port: None, }; // Add a copy of the header to the request builder r = r.header(header.clone()); // then check it was actually added assert_eq!(r.headers.get::(), Some(&header)); } #[test] fn add_headers() { let client = Client::new().unwrap(); let some_url = "https://google.com/"; let mut r = client.post(some_url); let header = Host { hostname: "google.com".to_string(), port: None, }; let mut headers = Headers::new(); headers.set(header); // Add a copy of the headers to the request builder r = r.headers(headers.clone()); // then make sure they were added correctly assert_eq!(r.headers, headers); } #[test] fn add_body() { let client = Client::new().unwrap(); let some_url = "https://google.com/"; let mut r = client.post(some_url); let body = "Some interesting content"; r = r.body(body); let buf = body::read_to_string(r.body.unwrap().unwrap()).unwrap(); assert_eq!(buf, body); } #[test] fn add_form() { let client = Client::new().unwrap(); let some_url = "https://google.com/"; let mut r = client.post(some_url); let mut form_data = HashMap::new(); form_data.insert("foo", "bar"); r = r.form(&form_data); // Make sure the content type was set assert_eq!(r.headers.get::(), Some(&ContentType::form_url_encoded())); let buf = body::read_to_string(r.body.unwrap().unwrap()).unwrap(); let body_should_be = serde_urlencoded::to_string(&form_data).unwrap(); assert_eq!(buf, body_should_be); } #[test] fn add_json() { let client = Client::new().unwrap(); let some_url = "https://google.com/"; let mut r = client.post(some_url); let mut json_data = HashMap::new(); json_data.insert("foo", "bar"); r = r.json(&json_data); // Make sure the content type was set assert_eq!(r.headers.get::(), Some(&ContentType::json())); let buf = body::read_to_string(r.body.unwrap().unwrap()).unwrap(); let body_should_be = serde_json::to_string(&json_data).unwrap(); assert_eq!(buf, body_should_be); } }