ffsend_api/action/
upload.rs

1extern crate mime;
2
3use std::fs::File;
4use std::io::{self, Error as IoError, Read};
5#[cfg(feature = "send3")]
6use std::mem;
7use std::path::PathBuf;
8use std::sync::{Arc, Mutex};
9
10#[cfg(feature = "send2")]
11use self::mime::APPLICATION_OCTET_STREAM;
12#[cfg(feature = "send3")]
13use chrono::Duration;
14use chrono::{DateTime, Utc};
15use mime_guess::{self, Mime};
16#[cfg(feature = "send2")]
17use reqwest::blocking::multipart::{Form, Part};
18#[cfg(feature = "send2")]
19use reqwest::blocking::Request;
20#[cfg(feature = "send2")]
21use reqwest::header::AUTHORIZATION;
22use reqwest::Error as ReqwestError;
23#[cfg(feature = "send3")]
24use serde_json;
25use thiserror::Error;
26use url::{ParseError as UrlParseError, Url};
27#[cfg(feature = "send3")]
28use websocket::{result::WebSocketError, OwnedMessage};
29
30#[cfg(feature = "send2")]
31use super::params::Params;
32use super::params::{Error as ParamsError, ParamsData};
33use super::password::{Error as PasswordError, Password};
34#[cfg(feature = "send2")]
35use crate::api::nonce::header_nonce;
36#[cfg(feature = "send2")]
37use crate::api::request::ensure_success;
38use crate::api::request::ResponseError;
39use crate::api::Version;
40use crate::client::Client;
41use crate::crypto::{self, api::MetadataError, key_set::KeySet};
42#[cfg(feature = "send3")]
43use crate::file::info::FileInfo;
44use crate::file::metadata::Metadata;
45use crate::file::remote_file::RemoteFile;
46#[cfg(feature = "send3")]
47use crate::io::ChunkRead;
48#[cfg(feature = "send2")]
49use crate::pipe::crypto::GcmCrypt;
50#[cfg(feature = "send3")]
51use crate::pipe::crypto::{ece, EceCrypt};
52use crate::pipe::{
53    prelude::*,
54    progress::{ProgressPipe, ProgressReporter},
55};
56
57/// A file upload action to a Send server.
58///
59/// The server API version to use must be given.
60pub struct Upload {
61    /// The server API version to use.
62    version: Version,
63
64    /// The Send host to upload the file to.
65    host: Url,
66
67    /// The file to upload.
68    path: PathBuf,
69
70    /// The name of the file being uploaded.
71    /// This has no relation to the file path, and will become the name of the
72    /// shared file if set.
73    name: Option<String>,
74
75    /// An optional password to protect the file with.
76    password: Option<String>,
77
78    /// Optional file parameters to set.
79    params: Option<ParamsData>,
80}
81
82impl Upload {
83    /// Construct a new upload action.
84    pub fn new(
85        version: Version,
86        host: Url,
87        path: PathBuf,
88        name: Option<String>,
89        password: Option<String>,
90        params: Option<ParamsData>,
91    ) -> Self {
92        Self {
93            version,
94            host,
95            path,
96            name,
97            password,
98            params,
99        }
100    }
101
102    /// Invoke the upload action.
103    pub fn invoke(
104        self,
105        client: &Client,
106        reporter: Option<&Arc<Mutex<dyn ProgressReporter>>>,
107    ) -> Result<RemoteFile, Error> {
108        // Create file data, generate a key
109        let file = FileData::from(&self.path)?;
110        let key = KeySet::generate(true);
111
112        // Create the file reader
113        let reader = self.create_reader(&key, reporter.cloned())?;
114        let reader_len = reader.len_in() as u64;
115
116        // Start the reporter
117        if let Some(reporter) = reporter {
118            reporter
119                .lock()
120                .map_err(|_| UploadError::Progress)?
121                .start(reader_len);
122        }
123
124        // Execute the request
125        let (result, nonce) = match self.version {
126            #[cfg(feature = "send2")]
127            Version::V2 => self.upload_send2(client, &key, &file, reader)?,
128            #[cfg(feature = "send3")]
129            Version::V3 => self.upload_send3(client, &key, &file, reader)?,
130        };
131
132        // Mark the reporter as finished
133        if let Some(reporter) = reporter {
134            reporter.lock().map_err(|_| UploadError::Progress)?.finish();
135        }
136
137        // Change the password if set
138        if let Some(password) = self.password {
139            Password::new(&result, &password, nonce.clone()).invoke(client)?;
140        }
141
142        // Change parameters if any non-default, are already set with upload for Send v3
143        #[cfg(feature = "send2")]
144        {
145            #[allow(unreachable_patterns)]
146            match self.version {
147                Version::V2 => {
148                    if let Some(mut params) = self.params {
149                        params.normalize(self.version);
150                        if !params.is_empty() {
151                            Params::new(&result, params, nonce.clone()).invoke(client)?;
152                        }
153                    }
154                }
155                _ => {}
156            }
157        }
158
159        Ok(result)
160    }
161
162    /// Create an encoded blob of encrypted metadata, used in Firefox Send v2.
163    #[cfg(feature = "send2")]
164    fn create_metadata(&self, key_set: &KeySet, file: &FileData) -> Result<String, MetadataError> {
165        // Determine what filename to use
166        let name = self.name.clone().unwrap_or_else(|| file.name().to_owned());
167
168        // Construct the metadata, encrypt it
169        let metadata = Metadata::from_send2(key_set.nonce(), name, &file.mime());
170        crypto::api::encrypt_metadata(metadata, key_set)
171    }
172
173    /// Create file info to send to the server, used for Firefox Send v3.
174    #[cfg(feature = "send3")]
175    fn create_file_info(&self, key_set: &KeySet, file: &FileData) -> Result<String, MetadataError> {
176        // Determine what filename to use, build the metadata
177        let name = self.name.clone().unwrap_or_else(|| file.name().to_owned());
178
179        // Construct the metadata, encrypt it
180        let mime = format!("{}", file.mime());
181        let metadata = Metadata::from_send3(name, mime, file.size());
182        let metadata = crypto::api::encrypt_metadata(metadata, key_set)?;
183
184        // Get the expiry time and donwload limit
185        let expiry = self.params.as_ref().and_then(|p| p.expiry_time);
186        let downloads = self.params.as_ref().and_then(|p| p.download_limit);
187
188        // Build file info for this metadata and return it as JSON
189        Ok(FileInfo::from(expiry, downloads, metadata, key_set).to_json())
190    }
191
192    /// Create a reader that reads the file as encrypted stream.
193    fn create_reader(
194        &self,
195        key: &KeySet,
196        reporter: Option<Arc<Mutex<dyn ProgressReporter>>>,
197    ) -> Result<Reader, Error> {
198        // Open the file
199        let file = match File::open(self.path.as_path()) {
200            Ok(file) => file,
201            Err(err) => return Err(FileError::Open(err).into()),
202        };
203
204        // Get the file length
205        let len = file
206            .metadata()
207            .expect("failed to fetch file metadata")
208            .len();
209
210        // Build the progress pipe file reader
211        let progress = ProgressPipe::zero(len, reporter);
212        let reader = progress.reader(Box::new(file));
213
214        // Build the encrypting file reader
215        match self.version {
216            #[cfg(feature = "send2")]
217            Version::V2 => {
218                let encrypt = GcmCrypt::encrypt(len as usize, key.file_key().unwrap(), key.nonce());
219                let reader = encrypt.reader(Box::new(reader));
220                Ok(Reader::new(Box::new(reader)))
221            }
222            #[cfg(feature = "send3")]
223            Version::V3 => {
224                let ikm = key.secret().to_vec();
225                let encrypt = EceCrypt::encrypt(len as usize, ikm, None);
226                let reader = encrypt.reader(Box::new(reader));
227                Ok(Reader::new(Box::new(reader)))
228            }
229        }
230    }
231
232    /// Upload the file to the server, used in Firefox Send v2.
233    #[cfg(feature = "send2")]
234    fn upload_send2(
235        &self,
236        client: &Client,
237        key: &KeySet,
238        file: &FileData,
239        reader: Reader,
240    ) -> Result<(RemoteFile, Option<Vec<u8>>), Error> {
241        // Create metadata
242        let metadata = self.create_metadata(&key, file)?;
243
244        // Create the request to send
245        let req = self.create_request_send2(client, &key, &metadata, reader)?;
246
247        // Execute the request
248        self.execute_request_send2(req, client, &key)
249            .map_err(|e| e.into())
250    }
251
252    /// Build the request that will be send to the server, used in Firefox Send v2.
253    #[cfg(feature = "send2")]
254    fn create_request_send2(
255        &self,
256        client: &Client,
257        key: &KeySet,
258        metadata: &str,
259        reader: Reader,
260    ) -> Result<Request, UploadError> {
261        // Get the reader output length
262        let len = reader.len_out() as u64;
263
264        // Configure a form to send
265        let part = Part::reader_with_length(reader, len)
266            .mime_str(APPLICATION_OCTET_STREAM.as_ref())
267            .expect("failed to set request mime");
268        let form = Form::new().part("data", part);
269
270        // Define the URL to call
271        let url = self.host.join("api/upload")?;
272
273        // Build the request
274        client
275            .post(url.as_str())
276            .header(
277                AUTHORIZATION.as_str(),
278                format!("send-v1 {}", key.auth_key_encoded().unwrap()),
279            )
280            .header("X-File-Metadata", metadata)
281            .multipart(form)
282            .build()
283            .map_err(|_| UploadError::Request)
284    }
285
286    /// Execute the given request, and create a file object that represents the
287    /// uploaded file, used in Firefox Send v2.
288    #[cfg(feature = "send2")]
289    fn execute_request_send2(
290        &self,
291        req: Request,
292        client: &Client,
293        key: &KeySet,
294    ) -> Result<(RemoteFile, Option<Vec<u8>>), UploadError> {
295        // Execute the request
296        let response = match client.execute(req) {
297            Ok(response) => response,
298            // TODO: attach the error context
299            Err(_) => return Err(UploadError::Request),
300        };
301
302        // Ensure the response is successful
303        ensure_success(&response).map_err(UploadError::Response)?;
304
305        // Try to get the nonce, don't error on failure
306        let nonce = header_nonce(&response).ok();
307
308        // Decode the response
309        let response: UploadResponse = match response.json() {
310            Ok(response) => response,
311            Err(err) => return Err(UploadError::Decode(err)),
312        };
313
314        // Transform the responce into a file object
315        Ok((response.into_file(self.host.clone(), &key, None)?, nonce))
316    }
317
318    /// Upload the file to the server, used in Firefox Send v3.
319    #[cfg(feature = "send3")]
320    fn upload_send3(
321        &self,
322        client: &Client,
323        key: &KeySet,
324        file_data: &FileData,
325        mut reader: Reader,
326    ) -> Result<(RemoteFile, Option<Vec<u8>>), Error> {
327        // Build the uploading websocket URL
328        let ws_url = self
329            .host
330            .join("api/ws")
331            .map_err(|e| Error::Upload(e.into()))?;
332
333        // Build the websocket client used for uploading
334        let mut client = client
335            .websocket(ws_url.as_str())
336            .map_err(|_| Error::Upload(UploadError::Request))?;
337
338        // Create file info to sent when uploading
339        let file_info = self
340            .create_file_info(&key, file_data)
341            .map_err(|e| -> Error { e.into() })?;
342        let ws_metadata = OwnedMessage::Text(file_info);
343        client
344            .send_message(&ws_metadata)
345            .map_err(|e| Error::Upload(e.into()))?;
346
347        // Read the upload initialization response from the server
348        let result = client
349            .recv_message()
350            .map_err(|_| Error::Upload(UploadError::InvalidResponse))?;
351        let upload_response: UploadResponse = match result {
352            OwnedMessage::Text(ref data) => serde_json::from_str(data)
353                .map_err(|_| Error::Upload(UploadError::InvalidResponse))?,
354            _ => return Err(UploadError::InvalidResponse.into()),
355        };
356
357        // Read the header part, and send it
358        let mut header = vec![0u8; ece::HEADER_LEN as usize];
359        reader
360            .read_exact(&mut header)
361            .expect("failed to read header from reader");
362        client
363            .send_message(&OwnedMessage::Binary(header))
364            .map_err(|e| Error::Upload(e.into()))?;
365
366        // Send the whole encrypted file in chunks as binary websocket messages
367        let result =
368            reader
369                .chunks(ece::RS as usize)
370                .fold(None, |result: Option<UploadError>, chunk| {
371                    // Skip if an error occurred
372                    if result.is_some() {
373                        return result;
374                    }
375
376                    // Send the message, capture errors
377                    let message = OwnedMessage::Binary(chunk.expect("invalid chunk"));
378                    client.send_message(&message).err().map(|e| e.into())
379                });
380        if let Some(err) = result {
381            return Err(err.into());
382        }
383
384        // Send the file footer
385        client
386            .send_message(&OwnedMessage::Binary(vec![0]))
387            .map_err(|e| Error::Upload(e.into()))?;
388
389        // Make sure we receive a success message from the server
390        let status = match client
391            .recv_message()
392            .map_err(|_| Error::Upload(UploadError::InvalidResponse))?
393        {
394            OwnedMessage::Text(status) => Some(status),
395            _ => None,
396        };
397        let ok = status
398            .and_then(|s| serde_json::from_str::<UploadStatusResponse>(&s).ok())
399            .map(|s| s.is_ok())
400            .unwrap_or(false);
401        if !ok {
402            return Err(UploadError::Response(ResponseError::Undefined).into());
403        }
404
405        // Done uploading, explicitly close upload client
406        let _ = client.shutdown();
407        mem::drop(client);
408
409        // Construct the remote file from the data we've obtained
410        let remote_file = upload_response.into_file(
411            self.host.clone(),
412            &key,
413            self.params.as_ref().and_then(|p| {
414                p.expiry_time
415                    .map(|s| Utc::now() + Duration::seconds(s as i64))
416            }),
417        )?;
418
419        Ok((remote_file, None))
420    }
421}
422
423/// The response from the server after a file has been uploaded.
424/// This response contains the file ID and owner key, to manage the file.
425///
426/// It also contains the download URL, although an additional secret is
427/// required.
428///
429/// The download URL can be generated using `download_url()` which will
430/// include the required secret in the URL.
431#[derive(Debug, Deserialize)]
432struct UploadResponse {
433    /// The file ID.
434    id: String,
435
436    /// The URL the file is reachable at.
437    /// This includes the file ID, but does not include the secret.
438    url: String,
439
440    /// The owner key, used to do further file modifications.
441    ///
442    /// Called `owner` in Firefox Send v2, and `ownerToken` in Firefox Send v3.
443    #[serde(alias = "ownerToken", alias = "owner")]
444    owner_token: String,
445}
446
447impl UploadResponse {
448    /// Convert this response into a file object.
449    ///
450    /// The `host` and `key` must be given.
451    pub fn into_file(
452        self,
453        host: Url,
454        key: &KeySet,
455        expiry_time: Option<DateTime<Utc>>,
456    ) -> Result<RemoteFile, UploadError> {
457        Ok(RemoteFile::new(
458            self.id,
459            Some(Utc::now()),
460            expiry_time,
461            host,
462            Url::parse(&self.url)?,
463            key.secret().to_vec(),
464            Some(self.owner_token),
465        ))
466    }
467}
468
469/// The status response from the server over the websocket during or after uploading.
470/// This defines whether uploading was successful.
471#[derive(Debug, Deserialize)]
472#[cfg(feature = "send3")]
473struct UploadStatusResponse {
474    /// True if ok, false if not.
475    ok: bool,
476}
477
478#[cfg(feature = "send3")]
479impl UploadStatusResponse {
480    /// Check if OK.
481    pub fn is_ok(&self) -> bool {
482        self.ok
483    }
484}
485
486/// A struct that holds various file properties, such as it's name and it's
487/// mime type.
488struct FileData<'a> {
489    /// The file name.
490    name: &'a str,
491
492    /// The file mime type.
493    mime: Mime,
494
495    /// The file size.
496    #[allow(unused)]
497    size: u64,
498}
499
500impl<'a> FileData<'a> {
501    /// Create a file data object, from the file at the given path.
502    pub fn from(path: &'a PathBuf) -> Result<Self, FileError> {
503        // Make sure the given path is a file
504        if !path.is_file() {
505            return Err(FileError::NotAFile);
506        }
507
508        // Get the file name
509        let name = match path.file_name() {
510            Some(name) => name.to_str().unwrap_or("file"),
511            None => "file",
512        };
513
514        // Get the file size
515        let size = path.metadata()?.len();
516
517        Ok(Self {
518            name,
519            mime: mime_guess::from_path(path).first_or_octet_stream(),
520            size,
521        })
522    }
523
524    /// Get the file name.
525    pub fn name(&self) -> &str {
526        self.name
527    }
528
529    /// Get the file mime type.
530    pub fn mime(&self) -> &Mime {
531        &self.mime
532    }
533
534    /// Get the file size in bytes.
535    #[cfg(feature = "send3")]
536    pub fn size(&self) -> u64 {
537        self.size
538    }
539}
540
541/// A wrapped reader to make it easier to pass around.
542struct Reader {
543    // TODO: use better type
544    inner: Box<dyn ReadLen>,
545}
546
547impl Reader {
548    /// Construct a new wrapped reader.
549    pub fn new(inner: Box<dyn ReadLen>) -> Self {
550        Self { inner }
551    }
552}
553
554impl Read for Reader {
555    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
556        self.inner.read(buf)
557    }
558}
559
560impl PipeLen for Reader {
561    fn len_in(&self) -> usize {
562        self.inner.len_in()
563    }
564
565    fn len_out(&self) -> usize {
566        self.inner.len_out()
567    }
568}
569
570#[derive(Error, Debug)]
571pub enum Error {
572    /// An error occurred while preparing a file for uploading.
573    #[error("failed to prepare uploading the file")]
574    Prepare(#[from] PrepareError),
575
576    /// An error occurred while opening, reading or using the file that
577    /// the should be uploaded.
578    // TODO: maybe append the file path here for further information
579    #[error("")]
580    File(#[from] FileError),
581
582    /// An error occurred while uploading the file.
583    #[error("failed to upload the file")]
584    Upload(#[from] UploadError),
585
586    /// An error occurred while chaining file parameters.
587    #[error("failed to change file parameters")]
588    Params(#[from] ParamsError),
589
590    /// An error occurred while setting the password.
591    #[error("failed to set the password")]
592    Password(#[from] PasswordError),
593}
594
595impl From<MetadataError> for Error {
596    fn from(err: MetadataError) -> Error {
597        Error::Prepare(PrepareError::Meta(err))
598    }
599}
600
601impl From<ReaderError> for Error {
602    fn from(err: ReaderError) -> Error {
603        Error::Prepare(PrepareError::Reader(err))
604    }
605}
606
607#[derive(Error, Debug)]
608pub enum PrepareError {
609    /// Failed to prepare the file metadata for uploading.
610    #[error("failed to prepare file metadata")]
611    Meta(#[from] MetadataError),
612
613    /// Failed to create an encrypted file reader, that encrypts
614    /// the file on the fly when it is read.
615    #[error("failed to access the file to upload")]
616    Reader(#[from] ReaderError),
617
618    /// Failed to create a client for uploading a file.
619    #[error("failed to create uploader client")]
620    Client,
621}
622
623#[derive(Error, Debug)]
624pub enum ReaderError {
625    /// An error occurred while creating the file encryptor.
626    #[error("failed to create file encryptor")]
627    Encrypt,
628
629    /// Failed to create the progress reader, attached to the file reader,
630    /// to measure the uploading progress.
631    #[error("failed to create progress reader")]
632    Progress,
633}
634
635#[derive(Error, Debug)]
636pub enum FileError {
637    /// The given path, is not not a file or doesn't exist.
638    #[error("the given path is not an existing file")]
639    NotAFile,
640
641    /// Failed to open the file that must be uploaded for reading.
642    #[error("failed to open the file to upload")]
643    Open(#[from] IoError),
644}
645
646#[derive(Error, Debug)]
647pub enum UploadError {
648    /// Failed to start or update the uploading progress, because of this the
649    /// upload can't continue.
650    #[error("failed to update upload progress")]
651    Progress,
652
653    /// Sending the request to upload the file failed.
654    #[error("failed to request file upload")]
655    Request,
656
657    /// An error occurred while streaming the encrypted file (including file info, header and
658    /// footer) for uploading over a websocket.
659    #[error("failed to stream file for upload over websocket")]
660    #[cfg(feature = "send3")]
661    UploadStream(#[from] WebSocketError),
662
663    /// The server responded with data that was not understood, or did not respond at all while a
664    /// response was espected.
665    #[error("got invalid response from server")]
666    InvalidResponse,
667
668    /// The server responded with an error for uploading.
669    #[error("bad response from server for uploading")]
670    Response(#[from] ResponseError),
671
672    /// Failed to decode the upload response from the server.
673    /// Maybe the server responded with data from a newer API version.
674    #[error("failed to decode upload response")]
675    Decode(#[from] ReqwestError),
676
677    /// Failed to parse the retrieved URL from the upload response.
678    #[error("failed to parse received URL")]
679    ParseUrl(#[from] UrlParseError),
680}