use std::path::PathBuf;
use base64::engine::general_purpose;
use base64::Engine;
use clap::{ArgAction, Parser};
use chrono::prelude::*;
use eyre::eyre;
use mime_guess::from_ext;
use reqwest::header;
use reqwest::header::HeaderValue;
use reqwest::Url;
use crate::cli::utils::extension_from_mime;
use crate::request::{HttpRequest, Payload, Verb};
use crate::user_agent::DEFAULT_USER_AGENT;
use super::cli_bool::CliBool;
use super::param::Param;
use super::utils;
#[derive(Debug, Parser)]
#[command(arg_required_else_help = true)]
#[command(about, author, name = "http", version)]
#[command(long_about = "https://codeberg.org/cacilhas/microcli/src/branch/master/http")]
pub struct Cli {
#[arg(value_enum)]
verb: Verb,
#[arg()]
url: Url,
#[arg()]
params: Vec<Param>,
#[arg(short, long, action = ArgAction::SetTrue, env = "HTTP_FORCE")]
force: CliBool,
#[arg(short, long, action = ArgAction::SetTrue)]
download: bool,
#[arg(short, long)]
output: Option<String>,
#[arg(short, long, env = "HTTP_AUTH")]
auth: Option<String>,
#[arg(long)]
etag: Option<String>,
#[arg(short = 'F', long, action = ArgAction::SetTrue, env = "HTTP_FOLLOW")]
follow: CliBool,
#[arg(short, long, default_value_t = 30, env = "HTTP_MAX_REDIRECTS")]
max_redirects: usize,
#[arg(long, default_value_t = CliBool::Yes, env = "HTTP_VERIFY", conflicts_with = "no_verify")]
verify: CliBool,
#[arg(short = 'X', long, action = ArgAction::SetTrue)]
no_verify: bool,
#[arg(long, action = ArgAction::SetTrue, env = "HTTP_FAIL")]
fail: CliBool,
#[arg(short, long, action = ArgAction::SetTrue, env = "HTTP_VERBOSE")]
verbose: CliBool,
#[arg(long, action = ArgAction::SetTrue)]
dry_run: bool,
}
impl TryFrom<Cli> for HttpRequest {
type Error = eyre::Error;
fn try_from(cli: Cli) -> Result<Self, Self::Error> {
let mut request = HttpRequest::default();
request.verb = cli.verb;
request.url = cli.url.clone();
let mut connection_set = false;
let mut content_length_set = false;
let mut accept = None;
let mut content_type = None;
let mut user_agent_set = false;
for param in &cli.params {
if let Param::Header { name, content } = param {
request.headers.push((name.to_owned(), content.to_owned()));
match *name {
header::USER_AGENT => user_agent_set = true,
header::CONNECTION => connection_set = true,
header::CONTENT_LENGTH => content_length_set = true,
header::ACCEPT => accept = content.to_str()
.map(|s| s.to_string())
.ok(),
header::CONTENT_TYPE => content_type = content.to_str()
.map(|s| s.to_string())
.ok(),
_ => (),
}
}
if let Param::Query { key, value } = param {
request.url.query_pairs_mut().append_pair(key, value);
}
}
if !connection_set {
request.headers.push((
header::CONNECTION,
HeaderValue::from_static("close"),
));
}
if !user_agent_set {
request.headers.push((
header::USER_AGENT,
HeaderValue::from_str(DEFAULT_USER_AGENT.as_str())?,
));
}
if let Some(auth) = cli.auth {
request.headers.push((
header::AUTHORIZATION,
HeaderValue::from_str(&convert_to_authorization(&auth))?,
));
}
if let Some(etag) = cli.etag {
request.headers.push((
header::IF_NONE_MATCH,
HeaderValue::from_str(&format!("W/\"{}\"", etag))?,
));
}
if content_type.is_none() {
for param in &cli.params {
match param {
Param::Pair { .. } => {
content_type = Some("application/json".to_string());
break;
}
Param::FileUpload(filename) => {
if let Some(ext) = utils::extension_from_filename(filename) {
let ext = match ext.strip_prefix('.') {
Some(ext) => ext.to_string(),
None => ext,
};
content_type = Some(
from_ext(&ext)
.first_or_octet_stream()
.to_string()
);
} else {
content_type = Some("text/plain".to_string());
}
break;
}
_ => (),
}
}
if let Some(ref content_type) = content_type {
request.headers.push((
header::CONTENT_TYPE,
HeaderValue::from_str(content_type)?,
));
}
}
request.output = cli.output;
if request.output.is_none() && cli.download {
request.output = PathBuf::from(request.url.path())
.file_name()
.map(|name| name.to_string_lossy().into_owned());
if request.output.is_none() {
let now = Local::now();
let filename = now.format("http-%Y-%m-%d-%H%M%S")
.to_string();
match accept {
Some(ref accept) => {
let ext = extension_from_mime(accept);
request.output = Some(format!("{}.{}", filename, ext));
}
None => {
request.output = Some(filename);
}
}
}
}
if let Some(output) = &request.output {
let force: bool = cli.force.into();
if !force && PathBuf::from(output).exists() {
return Err(eyre!("File already exists: {}", output));
}
}
request.follow = cli.follow.into();
request.max_redirects = cli.max_redirects;
request.verify = if cli.no_verify {
false
} else {
cli.verify.into()
};
request.fail = cli.fail.into();
request.verbose = cli.verbose.into();
request.payload = cli.params.try_into()?;
request.verbose = cli.verbose.into() || cli.dry_run;
request.dry_run = cli.dry_run;
if !content_length_set {
match &request.payload {
Payload::Content(content) => request.headers.push((
header::CONTENT_LENGTH,
HeaderValue::from_str(&content.len().to_string())?,
)),
Payload::FileUpload(filename) => {
let metadata = std::fs::metadata(filename)?;
request.headers.push((
header::CONTENT_LENGTH,
HeaderValue::from_str(&metadata.len().to_string())?,
));
}
_ => (),
}
}
Ok(request)
}
}
fn convert_to_authorization(auth: &str) -> String {
if let Some(auth) = auth.strip_prefix("!!basic ") {
let auth = general_purpose::STANDARD.encode(auth.as_bytes());
format!("Basic {}", auth)
} else if let Some(auth) = auth.strip_prefix("!!bearer ") {
format!("Bearer {}", auth)
} else if auth.contains(":") {
let auth = general_purpose::STANDARD.encode(auth.as_bytes());
format!("Basic {}", auth)
} else {
format!("Bearer {}", auth)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_forced_basic_authorization() {
let auth = convert_to_authorization("!!basic some random value");
assert_eq!("Basic c29tZSByYW5kb20gdmFsdWU=", auth);
}
#[test]
fn test_force_bearer_token() {
let auth = convert_to_authorization("!!bearer user:pass");
assert_eq!("Bearer user:pass", auth);
}
#[test]
fn test_basic_authorization() {
let auth = convert_to_authorization("user:pass");
assert_eq!("Basic dXNlcjpwYXNz", auth);
}
#[test]
fn test_bearer_token() {
let auth = convert_to_authorization("ONXW2ZJAOJQW4ZDPNUQHMYLMOVSQ");
assert_eq!("Bearer ONXW2ZJAOJQW4ZDPNUQHMYLMOVSQ", auth);
}
}