mod cli_bool;
mod param;
mod util;
use std::{env::consts, str::FromStr};
use base64::{engine, Engine};
use clap::{ArgAction, Parser, Subcommand};
use cli_bool::CliBool;
use eyre::eyre;
pub use param::Param;
use reqwest::{header::{HeaderName, HeaderValue}, redirect::Policy, Method, Request, Url};
use serde_json::Value;
use util::parse_string;
#[derive(Debug, Parser)]
#[command(about, author, name = "http", version)]
pub struct Cli {
#[arg(short, long, action = ArgAction::SetTrue, default_value_t = true)]
pub json: bool,
#[arg(short, long, action = ArgAction::SetTrue)]
pub form: bool,
#[arg(long, action = ArgAction::SetTrue)]
pub multipart: bool,
#[arg(long)]
pub boundary: Option<String>,
#[arg(long)]
pub raw: Option<String>,
#[arg(short, long)]
pub output: Option<String>,
#[arg(short, long)]
pub download: bool,
#[arg(short, long)]
pub auth: Option<String>,
#[arg(short = 'F', long, action = ArgAction::SetTrue)]
pub follow: bool,
#[arg(long, default_value_t = 30)]
pub max_redirects: usize,
#[arg(long, default_value_t = CliBool::Yes)]
pub verify: CliBool,
#[arg(short, long, action = ArgAction::SetTrue)]
pub verbose: bool,
#[command(subcommand)]
pub verb: Verb,
}
#[derive(Debug, Subcommand)]
pub enum Verb {
#[command()]
Connect {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
#[command()]
Delete {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
#[command()]
Get {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
#[command()]
Head {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
#[command()]
Option {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
#[command()]
Patch {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
#[command()]
Post {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
#[command()]
Put {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
#[command()]
Trace {
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
},
}
impl Cli {
pub fn payload(&self) -> Result<Value, Option<eyre::ErrReport>> {
if let Some(raw) = &self.raw {
return Ok(Value::String(parse_string(raw.to_string())?));
}
let mut payload: Option<Value> = None;
for param in self.verb.params().iter() {
if let Param::Payload(param) = param {
match payload {
None => payload.insert(param.clone()).ignore(),
Some(Value::Object(ref mut payload)) =>
match param {
Value::Object(param) => payload.extend(
param.iter()
.map(|(k, v)| (k.to_owned(), v.clone()))
).ignore(),
_ => return Err(Some(eyre!("invalid payload"))),
},
Some(_) => return Err(Some(eyre!("invalid payload"))),
}
}
}
match payload {
Some(payload) => Ok(payload),
None => Err(None),
}
}
}
impl Verb {
pub fn url(&self) -> &Url {
match self {
Verb::Connect { url, .. } => url,
Verb::Delete { url, .. } => url,
Verb::Get { url, .. } => url,
Verb::Head { url, .. } => url,
Verb::Option { url, .. } => url,
Verb::Patch { url, .. } => url,
Verb::Post { url, .. } => url,
Verb::Put { url, .. } => url,
Verb::Trace { url, .. } => url,
}
}
pub fn params(&self) -> &Vec<Param> {
match self {
Verb::Connect { params, .. } => params,
Verb::Delete { params, .. } => params,
Verb::Get { params, .. } => params,
Verb::Head { params, .. } => params,
Verb::Option { params, .. } => params,
Verb::Patch { params, .. } => params,
Verb::Post { params, .. } => params,
Verb::Put { params, .. } => params,
Verb::Trace { params, .. } => params,
}
}
}
impl From<&Verb> for Method {
fn from(value: &Verb) -> Self {
match value {
Verb::Connect { .. } => Method::CONNECT,
Verb::Delete { .. } => Method::DELETE,
Verb::Get { .. } => Method::GET,
Verb::Head { .. } => Method::HEAD,
Verb::Option { .. } => Method::OPTIONS,
Verb::Patch { .. } => Method::PATCH,
Verb::Post { .. } => Method::POST,
Verb::Put { .. } => Method::PUT,
Verb::Trace { .. } => Method::TRACE,
}
}
}
impl TryFrom<&Cli> for Request {
type Error = eyre::Error;
fn try_from(value: &Cli) -> Result<Self, Self::Error> {
if (value.json && value.form)
|| (value.json && value.multipart)
|| (value.form && value.multipart) {
return Err(eyre!("--json, --form, and --multipart are mutually exclusive"));
}
let method: Method = (&value.verb).into();
let mut url = value.verb.url().clone();
let mut headers: Vec<(String, String)> = vec![];
let mut user_agent_set = false;
let user_agent = reqwest::header::USER_AGENT.to_string();
for param in value.verb.params().iter() {
match param {
Param::Header(name, value) => {
if user_agent == **name {
user_agent_set = true;
}
headers.push((name.to_owned(), value.to_owned()));
}
Param::Query(key, value) => url.query_pairs_mut()
.append_pair(&key, &value)
.ignore(),
_ => (),
}
}
if !user_agent_set {
headers.push((
user_agent.to_owned(),
format!(
"Mozilla/5.0 ({} {}) AppleWebKit/537.36 (KHTML like Gecko) {}/{} Chrome/129.0.0.0 Safari/537.36",
consts::OS,
consts::ARCH,
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
),
));
}
let mut request = Request::new(method, url);
for (name, value) in headers.iter() {
let _ = request.headers_mut().insert(
HeaderName::from_str(name)?,
HeaderValue::from_str(value)?,
);
}
if let Some(auth) = &value.auth {
if let Some((username, password)) = auth.split_once(':') {
let auth = format!("{}:{}", username, password);
let engine = engine::general_purpose::STANDARD;
let auth = engine.encode(auth.into_bytes());
let _ = request.headers_mut().insert(
reqwest::header::AUTHORIZATION,
HeaderValue::from_str(&format!("Basic {}", auth))?,
);
} else {
let _ = request.headers_mut().insert(
reqwest::header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", auth))?,
);
}
}
Ok(request)
}
}
impl From<&Cli> for Policy {
fn from(value: &Cli) -> Self {
if value.follow {
Policy::limited(value.max_redirects)
} else {
Policy::none()
}
}
}
trait Ignore {
fn ignore(&self) {}
}
impl<T> Ignore for T {
}