Merge branch 'release-2.0' into rm-rayon
This commit is contained in:
1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
@@ -27,7 +27,6 @@ jobs:
|
|||||||
tls:
|
tls:
|
||||||
- ""
|
- ""
|
||||||
- tls
|
- tls
|
||||||
- native-tls
|
|
||||||
feature:
|
feature:
|
||||||
- ""
|
- ""
|
||||||
- json
|
- json
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ edition = "2018"
|
|||||||
features = [ "tls", "json", "charset", "cookies", "socks-proxy" ]
|
features = [ "tls", "json", "charset", "cookies", "socks-proxy" ]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["tls", "cookies"]
|
default = ["tls"]
|
||||||
json = ["serde", "serde_json"]
|
json = ["serde", "serde_json"]
|
||||||
charset = ["encoding"]
|
charset = ["encoding"]
|
||||||
tls = ["rustls", "webpki", "webpki-roots"]
|
tls = ["rustls", "webpki", "webpki-roots"]
|
||||||
@@ -38,13 +38,12 @@ rustls-native-certs = { version = "0.4", optional = true }
|
|||||||
serde = { version = "1", optional = true }
|
serde = { version = "1", optional = true }
|
||||||
serde_json = { version = "1", optional = true }
|
serde_json = { version = "1", optional = true }
|
||||||
encoding = { version = "0.2", optional = true }
|
encoding = { version = "0.2", optional = true }
|
||||||
native-tls = { version = "0.2", optional = true }
|
|
||||||
cookie_store = { version = "0.12.0", optional = true }
|
cookie_store = { version = "0.12.0", optional = true }
|
||||||
log = "0.4.11"
|
log = "0.4.11"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
env_logger = "0.7.1"
|
env_logger = "0.8.1"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "smoke-test"
|
name = "smoke-test"
|
||||||
|
|||||||
@@ -61,9 +61,7 @@ You can control them when including `ureq` as a dependency.
|
|||||||
```
|
```
|
||||||
|
|
||||||
* `tls` enables https. This is enabled by default.
|
* `tls` enables https. This is enabled by default.
|
||||||
* `native-tls` enables https using the [`native-tls`](https://crates.io/crates/native-tls) crate.
|
* `cookies` enables handling cookies between requests in an agent.
|
||||||
NB: To make this work you currently need to use `default-features: false` to disable `tls`.
|
|
||||||
We plan on fixing that.
|
|
||||||
* `json` enables `response.into_json()` and `request.send_json()` via serde_json.
|
* `json` enables `response.into_json()` and `request.send_json()` via serde_json.
|
||||||
* `charset` enables interpreting the charset part of
|
* `charset` enables interpreting the charset part of
|
||||||
`Content-Type: text/plain; charset=iso-8859-1`. Without this, the library
|
`Content-Type: text/plain; charset=iso-8859-1`. Without this, the library
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ impl fmt::Display for Oops {
|
|||||||
type Result<T> = result::Result<T, Oops>;
|
type Result<T> = result::Result<T, Oops>;
|
||||||
|
|
||||||
fn get(agent: &ureq::Agent, url: &str) -> Result<Vec<u8>> {
|
fn get(agent: &ureq::Agent, url: &str) -> Result<Vec<u8>> {
|
||||||
let response = agent
|
let response = agent.get(url).call()?;
|
||||||
.get(url)
|
|
||||||
.timeout_connect(std::time::Duration::from_secs(5))
|
|
||||||
.timeout(Duration::from_secs(20))
|
|
||||||
.call()?;
|
|
||||||
let mut reader = response.into_reader();
|
let mut reader = response.into_reader();
|
||||||
let mut bytes = vec![];
|
let mut bytes = vec![];
|
||||||
reader.read_to_end(&mut bytes)?;
|
reader.read_to_end(&mut bytes)?;
|
||||||
@@ -53,7 +49,10 @@ fn get_and_write(agent: &ureq::Agent, url: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_many(urls: Vec<String>, simultaneous_fetches: usize) -> Result<()> {
|
fn get_many(urls: Vec<String>, simultaneous_fetches: usize) -> Result<()> {
|
||||||
let agent = ureq::Agent::default();
|
let agent = ureq::builder()
|
||||||
|
.timeout_connect(Duration::from_secs(5))
|
||||||
|
.timeout(Duration::from_secs(20))
|
||||||
|
.build();
|
||||||
let mutex = Arc::new(Mutex::new(urls));
|
let mutex = Arc::new(Mutex::new(urls));
|
||||||
let mut join_handles: Vec<JoinHandle<()>> = vec![];
|
let mut join_handles: Vec<JoinHandle<()>> = vec![];
|
||||||
for _ in 0..simultaneous_fetches {
|
for _ in 0..simultaneous_fetches {
|
||||||
|
|||||||
324
src/agent.rs
324
src/agent.rs
@@ -1,36 +1,39 @@
|
|||||||
#[cfg(feature = "cookie")]
|
|
||||||
use cookie::Cookie;
|
|
||||||
#[cfg(feature = "cookie")]
|
|
||||||
use cookie_store::CookieStore;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(feature = "cookie")]
|
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
|
||||||
use crate::cookies::CookieTin;
|
|
||||||
use crate::header::{self, Header};
|
use crate::header::{self, Header};
|
||||||
use crate::pool::ConnectionPool;
|
use crate::pool::ConnectionPool;
|
||||||
use crate::proxy::Proxy;
|
use crate::proxy::Proxy;
|
||||||
use crate::request::Request;
|
use crate::request::Request;
|
||||||
use crate::resolve::ArcResolver;
|
use crate::resolve::{ArcResolver, StdResolver};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[cfg(feature = "cookies")]
|
||||||
|
use {crate::cookies::CookieTin, cookie::Cookie, cookie_store::CookieStore, url::Url};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct AgentBuilder {
|
pub struct AgentBuilder {
|
||||||
headers: Vec<Header>,
|
headers: Vec<Header>,
|
||||||
proxy: Option<Proxy>,
|
config: AgentConfig,
|
||||||
max_idle_connections: usize,
|
|
||||||
max_idle_connections_per_host: usize,
|
|
||||||
/// Cookies saved between requests.
|
/// Cookies saved between requests.
|
||||||
/// Invariant: All cookies must have a nonempty domain and path.
|
/// Invariant: All cookies must have a nonempty domain and path.
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
jar: CookieStore,
|
cookie_store: Option<CookieStore>,
|
||||||
resolver: ArcResolver,
|
resolver: ArcResolver,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Agent {
|
/// Config as built by AgentBuilder and then static for the lifetime of the Agent.
|
||||||
fn default() -> Self {
|
#[derive(Debug, Clone)]
|
||||||
AgentBuilder::new().build()
|
pub(crate) struct AgentConfig {
|
||||||
}
|
pub max_idle_connections: usize,
|
||||||
|
pub max_idle_connections_per_host: usize,
|
||||||
|
pub proxy: Option<Proxy>,
|
||||||
|
pub timeout_connect: Option<Duration>,
|
||||||
|
pub timeout_read: Option<Duration>,
|
||||||
|
pub timeout_write: Option<Duration>,
|
||||||
|
pub timeout: Option<Duration>,
|
||||||
|
pub redirects: u32,
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
pub tls_config: Option<TLSClientConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Agents keep state between requests.
|
/// Agents keep state between requests.
|
||||||
@@ -40,12 +43,13 @@ impl Default for Agent {
|
|||||||
/// can keep a state.
|
/// can keep a state.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let agent = ureq::agent();
|
/// let mut agent = ureq::agent();
|
||||||
|
///
|
||||||
|
/// agent.set("x-my-secret-header", "very secret");
|
||||||
///
|
///
|
||||||
/// let auth = agent
|
/// let auth = agent
|
||||||
/// .post("/login")
|
/// .post("/login")
|
||||||
/// .auth("martin", "rubbermashgum")
|
/// .call(); // blocks.
|
||||||
/// .call(); // blocks. puts auth cookies in agent.
|
|
||||||
///
|
///
|
||||||
/// if auth.is_err() {
|
/// if auth.is_err() {
|
||||||
/// println!("Noes!");
|
/// println!("Noes!");
|
||||||
@@ -66,6 +70,7 @@ impl Default for Agent {
|
|||||||
/// that shares the same underlying connection pool and other state.
|
/// that shares the same underlying connection pool and other state.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Agent {
|
pub struct Agent {
|
||||||
|
pub(crate) config: Arc<AgentConfig>,
|
||||||
/// Copied into each request of this agent.
|
/// Copied into each request of this agent.
|
||||||
pub(crate) headers: Vec<Header>,
|
pub(crate) headers: Vec<Header>,
|
||||||
/// Reused agent state for repeated requests from this agent.
|
/// Reused agent state for repeated requests from this agent.
|
||||||
@@ -75,19 +80,47 @@ pub struct Agent {
|
|||||||
/// Container of the state
|
/// Container of the state
|
||||||
///
|
///
|
||||||
/// *Internal API*.
|
/// *Internal API*.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct AgentState {
|
pub(crate) struct AgentState {
|
||||||
/// Reused connections between requests.
|
/// Reused connections between requests.
|
||||||
pub(crate) pool: ConnectionPool,
|
pub(crate) pool: ConnectionPool,
|
||||||
pub(crate) proxy: Option<Proxy>,
|
pub(crate) proxy: Option<Proxy>,
|
||||||
/// Cookies saved between requests.
|
/// Cookies saved between requests.
|
||||||
/// Invariant: All cookies must have a nonempty domain and path.
|
/// Invariant: All cookies must have a nonempty domain and path.
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
pub(crate) jar: CookieTin,
|
pub(crate) cookie_tin: CookieTin,
|
||||||
pub(crate) resolver: ArcResolver,
|
pub(crate) resolver: ArcResolver,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Agent {
|
impl Agent {
|
||||||
|
/// Creates an Agent with default settings.
|
||||||
|
///
|
||||||
|
/// Same as `AgentBuilder::new().build()`.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
AgentBuilder::new().build()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a extra header field that will be present in all following requests using the agent.
|
||||||
|
///
|
||||||
|
/// This is useful for cases like auth, where we do a number of requests before getting
|
||||||
|
/// some credential that later must be presented in a header.
|
||||||
|
///
|
||||||
|
/// Notice that fixed headers can also be set in the `AgentBuilder`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let mut agent = ureq::agent();
|
||||||
|
///
|
||||||
|
/// agent.set("X-API-Key", "foobar");
|
||||||
|
/// agent.set("Accept", "text/plain");
|
||||||
|
///
|
||||||
|
/// let r = agent
|
||||||
|
/// .get("/my-page")
|
||||||
|
/// .call();
|
||||||
|
/// ```
|
||||||
|
pub fn set(&mut self, header: &str, value: &str) {
|
||||||
|
header::add_header(&mut self.headers, Header::new(header, value));
|
||||||
|
}
|
||||||
|
|
||||||
/// Request by providing the HTTP verb such as `GET`, `POST`...
|
/// Request by providing the HTTP verb such as `GET`, `POST`...
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
@@ -99,7 +132,7 @@ impl Agent {
|
|||||||
/// println!("{:?}", r);
|
/// println!("{:?}", r);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn request(&self, method: &str, path: &str) -> Request {
|
pub fn request(&self, method: &str, path: &str) -> Request {
|
||||||
Request::new(&self, method.into(), path.into())
|
Request::new(self.clone(), method.into(), path.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store a cookie in this agent.
|
/// Store a cookie in this agent.
|
||||||
@@ -112,10 +145,10 @@ impl Agent {
|
|||||||
/// .finish();
|
/// .finish();
|
||||||
/// agent.set_cookie(cookie, &"https://example.com/".parse().unwrap());
|
/// agent.set_cookie(cookie, &"https://example.com/".parse().unwrap());
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
pub fn set_cookie(&self, cookie: Cookie<'static>, url: &Url) {
|
pub fn set_cookie(&self, cookie: Cookie<'static>, url: &Url) {
|
||||||
self.state
|
self.state
|
||||||
.jar
|
.cookie_tin
|
||||||
.store_response_cookies(Some(cookie).into_iter(), url);
|
.store_response_cookies(Some(cookie).into_iter(), url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,11 +194,24 @@ impl Agent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AgentBuilder {
|
impl AgentBuilder {
|
||||||
pub fn new() -> AgentBuilder {
|
pub fn new() -> Self {
|
||||||
AgentBuilder {
|
AgentBuilder {
|
||||||
max_idle_connections: 100,
|
headers: vec![],
|
||||||
max_idle_connections_per_host: 1,
|
config: AgentConfig {
|
||||||
..Default::default()
|
max_idle_connections: crate::pool::DEFAULT_MAX_IDLE_CONNECTIONS,
|
||||||
|
max_idle_connections_per_host: crate::pool::DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST,
|
||||||
|
proxy: None,
|
||||||
|
timeout_connect: Some(Duration::from_secs(30)),
|
||||||
|
timeout_read: None,
|
||||||
|
timeout_write: None,
|
||||||
|
timeout: None,
|
||||||
|
redirects: 5,
|
||||||
|
#[cfg(feature = "tls")]
|
||||||
|
tls_config: None,
|
||||||
|
},
|
||||||
|
resolver: StdResolver.into(),
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
cookie_store: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,25 +221,29 @@ impl AgentBuilder {
|
|||||||
// not implement clone, so we have to give ownership to the newly
|
// not implement clone, so we have to give ownership to the newly
|
||||||
// built Agent.
|
// built Agent.
|
||||||
pub fn build(self) -> Agent {
|
pub fn build(self) -> Agent {
|
||||||
|
let config = Arc::new(self.config);
|
||||||
Agent {
|
Agent {
|
||||||
headers: self.headers.clone(),
|
headers: self.headers,
|
||||||
state: Arc::new(AgentState {
|
state: Arc::new(AgentState {
|
||||||
pool: ConnectionPool::new(
|
pool: ConnectionPool::new_with_limits(
|
||||||
self.max_idle_connections,
|
config.max_idle_connections,
|
||||||
self.max_idle_connections_per_host,
|
config.max_idle_connections_per_host,
|
||||||
|
),
|
||||||
|
proxy: config.proxy.clone(),
|
||||||
|
#[cfg(feature = "cookies")]
|
||||||
|
cookie_tin: CookieTin::new(
|
||||||
|
self.cookie_store.unwrap_or_else(|| CookieStore::default()),
|
||||||
),
|
),
|
||||||
proxy: self.proxy.clone(),
|
|
||||||
#[cfg(feature = "cookie")]
|
|
||||||
jar: CookieTin::new(self.jar),
|
|
||||||
resolver: self.resolver,
|
resolver: self.resolver,
|
||||||
}),
|
}),
|
||||||
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set a header field that will be present in all requests using the agent.
|
/// Set a header field that will be present in all requests using the agent.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let agent = ureq::AgentBuilder::new()
|
/// let agent = ureq::builder()
|
||||||
/// .set("X-API-Key", "foobar")
|
/// .set("X-API-Key", "foobar")
|
||||||
/// .set("Accept", "text/plain")
|
/// .set("Accept", "text/plain")
|
||||||
/// .build();
|
/// .build();
|
||||||
@@ -213,39 +263,18 @@ impl AgentBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Basic auth that will be present in all requests using the agent.
|
/// Set the proxy server to use for all connections from this Agent.
|
||||||
///
|
///
|
||||||
|
/// Example:
|
||||||
/// ```
|
/// ```
|
||||||
|
/// let proxy = ureq::Proxy::new("user:password@cool.proxy:9090").unwrap();
|
||||||
/// let agent = ureq::AgentBuilder::new()
|
/// let agent = ureq::AgentBuilder::new()
|
||||||
/// .auth("martin", "rubbermashgum")
|
/// .proxy(proxy)
|
||||||
/// .build();
|
/// .build();
|
||||||
///
|
|
||||||
/// let r = agent
|
|
||||||
/// .get("/my_page")
|
|
||||||
/// .call();
|
|
||||||
/// println!("{:?}", r);
|
|
||||||
/// ```
|
/// ```
|
||||||
pub fn auth(self, user: &str, pass: &str) -> Self {
|
pub fn proxy(mut self, proxy: Proxy) -> Self {
|
||||||
let pass = basic_auth(user, pass);
|
self.config.proxy = Some(proxy);
|
||||||
self.auth_kind("Basic", &pass)
|
self
|
||||||
}
|
|
||||||
|
|
||||||
/// Auth of other kinds such as `Digest`, `Token` etc, that will be present
|
|
||||||
/// in all requests using the agent.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// // sets a header "Authorization: token secret"
|
|
||||||
/// let agent = ureq::AgentBuilder::new()
|
|
||||||
/// .auth_kind("token", "secret")
|
|
||||||
/// .build();
|
|
||||||
///
|
|
||||||
/// let r = agent
|
|
||||||
/// .get("/my_page")
|
|
||||||
/// .call();
|
|
||||||
/// ```
|
|
||||||
pub fn auth_kind(self, kind: &str, pass: &str) -> Self {
|
|
||||||
let value = format!("{} {}", kind, pass);
|
|
||||||
self.set("Authorization", &value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the maximum number of connections allowed in the connection pool.
|
/// Sets the maximum number of connections allowed in the connection pool.
|
||||||
@@ -253,10 +282,10 @@ impl AgentBuilder {
|
|||||||
/// connection pooling.
|
/// connection pooling.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let agent = ureq::AgentBuilder::new().max_pool_connections(200).build();
|
/// let agent = ureq::AgentBuilder::new().max_idle_connections(200).build();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn max_pool_connections(mut self, max: usize) -> Self {
|
pub fn max_idle_connections(mut self, max: usize) -> Self {
|
||||||
self.max_idle_connections = max;
|
self.config.max_idle_connections = max;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,10 +294,10 @@ impl AgentBuilder {
|
|||||||
/// would disable connection pooling.
|
/// would disable connection pooling.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let agent = ureq::AgentBuilder::new().max_pool_connections_per_host(200).build();
|
/// let agent = ureq::AgentBuilder::new().max_idle_connections_per_host(200).build();
|
||||||
/// ```
|
/// ```
|
||||||
pub fn max_pool_connections_per_host(mut self, max: usize) -> Self {
|
pub fn max_idle_connections_per_host(mut self, max: usize) -> Self {
|
||||||
self.max_idle_connections_per_host = max;
|
self.config.max_idle_connections_per_host = max;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,27 +325,123 @@ impl AgentBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the proxy server to use for all connections from this Agent.
|
/// Timeout for the socket connection to be successful.
|
||||||
|
/// If both this and `.timeout()` are both set, `.timeout_connect()`
|
||||||
|
/// takes precedence.
|
||||||
|
///
|
||||||
|
/// The default is 30 seconds.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let agent = ureq::builder()
|
||||||
|
/// .timeout_connect(std::time::Duration::from_secs(1))
|
||||||
|
/// .build();
|
||||||
|
/// let r = agent.get("/my_page").call();
|
||||||
|
/// ```
|
||||||
|
pub fn timeout_connect(mut self, timeout: Duration) -> Self {
|
||||||
|
self.config.timeout_connect = Some(timeout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timeout for the individual reads of the socket.
|
||||||
|
/// If both this and `.timeout()` are both set, `.timeout()`
|
||||||
|
/// takes precedence.
|
||||||
|
///
|
||||||
|
/// The default is `0`, which means it can block forever.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let agent = ureq::builder()
|
||||||
|
/// .timeout_read(std::time::Duration::from_secs(1))
|
||||||
|
/// .build();
|
||||||
|
/// let r = agent.get("/my_page").call();
|
||||||
|
/// ```
|
||||||
|
pub fn timeout_read(mut self, timeout: Duration) -> Self {
|
||||||
|
self.config.timeout_read = Some(timeout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timeout for the individual writes to the socket.
|
||||||
|
/// If both this and `.timeout()` are both set, `.timeout()`
|
||||||
|
/// takes precedence.
|
||||||
|
///
|
||||||
|
/// The default is `0`, which means it can block forever.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let agent = ureq::builder()
|
||||||
|
/// .timeout_write(std::time::Duration::from_secs(1))
|
||||||
|
/// .build();
|
||||||
|
/// let r = agent.get("/my_page").call();
|
||||||
|
/// ```
|
||||||
|
pub fn timeout_write(mut self, timeout: Duration) -> Self {
|
||||||
|
self.config.timeout_write = Some(timeout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Timeout for the overall request, including DNS resolution, connection
|
||||||
|
/// time, redirects, and reading the response body. Slow DNS resolution
|
||||||
|
/// may cause a request to exceed the timeout, because the DNS request
|
||||||
|
/// cannot be interrupted with the available APIs.
|
||||||
|
///
|
||||||
|
/// This takes precedence over `.timeout_read()` and `.timeout_write()`, but
|
||||||
|
/// not `.timeout_connect()`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// // wait max 1 second for whole request to complete.
|
||||||
|
/// let agent = ureq::builder()
|
||||||
|
/// .timeout(std::time::Duration::from_secs(1))
|
||||||
|
/// .build();
|
||||||
|
/// let r = agent.get("/my_page").call();
|
||||||
|
/// ```
|
||||||
|
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.config.timeout = Some(timeout);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many redirects to follow.
|
||||||
|
///
|
||||||
|
/// Defaults to `5`. Set to `0` to avoid redirects and instead
|
||||||
|
/// get a response object with the 3xx status code.
|
||||||
|
///
|
||||||
|
/// If the redirect count hits this limit (and it's > 0), TooManyRedirects is returned.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// let r = ureq::builder()
|
||||||
|
/// .redirects(10)
|
||||||
|
/// .build()
|
||||||
|
/// .get("/my_page")
|
||||||
|
/// .call();
|
||||||
|
/// println!("{:?}", r);
|
||||||
|
/// ```
|
||||||
|
pub fn redirects(mut self, n: u32) -> Self {
|
||||||
|
self.config.redirects = n;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the TLS client config to use for the connection. See [`ClientConfig`](https://docs.rs/rustls/latest/rustls/struct.ClientConfig.html).
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
/// ```
|
/// ```
|
||||||
/// let proxy = ureq::Proxy::new("user:password@cool.proxy:9090").unwrap();
|
/// let tls_config = std::sync::Arc::new(rustls::ClientConfig::new());
|
||||||
/// let agent = ureq::AgentBuilder::new()
|
/// let agent = ureq::builder()
|
||||||
/// .proxy(proxy)
|
/// .set_tls_config(tls_config.clone())
|
||||||
/// .build();
|
/// .build();
|
||||||
|
/// let req = agent.post("https://cool.server");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn proxy(mut self, proxy: Proxy) -> Self {
|
#[cfg(feature = "tls")]
|
||||||
self.proxy = Some(proxy);
|
pub fn set_tls_config(mut self, tls_config: Arc<rustls::ClientConfig>) -> Self {
|
||||||
|
self.config.tls_config = Some(TLSClientConfig(tls_config));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn basic_auth(user: &str, pass: &str) -> String {
|
#[cfg(feature = "tls")]
|
||||||
let safe = match user.find(':') {
|
#[derive(Clone)]
|
||||||
Some(idx) => &user[..idx],
|
pub(crate) struct TLSClientConfig(pub(crate) Arc<rustls::ClientConfig>);
|
||||||
None => user,
|
|
||||||
};
|
#[cfg(feature = "tls")]
|
||||||
base64::encode(&format!("{}:{}", safe, pass))
|
impl std::fmt::Debug for TLSClientConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("TLSClientConfig").finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -330,31 +455,4 @@ mod tests {
|
|||||||
let _agent: Box<dyn Send> = Box::new(AgentBuilder::new().build());
|
let _agent: Box<dyn Send> = Box::new(AgentBuilder::new().build());
|
||||||
let _agent: Box<dyn Sync> = Box::new(AgentBuilder::new().build());
|
let _agent: Box<dyn Sync> = Box::new(AgentBuilder::new().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(any(feature = "tls", feature = "native-tls"))]
|
|
||||||
fn agent_pool() {
|
|
||||||
use std::io::Read;
|
|
||||||
|
|
||||||
let agent = crate::agent();
|
|
||||||
let url = "http://example.com";
|
|
||||||
// req 1
|
|
||||||
let resp = agent.get(url).call().unwrap();
|
|
||||||
let mut reader = resp.into_reader();
|
|
||||||
let mut buf = vec![];
|
|
||||||
// reading the entire content will return the connection to the pool
|
|
||||||
reader.read_to_end(&mut buf).unwrap();
|
|
||||||
|
|
||||||
fn poolsize(agent: &Agent) -> usize {
|
|
||||||
agent.state.pool.len()
|
|
||||||
}
|
|
||||||
assert_eq!(poolsize(&agent), 1);
|
|
||||||
|
|
||||||
// req 2 should be done with a reused connection
|
|
||||||
let resp = agent.get(url).call().unwrap();
|
|
||||||
assert_eq!(poolsize(&agent), 0);
|
|
||||||
let mut reader = resp.into_reader();
|
|
||||||
let mut buf = vec![];
|
|
||||||
reader.read_to_end(&mut buf).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/body.rs
30
src/body.rs
@@ -15,16 +15,16 @@ use super::SerdeValue;
|
|||||||
/// The different kinds of bodies to send.
|
/// The different kinds of bodies to send.
|
||||||
///
|
///
|
||||||
/// *Internal API*
|
/// *Internal API*
|
||||||
pub(crate) enum Payload {
|
pub(crate) enum Payload<'a> {
|
||||||
Empty,
|
Empty,
|
||||||
Text(String, String),
|
Text(&'a str, String),
|
||||||
#[cfg(feature = "json")]
|
#[cfg(feature = "json")]
|
||||||
JSON(SerdeValue),
|
JSON(SerdeValue),
|
||||||
Reader(Box<dyn Read + 'static>),
|
Reader(Box<dyn Read + 'a>),
|
||||||
Bytes(Vec<u8>),
|
Bytes(&'a [u8]),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Payload {
|
impl fmt::Debug for Payload<'_> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Payload::Empty => write!(f, "Empty"),
|
Payload::Empty => write!(f, "Empty"),
|
||||||
@@ -37,8 +37,8 @@ impl fmt::Debug for Payload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Payload {
|
impl Default for Payload<'_> {
|
||||||
fn default() -> Payload {
|
fn default() -> Self {
|
||||||
Payload::Empty
|
Payload::Empty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,25 +56,25 @@ pub(crate) enum BodySize {
|
|||||||
/// Payloads are turned into this type where we can hold both a size and the reader.
|
/// Payloads are turned into this type where we can hold both a size and the reader.
|
||||||
///
|
///
|
||||||
/// *Internal API*
|
/// *Internal API*
|
||||||
pub(crate) struct SizedReader {
|
pub(crate) struct SizedReader<'a> {
|
||||||
pub size: BodySize,
|
pub size: BodySize,
|
||||||
pub reader: Box<dyn Read + 'static>,
|
pub reader: Box<dyn Read + 'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for SizedReader {
|
impl fmt::Debug for SizedReader<'_> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "SizedReader[size={:?},reader]", self.size)
|
write!(f, "SizedReader[size={:?},reader]", self.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SizedReader {
|
impl<'a> SizedReader<'a> {
|
||||||
fn new(size: BodySize, reader: Box<dyn Read + 'static>) -> Self {
|
fn new(size: BodySize, reader: Box<dyn Read + 'a>) -> Self {
|
||||||
SizedReader { size, reader }
|
SizedReader { size, reader }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Payload {
|
impl<'a> Payload<'a> {
|
||||||
pub fn into_read(self) -> SizedReader {
|
pub fn into_read(self) -> SizedReader<'a> {
|
||||||
match self {
|
match self {
|
||||||
Payload::Empty => SizedReader::new(BodySize::Empty, Box::new(empty())),
|
Payload::Empty => SizedReader::new(BodySize::Empty, Box::new(empty())),
|
||||||
Payload::Text(text, _charset) => {
|
Payload::Text(text, _charset) => {
|
||||||
@@ -86,7 +86,7 @@ impl Payload {
|
|||||||
encoding.encode(&text, EncoderTrap::Replace).unwrap()
|
encoding.encode(&text, EncoderTrap::Replace).unwrap()
|
||||||
};
|
};
|
||||||
#[cfg(not(feature = "charset"))]
|
#[cfg(not(feature = "charset"))]
|
||||||
let bytes = text.into_bytes();
|
let bytes = text.as_bytes();
|
||||||
let len = bytes.len();
|
let len = bytes.len();
|
||||||
let cursor = Cursor::new(bytes);
|
let cursor = Cursor::new(bytes);
|
||||||
SizedReader::new(BodySize::Known(len as u64), Box::new(cursor))
|
SizedReader::new(BodySize::Known(len as u64), Box::new(cursor))
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
use cookie_store::CookieStore;
|
use cookie_store::CookieStore;
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
#[derive(Default, Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct CookieTin {
|
pub(crate) struct CookieTin {
|
||||||
inner: RwLock<CookieStore>,
|
inner: RwLock<CookieStore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
impl CookieTin {
|
impl CookieTin {
|
||||||
pub(crate) fn new(store: CookieStore) -> Self {
|
pub(crate) fn new(store: CookieStore) -> Self {
|
||||||
CookieTin {
|
CookieTin {
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ pub enum Error {
|
|||||||
/// Read the inner response body for details and to return
|
/// Read the inner response body for details and to return
|
||||||
/// the connection to the pool.
|
/// the connection to the pool.
|
||||||
HTTP(Box<Response>),
|
HTTP(Box<Response>),
|
||||||
/// TLS Error
|
|
||||||
#[cfg(feature = "native-tls")]
|
|
||||||
TlsError(native_tls::Error),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
@@ -70,8 +67,6 @@ impl fmt::Display for Error {
|
|||||||
Error::ProxyConnect => write!(f, "Proxy failed to connect"),
|
Error::ProxyConnect => write!(f, "Proxy failed to connect"),
|
||||||
Error::InvalidProxyCreds => write!(f, "Provided proxy credentials are incorrect"),
|
Error::InvalidProxyCreds => write!(f, "Provided proxy credentials are incorrect"),
|
||||||
Error::HTTP(response) => write!(f, "HTTP status {}", response.status()),
|
Error::HTTP(response) => write!(f, "HTTP status {}", response.status()),
|
||||||
#[cfg(feature = "native-tls")]
|
|
||||||
Error::TlsError(err) => write!(f, "TLS Error: {}", err),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/lib.rs
21
src/lib.rs
@@ -130,15 +130,20 @@ pub use crate::resolve::Resolver;
|
|||||||
pub use crate::response::Response;
|
pub use crate::response::Response;
|
||||||
|
|
||||||
// re-export
|
// re-export
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
pub use cookie::Cookie;
|
pub use cookie::Cookie;
|
||||||
#[cfg(feature = "json")]
|
#[cfg(feature = "json")]
|
||||||
pub use serde_json::{to_value as serde_to_value, Map as SerdeMap, Value as SerdeValue};
|
pub use serde_json::{to_value as serde_to_value, Map as SerdeMap, Value as SerdeValue};
|
||||||
|
|
||||||
|
/// Creates an agent builder.
|
||||||
|
pub fn builder() -> AgentBuilder {
|
||||||
|
AgentBuilder::new()
|
||||||
|
}
|
||||||
|
|
||||||
/// Agents are used to keep state between requests.
|
/// Agents are used to keep state between requests.
|
||||||
pub fn agent() -> Agent {
|
pub fn agent() -> Agent {
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
return Agent::default();
|
return AgentBuilder::new().build();
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
return test::test_agent();
|
return test::test_agent();
|
||||||
}
|
}
|
||||||
@@ -198,7 +203,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn connect_http_google() {
|
fn connect_http_google() {
|
||||||
let resp = get("http://www.google.com/").call().unwrap();
|
let agent = Agent::new();
|
||||||
|
|
||||||
|
let resp = agent.get("http://www.google.com/").call().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"text/html; charset=ISO-8859-1",
|
"text/html; charset=ISO-8859-1",
|
||||||
resp.header("content-type").unwrap()
|
resp.header("content-type").unwrap()
|
||||||
@@ -207,9 +214,11 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(any(feature = "tls", feature = "native-tls"))]
|
#[cfg(feature = "tls")]
|
||||||
fn connect_https_google() {
|
fn connect_https_google() {
|
||||||
let resp = get("https://www.google.com/").call().unwrap();
|
let agent = Agent::new();
|
||||||
|
|
||||||
|
let resp = agent.get("https://www.google.com/").call().unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"text/html; charset=ISO-8859-1",
|
"text/html; charset=ISO-8859-1",
|
||||||
resp.header("content-type").unwrap()
|
resp.header("content-type").unwrap()
|
||||||
@@ -218,7 +227,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(any(feature = "tls", feature = "native-tls"))]
|
#[cfg(feature = "tls")]
|
||||||
fn connect_https_invalid_name() {
|
fn connect_https_invalid_name() {
|
||||||
let result = get("https://example.com{REQUEST_URI}/").call();
|
let result = get("https://example.com{REQUEST_URI}/").call();
|
||||||
assert!(matches!(result.unwrap_err(), Error::DnsFailed(_)));
|
assert!(matches!(result.unwrap_err(), Error::DnsFailed(_)));
|
||||||
|
|||||||
56
src/pool.rs
56
src/pool.rs
@@ -9,8 +9,8 @@ use crate::Proxy;
|
|||||||
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
const DEFAULT_MAX_IDLE_CONNECTIONS: usize = 100;
|
pub const DEFAULT_MAX_IDLE_CONNECTIONS: usize = 100;
|
||||||
const DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST: usize = 1;
|
pub const DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST: usize = 1;
|
||||||
|
|
||||||
/// Holder of recycled connections.
|
/// Holder of recycled connections.
|
||||||
///
|
///
|
||||||
@@ -74,25 +74,23 @@ fn remove_last_match(list: &mut VecDeque<PoolKey>, key: &PoolKey) -> Option<Pool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ConnectionPool {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
max_idle_connections: DEFAULT_MAX_IDLE_CONNECTIONS,
|
|
||||||
max_idle_connections_per_host: DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST,
|
|
||||||
inner: Mutex::new(Inner {
|
|
||||||
recycle: HashMap::default(),
|
|
||||||
lru: VecDeque::default(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectionPool {
|
impl ConnectionPool {
|
||||||
pub(crate) fn new(max_idle_connections: usize, max_idle_connections_per_host: usize) -> Self {
|
#[cfg(test)]
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self::new_with_limits(
|
||||||
|
DEFAULT_MAX_IDLE_CONNECTIONS,
|
||||||
|
DEFAULT_MAX_IDLE_CONNECTIONS_PER_HOST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_with_limits(
|
||||||
|
max_idle_connections: usize,
|
||||||
|
max_idle_connections_per_host: usize,
|
||||||
|
) -> Self {
|
||||||
ConnectionPool {
|
ConnectionPool {
|
||||||
inner: Mutex::new(Inner {
|
inner: Mutex::new(Inner {
|
||||||
recycle: HashMap::default(),
|
recycle: HashMap::new(),
|
||||||
lru: VecDeque::default(),
|
lru: VecDeque::new(),
|
||||||
}),
|
}),
|
||||||
max_idle_connections,
|
max_idle_connections,
|
||||||
max_idle_connections_per_host,
|
max_idle_connections_per_host,
|
||||||
@@ -105,7 +103,7 @@ impl ConnectionPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// How the unit::connect tries to get a pooled connection.
|
/// How the unit::connect tries to get a pooled connection.
|
||||||
pub fn try_get_connection(&self, url: &Url, proxy: &Option<Proxy>) -> Option<Stream> {
|
pub fn try_get_connection(&self, url: &Url, proxy: Option<Proxy>) -> Option<Stream> {
|
||||||
let key = PoolKey::new(url, proxy);
|
let key = PoolKey::new(url, proxy);
|
||||||
self.remove(&key)
|
self.remove(&key)
|
||||||
}
|
}
|
||||||
@@ -211,13 +209,13 @@ impl fmt::Debug for PoolKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PoolKey {
|
impl PoolKey {
|
||||||
fn new(url: &Url, proxy: &Option<Proxy>) -> Self {
|
fn new(url: &Url, proxy: Option<Proxy>) -> Self {
|
||||||
let port = url.port_or_known_default();
|
let port = url.port_or_known_default();
|
||||||
PoolKey {
|
PoolKey {
|
||||||
scheme: url.scheme().to_string(),
|
scheme: url.scheme().to_string(),
|
||||||
hostname: url.host_str().unwrap_or("").to_string(),
|
hostname: url.host_str().unwrap_or("").to_string(),
|
||||||
port,
|
port,
|
||||||
proxy: proxy.clone(),
|
proxy: proxy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,7 +223,7 @@ impl PoolKey {
|
|||||||
#[test]
|
#[test]
|
||||||
fn poolkey_new() {
|
fn poolkey_new() {
|
||||||
// Test that PoolKey::new() does not panic on unrecognized schemes.
|
// Test that PoolKey::new() does not panic on unrecognized schemes.
|
||||||
PoolKey::new(&Url::parse("zzz:///example.com").unwrap(), &None);
|
PoolKey::new(&Url::parse("zzz:///example.com").unwrap(), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -233,7 +231,7 @@ fn pool_connections_limit() {
|
|||||||
// Test inserting connections with different keys into the pool,
|
// Test inserting connections with different keys into the pool,
|
||||||
// filling and draining it. The pool should evict earlier connections
|
// filling and draining it. The pool should evict earlier connections
|
||||||
// when the connection limit is reached.
|
// when the connection limit is reached.
|
||||||
let pool = ConnectionPool::default();
|
let pool = ConnectionPool::new();
|
||||||
let hostnames = (0..DEFAULT_MAX_IDLE_CONNECTIONS * 2).map(|i| format!("{}.example", i));
|
let hostnames = (0..DEFAULT_MAX_IDLE_CONNECTIONS * 2).map(|i| format!("{}.example", i));
|
||||||
let poolkeys = hostnames.map(|hostname| PoolKey {
|
let poolkeys = hostnames.map(|hostname| PoolKey {
|
||||||
scheme: "https".to_string(),
|
scheme: "https".to_string(),
|
||||||
@@ -258,7 +256,7 @@ fn pool_per_host_connections_limit() {
|
|||||||
// Test inserting connections with the same key into the pool,
|
// Test inserting connections with the same key into the pool,
|
||||||
// filling and draining it. The pool should evict earlier connections
|
// filling and draining it. The pool should evict earlier connections
|
||||||
// when the per-host connection limit is reached.
|
// when the per-host connection limit is reached.
|
||||||
let pool = ConnectionPool::default();
|
let pool = ConnectionPool::new();
|
||||||
let poolkey = PoolKey {
|
let poolkey = PoolKey {
|
||||||
scheme: "https".to_string(),
|
scheme: "https".to_string(),
|
||||||
hostname: "example.com".to_string(),
|
hostname: "example.com".to_string(),
|
||||||
@@ -285,17 +283,17 @@ fn pool_per_host_connections_limit() {
|
|||||||
fn pool_checks_proxy() {
|
fn pool_checks_proxy() {
|
||||||
// Test inserting different poolkeys with same address but different proxies.
|
// Test inserting different poolkeys with same address but different proxies.
|
||||||
// Each insertion should result in an additional entry in the pool.
|
// Each insertion should result in an additional entry in the pool.
|
||||||
let pool = ConnectionPool::default();
|
let pool = ConnectionPool::new();
|
||||||
let url = Url::parse("zzz:///example.com").unwrap();
|
let url = Url::parse("zzz:///example.com").unwrap();
|
||||||
|
|
||||||
pool.add(
|
pool.add(
|
||||||
PoolKey::new(&url, &None),
|
PoolKey::new(&url, None),
|
||||||
Stream::Cursor(std::io::Cursor::new(vec![])),
|
Stream::Cursor(std::io::Cursor::new(vec![])),
|
||||||
);
|
);
|
||||||
assert_eq!(pool.len(), 1);
|
assert_eq!(pool.len(), 1);
|
||||||
|
|
||||||
pool.add(
|
pool.add(
|
||||||
PoolKey::new(&url, &Some(Proxy::new("localhost:9999").unwrap())),
|
PoolKey::new(&url, Some(Proxy::new("localhost:9999").unwrap())),
|
||||||
Stream::Cursor(std::io::Cursor::new(vec![])),
|
Stream::Cursor(std::io::Cursor::new(vec![])),
|
||||||
);
|
);
|
||||||
assert_eq!(pool.len(), 2);
|
assert_eq!(pool.len(), 2);
|
||||||
@@ -303,7 +301,7 @@ fn pool_checks_proxy() {
|
|||||||
pool.add(
|
pool.add(
|
||||||
PoolKey::new(
|
PoolKey::new(
|
||||||
&url,
|
&url,
|
||||||
&Some(Proxy::new("user:password@localhost:9999").unwrap()),
|
Some(Proxy::new("user:password@localhost:9999").unwrap()),
|
||||||
),
|
),
|
||||||
Stream::Cursor(std::io::Cursor::new(vec![])),
|
Stream::Cursor(std::io::Cursor::new(vec![])),
|
||||||
);
|
);
|
||||||
@@ -343,7 +341,7 @@ impl<R: Read + Sized + Into<Stream>> PoolReturnRead<R> {
|
|||||||
stream.reset()?;
|
stream.reset()?;
|
||||||
|
|
||||||
// insert back into pool
|
// insert back into pool
|
||||||
let key = PoolKey::new(&unit.url, &unit.req.proxy);
|
let key = PoolKey::new(&unit.url, unit.req.proxy());
|
||||||
unit.req.agent.state.pool.add(key, stream);
|
unit.req.agent.state.pool.add(key, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
330
src/request.rs
330
src/request.rs
@@ -1,13 +1,10 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
#[cfg(any(feature = "native-tls", feature = "tls"))]
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time;
|
|
||||||
|
|
||||||
use qstring::QString;
|
use qstring::QString;
|
||||||
use url::{form_urlencoded, Url};
|
use url::{form_urlencoded, Url};
|
||||||
|
|
||||||
use crate::agent::{self, Agent};
|
use crate::agent::Agent;
|
||||||
use crate::body::BodySize;
|
use crate::body::BodySize;
|
||||||
use crate::body::{Payload, SizedReader};
|
use crate::body::{Payload, SizedReader};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
@@ -30,28 +27,14 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
/// .query("foo", "bar baz") // add ?foo=bar%20baz
|
/// .query("foo", "bar baz") // add ?foo=bar%20baz
|
||||||
/// .call(); // run the request
|
/// .call(); // run the request
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
pub(crate) agent: Agent,
|
pub(crate) agent: Agent,
|
||||||
|
|
||||||
// via agent
|
|
||||||
pub(crate) method: String,
|
pub(crate) method: String,
|
||||||
url: String,
|
url: String,
|
||||||
|
|
||||||
// from request itself
|
|
||||||
return_error_for_status: bool,
|
return_error_for_status: bool,
|
||||||
pub(crate) headers: Vec<Header>,
|
pub(crate) headers: Vec<Header>,
|
||||||
pub(crate) query: QString,
|
pub(crate) query: QString,
|
||||||
pub(crate) timeout_connect: Option<time::Duration>,
|
|
||||||
pub(crate) timeout_read: Option<time::Duration>,
|
|
||||||
pub(crate) timeout_write: Option<time::Duration>,
|
|
||||||
pub(crate) timeout: Option<time::Duration>,
|
|
||||||
pub(crate) redirects: u32,
|
|
||||||
pub(crate) proxy: Option<Proxy>,
|
|
||||||
#[cfg(feature = "tls")]
|
|
||||||
pub(crate) tls_config: Option<TLSClientConfig>,
|
|
||||||
#[cfg(all(feature = "native-tls", not(feature = "tls")))]
|
|
||||||
pub(crate) tls_connector: Option<TLSConnector>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Request {
|
impl fmt::Debug for Request {
|
||||||
@@ -72,43 +55,32 @@ impl fmt::Debug for Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Request {
|
impl Request {
|
||||||
pub(crate) fn new(agent: &Agent, method: String, url: String) -> Request {
|
pub(crate) fn new(agent: Agent, method: String, url: String) -> Request {
|
||||||
|
let headers = agent.headers.clone();
|
||||||
Request {
|
Request {
|
||||||
agent: agent.clone(),
|
agent,
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
headers: agent.headers.clone(),
|
headers,
|
||||||
redirects: 5,
|
|
||||||
return_error_for_status: true,
|
return_error_for_status: true,
|
||||||
..Default::default()
|
query: QString::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// "Builds" this request which is effectively the same as cloning.
|
|
||||||
/// This is needed when we use a chain of request builders, but
|
|
||||||
/// don't want to send the request at the end of the chain.
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// let r = ureq::get("/my_page")
|
|
||||||
/// .set("X-Foo-Bar", "Baz")
|
|
||||||
/// .build();
|
|
||||||
/// ```
|
|
||||||
pub fn build(&self) -> Request {
|
|
||||||
self.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Executes the request and blocks the caller until done.
|
/// Executes the request and blocks the caller until done.
|
||||||
///
|
///
|
||||||
/// Use `.timeout_connect()` and `.timeout_read()` to avoid blocking forever.
|
/// Use `.timeout_connect()` and `.timeout_read()` to avoid blocking forever.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let r = ureq::get("/my_page")
|
/// let r = ureq::builder()
|
||||||
/// .timeout_connect(std::time::Duration::from_secs(10)) // max 10 seconds
|
/// .timeout_connect(std::time::Duration::from_secs(10)) // max 10 seconds
|
||||||
|
/// .build()
|
||||||
|
/// .get("/my_page")
|
||||||
/// .call();
|
/// .call();
|
||||||
///
|
///
|
||||||
/// println!("{:?}", r);
|
/// println!("{:?}", r);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn call(&mut self) -> Result<Response> {
|
pub fn call(self) -> Result<Response> {
|
||||||
self.do_call(Payload::Empty)
|
self.do_call(Payload::Empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,11 +118,12 @@ impl Request {
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg(feature = "json")]
|
#[cfg(feature = "json")]
|
||||||
pub fn send_json(&mut self, data: SerdeValue) -> Result<Response> {
|
pub fn send_json(self, data: SerdeValue) -> Result<Response> {
|
||||||
if self.header("Content-Type").is_none() {
|
let mut this = self;
|
||||||
self.set("Content-Type", "application/json");
|
if this.header("Content-Type").is_none() {
|
||||||
|
this = this.set("Content-Type", "application/json");
|
||||||
}
|
}
|
||||||
self.do_call(Payload::JSON(data))
|
this.do_call(Payload::JSON(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send data as bytes.
|
/// Send data as bytes.
|
||||||
@@ -163,8 +136,8 @@ impl Request {
|
|||||||
/// .send_bytes(body);
|
/// .send_bytes(body);
|
||||||
/// println!("{:?}", r);
|
/// println!("{:?}", r);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn send_bytes(&mut self, data: &[u8]) -> Result<Response> {
|
pub fn send_bytes(self, data: &[u8]) -> Result<Response> {
|
||||||
self.do_call(Payload::Bytes(data.to_owned()))
|
self.do_call(Payload::Bytes(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send data as a string.
|
/// Send data as a string.
|
||||||
@@ -188,11 +161,10 @@ impl Request {
|
|||||||
/// .send_string("Hällo Wörld!");
|
/// .send_string("Hällo Wörld!");
|
||||||
/// println!("{:?}", r);
|
/// println!("{:?}", r);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn send_string(&mut self, data: &str) -> Result<Response> {
|
pub fn send_string(self, data: &str) -> Result<Response> {
|
||||||
let text = data.into();
|
|
||||||
let charset =
|
let charset =
|
||||||
crate::response::charset_from_content_type(self.header("content-type")).to_string();
|
crate::response::charset_from_content_type(self.header("content-type")).to_string();
|
||||||
self.do_call(Payload::Text(text, charset))
|
self.do_call(Payload::Text(data, charset))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a sequence of (key, value) pairs as form-urlencoded data.
|
/// Send a sequence of (key, value) pairs as form-urlencoded data.
|
||||||
@@ -210,14 +182,15 @@ impl Request {
|
|||||||
/// println!("{:?}", r);
|
/// println!("{:?}", r);
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn send_form(&mut self, data: &[(&str, &str)]) -> Result<Response> {
|
pub fn send_form(self, data: &[(&str, &str)]) -> Result<Response> {
|
||||||
if self.header("Content-Type").is_none() {
|
let mut this = self;
|
||||||
self.set("Content-Type", "application/x-www-form-urlencoded");
|
if this.header("Content-Type").is_none() {
|
||||||
|
this = this.set("Content-Type", "application/x-www-form-urlencoded");
|
||||||
}
|
}
|
||||||
let encoded = form_urlencoded::Serializer::new(String::new())
|
let encoded = form_urlencoded::Serializer::new(String::new())
|
||||||
.extend_pairs(data)
|
.extend_pairs(data)
|
||||||
.finish();
|
.finish();
|
||||||
self.do_call(Payload::Bytes(encoded.into_bytes()))
|
this.do_call(Payload::Bytes(&encoded.into_bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send data from a reader.
|
/// Send data from a reader.
|
||||||
@@ -238,7 +211,7 @@ impl Request {
|
|||||||
/// .set("Content-Type", "text/plain")
|
/// .set("Content-Type", "text/plain")
|
||||||
/// .send(read);
|
/// .send(read);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn send(&mut self, reader: impl Read + 'static) -> Result<Response> {
|
pub fn send(self, reader: impl Read) -> Result<Response> {
|
||||||
self.do_call(Payload::Reader(Box::new(reader)))
|
self.do_call(Payload::Reader(Box::new(reader)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +229,7 @@ impl Request {
|
|||||||
/// println!("Oh no error!");
|
/// println!("Oh no error!");
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn set(&mut self, header: &str, value: &str) -> &mut Request {
|
pub fn set(mut self, header: &str, value: &str) -> Self {
|
||||||
header::add_header(&mut self.headers, Header::new(header, value));
|
header::add_header(&mut self.headers, Header::new(header, value));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -265,8 +238,7 @@ impl Request {
|
|||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let req = ureq::get("/my_page")
|
/// let req = ureq::get("/my_page")
|
||||||
/// .set("X-API-Key", "foobar")
|
/// .set("X-API-Key", "foobar");
|
||||||
/// .build();
|
|
||||||
/// assert_eq!("foobar", req.header("x-api-Key").unwrap());
|
/// assert_eq!("foobar", req.header("x-api-Key").unwrap());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn header(&self, name: &str) -> Option<&str> {
|
pub fn header(&self, name: &str) -> Option<&str> {
|
||||||
@@ -278,8 +250,7 @@ impl Request {
|
|||||||
/// ```
|
/// ```
|
||||||
/// let req = ureq::get("/my_page")
|
/// let req = ureq::get("/my_page")
|
||||||
/// .set("X-API-Key", "foobar")
|
/// .set("X-API-Key", "foobar")
|
||||||
/// .set("Content-Type", "application/json")
|
/// .set("Content-Type", "application/json");
|
||||||
/// .build();
|
|
||||||
/// assert_eq!(req.header_names(), vec!["x-api-key", "content-type"]);
|
/// assert_eq!(req.header_names(), vec!["x-api-key", "content-type"]);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn header_names(&self) -> Vec<String> {
|
pub fn header_names(&self) -> Vec<String> {
|
||||||
@@ -293,8 +264,7 @@ impl Request {
|
|||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// let req = ureq::get("/my_page")
|
/// let req = ureq::get("/my_page")
|
||||||
/// .set("X-API-Key", "foobar")
|
/// .set("X-API-Key", "foobar");
|
||||||
/// .build();
|
|
||||||
/// assert_eq!(true, req.has("x-api-Key"));
|
/// assert_eq!(true, req.has("x-api-Key"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn has(&self, name: &str) -> bool {
|
pub fn has(&self, name: &str) -> bool {
|
||||||
@@ -306,8 +276,8 @@ impl Request {
|
|||||||
/// ```
|
/// ```
|
||||||
/// let req = ureq::get("/my_page")
|
/// let req = ureq::get("/my_page")
|
||||||
/// .set("X-Forwarded-For", "1.2.3.4")
|
/// .set("X-Forwarded-For", "1.2.3.4")
|
||||||
/// .set("X-Forwarded-For", "2.3.4.5")
|
/// .set("X-Forwarded-For", "2.3.4.5");
|
||||||
/// .build();
|
///
|
||||||
/// assert_eq!(req.all("x-forwarded-for"), vec.
|
|
||||||
///
|
|
||||||
/// See [`ClientConfig`](https://docs.rs/rustls/latest/rustls/struct.ClientConfig.html).
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```
|
|
||||||
/// let tls_config = std::sync::Arc::new(rustls::ClientConfig::new());
|
|
||||||
/// let req = ureq::post("https://cool.server")
|
|
||||||
/// .set_tls_config(tls_config.clone());
|
|
||||||
/// ```
|
|
||||||
#[cfg(feature = "tls")]
|
|
||||||
pub fn set_tls_config(&mut self, tls_config: Arc<rustls::ClientConfig>) -> &mut Request {
|
|
||||||
self.tls_config = Some(TLSClientConfig(tls_config));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the TLS connector that will be used for the connection.
|
|
||||||
///
|
|
||||||
/// Example:
|
|
||||||
/// ```
|
|
||||||
/// let tls_connector = std::sync::Arc::new(native_tls::TlsConnector::new().unwrap());
|
|
||||||
/// let req = ureq::post("https://cool.server")
|
|
||||||
/// .set_tls_connector(tls_connector.clone());
|
|
||||||
/// ```
|
|
||||||
#[cfg(all(feature = "native-tls", not(feature = "tls")))]
|
|
||||||
pub fn set_tls_connector(
|
|
||||||
&mut self,
|
|
||||||
tls_connector: Arc<native_tls::TlsConnector>,
|
|
||||||
) -> &mut Request {
|
|
||||||
self.tls_connector = Some(TLSConnector(tls_connector));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if this request, with the provided body, is retryable.
|
// Returns true if this request, with the provided body, is retryable.
|
||||||
pub(crate) fn is_retryable(&self, body: &SizedReader) -> bool {
|
pub(crate) fn is_retryable(&self, body: &SizedReader) -> bool {
|
||||||
// Per https://tools.ietf.org/html/rfc7231#section-8.1.3
|
// Per https://tools.ietf.org/html/rfc7231#section-8.1.3
|
||||||
@@ -660,32 +453,10 @@ impl Request {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "tls")]
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct TLSClientConfig(pub(crate) Arc<rustls::ClientConfig>);
|
|
||||||
|
|
||||||
#[cfg(feature = "tls")]
|
|
||||||
impl fmt::Debug for TLSClientConfig {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("TLSClientConfig").finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(feature = "native-tls", not(feature = "tls")))]
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct TLSConnector(pub(crate) Arc<native_tls::TlsConnector>);
|
|
||||||
|
|
||||||
#[cfg(all(feature = "native-tls", not(feature = "tls")))]
|
|
||||||
impl fmt::Debug for TLSConnector {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("TLSConnector").finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_hostname() {
|
fn no_hostname() {
|
||||||
let req = Request::new(
|
let req = Request::new(
|
||||||
&Agent::default(),
|
Agent::new(),
|
||||||
"GET".to_string(),
|
"GET".to_string(),
|
||||||
"unix:/run/foo.socket".to_string(),
|
"unix:/run/foo.socket".to_string(),
|
||||||
);
|
);
|
||||||
@@ -695,13 +466,22 @@ fn no_hostname() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn request_implements_send_and_sync() {
|
fn request_implements_send_and_sync() {
|
||||||
let _request: Box<dyn Send> = Box::new(Request::new(
|
let _request: Box<dyn Send> = Box::new(Request::new(
|
||||||
&Agent::default(),
|
Agent::new(),
|
||||||
"GET".to_string(),
|
"GET".to_string(),
|
||||||
"https://example.com/".to_string(),
|
"https://example.com/".to_string(),
|
||||||
));
|
));
|
||||||
let _request: Box<dyn Sync> = Box::new(Request::new(
|
let _request: Box<dyn Sync> = Box::new(Request::new(
|
||||||
&Agent::default(),
|
Agent::new(),
|
||||||
"GET".to_string(),
|
"GET".to_string(),
|
||||||
"https://example.com/".to_string(),
|
"https://example.com/".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn send_byte_slice() {
|
||||||
|
let bytes = vec![1, 2, 3];
|
||||||
|
crate::agent()
|
||||||
|
.post("http://example.com")
|
||||||
|
.send(&bytes[1..2])
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,9 +51,3 @@ impl std::ops::Deref for ArcResolver {
|
|||||||
self.0.as_ref()
|
self.0.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ArcResolver {
|
|
||||||
fn default() -> Self {
|
|
||||||
StdResolver.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::{self, Cursor, ErrorKind, Read};
|
use std::io::{self, Cursor, ErrorKind, Read};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use chunked_transfer::Decoder as ChunkDecoder;
|
use chunked_transfer::Decoder as ChunkDecoder;
|
||||||
|
|
||||||
@@ -47,7 +46,6 @@ pub struct Response {
|
|||||||
headers: Vec<Header>,
|
headers: Vec<Header>,
|
||||||
unit: Option<Unit>,
|
unit: Option<Unit>,
|
||||||
stream: Option<Stream>,
|
stream: Option<Stream>,
|
||||||
deadline: Option<Instant>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// index into status_line where we split: HTTP/1.1 200 OK
|
/// index into status_line where we split: HTTP/1.1 200 OK
|
||||||
@@ -189,7 +187,7 @@ impl Response {
|
|||||||
.unwrap_or(DEFAULT_CONTENT_TYPE)
|
.unwrap_or(DEFAULT_CONTENT_TYPE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The character set part of the "Content-Type" header.native_tls
|
/// The character set part of the "Content-Type".
|
||||||
///
|
///
|
||||||
/// Example:
|
/// Example:
|
||||||
///
|
///
|
||||||
@@ -267,12 +265,17 @@ impl Response {
|
|||||||
|
|
||||||
let stream = self.stream.expect("No reader in response?!");
|
let stream = self.stream.expect("No reader in response?!");
|
||||||
let unit = self.unit;
|
let unit = self.unit;
|
||||||
|
if let Some(unit) = &unit {
|
||||||
|
let result = stream.set_read_timeout(unit.req.agent.config.timeout_read);
|
||||||
|
if let Err(e) = result {
|
||||||
|
return Box::new(ErrorReader(e)) as Box<dyn Read + Send>;
|
||||||
|
}
|
||||||
|
}
|
||||||
let deadline = unit.as_ref().and_then(|u| u.deadline);
|
let deadline = unit.as_ref().and_then(|u| u.deadline);
|
||||||
let stream = DeadlineStream::new(stream, deadline);
|
let stream = DeadlineStream::new(stream, deadline);
|
||||||
|
|
||||||
match (use_chunked, limit_bytes) {
|
match (use_chunked, limit_bytes) {
|
||||||
(true, _) => Box::new(PoolReturnRead::new(unit, ChunkDecoder::new(stream)))
|
(true, _) => Box::new(PoolReturnRead::new(unit, ChunkDecoder::new(stream))),
|
||||||
as Box<dyn Read + Send>,
|
|
||||||
(false, Some(len)) => {
|
(false, Some(len)) => {
|
||||||
Box::new(PoolReturnRead::new(unit, LimitedRead::new(stream, len)))
|
Box::new(PoolReturnRead::new(unit, LimitedRead::new(stream, len)))
|
||||||
}
|
}
|
||||||
@@ -438,7 +441,6 @@ impl Response {
|
|||||||
headers,
|
headers,
|
||||||
unit: None,
|
unit: None,
|
||||||
stream: None,
|
stream: None,
|
||||||
deadline: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,9 +509,6 @@ impl FromStr for Response {
|
|||||||
/// *Internal API*
|
/// *Internal API*
|
||||||
pub(crate) fn set_stream(resp: &mut Response, url: String, unit: Option<Unit>, stream: Stream) {
|
pub(crate) fn set_stream(resp: &mut Response, url: String, unit: Option<Unit>, stream: Stream) {
|
||||||
resp.url = Some(url);
|
resp.url = Some(url);
|
||||||
if let Some(unit) = &unit {
|
|
||||||
resp.deadline = unit.deadline;
|
|
||||||
}
|
|
||||||
resp.unit = unit;
|
resp.unit = unit;
|
||||||
resp.stream = Some(stream);
|
resp.stream = Some(stream);
|
||||||
}
|
}
|
||||||
@@ -730,3 +729,14 @@ mod tests {
|
|||||||
assert!(matches!(err, Error::BadStatus));
|
assert!(matches!(err, Error::BadStatus));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorReader returns an error for every read.
|
||||||
|
// The error is as close to a clone of the underlying
|
||||||
|
// io::Error as we can get.
|
||||||
|
struct ErrorReader(io::Error);
|
||||||
|
|
||||||
|
impl Read for ErrorReader {
|
||||||
|
fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
Err(io::Error::new(self.0.kind(), self.0.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
119
src/stream.rs
119
src/stream.rs
@@ -15,9 +15,6 @@ use rustls::StreamOwned;
|
|||||||
#[cfg(feature = "socks-proxy")]
|
#[cfg(feature = "socks-proxy")]
|
||||||
use socks::{TargetAddr, ToTargetAddr};
|
use socks::{TargetAddr, ToTargetAddr};
|
||||||
|
|
||||||
#[cfg(feature = "native-tls")]
|
|
||||||
use native_tls::{HandshakeError, TlsStream};
|
|
||||||
|
|
||||||
use crate::proxy::Proto;
|
use crate::proxy::Proto;
|
||||||
use crate::proxy::Proxy;
|
use crate::proxy::Proxy;
|
||||||
|
|
||||||
@@ -27,10 +24,8 @@ use crate::unit::Unit;
|
|||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
pub enum Stream {
|
pub enum Stream {
|
||||||
Http(BufReader<TcpStream>),
|
Http(BufReader<TcpStream>),
|
||||||
#[cfg(all(feature = "tls", not(feature = "native-tls")))]
|
#[cfg(feature = "tls")]
|
||||||
Https(BufReader<rustls::StreamOwned<rustls::ClientSession, TcpStream>>),
|
Https(BufReader<rustls::StreamOwned<rustls::ClientSession, TcpStream>>),
|
||||||
#[cfg(all(feature = "native-tls", not(feature = "tls")))]
|
|
||||||
Https(BufReader<TlsStream<TcpStream>>),
|
|
||||||
Cursor(Cursor<Vec<u8>>),
|
Cursor(Cursor<Vec<u8>>),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
Test(Box<dyn BufRead + Send + Sync>, Vec<u8>),
|
Test(Box<dyn BufRead + Send + Sync>, Vec<u8>),
|
||||||
@@ -101,10 +96,7 @@ impl fmt::Debug for Stream {
|
|||||||
"Stream[{}]",
|
"Stream[{}]",
|
||||||
match self {
|
match self {
|
||||||
Stream::Http(_) => "http",
|
Stream::Http(_) => "http",
|
||||||
#[cfg(any(
|
#[cfg(feature = "tls")]
|
||||||
all(feature = "tls", not(feature = "native-tls")),
|
|
||||||
all(feature = "native-tls", not(feature = "tls")),
|
|
||||||
))]
|
|
||||||
Stream::Https(_) => "https",
|
Stream::Https(_) => "https",
|
||||||
Stream::Cursor(_) => "cursor",
|
Stream::Cursor(_) => "cursor",
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -144,10 +136,7 @@ impl Stream {
|
|||||||
pub fn is_poolable(&self) -> bool {
|
pub fn is_poolable(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Stream::Http(_) => true,
|
Stream::Http(_) => true,
|
||||||
#[cfg(any(
|
#[cfg(feature = "tls")]
|
||||||
all(feature = "tls", not(feature = "native-tls")),
|
|
||||||
all(feature = "native-tls", not(feature = "tls")),
|
|
||||||
))]
|
|
||||||
Stream::Https(_) => true,
|
Stream::Https(_) => true,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@@ -173,6 +162,14 @@ impl Stream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
|
||||||
|
if let Some(socket) = self.socket() {
|
||||||
|
socket.set_read_timeout(timeout)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn to_write_vec(&self) -> Vec<u8> {
|
pub fn to_write_vec(&self) -> Vec<u8> {
|
||||||
match self {
|
match self {
|
||||||
@@ -186,10 +183,7 @@ impl Read for Stream {
|
|||||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
Stream::Http(sock) => sock.read(buf),
|
Stream::Http(sock) => sock.read(buf),
|
||||||
#[cfg(any(
|
#[cfg(feature = "tls")]
|
||||||
all(feature = "tls", not(feature = "native-tls")),
|
|
||||||
all(feature = "native-tls", not(feature = "tls")),
|
|
||||||
))]
|
|
||||||
Stream::Https(stream) => read_https(stream, buf),
|
Stream::Https(stream) => read_https(stream, buf),
|
||||||
Stream::Cursor(read) => read.read(buf),
|
Stream::Cursor(read) => read.read(buf),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -202,10 +196,7 @@ impl BufRead for Stream {
|
|||||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||||
match self {
|
match self {
|
||||||
Stream::Http(r) => r.fill_buf(),
|
Stream::Http(r) => r.fill_buf(),
|
||||||
#[cfg(any(
|
#[cfg(feature = "tls")]
|
||||||
all(feature = "tls", not(feature = "native-tls")),
|
|
||||||
all(feature = "native-tls", not(feature = "tls")),
|
|
||||||
))]
|
|
||||||
Stream::Https(r) => r.fill_buf(),
|
Stream::Https(r) => r.fill_buf(),
|
||||||
Stream::Cursor(r) => r.fill_buf(),
|
Stream::Cursor(r) => r.fill_buf(),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -216,10 +207,7 @@ impl BufRead for Stream {
|
|||||||
fn consume(&mut self, amt: usize) {
|
fn consume(&mut self, amt: usize) {
|
||||||
match self {
|
match self {
|
||||||
Stream::Http(r) => r.consume(amt),
|
Stream::Http(r) => r.consume(amt),
|
||||||
#[cfg(any(
|
#[cfg(feature = "tls")]
|
||||||
all(feature = "tls", not(feature = "native-tls")),
|
|
||||||
all(feature = "native-tls", not(feature = "tls")),
|
|
||||||
))]
|
|
||||||
Stream::Https(r) => r.consume(amt),
|
Stream::Https(r) => r.consume(amt),
|
||||||
Stream::Cursor(r) => r.consume(amt),
|
Stream::Cursor(r) => r.consume(amt),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -238,7 +226,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "tls", not(feature = "native-tls")))]
|
#[cfg(feature = "tls")]
|
||||||
fn read_https(
|
fn read_https(
|
||||||
stream: &mut BufReader<StreamOwned<ClientSession, TcpStream>>,
|
stream: &mut BufReader<StreamOwned<ClientSession, TcpStream>>,
|
||||||
buf: &mut [u8],
|
buf: &mut [u8],
|
||||||
@@ -250,17 +238,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)]
|
#[allow(deprecated)]
|
||||||
#[cfg(any(feature = "tls", feature = "native-tls"))]
|
#[cfg(feature = "tls")]
|
||||||
fn is_close_notify(e: &std::io::Error) -> bool {
|
fn is_close_notify(e: &std::io::Error) -> bool {
|
||||||
if e.kind() != ErrorKind::ConnectionAborted {
|
if e.kind() != ErrorKind::ConnectionAborted {
|
||||||
return false;
|
return false;
|
||||||
@@ -279,10 +258,7 @@ impl Write for Stream {
|
|||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
match self {
|
match self {
|
||||||
Stream::Http(sock) => sock.get_mut().write(buf),
|
Stream::Http(sock) => sock.get_mut().write(buf),
|
||||||
#[cfg(any(
|
#[cfg(feature = "tls")]
|
||||||
all(feature = "tls", not(feature = "native-tls")),
|
|
||||||
all(feature = "native-tls", not(feature = "tls")),
|
|
||||||
))]
|
|
||||||
Stream::Https(stream) => stream.get_mut().write(buf),
|
Stream::Https(stream) => stream.get_mut().write(buf),
|
||||||
Stream::Cursor(_) => panic!("Write to read only stream"),
|
Stream::Cursor(_) => panic!("Write to read only stream"),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -292,10 +268,7 @@ impl Write for Stream {
|
|||||||
fn flush(&mut self) -> io::Result<()> {
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
Stream::Http(sock) => sock.get_mut().flush(),
|
Stream::Http(sock) => sock.get_mut().flush(),
|
||||||
#[cfg(any(
|
#[cfg(feature = "tls")]
|
||||||
all(feature = "tls", not(feature = "native-tls")),
|
|
||||||
all(feature = "native-tls", not(feature = "tls")),
|
|
||||||
))]
|
|
||||||
Stream::Https(stream) => stream.get_mut().flush(),
|
Stream::Https(stream) => stream.get_mut().flush(),
|
||||||
Stream::Cursor(_) => panic!("Flush read only stream"),
|
Stream::Cursor(_) => panic!("Flush read only stream"),
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -326,7 +299,7 @@ fn configure_certs(config: &mut rustls::ClientConfig) {
|
|||||||
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
|
.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> {
|
pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error> {
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -343,6 +316,8 @@ pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error
|
|||||||
.map_err(|err| Error::DnsFailed(err.to_string()))?;
|
.map_err(|err| Error::DnsFailed(err.to_string()))?;
|
||||||
let tls_conf: &Arc<rustls::ClientConfig> = unit
|
let tls_conf: &Arc<rustls::ClientConfig> = unit
|
||||||
.req
|
.req
|
||||||
|
.agent
|
||||||
|
.config
|
||||||
.tls_config
|
.tls_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| &c.0)
|
.map(|c| &c.0)
|
||||||
@@ -356,35 +331,13 @@ pub(crate) fn connect_https(unit: &Unit, hostname: &str) -> Result<Stream, Error
|
|||||||
Ok(Stream::Https(BufReader::new(stream)))
|
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> {
|
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 {
|
let connect_deadline: Option<Instant> =
|
||||||
Instant::now().checked_add(timeout_connect)
|
if let Some(timeout_connect) = unit.req.agent.config.timeout_connect {
|
||||||
} else {
|
Instant::now().checked_add(timeout_connect)
|
||||||
unit.deadline
|
} else {
|
||||||
};
|
unit.deadline
|
||||||
|
};
|
||||||
let proxy: Option<Proxy> = unit.req.proxy();
|
let proxy: Option<Proxy> = unit.req.proxy();
|
||||||
let netloc = match proxy {
|
let netloc = match proxy {
|
||||||
Some(ref proxy) => format!("{}:{}", proxy.server, proxy.port),
|
Some(ref proxy) => format!("{}:{}", proxy.server, proxy.port),
|
||||||
@@ -412,7 +365,7 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<Tcp
|
|||||||
// Find the first sock_addr that accepts a connection
|
// Find the first sock_addr that accepts a connection
|
||||||
for sock_addr in sock_addrs {
|
for sock_addr in sock_addrs {
|
||||||
// ensure connect timeout or overall timeout aren't yet hit.
|
// ensure connect timeout or overall timeout aren't yet hit.
|
||||||
let timeout = match deadline {
|
let timeout = match connect_deadline {
|
||||||
Some(deadline) => Some(time_until_deadline(deadline)?),
|
Some(deadline) => Some(time_until_deadline(deadline)?),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
@@ -423,7 +376,7 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<Tcp
|
|||||||
connect_socks5(
|
connect_socks5(
|
||||||
&unit,
|
&unit,
|
||||||
proxy.clone().unwrap(),
|
proxy.clone().unwrap(),
|
||||||
deadline,
|
connect_deadline,
|
||||||
sock_addr,
|
sock_addr,
|
||||||
hostname,
|
hostname,
|
||||||
port,
|
port,
|
||||||
@@ -449,20 +402,16 @@ pub(crate) fn connect_host(unit: &Unit, hostname: &str, port: u16) -> Result<Tcp
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(deadline) = deadline {
|
if let Some(deadline) = unit.deadline {
|
||||||
stream.set_read_timeout(Some(time_until_deadline(deadline)?))?;
|
stream.set_read_timeout(Some(time_until_deadline(deadline)?))?;
|
||||||
} else if let Some(timeout_read) = unit.req.timeout_read {
|
|
||||||
stream.set_read_timeout(Some(timeout_read))?;
|
|
||||||
} else {
|
} else {
|
||||||
stream.set_read_timeout(None)?;
|
stream.set_read_timeout(unit.req.agent.config.timeout_read)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(deadline) = deadline {
|
if let Some(deadline) = unit.deadline {
|
||||||
stream.set_write_timeout(Some(time_until_deadline(deadline)?))?;
|
stream.set_write_timeout(Some(time_until_deadline(deadline)?))?;
|
||||||
} else if let Some(timeout_write) = unit.req.timeout_write {
|
|
||||||
stream.set_write_timeout(Some(timeout_write)).ok();
|
|
||||||
} else {
|
} else {
|
||||||
stream.set_write_timeout(None)?;
|
stream.set_write_timeout(unit.req.agent.config.timeout_write)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if proto == Some(Proto::HTTPConnect) {
|
if proto == Some(Proto::HTTPConnect) {
|
||||||
@@ -647,7 +596,7 @@ pub(crate) fn connect_test(unit: &Unit) -> Result<Stream, Error> {
|
|||||||
Err(Error::UnknownScheme(unit.url.scheme().to_string()))
|
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> {
|
pub(crate) fn connect_https(unit: &Unit, _hostname: &str) -> Result<Stream, Error> {
|
||||||
Err(Error::UnknownScheme(unit.url.scheme().to_string()))
|
Err(Error::UnknownScheme(unit.url.scheme().to_string()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ use super::super::*;
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn agent_reuse_headers() {
|
fn agent_reuse_headers() {
|
||||||
let agent = AgentBuilder::new()
|
let agent = builder().set("Authorization", "Foo 12345").build();
|
||||||
.set("Authorization", "Foo 12345")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
test::set_handler("/agent_reuse_headers", |unit| {
|
test::set_handler("/agent_reuse_headers", |unit| {
|
||||||
assert!(unit.has("Authorization"));
|
assert!(unit.has("Authorization"));
|
||||||
@@ -46,7 +44,7 @@ fn idle_timeout_handler(mut stream: TcpStream) -> io::Result<()> {
|
|||||||
fn connection_reuse() {
|
fn connection_reuse() {
|
||||||
let testserver = TestServer::new(idle_timeout_handler);
|
let testserver = TestServer::new(idle_timeout_handler);
|
||||||
let url = format!("http://localhost:{}", testserver.port);
|
let url = format!("http://localhost:{}", testserver.port);
|
||||||
let agent = Agent::default();
|
let agent = Agent::new();
|
||||||
let resp = agent.get(&url).call().unwrap();
|
let resp = agent.get(&url).call().unwrap();
|
||||||
|
|
||||||
// use up the connection so it gets returned to the pool
|
// use up the connection so it gets returned to the pool
|
||||||
@@ -96,7 +94,7 @@ fn custom_resolver() {
|
|||||||
assert_eq!(&server.join().unwrap(), b"GET / HTTP/1.1\r\n");
|
assert_eq!(&server.join().unwrap(), b"GET / HTTP/1.1\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn cookie_and_redirect(mut stream: TcpStream) -> io::Result<()> {
|
fn cookie_and_redirect(mut stream: TcpStream) -> io::Result<()> {
|
||||||
let headers = read_headers(&stream);
|
let headers = read_headers(&stream);
|
||||||
@@ -139,14 +137,14 @@ fn cookie_and_redirect(mut stream: TcpStream) -> io::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cookies_on_redirect() -> Result<(), Error> {
|
fn test_cookies_on_redirect() -> Result<(), Error> {
|
||||||
let testserver = TestServer::new(cookie_and_redirect);
|
let testserver = TestServer::new(cookie_and_redirect);
|
||||||
let url = format!("http://localhost:{}/first", testserver.port);
|
let url = format!("http://localhost:{}/first", testserver.port);
|
||||||
let agent = Agent::default();
|
let agent = Agent::new();
|
||||||
agent.post(&url).call()?;
|
agent.post(&url).call()?;
|
||||||
let cookies = agent.state.jar.get_request_cookies(
|
let cookies = agent.state.cookie_tin.get_request_cookies(
|
||||||
&format!("https://localhost:{}/", testserver.port)
|
&format!("https://localhost:{}/", testserver.port)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@@ -166,17 +164,17 @@ fn dirty_streams_not_returned() -> Result<(), Error> {
|
|||||||
stream.write_all(b"\r\n")?;
|
stream.write_all(b"\r\n")?;
|
||||||
stream.write_all(b"5\r\n")?;
|
stream.write_all(b"5\r\n")?;
|
||||||
stream.write_all(b"corgi\r\n")?;
|
stream.write_all(b"corgi\r\n")?;
|
||||||
stream.write_all(b"8\r\n")?;
|
stream.write_all(b"9\r\n")?;
|
||||||
stream.write_all(b"dachsund\r\n")?;
|
stream.write_all(b"dachshund\r\n")?;
|
||||||
stream.write_all(b"0\r\n")?;
|
stream.write_all(b"0\r\n")?;
|
||||||
stream.write_all(b"\r\n")?;
|
stream.write_all(b"\r\n")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
let url = format!("http://localhost:{}/", testserver.port);
|
let url = format!("http://localhost:{}/", testserver.port);
|
||||||
let agent = Agent::default();
|
let agent = Agent::new();
|
||||||
let resp = agent.get(&url).call()?;
|
let resp = agent.get(&url).call()?;
|
||||||
let resp_str = resp.into_string()?;
|
let resp_str = resp.into_string()?;
|
||||||
assert_eq!(resp_str, "corgidachsund");
|
assert_eq!(resp_str, "corgidachshund");
|
||||||
|
|
||||||
// Now fetch it again, but only read part of the body.
|
// Now fetch it again, but only read part of the body.
|
||||||
let resp_to_be_dropped = agent.get(&url).call()?;
|
let resp_to_be_dropped = agent.get(&url).call()?;
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
use crate::test;
|
|
||||||
|
|
||||||
use super::super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn basic_auth() {
|
|
||||||
test::set_handler("/basic_auth", |unit| {
|
|
||||||
assert_eq!(
|
|
||||||
unit.header("Authorization").unwrap(),
|
|
||||||
"Basic bWFydGluOnJ1YmJlcm1hc2hndW0="
|
|
||||||
);
|
|
||||||
test::make_response(200, "OK", vec![], vec![])
|
|
||||||
});
|
|
||||||
let resp = get("test://host/basic_auth")
|
|
||||||
.auth("martin", "rubbermashgum")
|
|
||||||
.call()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(resp.status(), 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn kind_auth() {
|
|
||||||
test::set_handler("/kind_auth", |unit| {
|
|
||||||
assert_eq!(unit.header("Authorization").unwrap(), "Digest abcdefgh123");
|
|
||||||
test::make_response(200, "OK", vec![], vec![])
|
|
||||||
});
|
|
||||||
let resp = get("test://host/kind_auth")
|
|
||||||
.auth_kind("Digest", "abcdefgh123")
|
|
||||||
.call()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(resp.status(), 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_auth() {
|
|
||||||
test::set_handler("/url_auth", |unit| {
|
|
||||||
assert_eq!(
|
|
||||||
unit.header("Authorization").unwrap(),
|
|
||||||
"Basic QWxhZGRpbjpPcGVuU2VzYW1l"
|
|
||||||
);
|
|
||||||
test::make_response(200, "OK", vec![], vec![])
|
|
||||||
});
|
|
||||||
let resp = get("test://Aladdin:OpenSesame@host/url_auth")
|
|
||||||
.call()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(resp.status(), 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_auth_overridden() {
|
|
||||||
test::set_handler("/url_auth_overridden", |unit| {
|
|
||||||
assert_eq!(
|
|
||||||
unit.header("Authorization").unwrap(),
|
|
||||||
"Basic bWFydGluOnJ1YmJlcm1hc2hndW0="
|
|
||||||
);
|
|
||||||
test::make_response(200, "OK", vec![], vec![])
|
|
||||||
});
|
|
||||||
let agent = AgentBuilder::new().auth("martin", "rubbermashgum").build();
|
|
||||||
let resp = agent
|
|
||||||
.get("test://Aladdin:OpenSesame@host/url_auth_overridden")
|
|
||||||
.call()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(resp.status(), 200);
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::{collections::HashMap, net::ToSocketAddrs};
|
use std::{collections::HashMap, net::ToSocketAddrs};
|
||||||
|
|
||||||
mod agent_test;
|
mod agent_test;
|
||||||
mod auth;
|
|
||||||
mod body_read;
|
mod body_read;
|
||||||
mod body_send;
|
mod body_send;
|
||||||
mod query_string;
|
mod query_string;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#[cfg(any(feature = "tls", feature = "native-tls"))]
|
#[cfg(feature = "tls")]
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
#[cfg(any(feature = "tls", feature = "native-tls"))]
|
#[cfg(feature = "tls")]
|
||||||
use super::super::*;
|
use super::super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(any(feature = "tls", feature = "native-tls"))]
|
#[cfg(feature = "tls")]
|
||||||
fn read_range() {
|
fn read_range() {
|
||||||
let resp = get("https://ureq.s3.eu-central-1.amazonaws.com/sherlock.txt")
|
let resp = get("https://ureq.s3.eu-central-1.amazonaws.com/sherlock.txt")
|
||||||
.set("Range", "bytes=1000-1999")
|
.set("Range", "bytes=1000-1999")
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ fn redirect_many() {
|
|||||||
test::set_handler("/redirect_many2", |_| {
|
test::set_handler("/redirect_many2", |_| {
|
||||||
test::make_response(302, "Go here", vec!["Location: /redirect_many3"], vec![])
|
test::make_response(302, "Go here", vec!["Location: /redirect_many3"], vec![])
|
||||||
});
|
});
|
||||||
let result = get("test://host/redirect_many1").redirects(1).call();
|
let result = builder()
|
||||||
|
.redirects(1)
|
||||||
|
.build()
|
||||||
|
.get("test://host/redirect_many1")
|
||||||
|
.call();
|
||||||
assert!(matches!(result, Err(Error::TooManyRedirects)));
|
assert!(matches!(result, Err(Error::TooManyRedirects)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +42,11 @@ fn redirect_off() -> Result<(), Error> {
|
|||||||
test::set_handler("/redirect_off", |_| {
|
test::set_handler("/redirect_off", |_| {
|
||||||
test::make_response(302, "Go here", vec!["Location: somewhere.else"], vec![])
|
test::make_response(302, "Go here", vec!["Location: somewhere.else"], vec![])
|
||||||
});
|
});
|
||||||
let resp = get("test://host/redirect_off").redirects(0).call()?;
|
let resp = builder()
|
||||||
|
.redirects(0)
|
||||||
|
.build()
|
||||||
|
.get("test://host/redirect_off")
|
||||||
|
.call()?;
|
||||||
assert_eq!(resp.status(), 302);
|
assert_eq!(resp.status(), 302);
|
||||||
assert!(resp.has("Location"));
|
assert!(resp.has("Location"));
|
||||||
assert_eq!(resp.header("Location").unwrap(), "somewhere.else");
|
assert_eq!(resp.header("Location").unwrap(), "somewhere.else");
|
||||||
@@ -96,7 +104,7 @@ fn redirect_host() {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
let url = format!("http://localhost:{}/", srv.port);
|
let url = format!("http://localhost:{}/", srv.port);
|
||||||
let resp = crate::Agent::default().get(&url).call();
|
let resp = crate::Agent::new().get(&url).call();
|
||||||
let err = resp.err();
|
let err = resp.err();
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err, Some(Error::DnsFailed(_))),
|
matches!(err, Some(Error::DnsFailed(_))),
|
||||||
|
|||||||
@@ -130,8 +130,7 @@ fn request_debug() {
|
|||||||
let req = get("http://localhost/my/page")
|
let req = get("http://localhost/my/page")
|
||||||
.set("Authorization", "abcdef")
|
.set("Authorization", "abcdef")
|
||||||
.set("Content-Length", "1234")
|
.set("Content-Length", "1234")
|
||||||
.set("Content-Type", "application/json")
|
.set("Content-Type", "application/json");
|
||||||
.build();
|
|
||||||
|
|
||||||
let s = format!("{:?}", req);
|
let s = format!("{:?}", req);
|
||||||
|
|
||||||
@@ -143,8 +142,7 @@ fn request_debug() {
|
|||||||
|
|
||||||
let req = get("http://localhost/my/page?q=z")
|
let req = get("http://localhost/my/page?q=z")
|
||||||
.query("foo", "bar baz")
|
.query("foo", "bar baz")
|
||||||
.set("Authorization", "abcdef")
|
.set("Authorization", "abcdef");
|
||||||
.build();
|
|
||||||
|
|
||||||
let s = format!("{:?}", req);
|
let s = format!("{:?}", req);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub struct TestHeaders(Vec<String>);
|
|||||||
|
|
||||||
impl TestHeaders {
|
impl TestHeaders {
|
||||||
// Return the path for a request, e.g. /foo from "GET /foo HTTP/1.1"
|
// Return the path for a request, e.g. /foo from "GET /foo HTTP/1.1"
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
pub fn path(&self) -> &str {
|
pub fn path(&self) -> &str {
|
||||||
if self.0.len() == 0 {
|
if self.0.len() == 0 {
|
||||||
""
|
""
|
||||||
@@ -22,7 +22,7 @@ impl TestHeaders {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
pub fn headers(&self) -> &[String] {
|
pub fn headers(&self) -> &[String] {
|
||||||
&self.0[1..]
|
&self.0[1..]
|
||||||
}
|
}
|
||||||
@@ -57,12 +57,12 @@ impl TestServer {
|
|||||||
eprintln!("testserver: handling just-accepted stream: {}", e);
|
eprintln!("testserver: handling just-accepted stream: {}", e);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
thread::spawn(move || handler(stream.unwrap()));
|
if done.load(Ordering::SeqCst) {
|
||||||
if done.load(Ordering::Relaxed) {
|
|
||||||
break;
|
break;
|
||||||
|
} else {
|
||||||
|
thread::spawn(move || handler(stream.unwrap()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("testserver on {} exiting", port);
|
|
||||||
});
|
});
|
||||||
TestServer {
|
TestServer {
|
||||||
port,
|
port,
|
||||||
@@ -73,7 +73,7 @@ impl TestServer {
|
|||||||
|
|
||||||
impl Drop for TestServer {
|
impl Drop for TestServer {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.done.store(true, Ordering::Relaxed);
|
self.done.store(true, Ordering::SeqCst);
|
||||||
// Connect once to unblock the listen loop.
|
// Connect once to unblock the listen loop.
|
||||||
TcpStream::connect(format!("localhost:{}", self.port)).unwrap();
|
TcpStream::connect(format!("localhost:{}", self.port)).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ fn dribble_body_respond(mut stream: TcpStream, contents: &[u8]) -> io::Result<()
|
|||||||
stream.write_all(&contents[i..i + 1])?;
|
stream.write_all(&contents[i..i + 1])?;
|
||||||
stream.write_all(&[b'\n'; 1])?;
|
stream.write_all(&[b'\n'; 1])?;
|
||||||
stream.flush()?;
|
stream.flush()?;
|
||||||
thread::sleep(Duration::from_millis(10));
|
thread::sleep(Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_and_expect_timeout(url: String) {
|
fn get_and_expect_timeout(url: String) {
|
||||||
let agent = Agent::default();
|
|
||||||
let timeout = Duration::from_millis(500);
|
let timeout = Duration::from_millis(500);
|
||||||
let resp = agent.get(&url).timeout(timeout).call().unwrap();
|
let agent = builder().timeout(timeout).build();
|
||||||
|
let resp = agent.get(&url).call().unwrap();
|
||||||
|
|
||||||
match resp.into_string() {
|
match resp.into_string() {
|
||||||
Err(io_error) => match io_error.kind() {
|
Err(io_error) => match io_error.kind() {
|
||||||
@@ -47,17 +47,38 @@ fn overall_timeout_during_body() {
|
|||||||
get_and_expect_timeout(url);
|
get_and_expect_timeout(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_timeout_during_body() {
|
||||||
|
let server = TestServer::new(|stream| dribble_body_respond(stream, &[b'a'; 300]));
|
||||||
|
let url = format!("http://localhost:{}/", server.port);
|
||||||
|
let agent = builder().timeout_read(Duration::from_millis(70)).build();
|
||||||
|
let resp = match agent.get(&url).call() {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => panic!("got error during headers, not body: {:?}", e),
|
||||||
|
};
|
||||||
|
match resp.into_string() {
|
||||||
|
Err(io_error) => match io_error.kind() {
|
||||||
|
io::ErrorKind::TimedOut => Ok(()),
|
||||||
|
_ => Err(format!("{:?}", io_error)),
|
||||||
|
},
|
||||||
|
Ok(_) => Err("successful response".to_string()),
|
||||||
|
}
|
||||||
|
.expect("expected timeout but got something else");
|
||||||
|
}
|
||||||
|
|
||||||
// Send HTTP headers on the TcpStream at a rate of one header every 100
|
// Send HTTP headers on the TcpStream at a rate of one header every 100
|
||||||
// milliseconds, for a total of 30 headers.
|
// milliseconds, for a total of 30 headers.
|
||||||
//fn dribble_headers_respond(mut stream: TcpStream) -> io::Result<()> {
|
fn dribble_headers_respond(mut stream: TcpStream) -> io::Result<()> {
|
||||||
// stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n")?;
|
stream.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n")?;
|
||||||
// for _ in 0..30 {
|
for _ in 0..30 {
|
||||||
// stream.write_all(b"a: b\n")?;
|
stream.write_all(b"a: b\r\n")?;
|
||||||
// stream.flush()?;
|
stream.flush()?;
|
||||||
// thread::sleep(Duration::from_millis(100));
|
thread::sleep(Duration::from_millis(100));
|
||||||
// }
|
}
|
||||||
// Ok(())
|
stream.write_all(b"\r\n")?;
|
||||||
//}
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
// TODO: Our current behavior is actually incorrect (we'll return BadHeader if a timeout occurs during headers).
|
// TODO: Our current behavior is actually incorrect (we'll return BadHeader if a timeout occurs during headers).
|
||||||
@@ -70,6 +91,35 @@ fn overall_timeout_during_body() {
|
|||||||
// let url = format!("http://localhost:{}/", server.port);
|
// let url = format!("http://localhost:{}/", server.port);
|
||||||
// get_and_expect_timeout(url);
|
// get_and_expect_timeout(url);
|
||||||
//}
|
//}
|
||||||
|
#[test]
|
||||||
|
fn read_timeout_during_headers() {
|
||||||
|
let server = TestServer::new(dribble_headers_respond);
|
||||||
|
let url = format!("http://localhost:{}/", server.port);
|
||||||
|
let agent = builder().timeout_read(Duration::from_millis(10)).build();
|
||||||
|
let resp = agent.get(&url).call();
|
||||||
|
match resp {
|
||||||
|
Ok(_) => Err("successful response".to_string()),
|
||||||
|
Err(Error::Io(e)) if e.kind() == io::ErrorKind::TimedOut => Ok(()),
|
||||||
|
Err(e) => Err(format!("Unexpected error type: {:?}", e)),
|
||||||
|
}
|
||||||
|
.expect("expected timeout but got something else");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overall_timeout_during_headers() {
|
||||||
|
// Start a test server on an available port, that dribbles out a response at 1 write per 10ms.
|
||||||
|
let server = TestServer::new(dribble_headers_respond);
|
||||||
|
let url = format!("http://localhost:{}/", server.port);
|
||||||
|
let agent = builder().timeout(Duration::from_millis(500)).build();
|
||||||
|
let resp = agent.get(&url).call();
|
||||||
|
match resp {
|
||||||
|
Ok(_) => Err("successful response".to_string()),
|
||||||
|
Err(Error::Io(e)) if e.kind() == io::ErrorKind::TimedOut => Ok(()),
|
||||||
|
Err(e) => Err(format!("Unexpected error type: {:?}", e)),
|
||||||
|
}
|
||||||
|
.expect("expected timeout but got something else");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(feature = "json")]
|
#[cfg(feature = "json")]
|
||||||
fn overall_timeout_reading_json() {
|
fn overall_timeout_reading_json() {
|
||||||
@@ -86,9 +136,9 @@ fn overall_timeout_reading_json() {
|
|||||||
});
|
});
|
||||||
let url = format!("http://localhost:{}/", server.port);
|
let url = format!("http://localhost:{}/", server.port);
|
||||||
|
|
||||||
let agent = Agent::default();
|
|
||||||
let timeout = Duration::from_millis(500);
|
let timeout = Duration::from_millis(500);
|
||||||
let resp = agent.get(&url).timeout(timeout).call().unwrap();
|
let agent = builder().timeout(timeout).build();
|
||||||
|
let resp = agent.get(&url).call().unwrap();
|
||||||
|
|
||||||
match resp.into_json() {
|
match resp.into_json() {
|
||||||
Ok(_) => Err("successful response".to_string()),
|
Ok(_) => Err("successful response".to_string()),
|
||||||
|
|||||||
28
src/unit.rs
28
src/unit.rs
@@ -5,14 +5,14 @@ use log::{debug, info};
|
|||||||
use qstring::QString;
|
use qstring::QString;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
|
|
||||||
use crate::body::{self, BodySize, Payload, SizedReader};
|
use crate::body::{self, BodySize, Payload, SizedReader};
|
||||||
use crate::header;
|
use crate::header;
|
||||||
use crate::resolve::ArcResolver;
|
use crate::resolve::ArcResolver;
|
||||||
use crate::stream::{self, connect_test, Stream};
|
use crate::stream::{self, connect_test, Stream};
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
use crate::Agent;
|
use crate::Agent;
|
||||||
use crate::{Error, Header, Request, Response};
|
use crate::{Error, Header, Request, Response};
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ impl Unit {
|
|||||||
extra.push(Header::new("Authorization", &format!("Basic {}", encoded)));
|
extra.push(Header::new("Authorization", &format!("Basic {}", encoded)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
extra.extend(extract_cookies(&req.agent, &url).into_iter());
|
extra.extend(extract_cookies(&req.agent, &url).into_iter());
|
||||||
|
|
||||||
extra
|
extra
|
||||||
@@ -94,7 +94,7 @@ impl Unit {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let deadline = match req.timeout {
|
let deadline = match req.agent.config.timeout {
|
||||||
None => None,
|
None => None,
|
||||||
Some(timeout) => {
|
Some(timeout) => {
|
||||||
let now = time::Instant::now();
|
let now = time::Instant::now();
|
||||||
@@ -203,12 +203,12 @@ pub(crate) fn connect(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// squirrel away cookies
|
// squirrel away cookies
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
save_cookies(&unit, &resp);
|
save_cookies(&unit, &resp);
|
||||||
|
|
||||||
// handle redirects
|
// handle redirects
|
||||||
if resp.redirect() && req.redirects > 0 {
|
if resp.redirect() && req.agent.config.redirects > 0 {
|
||||||
if redirect_count == req.redirects {
|
if redirect_count == req.agent.config.redirects {
|
||||||
return Err(Error::TooManyRedirects);
|
return Err(Error::TooManyRedirects);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,11 +255,11 @@ pub(crate) fn connect(
|
|||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
fn extract_cookies(agent: &Agent, url: &Url) -> Option<Header> {
|
fn extract_cookies(agent: &Agent, url: &Url) -> Option<Header> {
|
||||||
let header_value = agent
|
let header_value = agent
|
||||||
.state
|
.state
|
||||||
.jar
|
.cookie_tin
|
||||||
.get_request_cookies(url)
|
.get_request_cookies(url)
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.encoded().to_string())
|
.map(|c| c.encoded().to_string())
|
||||||
@@ -295,7 +295,7 @@ fn connect_socket(unit: &Unit, hostname: &str, use_pooled: bool) -> Result<(Stre
|
|||||||
while let Some(stream) = agent
|
while let Some(stream) = agent
|
||||||
.state
|
.state
|
||||||
.pool
|
.pool
|
||||||
.try_get_connection(&unit.url, &unit.req.proxy)
|
.try_get_connection(&unit.url, unit.req.agent.config.proxy.clone())
|
||||||
{
|
{
|
||||||
let server_closed = stream.server_closed()?;
|
let server_closed = stream.server_closed()?;
|
||||||
if !server_closed {
|
if !server_closed {
|
||||||
@@ -379,7 +379,7 @@ fn send_prelude(unit: &Unit, stream: &mut Stream, redir: bool) -> io::Result<()>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Investigate a response for "Set-Cookie" headers.
|
/// Investigate a response for "Set-Cookie" headers.
|
||||||
#[cfg(feature = "cookie")]
|
#[cfg(feature = "cookies")]
|
||||||
fn save_cookies(unit: &Unit, resp: &Response) {
|
fn save_cookies(unit: &Unit, resp: &Response) {
|
||||||
//
|
//
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ fn save_cookies(unit: &Unit, resp: &Response) {
|
|||||||
unit.req
|
unit.req
|
||||||
.agent
|
.agent
|
||||||
.state
|
.state
|
||||||
.jar
|
.cookie_tin
|
||||||
.store_response_cookies(cookies, &unit.url.clone());
|
.store_response_cookies(cookies, &unit.url.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,13 +411,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn match_cookies_returns_one_header() {
|
fn match_cookies_returns_one_header() {
|
||||||
let agent = Agent::default();
|
let agent = Agent::new();
|
||||||
let url: Url = "https://crates.io/".parse().unwrap();
|
let url: Url = "https://crates.io/".parse().unwrap();
|
||||||
let cookie1: Cookie = "cookie1=value1; Domain=crates.io; Path=/".parse().unwrap();
|
let cookie1: Cookie = "cookie1=value1; Domain=crates.io; Path=/".parse().unwrap();
|
||||||
let cookie2: Cookie = "cookie2=value2; Domain=crates.io; Path=/".parse().unwrap();
|
let cookie2: Cookie = "cookie2=value2; Domain=crates.io; Path=/".parse().unwrap();
|
||||||
agent
|
agent
|
||||||
.state
|
.state
|
||||||
.jar
|
.cookie_tin
|
||||||
.store_response_cookies(vec![cookie1, cookie2].into_iter(), &url);
|
.store_response_cookies(vec![cookie1, cookie2].into_iter(), &url);
|
||||||
|
|
||||||
// There's no guarantee to the order in which cookies are defined.
|
// There's no guarantee to the order in which cookies are defined.
|
||||||
|
|||||||
2
test.sh
2
test.sh
@@ -4,7 +4,7 @@ set -eu
|
|||||||
export RUST_BACKTRACE=1
|
export RUST_BACKTRACE=1
|
||||||
export RUSTFLAGS="-D dead_code -D unused-variables -D unused"
|
export RUSTFLAGS="-D dead_code -D unused-variables -D unused"
|
||||||
|
|
||||||
for tls in "" tls native-tls ; do
|
for tls in "" tls ; do
|
||||||
for feature in "" json charset cookies socks-proxy ; do
|
for feature in "" json charset cookies socks-proxy ; do
|
||||||
for what in --doc --tests ; do
|
for what in --doc --tests ; do
|
||||||
if ! cargo test "${what}" --no-default-features --features "${tls} ${feature}" ; then
|
if ! cargo test "${what}" --no-default-features --features "${tls} ${feature}" ; then
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ fn agent_set_cookie() {
|
|||||||
headers: HashMap<String, String>,
|
headers: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
let agent = ureq::Agent::default().build();
|
let agent = ureq::Agent::new();
|
||||||
let cookie = ureq::Cookie::build("name", "value")
|
let cookie = ureq::Cookie::build("name", "value")
|
||||||
.domain("httpbin.org")
|
.domain("httpbin.org")
|
||||||
.secure(true)
|
.secure(true)
|
||||||
.finish();
|
.finish();
|
||||||
agent.set_cookie(cookie);
|
agent.set_cookie(cookie, &"https://httpbin.org/".parse().unwrap());
|
||||||
let resp = agent
|
let resp = agent
|
||||||
.get("https://httpbin.org/get")
|
.get("https://httpbin.org/get")
|
||||||
.set("Connection", "close")
|
.set("Connection", "close")
|
||||||
@@ -102,8 +102,6 @@ m0Wqhhi8/24Sy934t5Txgkfoltg8ahkx934WjP6WWRnSAu+cf+vW
|
|||||||
#[cfg(feature = "tls")]
|
#[cfg(feature = "tls")]
|
||||||
#[test]
|
#[test]
|
||||||
fn tls_client_certificate() {
|
fn tls_client_certificate() {
|
||||||
let agent = ureq::Agent::default();
|
|
||||||
|
|
||||||
let mut tls_config = rustls::ClientConfig::new();
|
let mut tls_config = rustls::ClientConfig::new();
|
||||||
|
|
||||||
let certs = rustls::internal::pemfile::certs(&mut BADSSL_CLIENT_CERT_PEM.as_bytes()).unwrap();
|
let certs = rustls::internal::pemfile::certs(&mut BADSSL_CLIENT_CERT_PEM.as_bytes()).unwrap();
|
||||||
@@ -116,11 +114,11 @@ fn tls_client_certificate() {
|
|||||||
.root_store
|
.root_store
|
||||||
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
|
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
|
||||||
|
|
||||||
let resp = agent
|
let agent = ureq::builder()
|
||||||
.get("https://client.badssl.com/")
|
|
||||||
.set_tls_config(std::sync::Arc::new(tls_config))
|
.set_tls_config(std::sync::Arc::new(tls_config))
|
||||||
.call()
|
.build();
|
||||||
.unwrap();
|
|
||||||
|
let resp = agent.get("https://client.badssl.com/").call().unwrap();
|
||||||
|
|
||||||
assert_eq!(resp.status(), 200);
|
assert_eq!(resp.status(), 200);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user