Add password support to upload action

This commit is contained in:
timvisee 2018-03-29 00:20:57 +02:00
parent 4462e1d0a6
commit 1c17ef0fcd
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
4 changed files with 82 additions and 7 deletions

View file

@ -32,3 +32,4 @@
- Ubuntu PPA package - Ubuntu PPA package
- Move API URL generator methods out of remote file class - Move API URL generator methods out of remote file class
- Prompt if a file download password is required - Prompt if a file download password is required
- Do not allow empty passwords (must force with `-f`)

View file

@ -22,6 +22,7 @@ use url::{
Url, Url,
}; };
use crypto::b64;
use crypto::key_set::KeySet; use crypto::key_set::KeySet;
use ext::status_code::StatusCodeExt; use ext::status_code::StatusCodeExt;
use file::file::File as SendFile; use file::file::File as SendFile;
@ -32,9 +33,16 @@ use reader::{
ProgressReader, ProgressReader,
ProgressReporter, ProgressReporter,
}; };
use super::password::{
Error as PasswordError,
Password,
};
type EncryptedReader = ProgressReader<BufReader<EncryptedFileReader>>; type EncryptedReader = ProgressReader<BufReader<EncryptedFileReader>>;
/// The name of the header that is used for the authentication nonce.
const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
/// A file upload action to a Send server. /// A file upload action to a Send server.
pub struct Upload { pub struct Upload {
/// The Send host to upload the file to. /// The Send host to upload the file to.
@ -42,14 +50,18 @@ pub struct Upload {
/// The file to upload. /// The file to upload.
path: PathBuf, path: PathBuf,
/// An optional password to protect the file with.
password: Option<String>,
} }
impl Upload { impl Upload {
/// Construct a new upload action. /// Construct a new upload action.
pub fn new(host: Url, path: PathBuf) -> Self { pub fn new(host: Url, path: PathBuf, password: Option<String>) -> Self {
Self { Self {
host, host,
path, path,
password,
} }
} }
@ -82,15 +94,24 @@ impl Upload {
.start(reader_len); .start(reader_len);
// Execute the request // Execute the request
let result = self.execute_request(req, client, &key) // TODO: don't fail on nonce error, just don't use it
.map_err(|err| err.into()); let (result, nonce) = self.execute_request(req, client, &key)?;
// Mark the reporter as finished // Mark the reporter as finished
reporter.lock() reporter.lock()
.map_err(|_| UploadError::Progress)? .map_err(|_| UploadError::Progress)?
.finish(); .finish();
result // Change the password if set
if let Some(password) = self.password {
Password::new(
&result.to_download_file(),
&password,
nonce,
).invoke(client)?;
}
Ok(result)
} }
/// Create a blob of encrypted metadata. /// Create a blob of encrypted metadata.
@ -197,7 +218,7 @@ impl Upload {
/// Execute the given request, and create a file object that represents the /// Execute the given request, and create a file object that represents the
/// uploaded file. /// uploaded file.
fn execute_request(&self, req: Request, client: &Client, key: &KeySet) fn execute_request(&self, req: Request, client: &Client, key: &KeySet)
-> Result<SendFile, UploadError> -> Result<(SendFile, Option<Vec<u8>>), UploadError>
{ {
// Execute the request // Execute the request
let mut response = match client.execute(req) { let mut response = match client.execute(req) {
@ -214,6 +235,16 @@ impl Upload {
); );
} }
// Try to get the nonce, don't error on failure
let nonce = response.headers()
.get_raw(HEADER_AUTH_NONCE)
.and_then(|h| h.one())
.and_then(|line| String::from_utf8(line.to_vec()).ok())
.and_then(|line| line.split_terminator(" ").skip(1).next()
.map(|line| line.to_owned())
)
.and_then(|nonce| b64::decode(&nonce).ok());
// Decode the response // Decode the response
let response: UploadResponse = match response.json() { let response: UploadResponse = match response.json() {
Ok(response) => response, Ok(response) => response,
@ -221,7 +252,10 @@ impl Upload {
}; };
// Transform the responce into a file object // Transform the responce into a file object
Ok(response.into_file(self.host.clone(), &key)?) Ok((
response.into_file(self.host.clone(), &key)?,
nonce,
))
} }
} }
@ -323,6 +357,10 @@ pub enum Error {
/// An error occurred while uploading the file. /// An error occurred while uploading the file.
#[fail(display = "Failed to upload the file")] #[fail(display = "Failed to upload the file")]
Upload(#[cause] UploadError), Upload(#[cause] UploadError),
/// An error occurred while setting the password.
#[fail(display = "Failed to set the password")]
Password(#[cause] PasswordError),
} }
impl From<MetaError> for Error { impl From<MetaError> for Error {
@ -349,6 +387,12 @@ impl From<UploadError> for Error {
} }
} }
impl From<PasswordError> for Error {
fn from(err: PasswordError) -> Error {
Error::Password(err)
}
}
#[derive(Fail, Debug)] #[derive(Fail, Debug)]
pub enum PrepareError { pub enum PrepareError {
/// Failed to prepare the file metadata for uploading. /// Failed to prepare the file metadata for uploading.

View file

@ -39,7 +39,8 @@ impl<'a> Upload<'a> {
let bar = Arc::new(Mutex::new(ProgressBar::new_upload())); let bar = Arc::new(Mutex::new(ProgressBar::new_upload()));
// Execute an upload action // Execute an upload action
let file = ApiUpload::new(host, path).invoke(&client, bar)?; let file = ApiUpload::new(host, path, self.cmd.password())
.invoke(&client, bar)?;
// Get the download URL, and report it in the console // Get the download URL, and report it in the console
let url = file.download_url(true); let url = file.download_url(true);

View file

@ -1,5 +1,6 @@
use ffsend_api::url::{ParseError, Url}; use ffsend_api::url::{ParseError, Url};
use rpassword::prompt_password_stderr;
use super::clap::{App, Arg, ArgMatches, SubCommand}; use super::clap::{App, Arg, ArgMatches, SubCommand};
use app::SEND_DEF_HOST; use app::SEND_DEF_HOST;
@ -23,6 +24,14 @@ impl<'a: 'b, 'b> CmdUpload<'a> {
.help("The file to upload") .help("The file to upload")
.required(true) .required(true)
.multiple(false)) .multiple(false))
.arg(Arg::with_name("password")
.long("password")
.short("p")
.alias("pass")
.value_name("PASSWORD")
.min_values(0)
.max_values(1)
.help("Protect file with a password"))
.arg(Arg::with_name("host") .arg(Arg::with_name("host")
.long("host") .long("host")
.short("h") .short("h")
@ -97,4 +106,24 @@ impl<'a: 'b, 'b> CmdUpload<'a> {
pub fn copy(&self) -> bool { pub fn copy(&self) -> bool {
self.matches.is_present("copy") self.matches.is_present("copy")
} }
/// Get the password.
pub fn password(&'a self) -> Option<String> {
// Return none if the property was not set
if !self.matches.is_present("password") {
return None;
}
// Get the password from the arguments
if let Some(password) = self.matches.value_of("password") {
return Some(password.into());
}
// Prompt for the password
// TODO: don't unwrap/expect
Some(
prompt_password_stderr("Password: ")
.expect("failed to read password from stdin")
)
}
} }