use bytes::Bytes;
use http::{HeaderMap, HeaderName, HeaderValue, Method, Request, Uri, header};
use crate::body::{self, Body as EncodedBody};
use crate::client::TorClient;
use crate::compression::Compression;
use crate::config::Config;
use crate::error::{Error, Result};
use crate::isolation::IsolationToken;
use crate::response::Response;
pub struct RequestBuilder<'a> {
client: &'a TorClient,
method: Method,
uri: Uri,
headers: HeaderMap,
body: Option<Bytes>,
isolation_token: Option<IsolationToken>,
timeout: Option<std::time::Duration>,
accept_compression: bool,
}
impl<'a> RequestBuilder<'a> {
pub(crate) fn new(client: &'a TorClient, method: Method, uri: Uri) -> Self {
Self {
client,
method,
uri,
headers: HeaderMap::new(),
body: None,
isolation_token: None,
timeout: None,
accept_compression: true, }
}
pub fn header<K, V>(mut self, key: K, value: V) -> Self
where
K: TryInto<HeaderName>,
V: TryInto<HeaderValue>,
{
if let (Ok(key), Ok(value)) = (key.try_into(), value.try_into()) {
self.headers.insert(key, value);
}
self
}
pub fn headers(mut self, headers: HeaderMap) -> Self {
self.headers.extend(headers);
self
}
pub fn body(mut self, body: impl Into<Bytes>) -> Self {
self.body = Some(body.into());
self
}
pub fn json<T: serde::Serialize>(mut self, value: &T) -> Self {
match serde_json::to_vec(value) {
Ok(data) => {
self.headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
self.body = Some(Bytes::from(data));
}
Err(_) => {
}
}
self
}
pub fn json_raw(mut self, json_str: impl Into<Bytes>) -> Self {
let encoded = EncodedBody::json(json_str);
self.headers
.insert(header::CONTENT_TYPE, encoded.content_type);
self.body = Some(encoded.data);
self
}
pub fn form<I, K, V>(mut self, params: I) -> Result<Self>
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let encoded = EncodedBody::form(params)?;
self.headers
.insert(header::CONTENT_TYPE, encoded.content_type);
self.body = Some(encoded.data);
Ok(self)
}
pub fn text(mut self, text: impl Into<Bytes>) -> Self {
let encoded = EncodedBody::text(text);
self.headers
.insert(header::CONTENT_TYPE, encoded.content_type);
self.body = Some(encoded.data);
self
}
pub fn isolation(mut self, token: IsolationToken) -> Self {
self.isolation_token = Some(token);
self
}
pub fn query<I, K, V>(mut self, params: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: AsRef<str>,
{
let query_string: String = params
.into_iter()
.map(|(k, v)| {
format!(
"{}={}",
urlencoding::encode(k.as_ref()),
urlencoding::encode(v.as_ref())
)
})
.collect::<Vec<_>>()
.join("&");
if !query_string.is_empty() {
let uri_str = self.uri.to_string();
let new_uri = if uri_str.contains('?') {
format!("{}&{}", uri_str, query_string)
} else {
format!("{}?{}", uri_str, query_string)
};
if let Ok(uri) = new_uri.parse() {
self.uri = uri;
}
}
self
}
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn basic_auth(mut self, username: &str, password: &str) -> Self {
let auth_header = body::basic_auth(username, password);
self.headers.insert(header::AUTHORIZATION, auth_header);
self
}
pub fn bearer_auth(self, token: &str) -> Self {
self.header(header::AUTHORIZATION, format!("Bearer {}", token))
}
pub fn no_compression(mut self) -> Self {
self.accept_compression = false;
self
}
fn build_request(&self, config: &Config) -> Result<Request<Bytes>> {
let mut builder = Request::builder()
.method(self.method.clone())
.uri(self.uri.clone());
builder = builder.header(header::USER_AGENT, &config.user_agent);
if self.accept_compression && !self.headers.contains_key(header::ACCEPT_ENCODING) {
builder = builder.header(header::ACCEPT_ENCODING, Compression::accept_encoding());
}
for (key, value) in &self.headers {
builder = builder.header(key, value);
}
if !self.headers.contains_key(header::HOST) {
if let Some(host) = self.uri.host() {
let host_value = match self.uri.port_u16() {
Some(port) if port != 80 && port != 443 => format!("{}:{}", host, port),
_ => host.to_string(),
};
builder = builder.header(header::HOST, host_value);
}
}
if let Some(ref body) = self.body {
if !self.headers.contains_key(header::CONTENT_LENGTH) {
builder = builder.header(header::CONTENT_LENGTH, body.len().to_string());
}
}
let request = builder
.body(self.body.clone().unwrap_or_default())
.map_err(|e| Error::InvalidRequest {
message: e.to_string(),
})?;
Ok(request)
}
pub async fn send(self) -> Result<Response> {
let timeout = self.timeout.unwrap_or(self.client.config().timeout);
tokio::time::timeout(timeout, self.client.execute(self))
.await
.map_err(|_| Error::timeout("request", timeout))?
}
}
impl TorClient {
pub fn get(&self, url: &str) -> Result<RequestBuilder<'_>> {
let uri: Uri = url
.parse()
.map_err(|_| Error::invalid_url(url, "invalid URL"))?;
Ok(RequestBuilder::new(self, Method::GET, uri))
}
pub fn post(&self, url: &str) -> Result<RequestBuilder<'_>> {
let uri: Uri = url
.parse()
.map_err(|_| Error::invalid_url(url, "invalid URL"))?;
Ok(RequestBuilder::new(self, Method::POST, uri))
}
pub fn put(&self, url: &str) -> Result<RequestBuilder<'_>> {
let uri: Uri = url
.parse()
.map_err(|_| Error::invalid_url(url, "invalid URL"))?;
Ok(RequestBuilder::new(self, Method::PUT, uri))
}
pub fn delete(&self, url: &str) -> Result<RequestBuilder<'_>> {
let uri: Uri = url
.parse()
.map_err(|_| Error::invalid_url(url, "invalid URL"))?;
Ok(RequestBuilder::new(self, Method::DELETE, uri))
}
pub fn patch(&self, url: &str) -> Result<RequestBuilder<'_>> {
let uri: Uri = url
.parse()
.map_err(|_| Error::invalid_url(url, "invalid URL"))?;
Ok(RequestBuilder::new(self, Method::PATCH, uri))
}
pub fn head(&self, url: &str) -> Result<RequestBuilder<'_>> {
let uri: Uri = url
.parse()
.map_err(|_| Error::invalid_url(url, "invalid URL"))?;
Ok(RequestBuilder::new(self, Method::HEAD, uri))
}
pub fn request(&self, method: Method, url: &str) -> Result<RequestBuilder<'_>> {
let uri: Uri = url
.parse()
.map_err(|_| Error::invalid_url(url, "invalid URL"))?;
Ok(RequestBuilder::new(self, method, uri))
}
}
impl RequestBuilder<'_> {
pub fn get_method(&self) -> &Method {
&self.method
}
pub fn get_uri(&self) -> &Uri {
&self.uri
}
pub fn get_headers(&self) -> &HeaderMap {
&self.headers
}
pub fn get_body(&self) -> Option<&Bytes> {
self.body.as_ref()
}
pub fn get_isolation_token(&self) -> Option<IsolationToken> {
self.isolation_token
}
pub(crate) fn into_request(self, config: &Config) -> Result<Request<Bytes>> {
self.build_request(config)
}
}