Extract ffsend-api to separate repository, move client crate to root
This commit is contained in:
parent
e2b9b5c55c
commit
8ed530a83a
83 changed files with 132 additions and 3984 deletions
|
@ -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
157
Cargo.lock
generated
|
@ -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"
|
||||
|
|
54
Cargo.toml
54
Cargo.toml
|
@ -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 }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
20
api/LICENSE
20
api/LICENSE
|
@ -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.
|
||||
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
|
@ -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),
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
pub mod data;
|
||||
pub mod nonce;
|
||||
pub mod url;
|
||||
pub mod request;
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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))
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub mod status_code;
|
|
@ -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()))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod remote_file;
|
||||
pub mod metadata;
|
|
@ -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,
|
||||
}
|
|
@ -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;
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 }
|
Loading…
Reference in a new issue