#![forbid(unsafe_code)]
#![warn(clippy::all)]
#![deny(missing_docs)]
#![allow(clippy::needless_lifetimes)]
#[macro_use]
extern crate log;
use std::convert::TryFrom;
pub use ureq_proto::http;
pub use body::{Body, BodyBuilder, BodyReader, BodyWithConfig};
use http::Method;
use http::{Request, Response, Uri};
pub use proxy::Proxy;
pub use request::RequestBuilder;
use request::{WithBody, WithoutBody};
pub use response::ResponseExt;
pub use send_body::AsSendBody;
mod agent;
mod body;
pub mod config;
mod error;
mod pool;
mod proxy;
mod query;
mod request;
mod response;
mod run;
mod send_body;
mod timings;
mod util;
pub mod unversioned;
use unversioned::resolver;
use unversioned::transport;
pub mod middleware;
#[cfg(feature = "_tls")]
pub mod tls;
#[cfg(feature = "cookies")]
mod cookies;
#[cfg(feature = "cookies")]
pub use cookies::{Cookie, CookieJar};
pub use agent::Agent;
pub use error::Error;
pub use send_body::SendBody;
pub use timings::Timeout;
pub mod typestate {
pub use super::request::WithBody;
pub use super::request::WithoutBody;
pub use super::config::typestate::AgentScope;
pub use super::config::typestate::HttpCrateScope;
pub use super::config::typestate::RequestScope;
}
pub fn run(request: Request<impl AsSendBody>) -> Result<Response<Body>, Error> {
let agent = Agent::new_with_defaults();
agent.run(request)
}
pub fn agent() -> Agent {
Agent::new_with_defaults()
}
macro_rules! mk_method {
($f:tt, $m:tt, $b:ty) => {
#[doc = concat!("Make a ", stringify!($m), " request.\n\nRun on a use-once [`Agent`].")]
#[must_use]
pub fn $f<T>(uri: T) -> RequestBuilder<$b>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<$b>::new(Agent::new_with_defaults(), Method::$m, uri)
}
};
}
mk_method!(get, GET, WithoutBody);
mk_method!(post, POST, WithBody);
mk_method!(put, PUT, WithBody);
mk_method!(delete, DELETE, WithoutBody);
mk_method!(head, HEAD, WithoutBody);
mk_method!(options, OPTIONS, WithoutBody);
mk_method!(connect, CONNECT, WithoutBody);
mk_method!(patch, PATCH, WithBody);
mk_method!(trace, TRACE, WithoutBody);
#[cfg(test)]
pub(crate) mod test {
use std::{io, sync::OnceLock};
use assert_no_alloc::AllocDisabler;
use config::{Config, ConfigBuilder};
use typestate::AgentScope;
use super::*;
#[global_allocator]
static A: AllocDisabler = AllocDisabler;
pub fn init_test_log() {
static INIT_LOG: OnceLock<()> = OnceLock::new();
INIT_LOG.get_or_init(|| env_logger::init());
}
#[test]
fn connect_http_google() {
init_test_log();
let agent = Agent::new_with_defaults();
let res = agent.get("http://www.google.com/").call().unwrap();
assert_eq!(
"text/html;charset=ISO-8859-1",
res.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap()
.replace("; ", ";")
);
assert_eq!(res.body().mime_type(), Some("text/html"));
}
#[test]
#[cfg(feature = "rustls")]
fn connect_https_google_rustls() {
init_test_log();
use config::Config;
use crate::tls::{TlsConfig, TlsProvider};
let agent: Agent = Config::builder()
.tls_config(TlsConfig::builder().provider(TlsProvider::Rustls).build())
.build()
.into();
let res = agent.get("https://www.google.com/").call().unwrap();
assert_eq!(
"text/html;charset=ISO-8859-1",
res.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap()
.replace("; ", ";")
);
assert_eq!(res.body().mime_type(), Some("text/html"));
}
#[test]
#[cfg(feature = "native-tls")]
fn connect_https_google_native_tls_simple() {
init_test_log();
use config::Config;
use crate::tls::{TlsConfig, TlsProvider};
let agent: Agent = Config::builder()
.tls_config(
TlsConfig::builder()
.provider(TlsProvider::NativeTls)
.build(),
)
.build()
.into();
let mut res = agent.get("https://www.google.com/").call().unwrap();
assert_eq!(
"text/html;charset=ISO-8859-1",
res.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap()
.replace("; ", ";")
);
assert_eq!(res.body().mime_type(), Some("text/html"));
res.body_mut().read_to_string().unwrap();
}
#[test]
#[cfg(feature = "rustls")]
fn connect_https_google_rustls_webpki() {
init_test_log();
use crate::tls::{RootCerts, TlsConfig, TlsProvider};
use config::Config;
let agent: Agent = Config::builder()
.tls_config(
TlsConfig::builder()
.provider(TlsProvider::Rustls)
.root_certs(RootCerts::WebPki)
.build(),
)
.build()
.into();
agent.get("https://www.google.com/").call().unwrap();
}
#[test]
#[cfg(feature = "native-tls")]
fn connect_https_google_native_tls_webpki() {
init_test_log();
use crate::tls::{RootCerts, TlsConfig, TlsProvider};
use config::Config;
let agent: Agent = Config::builder()
.tls_config(
TlsConfig::builder()
.provider(TlsProvider::NativeTls)
.root_certs(RootCerts::WebPki)
.build(),
)
.build()
.into();
agent.get("https://www.google.com/").call().unwrap();
}
#[test]
#[cfg(feature = "rustls")]
fn connect_https_google_noverif() {
init_test_log();
use crate::tls::{TlsConfig, TlsProvider};
let agent: Agent = Config::builder()
.tls_config(
TlsConfig::builder()
.provider(TlsProvider::Rustls)
.disable_verification(true)
.build(),
)
.build()
.into();
let res = agent.get("https://www.google.com/").call().unwrap();
assert_eq!(
"text/html;charset=ISO-8859-1",
res.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap()
.replace("; ", ";")
);
assert_eq!(res.body().mime_type(), Some("text/html"));
}
#[test]
fn simple_put_content_len() {
init_test_log();
let mut res = put("http://httpbin.org/put").send(&[0_u8; 100]).unwrap();
res.body_mut().read_to_string().unwrap();
}
#[test]
fn simple_put_chunked() {
init_test_log();
let mut res = put("http://httpbin.org/put")
.header("transfer-encoding", "chunked")
.send(&[0_u8; 100])
.unwrap();
res.body_mut().read_to_string().unwrap();
}
#[test]
fn simple_get() {
init_test_log();
let mut res = get("http://httpbin.org/get").call().unwrap();
res.body_mut().read_to_string().unwrap();
}
#[test]
fn simple_head() {
init_test_log();
let mut res = head("http://httpbin.org/get").call().unwrap();
res.body_mut().read_to_string().unwrap();
}
#[test]
fn redirect_no_follow() {
init_test_log();
let agent: Agent = Config::builder().max_redirects(0).build().into();
let mut res = agent
.get("http://httpbin.org/redirect-to?url=%2Fget")
.call()
.unwrap();
let txt = res.body_mut().read_to_string().unwrap();
#[cfg(feature = "_test")]
assert_eq!(txt, "You've been redirected");
#[cfg(not(feature = "_test"))]
assert_eq!(txt, "");
}
#[test]
fn redirect_max_with_error() {
init_test_log();
let agent: Agent = Config::builder().max_redirects(3).build().into();
let res = agent
.get(
"http://httpbin.org/redirect-to?url=%2Fredirect-to%3F\
url%3D%2Fredirect-to%3Furl%3D%252Fredirect-to%253Furl%253D",
)
.call();
let err = res.unwrap_err();
assert_eq!(err.to_string(), "too many redirects");
}
#[test]
fn redirect_max_without_error() {
init_test_log();
let agent: Agent = Config::builder()
.max_redirects(3)
.max_redirects_will_error(false)
.build()
.into();
let res = agent
.get(
"http://httpbin.org/redirect-to?url=%2Fredirect-to%3F\
url%3D%2Fredirect-to%3Furl%3D%252Fredirect-to%253Furl%253D",
)
.call()
.unwrap();
assert_eq!(res.status(), 302);
}
#[test]
fn redirect_follow() {
init_test_log();
let res = get("http://httpbin.org/redirect-to?url=%2Fget")
.call()
.unwrap();
let response_uri = res.get_uri();
assert_eq!(response_uri.path(), "/get")
}
#[test]
fn redirect_history_none() {
init_test_log();
let res = get("http://httpbin.org/redirect-to?url=%2Fget")
.call()
.unwrap();
let redirect_history = res.get_redirect_history();
assert_eq!(redirect_history, None)
}
#[test]
fn redirect_history_some() {
init_test_log();
let agent: Agent = Config::builder()
.max_redirects(3)
.max_redirects_will_error(false)
.save_redirect_history(true)
.build()
.into();
let res = agent
.get("http://httpbin.org/redirect-to?url=%2Fget")
.call()
.unwrap();
let redirect_history = res.get_redirect_history();
assert_eq!(
redirect_history,
Some(
vec![
"http://httpbin.org/redirect-to?url=%2Fget".parse().unwrap(),
"http://httpbin.org/get".parse().unwrap()
]
.as_ref()
)
);
let res = agent
.get(
"http://httpbin.org/redirect-to?url=%2Fredirect-to%3F\
url%3D%2Fredirect-to%3Furl%3D%252Fredirect-to%253Furl%253D",
)
.call()
.unwrap();
let redirect_history = res.get_redirect_history();
assert_eq!(
redirect_history,
Some(vec![
"http://httpbin.org/redirect-to?url=%2Fredirect-to%3Furl%3D%2Fredirect-to%3Furl%3D%252Fredirect-to%253Furl%253D".parse().unwrap(),
"http://httpbin.org/redirect-to?url=/redirect-to?url=%2Fredirect-to%3Furl%3D".parse().unwrap(),
"http://httpbin.org/redirect-to?url=/redirect-to?url=".parse().unwrap(),
"http://httpbin.org/redirect-to?url=".parse().unwrap(),
].as_ref())
);
let res = agent.get("https://www.google.com/").call().unwrap();
let redirect_history = res.get_redirect_history();
assert_eq!(
redirect_history,
Some(vec!["https://www.google.com/".parse().unwrap()].as_ref())
);
}
#[test]
fn connect_https_invalid_name() {
let result = get("https://example.com{REQUEST_URI}/").call();
let err = result.unwrap_err();
assert!(matches!(err, Error::Http(_)));
assert_eq!(err.to_string(), "http: invalid uri character");
}
#[test]
fn post_big_body_chunked() {
let mut data = io::Cursor::new(vec![42; 153_600]);
post("http://httpbin.org/post")
.content_type("application/octet-stream")
.send(SendBody::from_reader(&mut data))
.expect("to send correctly");
}
#[test]
#[cfg(not(feature = "_test"))]
fn username_password_from_uri() {
init_test_log();
let mut res = get("https://martin:secret@httpbin.org/get").call().unwrap();
let body = res.body_mut().read_to_string().unwrap();
assert!(body.contains("Basic bWFydGluOnNlY3JldA=="));
}
#[test]
#[cfg(all(feature = "cookies", feature = "_test"))]
fn store_response_cookies() {
let agent = Agent::new_with_defaults();
let _ = agent.get("https://www.google.com").call().unwrap();
let mut all: Vec<_> = agent
.cookie_jar_lock()
.iter()
.map(|c| c.name().to_string())
.collect();
all.sort();
assert_eq!(all, ["AEC", "__Secure-ENID"])
}
#[test]
#[cfg(all(feature = "cookies", feature = "_test"))]
fn send_request_cookies() {
init_test_log();
let agent = Agent::new_with_defaults();
let uri = Uri::from_static("http://cookie.test/cookie-test");
let uri2 = Uri::from_static("http://cookie2.test/cookie-test");
let mut jar = agent.cookie_jar_lock();
jar.insert(Cookie::parse("a=1", &uri).unwrap(), &uri)
.unwrap();
jar.insert(Cookie::parse("b=2", &uri).unwrap(), &uri)
.unwrap();
jar.insert(Cookie::parse("c=3", &uri2).unwrap(), &uri2)
.unwrap();
jar.release();
let _ = agent.get("http://cookie.test/cookie-test").call().unwrap();
}
#[test]
#[cfg(all(feature = "_test", not(feature = "cookies")))]
fn partial_redirect_when_following() {
init_test_log();
get("http://my-host.com/partial-redirect").call().unwrap();
}
#[test]
#[cfg(feature = "_test")]
fn partial_redirect_when_not_following() {
init_test_log();
get("http://my-host.com/partial-redirect")
.config()
.max_redirects(0)
.build()
.call()
.unwrap_err();
}
#[test]
#[cfg(feature = "_test")]
fn http_connect_proxy() {
init_test_log();
let proxy = Proxy::new("http://my_proxy:1234/connect-proxy").unwrap();
let agent = Agent::config_builder()
.proxy(Some(proxy))
.build()
.new_agent();
let mut res = agent.get("http://httpbin.org/get").call().unwrap();
res.body_mut().read_to_string().unwrap();
}
#[test]
fn ensure_reasonable_stack_sizes() {
macro_rules! ensure {
($type:ty, $size:tt) => {
let sz = std::mem::size_of::<$type>();
assert!(
sz <= $size,
"Stack size of {} is too big {} > {}",
stringify!($type),
sz,
$size
);
};
}
ensure!(RequestBuilder<WithoutBody>, 400); ensure!(Agent, 100); ensure!(Config, 400); ensure!(ConfigBuilder<AgentScope>, 400); ensure!(Response<Body>, 800); ensure!(Body, 700); }
fn _ensure_send_sync() {
fn is_send(_t: impl Send) {}
fn is_sync(_t: impl Sync) {}
is_send(Agent::new_with_defaults());
is_sync(Agent::new_with_defaults());
is_send(get("https://example.test"));
is_sync(get("https://example.test"));
let data = vec![0_u8, 1, 2, 3, 4];
is_send(post("https://example.test").send(&data));
is_sync(post("https://example.test").send(&data));
is_send(Request::post("https://yaz").body(&data).unwrap());
is_sync(Request::post("https://yaz").body(&data).unwrap());
is_send(run(Request::post("https://yaz").body(&data).unwrap()));
is_sync(run(Request::post("https://yaz").body(&data).unwrap()));
let mut response = post("https://yaz").send(&data).unwrap();
let shared_reader = response.body_mut().as_reader();
is_send(shared_reader);
let shared_reader = response.body_mut().as_reader();
is_sync(shared_reader);
let response = post("https://yaz").send(&data).unwrap();
let owned_reader = response.into_parts().1.into_reader();
is_send(owned_reader);
let response = post("https://yaz").send(&data).unwrap();
let owned_reader = response.into_parts().1.into_reader();
is_sync(owned_reader);
}
}