diff --git a/Cargo.lock b/Cargo.lock index 3f3bf72..7c4deb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,6 +279,25 @@ dependencies = [ "backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "failure" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "failure_derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", + "synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -291,6 +310,8 @@ dependencies = [ "arrayref 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "hkdf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "hyper 0.11.24 (registry+https://github.com/rust-lang/crates.io-index)", "mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -310,6 +331,7 @@ version = "0.1.0" dependencies = [ "clap 2.31.2 (registry+https://github.com/rust-lang/crates.io-index)", "clipboard 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "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)", @@ -788,6 +810,11 @@ dependencies = [ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "quote" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "quote" version = "0.4.2" @@ -1021,6 +1048,16 @@ name = "strsim" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "syn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "syn" version = "0.12.14" @@ -1031,6 +1068,23 @@ dependencies = [ "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "synom" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "synstructure" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "take" version = "0.1.0" @@ -1265,6 +1319,11 @@ name = "unicode-width" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "unicode-xid" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "unicode-xid" version = "0.1.0" @@ -1414,6 +1473,8 @@ dependencies = [ "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" "checksum encoding_rs 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98fd0f24d1fb71a4a6b9330c8ca04cbd4e7cc5d846b54ca74ff376bc7c9f798d" "checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" +"checksum failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "934799b6c1de475a012a02dab0ace1ace43789ee4b99bcfbf1a2e3e8ced5de82" +"checksum failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cdda555bb90c9bb67a3b670a0f42de8e73f5981524123ad8578aafec8ddb8b" "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" "checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" "checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" @@ -1471,6 +1532,7 @@ dependencies = [ "checksum phf_shared 0.7.21 (registry+https://github.com/rust-lang/crates.io-index)" = "07e24b0ca9643bdecd0632f2b3da6b1b89bbb0030e0b992afc1113b23a7bc2f2" "checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903" "checksum proc-macro2 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "cd07deb3c6d1d9ff827999c7f9b04cdfd66b1b17ae508e14fe47b620f2282ae0" +"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum quote 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eca14c727ad12702eb4b6bfb5a232287dcf8385cb8ca83a3eeaf6519c44c408" "checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" @@ -1499,7 +1561,10 @@ dependencies = [ "checksum slab 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fdeff4cd9ecff59ec7e3744cbca73dfe5ac35c2aedb2cfba8a1c715a18912e9d" "checksum smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013" "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" +"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" "checksum syn 0.12.14 (registry+https://github.com/rust-lang/crates.io-index)" = "8c5bc2d6ff27891209efa5f63e9de78648d7801f085e4653701a692ce938d6fd" +"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" +"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd" "checksum take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" "checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" @@ -1524,6 +1589,7 @@ dependencies = [ "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "51ccda9ef9efa3f7ef5d91e8f9b83bbe6955f9bf86aec89d5cce2c874625920f" "checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" +"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" "checksum url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f808aadd8cfec6ef90e4a14eb46f24511824d1ac596b9682703c87056c8678b7" diff --git a/api/Cargo.toml b/api/Cargo.toml index ea19cd3..11045ca 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -8,6 +8,8 @@ workspace = ".." arrayref = "0.3" base64 = "0.9" chrono = "0.4" +failure = "0.1" +failure_derive = "0.1" hkdf = "0.3" hyper = "0.11.9" # same as reqwest mime_guess = "2.0.0-alpha.2" diff --git a/api/src/action/download.rs b/api/src/action/download.rs index c364c36..dc36fae 100644 --- a/api/src/action/download.rs +++ b/api/src/action/download.rs @@ -1,3 +1,5 @@ +// TODO: define redirect policy + use std::fs::File; use std::io::{ self, @@ -6,12 +8,9 @@ use std::io::{ }; use std::sync::{Arc, Mutex}; +use failure::Error as FailureError; use openssl::symm::decrypt_aead; -use reqwest::{ - Client, - Error as ReqwestError, - Response, -}; +use reqwest::{Client, Response, StatusCode}; use reqwest::header::Authorization; use reqwest::header::ContentLength; use serde_json; @@ -23,13 +22,11 @@ use file::file::DownloadFile; use file::metadata::Metadata; use reader::{EncryptedFileWriter, ProgressReporter, ProgressWriter}; -pub type Result = ::std::result::Result; -type StdResult = ::std::result::Result; - /// The name of the header that is used for the authentication nonce. const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate"; -// TODO: experiment with `iv` of `None` in decrypt logic +/// The HTTP status code that is returned for expired files. +const FILE_EXPIRED_STATUS: StatusCode = StatusCode::NotFound; /// A file upload action to a Send server. pub struct Download<'a> { @@ -50,25 +47,26 @@ impl<'a> Download<'a> { self, client: &Client, reporter: Arc>, - ) -> Result<()> { + ) -> Result<(), Error> { // Create a key set for the file let mut key = KeySet::from(self.file); // Fetch the authentication nonce - let auth_nonce = self.fetch_auth_nonce(client) - .map_err(|err| DownloadError::AuthError(err))?; + let auth_nonce = self.fetch_auth_nonce(client)?; // Fetch the meta nonce, set the input vector let meta_nonce = self.fetch_meta_nonce(&client, &mut key, auth_nonce) - .map_err(|err| DownloadError::MetaError(err))?; + .map_err(|err| Error::Request(RequestError::Meta(err)))?; // Open the file we will write to // TODO: this should become a temporary file first + // TODO: use the uploaded file name as default let out = File::create("downloaded.zip") - .map_err(|err| DownloadError::FileOpenError(err))?; + .map_err(|err| Error::File(FileError::Create(err)))?; // Create the file reader for downloading - let (reader, len) = self.create_file_reader(&key, meta_nonce, &client); + let (reader, len) = self.create_file_reader(&key, meta_nonce, &client) + .map_err(|err| Error::Download(err))?; // Create the file writer let writer = self.create_file_writer( @@ -76,10 +74,11 @@ impl<'a> Download<'a> { len, &key, reporter.clone(), - ); + ).map_err(|err| Error::File(err))?; // Download the file - self.download(reader, writer, len, reporter); + self.download(reader, writer, len, reporter) + .map_err(|err| Error::Download(err))?; // TODO: return the file path // TODO: return the new remote state (does it still exist remote) @@ -89,35 +88,40 @@ impl<'a> Download<'a> { /// Fetch the authentication nonce for the file from the Send server. fn fetch_auth_nonce(&self, client: &Client) - -> StdResult, AuthError> + -> Result, Error> { // Get the download url, and parse the nonce let download_url = self.file.download_url(false); let response = client.get(download_url) .send() - .map_err(|_| AuthError::NonceReqFail)?; + .map_err(|_| AuthError::NonceReq)?; // Validate the status code - // TODO: allow redirects here? - if !response.status().is_success() { - return Err(AuthError::NonceReqStatusErr); + let status = response.status(); + if !status.is_success() { + // Handle expired files + if status == FILE_EXPIRED_STATUS { + return Err(Error::Expired); + } else { + return Err(AuthError::NonceReqStatus(status, status.err_text()).into()); + } } // Get the authentication nonce b64::decode( response.headers() .get_raw(HEADER_AUTH_NONCE) - .ok_or(AuthError::MissingNonceHeader)? + .ok_or(AuthError::NoNonceHeader)? .one() - .ok_or(AuthError::EmptyNonceHeader) + .ok_or(AuthError::MalformedNonce) .and_then(|line| String::from_utf8(line.to_vec()) - .map_err(|_| AuthError::MalformedNonceHeader) + .map_err(|_| AuthError::MalformedNonce) )? .split_terminator(" ") .skip(1) .next() - .ok_or(AuthError::MissingNonceHeader)? - ).map_err(|_| AuthError::MalformedNonce) + .ok_or(AuthError::MalformedNonce)? + ).map_err(|_| AuthError::MalformedNonce.into()) } /// Fetch the metadata nonce. @@ -131,7 +135,7 @@ impl<'a> Download<'a> { client: &Client, key: &mut KeySet, auth_nonce: Vec, - ) -> StdResult, MetaError> { + ) -> Result, MetaError> { // Fetch the metadata and the nonce let (metadata, meta_nonce) = self.fetch_metadata(client, key, auth_nonce)?; @@ -151,47 +155,47 @@ impl<'a> Download<'a> { client: &Client, key: &KeySet, auth_nonce: Vec, - ) -> StdResult<(Metadata, Vec), MetaError> { + ) -> Result<(Metadata, Vec), MetaError> { // Compute the cryptographic signature for authentication let sig = signature_encoded(key.auth_key().unwrap(), &auth_nonce) - .map_err(|_| MetaError::ComputeSignatureFail)?; + .map_err(|_| MetaError::ComputeSignature)?; - // Buidl the request, fetch the encrypted metadata + // Build the request, fetch the encrypted metadata let mut response = client.get(self.file.api_meta_url()) .header(Authorization( format!("send-v1 {}", sig) )) .send() - .map_err(|_| MetaError::NonceReqFail)?; + .map_err(|_| MetaError::NonceReq)?; // Validate the status code - // TODO: allow redirects here? - if !response.status().is_success() { - return Err(MetaError::NonceReqStatusErr); + let status = response.status(); + if !status.is_success() { + return Err(MetaError::NonceReqStatus(status, status.err_text())); } // Get the metadata nonce let nonce = b64::decode( response.headers() .get_raw(HEADER_AUTH_NONCE) - .ok_or(MetaError::MissingNonceHeader)? + .ok_or(MetaError::NoNonceHeader)? .one() - .ok_or(MetaError::EmptyNonceHeader) + .ok_or(MetaError::MalformedNonce) .and_then(|line| String::from_utf8(line.to_vec()) - .map_err(|_| MetaError::MalformedNonceHeader) + .map_err(|_| MetaError::MalformedNonce) )? .split_terminator(" ") .skip(1) .next() - .ok_or(MetaError::MissingNonceHeader)? + .ok_or(MetaError::MalformedNonce)? ).map_err(|_| MetaError::MalformedNonce)?; // Parse the metadata response, and decrypt it Ok(( response.json::() - .map_err(|_| MetaError::MalformedMetadata)? + .map_err(|_| MetaError::Malformed)? .decrypt_metadata(&key) - .map_err(|_| MetaError::DecryptMetadataFail)?, + .map_err(|_| MetaError::Decrypt)?, nonce, )) } @@ -206,36 +210,31 @@ impl<'a> Download<'a> { key: &KeySet, meta_nonce: Vec, client: &Client, - ) -> (Response, u64) { + ) -> Result<(Response, u64), DownloadError> { // Compute the cryptographic signature - // TODO: use the metadata nonce here? - // TODO: do not unwrap, return an error let sig = signature_encoded(key.auth_key().unwrap(), &meta_nonce) - .expect("failed to compute file signature"); + .map_err(|_| DownloadError::ComputeSignature)?; // Build and send the download request - // TODO: do not unwrap here, return error let response = client.get(self.file.api_download_url()) .header(Authorization( format!("send-v1 {}", sig) )) .send() - .expect("failed to fetch file, failed to send request"); + .map_err(|_| DownloadError::Request)?; // Validate the status code - // TODO: allow redirects here? - if !response.status().is_success() { - // TODO: return error here - panic!("failed to fetch file, request status is not successful"); + let status = response.status(); + if !status.is_success() { + return Err(DownloadError::RequestStatus(status, status.err_text())); } // Get the content length // TODO: make sure there is enough disk space let len = response.headers().get::() - .expect("failed to fetch file, missing content length header") - .0; + .ok_or(DownloadError::NoLength)?.0; - (response, len) + Ok((response, len)) } /// Create a file writer. @@ -248,7 +247,7 @@ impl<'a> Download<'a> { len: u64, key: &KeySet, reporter: Arc>, - ) -> ProgressWriter { + ) -> Result, FileError> { // Build an encrypted writer let mut writer = ProgressWriter::new( EncryptedFileWriter::new( @@ -257,13 +256,13 @@ impl<'a> Download<'a> { KeySet::cipher(), key.file_key().unwrap(), key.iv(), - ).expect("failed to create encrypted writer") - ).expect("failed to create encrypted writer"); + ).map_err(|_| FileError::EncryptedWriter)? + ).map_err(|_| FileError::EncryptedWriter)?; // Set the reporter writer.set_reporter(reporter.clone()); - writer + Ok(writer) } /// Download the file from the reader, and write it to the writer. @@ -275,80 +274,29 @@ impl<'a> Download<'a> { mut writer: ProgressWriter, len: u64, reporter: Arc>, - ) { + ) -> Result<(), DownloadError> { // Start the writer reporter.lock() - .expect("unable to start progress, failed to get lock") + .map_err(|_| DownloadError::Progress)? .start(len); // Write to the output file - io::copy(&mut reader, &mut writer) - .expect("failed to download and decrypt file"); + io::copy(&mut reader, &mut writer).map_err(|_| DownloadError::Download)?; // Finish reporter.lock() - .expect("unable to finish progress, failed to get lock") + .map_err(|_| DownloadError::Progress)? .finish(); // Verify the writer - // TODO: delete the file if verification failed, show a proper error - assert!(writer.unwrap().verified(), "downloaded and decrypted file could not be verified"); + if writer.unwrap().verified() { + Ok(()) + } else { + Err(DownloadError::Verify) + } } } -/// Errors that may occur in the upload action. -#[derive(Debug)] -pub enum DownloadError { - /// An authentication related error. - AuthError(AuthError), - - /// An metadata related error. - MetaError(MetaError), - - /// An error occurred while opening the file for writing. - FileOpenError(IoError), - - /// The given file is not not an existing file. - /// Maybe it is a directory, or maybe it doesn't exist. - NotAFile, - - /// An error occurred while opening or reading a file. - FileError, - - /// An error occurred while encrypting the file. - EncryptionError, - - /// An error occurred while while processing the request. - /// This also covers things like HTTP 404 errors. - RequestError(ReqwestError), - - /// An error occurred while decoding the response data. - DecodeError, -} - -#[derive(Debug)] -pub enum AuthError { - NonceReqFail, - NonceReqStatusErr, - MissingNonceHeader, - EmptyNonceHeader, - MalformedNonceHeader, - MalformedNonce, -} - -#[derive(Debug)] -pub enum MetaError { - ComputeSignatureFail, - NonceReqFail, - NonceReqStatusErr, - MissingNonceHeader, - EmptyNonceHeader, - MalformedNonceHeader, - MalformedNonce, - MalformedMetadata, - DecryptMetadataFail, -} - /// The metadata response from the server, when fetching the data through /// the API. /// @@ -366,11 +314,9 @@ impl MetadataResponse { /// /// The decrypted data is verified using an included tag. /// If verification failed, an error is returned. - // TODO: do not unwrap, return a proper error - pub fn decrypt_metadata(&self, key_set: &KeySet) -> Result { + pub fn decrypt_metadata(&self, key_set: &KeySet) -> Result { // Decode the metadata - let raw = b64::decode(&self.meta) - .expect("failed to decode metadata from server"); + let raw = b64::decode(&self.meta)?; // Get the encrypted metadata, and it's tag let (encrypted, tag) = raw.split_at(raw.len() - 16); @@ -378,7 +324,6 @@ impl MetadataResponse { assert_eq!(tag.len(), 16); // Decrypt the metadata - // TODO: do not unwrap, return an error let meta = decrypt_aead( KeySet::cipher(), key_set.meta_key().unwrap(), @@ -386,12 +331,177 @@ impl MetadataResponse { &[], encrypted, &tag, - ).expect("failed to decrypt metadata, invalid tag?"); + )?; // Parse the metadata, and return - Ok( - serde_json::from_slice(&meta) - .expect("failed to parse decrypted metadata as JSON") - ) + Ok(serde_json::from_slice(&meta)?) + } +} + +#[derive(Fail, Debug)] +pub enum Error { + /// A general error occurred while requesting the file data. + /// This may be because authentication failed, because decrypting the + /// file metadata didn't succeed, or due to some other reason. + #[fail(display = "failed to request file data")] + Request(#[cause] RequestError), + + /// The given Send file has expired, or did never exist in the first place. + /// Therefore the file could not be downloaded. + #[fail(display = "the file has expired or did never exist")] + Expired, + + /// An error occurred while downloading the file. + #[fail(display = "failed to download the file")] + Download(#[cause] DownloadError), + + /// An error occurred while decrypting the downloaded file. + #[fail(display = "failed to decrypt the downloaded file")] + Decrypt, + + /// An error occurred while opening or writing to the target file. + // TODO: show what file this is about + #[fail(display = "could not open the file for writing")] + File(#[cause] FileError), +} + +impl From for Error { + fn from(err: AuthError) -> Error { + Error::Request(RequestError::Auth(err)) + } +} + +#[derive(Fail, Debug)] +pub enum RequestError { + /// Failed authenticating, in order to fetch the file data. + #[fail(display = "failed to authenticate")] + Auth(#[cause] AuthError), + + /// Failed to retrieve the file metadata. + #[fail(display = "failed to retrieve file metadata")] + Meta(#[cause] MetaError), +} + +#[derive(Fail, Debug)] +pub enum AuthError { + /// Sending the request to gather the authentication encryption nonce + /// failed. + #[fail(display = "failed to request authentication nonce")] + NonceReq, + + /// The response for fetching the authentication encryption nonce + /// indicated an error and wasn't successful. + #[fail(display = "bad HTTP response '{}' while requesting authentication nonce", _1)] + NonceReqStatus(StatusCode, String), + + /// No authentication encryption nonce was included in the response + /// from the server, it was missing. + #[fail(display = "missing authentication nonce in server response")] + NoNonceHeader, + + /// The authentication encryption nonce from the response malformed or + /// empty. + /// Maybe the server responded with a new format that isn't supported yet + /// by this client. + #[fail(display = "received malformed authentication nonce")] + MalformedNonce, +} + +#[derive(Fail, Debug)] +pub enum MetaError { + /// An error occurred while computing the cryptographic signature used for + /// decryption. + #[fail(display = "failed to compute cryptographic signature")] + ComputeSignature, + + /// Sending the request to gather the metadata encryption nonce failed. + #[fail(display = "failed to request metadata nonce")] + NonceReq, + + /// The response for fetching the metadata encryption nonce indicated an + /// error and wasn't successful. + #[fail(display = "bad HTTP response '{}' while requesting metadata nonce", _1)] + NonceReqStatus(StatusCode, String), + + /// No metadata encryption nonce was included in the response from the + /// server, it was missing. + #[fail(display = "missing metadata nonce in server response")] + NoNonceHeader, + + /// The metadata encryption nonce from the response malformed or empty. + /// Maybe the server responded with a new format that isn't supported yet + /// by this client. + #[fail(display = "received malformed metadata nonce")] + MalformedNonce, + + /// The received metadata is malformed, and couldn't be decoded or + /// interpreted. + #[fail(display = "received malformed metadata")] + Malformed, + + /// Failed to decrypt the received metadata. + #[fail(display = "failed to decrypt received metadata")] + Decrypt, +} + +#[derive(Fail, Debug)] +pub enum DownloadError { + /// An error occurred while computing the cryptographic signature used for + /// downloading the file. + #[fail(display = "failed to compute cryptographic signature")] + ComputeSignature, + + /// Sending the request to gather the metadata encryption nonce failed. + #[fail(display = "failed to request file download")] + Request, + + /// The response for downloading the indicated an error and wasn't successful. + #[fail(display = "bad HTTP response '{}' while requesting file download", _1)] + RequestStatus(StatusCode, String), + + /// The length of the file is missing, thus the length of the file to download + /// couldn't be determined. + #[fail(display = "couldn't determine file download length, missing property")] + NoLength, + + /// Failed to start or update the downloading progress, because of this the + /// download can't continue. + #[fail(display = "failed to update download progress")] + Progress, + + /// The actual download and decryption process the server. + /// This covers reading the file from the server, decrypting the file, + /// and writing it to the file system. + #[fail(display = "failed to download the file")] + Download, + + /// Verifiying the downloaded file failed. + #[fail(display = "file verification failed")] + Verify, +} + +#[derive(Fail, Debug)] +pub enum FileError { + /// An error occurred while creating or opening the file to write to. + #[fail(display = "failed to create or open file")] + Create(#[cause] IoError), + + /// Failed to create an encrypted writer for the file, which is used to + /// decrypt the downloaded file. + #[fail(display = "failed to create file decryptor")] + EncryptedWriter, +} + +/// Reqwest status code extention, to easily retrieve an error message. +trait StatusCodeExt { + /// Build a basic error message based on the status code. + fn err_text(&self) -> String; +} + +impl StatusCodeExt for StatusCode { + fn err_text(&self) -> String { + self.canonical_reason() + .map(|text| text.to_owned()) + .unwrap_or(format!("{}", self.as_u16())) } } diff --git a/api/src/lib.rs b/api/src/lib.rs index 828e630..7a934d3 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1,14 +1,19 @@ #[macro_use] extern crate arrayref; +extern crate failure; +#[macro_use] +extern crate failure_derive; extern crate mime_guess; extern crate openssl; pub extern crate reqwest; -pub extern crate url; #[macro_use] extern crate serde_derive; extern crate serde_json; +pub extern crate url; pub mod action; pub mod crypto; pub mod file; pub mod reader; + +pub use failure::Error; diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 47ac81f..9c9a4ca 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -14,6 +14,7 @@ default = ["clipboard"] [dependencies] clap = "2.31" clipboard = { version = "0.4", optional = true } +failure = "0.1" ffsend-api = { version = "*", path = "../api" } open = "1" pbr = "1" diff --git a/cli/src/action/download.rs b/cli/src/action/download.rs index f4b93c7..d12c4f3 100644 --- a/cli/src/action/download.rs +++ b/cli/src/action/download.rs @@ -6,6 +6,7 @@ use ffsend_api::reqwest::Client; use cmd::cmd_download::CmdDownload; use progress::ProgressBar; +use util::quit_error; /// A file download action. pub struct Download<'a> { @@ -38,7 +39,9 @@ impl<'a> Download<'a> { // Execute an download action // TODO: do not unwrap, but return an error - ApiDownload::new(&file).invoke(&client, bar).unwrap(); + if let Err(err) = ApiDownload::new(&file).invoke(&client, bar) { + quit_error(err); + } // TODO: open the file, or it's location // TODO: copy the file location diff --git a/cli/src/cmd/cmd_download.rs b/cli/src/cmd/cmd_download.rs index 690a2c7..355dd9a 100644 --- a/cli/src/cmd/cmd_download.rs +++ b/cli/src/cmd/cmd_download.rs @@ -2,7 +2,7 @@ use ffsend_api::url::{ParseError, Url}; use super::clap::{App, Arg, ArgMatches, SubCommand}; -use util::quit_error; +use util::quit_error_msg; /// The download command. pub struct CmdDownload<'a> { @@ -47,18 +47,18 @@ impl<'a: 'b, 'b> CmdDownload<'a> { match Url::parse(url) { Ok(url) => url, Err(ParseError::EmptyHost) => - quit_error("emtpy host given"), + quit_error_msg("emtpy host given"), Err(ParseError::InvalidPort) => - quit_error("invalid host port"), + quit_error_msg("invalid host port"), Err(ParseError::InvalidIpv4Address) => - quit_error("invalid IPv4 address in host"), + quit_error_msg("invalid IPv4 address in host"), Err(ParseError::InvalidIpv6Address) => - quit_error("invalid IPv6 address in host"), + quit_error_msg("invalid IPv6 address in host"), Err(ParseError::InvalidDomainCharacter) => - quit_error("host domains contains an invalid character"), + quit_error_msg("host domains contains an invalid character"), Err(ParseError::RelativeUrlWithoutBase) => - quit_error("host domain doesn't contain a host"), - _ => quit_error("the given host is invalid"), + quit_error_msg("host domain doesn't contain a host"), + _ => quit_error_msg("the given host is invalid"), } } } diff --git a/cli/src/cmd/cmd_upload.rs b/cli/src/cmd/cmd_upload.rs index f29932d..d09f33f 100644 --- a/cli/src/cmd/cmd_upload.rs +++ b/cli/src/cmd/cmd_upload.rs @@ -3,7 +3,7 @@ use ffsend_api::url::{ParseError, Url}; use super::clap::{App, Arg, ArgMatches, SubCommand}; use app::SEND_DEF_HOST; -use util::quit_error; +use util::quit_error_msg; /// The upload command. pub struct CmdUpload<'a> { @@ -72,18 +72,18 @@ impl<'a: 'b, 'b> CmdUpload<'a> { match Url::parse(host) { Ok(url) => url, Err(ParseError::EmptyHost) => - quit_error("emtpy host given"), + quit_error_msg("emtpy host given"), Err(ParseError::InvalidPort) => - quit_error("invalid host port"), + quit_error_msg("invalid host port"), Err(ParseError::InvalidIpv4Address) => - quit_error("invalid IPv4 address in host"), + quit_error_msg("invalid IPv4 address in host"), Err(ParseError::InvalidIpv6Address) => - quit_error("invalid IPv6 address in host"), + quit_error_msg("invalid IPv6 address in host"), Err(ParseError::InvalidDomainCharacter) => - quit_error("host domains contains an invalid character"), + quit_error_msg("host domains contains an invalid character"), Err(ParseError::RelativeUrlWithoutBase) => - quit_error("host domain doesn't contain a host"), - _ => quit_error("the given host is invalid"), + quit_error_msg("host domain doesn't contain a host"), + _ => quit_error_msg("the given host is invalid"), } } diff --git a/cli/src/util.rs b/cli/src/util.rs index 5042e8f..c47cff2 100644 --- a/cli/src/util.rs +++ b/cli/src/util.rs @@ -1,21 +1,40 @@ #[cfg(feature = "clipboard")] extern crate clipboard; +extern crate failure; extern crate open; #[cfg(feature = "clipboard")] -use std::error::Error; +use std::error::Error as StdError; +use std::fmt::{Debug, Display}; use std::io::Error as IoError; use std::process::{exit, ExitStatus}; #[cfg(feature = "clipboard")] use self::clipboard::{ClipboardContext, ClipboardProvider}; +use self::failure::{Fail}; use ffsend_api::url::Url; /// Quit the application with an error code, -/// and print the given error message. -pub fn quit_error>(err: S) -> ! { +/// and print the given error. +pub fn quit_error(err: E) -> ! { // Print the error message - eprintln!("error: {}", err.as_ref()); + eprintln!("error: {}", err); + + // Quit + exit(1); +} + +/// Quit the application with an error code, +/// and print the given error message. +pub fn quit_error_msg(err: S) -> ! + where + S: AsRef + Display + Debug + Sync + Send + 'static +{ + // TODO: forward the error the `quit_error` here + // quit_error(failure::err_msg(err)); + + // Print the error message + eprintln!("error: {}", err); // Quit exit(1); @@ -35,7 +54,7 @@ pub fn open_path(path: &str) -> Result { /// Set the clipboard of the user to the given `content` string. #[cfg(feature = "clipboard")] -pub fn set_clipboard(content: String) -> Result<(), Box> { +pub fn set_clipboard(content: String) -> Result<(), Box> { let mut context: ClipboardContext = ClipboardProvider::new()?; context.set_contents(content) }