mirror of
https://github.com/timvisee/ffsend.git
synced 2025-10-03 17:49:15 +02:00
Start implementing upload logic in an update module
This commit is contained in:
parent
a4ccf395b9
commit
fdb5a5a8ac
11 changed files with 180 additions and 161 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -185,7 +185,6 @@ dependencies = [
|
|||
"clap 2.31.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hkdf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"hyper 0.11.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"mime_guess 2.0.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"openssl 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -195,7 +194,6 @@ dependencies = [
|
|||
"serde_json 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"sha2 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"version-compare 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -988,11 +986,6 @@ name = "vec_map"
|
|||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.1.3"
|
||||
|
@ -1155,7 +1148,6 @@ dependencies = [
|
|||
"checksum uuid 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
|
||||
"checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b"
|
||||
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
|
||||
"checksum version-compare 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "78068add8bf1e4d37d13fa5867182fe4c03f8e525c831053733f83aaba942d37"
|
||||
"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d"
|
||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
"checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3"
|
||||
|
|
|
@ -9,7 +9,6 @@ chrono = "0.4"
|
|||
clap = "2.31"
|
||||
hkdf = "0.3"
|
||||
hyper = "0.11.9" # same as reqwest
|
||||
lazy_static = "1.0"
|
||||
mime_guess = "2.0.0-alpha.2"
|
||||
open = "1"
|
||||
openssl = "0.10"
|
||||
|
@ -19,4 +18,3 @@ serde_derive = "1.0"
|
|||
serde_json = "1.0"
|
||||
sha2 = "0.7"
|
||||
url = "1.7"
|
||||
version-compare = "0.0"
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
use super::super::url::Url;
|
||||
|
||||
use super::super::send::file::File;
|
||||
|
||||
/// The response from the server after a file has been uploaded.
|
||||
/// This response contains the file ID and owner key, to manage the file.
|
||||
///
|
||||
/// It also contains the download URL, although an additional secret is
|
||||
/// required.
|
||||
///
|
||||
/// The download URL can be generated using `download_url()` which will
|
||||
/// include the required secret in the URL.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UploadResponse {
|
||||
/// The file ID.
|
||||
id: String,
|
||||
|
||||
/// The URL the file is reachable at.
|
||||
/// This includes the file ID, but does not include the secret.
|
||||
url: String,
|
||||
|
||||
/// The owner key, used to do further file modifications.
|
||||
owner: String,
|
||||
}
|
||||
|
||||
impl UploadResponse {
|
||||
/// Convert this response into a file object.
|
||||
///
|
||||
/// The `host` and `secret` must be given.
|
||||
pub fn into_file(self, host: Url, secret: Vec<u8>) -> File {
|
||||
File::new_now(
|
||||
self.id,
|
||||
host,
|
||||
self.url,
|
||||
secret,
|
||||
self.owner,
|
||||
)
|
||||
}
|
||||
}
|
10
src/b64.rs
10
src/b64.rs
|
@ -6,14 +6,14 @@
|
|||
|
||||
extern crate base64;
|
||||
|
||||
use self::base64::DecodeError;
|
||||
// use self::base64::DecodeError;
|
||||
|
||||
/// Encode the given byte slice using base64, in an URL-safe manner.
|
||||
pub fn encode(input: &[u8]) -> String {
|
||||
base64::encode_config(input, base64::URL_SAFE_NO_PAD)
|
||||
}
|
||||
|
||||
/// Decode the given string as base64, in an URL-safe manner.
|
||||
pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
|
||||
base64::decode_config(input, base64::URL_SAFE_NO_PAD)
|
||||
}
|
||||
// /// Decode the given string as base64, in an URL-safe manner.
|
||||
// pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
|
||||
// base64::decode_config(input, base64::URL_SAFE_NO_PAD)
|
||||
// }
|
||||
|
|
108
src/main.rs
108
src/main.rs
|
@ -1,5 +1,4 @@
|
|||
extern crate hyper;
|
||||
extern crate lazy_static;
|
||||
extern crate mime_guess;
|
||||
extern crate open;
|
||||
extern crate openssl;
|
||||
|
@ -13,26 +12,11 @@ mod app;
|
|||
mod b64;
|
||||
mod cmd;
|
||||
mod crypto;
|
||||
mod metadata;
|
||||
mod reader;
|
||||
mod send;
|
||||
mod util;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
use openssl::symm::{Cipher, encrypt_aead};
|
||||
use reqwest::header::Authorization;
|
||||
use reqwest::mime::APPLICATION_OCTET_STREAM;
|
||||
use reqwest::multipart::Part;
|
||||
|
||||
use action::upload::UploadResponse;
|
||||
use cmd::Handler;
|
||||
use cmd::cmd_upload::CmdUpload;
|
||||
use metadata::{Metadata, XFileMetadata};
|
||||
use reader::EncryptedFileReaderTagged;
|
||||
use send::key_set::KeySet;
|
||||
|
||||
/// Application entrypoint.
|
||||
fn main() {
|
||||
|
@ -61,92 +45,10 @@ fn invoke_action(handler: &Handler) {
|
|||
|
||||
/// The upload action.
|
||||
fn action_upload(cmd_upload: &CmdUpload) {
|
||||
// Get the path and host
|
||||
let path = Path::new(cmd_upload.file());
|
||||
let host = cmd_upload.host();
|
||||
// // Get the path and host
|
||||
// let path = Path::new(cmd_upload.file());
|
||||
// let host = cmd_upload.host();
|
||||
|
||||
// Make sure the path is a file
|
||||
if !path.is_file() {
|
||||
panic!("The selected path is not a file");
|
||||
// // Open the URL in the browser
|
||||
// open::that(url).expect("failed to open URL");
|
||||
}
|
||||
|
||||
// TODO: a fixed path for now, as upload test
|
||||
let file_ext = path.extension().unwrap().to_str().unwrap();
|
||||
let file_name = path.file_name().unwrap().to_str().unwrap().to_owned();
|
||||
|
||||
// Create a new reqwest client
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Generate a key
|
||||
let key = KeySet::generate(true);
|
||||
|
||||
// Guess the mimetype of the file
|
||||
let file_mime = mime_guess::get_mime_type(file_ext);
|
||||
|
||||
// Construct the metadata
|
||||
let metadata = Metadata::from(key.iv(), file_name.clone(), file_mime);
|
||||
|
||||
// Convert the metadata to JSON bytes
|
||||
let metadata = metadata.to_json().into_bytes();
|
||||
|
||||
// Choose a file and meta cipher type
|
||||
let cipher = Cipher::aes_128_gcm();
|
||||
|
||||
// Encrypt the metadata, and append the tag to it
|
||||
let mut metadata_tag = vec![0u8; 16];
|
||||
let mut metadata = encrypt_aead(
|
||||
cipher,
|
||||
key.meta_key().unwrap(),
|
||||
Some(&[0u8; 12]),
|
||||
&[],
|
||||
&metadata,
|
||||
&mut metadata_tag,
|
||||
).unwrap();
|
||||
metadata.append(&mut metadata_tag);
|
||||
|
||||
// Open the file and create an encrypted file reader
|
||||
let file = File::open(path).unwrap();
|
||||
let reader = EncryptedFileReaderTagged::new(
|
||||
file,
|
||||
cipher,
|
||||
key.file_key().unwrap(),
|
||||
key.iv(),
|
||||
).unwrap();
|
||||
|
||||
// Buffer the encrypted reader, and determine the length
|
||||
let reader_len = reader.len().unwrap();
|
||||
let reader = BufReader::new(reader);
|
||||
|
||||
// Build the file part, configure the form to send
|
||||
let part = Part::reader_with_length(reader, reader_len)
|
||||
.file_name(file_name)
|
||||
.mime(APPLICATION_OCTET_STREAM);
|
||||
let form = reqwest::multipart::Form::new()
|
||||
.part("data", part);
|
||||
|
||||
// Make the request
|
||||
// TODO: properly format an URL here
|
||||
let url = host.join("api/upload").expect("invalid host");
|
||||
let mut res = client.post(url.as_str())
|
||||
.header(Authorization(format!("send-v1 {}", key.auth_key_encoded().unwrap())))
|
||||
.header(XFileMetadata::from(&metadata))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
// Parse the response
|
||||
let upload_res: UploadResponse = res.json().unwrap();
|
||||
|
||||
// Print the response
|
||||
let file = upload_res.into_file(host, key.secret().to_vec());
|
||||
let url = file.download_url();
|
||||
println!("File: {:#?}", file);
|
||||
println!("Secret key: {}", key.secret_encoded());
|
||||
println!("Download URL: {}", url);
|
||||
|
||||
// Open the URL in the browser
|
||||
open::that(url).expect("failed to open URL");
|
||||
}
|
||||
|
||||
// TODO: implement this some other way
|
||||
unsafe impl Send for EncryptedFileReaderTagged {}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use super::super::openssl::symm::Cipher;
|
||||
|
||||
use b64;
|
||||
use crypto::{derive_auth_key, derive_file_key, derive_meta_key, rand_bytes};
|
||||
|
||||
|
@ -98,4 +100,9 @@ impl KeySet {
|
|||
pub fn meta_key(&self) -> Option<&Vec<u8>> {
|
||||
self.meta_key.as_ref()
|
||||
}
|
||||
|
||||
/// Get the cipher type to use in combination with these keys.
|
||||
pub fn cipher() -> Cipher {
|
||||
Cipher::aes_128_gcm()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,8 @@
|
|||
//! This API client may be used to upload, download, modify or delete files
|
||||
//! to and from a secure Firefox Send server.
|
||||
|
||||
pub mod file;
|
||||
pub mod key_set;
|
||||
pub mod metadata;
|
||||
pub mod reader;
|
||||
pub mod send_file;
|
||||
pub mod upload;
|
||||
|
|
|
@ -215,3 +215,6 @@ impl Read for EncryptedFileReaderTagged {
|
|||
Ok(self.read(buf)? + total)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement this some other way
|
||||
unsafe impl Send for EncryptedFileReaderTagged {}
|
|
@ -10,7 +10,7 @@ use super::super::b64;
|
|||
/// The struct contains the file ID, the file URL, the key that is required
|
||||
/// in combination with the file, and the owner key.
|
||||
#[derive(Debug)]
|
||||
pub struct File {
|
||||
pub struct SendFile {
|
||||
/// The ID of the file on that server.
|
||||
id: String,
|
||||
|
||||
|
@ -30,7 +30,7 @@ pub struct File {
|
|||
owner_key: String,
|
||||
}
|
||||
|
||||
impl File {
|
||||
impl SendFile {
|
||||
/// Construct a new file.
|
||||
pub fn new(
|
||||
id: String,
|
||||
|
@ -40,7 +40,7 @@ impl File {
|
|||
secret: Vec<u8>,
|
||||
owner_key: String,
|
||||
) -> Self {
|
||||
File {
|
||||
Self {
|
||||
id,
|
||||
time,
|
||||
host,
|
153
src/send/upload.rs
Normal file
153
src/send/upload.rs
Normal file
|
@ -0,0 +1,153 @@
|
|||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::path::Path;
|
||||
|
||||
use super::super::mime_guess::get_mime_type;
|
||||
use super::super::openssl::symm::encrypt_aead;
|
||||
use super::super::reqwest;
|
||||
use super::super::reqwest::header::Authorization;
|
||||
use super::super::reqwest::mime::APPLICATION_OCTET_STREAM;
|
||||
use super::super::reqwest::multipart::Part;
|
||||
use super::super::url::Url;
|
||||
|
||||
use super::key_set::KeySet;
|
||||
use super::metadata::{Metadata, XFileMetadata};
|
||||
use super::reader::EncryptedFileReaderTagged;
|
||||
use super::send_file::SendFile;
|
||||
|
||||
pub type Result<T> = ::std::result::Result<T, UploadError>;
|
||||
|
||||
/// A file upload action to a Send server.
|
||||
pub struct Upload {
|
||||
/// The Send host to upload the file to.
|
||||
host: Url,
|
||||
|
||||
/// The file to upload.
|
||||
path: Box<Path>,
|
||||
}
|
||||
|
||||
impl Upload {
|
||||
/// Construct a new upload action.
|
||||
pub fn new(host: Url, path: Box<Path>) -> Self {
|
||||
Self {
|
||||
host,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Invoke the upload action.
|
||||
pub fn invoke(self) -> Result<SendFile> {
|
||||
// Make sure the given path is a file
|
||||
if !self.path.is_file() {
|
||||
return Err(UploadError::NotAFile);
|
||||
}
|
||||
|
||||
// Grab some file details
|
||||
let file_ext = self.path.extension().unwrap().to_str().unwrap();
|
||||
let file_name = self.path.file_name().unwrap().to_str().unwrap().to_owned();
|
||||
let file_mime = get_mime_type(file_ext);
|
||||
|
||||
// Generate a key set
|
||||
let key = KeySet::generate(true);
|
||||
|
||||
// Construct the metadata
|
||||
let metadata = Metadata::from(key.iv(), file_name.clone(), file_mime)
|
||||
.to_json()
|
||||
.into_bytes();
|
||||
|
||||
// Encrypt the metadata, and append the tag to it
|
||||
let mut metadata_tag = vec![0u8; 16];
|
||||
let mut metadata = encrypt_aead(
|
||||
KeySet::cipher(),
|
||||
key.meta_key().unwrap(),
|
||||
Some(&[0u8; 12]),
|
||||
&[],
|
||||
&metadata,
|
||||
&mut metadata_tag,
|
||||
).unwrap();
|
||||
metadata.append(&mut metadata_tag);
|
||||
|
||||
// Open the file and create an encrypted file reader
|
||||
let file = File::open(&self.path).unwrap();
|
||||
let reader = EncryptedFileReaderTagged::new(
|
||||
file,
|
||||
KeySet::cipher(),
|
||||
key.file_key().unwrap(),
|
||||
key.iv(),
|
||||
).unwrap();
|
||||
|
||||
// Buffer the encrypted reader, and determine the length
|
||||
let reader_len = reader.len().unwrap();
|
||||
let reader = BufReader::new(reader);
|
||||
|
||||
// Build the file part, configure the form to send
|
||||
let part = Part::reader_with_length(reader, reader_len)
|
||||
.file_name(file_name)
|
||||
.mime(APPLICATION_OCTET_STREAM);
|
||||
let form = reqwest::multipart::Form::new()
|
||||
.part("data", part);
|
||||
|
||||
// Create a new reqwest client
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Make the request
|
||||
// TODO: properly format an URL here
|
||||
let url = self.host.join("api/upload").expect("invalid host");
|
||||
let mut res = client.post(url.as_str())
|
||||
.header(Authorization(format!("send-v1 {}", key.auth_key_encoded().unwrap())))
|
||||
.header(XFileMetadata::from(&metadata))
|
||||
.multipart(form)
|
||||
.send()
|
||||
.unwrap();
|
||||
|
||||
// Parse the response
|
||||
let upload_res: UploadResponse = res.json().unwrap();
|
||||
|
||||
// Print the response
|
||||
Ok(
|
||||
upload_res.into_file(self.host, key.secret().to_vec())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UploadError {
|
||||
/// The given file is not not an existing file.
|
||||
/// Maybe it is a directory, or maybe it doesn't exist.
|
||||
NotAFile,
|
||||
}
|
||||
|
||||
/// The response from the server after a file has been uploaded.
|
||||
/// This response contains the file ID and owner key, to manage the file.
|
||||
///
|
||||
/// It also contains the download URL, although an additional secret is
|
||||
/// required.
|
||||
///
|
||||
/// The download URL can be generated using `download_url()` which will
|
||||
/// include the required secret in the URL.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UploadResponse {
|
||||
/// The file ID.
|
||||
id: String,
|
||||
|
||||
/// The URL the file is reachable at.
|
||||
/// This includes the file ID, but does not include the secret.
|
||||
url: String,
|
||||
|
||||
/// The owner key, used to do further file modifications.
|
||||
owner: String,
|
||||
}
|
||||
|
||||
impl UploadResponse {
|
||||
/// Convert this response into a file object.
|
||||
///
|
||||
/// The `host` and `secret` must be given.
|
||||
pub fn into_file(self, host: Url, secret: Vec<u8>) -> SendFile {
|
||||
SendFile::new_now(
|
||||
self.id,
|
||||
host,
|
||||
self.url,
|
||||
secret,
|
||||
self.owner,
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue