浏览代码

Create upload command, implement download data decryption logic

Tim Visée 7 年之前
父节点
当前提交
5b1724ef9e

+ 243 - 186
api/src/action/download.rs

@@ -1,39 +1,30 @@
-use std::fs::File;
-use std::io::BufReader;
-use std::path::{Path, PathBuf};
-use std::sync::{Arc, Mutex};
+use std::path::Path;
 
 
 use mime_guess::{get_mime_type, Mime};
 use mime_guess::{get_mime_type, Mime};
-use openssl::symm::encrypt_aead;
+use openssl::symm::{decrypt_aead, encrypt_aead};
 use reqwest::{
 use reqwest::{
     Client, 
     Client, 
     Error as ReqwestError,
     Error as ReqwestError,
-    Request,
 };
 };
 use reqwest::header::Authorization;
 use reqwest::header::Authorization;
-use reqwest::mime::APPLICATION_OCTET_STREAM;
-use reqwest::multipart::{Form, Part};
-use url::Url;
+use serde_json;
 
 
+use crypto::b64;
 use crypto::key_set::KeySet;
 use crypto::key_set::KeySet;
-use reader::{
-    EncryptedFileReaderTagged,
-    ExactLengthReader,
-    ProgressReader,
-    ProgressReporter,
-};
 use file::file::DownloadFile;
 use file::file::DownloadFile;
-use file::metadata::{Metadata, XFileMetadata};
+use file::metadata::Metadata;
 
 
 pub type Result<T> = ::std::result::Result<T, DownloadError>;
 pub type Result<T> = ::std::result::Result<T, DownloadError>;
 
 
 /// The name of the header that is used for the authentication nonce.
 /// The name of the header that is used for the authentication nonce.
 const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
 const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
 
 
+// TODO: experiment with `iv` of `None` in decrypt logic
+
 /// A file upload action to a Send server.
 /// A file upload action to a Send server.
 pub struct Download<'a> {
 pub struct Download<'a> {
     /// The Send file to download.
     /// The Send file to download.
-    file: &DownloadFile,
+    file: &'a DownloadFile,
 }
 }
 
 
 impl<'a> Download<'a> {
 impl<'a> Download<'a> {
@@ -48,7 +39,7 @@ impl<'a> Download<'a> {
     pub fn invoke(
     pub fn invoke(
         self,
         self,
         client: &Client,
         client: &Client,
-    ) -> Result<SendFile> {
+    ) -> Result<()> {
         // Create a key set for the file
         // Create a key set for the file
         let key = KeySet::from(self.file);
         let key = KeySet::from(self.file);
 
 
@@ -68,7 +59,7 @@ impl<'a> Download<'a> {
 
 
         // Get the download url, and parse the nonce
         // Get the download url, and parse the nonce
         // TODO: do not unwrap here, return error
         // TODO: do not unwrap here, return error
-        let download_url = file.download_url(false);
+        let download_url = self.file.download_url(false);
         let response = client.get(download_url)
         let response = client.get(download_url)
             .send()
             .send()
             .expect("failed to get nonce, failed to send file request");
             .expect("failed to get nonce, failed to send file request");
@@ -95,158 +86,213 @@ impl<'a> Download<'a> {
                 .skip(1)
                 .skip(1)
                 .next()
                 .next()
                 .expect("missing authentication nonce")
                 .expect("missing authentication nonce")
-        );
-
-        // TODO: set the input vector
+        ).expect("failed to decode authentication nonce");
+
+        // Determine the signature
+        // TODO: use a tag length const here
+        // TODO: do not unwrap, return an error
+        let mut sig = vec![0u8; 16];
+		encrypt_aead(
+			KeySet::cipher(),
+			key.auth_key().unwrap(),
+			None,
+			&[],
+			&nonce,
+			&mut sig,
+		).expect("failed to derive signature");
+        let sig_encoded = b64::encode(&sig);
+
+        // Get the meta URL, fetch the metadata
+        // TODO: do not unwrap here, return error
+        let meta_url = self.file.api_meta_url();
+        let mut response = client.get(meta_url)
+            .header(Authorization(
+                format!("send-v1 {}", sig_encoded)
+            ))
+            .send()
+            .expect("failed to fetch metadata, failed to send request");
 
 
-        // Crpate metadata and a file reader
-        let metadata = self.create_metadata(&key, &file)?;
-        let reader = self.create_reader(&key, reporter.clone())?;
-        let reader_len = reader.len().unwrap();
+        // Validate the status code
+        // TODO: allow redirects here?
+        if !response.status().is_success() {
+            // TODO: return error here
+            panic!("failed to fetch metadata, request status is not successful");
+        }
 
 
-        // Create the request to send
-        let req = self.create_request(
-            client,
-            &key,
-            metadata,
-            reader,
+        // Get the metadata nonce
+        // TODO: don't unwrap here, return an error
+        let nonce = b64::decode(
+            response.headers()
+                .get_raw(HEADER_AUTH_NONCE)
+                .expect("missing authenticate header") 
+                .one()
+                .map(|line| String::from_utf8(line.to_vec())
+                    .expect("invalid authentication header contents")
+                )
+                .expect("authentication header is empty")
+                .split_terminator(" ")
+                .skip(1)
+                .next()
+                .expect("missing metadata nonce")
         );
         );
 
 
-        // Start the reporter
-        reporter.lock()
-            .expect("unable to start progress, failed to get lock")
-            .start(reader_len);
-
-        // Execute the request
-        let result = self.execute_request(req, client, &key);
-
-        // Mark the reporter as finished
-        reporter.lock()
-            .expect("unable to finish progress, failed to get lock")
-            .finish();
-
-        result
-    }
-
-    /// Create a blob of encrypted metadata.
-    fn create_metadata(&self, key: &KeySet, file: &FileData)
-        -> Result<Vec<u8>>
-    {
-        // Construct the metadata
-        let metadata = Metadata::from(
-            key.iv(),
-            file.name().to_owned(),
-            file.mime().clone(),
-        ).to_json().into_bytes();
-
-        // Encrypt the metadata
-        let mut metadata_tag = vec![0u8; 16];
-        let mut metadata = match encrypt_aead(
-            KeySet::cipher(),
-            key.meta_key().unwrap(),
-            Some(&[0u8; 12]),
-            &[],
-            &metadata,
-            &mut metadata_tag,
-        ) {
-            Ok(metadata) => metadata,
-            Err(_) => return Err(DownloadError::EncryptionError),
-        };
-
-        // Append the encryption tag
-        metadata.append(&mut metadata_tag);
+        // Parse the metadata response
+        let meta_response: MetadataResponse = response.json()
+            .expect("failed to parse metadata response");
 
 
-        Ok(metadata)
-    }
+        // Decrypt the metadata
+        let metadata = meta_response.decrypt_metadata(&key);
 
 
-    /// Create a reader that reads the file as encrypted stream.
-    fn create_reader(
-        &self,
-        key: &KeySet,
-        reporter: Arc<Mutex<ProgressReporter>>,
-    ) -> Result<EncryptedReader> {
-        // Open the file
-        let file = match File::open(self.path.as_path()) {
-            Ok(file) => file,
-            Err(_) => return Err(DownloadError::FileError),
-        };
+        println!("GOT METADATA: {:?}", metadata);
 
 
-        // Create an encrypted reader
-        let reader = match EncryptedFileReaderTagged::new(
-            file,
-            KeySet::cipher(),
-            key.file_key().unwrap(),
-            key.iv(),
-        ) {
-            Ok(reader) => reader,
-            Err(_) => return Err(DownloadError::EncryptionError),
-        };
+        // // Crpate metadata and a file reader
+        // let metadata = self.create_metadata(&key, &file)?;
+        // let reader = self.create_reader(&key, reporter.clone())?;
+        // let reader_len = reader.len().unwrap();
 
 
-        // Buffer the encrypted reader
-        let reader = BufReader::new(reader);
+        // // Create the request to send
+        // let req = self.create_request(
+        //     client,
+        //     &key,
+        //     metadata,
+        //     reader,
+        // );
 
 
-        // Wrap into the encrypted reader
-        let mut reader = ProgressReader::new(reader)
-            .expect("failed to create progress reader");
+        // // Start the reporter
+        // reporter.lock()
+        //     .expect("unable to start progress, failed to get lock")
+        //     .start(reader_len);
 
 
-        // Initialize and attach the reporter
-        reader.set_reporter(reporter);
+        // // Execute the request
+        // let result = self.execute_request(req, client, &key);
 
 
-        Ok(reader)
-    }
+        // // Mark the reporter as finished
+        // reporter.lock()
+        //     .expect("unable to finish progress, failed to get lock")
+        //     .finish();
 
 
-    /// Build the request that will be send to the server.
-    fn create_request(
-        &self,
-        client: &Client,
-        key: &KeySet,
-        metadata: Vec<u8>,
-        reader: EncryptedReader,
-    ) -> Request {
-        // Get the reader length
-        let len = reader.len().expect("failed to get reader length");
-
-        // Configure a form to send
-        let part = Part::reader_with_length(reader, len)
-            // .file_name(file.name())
-            .mime(APPLICATION_OCTET_STREAM);
-        let form = Form::new()
-            .part("data", part);
-
-        // Define the URL to call
-        let url = self.host.join("api/upload").expect("invalid host");
-
-        // Build the request
-        client.post(url.as_str())
-            .header(Authorization(
-                format!("send-v1 {}", key.auth_key_encoded().unwrap())
-            ))
-            .header(XFileMetadata::from(&metadata))
-            .multipart(form)
-            .build()
-            .expect("failed to build an API request")
+        Ok(())
     }
     }
 
 
-    /// 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>
-    {
-        // Execute the request
-        let mut res = match client.execute(req) {
-            Ok(res) => res,
-            Err(err) => return Err(DownloadError::RequestError(err)),
-        };
-
-        // Decode the response
-        let res: DownloadResponse = match res.json() {
-            Ok(res) => res,
-            Err(_) => return Err(DownloadError::DecodeError),
-        };
-
-        // Transform the responce into a file object
-        Ok(res.into_file(self.host.clone(), &key))
-    }
+    // /// Create a blob of encrypted metadata.
+    // fn create_metadata(&self, key: &KeySet, file: &FileData)
+    //     -> Result<Vec<u8>>
+    // {
+    //     // Construct the metadata
+    //     let metadata = Metadata::from(
+    //         key.iv(),
+    //         file.name().to_owned(),
+    //         file.mime().clone(),
+    //     ).to_json().into_bytes();
+
+    //     // Encrypt the metadata
+    //     let mut metadata_tag = vec![0u8; 16];
+    //     let mut metadata = match encrypt_aead(
+    //         KeySet::cipher(),
+    //         key.meta_key().unwrap(),
+    //         Some(&[0u8; 12]),
+    //         &[],
+    //         &metadata,
+    //         &mut metadata_tag,
+    //     ) {
+    //         Ok(metadata) => metadata,
+    //         Err(_) => return Err(DownloadError::EncryptionError),
+    //     };
+
+    //     // Append the encryption tag
+    //     metadata.append(&mut metadata_tag);
+
+    //     Ok(metadata)
+    // }
+
+    // /// Create a reader that reads the file as encrypted stream.
+    // fn create_reader(
+    //     &self,
+    //     key: &KeySet,
+    //     reporter: Arc<Mutex<ProgressReporter>>,
+    // ) -> Result<EncryptedReader> {
+    //     // Open the file
+    //     let file = match File::open(self.path.as_path()) {
+    //         Ok(file) => file,
+    //         Err(_) => return Err(DownloadError::FileError),
+    //     };
+
+    //     // Create an encrypted reader
+    //     let reader = match EncryptedFileReaderTagged::new(
+    //         file,
+    //         KeySet::cipher(),
+    //         key.file_key().unwrap(),
+    //         key.iv(),
+    //     ) {
+    //         Ok(reader) => reader,
+    //         Err(_) => return Err(DownloadError::EncryptionError),
+    //     };
+
+    //     // Buffer the encrypted reader
+    //     let reader = BufReader::new(reader);
+
+    //     // Wrap into the encrypted reader
+    //     let mut reader = ProgressReader::new(reader)
+    //         .expect("failed to create progress reader");
+
+    //     // Initialize and attach the reporter
+    //     reader.set_reporter(reporter);
+
+    //     Ok(reader)
+    // }
+
+    // /// Build the request that will be send to the server.
+    // fn create_request(
+    //     &self,
+    //     client: &Client,
+    //     key: &KeySet,
+    //     metadata: Vec<u8>,
+    //     reader: EncryptedReader,
+    // ) -> Request {
+    //     // Get the reader length
+    //     let len = reader.len().expect("failed to get reader length");
+
+    //     // Configure a form to send
+    //     let part = Part::reader_with_length(reader, len)
+    //         // .file_name(file.name())
+    //         .mime(APPLICATION_OCTET_STREAM);
+    //     let form = Form::new()
+    //         .part("data", part);
+
+    //     // Define the URL to call
+    //     let url = self.host.join("api/upload").expect("invalid host");
+
+    //     // Build the request
+    //     client.post(url.as_str())
+    //         .header(Authorization(
+    //             format!("send-v1 {}", key.auth_key_encoded().unwrap())
+    //         ))
+    //         .header(XFileMetadata::from(&metadata))
+    //         .multipart(form)
+    //         .build()
+    //         .expect("failed to build an API request")
+    // }
+
+    // /// 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>
+    // {
+    //     // Execute the request
+    //     let mut res = match client.execute(req) {
+    //         Ok(res) => res,
+    //         Err(err) => return Err(DownloadError::RequestError(err)),
+    //     };
+
+    //     // Decode the response
+    //     let res: DownloadResponse = match res.json() {
+    //         Ok(res) => res,
+    //         Err(_) => return Err(DownloadError::DecodeError),
+    //     };
+
+    //     // Transform the responce into a file object
+    //     Ok(res.into_file(self.host.clone(), &key))
+    // }
 }
 }
 
 
 /// Errors that may occur in the upload action. 
 /// Errors that may occur in the upload action. 
@@ -270,39 +316,50 @@ pub enum DownloadError {
     DecodeError,
     DecodeError,
 }
 }
 
 
-/// The response from the server after a file has been uploaded.
-/// This response contains the file ID and owner key, to manage the file.
-///
-/// It also contains the download URL, although an additional secret is
-/// required.
-///
-/// The download URL can be generated using `download_url()` which will
-/// include the required secret in the URL.
+/// The metadata response from the server, when fetching the data through
+/// the API.
+/// 
+/// This metadata is required to successfully download and decrypt the
+/// corresponding file.
 #[derive(Debug, Deserialize)]
 #[derive(Debug, Deserialize)]
-struct DownloadResponse {
-    /// The file ID.
-    id: String,
-
-    /// The URL the file is reachable at.
-    /// This includes the file ID, but does not include the secret.
-    url: String,
-
-    /// The owner key, used to do further file modifications.
-    owner: String,
+struct MetadataResponse {
+    /// The encrypted metadata.
+    #[serde(rename="metadata")]
+    meta: String,
 }
 }
 
 
-impl DownloadResponse {
-    /// Convert this response into a file object.
+impl MetadataResponse {
+    /// Get and decrypt the metadata, based on the raw data in this response.
     ///
     ///
-    /// The `host` and `key` must be given.
-    pub fn into_file(self, host: Url, key: &KeySet) -> SendFile {
-        SendFile::new_now(
-            self.id,
-            host,
-            Url::parse(&self.url)
-                .expect("upload response URL parse error"),
-            key.secret().to_vec(),
-            self.owner,
+    /// 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<Metadata> {
+        // Decode the metadata
+        let raw = b64::decode(&self.meta)
+            .expect("failed to decode metadata from server");
+
+        // Get the encrypted metadata, and it's tag
+        let (encrypted, tag) = raw.split_at(raw.len() - 16);
+        // TODO: is the tag length correct, remove assert if it is
+        assert_eq!(tag.len(), 16);
+
+        // Decrypt the metadata
+        // TODO: is the tag verified here?
+        // TODO: do not unwrap, return an error
+		let meta = decrypt_aead(
+			KeySet::cipher(),
+			key_set.meta_key().unwrap(),
+			Some(key_set.iv()),
+			&[],
+			encrypted,
+			&tag,
+		).expect("failed to decrypt metadata");
+
+        // Parse the metadata, and return
+        Ok(
+            serde_json::from_slice(&meta)
+                .expect("failed to parse decrypted metadata as JSON")
         )
         )
     }
     }
 }
 }

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

@@ -1,2 +1,2 @@
-//pub mod download;
+pub mod download;
 pub mod upload;
 pub mod upload;

+ 14 - 10
api/src/file/file.rs

@@ -14,11 +14,11 @@ use crypto::b64;
 // TODO: match any sub-path?
 // TODO: match any sub-path?
 // TODO: match URL-safe base64 chars for the file ID?
 // TODO: match URL-safe base64 chars for the file ID?
 // TODO: constrain the ID length?
 // TODO: constrain the ID length?
-const DOWNLOAD_PATH_PATTERN: &'static str = r"$/?download/([[:alnum:]]{8,}={0,3})/?^";
+const DOWNLOAD_PATH_PATTERN: &'static str = r"^/?download/([[:alnum:]]{8,}={0,3})/?$";
 
 
 /// A pattern for Send download URL fragments, capturing the file secret.
 /// A pattern for Send download URL fragments, capturing the file secret.
 // TODO: constrain the secret length?
 // TODO: constrain the secret length?
-const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"$([a-zA-Z0-9-_+\/]+)?\s*^";
+const DOWNLOAD_FRAGMENT_PATTERN: &'static str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
 
 
 /// A struct representing an uploaded file on a Send host.
 /// A struct representing an uploaded file on a Send host.
 ///
 ///
@@ -159,20 +159,13 @@ impl DownloadFile {
     /// If the URL fragmet contains a file secret, it is also parsed.
     /// 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
     /// If it does not, the secret is left empty and must be specified
     /// manually.
     /// manually.
-    pub fn parse_url(url: String) -> Result<DownloadFile, FileParseError> {
-        // Try to parse as an URL
-        let url = Url::parse(&url)
-            .map_err(|err| FileParseError::UrlFormatError(err))?;
-
+    pub fn parse_url(url: Url) -> Result<DownloadFile, FileParseError> {
         // Build the host
         // Build the host
         let mut host = url.clone();
         let mut host = url.clone();
         host.set_fragment(None);
         host.set_fragment(None);
         host.set_query(None);
         host.set_query(None);
         host.set_path("");
         host.set_path("");
 
 
-        // TODO: remove this after debugging
-        println!("DEBUG: Extracted host: {}", host);
-
         // Validate the path, get the file ID
         // Validate the path, get the file ID
         let re_path = Regex::new(DOWNLOAD_PATH_PATTERN).unwrap();
         let re_path = Regex::new(DOWNLOAD_PATH_PATTERN).unwrap();
         let id = re_path.captures(url.path())
         let id = re_path.captures(url.path())
@@ -243,8 +236,19 @@ impl DownloadFile {
 
 
         url
         url
     }
     }
+
+    /// Get the API metadata URL of the file.
+    pub fn api_meta_url(&self) -> Url {
+        // Get the download 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);
+
+        url
+    }
 }
 }
 
 
+#[derive(Debug)]
 pub enum FileParseError {
 pub enum FileParseError {
     /// An URL format error.
     /// An URL format error.
     UrlFormatError(UrlParseError),
     UrlFormatError(UrlParseError),

+ 1 - 1
api/src/file/metadata.rs

@@ -14,7 +14,7 @@ use serde_json;
 use crypto::b64;
 use crypto::b64;
 
 
 /// File metadata, which is send to the server.
 /// File metadata, which is send to the server.
-#[derive(Serialize)]
+#[derive(Debug, Serialize, Deserialize)]
 pub struct Metadata {
 pub struct Metadata {
     /// The input vector.
     /// The input vector.
     iv: String,
     iv: String,

+ 66 - 0
cli/src/action/download.rs

@@ -0,0 +1,66 @@
+use std::path::Path;
+use std::sync::{Arc, Mutex};
+
+use ffsend_api::action::download::Download as ApiDownload;
+use ffsend_api::file::file::DownloadFile;
+use ffsend_api::reqwest::Client;
+
+use cmd::cmd_download::CmdDownload;
+use progress::ProgressBar;
+use util::open_url;
+#[cfg(feature = "clipboard")]
+use util::set_clipboard;
+
+/// A file download action.
+pub struct Download<'a> {
+    cmd: &'a CmdDownload<'a>,
+}
+
+impl<'a> Download<'a> {
+    /// Construct a new download action.
+    pub fn new(cmd: &'a CmdDownload<'a>) -> Self {
+        Self {
+            cmd,
+        }
+    }
+
+    /// Invoke the download action.
+    // TODO: create a trait for this method
+    pub fn invoke(&self) {
+        // Get the download URL
+        let url = self.cmd.url();
+
+        // Create a reqwest client
+        let client = Client::new();
+
+        // Parse the file based on the URL
+        let file = DownloadFile::parse_url(url)
+            .expect("invalid download URL, could not parse file data");
+
+        // Execute an download action
+        // TODO: do not unwrap, but return an error
+        ApiDownload::new(&file).invoke(&client).unwrap();
+
+        // // Get the download URL, and report it in the console
+        // let url = file.download_url(true);
+        // println!("Download URL: {}", url);
+
+        // // Open the URL in the browser
+        // if self.cmd.open() {
+        //     // TODO: do not expect, but return an error
+        //     open_url(url.clone()).expect("failed to open URL");
+        // }
+
+        // // Copy the URL in the user's clipboard
+        // #[cfg(feature = "clipboard")]
+        // {
+        //     if self.cmd.copy() {
+        //         // TODO: do not expect, but return an error
+        //         set_clipboard(url.as_str().to_owned())
+        //             .expect("failed to put download URL in user clipboard");
+        //     }
+        // }
+        
+        panic!("DONE");
+    }
+}

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

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

+ 65 - 0
cli/src/cmd/cmd_download.rs

@@ -0,0 +1,65 @@
+use ffsend_api::url::{ParseError, Url};
+
+use super::clap::{App, Arg, ArgMatches, SubCommand};
+
+use app::SEND_DEF_HOST;
+use util::quit_error;
+
+/// The download command.
+pub struct CmdDownload<'a> {
+    matches: &'a ArgMatches<'a>,
+}
+
+impl<'a: 'b, 'b> CmdDownload<'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("download")
+            .about("Download files")
+            .visible_alias("d")
+            .visible_alias("down")
+            .arg(Arg::with_name("URL")
+                .help("The download URL")
+                .required(true)
+                .multiple(false));
+
+        cmd
+    }
+
+    /// Parse CLI arguments, from the given parent command matches.
+    pub fn parse(parent: &'a ArgMatches<'a>) -> Option<CmdDownload<'a>> {
+        parent.subcommand_matches("download")
+            .map(|matches| CmdDownload { matches })
+    }
+
+    /// Get the URL to download the file from.
+    ///
+    /// 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("emtpy host given"),
+            Err(ParseError::InvalidPort) =>
+                quit_error("invalid host port"),
+            Err(ParseError::InvalidIpv4Address) =>
+                quit_error("invalid IPv4 address in host"),
+            Err(ParseError::InvalidIpv6Address) =>
+                quit_error("invalid IPv6 address in host"),
+            Err(ParseError::InvalidDomainCharacter) =>
+                quit_error("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"),
+        }
+    }
+}

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

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

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

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

+ 6 - 0
cli/src/main.rs

@@ -6,6 +6,7 @@ mod cmd;
 mod progress;
 mod progress;
 mod util;
 mod util;
 
 
+use action::download::Download;
 use action::upload::Upload;
 use action::upload::Upload;
 use cmd::Handler;
 use cmd::Handler;
 
 
@@ -28,6 +29,11 @@ fn invoke_action(handler: &Handler) {
         return Upload::new(&cmd).invoke();
         return Upload::new(&cmd).invoke();
     }
     }
 
 
+    // Match the download command
+    if let Some(cmd) = handler.download() {
+        return Download::new(&cmd).invoke();
+    }
+
     // No subcommand was selected, show general help
     // No subcommand was selected, show general help
     Handler::build()
     Handler::build()
         .print_help()
         .print_help()