Browse Source

Add password support to upload action

timvisee 7 years ago
parent
commit
1c17ef0fcd
4 changed files with 82 additions and 7 deletions
  1. 1 0
      IDEAS.md
  2. 50 6
      api/src/action/upload.rs
  3. 2 1
      cli/src/action/upload.rs
  4. 29 0
      cli/src/cmd/cmd_upload.rs

+ 1 - 0
IDEAS.md

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

+ 50 - 6
api/src/action/upload.rs

@@ -22,6 +22,7 @@ use url::{
     Url,
 };
 
+use crypto::b64;
 use crypto::key_set::KeySet;
 use ext::status_code::StatusCodeExt;
 use file::file::File as SendFile;
@@ -32,9 +33,16 @@ use reader::{
     ProgressReader,
     ProgressReporter,
 };
+use super::password::{
+    Error as PasswordError,
+    Password,
+};
 
 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.
 pub struct Upload {
     /// The Send host to upload the file to.
@@ -42,14 +50,18 @@ pub struct Upload {
 
     /// The file to upload.
     path: PathBuf,
+
+    /// An optional password to protect the file with.
+    password: Option<String>,
 }
 
 impl Upload {
     /// 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 {
             host,
             path,
+            password,
         }
     }
 
@@ -82,15 +94,24 @@ impl Upload {
             .start(reader_len);
 
         // Execute the request
-        let result = self.execute_request(req, client, &key)
-            .map_err(|err| err.into());
+        // TODO: don't fail on nonce error, just don't use it
+        let (result, nonce) = self.execute_request(req, client, &key)?;
 
         // Mark the reporter as finished
         reporter.lock()
             .map_err(|_| UploadError::Progress)?
             .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.
@@ -197,7 +218,7 @@ impl Upload {
     /// Execute the given request, and create a file object that represents the
     /// uploaded file.
     fn execute_request(&self, req: Request, client: &Client, key: &KeySet) 
-        -> Result<SendFile, UploadError>
+        -> Result<(SendFile, Option<Vec<u8>>), UploadError>
     {
         // Execute the request
         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
         let response: UploadResponse = match response.json() {
             Ok(response) => response,
@@ -221,7 +252,10 @@ impl Upload {
         };
 
         // 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.
     #[fail(display = "Failed to upload the file")]
     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 {
@@ -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)]
 pub enum PrepareError {
     /// Failed to prepare the file metadata for uploading.

+ 2 - 1
cli/src/action/upload.rs

@@ -39,7 +39,8 @@ impl<'a> Upload<'a> {
         let bar = Arc::new(Mutex::new(ProgressBar::new_upload()));
 
         // 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
         let url = file.download_url(true);

+ 29 - 0
cli/src/cmd/cmd_upload.rs

@@ -1,5 +1,6 @@
 use ffsend_api::url::{ParseError, Url};
 
+use rpassword::prompt_password_stderr;
 use super::clap::{App, Arg, ArgMatches, SubCommand};
 
 use app::SEND_DEF_HOST;
@@ -23,6 +24,14 @@ impl<'a: 'b, 'b> CmdUpload<'a> {
                 .help("The file to upload")
                 .required(true)
                 .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")
                 .long("host")
                 .short("h")
@@ -97,4 +106,24 @@ impl<'a: 'b, 'b> CmdUpload<'a> {
     pub fn copy(&self) -> bool {
         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")
+        )
+    }
 }