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
use crate::api::data::{Error as DataError, OwnedData};
use crate::api::nonce::{request_nonce, NonceError};
use crate::api::request::{ensure_success, ResponseError};
use crate::api::url::UrlBuilder;
use crate::api::Version;
use crate::client::Client;
use crate::file::remote_file::RemoteFile;

/// The minimum allowed number of downloads, enforced by the server.
// TODO: remove parameter, use from config
pub const PARAMS_DOWNLOAD_MIN: u8 = 1;

/// The maximum (inclusive) allowed number of downloads,
/// enforced by the server.
// TODO: remove parameter, use from config
pub const PARAMS_DOWNLOAD_MAX: u8 = 20;

/// An action to set parameters for a shared file.
///
/// This API specification for this action is compatible with both Firefox Send v2 and v3.
pub struct Params<'a> {
    /// The remote file to change the parameters for.
    file: &'a RemoteFile,

    /// The parameter data that is sent to the server.
    params: ParamsData,

    /// The authentication nonce.
    /// May be an empty vector if the nonce is unknown.
    nonce: Vec<u8>,
}

impl<'a> Params<'a> {
    /// Construct a new parameters action for the given remote file.
    pub fn new(file: &'a RemoteFile, params: ParamsData, nonce: Option<Vec<u8>>) -> Self {
        Self {
            file,
            params,
            nonce: nonce.unwrap_or_default(),
        }
    }

    /// Invoke the parameters action.
    pub fn invoke(mut self, client: &Client) -> Result<(), Error> {
        // TODO: validate that the parameters object isn't empty

        // Fetch the authentication nonce if not set yet
        if self.nonce.is_empty() {
            self.nonce = self.fetch_auth_nonce(client)?;
        }

        // Wrap the parameters data
        let data = OwnedData::from(self.params.clone(), &self.file)
            .map_err(|err| -> PrepareError { err.into() })?;

        // Send the request to change the parameters
        self.change_params(client, &data)
    }

    /// Fetch the authentication nonce for the file from the remote server.
    fn fetch_auth_nonce(&self, client: &Client) -> Result<Vec<u8>, Error> {
        request_nonce(client, UrlBuilder::download(self.file, false)).map_err(|err| err.into())
    }

    /// Send the request for changing the parameters.
    fn change_params(&self, client: &Client, data: &OwnedData<ParamsData>) -> Result<(), Error> {
        // Get the params URL, and send the change
        let url = UrlBuilder::api_params(self.file);
        let response = client
            .post(url)
            .json(&data)
            .send()
            .map_err(|_| ChangeError::Request)?;

        // Ensure the response is successful
        ensure_success(&response).map_err(|err| err.into())
    }
}

/// The parameters data object, that is sent to the server.
// TODO: make sure builder parameters are in-bound as well
#[derive(Clone, Debug, Builder, Serialize)]
pub struct ParamsData {
    /// The number of times this file may be downloaded.
    /// This value must be within a specific range, as enforced by Send servers.
    #[serde(default)]
    #[builder(default)]
    pub download_limit: Option<u8>,

    /// The time in seconds after when the file expires.
    /// This value must be within a specific range, as enforced by Send servers.
    /// Only used with Send v3.
    #[serde(default)]
    #[builder(default)]
    #[serde(rename = "timeLimit")]
    pub expiry_time: Option<usize>,
}

impl ParamsData {
    /// Construct a new parameters object, that is empty.
    pub fn new() -> Self {
        Self::default()
    }

    /// Create a new parameters data object, with the given parameters.
    // TODO: the values must be between bounds
    pub fn from(download_limit: Option<u8>, expiry_time: Option<usize>) -> Self {
        ParamsData {
            download_limit,
            expiry_time,
        }
    }

    /// Set the maximum number of allowed downloads, after which the file
    /// will be removed.
    ///
    /// `None` may be given, to keep this parameter as is.
    ///
    /// An error may be returned if the download value is out of the allowed
    /// bound. These bounds are fixed and enforced by the server.
    /// See `PARAMS_DOWNLOAD_MIN` and `PARAMS_DOWNLOAD_MAX`.
    pub fn set_download_limit(
        &mut self,
        download_limit: Option<u8>,
    ) -> Result<(), ParamsDataError> {
        // TODO: use better bound check based on version

        // Check the download limit bounds
        if let Some(d) = download_limit {
            if d < PARAMS_DOWNLOAD_MIN || d > PARAMS_DOWNLOAD_MAX {
                return Err(ParamsDataError::DownloadBounds);
            }
        }

        // Set the download limit
        self.download_limit = download_limit;
        Ok(())
    }

    /// Set the expiry time in seconds for files.
    ///
    /// `None` may be given, to keep this parameter as is.
    ///
    /// An error may be returned if the expiry time value is out of the allowed
    /// bound. These bounds are fixed and enforced by the server.
    #[cfg(feature = "send3")]
    pub fn set_expiry_time(&mut self, expiry_time: Option<usize>) -> Result<(), ParamsDataError> {
        // TODO: do proper bound check based on version

        // // Check the download limit bounds
        // if let Some(d) = expiry_time {
        //     if d < PARAMS_DOWNLOAD_MIN || d > PARAMS_DOWNLOAD_MAX {
        //         return Err(ParamsDataError::DownloadBounds);
        //     }
        // }

        // Set the expiry time
        self.expiry_time = expiry_time;
        Ok(())
    }

    /// Check whether this parameters object is empty,
    /// and wouldn't change any parameter on the server when sent.
    /// Sending an empty parameter data object would thus be useless.
    pub fn is_empty(&self) -> bool {
        self.download_limit.is_none() && self.expiry_time.is_none()
    }

    /// Normalize this data for the given version.
    ///
    /// For example, Send v2 does not support the expiry time field. If normalizing for this
    /// version this field is dropped from the struct.
    #[allow(unused_variables)]
    pub fn normalize(&mut self, version: Version) {
        #[cfg(feature = "send2")]
        {
            #[allow(unreachable_patterns)]
            match version {
                Version::V2 => self.expiry_time = None,
                _ => {}
            }
        }
    }
}

impl Default for ParamsData {
    fn default() -> ParamsData {
        ParamsData {
            download_limit: None,
            expiry_time: None,
        }
    }
}

#[derive(Fail, Debug)]
pub enum Error {
    /// An error occurred while preparing the action.
    #[fail(display = "failed to prepare setting the parameters")]
    Prepare(#[cause] PrepareError),

    /// The given Send file has expired, or did never exist in the first place.
    /// Therefore the file could not be downloaded.
    #[fail(display = "the file has expired or did never exist")]
    Expired,

    /// An error has occurred while sending the parameter change request to
    /// the server.
    #[fail(display = "failed to send the parameter change request")]
    Change(#[cause] ChangeError),
}

impl From<NonceError> for Error {
    fn from(err: NonceError) -> Error {
        match err {
            NonceError::Expired => Error::Expired,
            err => Error::Prepare(PrepareError::Auth(err)),
        }
    }
}

impl From<PrepareError> for Error {
    fn from(err: PrepareError) -> Error {
        Error::Prepare(err)
    }
}

impl From<ChangeError> for Error {
    fn from(err: ChangeError) -> Error {
        Error::Change(err)
    }
}

impl From<ResponseError> for Error {
    fn from(err: ResponseError) -> Error {
        match err {
            ResponseError::Expired => Error::Expired,
            err => Error::Change(ChangeError::Response(err)),
        }
    }
}

#[derive(Debug, Fail)]
pub enum ParamsDataError {
    /// The number of downloads is invalid, as it was out of the allowed
    /// bounds. See `PARAMS_DOWNLOAD_MIN` and `PARAMS_DOWNLOAD_MAX`.
    // TODO: use bound values from constants, don't hardcode them here
    #[fail(display = "invalid number of downloads, must be between 1 and 20")]
    DownloadBounds,

    /// Some error occurred while trying to wrap the parameter data in an
    /// owned object, which is required for authentication on the server.
    /// The wrapped error further described the problem.
    #[fail(display = "")]
    Owned(#[cause] DataError),
}

#[derive(Fail, Debug)]
pub enum PrepareError {
    /// Failed authenticating, needed to change the parameters.
    #[fail(display = "failed to authenticate")]
    Auth(#[cause] NonceError),

    /// An error occurred while building the parameter data that will be send
    /// to the server.
    #[fail(display = "invalid parameters")]
    ParamsData(#[cause] ParamsDataError),
}

impl From<DataError> for PrepareError {
    fn from(err: DataError) -> PrepareError {
        PrepareError::ParamsData(ParamsDataError::Owned(err))
    }
}

#[derive(Fail, Debug)]
pub enum ChangeError {
    /// Sending the request to change the parameters failed.
    #[fail(display = "failed to send parameter change request")]
    Request,

    /// The server responded with an error while changing the file parameters.
    #[fail(display = "bad response from server while changing parameters")]
    Response(#[cause] ResponseError),
}