lynx/
request.rs

1use std::convert::From;
2use std::fmt::Display;
3use std::io::{prelude::*, BufWriter};
4use std::str;
5
6#[cfg(feature = "compress")]
7use http::header::ACCEPT_ENCODING;
8use http::{
9    header::{HeaderValue, IntoHeaderName, CONNECTION, CONTENT_LENGTH, HOST},
10    status::StatusCode,
11    HeaderMap, HttpTryFrom, Method, Version,
12};
13use url::Url;
14
15#[cfg(feature = "charsets")]
16use crate::charsets::Charset;
17use crate::error::{HttpError, HttpResult};
18use crate::parsing::{parse_response, ResponseReader};
19use crate::streams::BaseStream;
20
21pub trait HttpTryInto<T> {
22    fn try_into(self) -> Result<T, http::Error>;
23}
24
25impl<T, U> HttpTryInto<U> for T
26where
27    U: HttpTryFrom<T>,
28    http::Error: From<<U as http::HttpTryFrom<T>>::Error>,
29{
30    fn try_into(self) -> Result<U, http::Error> {
31        let val = U::try_from(self)?;
32        Ok(val)
33    }
34}
35
36fn header_insert<H, V>(headers: &mut HeaderMap, header: H, value: V) -> HttpResult
37where
38    H: IntoHeaderName,
39    V: HttpTryInto<HeaderValue>,
40{
41    let value = value.try_into()?;
42    headers.insert(header, value);
43    Ok(())
44}
45
46fn header_append<H, V>(headers: &mut HeaderMap, header: H, value: V) -> HttpResult
47where
48    H: IntoHeaderName,
49    V: HttpTryInto<HeaderValue>,
50{
51    let value = value.try_into()?;
52    headers.append(header, value);
53    Ok(())
54}
55
56/// `Request` is the main way of performing requests.
57///
58/// Use one of its contructors to create a request and then use the `send` method
59/// to send the `Request` and get the status, headers and response.
60pub struct Request {
61    url: Url,
62    method: Method,
63    headers: HeaderMap,
64    body: Vec<u8>,
65    follow_redirects: bool,
66    #[cfg(feature = "charsets")]
67    pub(crate) default_charset: Option<Charset>,
68    #[cfg(feature = "compress")]
69    allow_compression: bool,
70}
71
72impl Request {
73    /// Create a new `Request` with the base URL and the given method.
74    pub fn new(base_url: &str, method: Method) -> Request {
75        let url = Url::parse(base_url).expect("invalid url");
76
77        match method {
78            Method::CONNECT => panic!("CONNECT is not yet supported"),
79            _ => {}
80        }
81
82        Request {
83            url,
84            method: method,
85            headers: HeaderMap::new(),
86            body: Vec::new(),
87            follow_redirects: true,
88            #[cfg(feature = "charsets")]
89            default_charset: None,
90            #[cfg(feature = "compress")]
91            allow_compression: true,
92        }
93    }
94
95    /// Create a new `Request` with the GET method.
96    pub fn get(base_url: &str) -> Request {
97        Request::new(base_url, Method::GET)
98    }
99
100    /// Create a new `Request` with the POST method.
101    pub fn post(base_url: &str) -> Request {
102        Request::new(base_url, Method::POST)
103    }
104
105    /// Create a new `Request` with the PUT method.
106    pub fn put(base_url: &str) -> Request {
107        Request::new(base_url, Method::PUT)
108    }
109
110    /// Create a new `Request` with the DELETE method.
111    pub fn delete(base_url: &str) -> Request {
112        Request::new(base_url, Method::DELETE)
113    }
114
115    /// Create a new `Request` with the HEAD method.
116    pub fn head(base_url: &str) -> Request {
117        Request::new(base_url, Method::HEAD)
118    }
119
120    /// Create a new `Request` with the OPTIONS method.
121    pub fn options(base_url: &str) -> Request {
122        Request::new(base_url, Method::OPTIONS)
123    }
124
125    /// Create a new `Request` with the PATCH method.
126    pub fn patch(base_url: &str) -> Request {
127        Request::new(base_url, Method::PATCH)
128    }
129
130    /// Create a new `Request` with the TRACE method.
131    pub fn trace(base_url: &str) -> Request {
132        Request::new(base_url, Method::TRACE)
133    }
134
135    /// Associate a query string parameter to the given value.
136    ///
137    /// The same key can be used multiple times.
138    pub fn param<V>(&mut self, key: &str, value: V)
139    where
140        V: Display,
141    {
142        self.url.query_pairs_mut().append_pair(key, &format!("{}", value));
143    }
144
145    /// Modify a header for this `Request`.
146    ///
147    /// If the header is already present, the value will be replaced. If you wish to append a new header,
148    /// use `header_append`.
149    pub fn header<H, V>(&mut self, header: H, value: V) -> HttpResult
150    where
151        H: IntoHeaderName,
152        V: HttpTryInto<HeaderValue>,
153    {
154        header_insert(&mut self.headers, header, value)
155    }
156
157    /// Append a new header to this `Request`.
158    ///
159    /// The new header is always appended to the `Request`, even if the header already exists.
160    pub fn header_append<H, V>(&mut self, header: H, value: V) -> HttpResult
161    where
162        H: IntoHeaderName,
163        V: HttpTryInto<HeaderValue>,
164    {
165        header_append(&mut self.headers, header, value)
166    }
167
168    /// Set the body of this request.
169    ///
170    /// The can be a `&[u8]` or a `str`, anything that's a sequence of bytes.
171    pub fn body(&mut self, body: impl AsRef<[u8]>) {
172        self.body = body.as_ref().to_owned();
173    }
174
175    /// Sets if this `Request` should follow redirects, 3xx codes.
176    ///
177    /// This value defaults to true.
178    pub fn follow_redirects(&mut self, follow_redirects: bool) {
179        self.follow_redirects = follow_redirects;
180    }
181
182    /// Set the default charset to use while parsing the response of this `Request`.
183    ///
184    /// If the response does not say which charset it uses, this charset will be used to decode the request.
185    /// This value defaults to `None`, in which case ISO-8859-1 is used.
186    #[cfg(feature = "charsets")]
187    pub fn default_charset(&mut self, default_charset: Option<Charset>) {
188        self.default_charset = default_charset;
189    }
190
191    /// Sets if this `Request` will announce that it accepts compression.
192    ///
193    /// This value defaults to true. Note that this only lets the browser know that this `Request` supports
194    /// compression, the server might choose not to compress the content.
195    #[cfg(feature = "compress")]
196    pub fn allow_compression(&mut self, allow_compression: bool) {
197        self.allow_compression = allow_compression;
198    }
199
200    fn base_redirect_url(&self, location: &str, previous_url: &Url) -> HttpResult<Url> {
201        Ok(match Url::parse(location) {
202            Ok(url) => url,
203            Err(url::ParseError::RelativeUrlWithoutBase) => previous_url
204                .join(location)
205                .map_err(|_| HttpError::InvalidUrl("cannot join location with new url"))?,
206            Err(_) => Err(HttpError::InvalidUrl("invalid redirection url"))?,
207        })
208    }
209
210    /// Send this `Request` to the server.
211    ///
212    /// This method consumes the object so that it cannot be used after sending the request.
213    pub fn send(mut self) -> HttpResult<(StatusCode, HeaderMap, ResponseReader)> {
214        let mut url = self.url.clone();
215        loop {
216            let mut stream = BaseStream::connect(&url)?;
217            self.write_request(&mut stream, &url)?;
218            let (status, headers, resp) = parse_response(stream, &self)?;
219
220            debug!("status code {}", status.as_u16());
221
222            if !self.follow_redirects || !status.is_redirection() {
223                return Ok((status, headers, resp));
224            }
225
226            // Handle redirect
227            let location = headers
228                .get(http::header::LOCATION)
229                .ok_or(HttpError::InvalidResponse("redirect has no location header"))?;
230            let location = location
231                .to_str()
232                .map_err(|_| HttpError::InvalidResponse("location to str error"))?;
233
234            let new_url = self.base_redirect_url(location, &url)?;
235            url = new_url;
236
237            debug!("redirected to {} giving url {}", location, url,);
238        }
239    }
240
241    fn write_request<W>(&mut self, writer: W, url: &Url) -> HttpResult
242    where
243        W: Write,
244    {
245        let mut writer = BufWriter::new(writer);
246        let version = Version::HTTP_11;
247        let has_body = !self.body.is_empty() && self.method != Method::TRACE;
248
249        if let Some(query) = url.query() {
250            debug!("{} {}?{} {:?}", self.method.as_str(), url.path(), query, version,);
251
252            write!(
253                writer,
254                "{} {}?{} {:?}\r\n",
255                self.method.as_str(),
256                url.path(),
257                query,
258                version,
259            )?;
260        } else {
261            debug!("{} {} {:?}", self.method.as_str(), url.path(), version);
262
263            write!(writer, "{} {} {:?}\r\n", self.method.as_str(), url.path(), version,)?;
264        }
265
266        header_insert(&mut self.headers, CONNECTION, "close")?;
267
268        let host = url.host_str().ok_or(HttpError::InvalidUrl("url has no host"))?;
269        if let Some(port) = url.port() {
270            header_insert(&mut self.headers, HOST, format!("{}:{}", host, port))?;
271        } else {
272            header_insert(&mut self.headers, HOST, host)?;
273        }
274
275        if has_body {
276            header_insert(&mut self.headers, CONTENT_LENGTH, format!("{}", self.body.len()))?;
277        }
278
279        self.compression_header()?;
280
281        self.write_headers(&mut writer)?;
282
283        if has_body {
284            debug!("writing out body of length {}", self.body.len());
285            writer.write_all(&self.body)?;
286        }
287
288        writer.flush()?;
289
290        Ok(())
291    }
292
293    fn write_headers<W>(&self, writer: &mut W) -> HttpResult
294    where
295        W: Write,
296    {
297        for (key, value) in self.headers.iter() {
298            write!(writer, "{}: ", key.as_str())?;
299            writer.write_all(value.as_bytes())?;
300            write!(writer, "\r\n")?;
301        }
302        write!(writer, "\r\n")?;
303        Ok(())
304    }
305
306    #[cfg(feature = "compress")]
307    fn compression_header(&mut self) -> HttpResult {
308        if self.allow_compression {
309            header_insert(&mut self.headers, ACCEPT_ENCODING, "gzip, deflate")?;
310        }
311        Ok(())
312    }
313
314    #[cfg(not(feature = "compress"))]
315    fn compression_header(&mut self) -> HttpResult {
316        Ok(())
317    }
318}