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