Files
ureq/src/response.rs
Martin Algesten ed999b579d Removing AsciiString dep and one unsafe
This library is not about enforcing standards, so the internal use of
AsciiString for headers and status lines is not necessary.
2019-10-20 10:40:30 +02:00

724 lines
22 KiB
Rust

use std::io::{Cursor, Error as IoError, ErrorKind, Read, Result as IoResult};
use std::str::FromStr;
use chunked_transfer::Decoder as ChunkDecoder;
use crate::error::Error;
use crate::header::Header;
use crate::pool::PoolReturnRead;
use crate::stream::Stream;
use crate::unit::Unit;
#[cfg(feature = "json")]
use serde_json;
#[cfg(feature = "charset")]
use encoding::label::encoding_from_whatwg_label;
#[cfg(feature = "charset")]
use encoding::DecoderTrap;
pub const DEFAULT_CONTENT_TYPE: &str = "text/plain";
pub const DEFAULT_CHARACTER_SET: &str = "utf-8";
/// Response instances are created as results of firing off requests.
///
/// The `Response` is used to read response headers and decide what to do with the body.
/// Note that the socket connection is open and the body not read until one of
/// [`into_reader()`](#method.into_reader), [`into_json()`](#method.into_json) or
/// [`into_string()`](#method.into_string) consumes the response.
///
/// ```
/// let response = ureq::get("https://www.google.com").call();
///
/// // socket is still open and the response body has not been read.
///
/// let text = response.into_string().unwrap();
///
/// // response is consumed, and body has been read.
/// ```
pub struct Response {
url: Option<String>,
error: Option<Error>,
status_line: String,
index: (usize, usize), // index into status_line where we split: HTTP/1.1 200 OK
status: u16,
headers: Vec<Header>,
unit: Option<Unit>,
stream: Option<Stream>,
}
impl ::std::fmt::Debug for Response {
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> {
write!(
f,
"Response[status: {}, status_text: {}]",
self.status(),
self.status_text()
)
}
}
impl Response {
/// Construct a response with a status, status text and a string body.
///
/// This is hopefully useful for unit tests.
///
/// Example:
///
/// ```
/// let resp = ureq::Response::new(401, "Authorization Required", "Please log in");
///
/// assert_eq!(resp.status(), 401);
/// ```
pub fn new(status: u16, status_text: &str, body: &str) -> Self {
let r = format!("HTTP/1.1 {} {}\r\n\r\n{}\n", status, status_text, body);
(r.as_ref() as &str)
.parse::<Response>()
.unwrap_or_else(|e| e.into())
}
/// The URL we ended up at. This can differ from the request url when
/// we have followed redirects.
pub fn get_url(&self) -> &str {
self.url.as_ref().map(|s| &s[..]).unwrap_or("")
}
/// The entire status line like: `HTTP/1.1 200 OK`
pub fn status_line(&self) -> &str {
self.status_line.as_str()
}
/// The http version: `HTTP/1.1`
pub fn http_version(&self) -> &str {
&self.status_line.as_str()[0..self.index.0]
}
/// The status as a u16: `200`
pub fn status(&self) -> u16 {
self.status
}
/// The status text: `OK`
pub fn status_text(&self) -> &str {
&self.status_line.as_str()[self.index.1 + 1..].trim()
}
/// The header corresponding header value for the give name, if any.
pub fn header<'a>(&self, name: &'a str) -> Option<&str> {
self.headers
.iter()
.find(|h| h.is_name(name))
.map(|h| h.value())
}
/// A list of the header names in this response.
/// Lowercased to be uniform.
pub fn headers_names(&self) -> Vec<String> {
self.headers
.iter()
.map(|h| h.name().to_lowercase())
.collect()
}
/// Tells if the response has the named header.
pub fn has<'a>(&self, name: &'a str) -> bool {
self.header(name).is_some()
}
/// All headers corresponding values for the give name, or empty vector.
pub fn all<'a>(&self, name: &'a str) -> Vec<&str> {
self.headers
.iter()
.filter(|h| h.is_name(name))
.map(|h| h.value())
.collect()
}
/// Whether the response status is: 200 <= status <= 299
pub fn ok(&self) -> bool {
self.status >= 200 && self.status <= 299
}
pub fn redirect(&self) -> bool {
self.status >= 300 && self.status <= 399
}
/// Whether the response status is: 400 <= status <= 499
pub fn client_error(&self) -> bool {
self.status >= 400 && self.status <= 499
}
/// Whether the response status is: 500 <= status <= 599
pub fn server_error(&self) -> bool {
self.status >= 500 && self.status <= 599
}
/// Whether the response status is: 400 <= status <= 599
pub fn error(&self) -> bool {
self.client_error() || self.server_error()
}
/// Tells if this response is "synthetic".
///
/// The [methods](struct.Request.html#method.call) [firing](struct.Request.html#method.send)
/// [off](struct.Request.html#method.send_string)
/// [requests](struct.Request.html#method.send_json)
/// all return a `Response`; there is no rust style `Result`.
///
/// Rather than exposing a custom error type through results, this library has opted
/// for representing potential connection/TLS/etc errors as HTTP response codes.
/// These invented codes are called "synthetic".
///
/// The idea is that from a library user's point of view the distinction
/// of whether a failure originated in the remote server (500, 502) etc, or some transient
/// network failure, the code path of handling that would most often be the same.
///
/// The specific mapping of error to code can be seen in the [`Error`](enum.Error.html) doc.
///
/// However if the distinction is important, this method can be used to tell. Also see
/// [synthetic_error()](struct.Response.html#method.synthetic_error)
/// to see the actual underlying error.
///
/// ```
/// // scheme that this library doesn't understand
/// let resp = ureq::get("borkedscheme://www.google.com").call();
///
/// // it's an error
/// assert!(resp.error());
///
/// // synthetic error code 400
/// assert_eq!(resp.status(), 400);
///
/// // tell that it's synthetic.
/// assert!(resp.synthetic());
/// ```
pub fn synthetic(&self) -> bool {
self.error.is_some()
}
/// Get the actual underlying error when the response is
/// ["synthetic"](struct.Response.html#method.synthetic).
pub fn synthetic_error(&self) -> &Option<Error> {
&self.error
}
/// The content type part of the "Content-Type" header without
/// the charset.
///
/// Example:
///
/// ```
/// let resp = ureq::get("https://www.google.com/").call();
/// assert_eq!("text/html; charset=ISO-8859-1", resp.header("content-type").unwrap());
/// assert_eq!("text/html", resp.content_type());
/// ```
pub fn content_type(&self) -> &str {
self.header("content-type")
.map(|header| {
header
.find(';')
.map(|index| &header[0..index])
.unwrap_or(header)
})
.unwrap_or(DEFAULT_CONTENT_TYPE)
}
/// The character set part of the "Content-Type" header.native_tls
///
/// Example:
///
/// ```
/// let resp = ureq::get("https://www.google.com/").call();
/// assert_eq!("text/html; charset=ISO-8859-1", resp.header("content-type").unwrap());
/// assert_eq!("ISO-8859-1", resp.charset());
/// ```
pub fn charset(&self) -> &str {
charset_from_content_type(self.header("content-type"))
}
/// Turn this response into a `impl Read` of the body.
///
/// 1. If `Transfer-Encoding: chunked`, the returned reader will unchunk it
/// and any `Content-Length` header is ignored.
/// 2. If `Content-Length` is set, the returned reader is limited to this byte
/// length regardless of how many bytes the server sends.
/// 3. If no length header, the reader is until server stream end.
///
/// Example:
///
/// ```
/// use std::io::Read;
///
/// let resp =
/// ureq::get("https://ureq.s3.eu-central-1.amazonaws.com/hello_world.json")
/// .call();
///
/// assert!(resp.has("Content-Length"));
/// let len = resp.header("Content-Length")
/// .and_then(|s| s.parse::<usize>().ok()).unwrap();
///
/// let mut reader = resp.into_reader();
/// let mut bytes = vec![];
/// reader.read_to_end(&mut bytes);
///
/// assert_eq!(bytes.len(), len);
/// ```
pub fn into_reader(self) -> impl Read {
//
let is_http10 = self.http_version().eq_ignore_ascii_case("HTTP/1.0");
let is_close = self
.header("connection")
.map(|c| c.eq_ignore_ascii_case("close"))
.unwrap_or(false);
let is_head = (&self.unit).as_ref().map(|u| u.is_head()).unwrap_or(false);
let is_chunked = self
.header("transfer-encoding")
.map(|enc| !enc.is_empty()) // whatever it says, do chunked
.unwrap_or(false);
let use_chunked = !is_http10 && !is_head && is_chunked;
let limit_bytes = if is_http10 || is_close {
None
} else if is_head {
// head requests never have a body
Some(0)
} else {
self.header("content-length")
.and_then(|l| l.parse::<usize>().ok())
};
let stream = Box::new(self.stream.expect("No reader in response?!"));
let stream_ptr = Box::into_raw(stream);
let mut yolo = YoloRead {
stream: stream_ptr,
dealloc: false,
};
let unit = self.unit;
match (use_chunked, limit_bytes) {
(true, _) => Box::new(PoolReturnRead::new(
unit,
stream_ptr,
ChunkDecoder::new(yolo),
)) as Box<dyn Read>,
(false, Some(len)) => Box::new(PoolReturnRead::new(
unit,
stream_ptr,
LimitedRead::new(yolo, len),
)),
(false, None) => {
yolo.dealloc = true; // dealloc when read drops.
Box::new(yolo)
}
}
}
/// Turn this response into a String of the response body. By default uses `utf-8`,
/// but can work with charset, see below.
///
/// This is potentially memory inefficient for large bodies since the
/// implementation first reads the reader to end into a `Vec<u8>` and then
/// attempts to decode it using the charset.
///
/// Example:
///
/// ```
/// let resp =
/// ureq::get("https://ureq.s3.eu-central-1.amazonaws.com/hello_world.json")
/// .call();
///
/// let text = resp.into_string().unwrap();
///
/// assert!(text.contains("hello"));
/// ```
///
/// ## Charset support
///
/// Requires feature `ureq = { version = "*", features = ["charset"] }`
///
/// Attempts to respect the character encoding of the `Content-Type` header and
/// falls back to `utf-8`.
///
/// I.e. `Content-Length: text/plain; charset=iso-8859-1` would be decoded in latin-1.
///
pub fn into_string(self) -> IoResult<String> {
#[cfg(feature = "charset")]
{
let encoding = encoding_from_whatwg_label(self.charset())
.or_else(|| encoding_from_whatwg_label(DEFAULT_CHARACTER_SET))
.unwrap();
let mut buf: Vec<u8> = vec![];
self.into_reader().read_to_end(&mut buf)?;
Ok(encoding.decode(&buf, DecoderTrap::Replace).unwrap())
}
#[cfg(not(feature = "charset"))]
{
let mut buf: Vec<u8> = vec![];
self.into_reader().read_to_end(&mut buf)?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
}
/// Turn this response into a (serde) JSON value of the response body.
///
/// Requires feature `ureq = { version = "*", features = ["json"] }`
///
/// Example:
///
/// ```
/// let resp =
/// ureq::get("https://ureq.s3.eu-central-1.amazonaws.com/hello_world.json")
/// .call();
///
/// let json = resp.into_json().unwrap();
///
/// assert_eq!(json["hello"], "world");
/// ```
#[cfg(feature = "json")]
pub fn into_json(self) -> IoResult<serde_json::Value> {
let reader = self.into_reader();
serde_json::from_reader(reader).map_err(|e| {
IoError::new(
ErrorKind::InvalidData,
format!("Failed to read JSON: {}", e),
)
})
}
/// Create a response from a Read trait impl.
///
/// This is hopefully useful for unit tests.
///
/// Example:
///
/// ```
/// use std::io::Cursor;
///
/// let text = "HTTP/1.1 401 Authorization Required\r\n\r\nPlease log in\n";
/// let read = Cursor::new(text.to_string().into_bytes());
/// let resp = ureq::Response::from_read(read);
///
/// assert_eq!(resp.status(), 401);
/// ```
pub fn from_read(reader: impl Read) -> Self {
Self::do_from_read(reader).unwrap_or_else(|e| e.into())
}
fn do_from_read(mut reader: impl Read) -> Result<Response, Error> {
//
// HTTP/1.1 200 OK\r\n
let status_line = read_next_line(&mut reader).map_err(|_| Error::BadStatus)?;
let (index, status) = parse_status_line(status_line.as_str())?;
let mut headers: Vec<Header> = Vec::new();
loop {
let line = read_next_line(&mut reader).map_err(|_| Error::BadHeader)?;
if line.is_empty() {
break;
}
if let Ok(header) = line.as_str().parse::<Header>() {
headers.push(header);
}
}
Ok(Response {
url: None,
error: None,
status_line,
index,
status,
headers,
unit: None,
stream: None,
})
}
#[cfg(test)]
pub fn to_write_vec(&self) -> Vec<u8> {
self.stream.as_ref().unwrap().to_write_vec()
}
}
/// parse a line like: HTTP/1.1 200 OK\r\n
fn parse_status_line(line: &str) -> Result<((usize, usize), u16), Error> {
//
let mut split = line.splitn(3, ' ');
let http_version = split.next().ok_or_else(|| Error::BadStatus)?;
if http_version.len() < 5 {
return Err(Error::BadStatus);
}
let index1 = http_version.len();
let status = split.next().ok_or_else(|| Error::BadStatus)?;
if status.len() < 2 {
return Err(Error::BadStatus);
}
let index2 = index1 + status.len();
let status = status.parse::<u16>().map_err(|_| Error::BadStatus)?;
Ok(((index1, index2), status))
}
impl FromStr for Response {
type Err = Error;
/// Parse a response from a string.
///
/// Example:
/// ```
/// let s = "HTTP/1.1 200 OK\r\n\
/// X-Forwarded-For: 1.2.3.4\r\n\
/// Content-Type: text/plain\r\n\
/// \r\n\
/// Hello World!!!";
/// let resp = s.parse::<ureq::Response>().unwrap();
/// assert!(resp.has("X-Forwarded-For"));
/// let body = resp.into_string().unwrap();
/// assert_eq!(body, "Hello World!!!");
/// ```
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s.as_bytes().to_owned();
let mut cursor = Cursor::new(bytes);
let mut resp = Self::do_from_read(&mut cursor)?;
set_stream(&mut resp, "".into(), None, Stream::Cursor(cursor));
Ok(resp)
}
}
impl Into<Response> for Error {
fn into(self) -> Response {
let status = self.status();
let status_text = self.status_text().to_string();
let body_text = self.body_text();
let mut resp = Response::new(status, &status_text, &body_text);
resp.error = Some(self);
resp
}
}
/// "Give away" Unit and Stream to the response.
///
/// *Internal API*
pub(crate) fn set_stream(resp: &mut Response, url: String, unit: Option<Unit>, stream: Stream) {
resp.url = Some(url);
resp.unit = unit;
resp.stream = Some(stream);
}
fn read_next_line<R: Read>(reader: &mut R) -> IoResult<String> {
let mut buf = Vec::new();
let mut prev_byte_was_cr = false;
loop {
let byte = reader.bytes().next();
let byte = match byte {
Some(b) => b?,
None => return Err(IoError::new(ErrorKind::ConnectionAborted, "Unexpected EOF")),
};
if byte == b'\n' && prev_byte_was_cr {
buf.pop(); // removing the '\r'
return String::from_utf8(buf)
.map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII"));
}
prev_byte_was_cr = byte == b'\r';
buf.push(byte);
}
}
/// Read Wrapper around an (unsafe) pointer to a Stream.
///
/// *Internal API*
pub(crate) struct YoloRead {
stream: *mut Stream,
dealloc: bool, // whether we are to dealloc stream on drop
}
impl Read for YoloRead {
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
unsafe {
if self.stream.is_null() {
return Ok(0);
}
let amount = (*self.stream).read(buf)?;
if amount == 0 {
if self.dealloc {
let _stream = Box::from_raw(self.stream);
}
self.stream = ::std::ptr::null_mut();
}
Ok(amount)
}
}
}
impl Drop for YoloRead {
fn drop(&mut self) {
if self.dealloc && !self.stream.is_null() {
unsafe {
let _stream = Box::from_raw(self.stream);
}
}
}
}
/// Limits a YoloRead to a content size (as set by a "Content-Length" header).
struct LimitedRead {
reader: YoloRead,
limit: usize,
position: usize,
}
impl LimitedRead {
fn new(reader: YoloRead, limit: usize) -> Self {
LimitedRead {
reader,
limit,
position: 0,
}
}
}
impl Read for LimitedRead {
fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
let left = self.limit - self.position;
if left == 0 {
return Ok(0);
}
let from = if left < buf.len() {
&mut buf[0..left]
} else {
buf
};
match self.reader.read(from) {
Ok(amount) => {
self.position += amount;
Ok(amount)
}
Err(e) => Err(e),
}
}
}
/// Extract the charset from a "Content-Type" header.
///
/// "Content-Type: text/plain; charset=iso8859-1" -> "iso8859-1"
///
/// *Internal API*
pub(crate) fn charset_from_content_type(header: Option<&str>) -> &str {
header
.and_then(|header| {
header.find(';').and_then(|semi| {
(&header[semi + 1..])
.find('=')
.map(|equal| (&header[semi + equal + 2..]).trim())
})
})
.unwrap_or(DEFAULT_CHARACTER_SET)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_type_without_charset() {
let s = "HTTP/1.1 200 OK\r\n\
Content-Type: application/json\r\n\
\r\n\
OK";
let resp = s.parse::<Response>().unwrap();
assert_eq!("application/json", resp.content_type());
}
#[test]
fn content_type_with_charset() {
let s = "HTTP/1.1 200 OK\r\n\
Content-Type: application/json; charset=iso-8859-4\r\n\
\r\n\
OK";
let resp = s.parse::<Response>().unwrap();
assert_eq!("application/json", resp.content_type());
}
#[test]
fn content_type_default() {
let s = "HTTP/1.1 200 OK\r\n\r\nOK";
let resp = s.parse::<Response>().unwrap();
assert_eq!("text/plain", resp.content_type());
}
#[test]
fn charset() {
let s = "HTTP/1.1 200 OK\r\n\
Content-Type: application/json; charset=iso-8859-4\r\n\
\r\n\
OK";
let resp = s.parse::<Response>().unwrap();
assert_eq!("iso-8859-4", resp.charset());
}
#[test]
fn charset_default() {
let s = "HTTP/1.1 200 OK\r\n\
Content-Type: application/json\r\n\
\r\n\
OK";
let resp = s.parse::<Response>().unwrap();
assert_eq!("utf-8", resp.charset());
}
#[test]
fn chunked_transfer() {
let s = "HTTP/1.1 200 OK\r\n\
Transfer-Encoding: Chunked\r\n\
\r\n\
3\r\n\
hel\r\n\
b\r\n\
lo world!!!\r\n\
0\r\n\
\r\n";
let resp = s.parse::<Response>().unwrap();
assert_eq!("hello world!!!", resp.into_string().unwrap());
}
#[test]
#[cfg(feature = "json")]
fn parse_simple_json() {
let s = format!(
"HTTP/1.1 200 OK\r\n\
\r\n\
{{\"hello\":\"world\"}}"
);
let resp = s.parse::<Response>().unwrap();
let v = resp.into_json().unwrap();
let compare = "{\"hello\":\"world\"}"
.parse::<serde_json::Value>()
.unwrap();
assert_eq!(v, compare);
}
#[test]
fn parse_borked_header() {
let s = "HTTP/1.1 BORKED\r\n".to_string();
let resp: Response = s.parse::<Response>().unwrap_err().into();
assert_eq!(resp.http_version(), "HTTP/1.1");
assert_eq!(resp.status(), 500);
assert_eq!(resp.status_text(), "Bad Status");
assert_eq!(resp.content_type(), "text/plain");
let v = resp.into_string().unwrap();
assert_eq!(v, "Bad Status\n");
}
}