1mod cli_bool;
2mod param;
3pub mod util;
4
5use std::{
6 collections::HashMap,
7 env::consts,
8 path::Path,
9 str::FromStr,
10 sync::LazyLock,
11};
12
13use base64::{engine, Engine};
14use clap::{ArgAction, Args, Parser, Subcommand};
15use cli_bool::CliBool;
16use eyre::eyre;
17pub use param::Param;
18use reqwest::{
19 header::{HeaderName, HeaderValue},
20 redirect::Policy,
21 Method,
22 Request,
23 Url,
24};
25use serde_json::Value;
26use util::parse_string;
27
28static DEFAULT_USER_AGENT: LazyLock<String> = LazyLock::new(||
29 format!(
30 "Mozilla/5.0 ({} {}) AppleWebKit/537.36 (KHTML like Gecko) {}/{} Chrome/129.0.0.0 Safari/537.36",
31 consts::OS,
32 consts::ARCH,
33 env!("CARGO_PKG_NAME"),
34 env!("CARGO_PKG_VERSION"),
35 )
36);
37
38
39pub trait CLParameters {
40
41 fn output(&self) -> Option<String>;
42 fn payload(&self) -> Result<Value, Option<eyre::ErrReport>>;
43 fn policy(&self) -> Policy;
44 fn request(&self) -> eyre::Result<Request>;
45 fn url(&self) -> &Url;
46 fn verify(&self) -> bool;
47 fn verbose(&self) -> bool;
48 fn fail(&self) -> bool;
49}
50
51
52#[derive(Debug, Parser)]
53#[command(about, author, name = "http", version)]
54pub struct Cli {
55
56 #[command(subcommand)]
57 verb: Verb,
58
59 #[arg(skip = reqwest::Url::parse("http://localhost/").unwrap())]
60 url: Url,
61}
62
63#[derive(Args, Debug)]
64struct VerbArgs {
65
66 #[arg()]
68 url: Url,
69
70 #[arg()]
72 params: Vec<Param>,
73
74 #[arg(long)]
84 raw: Option<String>,
85
86 #[arg(short, long)]
88 output: Option<String>,
89
90 #[arg(short, long, env = "HTTP_DOWNLOAD")]
92 download: bool,
93
94 #[arg(short, long, env = "HTTP_AUTH")]
99 auth: Option<String>,
100
101 #[arg(short = 'F', long, action = ArgAction::SetTrue, env = "HTTP_FOLLOW")]
103 follow: bool,
104
105 #[arg(long, default_value_t = 30, env = "HTTP_MAX_REDIRECTS")]
107 max_redirects: usize,
108
109 #[arg(long, default_value_t = CliBool::Yes, env = "HTTP_VERIFY")]
111 verify: CliBool,
112
113 #[arg(long, action = ArgAction::SetTrue, env = "HTTP_FAIL")]
115 fail: CliBool,
116
117 #[arg(short, long, action = ArgAction::SetTrue, env = "HTTP_VERBOSE")]
119 verbose: CliBool,
120}
121
122#[derive(Debug, Subcommand)]
123enum Verb {
124 #[command(aliases = ["Connect", "CONNECT"])]
126 Connect(VerbArgs),
127 #[command(aliases = ["Delete", "DELETE"])]
129 Delete(VerbArgs),
130 #[command(aliases = ["Get", "GET"])]
132 Get(VerbArgs),
133 #[command(aliases = ["Head", "HEAD"])]
135 Head(VerbArgs),
136 #[command(aliases = ["Option", "OPTION"])]
138 Option(VerbArgs),
139 #[command(aliases = ["Patch", "PATCH"])]
141 Patch(VerbArgs),
142 #[command(aliases = ["Post", "POST"])]
144 Post(VerbArgs),
145 #[command(aliases = ["Put", "PUT"])]
147 Put(VerbArgs),
148 #[command(aliases = ["Trace", "TRACE"])]
150 Trace(VerbArgs),
151}
152
153impl CLParameters for Cli {
154
155 fn output(&self) -> Option<String> {
156 let args = self.verb.args();
157 match args.output.clone() {
158 Some(output) => Some(output),
159 None => {
160 if args.download {
161 let path = Path::new(self.url().path());
162 path.file_name().map(|path| path.to_string_lossy().to_string())
163 } else {
164 None
165 }
166 }
167 }
168 }
169
170 fn payload(&self) -> Result<Value, Option<eyre::ErrReport>> {
171 let args = self.verb.args();
172
173 if let Some(raw) = &args.raw {
174 return Ok(Value::String(parse_string(raw.to_string())?));
175 }
176
177 let mut payload: Option<Value> = None;
178 for param in args.params.iter() {
179 if let Param::Payload(param) = param {
180 match payload {
181 None => drop(payload.insert(param.clone())),
182
183 Some(Value::Object(ref mut payload)) =>
184 match param {
185 Value::Object(param) =>
186 {
187 payload.extend(
188 param.iter()
189 .map(|(k, v)| (k.to_owned(), v.clone()))
190 );
191 },
192 _ => return Err(Some(eyre!("invalid payload"))),
193 },
194
195 Some(_) => return Err(Some(eyre!("invalid payload"))),
196 }
197 }
198 }
199
200 match payload {
201 Some(payload) => Ok(payload),
202 None => Err(None),
203 }
204 }
205
206 #[inline]
207 #[must_use]
208 fn policy(&self) -> Policy {
209 self.into()
210 }
211
212 #[inline]
213 fn request(&self) -> eyre::Result<Request> {
214 self.try_into()
215 }
216
217 #[inline]
218 #[must_use]
219 fn url(&self) -> &Url {
220 &self.url
221 }
222
223 #[inline]
224 #[must_use]
225 fn verify(&self) -> bool {
226 self.verb.args().verify.into()
227 }
228
229 #[inline]
230 #[must_use]
231 fn verbose(&self) -> bool {
232 self.verb.args().verbose.into()
233 }
234
235 #[inline]
236 #[must_use]
237 fn fail(&self) -> bool {
238 self.verb.args().fail.into()
239 }
240}
241
242impl Cli {
243
244 fn headers(&self) -> HashMap<String, String> {
245
246 let args = self.verb.args();
247 let mut headers: HashMap<String, String> = HashMap::new();
248 headers.insert(reqwest::header::CONNECTION.to_string(), "close".to_string());
249 headers.insert(reqwest::header::USER_AGENT.to_string(), DEFAULT_USER_AGENT.to_owned());
250
251 for param in args.params.iter() {
252 if let Param::Header(name, value) = param {
253 let entry = headers.entry(name.to_lowercase()).or_default();
254 *entry = value.to_owned();
255 }
256 }
257
258 headers
259 }
260
261 fn build_request(&self) -> eyre::Result<Request> {
262 let method: Method = (&self.verb).into();
263 let headers = self.headers();
264 let mut request = Request::new(method, self.url().clone());
265 for (name, value) in headers.iter() {
266 let _ = request.headers_mut().insert(
267 HeaderName::from_str(name)?,
268 HeaderValue::from_str(value)?,
269 );
270 }
271 Ok(self.set_authorization(request))
272 }
273
274 fn set_authorization(&self, mut request: Request) -> Request {
275 if let Some(auth) = &self.verb.args().auth {
276 if let Some((username, password)) = auth.split_once(':') {
277 let auth = format!("{}:{}", username, password);
278 let engine = engine::general_purpose::STANDARD;
279 let auth = engine.encode(auth.into_bytes());
280 let _ = request.headers_mut().insert(
281 reqwest::header::AUTHORIZATION,
282 HeaderValue::from_str(&format!("Basic {}", auth)).unwrap(),
283 );
284 } else {
285 let _ = request.headers_mut().insert(
286 reqwest::header::AUTHORIZATION,
287 HeaderValue::from_str(&format!("Bearer {}", auth)).unwrap(),
288 );
289 }
290 }
291 request
292 }
293
294 pub fn initialize(&mut self) -> eyre::Result<()> {
295 let args = self.verb.args();
296 let mut url = args.url.clone();
297
298 for param in args.params.iter() {
299 if let Param::Query(key, value) = param {
300 let _ = url.query_pairs_mut().append_pair(key, value);
301 }
302 }
303
304 self.url = url;
305
306 Ok(())
307 }
308}
309
310impl TryFrom<&Cli> for Request {
311
312 type Error = eyre::Error;
313
314 fn try_from(value: &Cli) -> Result<Self, Self::Error> {
315 value.build_request()
316 }
317}
318
319impl From<&Cli> for Policy {
320
321 fn from(value: &Cli) -> Self {
322 let args = value.verb.args();
323 if args.follow {
324 Policy::limited(args.max_redirects)
325 } else {
326 Policy::none()
327 }
328 }
329}
330
331impl Verb {
332
333 #[inline]
334 #[must_use]
335 fn args(&self) -> &VerbArgs {
336 match self {
337 Verb::Connect(args) => args,
338 Verb::Delete(args) => args,
339 Verb::Get(args) => args,
340 Verb::Head(args) => args,
341 Verb::Option(args) => args,
342 Verb::Patch(args) => args,
343 Verb::Post(args) => args,
344 Verb::Put(args) => args,
345 Verb::Trace(args) => args,
346 }
347 }
348}
349
350impl From<&Verb> for Method {
351
352 #[inline]
353 fn from(value: &Verb) -> Self {
354 match value {
355 Verb::Connect(_) => Method::CONNECT,
356 Verb::Delete(_) => Method::DELETE,
357 Verb::Get(_) => Method::GET,
358 Verb::Head(_) => Method::HEAD,
359 Verb::Option(_) => Method::OPTIONS,
360 Verb::Patch(_) => Method::PATCH,
361 Verb::Post(_) => Method::POST,
362 Verb::Put(_) => Method::PUT,
363 Verb::Trace(_) => Method::TRACE,
364 }
365 }
366}