yukikaze/client/
mod.rs

1//!Client module
2//!
3//!Entry point to HTTP client side.
4//!
5//!## API highlights
6//!
7//!- [Client](struct.Client.html) - Wraps `hyper::Client` and provides various async methods to send requests
8//!- [Request](request/struct.Request.html) - Entry point to creating requests.
9//!- [Response](response/struct.Response.html) - Result of successful requests. Provides various async methods to read body.
10//!
11//!## Usage
12//!
13//!### Simple
14//!
15//!```rust, no_run
16//!use yukikaze::{matsu, client};
17//!
18//!async fn example() {
19//!    let client = client::Client::default();
20//!
21//!    let req = client::Request::get("https://google.com").expect("To create request").empty();
22//!    let mut result = matsu!(client.send(req)).expect("Not timedout").expect("Successful");
23//!    assert!(result.is_success());
24//!
25//!    let html = matsu!(result.text()).expect("To read HTML");
26//!    println!("Google page:\n{}", html);
27//!}
28//!```
29//!
30//!### Custom configuration
31//!
32//!```rust, no_run
33//!use yukikaze::{matsu, client};
34//!
35//!use core::time;
36//!
37//!pub struct TimeoutCfg;
38//!
39//!impl client::config::Config for TimeoutCfg {
40//!    type Connector = client::config::DefaultConnector;
41//!    type Timer = client::config::DefaultTimer;
42//!
43//!    fn timeout() -> time::Duration {
44//!        //never times out
45//!        time::Duration::from_secs(0)
46//!    }
47//!}
48//!
49//!async fn example() {
50//!    let client = client::Client::<TimeoutCfg>::new();
51//!
52//!    let req = client::Request::get("https://google.com").expect("To create request").empty();
53//!    let result = matsu!(client.send(req)).expect("Not timedout").expect("Successful");
54//!    assert!(result.is_success());
55//!}
56//!```
57
58use core::marker::PhantomData;
59use core::future::Future;
60use core::fmt;
61use std::path::Path;
62
63use crate::header;
64
65pub mod config;
66pub mod request;
67pub mod response;
68
69pub use request::Request;
70pub use response::Response;
71
72///HTTP Client
73pub struct Client<C=config::DefaultCfg> where C: config::Config + 'static {
74    inner: hyper::Client<C::Connector>,
75    _config: PhantomData<C>
76}
77
78impl Default for Client {
79    ///Creates Client with default configuration.
80    fn default() -> Self {
81        Client::<config::DefaultCfg>::new()
82    }
83}
84
85impl<C: config::Config> fmt::Debug for Client<C> {
86    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
87        write!(f, "Yukikaze {{ HyperClient={:?} }}", self.inner)
88    }
89}
90
91///Alias to result of sending request.
92pub type RequestResult = Result<response::Response, hyper::Error>;
93
94use tokio::io::{AsyncRead, AsyncWrite};
95
96impl<C: config::Config> Client<C> where <C::Connector as hyper::service::Service<hyper::Uri>>::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
97                                        <C::Connector as hyper::service::Service<hyper::Uri>>::Future: Send + Unpin,
98                                        <C::Connector as hyper::service::Service<hyper::Uri>>::Response: AsyncRead + AsyncWrite + hyper::client::connect::Connection + Unpin + Send
99{
100    ///Creates new instance of client with specified configuration.
101    ///
102    ///Use `Default` if you'd like to use [default](config/struct.DefaultCfg.html) config.
103    pub fn new() -> Client<C> {
104        let inner = C::config_hyper(&mut hyper::Client::builder()).build(C::Connector::default());
105
106        Self {
107            inner,
108            _config: PhantomData
109        }
110    }
111
112    fn apply_headers(request: &mut request::Request) {
113        C::default_headers(request);
114
115        #[cfg(feature = "compu")]
116        {
117            const DEFAULT_COMPRESS: &'static str = "br, gzip, deflate";
118
119            if C::decompress() {
120                let headers = request.headers_mut();
121                if !headers.contains_key(header::ACCEPT_ENCODING) && headers.contains_key(header::RANGE) {
122                    headers.insert(header::ACCEPT_ENCODING, header::HeaderValue::from_static(DEFAULT_COMPRESS));
123                }
124            }
125        }
126    }
127
128    ///Sends request, and returns response
129    pub async fn request(&self, mut req: request::Request) -> RequestResult {
130        Self::apply_headers(&mut req);
131
132        #[cfg(feature = "carry_extensions")]
133        let mut extensions = req.extract_extensions();
134
135        let ongoing = self.inner.request(req.into());
136        let ongoing = matsu!(ongoing).map(|res| response::Response::new(res));
137
138        #[cfg(feature = "carry_extensions")]
139        {
140            ongoing.map(move |resp| resp.replace_extensions(&mut extensions))
141        }
142        #[cfg(not(feature = "carry_extensions"))]
143        {
144            ongoing
145        }
146    }
147
148    ///Sends request and returns response. Timed version.
149    ///
150    ///On timeout error it returns `async_timer::Expired` as `Error`
151    ///`Expired` implements `Future` that can be used to re-spawn ongoing request again.
152    ///
153    ///If request resolves in time returns `Result<response::Response, hyper::Error>` as `Ok`
154    ///variant.
155    pub async fn send(&self, mut req: request::Request) -> Result<RequestResult, async_timer::Expired<impl Future<Output=RequestResult>, C::Timer>> {
156        Self::apply_headers(&mut req);
157
158        #[cfg(feature = "carry_extensions")]
159        let mut extensions = req.extract_extensions();
160
161        let ongoing = self.inner.request(req.into());
162        let ongoing = async {
163            let res = matsu!(ongoing);
164            res.map(|resp| response::Response::new(resp))
165        };
166
167        let timeout = C::timeout();
168        match timeout.as_secs() == 0 && timeout.subsec_nanos() == 0 {
169            #[cfg(not(feature = "carry_extensions"))]
170            true => Ok(matsu!(ongoing)),
171            #[cfg(feature = "carry_extensions")]
172            true => Ok(matsu!(ongoing).map(move |resp| resp.replace_extensions(&mut extensions))),
173            false => {
174                let job = unsafe { async_timer::Timed::<_, C::Timer>::new_unchecked(ongoing, timeout) };
175                #[cfg(not(feature = "carry_extensions"))]
176                {
177                    matsu!(job)
178                }
179                #[cfg(feature = "carry_extensions")]
180                {
181                    matsu!(job).map(move |res| res.map(move |resp| resp.replace_extensions(&mut extensions)))
182                }
183            }
184        }
185    }
186
187    ///Sends request and returns response, while handling redirects. Timed version.
188    ///
189    ///On timeout error it returns `async_timer::Expired` as `Error`
190    ///`Expired` implements `Future` that can be used to re-spawn ongoing request again.
191    ///
192    ///If request resolves in time returns `Result<response::Response, hyper::Error>` as `Ok`
193    ///variant.
194    pub async fn send_redirect(&'static self, req: request::Request) -> Result<RequestResult, async_timer::Expired<impl Future<Output=RequestResult> + 'static, C::Timer>> {
195        let timeout = C::timeout();
196        match timeout.as_secs() == 0 && timeout.subsec_nanos() == 0 {
197            true => Ok(matsu!(self.redirect_request(req))),
198            false => {
199                //Note on unsafety.
200                //Here we assume that all references to self, as it is being 'static will be safe
201                //within ongoing request regardless of when user will restart expired request.
202                //But technically, even though it is static, user still should be able to move it
203                //around so it is a bit unsafe in some edgy cases.
204                let ongoing = self.redirect_request(req);
205                let job = unsafe { async_timer::Timed::<_, C::Timer>::new_unchecked(ongoing, timeout) };
206                matsu!(job)
207            }
208        }
209    }
210
211    ///Sends request and returns response, while handling redirects.
212    pub async fn redirect_request(&self, mut req: request::Request) -> RequestResult {
213        use http::{Method, StatusCode};
214
215        Self::apply_headers(&mut req);
216
217        let mut rem_redirect = C::max_redirect_num();
218
219        let mut method = req.parts.method.clone();
220        let uri = req.parts.uri.clone();
221        let mut headers = req.parts.headers.clone();
222        let mut body = req.body.clone();
223        #[cfg(feature = "carry_extensions")]
224        let mut extensions = req.extract_extensions();
225
226        loop {
227            let ongoing = self.inner.request(req.into());
228            let res = matsu!(ongoing).map(|resp| response::Response::new(resp))?;
229
230            match res.status() {
231                StatusCode::SEE_OTHER => {
232                    rem_redirect -= 1;
233                    match rem_redirect {
234                        #[cfg(feature = "carry_extensions")]
235                        0 => return Ok(res.replace_extensions(&mut extensions)),
236                        #[cfg(not(feature = "carry_extensions"))]
237                        0 => return Ok(res),
238                        _ => {
239                            //All requests should be changed to GET with no body.
240                            //In most cases it is result of successful POST.
241                            body = None;
242                            method = Method::GET;
243                        }
244                    }
245                },
246                StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND | StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT => {
247                    rem_redirect -= 1;
248                    match rem_redirect {
249                        #[cfg(feature = "carry_extensions")]
250                        0 => return Ok(res.replace_extensions(&mut extensions)),
251                        #[cfg(not(feature = "carry_extensions"))]
252                        0 => return Ok(res),
253                        _ => (),
254                    }
255                }
256                #[cfg(feature = "carry_extensions")]
257                _ => return Ok(res.replace_extensions(&mut extensions)),
258                #[cfg(not(feature = "carry_extensions"))]
259                _ => return Ok(res),
260            }
261
262            let location = match res.headers().get(header::LOCATION).and_then(|loc| loc.to_str().ok()).and_then(|loc| loc.parse::<hyper::Uri>().ok()) {
263                Some(loc) => match loc.scheme().is_some() {
264                    //We assume that if scheme is present then it is absolute redirect
265                    true => {
266                        //Well, it is unlikely that host would be empty, but just in case, right?
267                        if let Some(prev_host) = uri.authority().map(|part| part.host()) {
268                            match loc.authority().map(|part| part.host() == prev_host).unwrap_or(false) {
269                                true => (),
270                                false => {
271                                    headers.remove("authorization");
272                                    headers.remove("cookie");
273                                    headers.remove("cookie2");
274                                    headers.remove("www-authenticate");
275                                }
276                            }
277                        }
278
279                        loc
280                    },
281                    //Otherwise it is relative to current location.
282                    false => {
283                        let current = Path::new(uri.path());
284                        let loc = Path::new(loc.path());
285                        let loc = current.join(loc);
286                        let loc = loc.to_str().expect("Valid UTF-8 path").parse::<hyper::Uri>().expect("Valid URI");
287                        let mut loc_parts = loc.into_parts();
288
289                        loc_parts.scheme = uri.scheme().cloned();
290                        loc_parts.authority = uri.authority().cloned();
291
292                        hyper::Uri::from_parts(loc_parts).expect("Create redirect URI")
293                    },
294                },
295                #[cfg(feature = "carry_extensions")]
296                None => return Ok(res.replace_extensions(&mut extensions)),
297                #[cfg(not(feature = "carry_extensions"))]
298                None => return Ok(res),
299            };
300
301            let (mut parts, _) = hyper::Request::<()>::new(()).into_parts();
302            parts.method = method.clone();
303            parts.uri = location;
304            parts.headers = headers.clone();
305
306            req = request::Request {
307                parts,
308                body: body.clone()
309            };
310        }
311    }
312}