API changes for 2.0

* Remove Request::build
* All mutations on Request follow builder pattern

The previous `build()` on request was necessary because mutating
functions did not follow a proper builder pattern (taking `&mut self`
instead of `mut self`). With a proper builder pattern, the need for
`.build()` goes away.

* All Request body and call methods consume self

Anything which "executes" the request will now consume the `Request`
to produce a `Result<Response>`.

* Move all config from request to agent builder

Timeouts, redirect config, proxy settings and TLS config are now on
`AgentBuilder`.

* Rename max_pool_connections -> max_idle_connections
* Rename max_pool_connections_per_host ->  max_idle_connections_per_host

Consistent internal and external naming.

* Introduce new AgentConfig for static config created by builder.

`Agent` can be seen as having two parts. Static config and a mutable
shared state between all states. The static config goes into
`AgentConfig` and the mutable shared state into `AgentState`.

* Replace all use of `Default` for `new`.

Deriving or implementing `Default` makes for a secondary instantiation
API.  It is useful in some cases, but gets very confusing when there
is both `new` _and_ a `Default`. It's especially devious for derived
values where a reasonable default is not `0`, `false` or `None`.

* Remove feature native_tls, we want only native rustls.

This feature made for very clunky handling throughout the code. From a
security point of view, it's better to stick with one single TLS API.
Rustls recently got an official audit (very positive).

https://github.com/ctz/rustls/tree/master/audit

Rustls deliberately omits support for older, insecure TLS such as TLS
1.1 or RC4. This might be a problem for a user of ureq, but on balance
not considered important enough to keep native_tls.

* Remove auth and support for basic auth.

The API just wasn't enough. A future reintroduction should at least
also provide a `Bearer` mechanism and possibly more.

* Rename jar -> cookie_store
* Rename jar -> cookie_tin

Just make some field names sync up with the type.

* Drop "cookies" as default feature

The need for handling cookies is probably rare, let's not enable it by
default.

* Change all feature checks for "cookie" to "cookies"

The outward facing feature is "cookies" and I think it's better form
that the code uses the official feature name instead of the optional
library "cookies".

* Keep `set` on Agent level as well as AgentBuilder.

The idea is that an auth exchange might result in a header that need
to be set _after_ the agent has been built.
This commit is contained in:
Martin Algesten
2020-10-25 11:08:50 +01:00
parent 703ca41960
commit 1369c32351
24 changed files with 398 additions and 647 deletions

View File

@@ -15,9 +15,6 @@ use rustls::StreamOwned;
#[cfg(feature = "socks-proxy")]
use socks::{TargetAddr, ToTargetAddr};
#[cfg(feature = "native-tls")]
use native_tls::{HandshakeError, TlsStream};
use crate::proxy::Proto;
use crate::proxy::Proxy;
@@ -27,10 +24,8 @@ use crate::unit::Unit;
#[allow(clippy::large_enum_variant)]
pub enum Stream {
Http(BufReader<TcpStream>),
#[cfg(all(feature = "tls", not(feature = "native-tls")))]
#[cfg(feature = "tls")]
Https(BufReader<rustls::StreamOwned<rustls::ClientSession, TcpStream>>),
#[cfg(all(feature = "native-tls", not(feature = "tls")))]
Https(BufReader<TlsStream<TcpStream>>),
Cursor(Cursor<Vec<u8>>),
#[cfg(test)]
Test(Box<dyn BufRead + Send + Sync>, Vec<u8>),
@@ -101,10 +96,7 @@ impl fmt::Debug for Stream {
"Stream[{}]",
match self {
Stream::Http(_) => "http",
#[cfg(any(
all(feature = "tls", not(feature = "native-tls")),
all(feature = "native-tls", not(feature = "tls")),
))]
#[cfg(feature = "tls")]
Stream::Https(_) => "https",
Stream::Cursor(_) => "cursor",
#[cfg(test)]
@@ -144,10 +136,7 @@ impl Stream {
pub fn is_poolable(&self) -> bool {
match self {
Stream::Http(_) => true,
#[cfg(any(
all(feature = "tls", not(feature = "native-tls")),
all(feature = "native-tls", not(feature = "tls")),
))]
#[cfg(feature = "tls")]
Stream::Https(_) => true,
_ => false,
}
@@ -186,10 +175,7 @@ impl Read for Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Stream::Http(sock) => sock.read(buf),
#[cfg(any(
all(feature = "tls", not(feature = "native-tls")),
all(feature = "native-tls", not(feature = "tls")),
))]
#[cfg(feature = "tls")]
Stream::Https(stream) => read_https(stream, buf),
Stream::Cursor(read) => read.read(buf),
#[cfg(test)]
@@ -202,10 +188,7 @@ impl BufRead for Stream {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
match self {
Stream::Http(r) => r.fill_buf(),
#[cfg(any(
all(feature = "tls", not(feature = "native-tls")),
all(feature = "native-tls", not(feature = "tls")),
))]
#[cfg(feature = "tls")]
Stream::Https(r) => r.fill_buf(),
Stream::Cursor(r) => r.fill_buf(),
#[cfg(test)]
@@ -216,10 +199,7 @@ impl BufRead for Stream {
fn consume(&mut self, amt: usize) {
match self {
Stream::Http(r) => r.consume(amt),
#[cfg(any(
all(feature = "tls", not(feature = "native-tls")),
all(feature = "native-tls", not(feature = "tls")),
))]
#[cfg(feature = "tls")]
Stream::Https(r) => r.consume(amt),
Stream::Cursor(r) => r.consume(amt),
#[cfg(test)]
@@ -238,7 +218,7 @@ where
}
}
#[cfg(all(feature = "tls", not(feature = "native-tls")))]
#[cfg(feature = "tls")]
fn read_https(
stream: &mut BufReader<StreamOwned<ClientSession, TcpStream>>,
buf: &mut [u8],
@@ -250,17 +230,8 @@ fn read_https(
}
}
#[cfg(all(feature = "native-tls", not(feature = "tls")))]
fn read_https(stream: &mut BufReader<TlsStream<TcpStream>>, buf: &mut [u8]) -> io::Result<usize> {
match stream.read(buf) {
Ok(size) => Ok(size),
Err(ref e) if is_close_notify(e) => Ok(0),
Err(e) => Err(e),
}
}
#[allow(deprecated)]
#[cfg(any(feature = "tls", feature = "native-tls"))]
#[cfg(feature = "tls")]
fn is_close_notify(e: &std::io::Error) -> bool {
if e.kind() != ErrorKind::ConnectionAborted {
return false;
@@ -279,10 +250,7 @@ impl Write for Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Stream::Http(sock) => sock.get_mut().write(buf),
#[cfg(any(
all(feature = "tls", not(feature = "native-tls")),
all(feature = "native-tls", not(feature = "tls")),
))]
#[cfg(feature = "tls")]
Stream::Https(stream) => stream.get_mut().write(buf),
Stream::Cursor(_) => panic!("Write to read only stream"),
#[cfg(test)]
@@ -292,10 +260,7 @@ impl Write for Stream {
fn flush(&mut self) -> io::Result<()> {
match self {
Stream::Http(sock) => sock.get_mut().flush(),
#[cfg(any(
all(feature = "tls", not(feature = "native-tls")),
all(feature = "native-tls", not(feature = "tls")),
))]
#[cfg(feature = "tls")]
Stream::Https(stream) => stream.get_mut().flush(),
Stream::Cursor(_) => panic!("Flush read only stream"),
#[cfg(test)]
@@ -326,7 +291,7 @@ fn configure_certs(config: &mut rustls::ClientConfig) {
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
}
#[cfg(all(feature = "tls", not(feature = "native-tls")))]
#[cfg(feature = "tls")]
pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error> {
use once_cell::sync::Lazy;
use std::sync::Arc;
@@ -343,6 +308,8 @@ pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error
.map_err(|err| Error::DnsFailed(err.to_string()))?;
let tls_conf: &Arc<rustls::ClientConfig> = unit
.req
.agent
.config
.tls_config
.as_ref()
.map(|c| &c.0)
@@ -356,35 +323,13 @@ pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error
Ok(Stream::Https(BufReader::new(stream)))
}
#[cfg(all(feature = "native-tls", not(feature = "tls")))]
pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error> {
use std::sync::Arc;
let port = unit.url.port().unwrap_or(443);
let sock = connect_host(unit, hostname, port)?;
let tls_connector: Arc<native_tls::TlsConnector> = match &unit.req.tls_connector {
Some(connector) => connector.0.clone(),
None => Arc::new(native_tls::TlsConnector::new().map_err(|e| Error::TlsError(e))?),
};
let stream = tls_connector
.connect(&hostname.trim_matches(|c| c == '[' || c == ']'), sock)
.map_err(|e| match e {
HandshakeError::Failure(err) => Error::TlsError(err),
// The only other possibility is WouldBlock. Since we don't
// handle retries of WouldBlock, turn it into a generic error.
_ => Error::ConnectionFailed("TLS handshake unexpected error".to_string()),
})?;
Ok(Stream::Https(BufReader::new(stream)))
}
pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<TcpStream, Error> {
let deadline: Option<Instant> = if let Some(timeout_connect) = unit.req.timeout_connect {
Instant::now().checked_add(timeout_connect)
} else {
unit.deadline
};
let deadline: Option<Instant> =
if let Some(timeout_connect) = unit.req.agent.config.timeout_connect {
Instant::now().checked_add(timeout_connect)
} else {
unit.deadline
};
let proxy: Option<Proxy> = unit.req.proxy();
let netloc = match proxy {
Some(ref proxy) => format!("{}:{}", proxy.server, proxy.port),
@@ -451,7 +396,7 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<Tcp
if let Some(deadline) = deadline {
stream.set_read_timeout(Some(time_until_deadline(deadline)?))?;
} else if let Some(timeout_read) = unit.req.timeout_read {
} else if let Some(timeout_read) = unit.req.agent.config.timeout_read {
stream.set_read_timeout(Some(timeout_read))?;
} else {
stream.set_read_timeout(None)?;
@@ -459,7 +404,7 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<Tcp
if let Some(deadline) = deadline {
stream.set_write_timeout(Some(time_until_deadline(deadline)?))?;
} else if let Some(timeout_write) = unit.req.timeout_write {
} else if let Some(timeout_write) = unit.req.agent.config.timeout_write {
stream.set_write_timeout(Some(timeout_write)).ok();
} else {
stream.set_write_timeout(None)?;
@@ -647,7 +592,7 @@ pub(crate) fn connect_test(unit: &Unit) -> Result<Stream, Error> {
Err(Error::UnknownScheme(unit.url.scheme().to_string()))
}
#[cfg(not(any(feature = "tls", feature = "native-tls")))]
#[cfg(not(feature = "tls"))]
pub(crate) fn connect_https(unit: &Unit, _hostname: &str) -> Result<Stream, Error> {
Err(Error::UnknownScheme(unit.url.scheme().to_string()))
}