Parcourir la source

Add password change user action

timvisee il y a 7 ans
Parent
commit
16133ce667

+ 12 - 0
Cargo.lock

@@ -345,6 +345,7 @@ dependencies = [
  "ffsend-api 0.1.0",
  "open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "pbr 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -926,6 +927,16 @@ dependencies = [
  "uuid 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rpassword"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.39 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "rustc-demangle"
 version = "0.1.7"
@@ -1554,6 +1565,7 @@ dependencies = [
 "checksum relay 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1576e382688d7e9deecea24417e350d3062d97e32e45d70b1cde65994ff1489a"
 "checksum remove_dir_all 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dfc5b3ce5d5ea144bb04ebd093a9e14e9765bcfec866aecda9b6dec43b3d1e24"
 "checksum reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)" = "241faa9a8ca28a03cbbb9815a5d085f271d4c0168a19181f106aa93240c22ddb"
+"checksum rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d127299b02abda51634f14025aec43ae87a7aa7a95202b6a868ec852607d1451"
 "checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb"
 "checksum safemem 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e27a8b19b835f7aea908818e871f5cc3a5a186550c30773be987e155e8163d8f"
 "checksum schannel 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "fbaffce35eb61c5b00846e73128b0cd62717e7c0ec46abbec132370d013975b4"

+ 2 - 0
IDEAS.md

@@ -1,6 +1,7 @@
 # Ideas
 - Rename DownloadFile to RemoteFile
 - Box errors
+- Info endpoint, to view file info
 - Implement error handling everywhere properly
 - `-y` flag for assume yes
 - `-f` flag for forcing (no interact?)
@@ -29,3 +30,4 @@
 - Release binaries on GitHub
 - Ubuntu PPA package
 - Move API URL generator methods out of remote file class
+- Prompt if a file download password is required

+ 1 - 0
api/src/action/mod.rs

@@ -1,2 +1,3 @@
 pub mod download;
+pub mod password;
 pub mod upload;

+ 29 - 21
api/src/action/password.rs

@@ -1,26 +1,16 @@
 // TODO: define redirect policy
 
-use std::io::{
-    self,
-    Error as IoError,
-    Read,
-};
-use std::sync::{Arc, Mutex};
-
-use failure::Error as FailureError;
-use openssl::symm::decrypt_aead;
-use reqwest::{Client, Response, StatusCode};
+use reqwest::{Client, StatusCode};
 use reqwest::header::Authorization;
-use reqwest::header::ContentLength;
-use serde_json;
 
 use crypto::b64;
 use crypto::key_set::KeySet;
 use crypto::sig::signature_encoded;
 use ext::status_code::StatusCodeExt;
 use file::file::DownloadFile;
-use file::metadata::Metadata;
-use reader::{EncryptedFileWriter, ProgressReporter, ProgressWriter};
+
+/// The name of the header that is used for the authentication nonce.
+const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
 
 /// An action to change a password of an uploaded Send file.
 pub struct Password<'a> {
@@ -33,7 +23,7 @@ pub struct Password<'a> {
 
 impl<'a> Password<'a> {
     /// Construct a new password action for the given file.
-    pub fn new(file: &'a DownloadFile, password: &'a str) -> Self {
+    pub fn new(file: &'a DownloadFile, password: &'a sts) -> Self {
         Self {
             file,
             password,
@@ -50,14 +40,14 @@ impl<'a> Password<'a> {
         let auth_nonce = self.fetch_auth_nonce(client)?;
 
         // Compute a signature
-        let sig = signature_encoded(key.auth_key().unwrap(), &auth_nonce)
+        let sig = signature_encoded(key.auth_key().unwrap(), &nonce)
             .map_err(|_| PrepareError::ComputeSignature)?;
 
         // Derive a new authentication key
-        key.derive_auth_password(self.password, self.file.download_url(true));
+        key.derive_auth_password(self.password, &self.file.download_url(true));
 
         // Send the request to change the password
-        change_password(client, &key, sig)
+        self.change_password(client, &key, sig).map_err(|err| err.into())
     }
 
     /// Fetch the authentication nonce for the file from the Send server.
@@ -105,11 +95,11 @@ impl<'a> Password<'a> {
         client: &Client,
         key: &KeySet,
         sig: String,
-    ) -> Result<Vec<u8>, ChangeError> {
+    ) -> Result<(), ChangeError> {
         // Get the password URL, and send the change
         let url = self.file.api_password_url();
         let response = client.post(url)
-            .json(PasswordData::from(&key))
+            .json(&PasswordData::from(&key))
             .header(Authorization(
                 format!("send-v1 {}", sig)
             ))
@@ -128,7 +118,7 @@ impl<'a> Password<'a> {
 
 /// The data object to send to the password endpoint,
 /// which sets the file password.
-#[derive(Debug, Serializable)]
+#[derive(Debug, Serialize)]
 struct PasswordData {
     /// The authentication key
     auth: String,
@@ -161,6 +151,24 @@ pub enum Error {
     Change(#[cause] ChangeError),
 }
 
+impl From<PrepareError> for Error {
+    fn from(err: PrepareError) -> Error {
+        Error::Prepare(err)
+    }
+}
+
+impl From<AuthError> for Error {
+    fn from(err: AuthError) -> Error {
+        PrepareError::Auth(err).into()
+    }
+}
+
+impl From<ChangeError> for Error {
+    fn from(err: ChangeError) -> Error {
+        Error::Change(err)
+    }
+}
+
 #[derive(Fail, Debug)]
 pub enum PrepareError {
     /// Failed authenticating, needed to set a new password.

+ 1 - 0
cli/Cargo.toml

@@ -23,3 +23,4 @@ failure_derive = "0.1"
 ffsend-api = { version = "*", path = "../api" }
 open = "1"
 pbr = "1"
+rpassword = "2.0"

+ 2 - 3
cli/src/action/download.rs

@@ -24,7 +24,7 @@ impl<'a> Download<'a> {
     /// Invoke the download action.
     // TODO: create a trait for this method
     pub fn invoke(&self) -> Result<(), ActionError> {
-        // Get the download URL
+        // Get the share URL
         let url = self.cmd.url();
 
         // Create a reqwest client
@@ -33,13 +33,12 @@ impl<'a> Download<'a> {
         // Parse the file based on the URL
         // TODO: handle error here
         let file = DownloadFile::parse_url(url)
-            .expect("invalid download URL, could not parse file data");
+            .expect("invalid share URL, could not parse file data");
 
         // Create a progress bar reporter
         let bar = Arc::new(Mutex::new(ProgressBar::new_download()));
 
         // Execute an download action
-        // TODO: do not unwrap, but return an error
         ApiDownload::new(&file).invoke(&client, bar)?;
 
         // TODO: open the file, or it's location

+ 1 - 0
cli/src/action/mod.rs

@@ -1,2 +1,3 @@
 pub mod download;
+pub mod password;
 pub mod upload;

+ 44 - 0
cli/src/action/password.rs

@@ -0,0 +1,44 @@
+use ffsend_api::action::password::Password as ApiPassword;
+use ffsend_api::file::file::DownloadFile;
+use ffsend_api::reqwest::Client;
+
+use cmd::cmd_password::CmdPassword;
+use error::ActionError;
+use util::print_success;
+
+/// A file password action.
+pub struct Password<'a> {
+    cmd: &'a CmdPassword<'a>,
+}
+
+impl<'a> Password<'a> {
+    /// Construct a new password action.
+    pub fn new(cmd: &'a CmdPassword<'a>) -> Self {
+        Self {
+            cmd,
+        }
+    }
+
+    /// Invoke the password action.
+    // TODO: create a trait for this method
+    pub fn invoke(&self) -> Result<(), ActionError> {
+        // Get the share URL
+        let url = self.cmd.url();
+
+        // Create a reqwest client
+        let client = Client::new();
+
+        // Parse the file based on the URL
+        // TODO: handle error here
+        let file = DownloadFile::parse_url(url)
+            .expect("invalid share URL, could not parse file data");
+
+        // Execute an password action
+        ApiPassword::new(&file, &self.cmd.password()).invoke(&client)?;
+
+        // Print a success message
+        print_success("Password set");
+
+        Ok(())
+    }
+}

+ 10 - 10
cli/src/cmd/cmd_download.rs

@@ -15,11 +15,11 @@ impl<'a: 'b, 'b> CmdDownload<'a> {
         // Build the subcommand
         #[allow(unused_mut)]
         let mut cmd = SubCommand::with_name("download")
-            .about("Download files")
+            .about("Download files.")
             .visible_alias("d")
             .visible_alias("down")
             .arg(Arg::with_name("URL")
-                .help("The download URL")
+                .help("The share URL")
                 .required(true)
                 .multiple(false));
 
@@ -32,7 +32,7 @@ impl<'a: 'b, 'b> CmdDownload<'a> {
             .map(|matches| CmdDownload { matches })
     }
 
-    /// Get the URL to download the file from.
+    /// Get the file share URL, to download the file from.
     ///
     /// This method parses the URL into an `Url`.
     /// If the given URL is invalid,
@@ -47,18 +47,18 @@ impl<'a: 'b, 'b> CmdDownload<'a> {
         match Url::parse(url) {
             Ok(url) => url,
             Err(ParseError::EmptyHost) =>
-                quit_error_msg("emtpy host given"),
+                quit_error_msg("Emtpy host given"),
             Err(ParseError::InvalidPort) =>
-                quit_error_msg("invalid host port"),
+                quit_error_msg("Invalid host port"),
             Err(ParseError::InvalidIpv4Address) =>
-                quit_error_msg("invalid IPv4 address in host"),
+                quit_error_msg("Invalid IPv4 address in host"),
             Err(ParseError::InvalidIpv6Address) =>
-                quit_error_msg("invalid IPv6 address in host"),
+                quit_error_msg("Invalid IPv6 address in host"),
             Err(ParseError::InvalidDomainCharacter) =>
-                quit_error_msg("host domains contains an invalid character"),
+                quit_error_msg("Host domains contains an invalid character"),
             Err(ParseError::RelativeUrlWithoutBase) =>
-                quit_error_msg("host domain doesn't contain a host"),
-            _ => quit_error_msg("the given host is invalid"),
+                quit_error_msg("Host domain doesn't contain a host"),
+            _ => quit_error_msg("The given host is invalid"),
         }
     }
 }

+ 84 - 0
cli/src/cmd/cmd_password.rs

@@ -0,0 +1,84 @@
+use ffsend_api::url::{ParseError, Url};
+
+use super::clap::{App, Arg, ArgMatches, SubCommand};
+use rpassword::prompt_password_stderr;
+
+use util::quit_error_msg;
+
+/// The password command.
+pub struct CmdPassword<'a> {
+    matches: &'a ArgMatches<'a>,
+}
+
+impl<'a: 'b, 'b> CmdPassword<'a> {
+    /// Build the sub command definition.
+    pub fn build<'y, 'z>() -> App<'y, 'z> {
+        // Build the subcommand
+        #[allow(unused_mut)]
+        let mut cmd = SubCommand::with_name("password")
+            .about("Change the password of a shared file.")
+            .visible_alias("p")
+            .visible_alias("pass")
+            .arg(Arg::with_name("URL")
+                .help("The share URL")
+                .required(true)
+                .multiple(false))
+            .arg(Arg::with_name("password")
+                .long("password")
+                .short("p")
+                .alias("pass")
+                .value_name("PASSWORD")
+                .help("Specify a password, do not prompt"));
+
+        cmd
+    }
+
+    /// Parse CLI arguments, from the given parent command matches.
+    pub fn parse(parent: &'a ArgMatches<'a>) -> Option<CmdPassword<'a>> {
+        parent.subcommand_matches("password")
+            .map(|matches| CmdPassword { matches })
+    }
+
+    /// Get the file share URL.
+    ///
+    /// This method parses the URL into an `Url`.
+    /// If the given URL is invalid,
+    /// the program will quit with an error message.
+    pub fn url(&'a self) -> Url {
+        // Get the host
+        let url = self.matches.value_of("URL")
+            .expect("missing URL");
+
+        // Parse the URL
+        // TODO: improve these error messages
+        match Url::parse(url) {
+            Ok(url) => url,
+            Err(ParseError::EmptyHost) =>
+                quit_error_msg("Emtpy host given"),
+            Err(ParseError::InvalidPort) =>
+                quit_error_msg("Invalid host port"),
+            Err(ParseError::InvalidIpv4Address) =>
+                quit_error_msg("Invalid IPv4 address in host"),
+            Err(ParseError::InvalidIpv6Address) =>
+                quit_error_msg("Invalid IPv6 address in host"),
+            Err(ParseError::InvalidDomainCharacter) =>
+                quit_error_msg("Host domains contains an invalid character"),
+            Err(ParseError::RelativeUrlWithoutBase) =>
+                quit_error_msg("Host domain doesn't contain a host"),
+            _ => quit_error_msg("The given host is invalid"),
+        }
+    }
+
+    /// Get the password.
+    pub fn password(&'a self) -> String {
+        // Get the password from the arguments
+        if let Some(password) = self.matches.value_of("password") {
+            return password.into();
+        }
+
+        // Prompt for the password
+        // TODO: don't unwrap/expect
+        prompt_password_stderr("New password: ")
+            .expect("failed to read password from stdin")
+    }
+}

+ 8 - 8
cli/src/cmd/cmd_upload.rs

@@ -16,7 +16,7 @@ impl<'a: 'b, 'b> CmdUpload<'a> {
         // Build the subcommand
         #[allow(unused_mut)]
         let mut cmd = SubCommand::with_name("upload")
-            .about("Upload files")
+            .about("Upload files.")
             .visible_alias("u")
             .visible_alias("up")
             .arg(Arg::with_name("FILE")
@@ -72,18 +72,18 @@ impl<'a: 'b, 'b> CmdUpload<'a> {
         match Url::parse(host) {
             Ok(url) => url,
             Err(ParseError::EmptyHost) =>
-                quit_error_msg("emtpy host given"),
+                quit_error_msg("Emtpy host given"),
             Err(ParseError::InvalidPort) =>
-                quit_error_msg("invalid host port"),
+                quit_error_msg("Invalid host port"),
             Err(ParseError::InvalidIpv4Address) =>
-                quit_error_msg("invalid IPv4 address in host"),
+                quit_error_msg("Invalid IPv4 address in host"),
             Err(ParseError::InvalidIpv6Address) =>
-                quit_error_msg("invalid IPv6 address in host"),
+                quit_error_msg("Invalid IPv6 address in host"),
             Err(ParseError::InvalidDomainCharacter) =>
-                quit_error_msg("host domains contains an invalid character"),
+                quit_error_msg("Host domains contains an invalid character"),
             Err(ParseError::RelativeUrlWithoutBase) =>
-                quit_error_msg("host domain doesn't contain a host"),
-            _ => quit_error_msg("the given host is invalid"),
+                quit_error_msg("Host domain doesn't contain a host"),
+            _ => quit_error_msg("The given host is invalid"),
         }
     }
 

+ 7 - 0
cli/src/cmd/handler.rs

@@ -3,6 +3,7 @@ use super::clap::{App, ArgMatches};
 use app::*;
 
 use super::cmd_download::CmdDownload;
+use super::cmd_password::CmdPassword;
 use super::cmd_upload::CmdUpload;
 
 /// CLI argument handler.
@@ -20,6 +21,7 @@ impl<'a: 'b, 'b> Handler<'a> {
             .about(APP_ABOUT)
             .subcommand(CmdUpload::build().display_order(1))
             .subcommand(CmdDownload::build().display_order(2))
+            .subcommand(CmdPassword::build())
     }
 
     /// Parse CLI arguments.
@@ -39,4 +41,9 @@ impl<'a: 'b, 'b> Handler<'a> {
     pub fn download(&'a self) -> Option<CmdDownload<'a>> {
         CmdDownload::parse(&self.matches)
     }
+
+    /// Get the password sub command, if matched.
+    pub fn password(&'a self) -> Option<CmdPassword<'a>> {
+        CmdPassword::parse(&self.matches)
+    }
 }

+ 1 - 0
cli/src/cmd/mod.rs

@@ -1,6 +1,7 @@
 extern crate clap;
 
 pub mod cmd_download;
+pub mod cmd_password;
 pub mod cmd_upload;
 pub mod handler;
 

+ 11 - 0
cli/src/error.rs

@@ -1,4 +1,5 @@
 use ffsend_api::action::download::Error as DownloadError;
+use ffsend_api::action::password::Error as PasswordError;
 use ffsend_api::action::upload::Error as UploadError;
 
 #[derive(Fail, Debug)]
@@ -24,6 +25,10 @@ pub enum ActionError {
     /// An error occurred while invoking the download action.
     #[fail(display = "Failed to download the requested file")]
     Download(#[cause] DownloadError),
+
+    /// An error occurred while invoking the password action.
+    #[fail(display = "Failed to change the password")]
+    Password(#[cause] PasswordError),
 }
 
 impl From<DownloadError> for ActionError {
@@ -32,6 +37,12 @@ impl From<DownloadError> for ActionError {
     }
 }
 
+impl From<PasswordError> for ActionError {
+    fn from(err: PasswordError) -> ActionError {
+        ActionError::Password(err)
+    }
+}
+
 impl From<UploadError> for ActionError {
     fn from(err: UploadError) -> ActionError {
         ActionError::Upload(err)

+ 8 - 0
cli/src/main.rs

@@ -2,6 +2,7 @@ extern crate failure;
 #[macro_use]
 extern crate failure_derive;
 extern crate ffsend_api;
+extern crate rpassword;
 
 mod action;
 mod app;
@@ -11,6 +12,7 @@ mod progress;
 mod util;
 
 use action::download::Download;
+use action::password::Password;
 use action::upload::Upload;
 use cmd::Handler;
 use error::Error;
@@ -44,6 +46,12 @@ fn invoke_action(handler: &Handler) -> Result<(), Error> {
             .map_err(|err| err.into());
     }
 
+    // Match the password command
+    if let Some(cmd) = handler.password() {
+        return Password::new(&cmd).invoke()
+            .map_err(|err| err.into());
+    }
+
     // No subcommand was selected, show general help
     Handler::build()
         .print_help()

+ 5 - 0
cli/src/util.rs

@@ -15,6 +15,11 @@ use self::colored::*;
 use failure::{self, Fail};
 use ffsend_api::url::Url;
 
+/// Print a success message.
+pub fn print_success(msg: &str) {
+    println!("{}", msg.green());
+}
+
 /// Print the given error in a proper format for the user,
 /// with it's causes.
 pub fn print_error<E: Fail>(err: E) {