Extract ffsend-api to separate repository, move client crate to root

This commit is contained in:
timvisee 2018-05-21 22:05:18 +02:00
parent e2b9b5c55c
commit 8ed530a83a
No known key found for this signature in database
GPG key ID: 109CBA0BF74036C2
83 changed files with 132 additions and 3984 deletions

View file

@ -20,8 +20,8 @@ script:
- cargo build --verbose --all
# Other feature combinations
- cargo build --package ffsend --no-default-features --verbose --all
- cargo build --package ffsend --features no-color --verbose --all
- cargo build --no-default-features --verbose --all
- cargo build --features no-color --verbose --all
# Tests
- cargo test --verbose --all

157
Cargo.lock generated
View file

@ -37,7 +37,7 @@ name = "atty"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -49,7 +49,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-demangle 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -60,7 +60,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -137,7 +137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-integer 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -151,7 +151,7 @@ dependencies = [
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -194,7 +194,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -202,7 +202,7 @@ name = "core-foundation-sys"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -365,8 +365,8 @@ dependencies = [
"pbr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"prettytable-rs 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
"rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"tar 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
"toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
@ -386,11 +386,11 @@ dependencies = [
"hkdf 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.11.27 (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)",
"openssl 0.10.7 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.10.8 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
"sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
@ -404,8 +404,8 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -426,7 +426,7 @@ name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -545,7 +545,7 @@ name = "iovec"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -585,7 +585,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.40"
version = "0.2.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -619,7 +619,7 @@ name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -632,7 +632,7 @@ name = "memchr"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -640,7 +640,7 @@ name = "memchr"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -677,7 +677,7 @@ dependencies = [
"iovec 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"miow 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"net2 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)",
@ -702,7 +702,7 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)",
"schannel 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
@ -716,7 +716,7 @@ version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -743,7 +743,7 @@ name = "num_cpus"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -785,29 +785,30 @@ dependencies = [
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
"foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.30 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl"
version = "0.10.7"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.30 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl-sys"
version = "0.9.30"
version = "0.9.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cc 1.0.15 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
"vcpkg 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -818,7 +819,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
@ -879,12 +880,12 @@ dependencies = [
"encode_unicode 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"term 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "proc-macro2"
version = "0.3.8"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -897,10 +898,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "quote"
version = "0.5.2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -909,7 +910,7 @@ version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -919,13 +920,13 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.37"
version = "0.1.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -933,7 +934,7 @@ name = "redox_termios"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -986,7 +987,7 @@ dependencies = [
"log 0.4.1 (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)",
"native-tls 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_urlencoded 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
@ -1002,7 +1003,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1047,7 +1048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"core-foundation 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1057,22 +1058,22 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"core-foundation-sys 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde"
version = "1.0.55"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde_derive"
version = "1.0.55"
version = "1.0.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.13.10 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1082,7 +1083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1092,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1144,11 +1145,11 @@ dependencies = [
[[package]]
name = "syn"
version = "0.13.10"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1180,8 +1181,8 @@ version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"filetime 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
"xattr 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1199,9 +1200,9 @@ name = "tempfile"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
"remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1220,8 +1221,8 @@ name = "termion"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1230,7 +1231,7 @@ name = "textwrap"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1247,8 +1248,8 @@ name = "time"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1417,7 +1418,7 @@ name = "toml"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1466,7 +1467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-width"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
@ -1502,7 +1503,7 @@ name = "url_serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1606,7 +1607,7 @@ name = "xattr"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -1614,7 +1615,7 @@ name = "xcb"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
@ -1685,7 +1686,7 @@ dependencies = [
"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73"
"checksum lazy_static 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d"
"checksum lazycell 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a6f08839bc70ef4a3fe1d566d5350f519c5912ea86be0df1740a7d247c7fc0ef"
"checksum libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "6fd41f331ac7c5b8ac259b8bf82c75c0fb2e469bbf37d2becbba9a6a2221965b"
"checksum libc 0.2.41 (registry+https://github.com/rust-lang/crates.io-index)" = "ac8ebf8343a981e2fa97042b14768f02ed3e1d602eac06cae6166df3c8ced206"
"checksum libflate 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "1a429b86418868c7ea91ee50e9170683f47fd9d94f5375438ec86ec3adb74e8e"
"checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b"
"checksum log 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "89f010e843f2b1a31dbd316b3b8d443758bc634bed37aabade59c686d644e0a2"
@ -1708,9 +1709,9 @@ dependencies = [
"checksum objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
"checksum objc_id 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e4730aa1c64d722db45f7ccc4113a3e2c465d018de6db4d3e7dfe031e8c8a297"
"checksum open 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c281318d992e4432cfa799969467003d05921582a7489a8325e37f8a450d5113"
"checksum openssl 0.10.7 (registry+https://github.com/rust-lang/crates.io-index)" = "63c6ff2c7d9903daf9f3429eb2f6beedb15b1f7362e3529e5bf00b6caf182400"
"checksum openssl 0.10.8 (registry+https://github.com/rust-lang/crates.io-index)" = "736898acffb0e00a14d86c5b836aee2ca1c502efcf1c1b0d17a936dfc49ec47f"
"checksum openssl 0.9.24 (registry+https://github.com/rust-lang/crates.io-index)" = "a3605c298474a3aa69de92d21139fb5e2a81688d308262359d85cdd0d12a7985"
"checksum openssl-sys 0.9.30 (registry+https://github.com/rust-lang/crates.io-index)" = "73ae718c3562989cd3a0a5c26610feca02f8116822f6f195e6cf4887481e57f5"
"checksum openssl-sys 0.9.31 (registry+https://github.com/rust-lang/crates.io-index)" = "a4d6a27d108b29befe1822d40e2e22f85518dac59acbf7f30fdc532f48fd0a77"
"checksum pbr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "deb73390ab68d81992bd994d145f697451bb0b54fd39738e72eef32458ad6907"
"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
"checksum phf 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "7d37a244c75a9748e049225155f56dbcb98fe71b192fd25fd23cb914b5ad62f2"
@ -1719,12 +1720,12 @@ dependencies = [
"checksum phf_shared 0.7.22 (registry+https://github.com/rust-lang/crates.io-index)" = "c2261d544c2bb6aa3b10022b0be371b9c7c64f762ef28c6f5d4f1ef6d97b5930"
"checksum pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "110d5ee3593dbb73f56294327fe5668bcc997897097cbc76b51e7aed3f52452f"
"checksum prettytable-rs 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "34dc1f4f6dddab3bf008ecfd4fd2a631b585fbf0af123f34c1324f51a034ff5f"
"checksum proc-macro2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "1b06e2f335f48d24442b35a19df506a835fb3547bc3c06ef27340da9acf5cae7"
"checksum proc-macro2 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a45f2f0ae0b5757f6fe9e68745ba25f5246aea3598984ed81d013865873c1f84"
"checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a"
"checksum quote 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9949cfe66888ffe1d53e6ec9d9f3b70714083854be20fd5e271b232a017401e8"
"checksum quote 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e53eeda07ddbd8b057dde66d9beded11d0dfda13f0db0769e6b71d6bcf2074e"
"checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1"
"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5"
"checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd"
"checksum redox_syscall 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "0a12d51a5b5fd700e6c757f15877685bfa04fd7eb60c108f01d045cafa0073c2"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384"
"checksum regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7"
@ -1740,8 +1741,8 @@ dependencies = [
"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
"checksum security-framework 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "dfa44ee9c54ce5eecc9de7d5acbad112ee58755239381f687e564004ba4a2332"
"checksum security-framework-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5421621e836278a0b139268f36eee0dc7e389b784dc3f79d8f11aabadf41bead"
"checksum serde 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)" = "97f6a6c3caba0cf8f883b53331791036404ce3c1bd895961cf8bb2f8cecfd84b"
"checksum serde_derive 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)" = "f51b0ef935cf8a41a77bce553da1f8751a739b7ad82dd73669475a22e6ecedb0"
"checksum serde 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "2a4d976362a13caad61c38cf841401d2d4d480496a9391c3842c288b01f9de95"
"checksum serde_derive 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "94bb618afe46430c6b089e9b111dc5b2fcd3e26a268da0993f6d16bea51c6021"
"checksum serde_json 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "f3ad6d546e765177cf3dded3c2e424a8040f870083a0e64064746b958ece9cb1"
"checksum serde_urlencoded 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e703cef904312097cfceab9ce131ff6bbe09e8c964a0703345a5f49238757bc1"
"checksum sha2 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9eb6be24e4c23a84d7184280d2722f7f2731fcdd4a9d886efbfe4413e4847ea0"
@ -1751,7 +1752,7 @@ dependencies = [
"checksum smallvec 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4c8cbcd6df1e117c2210e13ab5109635ad68a929fcbb8964dc965b76cb5ee013"
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
"checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad"
"checksum syn 0.13.10 (registry+https://github.com/rust-lang/crates.io-index)" = "77961dcdac942fa8bc033c16f3a790b311c8a27d00811b878ebd8cf9b7ba39d5"
"checksum syn 0.14.0 (registry+https://github.com/rust-lang/crates.io-index)" = "99d991a9e7c33123925e511baab68f7ec25c3795962fe326a2395e5a42a614f0"
"checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6"
"checksum synstructure 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3a761d12e6d8dcb4dcf952a7a89b475e3a9d69e4a69307e01a470977642914bd"
"checksum take 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5"
@ -1784,7 +1785,7 @@ dependencies = [
"checksum unicase 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "284b6d3db520d67fbe88fd778c21510d1b0ba4a551e5d0fbb023d33405f6de8a"
"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
"checksum unicode-normalization 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "6a0180bc61fc5a987082bfa111f4cc95c4caff7f9799f3e46df09163a937aa25"
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
"checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc"
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"

View file

@ -1,5 +1,49 @@
[workspace]
members = [
"api",
"cli",
]
[package]
name = "ffsend"
description = """\
Easily and securely share files from the command line.\n\
A fully featured Firefox Send client.\
"""
version = "0.0.1"
authors = ["Tim Visee <https://timvisee.com/>"]
[[bin]]
path = "src/main.rs"
name = "ffsend"
[features]
default = ["archive", "clipboard", "history"]
# Compile with file archiving support
archive = ["tar"]
# Compile with file history support
history = []
# Compile without colored output support
no-color = ["colored/no-color"]
[dependencies]
chrono = "0.4"
clap = "2.31"
colored = "1.6"
derive_builder = "0.5"
directories = "0.10"
failure = "0.1"
# ffsend-api = { version = "*", path = "../ffsend-api" }
ffsend-api = "0.0"
fs2 = "0.4"
lazy_static = "1.0"
open = "1"
pbr = "1"
prettytable-rs = "0.6"
rpassword = "2.0"
serde = "1.0"
serde_derive = "1.0"
tar = { version = "0.4", optional = true }
tempfile = "3"
toml = "0.4"
version-compare = "0.0.6"
[target.'cfg(not(target_os = "linux"))'.dependencies]
clipboard = { version = "0.4", optional = true }

View file

@ -173,7 +173,7 @@ Then, walk through one of the following steps to compile and install `ffsend`:
```bash
# Clone the project
git clone https://github.com/timvisee/ffsend.git
cd ffsend/cli
cd ffsend
# Compile and install
cargo install -f
@ -342,13 +342,9 @@ This application is not affiliated with Mozilla, Firefox or Firefox Send.
```
## License
The tool `ffsend` itself is released under the GNU GPL-3.0 license.
This project is released under the GNU GPL-3.0 license.
Check out the [LICENSE](LICENSE) file for more information.
The `ffsend-api` library that is part of this repository located [here](api),
is intended for use in other projects and is is released under the MIT license.
Check out the [LICENSE](api/LICENSE) file for more information.
[usage-demo-asciinema]: https://asciinema.org/a/182225
[usage-demo-gif]: ./res/ffsend-demo.gif
[usage-demo-mp4]: ./res/ffsend-demo.mp4?raw=true

View file

@ -1,27 +0,0 @@
[package]
name = "ffsend-api"
description = "A fully featured Firefox Send API client."
version = "0.0.1"
authors = ["Tim Visee <https://timvisee.com/>"]
workspace = ".."
[dependencies]
arrayref = "0.3"
base64 = "0.9"
chrono = {version = "0.4", features = ["serde"]}
derive_builder = "0.5"
failure = "0.1"
failure_derive = "0.1"
hkdf = "0.3"
hyper = "0.11.9" # same as reqwest
mime_guess = "2.0.0-alpha.2"
openssl = "0.10"
regex = "0.2"
reqwest = "0.8"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
sha2 = "0.7"
time = "0.1"
url = "1.7"
url_serde = "0.2"

View file

@ -1,20 +0,0 @@
Copyright (c) 2017 Tim Visée
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,166 +0,0 @@
use reqwest::Client;
use api::data::{
Error as DataError,
OwnedData,
};
use api::nonce::{NonceError, request_nonce};
use api::request::{ensure_success, ResponseError};
use api::url::UrlBuilder;
use file::remote_file::RemoteFile;
/// An action to delete a remote file.
pub struct Delete<'a> {
/// The remote file to delete.
file: &'a RemoteFile,
/// The authentication nonce.
/// May be an empty vector if the nonce is unknown.
nonce: Vec<u8>,
}
impl<'a> Delete<'a> {
/// Construct a new delete action for the given file.
pub fn new(file: &'a RemoteFile, nonce: Option<Vec<u8>>) -> Self {
Self {
file,
nonce: nonce.unwrap_or_default(),
}
}
/// Invoke the delete action.
pub fn invoke(mut self, client: &Client) -> Result<(), Error> {
// Fetch the authentication nonce if not set yet
if self.nonce.is_empty() {
self.nonce = self.fetch_auth_nonce(client)?;
}
// Create owned data, to send to the server for authentication
let data = OwnedData::from(DeleteData::new(), &self.file)
.map_err(|err| PrepareError::DeleteData(
DeleteDataError::Owned(err),
))?;
// Send the delete request
self.request_delete(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 a request to delete the remote file, with the given data.
fn request_delete(
&self,
client: &Client,
data: &OwnedData<DeleteData>,
) -> Result<(), Error> {
// Get the delete URL, and send the request
let url = UrlBuilder::api_delete(self.file);
let response = client.post(url)
.json(&data)
.send()
.map_err(|_| DeleteError::Request)?;
// Ensure the status code is succesful
ensure_success(&response)
.map_err(|err| err.into())
}
}
/// The delete data object.
/// This object is currently empty, as no additional data is sent to the
/// server.
#[derive(Debug, Serialize, Default)]
pub struct DeleteData { }
impl DeleteData {
/// Constructor.
pub fn new() -> Self {
DeleteData::default()
}
}
#[derive(Fail, Debug)]
pub enum Error {
/// An error occurred while preparing the action.
#[fail(display = "failed to prepare the action")]
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 filedeletion request.
#[fail(display = "failed to send the file deletion request")]
Delete(#[cause] DeleteError),
}
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<DeleteError> for Error {
fn from(err: DeleteError) -> Error {
Error::Delete(err)
}
}
#[derive(Debug, Fail)]
pub enum DeleteDataError {
/// Some error occurred while trying to wrap the deletion 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 to authenticate
#[fail(display = "failed to authenticate")]
Auth(#[cause] NonceError),
/// An error occurred while building the deletion data that will be
/// send to the server.
#[fail(display = "invalid parameters")]
DeleteData(#[cause] DeleteDataError),
}
#[derive(Fail, Debug)]
pub enum DeleteError {
/// Sending the file deletion request failed.
#[fail(display = "failed to send file deletion request")]
Request,
/// The server responded with an error while requesting file deletion.
#[fail(display = "bad response from server while deleting file")]
Response(#[cause] ResponseError),
}
impl From<ResponseError> for Error {
fn from(err: ResponseError) -> Self {
match err {
ResponseError::Expired => Error::Expired,
err => Error::Delete(DeleteError::Response(err)),
}
}
}

View file

@ -1,344 +0,0 @@
use std::fs::File;
use std::io::{
self,
Error as IoError,
Read,
};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use reqwest::{Client, Response};
use reqwest::header::Authorization;
use reqwest::header::ContentLength;
use api::url::UrlBuilder;
use api::request::{ensure_success, ResponseError};
use crypto::key_set::KeySet;
use crypto::sig::signature_encoded;
use file::remote_file::RemoteFile;
use reader::{EncryptedFileWriter, ProgressReporter, ProgressWriter};
use super::metadata::{
Error as MetadataError,
Metadata as MetadataAction,
MetadataResponse,
};
/// A file upload action to a Send server.
pub struct Download<'a> {
/// The remote file to download.
file: &'a RemoteFile,
/// The target file or directory, to download the file to.
target: PathBuf,
/// An optional password to decrypt a protected file.
password: Option<String>,
/// Check whether the file exists (recommended).
check_exists: bool,
/// The metadata response to work with,
/// which will skip the internal metadata request.
metadata_response: Option<MetadataResponse>,
}
impl<'a> Download<'a> {
/// Construct a new download action for the given remote file.
/// It is recommended to check whether the file exists,
/// unless that is already done.
pub fn new(
file: &'a RemoteFile,
target: PathBuf,
password: Option<String>,
check_exists: bool,
metadata_response: Option<MetadataResponse>,
) -> Self {
Self {
file,
target,
password,
check_exists,
metadata_response,
}
}
/// Invoke the download action.
pub fn invoke(
mut self,
client: &Client,
reporter: &Arc<Mutex<ProgressReporter>>,
) -> Result<(), Error> {
// Create a key set for the file
let mut key = KeySet::from(self.file, self.password.as_ref());
// Get the metadata, or fetch the file metadata,
// then update the input vector in the key set
let metadata: MetadataResponse = if self.metadata_response.is_some() {
self.metadata_response.take().unwrap()
} else {
MetadataAction::new(
self.file,
self.password.clone(),
self.check_exists,
)
.invoke(&client)?
};
key.set_iv(metadata.metadata().iv());
// Decide what actual file target to use
let path = self.decide_path(metadata.metadata().name());
let path_str = path.to_str().unwrap_or("?").to_owned();
// Open the file we will write to
// TODO: this should become a temporary file first
// TODO: use the uploaded file name as default
let out = File::create(path)
.map_err(|err| Error::File(
path_str.clone(),
FileError::Create(err),
))?;
// Create the file reader for downloading
let (reader, len) = self.create_file_reader(
&key,
metadata.nonce(),
&client,
)?;
// Create the file writer
let writer = self.create_file_writer(
out,
len,
&key,
&reporter,
).map_err(|err| Error::File(path_str.clone(), err))?;
// Download the file
self.download(reader, writer, len, &reporter)?;
// TODO: return the file path
// TODO: return the new remote state (does it still exist remote)
Ok(())
}
/// Decide what path we will download the file to.
///
/// A target file or directory, and a file name hint must be given.
/// The name hint can be derived from the retrieved metadata on this file.
///
/// The name hint is used as file name, if a directory was given.
fn decide_path(&self, name_hint: &str) -> PathBuf {
// Return the target if it is an existing file
if self.target.is_file() {
return self.target.clone();
}
// Append the name hint if this is a directory
if self.target.is_dir() {
return self.target.join(name_hint);
}
// Return if the parent is an existing directory
if self.target.parent().map(|p| p.is_dir()).unwrap_or(false) {
return self.target.clone();
}
// TODO: are these todos below already implemented in CLI client?
// TODO: canonicalize the path when possible
// TODO: allow using `file.toml` as target without directory indication
// TODO: return a nice error here as the path may be invalid
// TODO: maybe prompt the user to create the directory
panic!("Invalid (non-existing) output path given, not yet supported");
}
/// Make a download request, and create a reader that downloads the
/// encrypted file.
///
/// The response representing the file reader is returned along with the
/// length of the reader content.
fn create_file_reader(
&self,
key: &KeySet,
meta_nonce: &[u8],
client: &Client,
) -> Result<(Response, u64), DownloadError> {
// Compute the cryptographic signature
let sig = signature_encoded(key.auth_key().unwrap(), &meta_nonce)
.map_err(|_| DownloadError::ComputeSignature)?;
// Build and send the download request
let response = client.get(UrlBuilder::api_download(self.file))
.header(Authorization(
format!("send-v1 {}", sig)
))
.send()
.map_err(|_| DownloadError::Request)?;
// Ensure the response is succesful
ensure_success(&response)
.map_err(DownloadError::Response)?;
// Get the content length
// TODO: make sure there is enough disk space
let len = response.headers().get::<ContentLength>()
.ok_or(DownloadError::NoLength)?.0;
Ok((response, len))
}
/// Create a file writer.
///
/// This writer will will decrypt the input on the fly, and writes the
/// decrypted data to the given file.
fn create_file_writer(
&self,
file: File,
len: u64,
key: &KeySet,
reporter: &Arc<Mutex<ProgressReporter>>,
) -> Result<ProgressWriter<EncryptedFileWriter>, FileError> {
// Build an encrypted writer
let mut writer = ProgressWriter::new(
EncryptedFileWriter::new(
file,
len as usize,
KeySet::cipher(),
key.file_key().unwrap(),
key.iv(),
).map_err(|_| FileError::EncryptedWriter)?
).map_err(|_| FileError::EncryptedWriter)?;
// Set the reporter
writer.set_reporter(reporter.clone());
Ok(writer)
}
/// Download the file from the reader, and write it to the writer.
/// The length of the file must also be given.
/// The status will be reported to the given progress reporter.
fn download<R: Read>(
&self,
mut reader: R,
mut writer: ProgressWriter<EncryptedFileWriter>,
len: u64,
reporter: &Arc<Mutex<ProgressReporter>>,
) -> Result<(), DownloadError> {
// Start the writer
reporter.lock()
.map_err(|_| DownloadError::Progress)?
.start(len);
// Write to the output file
io::copy(&mut reader, &mut writer)
.map_err(|_| DownloadError::Download)?;
// Finish
reporter.lock()
.map_err(|_| DownloadError::Progress)?
.finish();
// Verify the writer
if writer.unwrap().verified() {
Ok(())
} else {
Err(DownloadError::Verify)
}
}
}
#[derive(Fail, Debug)]
pub enum Error {
/// An error occurred while fetching the metadata of the file.
/// This step is required in order to succsessfully decrypt the
/// file that will be downloaded.
#[fail(display = "failed to fetch file metadata")]
Meta(#[cause] MetadataError),
/// 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,
/// A password is required, but was not given.
#[fail(display = "missing password, password required")]
PasswordRequired,
/// An error occurred while downloading the file.
#[fail(display = "failed to download the file")]
Download(#[cause] DownloadError),
/// An error occurred while decrypting the downloaded file.
#[fail(display = "failed to decrypt the downloaded file")]
Decrypt,
/// An error occurred while opening or writing to the target file.
// TODO: show what file this is about
#[fail(display = "couldn't use the target file at '{}'", _0)]
File(String, #[cause] FileError),
}
impl From<MetadataError> for Error {
fn from(err: MetadataError) -> Error {
match err {
MetadataError::Expired => Error::Expired,
MetadataError::PasswordRequired => Error::PasswordRequired,
err => Error::Meta(err),
}
}
}
impl From<DownloadError> for Error {
fn from(err: DownloadError) -> Error {
Error::Download(err)
}
}
#[derive(Fail, Debug)]
pub enum DownloadError {
/// An error occurred while computing the cryptographic signature used for
/// downloading the file.
#[fail(display = "failed to compute cryptographic signature")]
ComputeSignature,
/// Sending the request to download the file failed.
#[fail(display = "failed to request file download")]
Request,
/// The server responded with an error while requesting the file download.
#[fail(display = "bad response from server while requesting download")]
Response(#[cause] ResponseError),
/// The length of the file is missing, thus the length of the file to download
/// couldn't be determined.
#[fail(display = "couldn't determine file download length, missing property")]
NoLength,
/// Failed to start or update the downloading progress, because of this the
/// download can't continue.
#[fail(display = "failed to update download progress")]
Progress,
/// The actual download and decryption process the server.
/// This covers reading the file from the server, decrypting the file,
/// and writing it to the file system.
#[fail(display = "failed to download the file")]
Download,
/// Verifying the downloaded file failed.
#[fail(display = "file verification failed")]
Verify,
}
#[derive(Fail, Debug)]
pub enum FileError {
/// An error occurred while creating or opening the file to write to.
#[fail(display = "failed to create or replace the file")]
Create(#[cause] IoError),
/// Failed to create an encrypted writer for the file, which is used to
/// decrypt the downloaded file.
#[fail(display = "failed to create file decryptor")]
EncryptedWriter,
}

View file

@ -1,119 +0,0 @@
use reqwest::Client;
use api::request::{ensure_success, ResponseError};
use api::url::UrlBuilder;
use file::remote_file::RemoteFile;
/// An action to check whether a remote file exists.
/// This aciton returns an `ExistsResponse`, that defines whether the file
/// exists, and whether it is protected by a password.
pub struct Exists<'a> {
/// The remote file to check.
file: &'a RemoteFile,
}
impl<'a> Exists<'a> {
/// Construct a new exists action.
pub fn new(file: &'a RemoteFile) -> Self {
Self {
file,
}
}
/// Invoke the exists action.
pub fn invoke(self, client: &Client) -> Result<ExistsResponse, Error> {
self.check_exists(&client)
}
/// Send a request to check whether the file exists
fn check_exists(&self, client: &Client) -> Result<ExistsResponse, Error> {
// Get the download url, and parse the nonce
let exists_url = UrlBuilder::api_exists(self.file);
let mut response = client.get(exists_url)
.send()
.map_err(|_| Error::Request)?;
// Ensure the status code is succesful, check the expiry state
match ensure_success(&response) {
Ok(_) => {},
Err(ResponseError::Expired) => return Ok(
ExistsResponse::new(false, false)
),
Err(err) => return Err(Error::Response(err)),
}
// Parse the response
let mut response = response.json::<ExistsResponse>()
.map_err(|_| Error::Malformed)?;
response.set_exists(true);
// TODO: fetch the metadata nonce from the response headers
Ok(response)
}
}
/// The exists response.
#[derive(Debug, Deserialize)]
pub struct ExistsResponse {
/// Whether the file exists.
#[serde(skip)]
exists: bool,
/// Whether this file requires a password.
#[serde(rename = "password")]
has_password: bool,
}
impl ExistsResponse {
/// Construct a new response.
pub fn new(exists: bool, has_password: bool) -> Self {
ExistsResponse {
exists,
has_password,
}
}
/// Whether the remote file exists on the server.
pub fn exists(&self) -> bool {
self.exists
}
/// Set whether the remote file exists.
pub fn set_exists(&mut self, exists: bool) {
self.exists = exists;
}
/// Whether the remote file is protected by a password.
pub fn has_password(&self) -> bool {
self.has_password
}
}
impl Default for ExistsResponse {
fn default() -> Self {
ExistsResponse {
exists: false,
has_password: false,
}
}
}
#[derive(Fail, Debug)]
pub enum Error {
/// Sending the request to check whether the file exists failed.
#[fail(display = "failed to send request whether the file exists")]
Request,
/// The server responded with an error while checking whether the file
/// exists.
#[fail(display = "bad response from server while checking file existence")]
Response(#[cause] ResponseError),
/// The response from the server when checking if the file exists was
/// malformed.
/// Maybe the server responded with a new format that isn't supported yet
/// by this client.
#[fail(display = "received malformed authentication nonce")]
Malformed,
}

View file

@ -1,226 +0,0 @@
use std::cmp::max;
use reqwest::{
Client,
Error as ReqwestError,
};
use api::data::{
Error as DataError,
OwnedData,
};
use api::nonce::{NonceError, request_nonce};
use api::request::{ensure_success, ResponseError};
use api::url::UrlBuilder;
use file::remote_file::RemoteFile;
/// An action to fetch info of a shared file.
pub struct Info<'a> {
/// The remote file to fetch the info for.
file: &'a RemoteFile,
/// The authentication nonce.
/// May be an empty vector if the nonce is unknown.
nonce: Vec<u8>,
}
impl<'a> Info<'a> {
/// Construct a new info action for the given remote file.
pub fn new(file: &'a RemoteFile, nonce: Option<Vec<u8>>) -> Self {
Self {
file,
nonce: nonce.unwrap_or_default(),
}
}
/// Invoke the info action.
pub fn invoke(mut self, client: &Client) -> Result<InfoResponse, Error> {
// Fetch the authentication nonce if not set yet
if self.nonce.is_empty() {
self.nonce = self.fetch_auth_nonce(client)?;
}
// Create owned data, to send to the server for authentication
let data = OwnedData::from(InfoData::new(), &self.file)
.map_err(|err| -> PrepareError { err.into() })?;
// Send the info request
self.fetch_info(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 fetching the remote file info.
fn fetch_info(
&self,
client: &Client,
data: &OwnedData<InfoData>,
) -> Result<InfoResponse, Error> {
// Get the info URL, and send the request
let url = UrlBuilder::api_info(self.file);
let mut response = client.post(url)
.json(&data)
.send()
.map_err(|_| InfoError::Request)?;
// Ensure the response is successful
ensure_success(&response)?;
// Decode the JSON response
let response: InfoResponse = match response.json() {
Ok(response) => response,
Err(err) => return Err(InfoError::Decode(err).into()),
};
Ok(response)
}
}
/// The info data object.
/// This object is currently empty, as no additional data is sent to the
/// server.
#[derive(Debug, Serialize, Default)]
pub struct InfoData { }
impl InfoData {
/// Constructor.
pub fn new() -> Self {
InfoData::default()
}
}
/// The file info response.
#[derive(Debug, Deserialize)]
pub struct InfoResponse {
/// The download limit.
#[serde(rename = "dlimit")]
download_limit: usize,
/// The total number of times the file has been downloaded.
#[serde(rename = "dtotal")]
download_count: usize,
/// The time to live for this file in milliseconds.
#[serde(rename = "ttl")]
ttl: u64,
}
impl InfoResponse {
/// Get the number of times this file has been downloaded.
pub fn download_count(&self) -> usize {
self.download_count
}
/// Get the maximum number of times the file may be downloaded.
pub fn download_limit(&self) -> usize {
self.download_limit
}
/// Get the number of times this file may still be downloaded.
pub fn download_left(&self) -> usize {
max(self.download_limit() - self.download_count(), 0)
}
/// Get the time to live for this file, in milliseconds from the time the
/// request was made.
pub fn ttl_millis(&self) -> u64 {
self.ttl
}
}
#[derive(Fail, Debug)]
pub enum Error {
/// An error occurred while preparing the action.
#[fail(display = "failed to prepare the action")]
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 info request to the server.
#[fail(display = "failed to send the file info request")]
Info(#[cause] InfoError),
}
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<ResponseError> for Error {
fn from(err: ResponseError) -> Error {
match err {
ResponseError::Expired => Error::Expired,
err => Error::Info(InfoError::Response(err)),
}
}
}
impl From<InfoError> for Error {
fn from(err: InfoError) -> Error {
Error::Info(err)
}
}
#[derive(Debug, Fail)]
pub enum InfoDataError {
/// Some error occurred while trying to wrap the info 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 fetch the info
#[fail(display = "failed to authenticate")]
Auth(#[cause] NonceError),
/// An error occurred while building the info data that will be
/// send to the server.
#[fail(display = "invalid parameters")]
InfoData(#[cause] InfoDataError),
}
impl From<DataError> for PrepareError {
fn from(err: DataError) -> PrepareError {
PrepareError::InfoData(InfoDataError::Owned(err))
}
}
#[derive(Fail, Debug)]
pub enum InfoError {
/// Sending the request to fetch the file info failed.
#[fail(display = "failed to send file info request")]
Request,
/// The server responded with an error while fetching the file info.
#[fail(display = "bad response from server while fetching file info")]
Response(#[cause] ResponseError),
/// Failed to decode the info response from the server.
/// Maybe the server responded with data from a newer API version.
#[fail(display = "failed to decode info response")]
Decode(#[cause] ReqwestError),
}

View file

@ -1,319 +0,0 @@
use failure::Error as FailureError;
use openssl::symm::decrypt_aead;
use reqwest::Client;
use reqwest::header::Authorization;
use serde_json;
use api::nonce::{header_nonce, NonceError, request_nonce};
use api::request::{ensure_success, ResponseError};
use api::url::UrlBuilder;
use crypto::b64;
use crypto::key_set::KeySet;
use crypto::sig::signature_encoded;
use file::metadata::Metadata as MetadataData;
use file::remote_file::RemoteFile;
use super::exists::{
Error as ExistsError,
Exists as ExistsAction,
};
/// 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>,
/// Check whether the file exists (recommended).
check_exists: bool,
}
impl<'a> Metadata<'a> {
/// Construct a new metadata action.
pub fn new(
file: &'a RemoteFile,
password: Option<String>,
check_exists: bool,
) -> Self {
Self {
file,
password,
check_exists,
}
}
/// Invoke the metadata action.
pub fn invoke(self, client: &Client) -> Result<MetadataResponse, Error> {
// Make sure the given file exists
if self.check_exists {
let exist_response = ExistsAction::new(&self.file)
.invoke(&client)?;
// Return an error if the file does not exist
if !exist_response.exists() {
return Err(Error::Expired);
}
// Make sure a password is given when it is required
if self.password.is_none() && exist_response.has_password() {
return Err(Error::PasswordRequired);
}
}
// Create a key set for the file
let 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, &key, &auth_nonce)
.map_err(|err| err.into())
}
/// 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())
}
/// 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: &[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(UrlBuilder::api_metadata(self.file))
.header(Authorization(
format!("send-v1 {}", sig)
))
.send()
.map_err(|_| MetaError::NonceRequest)?;
// Ensure the status code is successful
ensure_success(&response)
.map_err(MetaError::NonceResponse)?;
// Get the metadata nonce
let nonce = header_nonce(&response)
.map_err(MetaError::Nonce)?;
// Parse the metadata response
MetadataResponse::from(
&response.json::<RawMetadataResponse>()
.map_err(|_| MetaError::Malformed)?,
&key,
nonce,
).map_err(|_| MetaError::Decrypt)
}
}
/// 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,
/// The file size in bytes.
size: u64,
}
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)?)
}
/// Get the file size in bytes.
pub fn size(&self) -> u64 {
self.size
}
}
/// 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 file size in bytes.
size: u64,
/// The metadata nonce.
nonce: Vec<u8>,
}
impl<'a> MetadataResponse {
/// Construct a new response with the given metadata and nonce.
pub fn new(metadata: MetadataData, size: u64, nonce: Vec<u8>) -> Self {
MetadataResponse {
metadata,
size,
nonce,
}
}
// Construct a new metadata response from the given raw metadata response,
// with an additional key set and nonce.
//
// This internally decrypts the metadata from the raw response.
// An error is returned if decrypting the metadata failed.
pub fn from(raw: &RawMetadataResponse, key_set: &KeySet, nonce: Vec<u8>)
-> Result<Self, FailureError>
{
Ok(
Self::new(
raw.decrypt_metadata(key_set)?,
raw.size(),
nonce,
)
)
}
/// Get the metadata.
pub fn metadata(&self) -> &MetadataData {
&self.metadata
}
/// Get the file size in bytes.
pub fn size(&self) -> u64 {
self.size
}
/// Get the nonce.
pub fn nonce(&self) -> &Vec<u8> {
&self.nonce
}
}
#[derive(Fail, Debug)]
pub enum Error {
/// An error occurred while checking whether the file exists on the
/// server.
#[fail(display = "failed to check whether the file exists")]
Exists(#[cause] ExistsError),
/// 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,
/// A password is required, but was not given.
#[fail(display = "missing password, password required")]
PasswordRequired,
}
impl From<ExistsError> for Error {
fn from(err: ExistsError) -> Error {
Error::Exists(err)
}
}
impl From<RequestError> for Error {
fn from(err: RequestError) -> Error {
Error::Request(err)
}
}
impl From<MetaError> for Error {
fn from(err: MetaError) -> Error {
Error::Request(RequestError::Meta(err))
}
}
impl From<NonceError> for Error {
fn from(err: NonceError) -> Error {
match err {
NonceError::Expired => Error::Expired,
err => Error::Request(RequestError::Auth(err)),
}
}
}
#[derive(Fail, Debug)]
pub enum RequestError {
/// Failed authenticating, in order to fetch the file data.
#[fail(display = "failed to authenticate")]
Auth(#[cause] NonceError),
/// Failed to retrieve the file metadata.
#[fail(display = "failed to retrieve file metadata")]
Meta(#[cause] MetaError),
}
#[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")]
NonceRequest,
/// The server responded with an error while fetching the metadata
/// encryption nonce.
#[fail(display = "bad response from server while fetching metadata nonce")]
NonceResponse(#[cause] ResponseError),
/// Couldn't parse the metadata encryption nonce.
#[fail(display = "failed to parse the metadata encryption nonce")]
Nonce(#[cause] NonceError),
/// 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,
}

View file

@ -1,8 +0,0 @@
pub mod delete;
pub mod download;
pub mod exists;
pub mod info;
pub mod metadata;
pub mod params;
pub mod password;
pub mod upload;

View file

@ -1,250 +0,0 @@
use reqwest::Client;
use api::data::{
Error as DataError,
OwnedData,
};
use api::nonce::{NonceError, request_nonce};
use api::request::{ensure_success, ResponseError};
use api::url::UrlBuilder;
use file::remote_file::RemoteFile;
/// The default download count.
pub const PARAMS_DEFAULT_DOWNLOAD: u8 = 1;
pub const PARAMS_DEFAULT_DOWNLOAD_STR: &str = "1";
/// The minimum allowed number of downloads, enforced by the server.
pub const PARAMS_DOWNLOAD_MIN: u8 = 1;
/// The maximum (inclusive) allowed number of downloads,
/// enforced by the server.
pub const PARAMS_DOWNLOAD_MAX: u8 = 20;
/// An action to set parameters for a shared file.
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 downloads are in-bound when using the builder
#[derive(Clone, Debug, Builder, Serialize)]
pub struct ParamsData {
/// The number of times this file may be downloaded.
/// This value must be in the `(0,20)` bounds, as enforced by Send servers.
#[serde(rename = "dlimit")]
download_limit: Option<u8>,
}
impl ParamsData {
/// Construct a new parameters object, that is empty.
pub fn new() -> Self {
ParamsData {
download_limit: None,
}
}
/// Create a new parameters data object, with the given parameters.
// TODO: the downloads must be between bounds
pub fn from(download_limit: Option<u8>) -> Self {
ParamsData {
download_limit,
}
}
/// 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>
{
// 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(())
}
/// 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()
}
}
impl Default for ParamsData {
fn default() -> ParamsData {
ParamsData {
download_limit: 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),
}

View file

@ -1,182 +0,0 @@
use reqwest::Client;
use api::data::{
Error as DataError,
OwnedData,
};
use api::nonce::{NonceError, request_nonce};
use api::request::{ensure_success, ResponseError};
use api::url::UrlBuilder;
use crypto::key_set::KeySet;
use file::remote_file::RemoteFile;
/// An action to change a password of an uploaded Send file.
pub struct Password<'a> {
/// The remote file to change the password for.
file: &'a RemoteFile,
/// The new password to use for the file.
password: &'a str,
/// The authentication nonce.
/// May be an empty vector if the nonce is unknown.
nonce: Vec<u8>,
}
impl<'a> Password<'a> {
/// Construct a new password action for the given remote file.
pub fn new(
file: &'a RemoteFile,
password: &'a str,
nonce: Option<Vec<u8>>,
) -> Self {
Self {
file,
password,
nonce: nonce.unwrap_or_default(),
}
}
/// Invoke the password action.
pub fn invoke(mut self, client: &Client) -> Result<(), Error> {
// Create a key set for the file
let mut key = KeySet::from(self.file, None);
// Fetch the authentication nonce if not set yet
if self.nonce.is_empty() {
self.nonce = self.fetch_auth_nonce(client)?;
}
// Derive a new authentication key
key.derive_auth_password(self.password, &UrlBuilder::download(self.file, true));
// Build the password data, wrap it as owned
let data = OwnedData::from(PasswordData::from(&key), &self.file)
.map_err(|err| -> PrepareError { err.into() })?;
// Send the request to change the password
self.change_password(client, &data)
}
/// Fetch the authentication nonce for the file from the Send 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 file password.
fn change_password(
&self,
client: &Client,
data: &OwnedData<PasswordData>,
) -> Result<(), Error> {
// Get the password URL, and send the change
let url = UrlBuilder::api_password(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 data object to send to the password endpoint,
/// which sets the file password.
#[derive(Debug, Serialize)]
struct PasswordData {
/// The authentication key
auth: String,
}
impl PasswordData {
/// Create the password data object from the given key set.
pub fn from(key: &KeySet) -> PasswordData {
PasswordData {
auth: key.auth_key_encoded().unwrap(),
}
}
}
#[derive(Fail, Debug)]
pub enum Error {
/// An error occurred while preparing the action.
#[fail(display = "failed to prepare setting the password")]
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 password change request to
/// the server.
#[fail(display = "failed to send the password 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(Fail, Debug)]
pub enum PrepareError {
/// Failed authenticating, needed to set a new password.
#[fail(display = "failed to authenticate")]
Auth(#[cause] NonceError),
/// Some error occurred while building the data that will be sent.
/// The owner token might possibly be missing, the wrapped error will
/// describe this further.
#[fail(display = "")]
Data(#[cause] DataError),
}
impl From<DataError> for PrepareError {
fn from(err: DataError) -> PrepareError {
PrepareError::Data(err)
}
}
#[derive(Fail, Debug)]
pub enum ChangeError {
/// Sending the request to change the password failed.
#[fail(display = "failed to send password change request")]
Request,
/// The server responded with an error while changing the file password.
#[fail(display = "bad response from server while changing password")]
Response(#[cause] ResponseError),
}

View file

@ -1,487 +0,0 @@
use std::fs::File;
use std::io::{
BufReader,
Error as IoError,
};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use mime_guess::{guess_mime_type, Mime};
use openssl::symm::encrypt_aead;
use reqwest::{
Client,
Error as ReqwestError,
Request,
};
use reqwest::header::Authorization;
use reqwest::mime::APPLICATION_OCTET_STREAM;
use reqwest::multipart::{Form, Part};
use url::{
ParseError as UrlParseError,
Url,
};
use api::nonce::header_nonce;
use api::request::{ensure_success, ResponseError};
use crypto::key_set::KeySet;
use file::remote_file::RemoteFile;
use file::metadata::{Metadata, XFileMetadata};
use reader::{
EncryptedFileReader,
ExactLengthReader,
ProgressReader,
ProgressReporter,
};
use super::params::{
Error as ParamsError,
Params,
ParamsData,
};
use super::password::{
Error as PasswordError,
Password,
};
type EncryptedReader = ProgressReader<BufReader<EncryptedFileReader>>;
/// 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: PathBuf,
/// The name of the file being uploaded.
/// This has no relation to the file path, and will become the name of the
/// shared file if set.
name: Option<String>,
/// An optional password to protect the file with.
password: Option<String>,
/// Optional file parameters to set.
params: Option<ParamsData>,
}
impl Upload {
/// Construct a new upload action.
pub fn new(
host: Url,
path: PathBuf,
name: Option<String>,
password: Option<String>,
params: Option<ParamsData>,
) -> Self {
Self {
host,
path,
name,
password,
params,
}
}
/// Invoke the upload action.
pub fn invoke(
self,
client: &Client,
reporter: &Arc<Mutex<ProgressReporter>>,
) -> Result<RemoteFile, Error> {
// Create file data, generate a key
let file = FileData::from(&self.path)?;
let key = KeySet::generate(true);
// Create metadata and a file reader
let metadata = self.create_metadata(&key, &file)?;
let reader = self.create_reader(&key, reporter.clone())?;
let reader_len = reader.len().unwrap();
// Create the request to send
let req = self.create_request(
client,
&key,
&metadata,
reader,
);
// Start the reporter
reporter.lock()
.map_err(|_| UploadError::Progress)?
.start(reader_len);
// Execute the request
let (result, nonce) = self.execute_request(req, client, &key)?;
// Mark the reporter as finished
reporter.lock()
.map_err(|_| UploadError::Progress)?
.finish();
// Change the password if set
if let Some(password) = self.password {
Password::new(&result, &password, nonce.clone()).invoke(client)?;
}
// Change parameters if set
if let Some(params) = self.params {
Params::new(&result, params, nonce.clone()).invoke(client)?;
}
Ok(result)
}
/// Create a blob of encrypted metadata.
fn create_metadata(&self, key: &KeySet, file: &FileData)
-> Result<Vec<u8>, MetaError>
{
// Determine what filename to use
let name = self.name.clone()
.unwrap_or_else(|| file.name().to_owned());
// Construct the metadata
let metadata = Metadata::from(
key.iv(),
name,
&file.mime(),
).to_json().into_bytes();
// Encrypt the metadata
let mut metadata_tag = vec![0u8; 16];
let mut metadata = match encrypt_aead(
KeySet::cipher(),
key.meta_key().unwrap(),
Some(&[0u8; 12]),
&[],
&metadata,
&mut metadata_tag,
) {
Ok(metadata) => metadata,
Err(_) => return Err(MetaError::Encrypt),
};
// Append the encryption tag
metadata.append(&mut metadata_tag);
Ok(metadata)
}
/// Create a reader that reads the file as encrypted stream.
fn create_reader(
&self,
key: &KeySet,
reporter: Arc<Mutex<ProgressReporter>>,
) -> Result<EncryptedReader, Error> {
// Open the file
let file = match File::open(self.path.as_path()) {
Ok(file) => file,
Err(err) => return Err(FileError::Open(err).into()),
};
// Create an encrypted reader
let reader = match EncryptedFileReader::new(
file,
KeySet::cipher(),
key.file_key().unwrap(),
key.iv(),
) {
Ok(reader) => reader,
Err(_) => return Err(ReaderError::Encrypt.into()),
};
// Buffer the encrypted reader
let reader = BufReader::new(reader);
// Wrap into the encrypted reader
let mut reader = ProgressReader::new(reader)
.map_err(|_| ReaderError::Progress)?;
// Initialize and attach the reporter
reader.set_reporter(reporter);
Ok(reader)
}
/// Build the request that will be send to the server.
fn create_request(
&self,
client: &Client,
key: &KeySet,
metadata: &[u8],
reader: EncryptedReader,
) -> Request {
// Get the reader length
let len = reader.len().expect("failed to get reader length");
// Configure a form to send
let part = Part::reader_with_length(reader, len)
.mime(APPLICATION_OCTET_STREAM);
let form = Form::new()
.part("data", part);
// Define the URL to call
// TODO: create an error for this unwrap
let url = self.host.join("api/upload")
.expect("invalid host");
// Build the request
// TODO: create an error for this unwrap
client.post(url.as_str())
.header(Authorization(
format!("send-v1 {}", key.auth_key_encoded().unwrap())
))
.header(XFileMetadata::from(&metadata))
.multipart(form)
.build()
.expect("failed to build an API request")
}
/// Execute the given request, and create a file object that represents the
/// uploaded file.
fn execute_request(&self, req: Request, client: &Client, key: &KeySet)
-> Result<(RemoteFile, Option<Vec<u8>>), UploadError>
{
// Execute the request
let mut response = match client.execute(req) {
Ok(response) => response,
// TODO: attach the error context
Err(_) => return Err(UploadError::Request),
};
// Ensure the response is successful
ensure_success(&response)
.map_err(UploadError::Response)?;
// Try to get the nonce, don't error on failure
let nonce = header_nonce(&response).ok();
// Decode the response
let response: UploadResponse = match response.json() {
Ok(response) => response,
Err(err) => return Err(UploadError::Decode(err)),
};
// Transform the responce into a file object
Ok((
response.into_file(self.host.clone(), &key)?,
nonce,
))
}
}
/// 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)]
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 `key` must be given.
pub fn into_file(self, host: Url, key: &KeySet)
-> Result<RemoteFile, UploadError>
{
Ok(
RemoteFile::new_now(
self.id,
host,
Url::parse(&self.url)?,
key.secret().to_vec(),
Some(self.owner),
)
)
}
}
/// A struct that holds various file properties, such as it's name and it's
/// mime type.
struct FileData<'a> {
/// The file name.
name: &'a str,
/// The file mime type.
mime: Mime,
}
impl<'a> FileData<'a> {
/// Create a file data object, from the file at the given path.
pub fn from(path: &'a PathBuf) -> Result<Self, FileError> {
// Make sure the given path is a file
if !path.is_file() {
return Err(FileError::NotAFile);
}
// Get the file name
let name = match path.file_name() {
Some(name) => name.to_str().unwrap_or("file"),
None => "file",
};
Ok(
Self {
name,
mime: guess_mime_type(path),
}
)
}
/// Get the file name.
pub fn name(&self) -> &str {
self.name
}
/// Get the file mime type.
pub fn mime(&self) -> &Mime {
&self.mime
}
}
#[derive(Fail, Debug)]
pub enum Error {
/// An error occurred while preparing a file for uploading.
#[fail(display = "failed to prepare uploading the file")]
Prepare(#[cause] PrepareError),
/// An error occurred while opening, reading or using the file that
/// the should be uploaded.
// TODO: maybe append the file path here for further information
#[fail(display = "")]
File(#[cause] FileError),
/// An error occurred while uploading the file.
#[fail(display = "failed to upload the file")]
Upload(#[cause] UploadError),
/// An error occurred while chaining file parameters.
#[fail(display = "failed to change file parameters")]
Params(#[cause] ParamsError),
/// An error occurred while setting the password.
#[fail(display = "failed to set the password")]
Password(#[cause] PasswordError),
}
impl From<MetaError> for Error {
fn from(err: MetaError) -> Error {
Error::Prepare(PrepareError::Meta(err))
}
}
impl From<FileError> for Error {
fn from(err: FileError) -> Error {
Error::File(err)
}
}
impl From<ReaderError> for Error {
fn from(err: ReaderError) -> Error {
Error::Prepare(PrepareError::Reader(err))
}
}
impl From<UploadError> for Error {
fn from(err: UploadError) -> Error {
Error::Upload(err)
}
}
impl From<ParamsError> for Error {
fn from(err: ParamsError) -> Error {
Error::Params(err)
}
}
impl From<PasswordError> for Error {
fn from(err: PasswordError) -> Error {
Error::Password(err)
}
}
#[derive(Fail, Debug)]
pub enum PrepareError {
/// Failed to prepare the file metadata for uploading.
#[fail(display = "failed to prepare file metadata")]
Meta(#[cause] MetaError),
/// Failed to create an encrypted file reader, that encrypts
/// the file on the fly when it is read.
#[fail(display = "failed to access the file to upload")]
Reader(#[cause] ReaderError),
}
#[derive(Fail, Debug)]
pub enum MetaError {
/// An error occurred while encrypting the file metadata.
#[fail(display = "failed to encrypt file metadata")]
Encrypt,
}
#[derive(Fail, Debug)]
pub enum ReaderError {
/// An error occurred while creating the file encryptor.
#[fail(display = "failed to create file encryptor")]
Encrypt,
/// Failed to create the progress reader, attached to the file reader,
/// to measure the uploading progress.
#[fail(display = "failed to create progress reader")]
Progress,
}
#[derive(Fail, Debug)]
pub enum FileError {
/// The given path, is not not a file or doesn't exist.
#[fail(display = "the given path is not an existing file")]
NotAFile,
/// Failed to open the file that must be uploaded for reading.
#[fail(display = "failed to open the file to upload")]
Open(#[cause] IoError),
}
#[derive(Fail, Debug)]
pub enum UploadError {
/// Failed to start or update the uploading progress, because of this the
/// upload can't continue.
#[fail(display = "failed to update upload progress")]
Progress,
/// Sending the request to upload the file failed.
#[fail(display = "failed to request file upload")]
Request,
/// The server responded with an error while uploading.
#[fail(display = "bad response from server while uploading")]
Response(#[cause] ResponseError),
/// Failed to decode the upload response from the server.
/// Maybe the server responded with data from a newer API version.
#[fail(display = "failed to decode upload response")]
Decode(#[cause] ReqwestError),
/// Failed to parse the retrieved URL from the upload response.
#[fail(display = "failed to parse received URL")]
ParseUrl(#[cause] UrlParseError),
}
impl From<UrlParseError> for UploadError {
fn from(err: UrlParseError) -> UploadError {
UploadError::ParseUrl(err)
}
}

View file

@ -1,50 +0,0 @@
use serde::Serialize;
use file::remote_file::RemoteFile;
/// An owned data structure, that wraps generic data.
/// This structure is used to send owned data to the Send server.
/// This owned data is authenticated using an `owner_token`,
/// which this structure manages.
#[derive(Debug, Serialize)]
pub struct OwnedData<D> {
/// The owner token, used for request authentication purposes.
owner_token: String,
/// The wrapped data structure.
#[serde(flatten)]
inner: D,
}
impl<D> OwnedData<D>
where
D: Serialize,
{
/// Constructor.
pub fn new(owner_token: String, inner: D) -> Self {
OwnedData {
owner_token,
inner,
}
}
/// Wrap the given data structure with this owned data structure.
/// A `file` must be given, having a set owner token.
pub fn from(inner: D, file: &RemoteFile) -> Result<Self, Error> {
Ok(
Self::new(
file.owner_token()
.ok_or(Error::NoOwnerToken)?
.to_owned(),
inner,
)
)
}
}
#[derive(Debug, Fail)]
pub enum Error {
/// Missing owner token, which is required.
#[fail(display = "missing owner token, must be specified")]
NoOwnerToken,
}

View file

@ -1,4 +0,0 @@
pub mod data;
pub mod nonce;
pub mod url;
pub mod request;

View file

@ -1,81 +0,0 @@
use url::Url;
use reqwest::{Client, Response};
use api::request::{ensure_success, ResponseError};
use crypto::b64;
/// The name of the header the nonce is delivered in.
const HEADER_NONCE: &str = "WWW-Authenticate";
/// Do a new request, and extract the nonce from a header in the given
/// response.
pub fn request_nonce(client: &Client, url: Url)
-> Result<Vec<u8>, NonceError>
{
// Make the request
let response = client.get(url)
.send()
.map_err(|_| NonceError::Request)?;
// Ensure the response is successful
ensure_success(&response)?;
// Extract the nonce
header_nonce(&response)
}
/// Extract the nonce from a header in the given response.
pub fn header_nonce(response: &Response)
-> Result<Vec<u8>, NonceError>
{
// Get the authentication nonce
b64::decode(
response.headers()
.get_raw(HEADER_NONCE)
.ok_or(NonceError::NoNonceHeader)?
.one()
.ok_or(NonceError::MalformedNonce)
.and_then(|line| String::from_utf8(line.to_vec())
.map_err(|_| NonceError::MalformedNonce)
)?
.split_terminator(' ')
.nth(1)
.ok_or(NonceError::MalformedNonce)?
).map_err(|_| NonceError::MalformedNonce)
}
#[derive(Fail, Debug)]
pub enum NonceError {
/// Sending the request to fetch a nonce failed,
/// as the file has expired or did never exist.
#[fail(display = "the file has expired or did never exist")]
Expired,
/// Sending the request to fetch a nonce failed.
#[fail(display = "failed to request encryption nonce")]
Request,
/// The server responded with an error while requesting the encryption nonce,
/// required for some operations.
#[fail(display = "bad response from server while requesting encryption nonce")]
Response(#[cause] ResponseError),
/// The nonce header was missing from the request.
#[fail(display = "missing nonce in server response")]
NoNonceHeader,
/// The received nonce could not be parsed, because it was malformed.
/// Maybe the server responded with a new format that isn't supported yet
/// by this client.
#[fail(display = "received malformed nonce")]
MalformedNonce,
}
impl From<ResponseError> for NonceError {
fn from(err: ResponseError) -> Self {
match err {
ResponseError::Expired => NonceError::Expired,
err => NonceError::Response(err),
}
}
}

View file

@ -1,44 +0,0 @@
use reqwest::{Response, StatusCode};
use config::{HTTP_STATUS_EXPIRED, HTTP_STATUS_UNAUTHORIZED};
use ext::status_code::StatusCodeExt;
/// Ensure the given response is successful. IF it isn
pub fn ensure_success(response: &Response) -> Result<(), ResponseError> {
// Get the status
let status = response.status();
// Stop if succesful
if status.is_success() {
return Ok(());
}
// Handle the expired file error
if status == HTTP_STATUS_EXPIRED {
return Err(ResponseError::Expired);
}
// Handle the authentication issue error
if status == HTTP_STATUS_UNAUTHORIZED {
return Err(ResponseError::Unauthorized);
}
// Return the other error
Err(ResponseError::Other(status, status.err_text()))
}
#[derive(Fail, Debug)]
pub enum ResponseError {
/// This request lead to an expired file, or a file that never existed.
#[fail(display = "this file has expired or did never exist")]
Expired,
/// We were unauthorized to make this request.
/// This is usually because of an incorrect password.
#[fail(display = "unauthorized, are the credentials correct?")]
Unauthorized,
/// Some undefined error occurred with this response.
#[fail(display = "bad HTTP response: {}", _1)]
Other(StatusCode, String),
}

View file

@ -1,72 +0,0 @@
use url::Url;
use file::remote_file::RemoteFile;
/// A struct, that helps building URLs for communicating with a remote host.
pub struct UrlBuilder;
impl UrlBuilder {
/// Get the download URL of the given file.
/// This URL is identical to the share URL, a term used in this API.
/// Set `secret` to `true`, to include it in the URL if known.
pub fn download(file: &RemoteFile, secret: bool) -> Url {
// Get the share URL, and update the secret fragment
let mut url = file.url().clone();
if secret && file.has_secret() {
url.set_fragment(Some(&file.secret()));
} else {
url.set_fragment(None);
}
url
}
/// Generate an API file URL, with the given endpoint.
/// The endpoint should not contain any slashes.
///
/// Valid endpoints may be 'metadata', 'download' or for example
/// 'password'.
fn api(endpoint: &str, file: &RemoteFile) -> Url {
// Get the share URL, and add the secret fragment
let mut url = file.url().clone();
url.set_path(format!("/api/{}/{}", endpoint, file.id()).as_str());
url.set_fragment(None);
url
}
/// Get the API metadata URL for the given file.
pub fn api_metadata(file: &RemoteFile) -> Url {
Self::api("metadata", file)
}
/// Get the API download URL for the given file.
pub fn api_download(file: &RemoteFile) -> Url {
Self::api("download", file)
}
/// Get the API password URL for the given file.
pub fn api_password(file: &RemoteFile) -> Url {
Self::api("password", file)
}
/// Get the API params URL for the given file.
pub fn api_params(file: &RemoteFile) -> Url {
Self::api("params", file)
}
/// Get the API info URL for the given file.
pub fn api_info(file: &RemoteFile) -> Url {
Self::api("info", file)
}
/// Get the API exists URL for the given file.
pub fn api_exists(file: &RemoteFile) -> Url {
Self::api("exists", file)
}
/// Get the API delete URL for the given file.
pub fn api_delete(file: &RemoteFile) -> Url {
Self::api("delete", file)
}
}

View file

@ -1,19 +0,0 @@
use reqwest::StatusCode;
/// The Send host to use by default.
pub const SEND_DEFAULT_HOST: &str = "https://send.firefox.com/";
/// The default time after which uploaded files expire after, in seconds.
pub const SEND_DEFAULT_EXPIRE_TIME: i64 = 24 * 60 * 60;
/// The HTTP status code that is returned for expired or non existant files.
pub const HTTP_STATUS_EXPIRED: StatusCode = StatusCode::NotFound;
/// The HTTP status code that is returned on authentication failure.
pub const HTTP_STATUS_UNAUTHORIZED: StatusCode = StatusCode::Unauthorized;
/// The recommended maximum upload size in bytes.
pub const UPLOAD_SIZE_MAX_RECOMMENDED: u64 = 1024 * 1024 * 1024;
/// The maximum upload size in bytes.
pub const UPLOAD_SIZE_MAX: u64 = 1024 * 1024 * 1024 * 2;

View file

@ -1,33 +0,0 @@
//! A simple module for encoding or decoding a base64 string from or to a
//! byte array.
//!
//! This module uses an URL-safe scheme, and doesn't add additional padding
//! to the encoded strings.
extern crate base64;
pub use self::base64::{
CharacterSet,
Config,
DecodeError,
LineEnding,
LineWrap,
};
/// Encode the given byte slice using base64,
/// in an URL-safe manner without padding.
pub fn encode(input: &[u8]) -> String {
base64::encode_config(input, base64::URL_SAFE_NO_PAD)
}
/// Decode the given string as base64.
/// Standard and URL-safe character sets are both supported,
/// padding is optional.
pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
base64::decode_config(
input.replace('+', "-")
.replace('/', "_")
.trim_right_matches('='),
base64::URL_SAFE_NO_PAD,
)
}

View file

@ -1,81 +0,0 @@
extern crate hkdf;
extern crate sha2;
use self::hkdf::Hkdf;
use openssl::hash::MessageDigest;
use openssl::pkcs5::pbkdf2_hmac;
use self::sha2::Sha256;
use url::Url;
/// The length of the derived authentication key in bytes.
const KEY_AUTH_SIZE: usize = 64;
/// The number of iterations to do for deriving a passworded authentication
/// key.
const KEY_AUTH_ITERATIONS: usize = 100;
/// Derive a HKDF key.
///
/// No _salt_ bytes are used in this function.
///
/// # Arguments
/// * length - Length of the derived key value that is returned.
/// * ikm - The input keying material.
/// * info - Optional context and application specific information to use.
///
/// # Returns
/// The output keying material, with the length as as specified in the `length`
/// argument.
fn hkdf(
length: usize,
ikm: &[u8],
info: Option<&[u8]>,
) -> Vec<u8> {
// Unwrap info or use empty info
let info = info.unwrap_or(&[]);
// Derive a HKDF key with the given length
Hkdf::<Sha256>::new(&ikm, &[])
.derive(&info, length)
}
/// Derive a key to use for file data encryption, based on the given `secret`.
pub fn derive_file_key(secret: &[u8]) -> Vec<u8> {
hkdf(16, secret, Some(b"encryption"))
}
/// Derive a key to use for metadata encryption, based on the given `secret`.
pub fn derive_meta_key(secret: &[u8]) -> Vec<u8> {
hkdf(16, secret, Some(b"metadata"))
}
/// Derive a key used for authentication, based on the given `secret`.
///
/// A `password` and `url` may be given for special key deriving.
/// At this time this is not implemented however.
pub fn derive_auth_key(secret: &[u8], password: Option<&str>, url: Option<&Url>) -> Vec<u8> {
// Nothing, or both a password and URL must be given
assert_eq!(
password.is_none(),
url.is_none(),
"unable to derive authentication key, missing password or URL",
);
// Derive a key without a password
if password.is_none() {
return hkdf(KEY_AUTH_SIZE, secret, Some(b"authentication"));
}
// Derive a key with a password and URL
// TODO: do not expect/unwrap here
let mut key = vec![0u8; KEY_AUTH_SIZE];
pbkdf2_hmac(
password.unwrap().as_bytes(),
url.unwrap().as_str().as_bytes(),
KEY_AUTH_ITERATIONS,
MessageDigest::sha256(),
&mut key,
).expect("failed to derive passworded authentication key");
key
}

View file

@ -1,153 +0,0 @@
use openssl::symm::Cipher;
use url::Url;
use api::url::UrlBuilder;
use file::remote_file::RemoteFile;
use super::{b64, rand_bytes};
use super::hdkf::{derive_auth_key, derive_file_key, derive_meta_key};
/// The length of an input vector.
const KEY_IV_LEN: usize = 12;
pub struct KeySet {
/// A secret.
secret: Vec<u8>,
/// Input vector.
iv: [u8; KEY_IV_LEN],
/// A derived file encryption key.
file_key: Option<Vec<u8>>,
/// A derived authentication key.
auth_key: Option<Vec<u8>>,
/// A derived metadata key.
meta_key: Option<Vec<u8>>,
}
impl KeySet {
/// Construct a new key, with the given `secret` and `iv`.
pub fn new(secret: Vec<u8>, iv: [u8; 12]) -> Self {
Self {
secret,
iv,
file_key: None,
auth_key: None,
meta_key: None,
}
}
/// Create a key set from the given file ID and secret.
/// This method may be used to create a key set based on a share URL.
// TODO: add a parameter for the password and URL
// TODO: return a result?
// TODO: supply a client instance as parameter
pub fn from(file: &RemoteFile, password: Option<&String>) -> Self {
// Create a new key set instance
let mut set = Self::new(
file.secret_raw().clone(),
[0; 12],
);
// Derive all keys
set.derive();
// Derive a pasworded key
if let Some(password) = password {
set.derive_auth_password(password, &UrlBuilder::download(&file, true));
}
set
}
/// Generate a secure new key.
///
/// If `derive` is `true`, file, authentication and metadata keys will be
/// derived from the generated secret.
pub fn generate(derive: bool) -> Self {
// Allocate two keys
let mut secret = vec![0u8; 16];
let mut iv = [0u8; 12];
// Generate the secrets
rand_bytes(&mut secret)
.expect("failed to generate crypto secure random secret");
rand_bytes(&mut iv)
.expect("failed to generate crypto secure random input vector");
// Create the key
let mut key = Self::new(secret, iv);
// Derive
if derive {
key.derive();
}
key
}
/// Derive a file, authentication and metadata key.
// TODO: add support for deriving with a password and URL
pub fn derive(&mut self) {
self.file_key = Some(derive_file_key(&self.secret));
self.auth_key = Some(derive_auth_key(&self.secret, None, None));
self.meta_key = Some(derive_meta_key(&self.secret));
}
/// Derive an authentication key, with the given password and file URL.
/// This method does not derive a (new) file and metadata key.
pub fn derive_auth_password(&mut self, pass: &str, url: &Url) {
self.auth_key = Some(derive_auth_key(
&self.secret,
Some(pass),
Some(url),
));
}
/// Get the secret key.
pub fn secret(&self) -> &[u8] {
&self.secret
}
/// Get the secret key as URL-safe base64 encoded string.
pub fn secret_encoded(&self) -> String {
b64::encode(self.secret())
}
/// Get the input vector.
pub fn iv(&self) -> &[u8] {
&self.iv
}
/// Set the input vector.
pub fn set_iv(&mut self, iv: [u8; KEY_IV_LEN]) {
self.iv = iv;
}
/// Get the file encryption key, if derived.
pub fn file_key(&self) -> Option<&Vec<u8>> {
self.file_key.as_ref()
}
/// Get the authentication encryption key, if derived.
pub fn auth_key(&self) -> Option<&Vec<u8>> {
self.auth_key.as_ref()
}
/// Get the authentication encryption key, if derived,
/// as URL-safe base64 encoded string.
pub fn auth_key_encoded(&self) -> Option<String> {
self.auth_key().map(|key| b64::encode(key))
}
/// Get the metadata encryption key, if derived.
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()
}
}

View file

@ -1,7 +0,0 @@
pub mod b64;
pub mod hdkf;
pub mod key_set;
pub mod sig;
// Reexport the cryptographically secure random bytes generator
pub use super::openssl::rand::rand_bytes;

View file

@ -1,34 +0,0 @@
use openssl::error::ErrorStack;
use openssl::hash::MessageDigest;
use openssl::pkey::PKey;
use openssl::sign::Signer;
use super::b64;
/// Compute the signature for the given data and key.
/// This is done using an HMAC key using the SHA256 digest.
///
/// If computing the signature failed, an error is returned.
pub fn signature(key: &[u8], data: &[u8]) -> Result<Vec<u8>, ErrorStack> {
// Build the key, and signer
let pkey = PKey::hmac(&key)?;
let mut signer = Signer::new(MessageDigest::sha256(), &pkey)?;
// Feed the data
signer.update(&data)?;
// Compute the signature
Ok(signer.sign_to_vec()?)
}
/// Compute the signature for the given data and key.
/// This is done using an HMAC key using the SHA256 digest.
///
/// The resulting signature is encoded as base64 string in an URL-safe manner.
///
/// If computing the signature failed, an error is returned.
pub fn signature_encoded(key: &[u8], data: &[u8])
-> Result<String, ErrorStack>
{
signature(key, data).map(|sig| b64::encode(&sig))
}

View file

@ -1 +0,0 @@
pub mod status_code;

View file

@ -1,15 +0,0 @@
use reqwest::StatusCode;
/// Reqwest status code extention, to easily retrieve an error message.
pub trait StatusCodeExt {
/// Build a basic error message based on the status code.
fn err_text(&self) -> String;
}
impl StatusCodeExt for StatusCode {
fn err_text(&self) -> String {
self.canonical_reason()
.map(|text| format!("{} {}", self.as_u16(), text))
.unwrap_or_else(|| format!("{}", self.as_u16()))
}
}

View file

@ -1,117 +0,0 @@
extern crate hyper;
use std::fmt;
use mime_guess::Mime;
use reqwest::header::{
Formatter as HeaderFormatter,
Header,
Raw,
};
use self::hyper::error::Error as HyperError;
use serde_json;
use crypto::b64;
/// The MIME type string for a tar file.
const MIME_TAR: &str = "application/x-tar";
/// File metadata, which is send to the server.
#[derive(Debug, Serialize, Deserialize)]
pub struct Metadata {
/// The input vector.
iv: String,
/// The file name.
name: String,
/// The file mimetype.
#[serde(rename="type")]
mime: String,
}
impl Metadata {
/// Construct metadata from the given properties.
///
/// Parameters:
/// * iv: initialisation vector
/// * name: file name
/// * mime: file mimetype
pub fn from(iv: &[u8], name: String, mime: &Mime) -> Self {
Metadata {
iv: b64::encode(iv),
name,
mime: mime.to_string(),
}
}
/// Convert this structure to a JSON string.
pub fn to_json(&self) -> String {
serde_json::to_string(&self).unwrap()
}
/// Get the file name.
pub fn name(&self) -> &str {
&self.name
}
/// Get the file MIME type.
pub fn mime(&self) -> &str {
&self.mime
}
/// Get the input vector
// TODO: use an input vector length from a constant
pub fn iv(&self) -> [u8; 12] {
// Decode the input vector
let decoded = b64::decode(&self.iv).unwrap();
// Create a sized array
*array_ref!(decoded, 0, 12)
}
/**
* Check whether this MIME type is recognized as supported archive type.
* `true` is returned if it's an archive, `false` if not.
*/
pub fn is_archive(&self) -> bool {
self.mime.to_lowercase() == MIME_TAR.to_lowercase()
}
}
/// A X-File-Metadata header for reqwest, that is used to pass encrypted
/// metadata to the server.
///
/// The encrypted metadata (bytes) is base64 encoded when constructing this
/// header using `from`.
#[derive(Clone)]
pub struct XFileMetadata {
/// The metadata, as a base64 encoded string.
metadata: String,
}
impl XFileMetadata {
/// Construct the header from the given encrypted metadata.
pub fn from(bytes: &[u8]) -> Self {
XFileMetadata {
metadata: b64::encode(bytes),
}
}
}
/// Make this struct usable as reqwest header.
impl Header for XFileMetadata {
fn header_name() -> &'static str {
"X-File-Metadata"
}
fn parse_header(_raw: &Raw) -> Result<Self, HyperError> {
// TODO: implement this some time
unimplemented!();
}
fn fmt_header(&self, f: &mut HeaderFormatter) -> fmt::Result {
// TODO: is this encoding base64 for us?
f.fmt_line(&self.metadata)
}
}

View file

@ -1,2 +0,0 @@
pub mod remote_file;
pub mod metadata;

View file

@ -1,339 +0,0 @@
extern crate chrono;
extern crate regex;
use api::url::UrlBuilder;
use url::{
ParseError as UrlParseError,
Url,
};
use self::chrono::{DateTime, Duration, Utc};
use self::regex::Regex;
use url_serde;
use config::SEND_DEFAULT_EXPIRE_TIME;
use crypto::b64;
/// A pattern for share URL paths, capturing the file ID.
// TODO: match any sub-path?
// TODO: match URL-safe base64 chars for the file ID?
// TODO: constrain the ID length?
const SHARE_PATH_PATTERN: &str = r"^/?download/([[:alnum:]]{8,}={0,3})/?$";
/// A pattern for share URL fragments, capturing the file secret.
// TODO: constrain the secret length?
const SHARE_FRAGMENT_PATTERN: &str = r"^([a-zA-Z0-9-_+/]+)?\s*$";
/// A struct representing an uploaded file on a Send host.
///
/// The struct contains the file ID, the file URL, the key that is required
/// in combination with the file, and the owner key.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RemoteFile {
/// The ID of the file on that server.
id: String,
/// The time the file was uploaded at, if known.
upload_at: Option<DateTime<Utc>>,
/// The time the file will expire at.
expire_at: DateTime<Utc>,
/// Define whether the expiry time is uncertain.
expire_uncertain: bool,
/// The host the file was uploaded to.
#[serde(with = "url_serde")]
host: Url,
/// The file URL that was provided by the server.
#[serde(with = "url_serde")]
url: Url,
/// The secret key that is required to download the file.
secret: Vec<u8>,
/// The owner key, that can be used to manage the file on the server.
owner_token: Option<String>,
}
impl RemoteFile {
/// Construct a new file.
pub fn new(
id: String,
upload_at: Option<DateTime<Utc>>,
expire_at: Option<DateTime<Utc>>,
host: Url,
url: Url,
secret: Vec<u8>,
owner_token: Option<String>,
) -> Self {
// Assign the default expiry time if uncetain
let expire_uncertain = expire_at.is_none();
let expire_at = expire_at.unwrap_or(
Utc::now() + Duration::seconds(SEND_DEFAULT_EXPIRE_TIME)
);
// Build the object
Self {
id,
upload_at,
expire_at,
expire_uncertain,
host,
url,
secret,
owner_token,
}
}
/// Construct a new file, that was created at this exact time.
/// This will set the file expiration time
pub fn new_now(
id: String,
host: Url,
url: Url,
secret: Vec<u8>,
owner_token: Option<String>,
) -> Self {
// Get the current time
let now = Utc::now();
let expire_at = now + Duration::seconds(SEND_DEFAULT_EXPIRE_TIME);
// Construct and return
Self::new(
id,
Some(now),
Some(expire_at),
host,
url,
secret,
owner_token,
)
}
/// Try to parse the given share URL.
///
/// The given URL is matched against a share URL pattern,
/// this does not check whether the host is a valid and online host.
///
/// If the URL fragmet contains a file secret, it is also parsed.
/// If it does not, the secret is left empty and must be specified
/// manually.
///
/// An optional owner token may be given.
pub fn parse_url(url: Url, owner_token: Option<String>)
-> Result<RemoteFile, FileParseError>
{
// Build the host
let mut host = url.clone();
host.set_fragment(None);
host.set_query(None);
host.set_path("");
// Validate the path, get the file ID
let re_path = Regex::new(SHARE_PATH_PATTERN).unwrap();
let id = re_path.captures(url.path())
.ok_or(FileParseError::InvalidUrl)?[1]
.trim()
.to_owned();
// Get the file secret
let mut secret = Vec::new();
if let Some(fragment) = url.fragment() {
let re_fragment = Regex::new(SHARE_FRAGMENT_PATTERN).unwrap();
if let Some(raw) = re_fragment.captures(fragment)
.ok_or(FileParseError::InvalidSecret)?
.get(1)
{
secret = b64::decode(raw.as_str().trim())
.map_err(|_| FileParseError::InvalidSecret)?
}
}
// Construct the file
Ok(Self::new(
id,
None,
None,
host,
url,
secret,
owner_token,
))
}
/// Get the file ID.
pub fn id(&self) -> &str {
&self.id
}
/// Get the time the file will expire after.
/// Note that this time may not be correct as it may have been guessed,
/// see `expire_uncertain()`.
pub fn expire_at(&self) -> DateTime<Utc> {
self.expire_at
}
/// Get the duration the file will expire after.
/// Note that this time may not be correct as it may have been guessed,
/// see `expire_uncertain()`.
pub fn expire_duration(&self) -> Duration {
// Get the current time
let now = Utc::now();
// Return the duration if not expired, otherwise return zero
if self.expire_at > now {
self.expire_at - now
} else {
Duration::zero()
}
}
/// Set the time this file will expire at.
/// None may be given to assign the default expiry time with the
/// uncertainty flag set.
pub fn set_expire_at(&mut self, expire_at: Option<DateTime<Utc>>) {
if let Some(expire_at) = expire_at {
self.expire_at = expire_at;
} else {
self.expire_at = Utc::now() + Duration::seconds(SEND_DEFAULT_EXPIRE_TIME);
self.expire_uncertain = true;
}
}
/// Set the time this file will expire at,
/// based on the given duration from now.
pub fn set_expire_duration(&mut self, duration: Duration) {
self.set_expire_at(Some(Utc::now() + duration));
}
/// Check whether this file has expired, based on it's expiry property.
pub fn has_expired(&self) -> bool {
self.expire_at < Utc::now()
}
/// Check whehter the set expiry time is uncertain.
/// If the expiry time of a file is unknown,
/// the default time is assigned from the first time
/// the file was used. Such time will be uncertain as it probably isn't
/// correct.
/// This time may be used however to check for expiry.
pub fn expire_uncertain(&self) -> bool {
self.expire_uncertain
}
/// Get the file URL, provided by the server.
pub fn url(&self) -> &Url {
&self.url
}
/// Get the raw secret.
// TODO: ensure whether the secret is set?
pub fn secret_raw(&self) -> &Vec<u8> {
&self.secret
}
/// Get the secret as base64 encoded string.
pub fn secret(&self) -> String {
b64::encode(self.secret_raw())
}
/// Set the secret for this file.
pub fn set_secret(&mut self, secret: Vec<u8>) {
self.secret = secret;
}
/// Check whether a file secret is set.
/// This secret must be set to decrypt a downloaded Send file.
pub fn has_secret(&self) -> bool {
!self.secret.is_empty()
}
/// Get the owner token if set.
pub fn owner_token(&self) -> Option<&String> {
self.owner_token.as_ref()
}
/// Get the owner token if set.
pub fn owner_token_mut(&mut self) -> &mut Option<String> {
&mut self.owner_token
}
/// Set the owner token, wrapped in an option.
/// If `None` is given, the owner token will be unset.
pub fn set_owner_token(&mut self, token: Option<String>) {
self.owner_token = token;
}
/// Check whether an owner token is set in this remote file.
pub fn has_owner_token(&self) -> bool {
self.owner_token
.clone()
.map(|t| !t.is_empty())
.unwrap_or(false)
}
/// Get the host URL for this remote file.
pub fn host(&self) -> Url {
self.host.clone()
}
/// Build the download URL of the given file.
/// This URL is identical to the share URL, a term used in this API.
/// Set `secret` to `true`, to include it in the URL if known.
pub fn download_url(&self, secret: bool) -> Url {
UrlBuilder::download(&self, secret)
}
/// Merge properties non-existant into this file, from the given other file.
/// This is ofcourse only done for properties that may be empty.
///
/// The file IDs are not asserted for equality.
#[allow(unknown_lints, useless_let_if_seq)]
pub fn merge(&mut self, other: &RemoteFile, overwrite: bool) -> bool {
// Remember whether anything has changed
let mut changed = false;
// Set the upload time
if other.upload_at.is_some() && (self.upload_at.is_none() || overwrite) {
self.upload_at = other.upload_at;
changed = true;
}
// Set the expire time
if !other.expire_uncertain() && (self.expire_uncertain() || overwrite) {
self.expire_at = other.expire_at;
self.expire_uncertain = other.expire_uncertain();
changed = true;
}
// Set the secret
if other.has_secret() && (!self.has_secret() || overwrite) {
self.secret = other.secret_raw().clone();
changed = true;
}
// Set the owner token
if other.owner_token.is_some() && (self.owner_token.is_none() || overwrite) {
self.owner_token = other.owner_token.clone();
changed = true;
}
changed
}
}
#[derive(Debug, Fail)]
pub enum FileParseError {
/// An URL format error.
#[fail(display = "failed to parse remote file, invalid URL format")]
UrlFormatError(#[cause] UrlParseError),
/// An error for an invalid share URL format.
#[fail(display = "failed to parse remote file, invalid URL")]
InvalidUrl,
/// An error for an invalid secret format, if an URL fragmet exists.
#[fail(display = "failed to parse remote file, invalid secret in URL")]
InvalidSecret,
}

View file

@ -1,27 +0,0 @@
#[macro_use]
extern crate arrayref;
#[macro_use]
extern crate derive_builder;
extern crate failure;
#[macro_use]
extern crate failure_derive;
extern crate mime_guess;
extern crate openssl;
pub extern crate reqwest;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate time;
pub extern crate url;
pub extern crate url_serde;
pub mod action;
mod api;
pub mod config;
pub mod crypto;
mod ext;
pub mod file;
pub mod reader;
pub use failure::Error;

View file

@ -1,617 +0,0 @@
use std::cmp::{max, min};
use std::fs::File;
use std::io::{
self,
BufReader,
Cursor,
Error as IoError,
Read,
Write,
};
use std::sync::{Arc, Mutex};
use openssl::symm::{
Cipher,
Crypter,
Mode as CrypterMode,
};
/// The length in bytes of crytographic tags that are used.
const TAG_LEN: usize = 16;
// TODO: create a generic reader/writer wrapper for the the encryptor/decryptor.
/// A lazy file reader, that encrypts the file with the given `cipher`
/// and appends the cryptographic tag to the end of it.
///
/// This reader is lazy because the file data loaded from the system
/// and encrypted when it is read from the reader.
/// This greatly reduces memory usage for large files.
///
/// This reader encrypts the file data with an appended cryptographic tag.
///
/// The reader uses a small internal buffer as data is encrypted in blocks,
/// which may output more data than fits in the given buffer while reading.
/// The excess data is then returned on the next read.
pub struct EncryptedFileReader {
/// The raw file that is read from.
file: File,
/// The cipher type used for encrypting.
cipher: Cipher,
/// The crypter used for encrypting the read file.
crypter: Crypter,
/// A tag cursor that reads the tag to append,
/// when the file is fully read and the tag is known.
tag: Option<Cursor<Vec<u8>>>,
/// The internal buffer, containing encrypted data that has yet to be
/// outputted to the reader. This data is always outputted before any new
/// data is produced.
internal_buf: Vec<u8>,
}
impl EncryptedFileReader {
/// Construct a new reader for the given `file` with the given `cipher`.
///
/// This method consumes twice the size of the file in memory while
/// constructing, and constructs a reader that has a size similar to the
/// file.
///
/// It is recommended to wrap this reader in some sort of buffer, such as:
/// `std::io::BufReader`
pub fn new(file: File, cipher: Cipher, key: &[u8], iv: &[u8])
-> Result<Self, io::Error>
{
// Build the crypter
let crypter = Crypter::new(
cipher,
CrypterMode::Encrypt,
key,
Some(iv),
)?;
// Construct the encrypted reader
Ok(
EncryptedFileReader {
file,
cipher,
crypter,
tag: None,
internal_buf: Vec::new(),
}
)
}
/// Read data from the internal buffer if there is any data in it, into
/// the given `buf`.
///
/// The number of bytes that were read into `buf` is returned.
///
/// If there is no data to be read, or `buf` has a zero size, `0` is always
/// returned.
fn read_internal(&mut self, buf: &mut [u8]) -> usize {
// Return if there is no data to read
if self.internal_buf.is_empty() || buf.is_empty() {
return 0;
}
// Determine how much data will be read
let len = min(buf.len(), self.internal_buf.len());
// Slice the section we will read from, copy to the reader
{
let (out, _) = self.internal_buf.split_at(len);
let (buf, _) = buf.split_at_mut(len);
buf.copy_from_slice(out);
}
// Drain the read data from the internal buffer
self.internal_buf.drain(..len);
len
}
/// Read data directly from the file, and encrypt it.
///
/// Because data may be encrypted in blocks, it is possible more data
/// is produced than fits in the given `buf`. In that case the excess data
/// is stored in an internal buffer, and is ouputted the next time being
/// read from the reader.
///
/// The number of bytes that is read into `buf` is returned.
fn read_file_encrypted(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
// Get the block size, determine the buffer size, create a data buffer
let block_size = self.cipher.block_size();
let mut data = vec![0u8; buf.len()];
// Read the file, return if nothing was read
let len = self.file.read(&mut data)?;
if len == 0 {
return Ok(0);
}
// Create an encrypted buffer, truncate the data buffer
let mut encrypted = vec![0u8; len + block_size];
// Encrypt the data that was read
let len = self.crypter.update(&data[..len], &mut encrypted)?;
// Calculate how many bytes will be copied to the reader
let out_len = min(buf.len(), len);
// Fill the reader buffer
let (out, remaining) = encrypted.split_at(out_len);
let (buf, _) = buf.split_at_mut(out_len);
buf.copy_from_slice(out);
// Splice to the actual remaining bytes, store it for later
let (store, _) = remaining.split_at(len - out_len);
self.internal_buf.extend(store.iter());
// Return the number of bytes read to the reader
Ok(out_len)
}
/// Finalize the crypter once it is done encrypthing the whole file.
/// This finalization step produces a tag that is placed after the
/// encrypted file data.
///
/// This step must be invoked to start reading the tag,
/// and after it has been invoked no data must be encrypted anymore.
///
/// This method must only be invoked once.
fn finalize_file(&mut self) -> Result<(), io::Error> {
// Finalize the crypter, catch any remaining output
let mut output = vec![0u8; self.cipher.block_size()];
let len = self.crypter.finalize(&mut output)?;
// Move additional output in the internal buffer
if len > 0 {
self.internal_buf.extend(output.iter().take(len));
}
// Fetch the encryption tag, and create an internal reader for it
let mut tag = vec![0u8; TAG_LEN];
self.crypter.get_tag(&mut tag)?;
self.tag = Some(Cursor::new(tag));
Ok(())
}
}
impl ExactLengthReader for EncryptedFileReader {
/// Calculate the total length of the encrypted file with the appended
/// tag.
/// Useful in combination with some progress monitor, to determine how much
/// of the file is read or for example; sent over the network.
fn len(&self) -> Result<u64, io::Error> {
Ok(self.file.metadata()?.len() + TAG_LEN as u64)
}
}
/// The reader trait implementation.
impl Read for EncryptedFileReader {
/// Read from the encrypted file, and then the encryption tag.
fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
// Read from the internal buffer, return full or splice to empty
let len = self.read_internal(buf);
if len >= buf.len() {
return Ok(len);
}
let (_, buf) = buf.split_at_mut(len);
// Keep track of the total number of read bytes, to return
let mut total = len;
// If the tag reader has been created, only read from that one
if let Some(ref mut tag) = self.tag {
return Ok(tag.read(buf)? + total);
}
// Read the encrypted file, return full or splice to empty
let len = self.read_file_encrypted(buf)?;
total += len;
if len >= buf.len() {
return Ok(total);
}
let (_, buf) = buf.split_at_mut(len);
// Finalize the file crypter, and build the tag
self.finalize_file()?;
// Try to fill the remaining part of the buffer
Ok(self.read(buf)? + total)
}
}
// TODO: implement this some other way
unsafe impl Send for EncryptedFileReader {}
/// A reader wrapper, that measures the reading process for a reader with a
/// known length.
///
/// If the reader exceeds the initially specified length,
/// the reader will continue to allow reads.
/// The length property will grow accordingly.
///
/// The reader will only start producing `None` if the wrapped reader is doing
/// so.
pub struct ProgressReader<R> {
/// The wrapped reader.
inner: R,
/// The total length of the reader.
len: u64,
/// The current reading progress.
progress: u64,
/// A reporter, to report the progress status to.
reporter: Option<Arc<Mutex<ProgressReporter>>>,
}
impl<R: Read> ProgressReader<R> {
/// Wrap the given reader with an exact length, in a progress reader.
pub fn new(inner: R) -> Result<Self, IoError>
where
R: ExactLengthReader
{
Ok(
Self {
len: inner.len()?,
inner,
progress: 0,
reporter: None,
}
)
}
/// Wrap the given reader with the given length in a progress reader.
pub fn from(inner: R, len: u64) -> Self {
Self {
inner,
len,
progress: 0,
reporter: None,
}
}
/// Set the reporter to report the status to.
pub fn set_reporter(&mut self, reporter: Arc<Mutex<ProgressReporter>>) {
self.reporter = Some(reporter);
}
/// Get the current progress.
pub fn progress(&self) -> u64 {
self.progress
}
}
impl<R: Read> Read for ProgressReader<R> {
/// Read from the encrypted file, and then the encryption tag.
fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
// Read from the wrapped reader, increase the progress
let len = self.inner.read(buf)?;
self.progress += len as u64;
// Keep the specified length in-bound
if self.progress > self.len {
self.len = self.progress;
}
// Report
if let Some(reporter) = self.reporter.as_mut() {
let progress = self.progress;
let _ = reporter.lock().map(|mut r| r.progress(progress));
}
Ok(len)
}
}
impl<R: Read> ExactLengthReader for ProgressReader<R> {
// Return the specified length.
fn len(&self) -> Result<u64, io::Error> {
Ok(self.len)
}
}
/// A progress reporter.
pub trait ProgressReporter: Send {
/// Start the progress with the given total.
fn start(&mut self, total: u64);
/// A progress update.
fn progress(&mut self, progress: u64);
/// Finish the progress.
fn finish(&mut self);
}
/// A trait for readers, to get the exact length of a reader.
pub trait ExactLengthReader {
/// Get the exact length of the reader in bytes.
fn len(&self) -> Result<u64, io::Error>;
/// Check whehter this extact length reader is emtpy.
fn is_empty(&self) -> Result<bool, io::Error> {
self.len().map(|l| l == 0)
}
}
impl<R: ExactLengthReader + Read> ExactLengthReader for BufReader<R> {
fn len(&self) -> Result<u64, io::Error> {
self.get_ref().len()
}
}
/// A lazy file writer, that decrypt the file with the given `cipher`
/// and verifies it with the tag appended to the end of the input data.
///
/// This writer is lazy because the input data is decrypted and written to the
/// specified file on the fly, instead of buffering all the data first.
/// This greatly reduces memory usage for large files.
///
/// The length of the input data (including the appended tag) must be given
/// when this reader is initialized. When all data including the tag is read,
/// the decrypted data is verified with the tag. If the tag doesn't match the
/// decrypted data, a write error is returned on the last write.
/// This writer will never write more bytes than the length initially
/// specified.
///
/// This reader encrypts the input data with the given key and input vector.
///
/// A failed writing implies that no data could be written, or that the data
/// wasn't successfully decrypted because of an decryption or tag matching
/// error. Such a fail means that the file will be incomplete or corrupted,
/// and should therefore be removed from the disk.
///
/// It is highly recommended to invoke the `verified()` method after writing
/// the file, to ensure the written file is indeed complete and fully verified.
pub struct EncryptedFileWriter {
/// The file to write the decrypted data to.
file: File,
/// The number of bytes that have currently been written to this writer.
cur: usize,
/// The length of all the data, which includes the file data and the
/// appended tag.
len: usize,
/// The cipher type used for decrypting.
cipher: Cipher,
/// The crypter used for decrypting the data.
crypter: Crypter,
/// A buffer for the tag.
tag_buf: Vec<u8>,
/// A boolean that defines whether the decrypted data has successfully
/// been verified.
verified: bool,
}
impl EncryptedFileWriter {
/// Construct a new encrypted file writer.
///
/// The file to write to must be given to `file`, which must be open for
/// writing. The total length of the input data in bytes must be given to
/// `len`, which includes both the file bytes and the appended tag.
///
/// For decryption, a `cipher`, `key` and `iv` must also be given.
pub fn new(file: File, len: usize, cipher: Cipher, key: &[u8], iv: &[u8])
-> Result<Self, io::Error>
{
// Build the crypter
let crypter = Crypter::new(
cipher,
CrypterMode::Decrypt,
key,
Some(iv),
)?;
// Construct the encrypted reader
Ok(
EncryptedFileWriter {
file,
cur: 0,
len,
cipher,
crypter,
tag_buf: Vec::with_capacity(TAG_LEN),
verified: false,
}
)
}
/// Check wheher the complete tag is buffered.
pub fn has_tag(&self) -> bool {
self.tag_buf.len() >= TAG_LEN
}
/// Check whether the decrypted data is succesfsully verified.
///
/// If this method returns true the following is implied:
/// - The complete file has been written.
/// - The complete file was successfully decrypted.
/// - The included tag matches the decrypted file.
///
/// It is highly recommended to invoke this method and check the
/// verification after writing the file using this writer.
pub fn verified(&self) -> bool {
self.verified
}
}
impl ExactLengthReader for EncryptedFileWriter {
fn len(&self) -> Result<u64, IoError> {
Ok(self.len as u64)
}
}
/// The writer trait implementation.
impl Write for EncryptedFileWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
// Do not write anything if the tag was already written
if self.verified() || self.has_tag() {
return Ok(0);
}
// Determine how many file and tag bytes we still need to process
let file_bytes = max(self.len - TAG_LEN - self.cur, 0);
let tag_bytes = TAG_LEN - self.tag_buf.len();
// Split the input buffer
let (file_buf, tag_buf) = buf.split_at(min(file_bytes, buf.len()));
// Read from the file buf
if !file_buf.is_empty() {
// Create a decrypted buffer, with the proper size
let block_size = self.cipher.block_size();
let mut decrypted = vec![0u8; file_bytes + block_size];
// Decrypt bytes
// TODO: catch error in below statement
let len = self.crypter.update(
file_buf,
&mut decrypted,
)?;
// Write to the file
self.file.write_all(&decrypted[..len])?;
}
// Read from the tag part to fill the tag buffer
if !tag_buf.is_empty() {
self.tag_buf.extend(tag_buf.iter().take(tag_bytes));
}
// Verify the tag once it has been buffered completely
if self.has_tag() {
// Set the tag
self.crypter.set_tag(&self.tag_buf)?;
// Create a buffer for any remaining data
let block_size = self.cipher.block_size();
let mut extra = vec![0u8; block_size];
// Finalize, write all remaining data
let len = self.crypter.finalize(&mut extra)?;
self.file.write_all(&extra[..len])?;
// Set the verified flag
self.verified = true;
}
// Compute how many bytes were written
let len = file_buf.len() + min(tag_buf.len(), TAG_LEN);
self.cur += len;
Ok(len)
}
fn flush(&mut self) -> Result<(), io::Error> {
self.file.flush()
}
}
/// A writer wrapper, that measures the reading process for a writer with a
/// known length.
///
/// If the writer exceeds the initially specified length,
/// the writer will continue to allow reads.
/// The length property will grow accordingly.
///
/// The writer will only start producing `None` if the wrapped writer is doing
/// so.
pub struct ProgressWriter<W> {
/// The wrapped writer.
inner: W,
/// The total length of the writer.
len: u64,
/// The current reading progress.
progress: u64,
/// A reporter, to report the progress status to.
reporter: Option<Arc<Mutex<ProgressReporter>>>,
}
impl<W: Write> ProgressWriter<W> {
/// Wrap the given writer with an exact length, in a progress writer.
pub fn new(inner: W) -> Result<Self, IoError>
where
W: ExactLengthReader
{
Ok(
Self {
len: inner.len()?,
inner,
progress: 0,
reporter: None,
}
)
}
/// Wrap the given writer with the given length in a progress writer.
pub fn from(inner: W, len: u64) -> Self {
Self {
inner,
len,
progress: 0,
reporter: None,
}
}
/// Set the reporter to report the status to.
pub fn set_reporter(&mut self, reporter: Arc<Mutex<ProgressReporter>>) {
self.reporter = Some(reporter);
}
/// Get the current progress.
pub fn progress(&self) -> u64 {
self.progress
}
/// Unwrap the inner from the progress writer.
pub fn unwrap(self) -> W {
self.inner
}
}
impl<W: Write> Write for ProgressWriter<W> {
fn write(&mut self, buf: &[u8]) -> Result<usize, io::Error> {
// Write from the wrapped writer, increase the progress
let len = self.inner.write(buf)?;
self.progress += len as u64;
// Keep the specified length in-bound
if self.progress > self.len {
self.len = self.progress;
}
// Report
if let Some(reporter) = self.reporter.as_mut() {
let progress = self.progress;
let _ = reporter.lock().map(|mut r| r.progress(progress));
}
Ok(len)
}
fn flush(&mut self) -> Result<(), IoError> {
self.inner.flush()
}
}
impl<W: Write> ExactLengthReader for ProgressWriter<W> {
// Return the specified length.
fn len(&self) -> Result<u64, io::Error> {
Ok(self.len)
}
}

View file

@ -1,49 +0,0 @@
[package]
name = "ffsend"
description = """\
Easily and securely share files from the command line.\n\
A fully featured Firefox Send client.\
"""
version = "0.0.1"
authors = ["Tim Visee <https://timvisee.com/>"]
workspace = ".."
[[bin]]
path = "src/main.rs"
name = "ffsend"
[features]
default = ["archive", "clipboard", "history"]
# Compile with file archiving support
archive = ["tar"]
# Compile with file history support
history = []
# Compile without colored output support
no-color = ["colored/no-color"]
[dependencies]
chrono = "0.4"
clap = "2.31"
colored = "1.6"
derive_builder = "0.5"
directories = "0.10"
failure = "0.1"
ffsend-api = { version = "*", path = "../api" }
fs2 = "0.4"
lazy_static = "1.0"
open = "1"
pbr = "1"
prettytable-rs = "0.6"
rpassword = "2.0"
serde = "1.0"
serde_derive = "1.0"
tar = { version = "0.4", optional = true }
tempfile = "3"
toml = "0.4"
version-compare = "0.0.6"
[target.'cfg(not(target_os = "linux"))'.dependencies]
clipboard = { version = "0.4", optional = true }