omegaupload/cli/src/main.rs

173 lines
4.3 KiB
Rust
Raw Normal View History

2021-10-19 02:18:33 -07:00
#![warn(clippy::nursery, clippy::pedantic)]
#![deny(unsafe_code)]
2021-10-16 09:50:11 -07:00
use std::io::{Read, Write};
2021-10-30 21:00:09 -07:00
use std::path::PathBuf;
2021-10-16 09:50:11 -07:00
use anyhow::{anyhow, bail, Context, Result};
use atty::Stream;
2021-10-26 22:30:37 -07:00
use clap::Parser;
2021-10-30 18:38:55 -07:00
use omegaupload_common::crypto::{open_in_place, seal_in_place};
2021-10-30 21:00:09 -07:00
use omegaupload_common::secrecy::{ExposeSecret, SecretVec};
2021-10-27 19:16:43 -07:00
use omegaupload_common::{
2021-10-30 18:38:55 -07:00
base64, Expiration, ParsedUrl, Url, API_ENDPOINT, EXPIRATION_HEADER_NAME,
2021-10-27 19:16:43 -07:00
};
2021-10-16 09:50:11 -07:00
use reqwest::blocking::Client;
2021-10-21 18:35:54 -07:00
use reqwest::header::EXPIRES;
2021-10-16 09:50:11 -07:00
use reqwest::StatusCode;
2021-10-26 22:30:37 -07:00
#[derive(Parser)]
2021-10-16 09:50:11 -07:00
struct Opts {
#[clap(subcommand)]
action: Action,
}
2021-10-26 22:30:37 -07:00
#[derive(Parser)]
2021-10-16 09:50:11 -07:00
enum Action {
Upload {
2021-10-27 02:23:37 -07:00
/// The OmegaUpload instance to upload data to.
2021-10-16 09:50:11 -07:00
url: Url,
2021-10-27 02:23:37 -07:00
/// Encrypt the uploaded paste with the provided password, preventing
/// public access.
2021-10-16 09:50:11 -07:00
#[clap(short, long)]
2021-10-30 21:00:09 -07:00
password: bool,
2021-10-27 19:16:43 -07:00
#[clap(short, long)]
duration: Option<Expiration>,
2021-10-30 21:00:09 -07:00
path: PathBuf,
2021-10-16 09:50:11 -07:00
},
Download {
2021-10-27 02:23:37 -07:00
/// The paste to download.
2021-10-16 09:50:11 -07:00
url: ParsedUrl,
},
}
fn main() -> Result<()> {
let opts = Opts::parse();
match opts.action {
2021-10-27 19:16:43 -07:00
Action::Upload {
url,
password,
duration,
2021-10-30 21:00:09 -07:00
path,
} => handle_upload(url, password, duration, path),
2021-10-16 09:50:11 -07:00
Action::Download { url } => handle_download(url),
}?;
Ok(())
}
2021-10-27 19:16:43 -07:00
fn handle_upload(
mut url: Url,
2021-10-30 21:00:09 -07:00
password: bool,
2021-10-27 19:16:43 -07:00
duration: Option<Expiration>,
2021-10-30 21:00:09 -07:00
path: PathBuf,
2021-10-27 19:16:43 -07:00
) -> Result<()> {
2021-10-16 09:50:11 -07:00
url.set_fragment(None);
if atty::is(Stream::Stdin) {
bail!("This tool requires non interactive CLI. Pipe something in!");
}
2021-10-30 18:38:55 -07:00
let (data, key) = {
2021-10-30 21:00:09 -07:00
let mut container = std::fs::read(path)?;
let password = if password {
let mut buffer = vec![];
std::io::stdin().read_to_end(&mut buffer)?;
Some(SecretVec::new(buffer))
} else {
None
};
2021-10-30 18:38:55 -07:00
let enc_key = seal_in_place(&mut container, password)?;
2021-10-30 21:00:09 -07:00
let key = base64::encode(&enc_key.expose_secret().as_ref());
2021-10-30 18:38:55 -07:00
(container, key)
2021-10-16 09:50:11 -07:00
};
2021-10-27 19:16:43 -07:00
let mut res = Client::new().post(url.as_ref());
if let Some(duration) = duration {
res = res.header(&*EXPIRATION_HEADER_NAME, duration);
}
let res = res.body(data).send().context("Request to server failed")?;
2021-10-16 09:50:11 -07:00
if res.status() != StatusCode::OK {
bail!("Upload failed. Got HTTP error {}", res.status());
}
url.path_segments_mut()
.map_err(|_| anyhow!("Failed to get base URL"))?
.extend(std::iter::once(res.text()?));
2021-10-30 21:00:09 -07:00
let fragment = if password {
format!("key:{}!pw", key)
} else {
key
};
2021-10-16 09:50:11 -07:00
url.set_fragment(Some(&fragment));
println!("{}", url);
Ok(())
}
2021-10-27 01:49:06 -07:00
fn handle_download(mut url: ParsedUrl) -> Result<()> {
2021-10-27 19:16:43 -07:00
url.sanitized_url
.set_path(&format!("{}{}", API_ENDPOINT, url.sanitized_url.path()));
2021-10-16 09:50:11 -07:00
let res = Client::new()
.get(url.sanitized_url)
.send()
.context("Failed to get data")?;
if res.status() != StatusCode::OK {
bail!("Got bad response from server: {}", res.status());
}
2021-10-27 01:49:06 -07:00
let expiration_text = res
.headers()
2021-10-21 18:35:54 -07:00
.get(EXPIRES)
.and_then(|v| Expiration::try_from(v).ok())
.as_ref()
.map_or_else(
|| "This paste will not expire.".to_string(),
ToString::to_string,
);
2021-10-21 18:35:54 -07:00
2021-10-16 09:50:11 -07:00
let mut data = res.bytes()?.as_ref().to_vec();
2021-10-30 18:38:55 -07:00
let mut password = None;
2021-10-16 09:50:11 -07:00
if url.needs_password {
// Only print prompt on interactive, else it messes with output
if atty::is(Stream::Stdout) {
print!("Please enter the password to access this document: ");
std::io::stdout().flush()?;
}
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
input.pop(); // last character is \n, we need to drop it.
2021-10-30 18:38:55 -07:00
password = Some(input);
2021-10-16 09:50:11 -07:00
}
2021-10-30 21:00:09 -07:00
open_in_place(
&mut data,
&url.decryption_key,
password.map(|v| SecretVec::new(v.into_bytes())),
)?;
2021-10-16 09:50:11 -07:00
if atty::is(Stream::Stdout) {
if let Ok(data) = String::from_utf8(data) {
std::io::stdout().write_all(data.as_bytes())?;
} else {
bail!("Binary output detected. Please pipe to a file.");
}
} else {
std::io::stdout().write_all(&data)?;
}
2021-10-21 18:35:54 -07:00
eprintln!("{}", expiration_text);
2021-10-16 09:50:11 -07:00
Ok(())
}