Merge Send file types, rename it to RemoteFile, improve error handling

This commit is contained in:
timvisee 2018-03-29 01:04:54 +02:00
parent 376743915d
commit 8748d79934
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
10 changed files with 82 additions and 163 deletions

View file

@ -2,6 +2,7 @@
- Rename DownloadFile to RemoteFile
- Box errors
- Info endpoint, to view file info
- On download, mention a wrong or missing password with a HTTP 401 response
- Automatically get owner token, from file history when setting password
- Implement error handling everywhere properly
- `-y` flag for assume yes

View file

@ -19,8 +19,8 @@ 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 file::remote_file::RemoteFile;
use reader::{EncryptedFileWriter, ProgressReporter, ProgressWriter};
/// The name of the header that is used for the authentication nonce.
@ -31,16 +31,16 @@ const FILE_EXPIRED_STATUS: StatusCode = StatusCode::NotFound;
/// A file upload action to a Send server.
pub struct Download<'a> {
/// The Send file to download.
file: &'a DownloadFile,
/// The remote file to download.
file: &'a RemoteFile,
/// An optional password to decrypt a protected file.
password: Option<String>,
}
impl<'a> Download<'a> {
/// Construct a new download action for the given file.
pub fn new(file: &'a DownloadFile, password: Option<String>) -> Self {
/// Construct a new download action for the given remote file.
pub fn new(file: &'a RemoteFile, password: Option<String>) -> Self {
Self {
file,
password,

View file

@ -7,15 +7,15 @@ use crypto::b64;
use crypto::key_set::KeySet;
use crypto::sig::signature_encoded;
use ext::status_code::StatusCodeExt;
use file::file::DownloadFile;
use file::remote_file::RemoteFile;
/// 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> {
/// The uploaded file to change the password for.
file: &'a DownloadFile,
/// The remote file to change the password for.
file: &'a RemoteFile,
/// The new password to use for the file.
password: &'a str,
@ -26,9 +26,9 @@ pub struct Password<'a> {
}
impl<'a> Password<'a> {
/// Construct a new password action for the given file.
/// Construct a new password action for the given remote file.
pub fn new(
file: &'a DownloadFile,
file: &'a RemoteFile,
password: &'a str,
nonce: Option<Vec<u8>>,
) -> Self {
@ -143,7 +143,7 @@ struct PasswordData {
impl PasswordData {
/// Create the password data object from the given key set.
pub fn from(file: &DownloadFile, key: &KeySet)
pub fn from(file: &RemoteFile, key: &KeySet)
-> Result<PasswordData, PrepareError>
{
Ok(

View file

@ -25,7 +25,7 @@ use url::{
use crypto::b64;
use crypto::key_set::KeySet;
use ext::status_code::StatusCodeExt;
use file::file::File as SendFile;
use file::remote_file::RemoteFile;
use file::metadata::{Metadata, XFileMetadata};
use reader::{
EncryptedFileReader,
@ -70,7 +70,7 @@ impl Upload {
self,
client: &Client,
reporter: Arc<Mutex<ProgressReporter>>,
) -> Result<SendFile, Error> {
) -> Result<RemoteFile, Error> {
// Create file data, generate a key
let file = FileData::from(&self.path)?;
let key = KeySet::generate(true);
@ -105,7 +105,7 @@ impl Upload {
// Change the password if set
if let Some(password) = self.password {
Password::new(
&result.to_download_file(),
&result,
&password,
nonce,
).invoke(client)?;
@ -218,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, Option<Vec<u8>>), UploadError>
-> Result<(RemoteFile, Option<Vec<u8>>), UploadError>
{
// Execute the request
let mut response = match client.execute(req) {
@ -285,15 +285,15 @@ impl UploadResponse {
///
/// The `host` and `key` must be given.
pub fn into_file(self, host: Url, key: &KeySet)
-> Result<SendFile, UploadError>
-> Result<RemoteFile, UploadError>
{
Ok(
SendFile::new_now(
RemoteFile::new_now(
self.id,
host,
Url::parse(&self.url)?,
key.secret().to_vec(),
self.owner,
Some(self.owner),
)
)
}

View file

@ -1,7 +1,7 @@
use openssl::symm::Cipher;
use url::Url;
use file::file::DownloadFile;
use file::remote_file::RemoteFile;
use super::{b64, rand_bytes};
use super::hdkf::{derive_auth_key, derive_file_key, derive_meta_key};
@ -38,12 +38,11 @@ impl KeySet {
}
/// Create a key set from the given file ID and secret.
/// This method may be used to create a key set based on a Send download
/// URL.
/// This method may be used to create a key set based on a share URL.
// TODO: add a parameter for the password and URL
// TODO: return a result?
// TODO: supply a client instance as parameter
pub fn from(file: &DownloadFile, password: Option<&String>) -> Self {
pub fn from(file: &RemoteFile, password: Option<&String>) -> Self {
// Create a new key set instance
let mut set = Self::new(
file.secret_raw().clone(),

View file

@ -1,2 +1,2 @@
pub mod file;
pub mod remote_file;
pub mod metadata;

View file

@ -10,27 +10,27 @@ use self::regex::Regex;
use crypto::b64;
/// A pattern for Send download URL paths, capturing the file ID.
/// A pattern for share URL paths, capturing the file ID.
// TODO: match any sub-path?
// TODO: match URL-safe base64 chars for the file ID?
// TODO: constrain the ID length?
const DOWNLOAD_PATH_PATTERN: &'static str = r"^/?download/([[:alnum:]]{8,}={0,3})/?$";
const SHARE_PATH_PATTERN: &'static str = r"^/?download/([[:alnum:]]{8,}={0,3})/?$";
/// A pattern for Send download URL fragments, capturing the file secret.
/// A pattern for share URL fragments, capturing the file secret.
// TODO: constrain the secret length?
const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
const SHARE_FRAGMENT_PATTERN: &'static str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
/// A struct representing an uploaded file on a Send host.
///
/// The struct contains the file ID, the file URL, the key that is required
/// in combination with the file, and the owner key.
#[derive(Debug)]
pub struct File {
pub struct RemoteFile {
/// The ID of the file on that server.
id: String,
/// The time the file was uploaded at.
time: DateTime<Utc>,
/// The time the file was uploaded at, if known.
time: Option<DateTime<Utc>>,
/// The host the file was uploaded to.
host: Url,
@ -42,19 +42,18 @@ pub struct File {
secret: Vec<u8>,
/// The owner key, that can be used to manage the file on the server.
// TODO: rename this to owner token
owner_key: String,
owner_token: Option<String>,
}
impl File {
impl RemoteFile {
/// Construct a new file.
pub fn new(
id: String,
time: DateTime<Utc>,
time: Option<DateTime<Utc>>,
host: Url,
url: Url,
secret: Vec<u8>,
owner_key: String,
owner_token: Option<String>,
) -> Self {
Self {
id,
@ -62,7 +61,7 @@ impl File {
host,
url,
secret,
owner_key,
owner_token,
}
}
@ -72,115 +71,31 @@ impl File {
host: Url,
url: Url,
secret: Vec<u8>,
owner_key: String,
owner_token: Option<String>,
) -> Self {
Self::new(
id,
Utc::now(),
host,
url,
secret,
owner_key,
)
}
// TODO: this should be removed when merging the two file types
pub fn to_download_file(&self) -> DownloadFile {
DownloadFile::new(
self.id.clone(),
self.host.clone(),
self.url.clone(),
self.secret.clone(),
Some(self.owner_key.clone()),
)
}
/// Get the raw secret.
pub fn secret_raw(&self) -> &Vec<u8> {
// A secret must have been set
if !self.has_secret() {
// TODO: don't panic, return an error instead
panic!("missing secret");
}
&self.secret
}
/// Get the secret as base64 encoded string.
pub fn secret(&self) -> String {
b64::encode(self.secret_raw())
}
/// Check whether a file secret is set.
/// This secret must be set to decrypt a downloaded Send file.
pub fn has_secret(&self) -> bool {
!self.secret.is_empty()
}
/// Get the owner token if set.
pub fn owner_token(&self) -> Option<&String> {
Some(&self.owner_key)
}
/// Get the download URL of the file.
/// Set `secret` to `true`, to include it in the URL if known.
pub fn download_url(&self, secret: bool) -> Url {
// Get the download URL, and add the secret fragment
let mut url = self.url.clone();
if secret && self.has_secret() {
url.set_fragment(Some(&self.secret()));
} else {
url.set_fragment(None);
}
url
}
}
// TODO: merge this struct with `File`.
pub struct DownloadFile {
/// The ID of the file on that server.
id: String,
/// The host the file was uploaded to.
host: Url,
/// The file URL that was provided by the server.
url: Url,
/// The secret key that is required to download the file.
secret: Vec<u8>,
owner_token: Option<String>,
}
impl DownloadFile {
/// Construct a new instance.
pub fn new(
id: String,
host: Url,
url: Url,
secret: Vec<u8>,
owner_token: Option<String>,
) -> Self {
Self {
id,
Some(Utc::now()),
host,
url,
secret,
owner_token,
}
)
}
/// Try to parse the given Send download URL.
/// Try to parse the given share URL.
///
/// The given URL is matched against a Send download URL pattern,
/// this does not check whether the host is a valid and online Send host.
/// The given URL is matched against a share URL pattern,
/// this does not check whether the host is a valid and online host.
///
/// If the URL fragmet contains a file secret, it is also parsed.
/// If it does not, the secret is left empty and must be specified
/// manually.
pub fn parse_url(url: Url) -> Result<DownloadFile, FileParseError> {
///
/// An optional owner token may be given.
pub fn parse_url(url: Url, owner_token: Option<String>)
-> Result<RemoteFile, FileParseError>
{
// Build the host
let mut host = url.clone();
host.set_fragment(None);
@ -188,16 +103,16 @@ impl DownloadFile {
host.set_path("");
// Validate the path, get the file ID
let re_path = Regex::new(DOWNLOAD_PATH_PATTERN).unwrap();
let re_path = Regex::new(SHARE_PATH_PATTERN).unwrap();
let id = re_path.captures(url.path())
.ok_or(FileParseError::InvalidDownloadUrl)?[1]
.ok_or(FileParseError::InvalidUrl)?[1]
.trim()
.to_owned();
// Get the file secret
let mut secret = Vec::new();
if let Some(fragment) = url.fragment() {
let re_fragment = Regex::new(DOWNLOAD_FRAGMENT_PATTERN).unwrap();
let re_fragment = Regex::new(SHARE_FRAGMENT_PATTERN).unwrap();
if let Some(raw) = re_fragment.captures(fragment)
.ok_or(FileParseError::InvalidSecret)?
.get(1)
@ -210,10 +125,11 @@ impl DownloadFile {
// Construct the file
Ok(Self::new(
id,
None,
host,
url,
secret,
None,
owner_token,
))
}
@ -239,12 +155,6 @@ impl DownloadFile {
!self.secret.is_empty()
}
/// Set the secret for this file.
/// An empty vector will clear the secret.
pub fn set_secret(&mut self, secret: Vec<u8>) {
self.secret = secret;
}
/// Get the owner token if set.
pub fn owner_token(&self) -> Option<&String> {
self.owner_token.as_ref()
@ -255,10 +165,11 @@ impl DownloadFile {
self.owner_token = token;
}
/// Get the download URL of the file.
/// Get the download URL of the file
/// This URL is identical to the share URL, a term used in this API.
/// Set `secret` to `true`, to include it in the URL if known.
pub fn download_url(&self, secret: bool) -> Url {
// Get the download URL, and add the secret fragment
// Get the share URL, and add the secret fragment
let mut url = self.url.clone();
if secret && self.has_secret() {
url.set_fragment(Some(&self.secret()));
@ -271,7 +182,7 @@ impl DownloadFile {
/// Get the API metadata URL of the file.
pub fn api_meta_url(&self) -> Url {
// Get the download URL, and add the secret fragment
// Get the share URL, and add the secret fragment
let mut url = self.url.clone();
url.set_path(format!("/api/metadata/{}", self.id).as_str());
url.set_fragment(None);
@ -281,7 +192,7 @@ impl DownloadFile {
/// Get the API download URL of the file.
pub fn api_download_url(&self) -> Url {
// Get the download URL, and add the secret fragment
// Get the share URL, and add the secret fragment
let mut url = self.url.clone();
url.set_path(format!("/api/download/{}", self.id).as_str());
url.set_fragment(None);
@ -291,7 +202,7 @@ impl DownloadFile {
/// Get the API password URL of the file.
pub fn api_password_url(&self) -> Url {
// Get the download URL, and add the secret fragment
// Get the share URL, and add the secret fragment
let mut url = self.url.clone();
url.set_path(format!("/api/password/{}", self.id).as_str());
url.set_fragment(None);
@ -300,14 +211,17 @@ impl DownloadFile {
}
}
#[derive(Debug)]
#[derive(Debug, Fail)]
pub enum FileParseError {
/// An URL format error.
UrlFormatError(UrlParseError),
#[fail(display = "Failed to parse remote file, invalid URL format")]
UrlFormatError(#[cause] UrlParseError),
/// An error for an invalid download URL format.
InvalidDownloadUrl,
/// An error for an invalid share URL format.
#[fail(display = "Failed to parse remote file, invalid URL")]
InvalidUrl,
/// An error for an invalid secret format, if an URL fragmet exists.
#[fail(display = "Failed to parse remote file, invalid secret in URL")]
InvalidSecret,
}

View file

@ -1,7 +1,7 @@
use std::sync::{Arc, Mutex};
use ffsend_api::action::download::Download as ApiDownload;
use ffsend_api::file::file::DownloadFile;
use ffsend_api::file::remote_file::RemoteFile;
use ffsend_api::reqwest::Client;
use cmd::cmd_download::CmdDownload;
@ -30,10 +30,9 @@ impl<'a> Download<'a> {
// Create a reqwest client
let client = Client::new();
// Parse the file based on the URL
// Parse the remote file based on the share URL
// TODO: handle error here
let file = DownloadFile::parse_url(url)
.expect("invalid share URL, could not parse file data");
let file = RemoteFile::parse_url(url, None)?;
// Create a progress bar reporter
let bar = Arc::new(Mutex::new(ProgressBar::new_download()));

View file

@ -1,5 +1,5 @@
use ffsend_api::action::password::Password as ApiPassword;
use ffsend_api::file::file::DownloadFile;
use ffsend_api::file::remote_file::RemoteFile;
use ffsend_api::reqwest::Client;
use cmd::cmd_password::CmdPassword;
@ -28,15 +28,9 @@ impl<'a> Password<'a> {
// Create a reqwest client
let client = Client::new();
// Parse the file based on the URL
// Parse the remote file based on the share URL
// TODO: handle error here
let mut file = DownloadFile::parse_url(url)
.expect("invalid share URL, could not parse file data");
// Set the owner token
if let Some(token) = self.cmd.owner() {
file.set_owner_token(Some(token));
}
let file = RemoteFile::parse_url(url, self.cmd.owner())?;
// TODO: show an informative error if the owner token isn't set

View file

@ -1,6 +1,7 @@
use ffsend_api::action::download::Error as DownloadError;
use ffsend_api::action::password::Error as PasswordError;
use ffsend_api::action::upload::Error as UploadError;
use ffsend_api::file::remote_file::FileParseError;
#[derive(Fail, Debug)]
pub enum Error {
@ -29,6 +30,11 @@ pub enum ActionError {
/// An error occurred while invoking the password action.
#[fail(display = "Failed to change the password")]
Password(#[cause] PasswordError),
/// Failed to parse a share URL, it was invalid.
/// This error is not related to a specific action.
#[fail(display = "Invalid share URL")]
InvalidUrl(#[cause] FileParseError),
}
impl From<DownloadError> for ActionError {
@ -48,3 +54,9 @@ impl From<UploadError> for ActionError {
ActionError::Upload(err)
}
}
impl From<FileParseError> for ActionError {
fn from(err: FileParseError) -> ActionError {
ActionError::InvalidUrl(err)
}
}