Kaynağa Gözat

Add API action to fetch remote file metadata

timvisee 7 yıl önce
ebeveyn
işleme
2e4803848b
3 değiştirilmiş dosya ile 316 ekleme ve 0 silme
  1. 1 0
      IDEAS.md
  2. 314 0
      api/src/action/metadata.rs
  3. 1 0
      api/src/action/mod.rs

+ 1 - 0
IDEAS.md

@@ -40,3 +40,4 @@
 - Rename host to server?
 - Read and write files from and to stdin and stdout with `-` as file
 - Ask to add MIME extension to downloaded files without one on Windows
+- Fetch max file size from `server/jsconfig.js`

+ 314 - 0
api/src/action/metadata.rs

@@ -0,0 +1,314 @@
+// TODO: define redirect policy
+
+use failure::Error as FailureError;
+use openssl::symm::decrypt_aead;
+use reqwest::{Client, StatusCode};
+use reqwest::header::Authorization;
+use serde_json;
+
+use crypto::b64;
+use crypto::key_set::KeySet;
+use crypto::sig::signature_encoded;
+use ext::status_code::StatusCodeExt;
+use file::metadata::Metadata as MetadataData;
+use file::remote_file::RemoteFile;
+
+/// The name of the header that is used for the authentication nonce.
+const HEADER_AUTH_NONCE: &'static str = "WWW-Authenticate";
+
+/// The HTTP status code that is returned for expired files.
+const FILE_EXPIRED_STATUS: StatusCode = StatusCode::NotFound;
+
+/// An action to fetch file metadata.
+pub struct Metadata<'a> {
+    /// The remote file to fetch the metadata for.
+    file: &'a RemoteFile,
+
+    /// An optional password to decrypt a protected file.
+    password: Option<String>,
+}
+
+impl<'a> Metadata<'a> {
+    /// Construct a new metadata action.
+    pub fn new(file: &'a RemoteFile, password: Option<String>) -> Self {
+        Self {
+            file,
+            password,
+        }
+    }
+
+    /// Invoke the metadata action.
+    pub fn invoke(self, client: &Client) -> Result<MetadataResponse, Error> {
+        // Create a key set for the file
+        let mut key = KeySet::from(self.file, self.password.as_ref());
+
+        // Fetch the authentication nonce
+        let auth_nonce = self.fetch_auth_nonce(client)?;
+
+        // Fetch the metadata and the metadata nonce, return the result
+        self.fetch_metadata(&client, &mut key, auth_nonce)
+            .map_err(|err| err.into())
+    }
+
+    /// Fetch the authentication nonce for the file from the Send server.
+    fn fetch_auth_nonce(&self, client: &Client)
+        -> Result<Vec<u8>, Error>
+    {
+        // Get the download url, and parse the nonce
+        let download_url = self.file.download_url(false);
+        let response = client.get(download_url)
+            .send()
+            .map_err(|_| AuthError::NonceReq)?;
+
+        // Validate the status code
+        let status = response.status();
+        if !status.is_success() {
+            // Handle expired files
+            if status == FILE_EXPIRED_STATUS {
+                return Err(Error::Expired);
+            } else {
+                return Err(AuthError::NonceReqStatus(status, status.err_text()).into());
+            }
+        }
+
+        // Get the authentication nonce
+        b64::decode(
+            response.headers()
+                .get_raw(HEADER_AUTH_NONCE)
+                .ok_or(AuthError::NoNonceHeader)?
+                .one()
+                .ok_or(AuthError::MalformedNonce)
+                .and_then(|line| String::from_utf8(line.to_vec())
+                    .map_err(|_| AuthError::MalformedNonce)
+                )?
+                .split_terminator(" ")
+                .skip(1)
+                .next()
+                .ok_or(AuthError::MalformedNonce)?
+        ).map_err(|_| AuthError::MalformedNonce.into())
+    }
+
+    /// Create a metadata nonce, and fetch the metadata for the file from the
+    /// Send server.
+    ///
+    /// The key set, along with the authentication nonce must be given.
+    ///
+    /// The metadata, with the meta nonce is returned.
+    fn fetch_metadata(
+        &self,
+        client: &Client,
+        key: &KeySet,
+        auth_nonce: Vec<u8>,
+    ) -> Result<MetadataResponse, MetaError> {
+        // Compute the cryptographic signature for authentication
+        let sig = signature_encoded(key.auth_key().unwrap(), &auth_nonce)
+            .map_err(|_| MetaError::ComputeSignature)?;
+
+        // Build the request, fetch the encrypted metadata
+        let mut response = client.get(self.file.api_meta_url())
+            .header(Authorization(
+                format!("send-v1 {}", sig)
+            ))
+            .send()
+            .map_err(|_| MetaError::NonceReq)?;
+
+        // Validate the status code
+        let status = response.status();
+        if !status.is_success() {
+            return Err(MetaError::NonceReqStatus(status, status.err_text()));
+        }
+
+        // Get the metadata nonce
+        let nonce = b64::decode(
+            response.headers()
+                .get_raw(HEADER_AUTH_NONCE)
+                .ok_or(MetaError::NoNonceHeader)?
+                .one()
+                .ok_or(MetaError::MalformedNonce)
+                .and_then(|line| String::from_utf8(line.to_vec())
+                    .map_err(|_| MetaError::MalformedNonce)
+                )?
+                .split_terminator(" ")
+                .skip(1)
+                .next()
+                .ok_or(MetaError::MalformedNonce)?
+        ).map_err(|_| MetaError::MalformedNonce)?;
+
+        // Parse the metadata response, and decrypt it
+        Ok(MetadataResponse::from(
+            response.json::<RawMetadataResponse>()
+                .map_err(|_| MetaError::Malformed)?
+                .decrypt_metadata(&key)
+                .map_err(|_| MetaError::Decrypt)?,
+            nonce,
+        ))
+    }
+}
+
+/// The metadata response from the server, when fetching the data through
+/// the API.
+/// This response contains raw metadata, which is still encrypted.
+#[derive(Debug, Deserialize)]
+pub struct RawMetadataResponse {
+    /// The encrypted metadata.
+    #[serde(rename = "metadata")]
+    meta: String,
+}
+
+impl RawMetadataResponse {
+    /// Get and decrypt the metadata, based on the raw data in this response.
+    ///
+    /// The decrypted data is verified using an included tag.
+    /// If verification failed, an error is returned.
+    pub fn decrypt_metadata(&self, key_set: &KeySet) -> Result<MetadataData, FailureError> {
+        // Decode the metadata
+        let raw = b64::decode(&self.meta)?;
+
+        // 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
+		let meta = decrypt_aead(
+			KeySet::cipher(),
+			key_set.meta_key().unwrap(),
+			Some(key_set.iv()),
+			&[],
+			encrypted,
+			&tag,
+		)?;
+
+        // Parse the metadata, and return
+        Ok(serde_json::from_slice(&meta)?)
+    }
+}
+
+/// The decoded and decrypted metadata response, holding all the properties.
+/// This response object is returned from this action.
+pub struct MetadataResponse {
+    /// The actual metadata.
+    metadata: MetadataData,
+
+    /// The metadata nonce.
+    nonce: Vec<u8>,
+}
+
+impl<'a> MetadataResponse {
+    /// Construct a new response with the given metadata and nonce.
+    pub fn from(metadata: MetadataData, nonce: Vec<u8>) -> Self {
+        MetadataResponse {
+            metadata,
+            nonce,
+        }
+    }
+
+    /// Get the metadata.
+    pub fn metadata(&self) -> &MetadataData {
+        &self.metadata
+    }
+
+    /// Get the nonce.
+    pub fn nonce(&self) -> &Vec<u8> {
+        &self.nonce
+    }
+}
+
+#[derive(Fail, Debug)]
+pub enum Error {
+    /// A general error occurred while requesting the file data.
+    /// This may be because authentication failed, because decrypting the
+    /// file metadata didn't succeed, or due to some other reason.
+    #[fail(display = "Failed to request file data")]
+    Request(#[cause] RequestError),
+
+    /// 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,
+}
+
+impl From<AuthError> for Error {
+    fn from(err: AuthError) -> Error {
+        Error::Request(RequestError::Auth(err))
+    }
+}
+
+impl From<MetaError> for Error {
+    fn from(err: MetaError) -> Error {
+        Error::Request(RequestError::Meta(err))
+    }
+}
+
+#[derive(Fail, Debug)]
+pub enum RequestError {
+    /// Failed authenticating, in order to fetch the file data.
+    #[fail(display = "Failed to authenticate")]
+    Auth(#[cause] AuthError),
+
+    /// Failed to retrieve the file metadata.
+    #[fail(display = "Failed to retrieve file metadata")]
+    Meta(#[cause] MetaError),
+}
+
+#[derive(Fail, Debug)]
+pub enum AuthError {
+    /// Sending the request to gather the authentication encryption nonce
+    /// failed.
+    #[fail(display = "Failed to request authentication nonce")]
+    NonceReq,
+
+    /// The response for fetching the authentication encryption nonce
+    /// indicated an error and wasn't successful.
+    #[fail(display = "Bad HTTP response '{}' while requesting authentication nonce", _1)]
+    NonceReqStatus(StatusCode, String),
+
+    /// No authentication encryption nonce was included in the response
+    /// from the server, it was missing.
+    #[fail(display = "Missing authentication nonce in server response")]
+    NoNonceHeader,
+
+    /// The authentication encryption nonce from the response malformed or
+    /// empty.
+    /// Maybe the server responded with a new format that isn't supported yet
+    /// by this client.
+    #[fail(display = "Received malformed authentication nonce")]
+    MalformedNonce,
+}
+
+#[derive(Fail, Debug)]
+pub enum MetaError {
+    /// An error occurred while computing the cryptographic signature used for
+    /// decryption.
+    #[fail(display = "Failed to compute cryptographic signature")]
+    ComputeSignature,
+
+    /// Sending the request to gather the metadata encryption nonce failed.
+    #[fail(display = "Failed to request metadata nonce")]
+    NonceReq,
+
+    /// The response for fetching the metadata encryption nonce indicated an
+    /// error and wasn't successful.
+    #[fail(display = "Bad HTTP response '{}' while requesting metadata nonce", _1)]
+    NonceReqStatus(StatusCode, String),
+
+    /// No metadata encryption nonce was included in the response from the
+    /// server, it was missing.
+    #[fail(display = "Missing metadata nonce in server response")]
+    NoNonceHeader,
+
+    /// The metadata encryption nonce from the response malformed or empty.
+    /// Maybe the server responded with a new format that isn't supported yet
+    /// by this client.
+    #[fail(display = "Received malformed metadata nonce")]
+    MalformedNonce,
+
+    /// The received metadata is malformed, and couldn't be decoded or
+    /// interpreted.
+    #[fail(display = "Received malformed metadata")]
+    Malformed,
+
+    /// Failed to decrypt the received metadata.
+    #[fail(display = "Failed to decrypt received metadata")]
+    Decrypt,
+}

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

@@ -1,5 +1,6 @@
 pub mod download;
 pub mod info;
+pub mod metadata;
 pub mod params;
 pub mod password;
 pub mod upload;