ffsend_api/action/
download.rs

1use std::fs::File;
2use std::io::{self, Error as IoError, Read, Write};
3use std::path::PathBuf;
4use std::sync::{Arc, Mutex};
5
6use reqwest::{blocking::Response, header::AUTHORIZATION};
7use thiserror::Error;
8
9use super::metadata::{Error as MetadataError, Metadata as MetadataAction, MetadataResponse};
10use crate::api::request::{ensure_success, ResponseError};
11use crate::api::url::UrlBuilder;
12use crate::api::Version;
13use crate::client::Client;
14use crate::crypto::key_set::KeySet;
15use crate::crypto::sig::signature_encoded;
16use crate::file::remote_file::RemoteFile;
17#[cfg(feature = "send3")]
18use crate::pipe::crypto::EceCrypt;
19#[cfg(feature = "send2")]
20use crate::pipe::crypto::GcmCrypt;
21use crate::pipe::{
22    prelude::*,
23    progress::{ProgressPipe, ProgressReporter},
24};
25
26/// A file download action to a Send server.
27///
28/// This action is compatible with both Firefox Send v2 and v3, but the server API version to use
29/// must be explicitly given due to a version specific download method.
30pub struct Download<'a> {
31    /// The server API version to use when downloading the file.
32    version: Version,
33
34    /// The remote file to download.
35    file: &'a RemoteFile,
36
37    /// The target file or directory, to download the file to.
38    target: PathBuf,
39
40    /// An optional password to decrypt a protected file.
41    password: Option<String>,
42
43    /// Check whether the file exists (recommended).
44    check_exists: bool,
45
46    /// The metadata response to work with,
47    /// which will skip the internal metadata request.
48    metadata_response: Option<MetadataResponse>,
49}
50
51impl<'a> Download<'a> {
52    /// Construct a new download action for the given remote file.
53    /// It is recommended to check whether the file exists,
54    /// unless that is already done.
55    pub fn new(
56        version: Version,
57        file: &'a RemoteFile,
58        target: PathBuf,
59        password: Option<String>,
60        check_exists: bool,
61        metadata_response: Option<MetadataResponse>,
62    ) -> Self {
63        Self {
64            version,
65            file,
66            target,
67            password,
68            check_exists,
69            metadata_response,
70        }
71    }
72
73    /// Invoke the download action.
74    pub fn invoke(
75        mut self,
76        client: &Client,
77        reporter: Option<Arc<Mutex<dyn ProgressReporter>>>,
78    ) -> Result<(), Error> {
79        // Create a key set for the file
80        let mut key = KeySet::from(self.file, self.password.as_ref());
81
82        // Get the metadata, or fetch the file metadata,
83        // then update the input vector in the key set
84        let metadata: MetadataResponse = if self.metadata_response.is_some() {
85            self.metadata_response.take().unwrap()
86        } else {
87            MetadataAction::new(self.file, self.password.clone(), self.check_exists)
88                .invoke(&client)?
89        };
90
91        // Set the input vector if known, depending on the API version
92        if let Some(nonce) = metadata.metadata().iv() {
93            key.set_nonce(nonce);
94        }
95
96        // Decide what actual file target to use
97        let path = self.decide_path(metadata.metadata().name());
98        let path_str = path.to_str().unwrap_or("?").to_owned();
99
100        // Open the file we will write to
101        // TODO: this should become a temporary file first
102        // TODO: use the uploaded file name as default
103        let out = File::create(path)
104            .map_err(|err| Error::File(path_str.clone(), FileError::Create(err)))?;
105
106        // Create the file reader for downloading
107        let (reader, len) = self.create_file_reader(&key, &metadata, &client)?;
108
109        // Create the file writer
110        let writer = self
111            .create_writer(out, len, &key, reporter.clone())
112            .map_err(|err| Error::File(path_str.clone(), err))?;
113
114        // Download the file
115        self.download(reader, writer, len, reporter)?;
116
117        // TODO: return the file path
118        // TODO: return the new remote state (does it still exist remote)
119
120        Ok(())
121    }
122
123    /// Decide what path we will download the file to.
124    ///
125    /// A target file or directory, and a file name hint must be given.
126    /// The name hint can be derived from the retrieved metadata on this file.
127    ///
128    /// The name hint is used as file name, if a directory was given.
129    fn decide_path(&self, name_hint: &str) -> PathBuf {
130        // Return the target if it is an existing file
131        if self.target.is_file() {
132            return self.target.clone();
133        }
134
135        // Append the name hint if this is a directory
136        if self.target.is_dir() {
137            return self.target.join(name_hint);
138        }
139
140        // Return if the parent is an existing directory
141        if self.target.parent().map(|p| p.is_dir()).unwrap_or(false) {
142            return self.target.clone();
143        }
144
145        // TODO: are these todos below already implemented in CLI client?
146        // TODO: canonicalize the path when possible
147        // TODO: allow using `file.toml` as target without directory indication
148        // TODO: return a nice error here as the path may be invalid
149        // TODO: maybe prompt the user to create the directory
150        panic!("Invalid (non-existing) output path given, not yet supported");
151    }
152
153    /// Make a download request, and create a reader that downloads the
154    /// encrypted file.
155    ///
156    /// The response representing the file reader is returned along with the
157    /// length of the reader content.
158    fn create_file_reader(
159        &self,
160        key: &KeySet,
161        metadata: &MetadataResponse,
162        client: &Client,
163    ) -> Result<(Response, u64), DownloadError> {
164        // Compute the cryptographic signature
165        let sig = signature_encoded(key.auth_key().unwrap(), metadata.nonce())
166            .map_err(|_| DownloadError::ComputeSignature)?;
167
168        // Build and send the download request
169        let response = client
170            .get(UrlBuilder::api_download(self.file))
171            .header(AUTHORIZATION.as_str(), format!("send-v1 {}", sig))
172            .send()
173            .map_err(|_| DownloadError::Request)?;
174
175        // Ensure the response is successful
176        ensure_success(&response).map_err(DownloadError::Response)?;
177
178        // Get the content length
179        // TODO: make sure there is enough disk space
180        let len = metadata.size();
181
182        Ok((response, len))
183    }
184
185    /// Create a file writer.
186    ///
187    /// This writer will will decrypt the input on the fly, and writes the
188    /// decrypted data to the given file.
189    fn create_writer(
190        &self,
191        file: File,
192        len: u64,
193        key: &KeySet,
194        reporter: Option<Arc<Mutex<dyn ProgressReporter>>>,
195    ) -> Result<impl Write, FileError> {
196        // Build the decrypting file writer for the selected server API version
197        let writer: Box<dyn Write> = match self.version {
198            #[cfg(feature = "send2")]
199            Version::V2 => {
200                let decrypt = GcmCrypt::decrypt(len as usize, key.file_key().unwrap(), key.nonce());
201                let writer = decrypt.writer(Box::new(file));
202                Box::new(writer)
203            }
204            #[cfg(feature = "send3")]
205            Version::V3 => {
206                let ikm = key.secret().to_vec();
207                let decrypt = EceCrypt::decrypt(len as usize, ikm);
208                let writer = decrypt.writer(Box::new(file));
209                Box::new(writer)
210            }
211        };
212
213        // Build the progress pipe file writer
214        let progress = ProgressPipe::zero(len as u64, reporter);
215        let writer = progress.writer(writer);
216
217        Ok(writer)
218    }
219
220    /// Download the file from the reader, and write it to the writer.
221    /// The length of the file must also be given.
222    /// The status will be reported to the given progress reporter.
223    fn download<R, W>(
224        &self,
225        mut reader: R,
226        mut writer: W,
227        len: u64,
228        reporter: Option<Arc<Mutex<dyn ProgressReporter>>>,
229    ) -> Result<(), DownloadError>
230    where
231        R: Read,
232        W: Write,
233    {
234        // Start the writer
235        if let Some(reporter) = reporter.as_ref() {
236            reporter
237                .lock()
238                .map_err(|_| DownloadError::Progress)?
239                .start(len);
240        }
241
242        // Write to the output file
243        io::copy(&mut reader, &mut writer).map_err(|_| DownloadError::Download)?;
244
245        // Finish
246        if let Some(reporter) = reporter.as_ref() {
247            reporter
248                .lock()
249                .map_err(|_| DownloadError::Progress)?
250                .finish();
251        }
252
253        Ok(())
254    }
255}
256
257#[derive(Error, Debug)]
258pub enum Error {
259    /// An error occurred while fetching the metadata of the file.
260    /// This step is required in order to succsessfully decrypt the
261    /// file that will be downloaded.
262    #[error("failed to fetch file metadata")]
263    Meta(#[from] MetadataError),
264
265    /// The given Send file has expired, or did never exist in the first place.
266    /// Therefore the file could not be downloaded.
267    #[error("the file has expired or did never exist")]
268    Expired,
269
270    /// A password is required, but was not given.
271    #[error("missing password, password required")]
272    PasswordRequired,
273
274    /// An error occurred while downloading the file.
275    #[error("failed to download the file")]
276    Download(#[from] DownloadError),
277
278    /// An error occurred while decrypting the downloaded file.
279    #[error("failed to decrypt the downloaded file")]
280    Decrypt,
281
282    /// An error occurred while opening or writing to the target file.
283    // TODO: show what file this is about
284    #[error("couldn't use the target file at '{}'", _0)]
285    File(String, #[source] FileError),
286}
287
288#[derive(Error, Debug)]
289pub enum DownloadError {
290    /// An error occurred while computing the cryptographic signature used for
291    /// downloading the file.
292    #[error("failed to compute cryptographic signature")]
293    ComputeSignature,
294
295    /// Sending the request to download the file failed.
296    #[error("failed to request file download")]
297    Request,
298
299    /// The server responded with an error while requesting the file download.
300    #[error("bad response from server while requesting download")]
301    Response(#[from] ResponseError),
302
303    /// Failed to start or update the downloading progress, because of this the
304    /// download can't continue.
305    #[error("failed to update download progress")]
306    Progress,
307
308    /// The actual download and decryption process the server.
309    /// This covers reading the file from the server, decrypting the file,
310    /// and writing it to the file system.
311    #[error("failed to download the file")]
312    Download,
313    // /// Verifying the downloaded file failed.
314    // #[error("file verification failed")]
315    // Verify,
316}
317
318#[derive(Error, Debug)]
319pub enum FileError {
320    /// An error occurred while creating or opening the file to write to.
321    #[error("failed to create or replace the file")]
322    Create(#[from] IoError),
323
324    /// Failed to create an encrypted writer for the file, which is used to
325    /// decrypt the downloaded file.
326    #[error("failed to create file decryptor")]
327    EncryptedWriter,
328}