mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-03 09:39:15 +02:00
Add API action to fetch remote file metadata
This commit is contained in:
parent
6ccba568dd
commit
2e4803848b
3 changed files with 316 additions and 0 deletions
1
IDEAS.md
1
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
api/src/action/metadata.rs
Normal file
314
api/src/action/metadata.rs
Normal file
|
@ -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,5 +1,6 @@
|
|||
pub mod download;
|
||||
pub mod info;
|
||||
pub mod metadata;
|
||||
pub mod params;
|
||||
pub mod password;
|
||||
pub mod upload;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue