1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
//!Client module
//!
//!Entry point to HTTP client side.
//!
//!## API highlights
//!
//!- [Client](struct.Client.html) - Wraps `hyper::Client` and provides various async methods to send requests
//!- [Request](request/struct.Request.html) - Entry point to creating requests.
//!- [Response](response/struct.Response.html) - Result of successful requests. Provides various async methods to read body.
//!
//!## Usage
//!
//!### Simple
//!
//!```rust, no_run
//!#![feature(async_await)]
//!
//!use yukikaze::{matsu, client};
//!
//!async fn example() {
//!    let client = client::Client::default();
//!
//!    let req = client::Request::get("https://google.com").expect("To create request").empty();
//!    let mut result = matsu!(client.send(req)).expect("Not timedout").expect("Successful");
//!    assert!(result.is_success());
//!
//!    let html = matsu!(result.text()).expect("To read HTML");
//!    println!("Google page:\n{}", html);
//!}
//!```
//!
//!### Custom configuration
//!
//!```rust, no_run
//!#![feature(async_await)]
//!use yukikaze::{matsu, client};
//!
//!use core::time;
//!
//!pub struct TimeoutCfg;
//!
//!impl client::config::Config for TimeoutCfg {
//!    type Connector = client::config::DefaultConnector;
//!    type Timer = client::config::DefaultTimer;
//!
//!    fn timeout() -> time::Duration {
//!        //never times out
//!        time::Duration::from_secs(0)
//!    }
//!}
//!
//!async fn example() {
//!    let client = client::Client::<TimeoutCfg>::new();
//!
//!    let req = client::Request::get("https://google.com").expect("To create request").empty();
//!    let result = matsu!(client.send(req)).expect("Not timedout").expect("Successful");
//!    assert!(result.is_success());
//!}
//!```

use core::marker::PhantomData;
use core::future::Future;
use core::fmt;
use std::path::Path;

use crate::header;

pub mod config;
pub mod request;
pub mod response;

pub use request::Request;
pub use response::Response;

///HTTP Client
pub struct Client<C=config::DefaultCfg> where C: config::Config + 'static {
    inner: hyper::Client<C::Connector>,
    _config: PhantomData<C>
}

impl Default for Client {
    ///Creates Client with default configuration.
    fn default() -> Self {
        Client::<config::DefaultCfg>::new()
    }
}

impl<C: config::Config> fmt::Debug for Client<C> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Yukikaze {{ HyperClient={:?} }}", self.inner)
    }
}

///Alias to result of sending request.
pub type RequestResult = Result<response::Response, hyper::Error>;

impl<C: config::Config> Client<C> {
    ///Creates new instance of client with specified configuration.
    ///
    ///Use `Default` if you'd like to use [default](config/struct.DefaultCfg.html) config.
    pub fn new() -> Client<C> {
        use crate::connector::Connector;
        let inner = C::config_hyper(&mut hyper::Client::builder()).executor(config::DefaultExecutor).build(C::Connector::new());

        Self {
            inner,
            _config: PhantomData
        }
    }

    fn apply_headers(request: &mut request::Request) {
        C::default_headers(request);

        #[cfg(feature = "compu")]
        {
            const DEFAULT_COMPRESS: &'static str = "br, gzip, deflate";

            if C::decompress() {
                let headers = request.headers_mut();
                if !headers.contains_key(header::ACCEPT_ENCODING) && headers.contains_key(header::RANGE) {
                    headers.insert(header::ACCEPT_ENCODING, header::HeaderValue::from_static(DEFAULT_COMPRESS));
                }
            }
        }
    }

    ///Sends request, and returns response
    pub async fn request(&self, mut req: request::Request) -> RequestResult {
        Self::apply_headers(&mut req);

        #[cfg(feature = "carry_extensions")]
        let mut extensions = req.extract_extensions();

        let ongoing = self.inner.request(req.into());
        let ongoing = matsu!(ongoing).map(|res| response::Response::new(res));

        #[cfg(feature = "carry_extensions")]
        {
            ongoing.map(move |resp| resp.replace_extensions(&mut extensions))
        }
        #[cfg(not(feature = "carry_extensions"))]
        {
            ongoing
        }
    }

    ///Sends request and returns response. Timed version.
    ///
    ///On timeout error it returns `async_timer::Expired` as `Error`
    ///`Expired` implements `Future` that can be used to re-spawn ongoing request again.
    ///
    ///If request resolves in time returns `Result<response::Response, hyper::Error>` as `Ok`
    ///variant.
    pub async fn send(&self, mut req: request::Request) -> Result<RequestResult, async_timer::Expired<impl Future<Output=RequestResult>, C::Timer>> {
        Self::apply_headers(&mut req);

        #[cfg(feature = "carry_extensions")]
        let mut extensions = req.extract_extensions();

        let ongoing = self.inner.request(req.into());
        let ongoing = async {
            let res = matsu!(ongoing);
            res.map(|resp| response::Response::new(resp))
        };

        let timeout = C::timeout();
        match timeout.as_secs() == 0 && timeout.subsec_nanos() == 0 {
            #[cfg(not(feature = "carry_extensions"))]
            true => Ok(matsu!(ongoing)),
            #[cfg(feature = "carry_extensions")]
            true => Ok(matsu!(ongoing).map(move |resp| resp.replace_extensions(&mut extensions))),
            false => {
                let job = unsafe { async_timer::Timed::<_, C::Timer>::new_unchecked(ongoing, timeout) };
                #[cfg(not(feature = "carry_extensions"))]
                {
                    matsu!(job)
                }
                #[cfg(feature = "carry_extensions")]
                {
                    matsu!(job).map(move |res| res.map(move |resp| resp.replace_extensions(&mut extensions)))
                }
            }
        }
    }

    ///Sends request and returns response, while handling redirects. Timed version.
    ///
    ///On timeout error it returns `async_timer::Expired` as `Error`
    ///`Expired` implements `Future` that can be used to re-spawn ongoing request again.
    ///
    ///If request resolves in time returns `Result<response::Response, hyper::Error>` as `Ok`
    ///variant.
    pub async fn send_redirect(&'static self, req: request::Request) -> Result<RequestResult, async_timer::Expired<impl Future<Output=RequestResult> + 'static, C::Timer>> {
        let timeout = C::timeout();
        match timeout.as_secs() == 0 && timeout.subsec_nanos() == 0 {
            true => Ok(matsu!(self.redirect_request(req))),
            false => {
                //Note on unsafety.
                //Here we assume that all references to self, as it is being 'static will be safe
                //within ongoing request regardless of when user will restart expired request.
                //But technically, even though it is static, user still should be able to move it
                //around so it is a bit unsafe in some edgy cases.
                let ongoing = self.redirect_request(req);
                let job = unsafe { async_timer::Timed::<_, C::Timer>::new_unchecked(ongoing, timeout) };
                matsu!(job)
            }
        }
    }

    ///Sends request and returns response, while handling redirects.
    pub async fn redirect_request(&self, mut req: request::Request) -> RequestResult {
        use http::{Method, StatusCode};

        Self::apply_headers(&mut req);

        let mut rem_redirect = C::max_redirect_num();

        let mut method = req.parts.method.clone();
        let uri = req.parts.uri.clone();
        let mut headers = req.parts.headers.clone();
        let mut body = req.body.clone();
        #[cfg(feature = "carry_extensions")]
        let mut extensions = req.extract_extensions();

        loop {
            let ongoing = self.inner.request(req.into());
            let res = matsu!(ongoing).map(|resp| response::Response::new(resp))?;

            match res.status() {
                StatusCode::SEE_OTHER => {
                    rem_redirect -= 1;
                    match rem_redirect {
                        #[cfg(feature = "carry_extensions")]
                        0 => return Ok(res.replace_extensions(&mut extensions)),
                        #[cfg(not(feature = "carry_extensions"))]
                        0 => return Ok(res),
                        _ => {
                            //All requests should be changed to GET with no body.
                            //In most cases it is result of successful POST.
                            body = None;
                            method = Method::GET;
                        }
                    }
                },
                StatusCode::MOVED_PERMANENTLY | StatusCode::FOUND | StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT => {
                    rem_redirect -= 1;
                    match rem_redirect {
                        #[cfg(feature = "carry_extensions")]
                        0 => return Ok(res.replace_extensions(&mut extensions)),
                        #[cfg(not(feature = "carry_extensions"))]
                        0 => return Ok(res),
                        _ => (),
                    }
                }
                #[cfg(feature = "carry_extensions")]
                _ => return Ok(res.replace_extensions(&mut extensions)),
                #[cfg(not(feature = "carry_extensions"))]
                _ => return Ok(res),
            }

            let location = match res.headers().get(header::LOCATION).and_then(|loc| loc.to_str().ok()).and_then(|loc| loc.parse::<hyper::Uri>().ok()) {
                Some(loc) => match loc.scheme_part().is_some() {
                    //We assume that if scheme is present then it is absolute redirect
                    true => {
                        //Well, it is unlikely that host would be empty, but just in case, right?
                        if let Some(prev_host) = uri.authority_part().map(|part| part.host()) {
                            match loc.authority_part().map(|part| part.host() == prev_host).unwrap_or(false) {
                                true => (),
                                false => {
                                    headers.remove("authorization");
                                    headers.remove("cookie");
                                    headers.remove("cookie2");
                                    headers.remove("www-authenticate");
                                }
                            }
                        }

                        loc
                    },
                    //Otherwise it is relative to current location.
                    false => {
                        let current = Path::new(uri.path());
                        let loc = Path::new(loc.path());
                        let loc = current.join(loc);
                        let loc = loc.to_str().expect("Valid UTF-8 path").parse::<hyper::Uri>().expect("Valid URI");
                        let mut loc_parts = loc.into_parts();

                        loc_parts.scheme = uri.scheme_part().cloned();
                        loc_parts.authority = uri.authority_part().cloned();

                        hyper::Uri::from_parts(loc_parts).expect("Create redirect URI")
                    },
                },
                #[cfg(feature = "carry_extensions")]
                None => return Ok(res.replace_extensions(&mut extensions)),
                #[cfg(not(feature = "carry_extensions"))]
                None => return Ok(res),
            };

            let (mut parts, _) = hyper::Request::<()>::new(()).into_parts();
            parts.method = method.clone();
            parts.uri = location;
            parts.headers = headers.clone();

            req = request::Request {
                parts,
                body: body.clone()
            };
        }
    }
}