diff --git a/Cargo.toml b/Cargo.toml index 3ea828c..4239d8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,14 +2,27 @@ name = "reqwest" version = "0.0.0" description = "higher level HTTP client library" +keywords = ["http", "request", "client"] repository = "https://github.com/seanmonstar/reqwest" documentation = "https://docs.rs/reqwest" authors = ["Sean McArthur "] license = "MIT/Apache-2.0" [dependencies] -hyper = { version = "0.9" }#, default-features = false } +hyper = { version = "0.9" , default-features = false } +serde = "0.8" +serde_json = "0.8" log = "0.3" +[dependencies.native-tls] +git = "https://github.com/sfackler/rust-native-tls" +optional = true + +[features] +default = ["openssl"] +openssl = ["hyper/ssl"] +tls = ["native-tls"] + [dev-dependencies] env_logger = "0.3" +serde_derive = "0.8" diff --git a/README.md b/README.md index 8967ca4..ee22cbd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# request +# reqwest An ergonomic HTTP Client for Rust diff --git a/examples/post.rs b/examples/post.rs new file mode 100644 index 0000000..7c6379c --- /dev/null +++ b/examples/post.rs @@ -0,0 +1,40 @@ +//#![feature(proc_macro)] + +extern crate reqwest; +extern crate env_logger; +//#[macro_use] extern crate serde_derive; + +/* +#[derive(Serialize)] +struct Thingy { + a: i32, + b: bool, + c: String, +} +*/ + +fn main() { + env_logger::init().unwrap(); + + println!("POST https://httpbin.org/post"); + + /* + let thingy = Thingy { + a: 5, + b: true, + c: String::from("reqwest") + }; + */ + + let client = reqwest::Client::new(); + let mut res = client.post("https://httpbin.org/post") + .body("foo=bar") + .send().unwrap(); + + println!("Status: {}", res.status()); + println!("Headers:\n{}", res.headers()); + + ::std::io::copy(&mut res, &mut ::std::io::stdout()).unwrap(); + + println!("\n\nDone."); +} diff --git a/examples/simple.rs b/examples/simple.rs index 5ad1601..b891524 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -6,7 +6,7 @@ fn main() { println!("GET https://www.rust-lang.org"); - let mut res = reqwest::get("http://www.rust-lang.org").unwrap(); + let mut res = reqwest::get("https://www.rust-lang.org").unwrap(); println!("Status: {}", res.status()); println!("Headers:\n{}", res.headers()); diff --git a/src/body.rs b/src/body.rs index 93255a9..e5884ed 100644 --- a/src/body.rs +++ b/src/body.rs @@ -1,8 +1,17 @@ use std::io::Read; -pub struct Body(Kind); +pub struct Body { + reader: Kind, +} impl Body { + pub fn new(reader: R) -> Body { + Body { + reader: Kind::Reader(Box::new(reader), None), + } + } + + /* pub fn sized(reader: (), len: u64) -> Body { unimplemented!() } @@ -10,17 +19,20 @@ impl Body { pub fn chunked(reader: ()) -> Body { unimplemented!() } + */ } enum Kind { - Length, - Chunked + Reader(Box, Option), + Bytes(Vec), } impl From> for Body { #[inline] fn from(v: Vec) -> Body { - unimplemented!() + Body { + reader: Kind::Bytes(v), + } } } @@ -31,7 +43,34 @@ impl From for Body { } } -/// Wraps a `std::io::Write`. -pub struct Pipe(Kind); + +impl<'a> From<&'a [u8]> for Body { + #[inline] + fn from(s: &'a [u8]) -> Body { + s.to_vec().into() + } +} + +impl<'a> From<&'a str> for Body { + #[inline] + fn from(s: &'a str) -> Body { + s.as_bytes().into() + } +} + +// Wraps a `std::io::Write`. +//pub struct Pipe(Kind); + + +pub fn as_hyper_body<'a>(body: &'a Body) -> ::hyper::client::Body<'a> { + match body.reader { + Kind::Bytes(ref bytes) => { + let len = bytes.len(); + ::hyper::client::Body::BufBody(bytes, len) + }, + Kind::Reader(..) => unimplemented!() + } +} + diff --git a/src/client.rs b/src/client.rs index 999cd7d..2e36612 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,35 +1,58 @@ use std::io::{self, Read}; -use hyper::header::Headers; +use hyper::header::{Headers, ContentType, UserAgent}; use hyper::method::Method; use hyper::status::StatusCode; use hyper::version::HttpVersion; use hyper::{Url}; -use ::body::Body; +use serde::Serialize; +use serde_json; +use ::body::{self, Body}; + +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. pub struct Client { inner: ::hyper::Client, } impl Client { + /// Constructs a new `Client`. pub fn new() -> Client { Client { - inner: ::hyper::Client::new(), + inner: new_hyper_client() } } + /// Convenience method to make a `GET` request to a URL. pub fn get(&self, url: &str) -> RequestBuilder { self.request(Method::Get, Url::parse(url).unwrap()) } + /// Convenience method to make a `POST` request to a URL. + pub fn post(&self, url: &str) -> RequestBuilder { + self.request(Method::Post, Url::parse(url).unwrap()) + } + + /// 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: Url) -> RequestBuilder { debug!("request {:?} \"{}\"", method, url); RequestBuilder { client: self, method: method, url: url, - version: HttpVersion::Http11, + _version: HttpVersion::Http11, headers: Headers::new(), body: None, @@ -37,39 +60,75 @@ impl Client { } } +#[cfg(not(feature = "tls"))] +fn new_hyper_client() -> ::hyper::Client { + ::hyper::Client::new() +} + +#[cfg(feature = "tls")] +fn new_hyper_client() -> ::hyper::Client { + use tls::TlsClient; + ::hyper::Client::with_connector( + ::hyper::client::Pool::with_connector( + Default::default(), + ::hyper::net::HttpsConnector::new(TlsClient::new().unwrap()) + ) + ) +} + + +/// A builder to construct the properties of a `Request`. pub struct RequestBuilder<'a> { client: &'a Client, method: Method, url: Url, - version: HttpVersion, + _version: HttpVersion, headers: Headers, body: Option, } impl<'a> RequestBuilder<'a> { - + /// Add a `Header` to this Request. pub fn header(mut self, header: H) -> RequestBuilder<'a> { 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<'a> { self.headers.extend(headers.iter()); self } + /// Set the request body. pub fn body>(mut self, body: T) -> RequestBuilder<'a> { self.body = Some(body.into()); self } + pub fn json(mut self, json: T) -> RequestBuilder<'a> { + let body = serde_json::to_vec(&json).expect("serde to_vec cannot fail"); + self.headers.set(ContentType::json()); + self.body = Some(body.into()); + self + } + + /// Constructs the Request and sends it the target URL, returning a Response. pub fn send(mut self) -> ::Result { - let req = self.client.inner.request(self.method, self.url) + if !self.headers.has::() { + self.headers.set(UserAgent(DEFAULT_USER_AGENT.to_owned())); + } + + let mut req = self.client.inner.request(self.method, self.url) .headers(self.headers); - //TODO: body + if let Some(ref b) = self.body { + let body = body::as_hyper_body(b); + req = req.body(body); + } let res = try!(req.send()); Ok(Response { @@ -78,24 +137,29 @@ impl<'a> RequestBuilder<'a> { } } +/// A Response to a submitted `Request`. pub struct Response { inner: ::hyper::client::Response, } impl Response { + /// Get the `StatusCode`. pub fn status(&self) -> &StatusCode { &self.inner.status } + /// Get the `Headers`. pub fn headers(&self) -> &Headers { &self.inner.headers } + /// Get the `HttpVersion`. pub fn version(&self) -> &HttpVersion { &self.inner.version } } +/// Read the body of the Response. impl Read for Response { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.inner.read(buf) diff --git a/src/error.rs b/src/error.rs index 1463612..6bb8e3b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,7 @@ +/// The Errors that may occur when processing a `Request`. #[derive(Debug)] pub enum Error { + /// An HTTP error from the `hyper` crate. Http(::hyper::Error), #[doc(hidden)] __DontMatchMe, @@ -11,4 +13,5 @@ impl From<::hyper::Error> for Error { } } +/// A `Result` alias where the `Err` case is `reqwest::Error`. pub type Result = ::std::result::Result; diff --git a/src/lib.rs b/src/lib.rs index 47333f0..7559eed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,37 @@ -#![allow(warnings)] - +#![deny(warnings)] +#![deny(missing_docs)] +//! # reqwest +//! +//! The `reqwest` crate provides a convenient, higher-level HTTP Client. +//! +//! It handles many of the things that most people just expect an HTTP client +//! to do for them. +//! +//! - Uses system-native TLS +//! - Plain bodies, JSON, urlencoded, multipart +//! - Customizable redirect policy +//! - Cookies +//! +//! The `reqwest::Client` is synchronous, making it a great fit for +//! applications that only require a few HTTP requests, and wish to handle +//! them synchronously. When [hyper][] releases with asynchronous support, +//! `reqwest` will be updated to use it internally, but still provide a +//! synchronous Client, for convenience. A `reqwest::async::Client` will also +//! be added. +//! +//! ## Making a GET request +//! +//! ```no_run +//! let resp = reqwest::get("https://www.rust-lang.org").unwrap(); +//! assert!(resp.status().is_success()); +//! ``` extern crate hyper; #[macro_use] extern crate log; +#[cfg(feature = "tls")] extern crate native_tls; +extern crate serde; +extern crate serde_json; pub use hyper::header; pub use hyper::method::Method; @@ -18,6 +46,9 @@ mod body; mod client; mod error; +#[cfg(feature = "tls")] mod tls; + +/// Shortcut method to quickly make a `GET` request. pub fn get(url: &str) -> ::Result { let client = Client::new(); client.get(url).send() diff --git a/src/redirect.rs b/src/redirect.rs new file mode 100644 index 0000000..960e602 --- /dev/null +++ b/src/redirect.rs @@ -0,0 +1,7 @@ +pub struct RedirectPolicy { + inner: () +} + +impl RedirectPolicy { + +} diff --git a/src/tls.rs b/src/tls.rs new file mode 100644 index 0000000..4c11799 --- /dev/null +++ b/src/tls.rs @@ -0,0 +1,72 @@ +use std::io::{self, Read, Write}; +use std::net::SocketAddr; +use std::time::Duration; + +use hyper::net::{SslClient, HttpStream, NetworkStream}; +use native_tls::{ClientBuilder, TlsStream as NativeTlsStream, HandshakeError}; + +pub struct TlsClient(ClientBuilder); + +impl TlsClient { + pub fn new() -> ::Result { + ClientBuilder::new() + .map(TlsClient) + .map_err(|e| ::Error::Http(::hyper::Error::Ssl(Box::new(e)))) + } +} + +impl SslClient for TlsClient { + type Stream = TlsStream; + + fn wrap_client(&self, stream: HttpStream, host: &str) -> ::hyper::Result { + self.0.handshake(host, stream).map(TlsStream).map_err(|e| { + match e { + HandshakeError::Failure(e) => ::hyper::Error::Ssl(Box::new(e)), + HandshakeError::Interrupted(..) => { + // while using hyper 0.9, this won't happen, because the + // socket is in blocking mode. once we move to hyper 0.10, + // much of this `tls` module will go away anyways + unreachable!("TlsClient::handshake Interrupted") + } + } + }) + } +} + +pub struct TlsStream(NativeTlsStream); + +impl Read for TlsStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.0.read(buf) + } +} + +impl Write for TlsStream { + fn write(&mut self, data: &[u8]) -> io::Result { + self.0.write(data) + } + + fn flush(&mut self) -> io::Result<()> { + self.0.flush() + } +} + +impl Clone for TlsStream { + fn clone(&self) -> TlsStream { + unreachable!("TlsStream::clone is never used for the Client") + } +} + +impl NetworkStream for TlsStream { + fn peer_addr(&mut self) -> io::Result { + self.0.get_mut().peer_addr() + } + + fn set_read_timeout(&self, dur: Option) -> io::Result<()> { + self.0.get_ref().set_read_timeout(dur) + } + + fn set_write_timeout(&self, dur: Option) -> io::Result<()> { + self.0.get_ref().set_write_timeout(dur) + } +}