mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-04 18:09:17 +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?
|
- Rename host to server?
|
||||||
- Read and write files from and to stdin and stdout with `-` as file
|
- 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
|
- 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 download;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
|
pub mod metadata;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
pub mod password;
|
pub mod password;
|
||||||
pub mod upload;
|
pub mod upload;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue