1use std::path::PathBuf;
2
3use base64::engine::general_purpose;
4use base64::Engine;
5use clap::{ArgAction, Parser};
6use chrono::prelude::*;
7use eyre::eyre;
8use mime_guess::from_ext;
9use reqwest::header;
10use reqwest::header::HeaderValue;
11use reqwest::Url;
12
13use crate::cli::utils::extension_from_mime;
14use crate::request::{HttpRequest, Payload, Verb};
15use crate::user_agent::DEFAULT_USER_AGENT;
16
17use super::cli_bool::CliBool;
18use super::param::Param;
19use super::utils;
20
21#[derive(Debug, Parser)]
22#[command(arg_required_else_help = true)]
23#[command(about, author, name = "http", version)]
24#[command(long_about = "https://codeberg.org/cacilhas/microcli/src/branch/master/http")]
25pub struct Cli {
26 #[arg(value_enum)]
28 verb: Verb,
29
30 #[arg()]
32 url: Url,
33
34 #[arg()]
39 params: Vec<Param>,
40
41 #[arg(short, long, action = ArgAction::SetTrue, env = "HTTP_FORCE")]
43 force: CliBool,
44
45 #[arg(short, long, action = ArgAction::SetTrue)]
47 download: bool,
48
49 #[arg(short, long)]
51 output: Option<String>,
52
53 #[arg(short, long, env = "HTTP_AUTH")]
57 auth: Option<String>,
58
59 #[arg(long)]
61 etag: Option<String>,
62
63 #[arg(short = 'F', long, action = ArgAction::SetTrue, env = "HTTP_FOLLOW")]
65 follow: CliBool,
66
67 #[arg(short, long, default_value_t = 30, env = "HTTP_MAX_REDIRECTS")]
69 max_redirects: usize,
70
71 #[arg(long, default_value_t = CliBool::Yes, env = "HTTP_VERIFY", conflicts_with = "no_verify")]
73 verify: CliBool,
74
75 #[arg(short = 'X', long, action = ArgAction::SetTrue)]
78 no_verify: bool,
79
80 #[arg(long, action = ArgAction::SetTrue, env = "HTTP_FAIL")]
82 fail: CliBool,
83
84 #[arg(short, long, action = ArgAction::SetTrue, env = "HTTP_VERBOSE")]
86 verbose: CliBool,
87
88 #[arg(long, action = ArgAction::SetTrue)]
90 dry_run: bool,
91}
92
93impl TryFrom<Cli> for HttpRequest {
94
95 type Error = eyre::Error;
96
97 fn try_from(cli: Cli) -> Result<Self, Self::Error> {
98 let mut request = HttpRequest::default();
99 request.verb = cli.verb;
100 request.url = cli.url.clone();
101 let mut connection_set = false;
102 let mut content_length_set = false;
103 let mut accept = None;
104 let mut content_type = None;
105 let mut user_agent_set = false;
106 for param in &cli.params {
107 if let Param::Header { name, content } = param {
108 request.headers.push((name.to_owned(), content.to_owned()));
109 match *name {
110 header::USER_AGENT => user_agent_set = true,
111 header::CONNECTION => connection_set = true,
112 header::CONTENT_LENGTH => content_length_set = true,
113 header::ACCEPT => accept = content.to_str()
114 .map(|s| s.to_string())
115 .ok(),
116 header::CONTENT_TYPE => content_type = content.to_str()
117 .map(|s| s.to_string())
118 .ok(),
119 _ => (),
120 }
121 }
122 if let Param::Query { key, value } = param {
123 request.url.query_pairs_mut().append_pair(key, value);
124 }
125 }
126 if !connection_set {
127 request.headers.push((
128 header::CONNECTION,
129 HeaderValue::from_static("close"),
130 ));
131 }
132 if !user_agent_set {
133 request.headers.push((
134 header::USER_AGENT,
135 HeaderValue::from_str(DEFAULT_USER_AGENT.as_str())?,
136 ));
137 }
138 if let Some(auth) = cli.auth {
139 request.headers.push((
140 header::AUTHORIZATION,
141 HeaderValue::from_str(&convert_to_authorization(&auth))?,
142 ));
143 }
144 if let Some(etag) = cli.etag {
145 request.headers.push((
146 header::IF_NONE_MATCH,
147 HeaderValue::from_str(&format!("W/\"{}\"", etag))?,
148 ));
149 }
150
151 if content_type.is_none() {
152 for param in &cli.params {
153 match param {
154 Param::Pair { .. } => {
155 content_type = Some("application/json".to_string());
156 break;
157 }
158 Param::FileUpload(filename) => {
159 if let Some(ext) = utils::extension_from_filename(filename) {
160 let ext = match ext.strip_prefix('.') {
161 Some(ext) => ext.to_string(),
162 None => ext,
163 };
164 content_type = Some(
165 from_ext(&ext)
166 .first_or_octet_stream()
167 .to_string()
168 );
169
170 } else {
171 content_type = Some("text/plain".to_string());
172 }
173 break;
174 }
175 _ => (),
176 }
177 }
178 if let Some(ref content_type) = content_type {
179 request.headers.push((
180 header::CONTENT_TYPE,
181 HeaderValue::from_str(content_type)?,
182 ));
183 }
184 }
185
186 request.output = cli.output;
187 if request.output.is_none() && cli.download {
188 request.output = PathBuf::from(request.url.path())
189 .file_name()
190 .map(|name| name.to_string_lossy().into_owned());
191
192 if request.output.is_none() {
193 let now = Local::now();
194 let filename = now.format("http-%Y-%m-%d-%H%M%S")
195 .to_string();
196 match accept {
197 Some(ref accept) => {
198 let ext = extension_from_mime(accept);
199 request.output = Some(format!("{}.{}", filename, ext));
200 }
201 None => {
202 request.output = Some(filename);
203 }
204 }
205 }
206 }
207
208 if let Some(output) = &request.output {
209 let force: bool = cli.force.into();
210 if !force && PathBuf::from(output).exists() {
211 return Err(eyre!("File already exists: {}", output));
212 }
213 }
214
215 request.follow = cli.follow.into();
216 request.max_redirects = cli.max_redirects;
217 request.verify = if cli.no_verify {
218 false
219 } else {
220 cli.verify.into()
221 };
222 request.fail = cli.fail.into();
223 request.verbose = cli.verbose.into();
224 request.payload = cli.params.try_into()?;
225 request.verbose = cli.verbose.into() || cli.dry_run;
226 request.dry_run = cli.dry_run;
227
228 if !content_length_set {
229 match &request.payload {
230 Payload::Content(content) => request.headers.push((
231 header::CONTENT_LENGTH,
232 HeaderValue::from_str(&content.len().to_string())?,
233 )),
234 Payload::FileUpload(filename) => {
235 let metadata = std::fs::metadata(filename)?;
236 request.headers.push((
237 header::CONTENT_LENGTH,
238 HeaderValue::from_str(&metadata.len().to_string())?,
239 ));
240 }
241 _ => (),
242 }
243 }
244
245 Ok(request)
246 }
247}
248
249fn convert_to_authorization(auth: &str) -> String {
250 if let Some(auth) = auth.strip_prefix("!!basic ") {
251 let auth = general_purpose::STANDARD.encode(auth.as_bytes());
252 format!("Basic {}", auth)
253
254 } else if let Some(auth) = auth.strip_prefix("!!bearer ") {
255 format!("Bearer {}", auth)
256
257 } else if auth.contains(":") {
258 let auth = general_purpose::STANDARD.encode(auth.as_bytes());
259 format!("Basic {}", auth)
260
261 } else {
262 format!("Bearer {}", auth)
263 }
264}
265
266#[cfg(test)]
267mod tests {
268
269 use super::*;
270
271 #[test]
272 fn test_forced_basic_authorization() {
273 let auth = convert_to_authorization("!!basic some random value");
274 assert_eq!("Basic c29tZSByYW5kb20gdmFsdWU=", auth);
275 }
276
277 #[test]
278 fn test_force_bearer_token() {
279 let auth = convert_to_authorization("!!bearer user:pass");
280 assert_eq!("Bearer user:pass", auth);
281 }
282
283 #[test]
284 fn test_basic_authorization() {
285 let auth = convert_to_authorization("user:pass");
286 assert_eq!("Basic dXNlcjpwYXNz", auth);
287 }
288
289 #[test]
290 fn test_bearer_token() {
291 let auth = convert_to_authorization("ONXW2ZJAOJQW4ZDPNUQHMYLMOVSQ");
292 assert_eq!("Bearer ONXW2ZJAOJQW4ZDPNUQHMYLMOVSQ", auth);
293 }
294}