#![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()
}
#[must_use]
pub fn get<T>(uri: T) -> RequestBuilder<WithoutBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithoutBody>::new(Agent::new_with_defaults(), Method::GET, uri)
}
#[must_use]
pub fn post<T>(uri: T) -> RequestBuilder<WithBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithBody>::new(Agent::new_with_defaults(), Method::POST, uri)
}
#[must_use]
pub fn put<T>(uri: T) -> RequestBuilder<WithBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithBody>::new(Agent::new_with_defaults(), Method::PUT, uri)
}
#[must_use]
pub fn delete<T>(uri: T) -> RequestBuilder<WithoutBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithoutBody>::new(Agent::new_with_defaults(), Method::DELETE, uri)
}
#[must_use]
pub fn head<T>(uri: T) -> RequestBuilder<WithoutBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithoutBody>::new(Agent::new_with_defaults(), Method::HEAD, uri)
}
#[must_use]
pub fn options<T>(uri: T) -> RequestBuilder<WithoutBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithoutBody>::new(Agent::new_with_defaults(), Method::OPTIONS, uri)
}
#[must_use]
pub fn connect<T>(uri: T) -> RequestBuilder<WithoutBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithoutBody>::new(Agent::new_with_defaults(), Method::CONNECT, uri)
}
#[must_use]
pub fn patch<T>(uri: T) -> RequestBuilder<WithBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithBody>::new(Agent::new_with_defaults(), Method::PATCH, uri)
}
#[must_use]
pub fn trace<T>(uri: T) -> RequestBuilder<WithoutBody>
where
Uri: TryFrom<T>,
<Uri as TryFrom<T>>::Error: Into<http::Error>,
{
RequestBuilder::<WithoutBody>::new(Agent::new_with_defaults(), Method::TRACE, uri)
}
#[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); }
#[test]
fn limit_max_response_header_size() {
init_test_log();
let err = get("http://httpbin.org/get")
.config()
.max_response_header_size(5)
.build()
.call()
.unwrap_err();
assert!(matches!(err, Error::LargeResponseHeader(65, 5)));
}
#[test]
fn propfind_with_body() {
init_test_log();
let request = http::Request::builder()
.method("PROPFIND")
.uri("https://www.google.com/")
.body("Some really cool body")
.unwrap();
let _ = Agent::config_builder()
.allow_non_standard_methods(true)
.build()
.new_agent()
.run(request)
.unwrap();
}
#[test]
#[cfg(feature = "_test")]
fn non_standard_method() {
init_test_log();
let method = Method::from_bytes(b"FNORD").unwrap();
let req = Request::builder()
.method(method)
.uri("http://httpbin.org/fnord")
.body(())
.unwrap();
let agent = Agent::new_with_defaults();
let req = agent
.configure_request(req)
.allow_non_standard_methods(true)
.build();
agent.run(req).unwrap();
}
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);
let err = Error::HostNotFound;
is_send(err);
let err = Error::HostNotFound;
is_sync(err);
}
}