use bytes::Bytes;
use http::Method;
use http::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, USER_AGENT};
use mime::Mime;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
use std::time::Duration;
use crate::body::{Body, Multipart, ReadSeek, Segments, boundary, encode_form};
use crate::error::{Error, Result};
use crate::response::Response;
use crate::transport::{self, ResponseReader};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum HttpVersion {
Http11,
Http2,
Http2PriorKnowledge,
Http3,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Auth {
Basic {
user: String,
password: String,
},
Bearer(String),
Digest {
user: String,
password: String,
},
Negotiate,
Ntlm {
user: String,
password: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Progress {
pub downloaded: u64,
pub download_total: Option<u64>,
pub uploaded: u64,
pub upload_total: Option<u64>,
}
pub(crate) struct ProgressFn(pub(crate) Box<dyn FnMut(Progress)>);
impl std::fmt::Debug for ProgressFn {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ProgressFn")
}
}
#[derive(Debug)]
pub struct Request {
pub(crate) method: Method,
pub(crate) url: String,
pub(crate) headers: HeaderMap,
pub(crate) body: Body,
pub(crate) timeout: Duration,
pub(crate) connect_timeout: Duration,
pub(crate) stall_timeout: Duration,
pub(crate) follow_redirects: bool,
pub(crate) max_redirects: u32,
pub(crate) http_version: Option<HttpVersion>,
pub(crate) proxy: Option<String>,
pub(crate) unix_socket: Option<PathBuf>,
pub(crate) ca_bundle: Option<PathBuf>,
pub(crate) verify_tls: bool,
pub(crate) compression: bool,
pub(crate) auth: Option<Auth>,
pub(crate) netrc: bool,
pub(crate) proxy_auth: Option<Auth>,
pub(crate) client_cert: Option<(PathBuf, PathBuf)>,
pub(crate) progress: Option<ProgressFn>,
pub(crate) max_body_size: Option<u64>,
error: Option<Error>,
}
pub(crate) struct Upload {
pub(crate) source: Option<Box<dyn ReadSeek>>,
pub(crate) len: u64,
}
impl Request {
pub fn new(method: Method, url: &str) -> Self {
let url = url.trim();
let url = url.split_once('#').map_or(url, |(before, _)| before);
Request {
method,
url: url.to_owned(),
headers: HeaderMap::new(),
body: Body::Empty,
timeout: Duration::ZERO,
connect_timeout: Duration::from_secs(30),
stall_timeout: Duration::from_secs(30),
follow_redirects: true,
max_redirects: 10,
http_version: None,
proxy: None,
unix_socket: None,
ca_bundle: None,
verify_tls: true,
compression: true,
auth: None,
netrc: false,
proxy_auth: None,
client_cert: None,
progress: None,
max_body_size: Some(64 * 1024 * 1024),
error: None,
}
}
pub fn header<N, V>(mut self, name: N, value: V) -> Self
where
N: TryInto<HeaderName>,
N::Error: Into<Error>,
V: TryInto<HeaderValue>,
V::Error: Into<Error>,
{
match (name.try_into(), value.try_into()) {
(Ok(name), Ok(value)) => {
self.headers.insert(name, value);
}
(Err(e), _) => self.record(e.into()),
(_, Err(e)) => self.record(e.into()),
}
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.headers = headers;
self
}
pub fn query(mut self, name: &str, value: &str) -> Self {
let pair = encode_form([(name, value)]);
match self.url.split_once('?') {
None => {
self.url.push('?');
}
Some((_, query)) if !query.is_empty() && !query.ends_with('&') => {
self.url.push('&');
}
Some(_) => {}
}
self.url.push_str(&pair);
self
}
pub fn body(mut self, content_type: Mime, data: impl Into<Bytes>) -> Self {
self.body = Body::Raw {
data: data.into(),
content_type: Some(content_type),
};
self
}
pub fn body_reader(mut self, content_type: Mime, reader: impl Read + Seek + 'static) -> Self {
self.body = Body::Reader {
reader: Box::new(reader),
content_type,
};
self
}
#[cfg(feature = "json")]
pub fn json<T: serde::Serialize + ?Sized>(mut self, value: &T) -> Self {
match serde_json::to_vec(value) {
Ok(data) => self.body(mime::APPLICATION_JSON, data),
Err(e) => {
self.record(e.into());
self
}
}
}
pub fn form<'a>(mut self, pairs: impl IntoIterator<Item = (&'a str, &'a str)>) -> Self {
self.body = Body::Form(encode_form(pairs));
self
}
pub fn multipart(mut self, multipart: Multipart) -> Self {
self.body = Body::Multipart(multipart);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
pub fn stall_timeout(mut self, timeout: Duration) -> Self {
self.stall_timeout = timeout;
self
}
pub fn follow_redirects(mut self, follow: bool) -> Self {
self.follow_redirects = follow;
self
}
pub fn max_redirects(mut self, max: u32) -> Self {
self.max_redirects = max;
self
}
pub fn http_version(mut self, version: HttpVersion) -> Self {
self.http_version = Some(version);
self
}
pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
self.proxy = Some(proxy.into());
self
}
pub fn unix_socket(mut self, path: impl Into<PathBuf>) -> Self {
self.unix_socket = Some(path.into());
self
}
pub fn ca_bundle(mut self, path: impl Into<PathBuf>) -> Self {
self.ca_bundle = Some(path.into());
self
}
pub fn danger_accept_invalid_certs(mut self) -> Self {
self.verify_tls = false;
self
}
pub fn compression(mut self, enabled: bool) -> Self {
self.compression = enabled;
self
}
pub fn auth(mut self, auth: Auth) -> Self {
self.auth = Some(auth);
self
}
pub fn basic_auth(self, user: impl Into<String>, password: impl Into<String>) -> Self {
self.auth(Auth::Basic {
user: user.into(),
password: password.into(),
})
}
pub fn bearer_auth(self, token: impl Into<String>) -> Self {
self.auth(Auth::Bearer(token.into()))
}
pub fn netrc(mut self) -> Self {
self.netrc = true;
self
}
pub fn proxy_auth(mut self, auth: Auth) -> Self {
self.proxy_auth = Some(auth);
self
}
pub fn client_cert(mut self, cert: impl Into<PathBuf>, key: impl Into<PathBuf>) -> Self {
self.client_cert = Some((cert.into(), key.into()));
self
}
pub fn max_body_size(mut self, limit: Option<u64>) -> Self {
self.max_body_size = limit;
self
}
pub fn on_progress(mut self, f: impl FnMut(Progress) + 'static) -> Self {
self.progress = Some(ProgressFn(Box::new(f)));
self
}
pub fn send(mut self) -> Result<Response> {
let upload = self.prepare()?;
transport::send(self, upload)
}
pub fn send_to(mut self, writer: &mut dyn Write) -> Result<Response> {
let upload = self.prepare()?;
transport::send_to(self, upload, writer)
}
pub fn send_reader(mut self) -> Result<ResponseReader> {
let upload = self.prepare()?;
ResponseReader::new(self, upload)
}
fn prepare(&mut self) -> Result<Option<Upload>> {
if let Some(error) = self.error.take() {
return Err(error);
}
self.prepare_body()
}
pub fn url(&self) -> &str {
&self.url
}
pub(crate) fn record(&mut self, error: Error) {
if self.error.is_none() {
self.error = Some(error);
}
}
pub(crate) fn prepare_body(&mut self) -> Result<Option<Upload>> {
let set_content_type = |headers: &mut HeaderMap, mime: &Mime| -> Result<()> {
headers.insert(CONTENT_TYPE, HeaderValue::from_str(mime.as_ref())?);
Ok(())
};
if !self.headers.contains_key(USER_AGENT) {
self.headers.insert(
USER_AGENT,
HeaderValue::from_static(concat!("curlite/", env!("CARGO_PKG_VERSION"))),
);
}
let upload = match std::mem::take(&mut self.body) {
Body::Empty => None,
Body::Raw { data, content_type } => {
if let Some(ct) = content_type
&& !self.headers.contains_key(CONTENT_TYPE)
{
set_content_type(&mut self.headers, &ct)?;
}
Some(Upload::segments(vec![data]))
}
Body::Form(encoded) => {
set_content_type(&mut self.headers, &mime::APPLICATION_WWW_FORM_URLENCODED)?;
Some(Upload::segments(vec![Bytes::from(encoded)]))
}
Body::Multipart(multipart) => {
let boundary = boundary();
let mime: Mime = format!("multipart/form-data; boundary={boundary}")
.parse()
.expect("valid boundary");
set_content_type(&mut self.headers, &mime)?;
Some(Upload::segments(multipart.into_segments(&boundary)))
}
Body::Reader {
mut reader,
content_type,
} => {
if !self.headers.contains_key(CONTENT_TYPE) {
set_content_type(&mut self.headers, &content_type)?;
}
let len = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(0))?;
Some(Upload {
source: Some(reader),
len,
})
}
};
Ok(upload)
}
}
impl Upload {
fn segments(parts: Vec<Bytes>) -> Self {
let segments = Segments::new(parts);
Upload {
len: segments.len(),
source: Some(Box::new(segments)),
}
}
}
macro_rules! shorthand {
($($(#[$doc:meta])* $name:ident => $method:ident),* $(,)?) => {
$(
$(#[$doc])*
pub fn $name(url: &str) -> Request {
Request::new(Method::$method, url)
}
)*
};
}
shorthand! {
get => GET,
post => POST,
put => PUT,
patch => PATCH,
delete => DELETE,
head => HEAD,
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn url_keeps_query_and_drops_fragment() {
let req = get(" https://example.com/path?test=value&onlyname#frag ");
assert_eq!(req.url(), "https://example.com/path?test=value&onlyname");
}
#[test]
fn query_appends_encoded_pairs() {
let req = get("https://example.com/?test=value&onlyname")
.query("b", "b 1")
.query("c", "");
assert_eq!(
req.url(),
"https://example.com/?test=value&onlyname&b=b+1&c"
);
let req = get("https://example.com").query("a", "=");
assert_eq!(req.url(), "https://example.com?a=%3D");
let req = get("https://example.com/?").query("a", "1");
assert_eq!(req.url(), "https://example.com/?a=1");
let req = get("https://example.com/?x=1&").query("a", "1");
assert_eq!(req.url(), "https://example.com/?x=1&a=1");
}
#[test]
fn invalid_url_surfaces_at_send() {
let err = get("not a url").send().unwrap_err();
assert!(
matches!(err, Error::Curl(ref e) if e.is_url_malformed()),
"{err:?}"
);
}
#[test]
fn invalid_header_surfaces_at_send() {
let err = get("http://localhost")
.header("bad header", "x")
.send()
.unwrap_err();
assert!(matches!(err, Error::InvalidHeaderName(_)));
let err = get("http://localhost")
.header("ok", "bad\nvalue")
.send()
.unwrap_err();
assert!(matches!(err, Error::InvalidHeaderValue(_)));
}
fn read_all(upload: Option<Upload>) -> (u64, Vec<u8>) {
let mut upload = upload.expect("a body");
let mut out = Vec::new();
upload
.source
.as_mut()
.unwrap()
.read_to_end(&mut out)
.unwrap();
(upload.len, out)
}
#[test]
fn body_sets_content_type_unless_present() {
let mut req = post("http://localhost").body(mime::TEXT_XML, "<a/>");
let upload = req.prepare_body().unwrap();
assert_eq!(req.headers[CONTENT_TYPE], "text/xml");
assert_eq!(read_all(upload), (4, b"<a/>".to_vec()));
let mut req = post("http://localhost")
.header(CONTENT_TYPE, "application/custom")
.body(mime::TEXT_XML, "<a/>");
req.prepare_body().unwrap();
assert_eq!(req.headers[CONTENT_TYPE], "application/custom");
let mut req =
post("http://localhost").body_reader(mime::TEXT_PLAIN, Cursor::new(b"hello".to_vec()));
let upload = req.prepare_body().unwrap();
assert_eq!(req.headers[CONTENT_TYPE], "text/plain");
assert_eq!(read_all(upload), (5, b"hello".to_vec()));
}
#[test]
fn form_and_multipart_own_the_content_type() {
let mut req = post("http://localhost")
.header(CONTENT_TYPE, "text/plain")
.form([("a", "b"), ("c", "d")]);
let upload = req.prepare_body().unwrap();
assert_eq!(
req.headers[CONTENT_TYPE],
"application/x-www-form-urlencoded"
);
assert_eq!(read_all(upload).1, b"a=b&c=d");
let mut req = post("http://localhost")
.header(CONTENT_TYPE, "text/plain")
.multipart(Multipart::new().text("a", "b"));
let upload = req.prepare_body().unwrap();
let content_type = req.headers[CONTENT_TYPE].to_str().unwrap().to_owned();
let boundary = content_type
.strip_prefix("multipart/form-data; boundary=")
.unwrap();
let (len, encoded) = read_all(upload);
assert_eq!(len as usize, encoded.len());
assert!(encoded.starts_with(format!("--{boundary}\r\n").as_bytes()));
}
#[test]
fn default_user_agent() {
let mut req = get("http://localhost");
req.prepare_body().unwrap();
assert_eq!(
req.headers[USER_AGENT],
concat!("curlite/", env!("CARGO_PKG_VERSION"))
);
let mut req = get("http://localhost").header(USER_AGENT, "mine");
req.prepare_body().unwrap();
assert_eq!(req.headers[USER_AGENT], "mine");
}
}