diff options
| author | David Lönnhager <david.l@mullvad.net> | 2020-08-20 15:04:27 +0200 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2020-08-20 15:04:27 +0200 |
| commit | 3e277b10f220d022726430cbed20fab4d6b1957d (patch) | |
| tree | b8d26480c9e4bae70eee19c9484e5e7ef85c882c | |
| parent | 9a67c39d37c042df4b9381bd101a369efd516880 (diff) | |
| parent | 857a4101f98b564caba1455aabf988eac8adc55e (diff) | |
| download | mullvadvpn-3e277b10f220d022726430cbed20fab4d6b1957d.tar.xz mullvadvpn-3e277b10f220d022726430cbed20fab4d6b1957d.zip | |
Merge branch 'tonic-management-interface'
58 files changed, 5403 insertions, 2788 deletions
diff --git a/.gitignore b/.gitignore index bb585e09e2..9db2f0d726 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /gui/scripts/ne_50m_admin_0_countries/ /gui/scripts/ne_50m_admin_1_states_provinces_lines/ /gui/scripts/out/ +/gui/src/main/management_interface/ /build/ /dist .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index dab7696050..2f671367ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Line wrap the file at 100 chars. Th - Display the original block reason in the non-blocking error state, and why applying the blocking policy failed. - Don't show account time expired notification for newly created accounts. +- Use gRPC for communication between frontends and the backend instead of JSON-RPC. #### Android - Show a system notification when the account time will soon run out. diff --git a/Cargo.lock b/Cargo.lock index 1c7a581d0d..1f78c3f373 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -74,11 +74,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "assert_matches" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] name = "async-stream" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -99,7 +94,7 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.31" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", @@ -180,14 +175,6 @@ dependencies = [ ] [[package]] -name = "bstr" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "bumpalo" version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -785,30 +772,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "globset" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aho-corasick 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "globset" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", - "bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "h2" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1027,20 +990,6 @@ dependencies = [ [[package]] name = "jsonrpc-client-core" version = "0.5.0" -source = "git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b#68aac55b6ddff5e1242594b54f7f9149fe215ff7" -dependencies = [ - "error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-client-utils 0.1.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-core 8.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "jsonrpc-client-core" -version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1052,43 +1001,6 @@ dependencies = [ ] [[package]] -name = "jsonrpc-client-ipc" -version = "0.5.0" -source = "git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b#68aac55b6ddff5e1242594b54f7f9149fe215ff7" -dependencies = [ - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-client-core 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-server-utils 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-tokio-ipc 0.2.0 (git+https://github.com/nikvolf/parity-tokio-ipc)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "jsonrpc-client-pubsub" -version = "0.1.0" -source = "git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b#68aac55b6ddff5e1242594b54f7f9149fe215ff7" -dependencies = [ - "error-chain 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-client-core 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-client-utils 0.1.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "jsonrpc-client-utils" -version = "0.1.0" -source = "git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b#68aac55b6ddff5e1242594b54f7f9149fe215ff7" -dependencies = [ - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "jsonrpc-core" version = "8.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1101,79 +1013,6 @@ dependencies = [ ] [[package]] -name = "jsonrpc-core" -version = "8.0.2" -source = "git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork#9de0ea1ad4c296465f44f53050d99459ca885157" -dependencies = [ - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "jsonrpc-ipc-server" -version = "8.0.1" -source = "git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork#9de0ea1ad4c296465f44f53050d99459ca885157" -dependencies = [ - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-server-utils 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parity-tokio-ipc 0.2.0 (git+https://github.com/nikvolf/parity-tokio-ipc)", - "parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-service 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "jsonrpc-macros" -version = "8.0.1" -source = "git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork#9de0ea1ad4c296465f44f53050d99459ca885157" -dependencies = [ - "jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-pubsub 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "jsonrpc-pubsub" -version = "8.0.1" -source = "git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork#9de0ea1ad4c296465f44f53050d99459ca885157" -dependencies = [ - "jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "jsonrpc-server-utils" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "globset 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 8.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "jsonrpc-server-utils" -version = "8.0.1" -source = "git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork#9de0ea1ad4c296465f44f53050d99459ca885157" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "globset 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "kernel32-sys" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1207,15 +1046,6 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "lock_api" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ @@ -1248,14 +1078,6 @@ dependencies = [ [[package]] name = "memchr" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "memchr" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1366,13 +1188,14 @@ dependencies = [ "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "mullvad-ipc-client 0.1.0", + "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "mullvad-management-interface 0.1.0", "mullvad-paths 0.1.0", "mullvad-types 0.1.0", "natord 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", - "talpid-ipc 0.1.0", "talpid-types 0.1.0", + "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "winres 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1393,16 +1216,11 @@ dependencies = [ "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", "ipnetwork 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-client-core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-ipc-server 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-macros 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-pubsub 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "mullvad-ipc-client 0.1.0", + "mullvad-management-interface 0.1.0", "mullvad-paths 0.1.0", "mullvad-rpc 0.1.0", "mullvad-types 0.1.0", @@ -1414,12 +1232,12 @@ dependencies = [ "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", "simple-signal 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "talpid-core 0.1.0", - "talpid-ipc 0.1.0", "talpid-types 0.1.0", "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-retry 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "triggered 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "windows-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1437,26 +1255,6 @@ dependencies = [ ] [[package]] -name = "mullvad-ipc-client" -version = "0.1.0" -dependencies = [ - "err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-client-core 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-client-ipc 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-client-pubsub 0.1.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mullvad-paths 0.1.0", - "mullvad-types 0.1.0", - "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", - "talpid-ipc 0.1.0", - "talpid-types 0.1.0", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "mullvad-jni" version = "0.1.0" dependencies = [ @@ -1481,6 +1279,24 @@ dependencies = [ ] [[package]] +name = "mullvad-management-interface" +version = "0.1.0" +dependencies = [ + "err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "mullvad-paths 0.1.0", + "parity-tokio-ipc 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "prost 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "prost-types 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", + "tonic 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tonic-build 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "tower 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "triggered 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "mullvad-paths" version = "0.1.0" dependencies = [ @@ -1544,44 +1360,16 @@ dependencies = [ "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", "err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "mullvad-ipc-client 0.1.0", + "mullvad-management-interface 0.1.0", "mullvad-paths 0.1.0", "talpid-core 0.1.0", "talpid-types 0.1.0", + "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", "winres 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] -name = "mullvad-tests" -version = "0.1.0" -dependencies = [ - "duct 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-client-core 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-client-ipc 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-client-pubsub 0.1.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", - "mullvad-ipc-client 0.1.0", - "mullvad-paths 0.1.0", - "mullvad-rpc 0.1.0", - "mullvad-types 0.1.0", - "notify 4.0.13 (registry+https://github.com/rust-lang/crates.io-index)", - "openvpn-plugin 0.3.0 (git+https://github.com/mullvad/openvpn-plugin-rs?branch=auth-failed-event)", - "parity-tokio-ipc 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "prost 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", - "talpid-ipc 0.1.0", - "talpid-types 0.1.0", - "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-timer 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tonic 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "tonic-build 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tower 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "mullvad-types" version = "0.1.0" dependencies = [ @@ -1831,31 +1619,6 @@ dependencies = [ ] [[package]] -name = "owning_ref" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "parity-tokio-ipc" -version = "0.2.0" -source = "git+https://github.com/nikvolf/parity-tokio-ipc#2bfc4354fda235f0249b963853701e1571f04dbc" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "mio-named-pipes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "miow 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-named-pipes 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio-uds 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "parity-tokio-ipc" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1872,15 +1635,6 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "parking_lot_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "parking_lot" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ @@ -1891,18 +1645,6 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "parking_lot_core" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ @@ -2177,18 +1919,6 @@ dependencies = [ [[package]] name = "rand" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", - "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rand" version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ @@ -2534,11 +2264,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "scopeguard" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "scopeguard" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2696,11 +2421,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] -name = "stable_deref_trait" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2845,8 +2565,6 @@ dependencies = [ "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "ipnetwork 0.16.0 (registry+https://github.com/rust-lang/crates.io-index)", "jnix 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-macros 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2873,7 +2591,6 @@ dependencies = [ "shell-escape 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", "socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)", "system-configuration 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "talpid-ipc 0.1.0", "talpid-types 0.1.0", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tokio 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2893,28 +2610,6 @@ dependencies = [ ] [[package]] -name = "talpid-ipc" -version = "0.1.0" -dependencies = [ - "assert_matches 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", - "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", - "err-derive 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-client-core 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-client-ipc 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)", - "jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-ipc-server 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-macros 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "jsonrpc-pubsub 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)", - "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", - "uuid 0.7.4 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "talpid-openvpn-plugin" version = "2020.5.0" dependencies = [ @@ -3143,18 +2838,6 @@ dependencies = [ ] [[package]] -name = "tokio-named-pipes" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)", - "futures 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", - "mio 0.6.22 (registry+https://github.com/rust-lang/crates.io-index)", - "mio-named-pipes 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "tokio 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] name = "tokio-reactor" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3330,7 +3013,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "async-stream 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "async-trait 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)", + "async-trait 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "bytes 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", @@ -3930,10 +3613,9 @@ dependencies = [ "checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" "checksum arrayvec 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" "checksum ascii 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" -"checksum assert_matches 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7deb0a829ca7bcfaf5da70b073a8d128619259a7be8216a355e23f00763059e5" "checksum async-stream 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "22068c0c19514942eefcfd4daf8976ef1aad84e61539f95cd200c35202f80af5" "checksum async-stream-impl 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "25f9db3b38af870bf7e5cc649167533b493928e50744e2c30ae350230b414670" -"checksum async-trait 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "26c4f3195085c36ea8d24d32b2f828d23296a9370a28aa39d111f6f16bef9f3b" +"checksum async-trait 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "a265e3abeffdce30b2e26b7a11b222fe37c6067404001b434101457d0385eb92" "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" "checksum autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" @@ -3944,7 +3626,6 @@ dependencies = [ "checksum bitflags 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f67931368edf3a9a51d29886d245f1c3db2f1ef0dcc9e35ff70341b78c10d23" "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum blake2b_simd 0.5.8 (registry+https://github.com/rust-lang/crates.io-index)" = "5850aeee1552f495dd0250014cf64b82b7c8879a89d83b33bbdace2cc4f63182" -"checksum bstr 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8d6c2c5b58ab920a4f5aeaaca34b4488074e8cc7596af94e6f8c6ff247c60245" "checksum bumpalo 3.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "12ae9db68ad7fac5fe51304d20f016c911539251075a214f8e663babefa35187" "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" @@ -4014,8 +3695,6 @@ dependencies = [ "checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" "checksum getrandom 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "473a1265acc8ff1e808cd0a1af8cee3c2ee5200916058a2ca113c29f2d903571" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" -"checksum globset 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "90d069fe6beb9be359ef505650b3f73228c5591a3c4b1f32be2f4f44459ffa3a" -"checksum globset 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "925aa2cac82d8834e2b2a4415b6f6879757fb5c0928fc445ae76461a12eed8f2" "checksum h2 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "377038bf3c89d18d6ca1431e7a5027194fbd724ca10592b9487ede5e8e144f42" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e" @@ -4039,29 +3718,17 @@ dependencies = [ "checksum jnix 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6354ae923ca4df982181ae2cd77eb4214f8c11d11d0c0cd8606c9347ac2abc57" "checksum jnix-macros 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c78132fe420156f13b30518fcda9449b0ab8ae3b5584e8a1c53ce390fe770b44" "checksum js-sys 0.3.37 (registry+https://github.com/rust-lang/crates.io-index)" = "6a27d435371a2fa5b6d2b028a74bbdb1234f308da363226a2854ca3ff8ba7055" -"checksum jsonrpc-client-core 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)" = "<none>" "checksum jsonrpc-client-core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f29cb249837420fb0cee7fb0fbf1d22679e121b160e71bb5e0d90b9df241c23e" -"checksum jsonrpc-client-ipc 0.5.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)" = "<none>" -"checksum jsonrpc-client-pubsub 0.1.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)" = "<none>" -"checksum jsonrpc-client-utils 0.1.0 (git+https://github.com/mullvad/jsonrpc-client-rs?rev=68aac55b)" = "<none>" "checksum jsonrpc-core 8.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ddf83704f4e79979a424d1082dd2c1e52683058056c9280efa19ac5f6bc9033c" -"checksum jsonrpc-core 8.0.2 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)" = "<none>" -"checksum jsonrpc-ipc-server 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)" = "<none>" -"checksum jsonrpc-macros 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)" = "<none>" -"checksum jsonrpc-pubsub 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)" = "<none>" -"checksum jsonrpc-server-utils 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "513e981828a4953ea7ddbb64c24d15d4983ecf6900dc1cd36f257d61c27138d5" -"checksum jsonrpc-server-utils 8.0.1 (git+https://github.com/mullvad/jsonrpc?branch=mullvad-fork)" = "<none>" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum lazycell 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b294d6fa9ee409a054354afc4352b0b9ef7ca222c69b8812cbea9e7d2bf3783f" "checksum libc 0.2.69 (registry+https://github.com/rust-lang/crates.io-index)" = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" "checksum libdbus-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc12a3bc971424edbbf7edaf6e5740483444db63aa8e23d3751ff12a30f306f0" -"checksum lock_api 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "62ebf1391f6acad60e5c8b43706dde4582df75c06698ab44511d15016bc2442c" "checksum lock_api 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f8912e782533a93a167888781b836336a6ca5da6175c05944c86cf28c31104dc" "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" "checksum log-panics 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ae0136257df209261daa18d6c16394757c63e032e27aafd8b07788b051082bef" -"checksum memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" "checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" "checksum memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce6075db033bbbb7ee5a0bbd3a3186bbae616f57fb001c485c7ff77955f8177f" "checksum mio 0.6.22 (registry+https://github.com/rust-lang/crates.io-index)" = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" @@ -4096,12 +3763,8 @@ dependencies = [ "checksum openvpn-plugin 0.3.0 (git+https://github.com/mullvad/openvpn-plugin-rs?branch=auth-failed-event)" = "<none>" "checksum os_pipe 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "81e8dfa4c69d7bde595e9a940fcf1d7f60966d3fce8a8c4cad67c60e35ea2a11" "checksum os_pipe 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "db4d06355a7090ce852965b2d08e11426c315438462638c6d721448d0b47aa22" -"checksum owning_ref 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "49a4b8ea2179e6a2e27411d3bca09ca6dd630821cf6894c6c7c8467a8ee7ef13" -"checksum parity-tokio-ipc 0.2.0 (git+https://github.com/nikvolf/parity-tokio-ipc)" = "<none>" "checksum parity-tokio-ipc 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c1d417ba1ab454723ff2271bf999fd700027dc48759a13d43e488cc8ca38b87f" -"checksum parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0802bff09003b291ba756dc7e79313e51cc31667e94afbe847def490424cde5" "checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" -"checksum parking_lot_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad7f7e6ebdc79edff6fdcb87a55b620174f7a989e3eb31b65231f4af57f00b8c" "checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b" "checksum paste 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab4fb1930692d1b6a9cfabdde3d06ea0a7d186518e2f4d67660d8970e2fa647a" "checksum paste-impl 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "a62486e111e571b1e93b710b61e8f493c0013be39629b714cb166bdb06aa5a8a" @@ -4133,7 +3796,6 @@ dependencies = [ "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" "checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" "checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" -"checksum rand 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c618c47cd3ebd209790115ab837de41425723956ad3ce2e6a7f09890947cacb9" "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" "checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" "checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" @@ -4173,7 +3835,6 @@ dependencies = [ "checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" "checksum schannel 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "039c25b130bd8c1321ee2d7de7fde2659fa9c2744e4bb29711cfc852ea53cd19" "checksum scoped-tls 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "332ffa32bf586782a3efaeb58f127980944bbc8c4d6913a86107ac2a5ab24b28" -"checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27" "checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d" "checksum sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" "checksum security-framework 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "572dfa3a0785509e7a44b5b4bebcf94d41ba34e9ed9eb9df722545c3b3c4144a" @@ -4194,7 +3855,6 @@ dependencies = [ "checksum snailquote 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fc3e2894a343234fb8a8653cf9d49ef6aea44e6581612ca311c91c4bd356dec4" "checksum socket2 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)" = "e8b74de517221a2cb01a53349cf54182acdc31a074727d3079068448c0676d85" "checksum spin 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -"checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" "checksum subtle 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ab3af2eb31c42e8f0ccf43548232556c42737e01a96db6e1777b0be108e79799" @@ -4226,7 +3886,6 @@ dependencies = [ "checksum tokio-fs 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "3fe6dc22b08d6993916647d108a1a7d15b9cd29c4f4496c62b92c45b5041b7af" "checksum tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5090db468dad16e1a7a54c8c67280c5e4b544f3d3e018f0b913b400261f85926" "checksum tokio-macros 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c3acc6aa564495a0f2e1d59fab677cd7f81a19994cfc7f3ad0e64301560389" -"checksum tokio-named-pipes 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d282d483052288b2308ba5ee795f5673b159c9bdf63c385a05609da782a5eae" "checksum tokio-reactor 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "c56391be9805bc80163151c0b9e5164ee64f4b0200962c346fea12773158f22d" "checksum tokio-retry 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9c03755b956458582182941061def32b8123a26c98b08fc6ddcf49ae89d18f33" "checksum tokio-rustls 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "15cb62a0d2770787abc96e99c1cd98fcf17f94959f3af63ca85bdfb203f051b4" diff --git a/Cargo.toml b/Cargo.toml index fab359c830..f87705cf22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,16 +5,15 @@ members = [ "mullvad-cli", "mullvad-setup", "mullvad-problem-report", - "mullvad-ipc-client", "mullvad-jni", "mullvad-paths", "mullvad-types", "mullvad-rpc", - "mullvad-tests", +# "mullvad-tests", "mullvad-exclude", "talpid-openvpn-plugin", "talpid-core", - "talpid-ipc", + "mullvad-management-interface", ] exclude = ["dist-assets/binaries/shadowsocks-rust"] diff --git a/gui/gulpfile.js b/gui/gulpfile.js index f203fc16d0..6a7226c1aa 100644 --- a/gui/gulpfile.js +++ b/gui/gulpfile.js @@ -1,4 +1,4 @@ -const { task, series } = require('gulp'); +const { task, series, parallel } = require('gulp'); const rimraf = require('rimraf'); const scripts = require('./tasks/scripts'); @@ -9,8 +9,9 @@ const dist = require('./tasks/distribution'); task('clean', function (done) { rimraf('./build', done); }); -task('build', series('clean', assets.copyAll, scripts.build)); -task('develop', series('clean', watch.start)); +task('build-proto', scripts.buildProto); +task('build', series('clean', parallel(assets.copyAll, scripts.buildProto), scripts.build)); +task('develop', series('clean', scripts.buildProto, watch.start)); task('pack-win', series('build', dist.packWin)); task('pack-linux', series('build', dist.packLinux)); task('pack-mac', series('build', dist.packMac)); diff --git a/gui/package-lock.json b/gui/package-lock.json index c3b781bf69..57840f46f4 100644 --- a/gui/package-lock.json +++ b/gui/package-lock.json @@ -397,6 +397,21 @@ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, + "@grpc/grpc-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.1.2.tgz", + "integrity": "sha512-k2u86Bkm/3xrjUaSWeIyzXScBt/cC8uE7BznR0cpueQi11R33W6qfJdMrkrsmSHirp5likR55JSXUrcWG6ybHA==", + "requires": { + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -559,6 +574,12 @@ "@types/node": "*" } }, + "@types/google-protobuf": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@types/google-protobuf/-/google-protobuf-3.7.2.tgz", + "integrity": "sha512-ifFemzjNchFBCtHS6bZNhSZCBu7tbtOe0e8qY0z2J4HtFXmPJjm6fXSaQsTG7yhShBEZtt2oP/bkwu5k+emlkQ==", + "dev": true + }, "@types/history": { "version": "4.7.5", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz", @@ -872,8 +893,7 @@ "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accepts": { "version": "1.3.7", @@ -958,6 +978,105 @@ } } }, + "ansi-bgblack": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgblack/-/ansi-bgblack-0.1.1.tgz", + "integrity": "sha1-poulAHiHcBtqr74/oNrf36juPKI=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgblue": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgblue/-/ansi-bgblue-0.1.1.tgz", + "integrity": "sha1-Z73ATtybm1J4lp2hlt6j11yMNhM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgcyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgcyan/-/ansi-bgcyan-0.1.1.tgz", + "integrity": "sha1-WEiUJWAL3p9VBwaN2Wnr/bUP52g=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bggreen": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bggreen/-/ansi-bggreen-0.1.1.tgz", + "integrity": "sha1-TjGRJIUplD9DIelr8THRwTgWr0k=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgmagenta": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgmagenta/-/ansi-bgmagenta-0.1.1.tgz", + "integrity": "sha1-myhDLAduqpmUGGcqPvvhk5HCx6E=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgred": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgred/-/ansi-bgred-0.1.1.tgz", + "integrity": "sha1-p2+Sg4OCukMpCmwXeEJPmE1vEEE=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgwhite": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgwhite/-/ansi-bgwhite-0.1.1.tgz", + "integrity": "sha1-ZQRlE3elim7OzQMxmU5IAljhG6g=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bgyellow": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgyellow/-/ansi-bgyellow-0.1.1.tgz", + "integrity": "sha1-w/4usIzUdmSAKeaHTRWgs49h1E8=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-black": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-black/-/ansi-black-0.1.1.tgz", + "integrity": "sha1-9hheiJNgslRaHsUMC/Bj/EMDJFM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-blue": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-blue/-/ansi-blue-0.1.1.tgz", + "integrity": "sha1-FbgEmQ6S/JyoxUds6PaZd3wh7b8=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-bold": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bold/-/ansi-bold-0.1.1.tgz", + "integrity": "sha1-PmOVCvWswq4uZw5vZ96xFdGl9QU=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-colors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", @@ -967,6 +1086,24 @@ "ansi-wrap": "^0.1.0" } }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-dim": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-dim/-/ansi-dim-0.1.1.tgz", + "integrity": "sha1-QN5MYDqoCG2Oeoa4/5mNXDbu/Ww=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-escapes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", @@ -993,23 +1130,131 @@ "ansi-wrap": "0.1.0" } }, + "ansi-green": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-green/-/ansi-green-0.1.1.tgz", + "integrity": "sha1-il2al55FjVfEDjNYCzc5C44Q0Pc=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-grey": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-grey/-/ansi-grey-0.1.1.tgz", + "integrity": "sha1-WdmLasK6GfilF5jphT+6eDOaM8E=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-hidden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-hidden/-/ansi-hidden-0.1.1.tgz", + "integrity": "sha1-7WpMSY0rt8uyidvyqNHcyFZ/rg8=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-inverse": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-inverse/-/ansi-inverse-0.1.1.tgz", + "integrity": "sha1-tq9Fgm/oJr+1KKbHmIV5Q1XM0mk=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-italic": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-italic/-/ansi-italic-0.1.1.tgz", + "integrity": "sha1-EEdDRj9iXBQqA2c5z4XtpoiYbyM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-magenta": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-magenta/-/ansi-magenta-0.1.1.tgz", + "integrity": "sha1-BjtboW+z8j4c/aKwfAqJ3hHkMK4=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, + "ansi-reset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-reset/-/ansi-reset-0.1.1.tgz", + "integrity": "sha1-5+cSksPH3c1NYu9KbHwFmAkRw7c=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-strikethrough": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-strikethrough/-/ansi-strikethrough-0.1.1.tgz", + "integrity": "sha1-2Eh3FAss/wfRyT685pkE9oiF5Wg=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "ansi-underline": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-underline/-/ansi-underline-0.1.1.tgz", + "integrity": "sha1-38kg9Ml7WXfqFi34/7mIMIqqcaQ=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, + "ansi-white": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-white/-/ansi-white-0.1.1.tgz", + "integrity": "sha1-nHe3wZPF7pkuYBHTbsTJIbRXiUQ=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-wrap": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", "dev": true }, + "ansi-yellow": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-yellow/-/ansi-yellow-0.1.1.tgz", + "integrity": "sha1-y5NW8vRscy8OMZnmEClVp32oPB0=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "anymatch": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", @@ -1179,8 +1424,7 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "archy": { "version": "1.0.0", @@ -1192,7 +1436,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -1585,6 +1828,15 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "autolinker": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-0.28.1.tgz", + "integrity": "sha1-BlK0kYgYefB3XazgzcoyM5QqTkc=", + "dev": true, + "requires": { + "gulp-header": "^1.7.1" + } + }, "axios": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", @@ -2503,8 +2755,7 @@ "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "optional": true + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "chromium-pickle-js": { "version": "0.2.0", @@ -2696,6 +2947,23 @@ "typedarray": "^0.0.6" } }, + "concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dev": true, + "requires": { + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, "config-chain": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", @@ -2762,8 +3030,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "optional": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "content-type": { "version": "1.0.4", @@ -2813,6 +3080,38 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "create-frame": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/create-frame/-/create-frame-1.0.0.tgz", + "integrity": "sha1-i5XyaR4ySbYIBEPjPQutn49pdao=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "isobject": "^3.0.0", + "lazy-cache": "^2.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "cross-env": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", @@ -2932,6 +3231,26 @@ "resolve": "^1.1.10" } }, + "date.js": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz", + "integrity": "sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==", + "dev": true, + "requires": { + "debug": "~3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } + } + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3072,8 +3391,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { "version": "1.1.2", @@ -3096,8 +3414,7 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "detect-node": { "version": "2.0.4", @@ -4000,6 +4317,12 @@ "has-binary2": "~1.0.2" } }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, "entities": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", @@ -4089,6 +4412,12 @@ "is-arrayish": "^0.2.1" } }, + "error-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/error-symbol/-/error-symbol-0.1.0.tgz", + "integrity": "sha1-Ck2uN9YA0VopukU9jvkg8YRDM/Y=", + "dev": true + }, "es-abstract": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", @@ -4853,6 +5182,23 @@ } } }, + "falsey": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/falsey/-/falsey-0.3.2.tgz", + "integrity": "sha512-lxEuefF5MBIVDmE6XeqCdM4BWk1+vYmGZtkbKZ/VFcg6uBBw6fXNEbWmxCjDdQlFc9hy450nkiWwM3VAW6G1qg==", + "dev": true, + "requires": { + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, "fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -5108,6 +5454,12 @@ "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", "dev": true }, + "fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", + "dev": true + }, "fs-extra": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", @@ -5123,7 +5475,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "optional": true, "requires": { "minipass": "^2.6.0" } @@ -5739,7 +6090,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -5763,6 +6113,42 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-object": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/get-object/-/get-object-0.2.0.tgz", + "integrity": "sha1-2S/31RkMZFMM2gVD2sY6PUf+jAw=", + "dev": true, + "requires": { + "is-number": "^2.0.2", + "isobject": "^0.2.0" + }, + "dependencies": { + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, + "isobject": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-0.2.0.tgz", + "integrity": "sha1-o0MhkvObkQtfAsyYlIeDbscKqF4=", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -5993,6 +6379,11 @@ "sparkles": "^1.0.0" } }, + "google-protobuf": { + "version": "4.0.0-rc.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-4.0.0-rc.2.tgz", + "integrity": "sha512-KANe0ePi8Uk0m17xvjI1bXrE7LRflSuNczkAlHlOEeqXnSBy96tDS5beKwKOnNr0xYmlbC9XiQsj8fOLLyDp2Q==" + }, "got": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", @@ -6035,6 +6426,89 @@ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", "dev": true }, + "grpc-tools": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/grpc-tools/-/grpc-tools-1.9.1.tgz", + "integrity": "sha512-t2JFMPLjxcgwVSJwFEauFaoEiO56kijxSwehQDgZNR/hrStJCH0pHGsjqJNuCOvmI9Z31pYOfgj4zeInTQWh5A==", + "dev": true, + "requires": { + "node-pre-gyp": "^0.15.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "needle": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", + "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", + "dev": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.15.0.tgz", + "integrity": "sha512-7QcZa8/fpaU/BKenjcaeFF9hLz2+7S9AqyXFhlH/rilsQ/hPZKK32RtR5EQHJElgu+q5RfbJ34KriI79UWaorA==", + "dev": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.3", + "needle": "^2.5.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4.4.2" + } + } + } + }, + "grpc_tools_node_protoc_ts": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/grpc_tools_node_protoc_ts/-/grpc_tools_node_protoc_ts-4.1.0.tgz", + "integrity": "sha512-+10Q2LhTalc41e+hCfP7lCLYfyDlAdntEKTn43QHE3CPafNHRu5KgqAHkDg4cUiDEJFUcKhddaYUPI5uR7c2AA==", + "dev": true, + "requires": { + "google-protobuf": "3.5.0", + "handlebars": "4.7.4", + "handlebars-helpers": "0.10.0" + }, + "dependencies": { + "google-protobuf": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.5.0.tgz", + "integrity": "sha1-uMxjx02DRXvYqakEUDyO+ya8ozk=", + "dev": true + } + } + }, "gud": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz", @@ -6110,6 +6584,17 @@ } } }, + "gulp-header": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", + "integrity": "sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==", + "dev": true, + "requires": { + "concat-with-sourcemaps": "*", + "lodash.template": "^4.4.0", + "through2": "^2.0.0" + } + }, "gulp-inject-string": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/gulp-inject-string/-/gulp-inject-string-1.1.2.tgz", @@ -6166,6 +6651,325 @@ "glogg": "^1.0.0" } }, + "handlebars": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.4.tgz", + "integrity": "sha512-Is8+SzHv8K9STNadlBVpVhxXrSXxVgTyIvhdg2Qjak1SfSZ7iEozLHdwiX1jJ9lLFkcFJxqGK5s/cI7ZX+qGkQ==", + "dev": true, + "requires": { + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "handlebars-helper-create-frame": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/handlebars-helper-create-frame/-/handlebars-helper-create-frame-0.1.0.tgz", + "integrity": "sha1-iqUdEK62QI/MZgXUDXc1YohIegM=", + "dev": true, + "requires": { + "create-frame": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "handlebars-helpers": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/handlebars-helpers/-/handlebars-helpers-0.10.0.tgz", + "integrity": "sha512-QiyhQz58u/DbuV41VnfpE0nhy6YCH4vB514ajysV8SoKmP+DxU+pR+fahVyNECHj+jiwEN2VrvxD/34/yHaLUg==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-sort": "^0.1.4", + "create-frame": "^1.0.0", + "define-property": "^1.0.0", + "falsey": "^0.3.2", + "for-in": "^1.0.2", + "for-own": "^1.0.0", + "get-object": "^0.2.0", + "get-value": "^2.0.6", + "handlebars": "^4.0.11", + "handlebars-helper-create-frame": "^0.1.0", + "handlebars-utils": "^1.0.6", + "has-value": "^1.0.0", + "helper-date": "^1.0.1", + "helper-markdown": "^1.0.0", + "helper-md": "^0.2.2", + "html-tag": "^2.0.0", + "is-even": "^1.0.0", + "is-glob": "^4.0.0", + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "lazy-cache": "^2.0.2", + "logging-helpers": "^1.0.0", + "micromatch": "^3.1.4", + "relative": "^3.0.2", + "striptags": "^3.1.0", + "to-gfm-code-block": "^0.1.1", + "year": "^0.2.1" + }, + "dependencies": { + "array-sort": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-0.1.4.tgz", + "integrity": "sha512-BNcM+RXxndPxiZ2rd76k6nyQLRZr2/B/sdi8pQ+Joafr5AH279L40dfokSUTp8O+AaqYjXWhblBWa2st2nc4fQ==", + "dev": true, + "requires": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true + } + } + }, + "handlebars-utils": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/handlebars-utils/-/handlebars-utils-1.0.6.tgz", + "integrity": "sha512-d5mmoQXdeEqSKMtQQZ9WkiUcO1E3tPbWxluCK9hVgIDPzQa9WsKo3Lbe/sGflTe7TomHEeZaOgwIkyIr1kfzkw==", + "dev": true, + "requires": { + "kind-of": "^6.0.0", + "typeof-article": "^0.1.1" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -6221,8 +7025,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "optional": true + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "has-value": { "version": "1.0.0", @@ -6268,6 +7071,57 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "helper-date": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/helper-date/-/helper-date-1.0.1.tgz", + "integrity": "sha512-wU3VOwwTJvGr/w5rZr3cprPHO+hIhlblTJHD6aFBrKLuNbf4lAmkawd2iK3c6NbJEvY7HAmDpqjOFSI5/+Ey2w==", + "dev": true, + "requires": { + "date.js": "^0.3.1", + "handlebars-utils": "^1.0.4", + "moment": "^2.18.1" + } + }, + "helper-markdown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/helper-markdown/-/helper-markdown-1.0.0.tgz", + "integrity": "sha512-AnDqMS4ejkQK0MXze7pA9TM3pu01ZY+XXsES6gEE0RmCGk5/NIfvTn0NmItfyDOjRAzyo9z6X7YHbHX4PzIvOA==", + "dev": true, + "requires": { + "handlebars-utils": "^1.0.2", + "highlight.js": "^9.12.0", + "remarkable": "^1.7.1" + } + }, + "helper-md": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/helper-md/-/helper-md-0.2.2.tgz", + "integrity": "sha1-wfWdflW7riM2L9ig6XFgeuxp1B8=", + "dev": true, + "requires": { + "ent": "^2.2.0", + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "remarkable": "^1.6.2" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "highlight.js": { + "version": "9.18.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz", + "integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg==", + "dev": true + }, "history": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz", @@ -6303,6 +7157,16 @@ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", "dev": true }, + "html-tag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-tag/-/html-tag-2.0.0.tgz", + "integrity": "sha512-XxzooSo6oBoxBEUazgjdXj7VwTn/iSTSZzTYKzYY6I916tkaYzypHxy+pbVU1h+0UQ9JlVf5XkNQyxOAiiQO1g==", + "dev": true, + "requires": { + "is-self-closing": "^1.0.1", + "kind-of": "^6.0.0" + } + }, "htmlparser2": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", @@ -6392,7 +7256,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "optional": true, "requires": { "minimatch": "^3.0.4" } @@ -6445,6 +7308,12 @@ "wrappy": "1" } }, + "info-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/info-symbol/-/info-symbol-0.1.0.tgz", + "integrity": "sha1-J4QdcoZ920JCzWEtecEGM4gcang=", + "dev": true + }, "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", @@ -6792,6 +7661,15 @@ "integrity": "sha1-pGnQVvl1aXxYyYxgI+sKp5r4laI=", "dev": true }, + "is-even": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-even/-/is-even-1.0.0.tgz", + "integrity": "sha1-drUFX7rY0pSoa2qUkBXhyXtxfAY=", + "dev": true, + "requires": { + "is-odd": "^0.1.2" + } + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -6888,6 +7766,15 @@ "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true }, + "is-odd": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-0.1.2.tgz", + "integrity": "sha1-vFc7XONx7yqtbm9JeZtyvvE5eKc=", + "dev": true, + "requires": { + "is-number": "^3.0.0" + } + }, "is-path-inside": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", @@ -6933,6 +7820,15 @@ "is-unc-path": "^1.0.0" } }, + "is-self-closing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-self-closing/-/is-self-closing-1.0.1.tgz", + "integrity": "sha512-E+60FomW7Blv5GXTlYee2KDrnG6srxF7Xt1SjrhWUGUEsTFIqY/nq2y3DaftCsgUMdh89V07IVfhY9KIJhLezg==", + "dev": true, + "requires": { + "self-closing-tags": "^1.0.1" + } + }, "is-set": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", @@ -7217,6 +8113,15 @@ "package-json": "^6.3.0" } }, + "lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", + "dev": true, + "requires": { + "set-getter": "^0.1.0" + } + }, "lazy-val": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.4.tgz", @@ -7465,6 +8370,12 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, "lodash.escape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", @@ -7494,6 +8405,35 @@ "integrity": "sha1-+4m2WpqAKBgz8LdHizpRBPiY67M=", "dev": true }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "requires": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "log-ok": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/log-ok/-/log-ok-0.1.1.tgz", + "integrity": "sha1-vqPdNqzQuKckDXhza1uXxlREozQ=", + "dev": true, + "requires": { + "ansi-green": "^0.1.1", + "success-symbol": "^0.1.0" + } + }, "log-symbols": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", @@ -7555,6 +8495,68 @@ } } }, + "log-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/log-utils/-/log-utils-0.2.1.tgz", + "integrity": "sha1-pMIXoN2aUFFdm5ICBgkas9TgMc8=", + "dev": true, + "requires": { + "ansi-colors": "^0.2.0", + "error-symbol": "^0.1.0", + "info-symbol": "^0.1.0", + "log-ok": "^0.1.1", + "success-symbol": "^0.1.0", + "time-stamp": "^1.0.1", + "warning-symbol": "^0.1.0" + }, + "dependencies": { + "ansi-colors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-0.2.0.tgz", + "integrity": "sha1-csMd4qDZoszQysMMyYI+6y9kNLU=", + "dev": true, + "requires": { + "ansi-bgblack": "^0.1.1", + "ansi-bgblue": "^0.1.1", + "ansi-bgcyan": "^0.1.1", + "ansi-bggreen": "^0.1.1", + "ansi-bgmagenta": "^0.1.1", + "ansi-bgred": "^0.1.1", + "ansi-bgwhite": "^0.1.1", + "ansi-bgyellow": "^0.1.1", + "ansi-black": "^0.1.1", + "ansi-blue": "^0.1.1", + "ansi-bold": "^0.1.1", + "ansi-cyan": "^0.1.1", + "ansi-dim": "^0.1.1", + "ansi-gray": "^0.1.1", + "ansi-green": "^0.1.1", + "ansi-grey": "^0.1.1", + "ansi-hidden": "^0.1.1", + "ansi-inverse": "^0.1.1", + "ansi-italic": "^0.1.1", + "ansi-magenta": "^0.1.1", + "ansi-red": "^0.1.1", + "ansi-reset": "^0.1.1", + "ansi-strikethrough": "^0.1.1", + "ansi-underline": "^0.1.1", + "ansi-white": "^0.1.1", + "ansi-yellow": "^0.1.1", + "lazy-cache": "^2.0.1" + } + } + } + }, + "logging-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/logging-helpers/-/logging-helpers-1.0.0.tgz", + "integrity": "sha512-qyIh2goLt1sOgQQrrIWuwkRjUx4NUcEqEGAcYqD8VOnOC6ItwkrVE8/tA4smGpjzyp4Svhc6RodDp9IO5ghpyA==", + "dev": true, + "requires": { + "isobject": "^3.0.0", + "log-utils": "^0.2.1" + } + }, "lolex": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lolex/-/lolex-3.1.0.tgz", @@ -7575,24 +8577,6 @@ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "dev": true }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - }, - "dependencies": { - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -7781,7 +8765,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -7791,7 +8774,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "optional": true, "requires": { "minipass": "^2.9.0" } @@ -8351,6 +9333,12 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", "dev": true }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -8431,7 +9419,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "optional": true, "requires": { "abbrev": "1", "osenv": "^0.1.4" @@ -8474,7 +9461,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "optional": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } @@ -8502,14 +9488,12 @@ "npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "optional": true + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, "npm-packlist": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "optional": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1", @@ -8520,7 +9504,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -8790,8 +9773,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "optional": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "1.4.0", @@ -8811,7 +9793,6 @@ "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "optional": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -9246,12 +10227,6 @@ "event-stream": "=3.3.4" } }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -9753,6 +10728,42 @@ "rc": "^1.2.8" } }, + "relative": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/relative/-/relative-3.0.2.tgz", + "integrity": "sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8=", + "dev": true, + "requires": { + "isobject": "^2.0.0" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "remarkable": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-1.7.4.tgz", + "integrity": "sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==", + "dev": true, + "requires": { + "argparse": "^1.0.10", + "autolinker": "~0.28.0" + } + }, "remove-bom-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", @@ -10031,6 +11042,12 @@ "object-assign": "^4.1.1" } }, + "self-closing-tags": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/self-closing-tags/-/self-closing-tags-1.0.1.tgz", + "integrity": "sha512-7t6hNbYMxM+VHXTgJmxwgZgLGktuXtVVD5AivWzNTdJBM4DBjnDKDzkf2SrNjihaArpeJYNjxkELBu1evI4lQA==", + "dev": true + }, "semver": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", @@ -10208,6 +11225,15 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-getter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", + "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", + "dev": true, + "requires": { + "to-object-path": "^0.3.0" + } + }, "set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -11150,6 +12176,12 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, + "striptags": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.1.tgz", + "integrity": "sha1-yMPn/db7S7OjKjt1LltePjgJPr0=", + "dev": true + }, "styled-components": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.1.0.tgz", @@ -11182,6 +12214,12 @@ "resolved": "https://registry.npmjs.org/subscribableevent/-/subscribableevent-1.0.1.tgz", "integrity": "sha512-rFXit43+QoyWRUV/aTiYMUUJl71xmDcxED2cza3PEED75pgHTofMyPcPQrnHzyPqZh+kazEd36eydUKbOZBCpQ==" }, + "success-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/success-symbol/-/success-symbol-0.1.0.tgz", + "integrity": "sha1-JAIuSG878c3KCUKDt2nEctO3KJc=", + "dev": true + }, "sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -11291,7 +12329,6 @@ "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -11306,7 +12343,6 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "optional": true, "requires": { "minimist": "^1.2.5" } @@ -11379,6 +12415,16 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "through2-filter": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", @@ -11453,6 +12499,12 @@ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" }, + "to-gfm-code-block": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-gfm-code-block/-/to-gfm-code-block-0.1.1.tgz", + "integrity": "sha1-JdBFpfrlUxielje1kJANpzLYqoI=", + "dev": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -11707,6 +12759,26 @@ "is-typedarray": "^1.0.0" } }, + "typeof-article": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/typeof-article/-/typeof-article-0.1.1.tgz", + "integrity": "sha1-nwfnM8P7tkb/qeYcCN66zUYOBq8=", + "dev": true, + "requires": { + "kind-of": "^3.1.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, "typescript": { "version": "3.9.7", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", @@ -11719,6 +12791,13 @@ "integrity": "sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==", "dev": true }, + "uglify-js": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", + "integrity": "sha512-Esj5HG5WAyrLIdYU74Z3JdG2PxdIusvj6IWHMtlyESxc7kcDz7zYlYjpnSokn1UbpV0d/QX9fan7gkCNd/9BQA==", + "dev": true, + "optional": true + }, "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", @@ -12120,6 +13199,12 @@ "loose-envify": "^1.0.0" } }, + "warning-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/warning-symbol/-/warning-symbol-0.1.0.tgz", + "integrity": "sha1-uzHdEbeg+dZ6su2V9Fe2WCW7rSE=", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -12272,6 +13357,12 @@ "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=", "dev": true }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true + }, "y18n": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", @@ -12281,8 +13372,7 @@ "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "optional": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "yargs": { "version": "15.4.1", @@ -12632,6 +13722,12 @@ "fd-slicer": "~1.1.0" } }, + "year": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/year/-/year-0.2.1.tgz", + "integrity": "sha1-QIOuUgoxiyPshgN/MADLiSvfm7A=", + "dev": true + }, "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", diff --git a/gui/package.json b/gui/package.json index ff29dcbef0..927db17ba9 100644 --- a/gui/package.json +++ b/gui/package.json @@ -13,10 +13,12 @@ "license": "GPL-3.0", "dependencies": { "argv-split": "^2.0.1", + "@grpc/grpc-js": "^1.1.2", "connected-react-router": "^6.8.0", "d3-geo-projection": "^2.7.0", "electron-log": "^4.1.1", "gettext-parser": "^4.0.3", + "google-protobuf": "^4.0.0-rc.2", "history": "^4.6.1", "jsonrpc-lite": "^2.0.7", "linux-app-list": "^1.0.1", @@ -47,6 +49,7 @@ "@types/d3-geo": "^1.11.0", "@types/enzyme": "^3.1.15", "@types/enzyme-adapter-react-16": "^1.0.3", + "@types/google-protobuf": "^3.7.2", "@types/mkdirp": "^1.0.0", "@types/mocha": "^5.2.6", "@types/node": "^10.12.3", @@ -81,6 +84,8 @@ "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-react": "^7.18.3", "gettext-extractor": "^3.5.2", + "grpc-tools": "^1.9.1", + "grpc_tools_node_protoc_ts": "^4.1.0", "gulp": "^4.0.2", "gulp-inject-string": "^1.1.2", "gulp-typescript": "^5.0.1", @@ -94,6 +99,7 @@ "scripts": { "postinstall": "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true electron-builder install-app-deps", "build": "gulp build", + "build-proto": "gulp build-proto", "lint": "eslint --ext tsx,ts .", "format": "prettier \"**/*.{js,css,ts,tsx}\" --write", "develop": "gulp develop", diff --git a/gui/scripts/build-proto.sh b/gui/scripts/build-proto.sh new file mode 100755 index 0000000000..a6cf6a5d94 --- /dev/null +++ b/gui/scripts/build-proto.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR" + +NODE_MODULES_DIR="$(cd ../node_modules/.bin && pwd)" +PROTO_DIR="../../mullvad-management-interface/proto" +PROTO_FILENAME="management_interface.proto" +DESTINATION_DIR="../build/src/main/management_interface" +TYPES_DESTINATION_DIR="../src/main/management_interface" + +TS_PROTOC_PLUGIN="$NODE_MODULES_DIR/protoc-gen-ts" +if [[ "$(uname -s)" == "MINGW"* ]]; then + TS_PROTOC_PLUGIN="$TS_PROTOC_PLUGIN.cmd" +fi + +mkdir -p $DESTINATION_DIR +mkdir -p $TYPES_DESTINATION_DIR + +"$NODE_MODULES_DIR/grpc_tools_node_protoc" \ + --js_out=import_style=commonjs,binary:$DESTINATION_DIR \ + --grpc_out=generate_package_definition:$DESTINATION_DIR \ + --proto_path=$PROTO_DIR \ + $PROTO_DIR/$PROTO_FILENAME + +"$NODE_MODULES_DIR/grpc_tools_node_protoc" \ + --plugin=protoc-gen-ts=$TS_PROTOC_PLUGIN \ + --ts_out=generate_package_definition:$TYPES_DESTINATION_DIR \ + --proto_path=$PROTO_DIR \ + $PROTO_DIR/$PROTO_FILENAME + diff --git a/gui/src/main/daemon-rpc.ts b/gui/src/main/daemon-rpc.ts index c3d3fe33a6..ff755e2a36 100644 --- a/gui/src/main/daemon-rpc.ts +++ b/gui/src/main/daemon-rpc.ts @@ -1,324 +1,75 @@ +import * as grpc from '@grpc/grpc-js'; +import { + BoolValue, + StringValue, + UInt32Value, +} from 'google-protobuf/google/protobuf/wrappers_pb.js'; +import log from 'electron-log'; +import { Empty } from 'google-protobuf/google/protobuf/empty_pb.js'; +import { promisify } from 'util'; import { AccountToken, + Constraint, + IRelayListCountry, + IRelayListCity, + IRelayListHostname, + IWireguardTunnelData, + IBridgeConstraints, + IWireguardConstraints, + ITunnelOptions, + IOpenVpnConstraints, + IRelayList, + IShadowsocksEndpointData, + RelayProtocol, BridgeSettings, + FirewallPolicyError, BridgeState, - DaemonEvent, - IAccountData, - IAppVersionInfo, ILocation, - IRelayList, - ISettings, - IWireguardPublicKey, + IAppVersionInfo, + IAccountData, + IOpenVpnTunnelData, + TunnelState, + AfterDisconnect, + IErrorState, + ErrorStateCause, + TunnelParameterError, + ITunnelStateRelayInfo, + TunnelType, + IProxyEndpoint, + ProxyType, KeygenEvent, + IWireguardPublicKey, + ISettings, + ConnectionConfig, + DaemonEvent, + RelaySettings, RelaySettingsUpdate, - TunnelState, - VoucherErrorCode, + RelayLocation, + ProxySettings, VoucherResponse, + TunnelProtocol, } from '../shared/daemon-rpc-types'; -import { CommunicationError, InvalidAccountError, NoDaemonError } from './errors'; -import JsonRpcClient, { - RemoteError as JsonRpcRemoteError, - SocketTransport, - TimeOutError as JsonRpcTimeOutError, -} from './jsonrpc-client'; -import { camelCaseToSnakeCase, snakeCaseToCamelCase } from './transform-object-keys'; - -import { validate } from 'validated/object'; -import { - arrayOf, - boolean, - enumeration, - maybe, - Node as SchemaNode, - number, - object, - oneOf, - partialObject, - string, -} from 'validated/schema'; - -const locationSchema = maybe( - partialObject({ - ipv4: maybe(string), - ipv6: maybe(string), - country: string, - city: maybe(string), - latitude: number, - longitude: number, - mullvad_exit_ip: boolean, - hostname: maybe(string), - bridge_hostname: maybe(string), - }), -); - -const constraint = <T>(constraintValue: SchemaNode<T>) => { - return oneOf( - string, // any - object({ - only: constraintValue, - }), - ); -}; -const locationConstraintSchema = constraint( - oneOf( - object({ - hostname: arrayOf(string), - }), - object({ - city: arrayOf(string), - }), - object({ - country: string, - }), - ), -); - -const customTunnelEndpointSchema = oneOf( - object({ - openvpn: object({ - endpoint: object({ - address: string, - protocol: enumeration('udp', 'tcp'), - }), - username: string, - password: string, - }), - }), - object({ - wireguard: object({ - tunnel: object({ - private_key: string, - addresses: arrayOf(string), - }), - peer: object({ - public_key: string, - allowed_ips: arrayOf(string), - endpoint: string, - }), - ipv4_gateway: string, - ipv6_gateway: maybe(string), - }), - }), -); - -const relaySettingsSchema = oneOf( - object({ - normal: partialObject({ - location: locationConstraintSchema, - tunnel_protocol: constraint(enumeration('wireguard', 'openvpn')), - wireguard_constraints: partialObject({ - port: constraint(number), - }), - openvpn_constraints: partialObject({ - port: constraint(number), - protocol: constraint(enumeration('udp', 'tcp')), - }), - }), - }), - object({ - custom_tunnel_endpoint: partialObject({ - host: string, - config: customTunnelEndpointSchema, - }), - }), -); +import * as managementInterface from './management_interface/management_interface_grpc_pb'; +import * as grpcTypes from './management_interface/management_interface_pb'; +import { CommunicationError, InvalidAccountError } from './errors'; +import consumePromise from '../shared/promise'; -const relayListSchema = partialObject({ - countries: arrayOf( - partialObject({ - name: string, - code: string, - cities: arrayOf( - partialObject({ - name: string, - code: string, - latitude: number, - longitude: number, - relays: arrayOf( - partialObject({ - hostname: string, - ipv4_addr_in: string, - include_in_country: boolean, - active: boolean, - weight: number, - bridges: maybe( - partialObject({ - shadowsocks: arrayOf( - object({ - port: number, - cipher: string, - password: string, - protocol: enumeration('tcp', 'udp'), - }), - ), - }), - ), - tunnels: maybe( - partialObject({ - openvpn: arrayOf( - partialObject({ - port: number, - protocol: string, - }), - ), - wireguard: arrayOf( - partialObject({ - port_ranges: arrayOf(arrayOf(number)), - public_key: string, - }), - ), - }), - ), - }), - ), - }), - ), - }), - ), -}); - -const openVpnProxySchema = maybe( - oneOf( - object({ - local: partialObject({ - port: number, - peer: string, - }), - }), - object({ - remote: partialObject({ - address: string, - auth: maybe( - partialObject({ - username: string, - password: string, - }), - ), - }), - }), - object({ - shadowsocks: partialObject({ - peer: string, - password: string, - cipher: string, - }), - }), - ), -); +const NETWORK_CALL_TIMEOUT = 10000; +const CHANNEL_STATE_TIMEOUT = 1000 * 60 * 60; -const bridgeSettingsSchema = oneOf( - partialObject({ normal: partialObject({ location: locationConstraintSchema }) }), - partialObject({ custom: openVpnProxySchema }), +const ManagementServiceClient = grpc.makeClientConstructor( + // @ts-ignore + managementInterface['mullvad_daemon.management_interface.ManagementService'], + 'ManagementService', ); -const tunnelOptionsSchema = partialObject({ - openvpn: partialObject({ - mssfix: maybe(number), - }), - wireguard: partialObject({ - mtu: maybe(number), - // only relevant on linux - fmwark: maybe(number), - }), - generic: partialObject({ - enable_ipv6: boolean, - }), -}); - -const accountDataSchema = partialObject({ - expiry: string, -}); - -const voucherResponseSchema = partialObject({ - new_expiry: string, -}); - -const tunnelStateSchema = oneOf( - object({ - state: enumeration('disconnecting'), - details: enumeration('nothing', 'block', 'reconnect'), - }), - object({ - state: enumeration('connecting', 'connected'), - details: object({ - endpoint: partialObject({ - address: string, - protocol: enumeration('tcp', 'udp'), - tunnel_type: enumeration('wireguard', 'openvpn'), - proxy: maybe( - partialObject({ - address: string, - protocol: enumeration('tcp', 'udp'), - proxy_type: enumeration('shadowsocks', 'custom'), - }), - ), - }), - location: maybe(locationSchema), - }), - }), - object({ - state: enumeration('error'), - details: object({ - block_failure: maybe( - object({ - reason: enumeration('generic', 'locked'), - details: maybe( - object({ - name: string, - pid: number, - }), - ), - }), - ), - cause: oneOf( - object({ - reason: enumeration( - 'ipv6_unavailable', - 'set_dns_error', - 'start_tunnel_error', - 'is_offline', - 'tap_adapter_problem', - ), - }), - object({ - reason: enumeration('set_firewall_policy_error'), - details: object({ - reason: enumeration('generic', 'locked'), - details: maybe( - object({ - name: string, - pid: number, - }), - ), - }), - }), - object({ - reason: enumeration('auth_failed'), - details: maybe(string), - }), - object({ - reason: enumeration('tunnel_parameter_error'), - details: enumeration( - 'no_matching_relay', - 'no_matching_bridge_relay', - 'no_wireguard_key', - 'custom_tunnel_host_resultion_error', - ), - }), - ), - }), - }), - object({ - state: enumeration('connected', 'connecting', 'disconnected'), - }), +const noConnectionError = new Error('No connection established to daemon'); +const configNotSupported = new Error('Setting custom settings is not supported'); +const invalidErrorStateCause = new Error( + 'VPN_PERMISSION_DENIED is not a valid error state cause on desktop', ); -const appVersionInfoSchema = partialObject({ - supported: boolean, - suggested_upgrade: maybe(string), -}); - export class ConnectionObserver { constructor(private openHandler: () => void, private closeHandler: (error?: Error) => void) {} @@ -338,7 +89,7 @@ export class ConnectionObserver { export class SubscriptionListener<T> { // Only meant to be used by DaemonRpc // @internal - public subscriptionId?: string | number; + public subscriptionId?: number; constructor( private eventHandler: (payload: T) => void, @@ -358,328 +109,1130 @@ export class SubscriptionListener<T> { } } -const settingsSchema = partialObject({ - account_token: maybe(string), - allow_lan: boolean, - auto_connect: boolean, - block_when_disconnected: boolean, - show_beta_releases: boolean, - bridge_settings: bridgeSettingsSchema, - bridge_state: enumeration('on', 'auto', 'off'), - relay_settings: relaySettingsSchema, - tunnel_options: tunnelOptionsSchema, -}); +export class ResponseParseError extends Error { + constructor(message: string) { + super(message); + } +} -const wireguardPublicKey = object({ - key: string, - created: string, -}); +type CallFunctionArgument<T, R> = + | ((arg: T, callback: (error: Error | null, result: R) => void) => void) + | undefined; -const keygenEventSchema = oneOf( - enumeration('too_many_keys', 'generation_failure'), - object({ - new_key: object({ - key: string, - created: string, - }), - }), -); +export class DaemonRpc { + constructor(connectionParams: string) { + this.client = (new ManagementServiceClient( + connectionParams, + grpc.credentials.createInsecure(), + this.channelOptions(), + ) as unknown) as managementInterface.ManagementServiceClient; + } -const daemonEventSchema = oneOf( - object({ - tunnel_state: tunnelStateSchema, - }), - object({ - settings: settingsSchema, - }), - object({ - relay_list: relayListSchema, - }), - object({ - wireguard_key: keygenEventSchema, - }), - object({ - app_version_info: appVersionInfoSchema, - }), -); + private client: managementInterface.ManagementServiceClient; + private isConnected = false; + private connectionObservers: ConnectionObserver[] = []; + private nextSubscriptionId = 0; + private subscriptions: Map<number, grpc.ClientReadableStream<grpcTypes.DaemonEvent>> = new Map(); + private reconnectionTimeout?: number; -export class ResponseParseError extends Error { - constructor(message: string, private validationErrorValue?: Error) { - super(message); + private subscriptionId(): number { + const current = this.nextSubscriptionId; + this.nextSubscriptionId += 1; + return current; } - get validationError(): Error | undefined { - return this.validationErrorValue; + private deadlineFromNow() { + return Date.now() + NETWORK_CALL_TIMEOUT; } -} -// Timeout used for RPC calls that do networking -const NETWORK_CALL_TIMEOUT = 10000; + private channelStateTimeout(): number { + return Date.now() + CHANNEL_STATE_TIMEOUT; + } -export class DaemonRpc { - private transport = new JsonRpcClient(new SocketTransport()); + private callEmpty<R>(fn: CallFunctionArgument<Empty, R>): Promise<R> { + return this.call<Empty, R>(fn, new Empty()); + } + + private callString<R>(fn: CallFunctionArgument<StringValue, R>, value?: string): Promise<R> { + const googleString = new StringValue(); + + if (value !== undefined) { + googleString.setValue(value); + } + + return this.call<StringValue, R>(fn, googleString); + } + + private callBool<R>(fn: CallFunctionArgument<BoolValue, R>, value?: boolean): Promise<R> { + const googleBool = new BoolValue(); + + if (value !== undefined) { + googleBool.setValue(value); + } + + return this.call<BoolValue, R>(fn, googleBool); + } + + private callNumber<R>(fn: CallFunctionArgument<UInt32Value, R>, value?: number): Promise<R> { + const googleNumber = new UInt32Value(); + + if (value !== undefined) { + googleNumber.setValue(value); + } + + return this.call<UInt32Value, R>(fn, googleNumber); + } + + private call<T, R>(fn: CallFunctionArgument<T, R>, arg: T): Promise<R> { + if (fn && this.isConnected) { + return promisify<T, R>(fn.bind(this.client))(arg); + } else { + throw noConnectionError; + } + } + + public connect(): Promise<void> { + return new Promise((resolve, reject) => { + this.client.waitForReady(this.deadlineFromNow(), (error) => { + if (error) { + this.connectionObservers.forEach((observer) => observer.onClose(error)); + this.ensureConnectivity(); + reject(error); + } else { + this.reconnectionTimeout = undefined; + this.isConnected = true; + this.connectionObservers.forEach((observer) => observer.onOpen()); + this.setChannelCallback(); + resolve(); + } + }); + }); + } - public connect(connectionParams: { path: string }): Promise<void> { - return this.transport.connect(connectionParams); + private channelOptions(): grpc.ClientOptions { + return { + 'grpc.max_reconnect_backoff_ms': 3000, + 'grpc.initial_reconnect_backoff_ms': 3000, + 'grpc.keepalive_time_ms': Math.pow(2, 30), + 'grpc.keepalive_timeout_ms': Math.pow(2, 30), + }; + } + + private connectivityChangeCallback(timeoutErr?: Error) { + const channel = this.client.getChannel(); + const currentState = channel?.getConnectivityState(true); + log.debug(`GRPC Channel connectivity state changed to ${currentState}`); + if (channel) { + if (timeoutErr) { + this.setChannelCallback(currentState); + return; + } + const wasConnected = this.isConnected; + if (this.channelDisconnected(currentState)) { + this.connectionObservers.forEach((observer) => observer.onClose()); + this.isConnected = false; + // Try and reconnect in case + consumePromise( + this.connect().catch((error) => { + log.error(`Failed to reconnect - ${error}`); + }), + ); + this.setChannelCallback(currentState); + } else if (!wasConnected && currentState === grpc.connectivityState.READY) { + this.isConnected = true; + this.connectionObservers.forEach((observer) => observer.onOpen()); + this.setChannelCallback(currentState); + } + } + } + + private channelDisconnected(state: grpc.connectivityState): boolean { + return ( + (state === grpc.connectivityState.SHUTDOWN || + state === grpc.connectivityState.TRANSIENT_FAILURE || + state === grpc.connectivityState.IDLE) && + this.isConnected + ); + } + + private setChannelCallback(currentState?: grpc.connectivityState) { + const channel = this.client.getChannel(); + if (currentState === undefined && channel) { + currentState = channel?.getConnectivityState(false); + } + if (currentState) { + channel.watchConnectivityState(currentState, this.channelStateTimeout(), (error) => + this.connectivityChangeCallback(error), + ); + } + } + + // Since grpc.Channel.watchConnectivityState() isn't always running as intended, whenever the + // client fails to connect at first, `ensureConnectivity()` should be called so that it tries to + // check the connectivity state and nudge the client into connecting. + // `grpc.Channel.getConnectivityState(true)` should make it attempt to connect. + private ensureConnectivity() { + this.reconnectionTimeout = setTimeout(() => { + const lastState = this.client.getChannel().getConnectivityState(true); + if (this.channelDisconnected(lastState)) { + this.connectionObservers.forEach((observer) => observer.onClose()); + this.isConnected = false; + } + if (!this.isConnected) { + consumePromise( + this.connect().catch((error) => { + log.error(`Failed to reconnect - ${error}`); + }), + ); + } + }, 3000); } public disconnect() { - this.transport.disconnect(); + this.isConnected = false; + this.subscriptions.clear(); + this.client.close(); + if (this.reconnectionTimeout) { + clearTimeout(this.reconnectionTimeout); + } } public addConnectionObserver(observer: ConnectionObserver) { - this.transport.on('open', observer.onOpen).on('close', observer.onClose); + this.connectionObservers.push(observer); + const currentState = this.client.getChannel()?.getConnectivityState(true); + if ( + currentState === grpc.connectivityState.SHUTDOWN || + currentState === grpc.connectivityState.TRANSIENT_FAILURE || + currentState === grpc.connectivityState.IDLE + ) { + observer.onClose(); + } else { + observer.onOpen(); + } } public removeConnectionObserver(observer: ConnectionObserver) { - this.transport.off('open', observer.onOpen).off('close', observer.onClose); + const index = this.connectionObservers.indexOf(observer); + if (index !== -1) { + this.connectionObservers.splice(index, 1); + } } public async getAccountData(accountToken: AccountToken): Promise<IAccountData> { - let response; try { - response = await this.transport.send('get_account_data', accountToken, NETWORK_CALL_TIMEOUT); + const response = await this.callString<grpcTypes.AccountData>( + this.client.getAccountData, + accountToken, + ); + const expiry = response.getExpiry()!.toDate().toISOString(); + return { expiry }; } catch (error) { - if (error instanceof JsonRpcRemoteError) { + if (error.code) { switch (error.code) { - case -200: // Account doesn't exist + case grpc.status.UNAUTHENTICATED: throw new InvalidAccountError(); - case -32603: // Internal error + default: throw new CommunicationError(); } - } else if (error instanceof JsonRpcTimeOutError) { - throw new NoDaemonError(); - } else { - throw error; } - } - - try { - return validate(accountDataSchema, response); - } catch (error) { - throw new ResponseParseError('Invalid response from get_account_data', error); + throw error; } } public async getWwwAuthToken(): Promise<string> { - const response = await this.transport.send('get_www_auth_token'); - try { - return validate(string, response); - } catch (error) { - throw new ResponseParseError('Invalid response from get_www_auth_token', error); - } + const response = await this.callEmpty<StringValue>(this.client.getWwwAuthToken); + return response.getValue(); } public async submitVoucher(voucherCode: string): Promise<VoucherResponse> { try { - const response = await this.transport.send('submit_voucher', voucherCode); - const new_expiry = validate(voucherResponseSchema, response).new_expiry; - return { type: 'success', new_expiry }; + const response = await this.callString<grpcTypes.VoucherSubmission>( + this.client.submitVoucher, + voucherCode, + ); + + const secondsAdded = ensureExists( + response.getSecondsAdded(), + "no 'secondsAdded' field in voucher response", + ); + const newExpiry = ensureExists( + response.getNewExpiry(), + "no 'newExpiry' field in voucher response", + ) + .toDate() + .toISOString(); + return { + type: 'success', + secondsAdded, + newExpiry, + }; } catch (error) { - if (error instanceof JsonRpcRemoteError) { + if (error.code) { switch (error.code) { - case VoucherErrorCode.Invalid: + case grpc.status.NOT_FOUND: return { type: 'invalid' }; - case VoucherErrorCode.AlreadyUsed: + case grpc.status.RESOURCE_EXHAUSTED: return { type: 'already_used' }; } } + return { type: 'error' }; } - - return { type: 'error' }; } - public async getRelayLocations(): Promise<IRelayList> { - const response = await this.transport.send('get_relay_locations'); - try { - return snakeCaseToCamelCase(validate(relayListSchema, response)); - } catch (error) { - throw new ResponseParseError(`Invalid response from get_relay_locations: ${error}`, error); + public getRelayLocations(): Promise<IRelayList> { + if (this.isConnected) { + return new Promise((resolve, reject) => { + const relayLocations: IRelayListCountry[] = []; + const stream = this.client.getRelayLocations(new Empty()); + stream.on('data', (country: grpcTypes.RelayListCountry) => + relayLocations.push(convertFromRelayListCountry(country.toObject())), + ); + stream.on('end', () => resolve({ countries: relayLocations })); + stream.on('close', reject); + }); + } else { + throw noConnectionError; } } public async createNewAccount(): Promise<string> { - const response = await this.transport.send('create_new_account'); - return validate(string, response); + const response = await this.callEmpty<StringValue>(this.client.createNewAccount); + return response.getValue(); } public async setAccount(accountToken?: AccountToken): Promise<void> { - await this.transport.send('set_account', [accountToken]); + await this.callString(this.client.setAccount, accountToken); } + // TODO: Custom tunnel configurations are not supported by the GUI. public async updateRelaySettings(relaySettings: RelaySettingsUpdate): Promise<void> { - await this.transport.send('update_relay_settings', [camelCaseToSnakeCase(relaySettings)]); + if ('normal' in relaySettings) { + const settingsUpdate = relaySettings.normal; + const grpcRelaySettings = new grpcTypes.RelaySettingsUpdate(); + + const normalUpdate = new grpcTypes.NormalRelaySettingsUpdate(); + + if (settingsUpdate.tunnelProtocol) { + const tunnelTypeUpdate = new grpcTypes.TunnelTypeUpdate(); + tunnelTypeUpdate.setTunnelType( + convertToTunnelTypeConstraint(settingsUpdate.tunnelProtocol), + ); + normalUpdate.setTunnelType(tunnelTypeUpdate); + } + + if (settingsUpdate.location) { + normalUpdate.setLocation(convertToLocation(liftConstraint(settingsUpdate.location))); + } + + if (settingsUpdate.wireguardConstraints) { + normalUpdate.setWireguardConstraints( + convertToWireguardConstraints(settingsUpdate.wireguardConstraints), + ); + } + + if (settingsUpdate.openvpnConstraints) { + normalUpdate.setOpenvpnConstraints( + convertToOpenVpnConstraints(settingsUpdate.openvpnConstraints), + ); + } + + grpcRelaySettings.setNormal(normalUpdate); + await this.call<grpcTypes.RelaySettingsUpdate, Empty>( + this.client.updateRelaySettings, + grpcRelaySettings, + ); + } } public async setAllowLan(allowLan: boolean): Promise<void> { - await this.transport.send('set_allow_lan', [allowLan]); + await this.callBool(this.client.setAllowLan, allowLan); } public async setShowBetaReleases(showBetaReleases: boolean): Promise<void> { - await this.transport.send('set_show_beta_releases', [showBetaReleases]); + await this.callBool(this.client.setShowBetaReleases, showBetaReleases); } public async setEnableIpv6(enableIpv6: boolean): Promise<void> { - await this.transport.send('set_enable_ipv6', [enableIpv6]); + await this.callBool(this.client.setEnableIpv6, enableIpv6); } public async setBlockWhenDisconnected(blockWhenDisconnected: boolean): Promise<void> { - await this.transport.send('set_block_when_disconnected', [blockWhenDisconnected]); + await this.callBool(this.client.setBlockWhenDisconnected, blockWhenDisconnected); } public async setBridgeState(bridgeState: BridgeState): Promise<void> { - await this.transport.send('set_bridge_state', [bridgeState]); + const bridgeStateMap = { + auto: grpcTypes.BridgeState.State.AUTO, + on: grpcTypes.BridgeState.State.ON, + off: grpcTypes.BridgeState.State.OFF, + }; + + const grpcBridgeState = new grpcTypes.BridgeState(); + grpcBridgeState.setState(bridgeStateMap[bridgeState]); + await this.call<grpcTypes.BridgeState, Empty>(this.client.setBridgeState, grpcBridgeState); } public async setBridgeSettings(bridgeSettings: BridgeSettings): Promise<void> { - await this.transport.send('set_bridge_settings', [bridgeSettings]); + const grpcBridgeSettings = new grpcTypes.BridgeSettings(); + + if ('normal' in bridgeSettings) { + const normalSettings = convertToNormalBridgeSettings(bridgeSettings.normal); + grpcBridgeSettings.setNormal(normalSettings); + } + + if ('custom' in bridgeSettings) { + throw configNotSupported; + } + + await this.call<grpcTypes.BridgeSettings, Empty>( + this.client.setBridgeSettings, + grpcBridgeSettings, + ); } public async setOpenVpnMssfix(mssfix?: number): Promise<void> { - await this.transport.send('set_openvpn_mssfix', [mssfix]); + await this.callNumber(this.client.setOpenvpnMssfix, mssfix); } public async setWireguardMtu(mtu?: number): Promise<void> { - await this.transport.send('set_wireguard_mtu', [mtu]); + await this.callNumber(this.client.setWireguardMtu, mtu); } public async setAutoConnect(autoConnect: boolean): Promise<void> { - await this.transport.send('set_auto_connect', [autoConnect]); + await this.callBool(this.client.setAutoConnect, autoConnect); } public async connectTunnel(): Promise<void> { - await this.transport.send('connect'); + await this.callEmpty(this.client.connectTunnel); } public async disconnectTunnel(): Promise<void> { - await this.transport.send('disconnect'); + await this.callEmpty(this.client.disconnectTunnel); } public async reconnectTunnel(): Promise<void> { - await this.transport.send('reconnect'); + await this.callEmpty(this.client.reconnectTunnel); } - public async getLocation(): Promise<ILocation | undefined> { - const response = await this.transport.send('get_current_location', [], NETWORK_CALL_TIMEOUT); - try { - const validatedObject = validate(locationSchema, response); - if (validatedObject) { - return snakeCaseToCamelCase(validatedObject); - } else { - return undefined; - } - } catch (error) { - throw new ResponseParseError('Invalid response from get_current_location', error); - } + public async getLocation(): Promise<ILocation> { + const response = await this.callEmpty<grpcTypes.GeoIpLocation>(this.client.getCurrentLocation); + return response.toObject(); } public async getState(): Promise<TunnelState> { - const response = await this.transport.send('get_state'); - try { - return snakeCaseToCamelCase(validate(tunnelStateSchema, response)); - } catch (error) { - throw new ResponseParseError('Invalid response from get_state', error); - } + const response = await this.callEmpty<grpcTypes.TunnelState>(this.client.getTunnelState); + return convertFromTunnelState(response)!; } public async getSettings(): Promise<ISettings> { - const response = await this.transport.send('get_settings'); - try { - return snakeCaseToCamelCase(validate(settingsSchema, response)); - } catch (error) { - throw new ResponseParseError('Invalid response from get_settings', error); - } + const response = await this.callEmpty<grpcTypes.Settings>(this.client.getSettings); + return convertFromSettings(response)!; } - public async subscribeDaemonEventListener( - listener: SubscriptionListener<DaemonEvent>, - ): Promise<void> { - const subscriptionId = await this.transport.subscribe('daemon_event', (payload) => { - let daemonEvent: DaemonEvent; + public subscribeDaemonEventListener(listener: SubscriptionListener<DaemonEvent>) { + const call = this.isConnected && this.client.eventsListen(new Empty()); + if (!call) { + throw noConnectionError; + } + const subscriptionId = this.subscriptionId(); + listener.subscriptionId = subscriptionId; + this.subscriptions.set(subscriptionId, call); + call.on('data', (data: grpcTypes.DaemonEvent) => { try { - daemonEvent = snakeCaseToCamelCase(validate(daemonEventSchema, payload)); - } catch (error) { - listener.onError(new ResponseParseError('Invalid payload from daemon_event', error)); - return; + const daemonEvent = convertFromDaemonEvent(data); + listener.onEvent(daemonEvent); + } catch (err) { + listener.onError(err); } + }); - listener.onEvent(daemonEvent); + call.on('error', (error) => { + // if the subscription was cancelled by the client, there's no reason to + // invoke the onError handler + if ( + 'code' in error && + error['code'] === grpc.status.CANCELLED && + !this.subscriptions.has(subscriptionId) + ) { + return; + } + listener.onError(error); + this.removeSubscription(subscriptionId); }); + } - listener.subscriptionId = subscriptionId; + public unsubscribeDaemonEventListener(listener: SubscriptionListener<DaemonEvent>) { + const id = listener.subscriptionId; + if (id !== undefined) { + this.removeSubscription(id); + } } - public async unsubscribeDaemonEventListener( - listener: SubscriptionListener<DaemonEvent>, - ): Promise<void> { - if (listener.subscriptionId) { - return this.transport.unsubscribe('daemon_event', listener.subscriptionId); + private removeSubscription(id: number) { + const subscription = this.subscriptions.get(id); + if (subscription !== undefined) { + this.subscriptions.delete(id); + subscription.cancel(); } } public async getAccountHistory(): Promise<AccountToken[]> { - const response = await this.transport.send('get_account_history'); - try { - return validate(arrayOf(string), response); - } catch (error) { - throw new ResponseParseError('Invalid response from get_account_history'); - } + const response = await this.callEmpty<grpcTypes.AccountHistory>(this.client.getAccountHistory); + return response.toObject().tokenList; } public async removeAccountFromHistory(accountToken: AccountToken): Promise<void> { - await this.transport.send('remove_account_from_history', accountToken); + await this.callString(this.client.removeAccountFromHistory, accountToken); } public async getCurrentVersion(): Promise<string> { - const response = await this.transport.send('get_current_version'); - try { - return validate(string, response); - } catch (error) { - throw new ResponseParseError('Invalid response from get_current_version'); - } + const response = await this.callEmpty<StringValue>(this.client.getCurrentVersion); + return response.getValue(); } public async generateWireguardKey(): Promise<KeygenEvent> { - const response = await this.transport.send('generate_wireguard_key'); - try { - const validatedResponse = validate(keygenEventSchema, response); - switch (validatedResponse) { - case 'too_many_keys': - case 'generation_failure': - return validatedResponse; - default: - return snakeCaseToCamelCase(validatedResponse as object); + const response = await this.callEmpty<grpcTypes.KeygenEvent>(this.client.generateWireguardKey); + return convertFromKeygenEvent(response); + } + + public async getWireguardKey(): Promise<IWireguardPublicKey> { + const response = await this.callEmpty<grpcTypes.PublicKey>(this.client.getWireguardKey); + return { + created: response.getCreated()!.toDate().toISOString(), + key: convertFromWireguardKey(response.getKey()), + }; + } + + public async verifyWireguardKey(): Promise<boolean> { + const response = await this.callEmpty<BoolValue>(this.client.verifyWireguardKey); + return response.getValue(); + } + + public async getVersionInfo(): Promise<IAppVersionInfo> { + const response = await this.callEmpty<grpcTypes.AppVersionInfo>(this.client.getVersionInfo); + return response.toObject(); + } +} + +function liftConstraint<T>(constraint: Constraint<T> | undefined): T | undefined { + if (constraint !== undefined && constraint !== 'any') { + return constraint.only; + } + return undefined; +} + +function convertFromRelayListCountry( + country: grpcTypes.RelayListCountry.AsObject, +): IRelayListCountry { + return { + ...country, + cities: country.citiesList.map(convertFromRelayListCity), + }; +} + +function convertFromRelayListCity(city: grpcTypes.RelayListCity.AsObject): IRelayListCity { + return { + ...city, + relays: city.relaysList.map(convertFromRelayListRelay), + }; +} + +function convertFromRelayListRelay(relay: grpcTypes.Relay.AsObject): IRelayListHostname { + return { + ...relay, + tunnels: relay.tunnels && { + ...relay.tunnels, + openvpn: relay.tunnels.openvpnList.map(convertFromOpenvpnList), + wireguard: relay.tunnels.wireguardList.map(convertFromWireguardList), + }, + bridges: relay.bridges && { + shadowsocks: relay.bridges.shadowsocksList.map(convertFromShadowsocksList), + }, + }; +} + +function convertFromOpenvpnList( + openvpn: grpcTypes.OpenVpnEndpointData.AsObject, +): IOpenVpnTunnelData { + return { + ...openvpn, + protocol: convertFromTransportProtocol(openvpn.protocol), + }; +} + +function convertFromWireguardList( + wireguard: grpcTypes.WireguardEndpointData.AsObject, +): IWireguardTunnelData { + return { + ...wireguard, + portRanges: wireguard.portRangesList, + publicKey: convertFromWireguardKey(wireguard.publicKey), + }; +} + +function convertFromWireguardKey(publicKey: Uint8Array | string): string { + if (typeof publicKey === 'string') { + return publicKey; + } + return Buffer.from(publicKey).toString('base64'); +} + +function convertFromShadowsocksList( + shadowsocks: grpcTypes.ShadowsocksEndpointData.AsObject, +): IShadowsocksEndpointData { + return { + ...shadowsocks, + protocol: convertFromTransportProtocol(shadowsocks.protocol), + }; +} + +function convertFromTransportProtocol(protocol: grpcTypes.TransportProtocol): RelayProtocol { + const protocolMap: Record<grpcTypes.TransportProtocol, RelayProtocol> = { + [grpcTypes.TransportProtocol.TCP]: 'tcp', + [grpcTypes.TransportProtocol.UDP]: 'udp', + }; + return protocolMap[protocol]; +} + +function convertFromTunnelState(tunnelState: grpcTypes.TunnelState): TunnelState | undefined { + const tunnelStateObject = tunnelState.toObject(); + switch (tunnelState.getStateCase()) { + case grpcTypes.TunnelState.StateCase.STATE_NOT_SET: + return undefined; + case grpcTypes.TunnelState.StateCase.DISCONNECTED: + return { state: 'disconnected' }; + case grpcTypes.TunnelState.StateCase.DISCONNECTING: { + const detailsMap: Record<grpcTypes.AfterDisconnect, AfterDisconnect> = { + [grpcTypes.AfterDisconnect.NOTHING]: 'nothing', + [grpcTypes.AfterDisconnect.BLOCK]: 'block', + [grpcTypes.AfterDisconnect.RECONNECT]: 'reconnect', + }; + return ( + tunnelStateObject.disconnecting && { + state: 'disconnecting', + details: detailsMap[tunnelStateObject.disconnecting.afterDisconnect], + } + ); + } + case grpcTypes.TunnelState.StateCase.ERROR: + return ( + tunnelStateObject.error?.errorState && { + state: 'error', + details: convertFromTunnelStateError(tunnelStateObject.error.errorState), + } + ); + case grpcTypes.TunnelState.StateCase.CONNECTING: + return { + state: 'connecting', + details: + tunnelStateObject.connecting?.relayInfo && + convertFromTunnelStateRelayInfo(tunnelStateObject.connecting.relayInfo), + }; + case grpcTypes.TunnelState.StateCase.CONNECTED: { + const relayInfo = + tunnelStateObject.connected?.relayInfo && + convertFromTunnelStateRelayInfo(tunnelStateObject.connected.relayInfo); + return ( + relayInfo && { + state: 'connected', + details: relayInfo, + } + ); + } + } +} + +function convertFromTunnelStateError(state: grpcTypes.ErrorState.AsObject): IErrorState { + return { + ...state, + cause: convertFromTunnelStateErrorCause(state.cause, state), + blockFailure: state.blockingError + ? convertFromFirewallPolicyError(state.blockingError) + : undefined, + }; +} + +function convertFromTunnelStateErrorCause( + cause: grpcTypes.ErrorState.Cause, + state: grpcTypes.ErrorState.AsObject, +): ErrorStateCause { + switch (cause) { + case grpcTypes.ErrorState.Cause.IS_OFFLINE: + return { reason: 'is_offline' }; + case grpcTypes.ErrorState.Cause.SET_DNS_ERROR: + return { reason: 'set_dns_error' }; + case grpcTypes.ErrorState.Cause.IPV6_UNAVAILABLE: + return { reason: 'ipv6_unavailable' }; + case grpcTypes.ErrorState.Cause.START_TUNNEL_ERROR: + return { reason: 'start_tunnel_error' }; + case grpcTypes.ErrorState.Cause.TAP_ADAPTER_PROBLEM: + return { reason: 'tap_adapter_problem' }; + case grpcTypes.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR: + return { + reason: 'set_firewall_policy_error', + details: convertFromFirewallPolicyError(state.policyError!), + }; + case grpcTypes.ErrorState.Cause.AUTH_FAILED: + return { reason: 'auth_failed', details: state.authFailReason }; + case grpcTypes.ErrorState.Cause.TUNNEL_PARAMETER_ERROR: { + const parameterErrorMap: Record< + grpcTypes.ErrorState.GenerationError, + TunnelParameterError + > = { + [grpcTypes.ErrorState.GenerationError.NO_MATCHING_RELAY]: 'no_matching_relay', + [grpcTypes.ErrorState.GenerationError.NO_MATCHING_BRIDGE_RELAY]: 'no_matching_bridge_relay', + [grpcTypes.ErrorState.GenerationError.NO_WIREGUARD_KEY]: 'no_wireguard_key', + [grpcTypes.ErrorState.GenerationError.CUSTOM_TUNNEL_HOST_RESOLUTION_ERROR]: + 'custom_tunnel_host_resultion_error', + }; + return { reason: 'tunnel_parameter_error', details: parameterErrorMap[state.parameterError] }; + } + case grpcTypes.ErrorState.Cause.VPN_PERMISSION_DENIED: + // VPN_PERMISSION_DENIED is only ever created on Android + throw invalidErrorStateCause; + } +} + +function convertFromFirewallPolicyError( + error: grpcTypes.ErrorState.FirewallPolicyError.AsObject, +): FirewallPolicyError { + switch (error.type) { + case grpcTypes.ErrorState.FirewallPolicyError.ErrorType.GENERIC: + return { reason: 'generic' }; + case grpcTypes.ErrorState.FirewallPolicyError.ErrorType.LOCKED: { + const pid = error.lockPid; + const name = error.lockName; + return { reason: 'locked', details: pid && name ? { pid, name } : undefined }; + } + } +} + +function convertFromTunnelStateRelayInfo( + state: grpcTypes.TunnelStateRelayInfo.AsObject, +): ITunnelStateRelayInfo | undefined { + if (state.tunnelEndpoint) { + return { + ...state, + endpoint: { + ...state.tunnelEndpoint, + tunnelType: convertFromTunnelType(state.tunnelEndpoint.tunnelType), + protocol: convertFromTransportProtocol(state.tunnelEndpoint.protocol), + proxy: state.tunnelEndpoint.proxy && convertFromProxyEndpoint(state.tunnelEndpoint.proxy), + }, + }; + } + return undefined; +} + +function convertFromTunnelType(tunnelType: grpcTypes.TunnelType): TunnelType { + const tunnelTypeMap: Record<grpcTypes.TunnelType, TunnelType> = { + [grpcTypes.TunnelType.WIREGUARD]: 'wireguard', + [grpcTypes.TunnelType.OPENVPN]: 'openvpn', + }; + + return tunnelTypeMap[tunnelType]; +} + +function convertFromProxyEndpoint(proxyEndpoint: grpcTypes.ProxyEndpoint.AsObject): IProxyEndpoint { + const proxyTypeMap: Record<grpcTypes.ProxyType, ProxyType> = { + [grpcTypes.ProxyType.CUSTOM]: 'custom', + [grpcTypes.ProxyType.SHADOWSOCKS]: 'shadowsocks', + }; + + return { + ...proxyEndpoint, + protocol: convertFromTransportProtocol(proxyEndpoint.protocol), + proxyType: proxyTypeMap[proxyEndpoint.proxyType], + }; +} + +function convertFromSettings(settings: grpcTypes.Settings): ISettings | undefined { + const settingsObject = settings.toObject(); + const bridgeState = convertFromBridgeState(settingsObject.bridgeState!.state!); + const relaySettings = convertFromRelaySettings(settings.getRelaySettings())!; + const bridgeSettings = convertFromBridgeSettings(settingsObject.bridgeSettings!); + const tunnelOptions = convertFromTunnelOptions(settingsObject.tunnelOptions!); + return { + ...settings.toObject(), + bridgeState, + relaySettings, + bridgeSettings, + tunnelOptions, + }; +} + +function convertFromBridgeState(bridgeState: grpcTypes.BridgeState.State): BridgeState { + const bridgeStateMap: Record<grpcTypes.BridgeState.State, BridgeState> = { + [grpcTypes.BridgeState.State.AUTO]: 'auto', + [grpcTypes.BridgeState.State.ON]: 'on', + [grpcTypes.BridgeState.State.OFF]: 'off', + }; + + return bridgeStateMap[bridgeState]; +} + +function convertFromRelaySettings( + relaySettings?: grpcTypes.RelaySettings, +): RelaySettings | undefined { + if (relaySettings) { + switch (relaySettings.getEndpointCase()) { + case grpcTypes.RelaySettings.EndpointCase.ENDPOINT_NOT_SET: + return undefined; + case grpcTypes.RelaySettings.EndpointCase.CUSTOM: { + const custom = relaySettings.getCustom()?.toObject(); + const config = relaySettings.getCustom()?.getConfig(); + const connectionConfig = config && convertFromConnectionConfig(config); + return ( + custom && + connectionConfig && { + customTunnelEndpoint: { + ...custom, + config: connectionConfig, + }, + } + ); + } + case grpcTypes.RelaySettings.EndpointCase.NORMAL: { + const normal = relaySettings.getNormal()!; + const grpcLocation = normal.getLocation(); + const location = grpcLocation + ? { only: convertFromLocation(grpcLocation.toObject()) } + : 'any'; + const tunnelProtocol = convertFromTunnelTypeConstraint(normal.getTunnelType()!); + const openvpnConstraints = convertFromOpenVpnConstraints(normal.getOpenvpnConstraints()!); + const wireguardConstraints = convertFromWireguardConstraints( + normal.getWireguardConstraints()!, + ); + + return { + normal: { + location, + tunnelProtocol, + wireguardConstraints, + openvpnConstraints, + }, + }; } - } catch (error) { - throw new ResponseParseError(`Invalid response from generate_wireguard_key ${error}`); } + } else { + return undefined; } +} - public async getWireguardKey(): Promise<IWireguardPublicKey | undefined> { - const response = await this.transport.send('get_wireguard_key'); - try { - return validate(maybe(wireguardPublicKey), response) || undefined; - } catch (error) { - throw new ResponseParseError('Invalid response from get_wireguard_key'); +function convertFromBridgeSettings( + bridgeSettings: grpcTypes.BridgeSettings.AsObject, +): BridgeSettings { + const normalSettings = bridgeSettings.normal; + if (normalSettings) { + const grpcLocation = normalSettings.location; + const location = grpcLocation ? { only: convertFromLocation(grpcLocation) } : 'any'; + return { + normal: { + location, + }, + }; + } + + const customSettings = (settings: ProxySettings): BridgeSettings => { + return { custom: settings }; + }; + + const localSettings = bridgeSettings.local; + if (localSettings) { + return customSettings({ + port: localSettings?.port!, + peer: localSettings?.peer!, + }); + } + + const remoteSettings = bridgeSettings.remote; + if (remoteSettings) { + return customSettings({ + address: remoteSettings?.address!, + auth: { + ...remoteSettings?.auth!, + }, + }); + } + + const shadowsocksSettings = bridgeSettings.shadowsocks!; + return customSettings({ + peer: shadowsocksSettings.peer!, + password: shadowsocksSettings.password!, + cipher: shadowsocksSettings.cipher!, + }); +} + +function convertFromConnectionConfig( + connectionConfig: grpcTypes.ConnectionConfig, +): ConnectionConfig | undefined { + const connectionConfigObject = connectionConfig.toObject(); + switch (connectionConfig.getConfigCase()) { + case grpcTypes.ConnectionConfig.ConfigCase.CONFIG_NOT_SET: + return undefined; + case grpcTypes.ConnectionConfig.ConfigCase.WIREGUARD: + return ( + connectionConfigObject.wireguard && + connectionConfigObject.wireguard.tunnel && + connectionConfigObject.wireguard.peer && { + wireguard: { + ...connectionConfigObject.wireguard, + tunnel: { + privateKey: convertFromWireguardKey( + connectionConfigObject.wireguard.tunnel.privateKey, + ), + addresses: connectionConfigObject.wireguard.tunnel.addressesList, + }, + peer: { + ...connectionConfigObject.wireguard.peer, + addresses: connectionConfigObject.wireguard.peer.allowedIpsList, + publicKey: convertFromWireguardKey(connectionConfigObject.wireguard.peer.publicKey), + }, + }, + } + ); + case grpcTypes.ConnectionConfig.ConfigCase.OPENVPN: { + const [ip, port] = connectionConfigObject.openvpn!.address.split(':'); + return { + openvpn: { + ...connectionConfigObject.openvpn!, + endpoint: { + ip, + port: parseInt(port, 10), + protocol: convertFromTransportProtocol(connectionConfigObject.openvpn!.protocol), + }, + }, + }; } } +} - public async verifyWireguardKey(): Promise<boolean> { - const response = await this.transport.send('verify_wireguard_key'); - try { - return validate(boolean, response); - } catch (error) { - throw new ResponseParseError('Invalid response from verify_wireguard_key'); +function convertFromLocation(location: grpcTypes.RelayLocation.AsObject): RelayLocation { + if (location.hostname) { + return { hostname: [location.country, location.city, location.hostname] }; + } + if (location.city) { + return { city: [location.country, location.city] }; + } + + return { country: location.country }; +} + +function convertFromTunnelOptions(tunnelOptions: grpcTypes.TunnelOptions.AsObject): ITunnelOptions { + return { + openvpn: { + mssfix: tunnelOptions.openvpn!.mssfix, + }, + wireguard: { + mtu: tunnelOptions.wireguard!.mtu, + }, + generic: { + enableIpv6: tunnelOptions.generic!.enableIpv6, + }, + }; +} + +function convertFromDaemonEvent(data: grpcTypes.DaemonEvent): DaemonEvent { + const tunnelState = data.getTunnelState(); + if (tunnelState !== undefined) { + return { tunnelState: convertFromTunnelState(tunnelState)! }; + } + + const settings = data.getSettings(); + if (settings !== undefined) { + return { settings: convertFromSettings(settings)! }; + } + + const relayList = data.getRelayList(); + if (relayList !== undefined) { + return { + relayList: { + countries: relayList + .getCountriesList() + ?.map((country: grpcTypes.RelayListCountry) => + convertFromRelayListCountry(country.toObject()), + ), + }, + }; + } + + const keygenEvent = data.getKeyEvent(); + if (keygenEvent !== undefined) { + return { + wireguardKey: convertFromKeygenEvent(keygenEvent), + }; + } + + return { + appVersionInfo: data.getVersionInfo()!.toObject(), + }; +} + +function convertFromKeygenEvent(data: grpcTypes.KeygenEvent): KeygenEvent { + switch (data.getEvent()) { + case grpcTypes.KeygenEvent.KeygenEvent.TOO_MANY_KEYS: + return 'too_many_keys'; + case grpcTypes.KeygenEvent.KeygenEvent.NEW_KEY: { + const newKey = data.getNewKey(); + return newKey + ? { + newKey: { + created: newKey.getCreated()!.toDate().toISOString(), + key: convertFromWireguardKey(newKey.getKey()), + }, + } + : 'generation_failure'; } + case grpcTypes.KeygenEvent.KeygenEvent.GENERATION_FAILURE: + return 'generation_failure'; } +} - public async getVersionInfo(): Promise<IAppVersionInfo> { - const response = await this.transport.send('get_version_info', [], NETWORK_CALL_TIMEOUT); - try { - return snakeCaseToCamelCase(validate(appVersionInfoSchema, response)); - } catch (error) { - throw new ResponseParseError('Invalid response from get_version_info'); +function convertFromOpenVpnConstraints( + constraints: grpcTypes.OpenvpnConstraints, +): IOpenVpnConstraints { + const port = convertFromConstraint(constraints.getPort()); + let protocol: Constraint<RelayProtocol> = 'any'; + switch (constraints.getProtocol()?.getProtocol()) { + case grpcTypes.TransportProtocol.TCP: + protocol = { only: 'tcp' }; + break; + case grpcTypes.TransportProtocol.UDP: + protocol = { only: 'udp' }; + break; + } + + return { port, protocol }; +} + +function convertFromWireguardConstraints( + constraints: grpcTypes.WireguardConstraints, +): IWireguardConstraints { + const port = convertFromConstraint(constraints.getPort()); + return { port }; +} + +function convertFromTunnelTypeConstraint( + constraint: grpcTypes.TunnelTypeConstraint | undefined, +): Constraint<TunnelProtocol> { + switch (constraint?.getTunnelType()) { + case grpcTypes.TunnelType.WIREGUARD: { + return { only: 'wireguard' }; + } + case grpcTypes.TunnelType.OPENVPN: { + return { only: 'openvpn' }; } + default: { + return 'any'; + } + } +} + +function convertFromConstraint<T>(value: T | undefined): Constraint<T> { + if (value) { + return { only: value }; + } else { + return 'any'; + } +} + +function convertToNormalBridgeSettings( + constraints: IBridgeConstraints, +): grpcTypes.BridgeSettings.BridgeConstraints { + const normalBridgeSettings = new grpcTypes.BridgeSettings.BridgeConstraints(); + normalBridgeSettings.setLocation(convertToLocation(liftConstraint(constraints.location))); + + return normalBridgeSettings; +} + +function convertToLocation( + constraint: RelayLocation | undefined, +): grpcTypes.RelayLocation | undefined { + const location = new grpcTypes.RelayLocation(); + if (constraint && 'hostname' in constraint) { + const [countryCode, cityCode, hostname] = constraint.hostname; + location.setCountry(countryCode); + location.setCity(cityCode); + location.setHostname(hostname); + return location; + } else if (constraint && 'city' in constraint) { + location.setCountry(constraint.city[0]); + location.setCity(constraint.city[1]); + return location; + } else if (constraint && 'country' in constraint) { + location.setCountry(constraint.country); + return location; + } else { + return undefined; + } +} + +function convertToTunnelTypeConstraint( + constraint: Constraint<TunnelType>, +): grpcTypes.TunnelTypeConstraint | undefined { + const grpcConstraint = new grpcTypes.TunnelTypeConstraint(); + + if (constraint !== undefined && constraint !== 'any' && 'only' in constraint) { + switch (constraint.only) { + case 'wireguard': + grpcConstraint.setTunnelType(grpcTypes.TunnelType.WIREGUARD); + return grpcConstraint; + case 'openvpn': + grpcConstraint.setTunnelType(grpcTypes.TunnelType.OPENVPN); + return grpcConstraint; + } + } + return undefined; +} + +function convertToOpenVpnConstraints( + constraints: Partial<IOpenVpnConstraints> | undefined, +): grpcTypes.OpenvpnConstraints | undefined { + const openvpnConstraints = new grpcTypes.OpenvpnConstraints(); + if (constraints) { + const port = liftConstraint(constraints.port); + if (port) { + openvpnConstraints.setPort(port); + } + const protocol = liftConstraint(constraints.protocol); + if (protocol) { + const transportConstraint = new grpcTypes.TransportProtocolConstraint(); + transportConstraint.setProtocol(convertToTransportProtocol(protocol)); + openvpnConstraints.setProtocol(transportConstraint); + } + return openvpnConstraints; + } + + return undefined; +} + +function convertToWireguardConstraints( + constraint: Partial<IWireguardConstraints> | undefined, +): grpcTypes.WireguardConstraints | undefined { + if (constraint) { + const wireguardConstraints = new grpcTypes.WireguardConstraints(); + const port = liftConstraint(constraint.port); + if (port) { + wireguardConstraints.setPort(port); + } + return wireguardConstraints; + } + return undefined; +} + +function convertToTransportProtocol(protocol: RelayProtocol): grpcTypes.TransportProtocol { + switch (protocol) { + case 'udp': + return grpcTypes.TransportProtocol.UDP; + case 'tcp': + return grpcTypes.TransportProtocol.TCP; + } +} + +function ensureExists<T>(value: T | undefined, errorMessage: string): T { + if (value) { + return value; } + throw new ResponseParseError(errorMessage); } diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index 53ee113410..3ad81f453d 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -44,12 +44,7 @@ import consumePromise from '../shared/promise'; import { Scheduler } from '../shared/scheduler'; import AccountDataCache from './account-data-cache'; import { getOpenAtLogin, setOpenAtLogin } from './autostart'; -import { - ConnectionObserver, - DaemonRpc, - ResponseParseError, - SubscriptionListener, -} from './daemon-rpc'; +import { ConnectionObserver, DaemonRpc, SubscriptionListener } from './daemon-rpc'; import { InvalidAccountError } from './errors'; import Expectation from './expectation'; import GuiSettings from './gui-settings'; @@ -63,7 +58,7 @@ import WindowController from './window-controller'; const linuxSplitTunneling = process.platform === 'linux' && require('./linux-split-tunneling'); const DAEMON_RPC_PATH = - process.platform === 'win32' ? '//./pipe/Mullvad VPN' : '/var/run/mullvad-vpn'; + process.platform === 'win32' ? 'unix:////./pipe/Mullvad VPN' : 'unix:///var/run/mullvad-vpn'; const AUTO_CONNECT_FALLBACK_DELAY = 6000; @@ -95,7 +90,7 @@ class ApplicationMain { private windowController?: WindowController; private trayIconController?: TrayIconController; - private daemonRpc = new DaemonRpc(); + private daemonRpc = new DaemonRpc(DAEMON_RPC_PATH); private daemonEventListener?: SubscriptionListener<DaemonEvent>; private reconnectBackoff = new ReconnectionBackoff(); private connectedToDaemon = false; @@ -138,7 +133,6 @@ class ApplicationMain { }, wireguard: { mtu: undefined, - fwmark: undefined, }, }, }; @@ -310,7 +304,7 @@ class ApplicationMain { // Unsubscribe the event handler try { if (this.daemonEventListener) { - await this.daemonRpc.unsubscribeDaemonEventListener(this.daemonEventListener); + this.daemonRpc.unsubscribeDaemonEventListener(this.daemonEventListener); log.info('Unsubscribed from the daemon events'); } @@ -326,6 +320,8 @@ class ApplicationMain { if (process.platform === 'darwin' && this.windowController) { this.windowController.window.closable = true; } + + this.daemonRpc.disconnect(); } private detectLocale(): string { @@ -419,7 +415,7 @@ class ApplicationMain { // subscribe to events try { - this.daemonEventListener = await this.subscribeEvents(); + this.daemonEventListener = this.subscribeEvents(); } catch (error) { log.error(`Failed to subscribe: ${error.message}`); @@ -500,6 +496,9 @@ class ApplicationMain { }; private onDaemonDisconnected = (error?: Error) => { + if (this.daemonEventListener) { + this.daemonRpc.unsubscribeDaemonEventListener(this.daemonEventListener); + } // make sure we were connected before to distinguish between a failed attempt to reconnect and // connection loss. const wasConnected = this.connectedToDaemon; @@ -507,6 +506,9 @@ class ApplicationMain { // Reset the daemon event listener since it's going to be invalidated on disconnect this.daemonEventListener = undefined; + // TODO: GRPC doesn't set an error, but without an error, the UI won't be updated + error = error === undefined && wasConnected ? new Error('Connection to daemon lost') : error; + if (wasConnected) { this.connectedToDaemon = false; @@ -529,32 +531,24 @@ class ApplicationMain { } else { log.error(`Failed to connect to daemon: ${error.message}`); } - - this.reconnectToDaemon(); } else { log.info('Disconnected from the daemon'); } }; private connectToDaemon() { - consumePromise(this.daemonRpc.connect({ path: DAEMON_RPC_PATH })); - } - - private reconnectToDaemon() { - this.reconnectBackoff.attempt(() => { - this.connectToDaemon(); - }); + consumePromise(this.daemonRpc.connect()); } private recoverFromBootstrapError(_error?: Error) { // Attempt to reconnect to daemon if the program fails to fetch settings, tunnel state or // subscribe for RPC events. - this.daemonRpc.disconnect(); - - this.reconnectToDaemon(); + if (this.daemonEventListener) { + this.daemonRpc.unsubscribeDaemonEventListener(this.daemonEventListener); + } } - private async subscribeEvents(): Promise<SubscriptionListener<DaemonEvent>> { + private subscribeEvents(): SubscriptionListener<DaemonEvent> { const daemonEventListener = new SubscriptionListener( (daemonEvent: DaemonEvent) => { if ('tunnelState' in daemonEvent) { @@ -575,14 +569,11 @@ class ApplicationMain { }, (error: Error) => { log.error(`Cannot deserialize the daemon event: ${error.message}`); - - if (error instanceof ResponseParseError && error.validationError) { - log.error(error.validationError.message); - } + log.error(error.stack); }, ); - await this.daemonRpc.subscribeDaemonEventListener(daemonEventListener); + this.daemonRpc.subscribeDaemonEventListener(daemonEventListener); return daemonEventListener; } diff --git a/gui/src/renderer/components/RedeemVoucher.tsx b/gui/src/renderer/components/RedeemVoucher.tsx index daf5074603..7ca83c43d1 100644 --- a/gui/src/renderer/components/RedeemVoucher.tsx +++ b/gui/src/renderer/components/RedeemVoucher.tsx @@ -86,7 +86,7 @@ export function RedeemVoucherContainer(props: IRedeemVoucherProps) { if (response.type === 'success') { setValue(''); closeScheduler.schedule(() => { - updateAccountExpiry(response.new_expiry); + updateAccountExpiry(response.newExpiry); onSuccess?.(); }, 1000); } else { diff --git a/gui/src/shared/daemon-rpc-types.ts b/gui/src/shared/daemon-rpc-types.ts index 1f1779a4b0..78b6f60c17 100644 --- a/gui/src/shared/daemon-rpc-types.ts +++ b/gui/src/shared/daemon-rpc-types.ts @@ -46,14 +46,14 @@ export type ErrorStateCause = export type AfterDisconnect = 'nothing' | 'block' | 'reconnect'; -export type TunnelType = 'wireguard' | 'openvpn'; +export type TunnelType = 'any' | 'wireguard' | 'openvpn'; export function tunnelTypeToString(tunnel: TunnelType): string { switch (tunnel) { case 'wireguard': return 'WireGuard'; case 'openvpn': return 'OpenVPN'; - default: + case 'any': return ''; } } @@ -235,14 +235,16 @@ export interface IOpenVpnTunnelData { } export interface IWireguardTunnelData { - // Port ranges are an array of pairs, such as [[53,53], [10_000, 60_000]], - // which in this case translates that the specific tunnel can be connected on - // port 53 and ports 10'000 through 60'000. - portRanges: Array<[number, number]>; + portRanges: Array<IPortRange>; // Public key of the tunnel. publicKey: string; } +export interface IPortRange { + first: number; + last: number; +} + export interface IShadowsocksEndpointData { port: number; cipher: string; @@ -256,8 +258,6 @@ export interface ITunnelOptions { }; wireguard: { mtu?: number; - // Only relevant on Linux - fwmark?: number; }; generic: { enableIpv6: boolean; @@ -330,7 +330,7 @@ export interface ISocketAddress { } export type VoucherResponse = - | { type: 'success'; new_expiry: string } + | { type: 'success'; newExpiry: string; secondsAdded: number } | { type: 'invalid' | 'already_used' | 'error' }; export enum VoucherErrorCode { diff --git a/gui/tasks/scripts.js b/gui/tasks/scripts.js index 82896dac21..5ee33a9071 100644 --- a/gui/tasks/scripts.js +++ b/gui/tasks/scripts.js @@ -1,4 +1,5 @@ -const { src, dest } = require('gulp'); +const { exec } = require('child_process'); +const { src, dest, series } = require('gulp'); const ts = require('gulp-typescript'); const inject = require('gulp-inject-string'); const TscWatchClient = require('tsc-watch/client'); @@ -25,7 +26,13 @@ function compileScripts() { .pipe(dest('build')); } +function buildProto(callback) { + exec('bash ./scripts/build-proto.sh', (err) => callback(err)); +} + compileScripts.displayName = 'compile-scripts'; +buildProto.displayName = 'build-proto'; exports.build = compileScripts; +exports.buildProto = buildProto; exports.makeWatchCompiler = makeWatchCompiler; diff --git a/mullvad-cli/Cargo.toml b/mullvad-cli/Cargo.toml index 084be415fe..1746993bff 100644 --- a/mullvad-cli/Cargo.toml +++ b/mullvad-cli/Cargo.toml @@ -25,11 +25,13 @@ futures = "0.1" natord = "1.0.9" serde = "1.0" -mullvad-ipc-client = { path = "../mullvad-ipc-client" } mullvad-types = { path = "../mullvad-types" } mullvad-paths = { path = "../mullvad-paths" } talpid-types = { path = "../talpid-types" } -talpid-ipc = { path = "../talpid-ipc" } + +mullvad-management-interface = { path = "../mullvad-management-interface" } +futures03 = { package = "futures", version = "0.3", features = [ "compat" ]} +tokio = { version = "0.2", features = [ "io-util", "process", "rt-core", "rt-threaded", "stream"] } [target.'cfg(windows)'.build-dependencies] winres = "0.1" diff --git a/mullvad-cli/src/cmds/account.rs b/mullvad-cli/src/cmds/account.rs index e35699605c..ef5c117c41 100644 --- a/mullvad-cli/src/cmds/account.rs +++ b/mullvad-cli/src/cmds/account.rs @@ -1,9 +1,11 @@ -use crate::{new_rpc_client, Command, Result}; +use crate::{new_rpc_client, Command, Error, Result}; use clap::value_t_or_exit; -use mullvad_types::account::{AccountToken, VoucherError}; +use mullvad_management_interface::{types::Timestamp, Code}; +use mullvad_types::account::AccountToken; pub struct Account; +#[mullvad_management_interface::async_trait] impl Command for Account { fn name(&self) -> &'static str { "account" @@ -49,21 +51,21 @@ impl Command for Account { ) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { if let Some(set_matches) = matches.subcommand_matches("set") { let token = value_t_or_exit!(set_matches.value_of("token"), String); - self.set(Some(token)) + self.set(Some(token)).await } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get() + self.get().await } else if let Some(_matches) = matches.subcommand_matches("unset") { - self.set(None) + self.set(None).await } else if let Some(_matches) = matches.subcommand_matches("clear-history") { - self.clear_history() + self.clear_history().await } else if let Some(_matches) = matches.subcommand_matches("create") { - self.create() + self.create().await } else if let Some(matches) = matches.subcommand_matches("redeem") { let voucher = value_t_or_exit!(matches.value_of("voucher"), String); - self.redeem_voucher(voucher) + self.redeem_voucher(voucher).await } else { unreachable!("No account command given"); } @@ -71,9 +73,9 @@ impl Command for Account { } impl Account { - fn set(&self, token: Option<AccountToken>) -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.set_account(token.clone())?; + async fn set(&self, token: Option<AccountToken>) -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.set_account(token.clone().unwrap_or_default()).await?; if let Some(token) = token { println!("Mullvad account \"{}\" set", token); } else { @@ -82,45 +84,57 @@ impl Account { Ok(()) } - fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client()?; - let settings = rpc.get_settings()?; - if let Some(account_token) = settings.get_account_token() { - println!("Mullvad account: {}", account_token); - let expiry = rpc.get_account_data(account_token)?; - println!("Expires at : {}", expiry.expiry); + async fn get(&self) -> Result<()> { + let mut rpc = new_rpc_client().await?; + let settings = rpc.get_settings(()).await?.into_inner(); + if settings.account_token != "" { + println!("Mullvad account: {}", settings.account_token); + let expiry = rpc + .get_account_data(settings.account_token) + .await? + .into_inner(); + println!( + "Expires at : {}", + Self::format_expiry(&expiry.expiry.unwrap()) + ); } else { println!("No account configured"); } Ok(()) } - fn create(&self) -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.create_new_account()?; + async fn create(&self) -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.create_new_account(()).await?; println!("New account created!"); - self.get() + self.get().await } - fn redeem_voucher(&self, mut voucher: String) -> Result<()> { - let mut rpc = new_rpc_client()?; + async fn redeem_voucher(&self, mut voucher: String) -> Result<()> { + let mut rpc = new_rpc_client().await?; voucher.retain(|c| c.is_alphanumeric()); - match rpc.submit_voucher(voucher) { + match rpc.submit_voucher(voucher).await { Ok(submission) => { + let submission = submission.into_inner(); println!( "Added {} to the account", - Self::format_duration(submission.time_added) + Self::format_duration(submission.seconds_added) + ); + println!( + "New expiry date: {}", + Self::format_expiry(&submission.new_expiry.unwrap()) ); - println!("New expiry date: {}", submission.new_expiry); Ok(()) } Err(err) => { - eprintln!( - "Failed to submit voucher.\n{}", - VoucherError::from_rpc_error_code(Self::get_redeem_rpc_error_code(&err)) - ); - Err(err.into()) + match err.code() { + Code::NotFound | Code::ResourceExhausted => { + eprintln!("Failed to submit voucher: {}", err.message()); + } + _ => return Err(Error::GrpcClientError(err)), + } + std::process::exit(1); } } } @@ -138,16 +152,13 @@ impl Account { } } - fn get_redeem_rpc_error_code(error: &mullvad_ipc_client::Error) -> i64 { - match error.kind() { - mullvad_ipc_client::ErrorKind::JsonRpcError(ref rpc_error) => rpc_error.code.code(), - _ => 0, - } + fn format_expiry(expiry: &Timestamp) -> String { + chrono::NaiveDateTime::from_timestamp(expiry.seconds, expiry.nanos as u32).to_string() } - fn clear_history(&self) -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.clear_account_history()?; + async fn clear_history(&self) -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.clear_account_history(()).await?; println!("Removed account history and all associated keys"); Ok(()) } diff --git a/mullvad-cli/src/cmds/auto_connect.rs b/mullvad-cli/src/cmds/auto_connect.rs index 385a2de7b6..40f8f2bd9e 100644 --- a/mullvad-cli/src/cmds/auto_connect.rs +++ b/mullvad-cli/src/cmds/auto_connect.rs @@ -3,6 +3,7 @@ use clap::value_t_or_exit; pub struct AutoConnect; +#[mullvad_management_interface::async_trait] impl Command for AutoConnect { fn name(&self) -> &'static str { "auto-connect" @@ -27,12 +28,12 @@ impl Command for AutoConnect { ) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { if let Some(set_matches) = matches.subcommand_matches("set") { let auto_connect = value_t_or_exit!(set_matches.value_of("policy"), String); - self.set(auto_connect == "on") + self.set(auto_connect == "on").await } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get() + self.get().await } else { unreachable!("No auto-connect command given"); } @@ -40,16 +41,16 @@ impl Command for AutoConnect { } impl AutoConnect { - fn set(&self, auto_connect: bool) -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.set_auto_connect(auto_connect)?; + async fn set(&self, auto_connect: bool) -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.set_auto_connect(auto_connect).await?; println!("Changed auto-connect sharing setting"); Ok(()) } - fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client()?; - let auto_connect = rpc.get_settings()?.auto_connect; + async fn get(&self) -> Result<()> { + let mut rpc = new_rpc_client().await?; + let auto_connect = rpc.get_settings(()).await?.into_inner().auto_connect; println!("Autoconnect: {}", if auto_connect { "on" } else { "off" }); Ok(()) } diff --git a/mullvad-cli/src/cmds/beta_program.rs b/mullvad-cli/src/cmds/beta_program.rs index c4cbe6cf21..73a0ba15c7 100644 --- a/mullvad-cli/src/cmds/beta_program.rs +++ b/mullvad-cli/src/cmds/beta_program.rs @@ -3,6 +3,7 @@ use clap::value_t_or_exit; pub struct BetaProgram; +#[mullvad_management_interface::async_trait] impl Command for BetaProgram { fn name(&self) -> &'static str { "beta-program" @@ -24,11 +25,11 @@ impl Command for BetaProgram { .subcommand(clap::SubCommand::with_name("get").about("Get beta notifications setting")) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { ("get", Some(_)) => { - let mut rpc = new_rpc_client()?; - let settings = rpc.get_settings()?; + let mut rpc = new_rpc_client().await?; + let settings = rpc.get_settings(()).await?.into_inner(); let enabled_str = if settings.show_beta_releases { "on" } else { @@ -47,8 +48,8 @@ impl Command for BetaProgram { )); } - let mut rpc = new_rpc_client()?; - rpc.set_show_beta_releases(enable)?; + let mut rpc = new_rpc_client().await?; + rpc.set_show_beta_releases(enable).await?; println!("Beta program: {}", enable_str); Ok(()) diff --git a/mullvad-cli/src/cmds/block_when_disconnected.rs b/mullvad-cli/src/cmds/block_when_disconnected.rs index 57239963d7..3cb1baff5a 100644 --- a/mullvad-cli/src/cmds/block_when_disconnected.rs +++ b/mullvad-cli/src/cmds/block_when_disconnected.rs @@ -3,6 +3,7 @@ use clap::value_t_or_exit; pub struct BlockWhenDisconnected; +#[mullvad_management_interface::async_trait] impl Command for BlockWhenDisconnected { fn name(&self) -> &'static str { "always-require-vpn" @@ -27,12 +28,12 @@ impl Command for BlockWhenDisconnected { ) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { if let Some(set_matches) = matches.subcommand_matches("set") { let block_when_disconnected = value_t_or_exit!(set_matches.value_of("policy"), String); - self.set(block_when_disconnected == "on") + self.set(block_when_disconnected == "on").await } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get() + self.get().await } else { unreachable!("No block-when-disconnected command given"); } @@ -40,16 +41,21 @@ impl Command for BlockWhenDisconnected { } impl BlockWhenDisconnected { - fn set(&self, block_when_disconnected: bool) -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.set_block_when_disconnected(block_when_disconnected)?; + async fn set(&self, block_when_disconnected: bool) -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.set_block_when_disconnected(block_when_disconnected) + .await?; println!("Changed always require VPN setting"); Ok(()) } - fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client()?; - let block_when_disconnected = rpc.get_settings()?.block_when_disconnected; + async fn get(&self) -> Result<()> { + let mut rpc = new_rpc_client().await?; + let block_when_disconnected = rpc + .get_settings(()) + .await? + .into_inner() + .block_when_disconnected; println!( "Network traffic will be {} when the VPN is disconnected", if block_when_disconnected { diff --git a/mullvad-cli/src/cmds/bridge.rs b/mullvad-cli/src/cmds/bridge.rs index 0e5c339744..3118c09a39 100644 --- a/mullvad-cli/src/cmds/bridge.rs +++ b/mullvad-cli/src/cmds/bridge.rs @@ -1,13 +1,18 @@ use crate::{location, new_rpc_client, Command, Result}; use clap::value_t; -use mullvad_types::relay_constraints::{BridgeConstraints, BridgeSettings, BridgeState}; -use talpid_types::net::openvpn::{self, SHADOWSOCKS_CIPHERS}; +use mullvad_management_interface::types::{ + bridge_settings::{Type as BridgeSettingsType, *}, + bridge_state::State as BridgeStateType, + BridgeSettings, BridgeState, +}; +use talpid_types::net::openvpn::SHADOWSOCKS_CIPHERS; use std::net::{IpAddr, SocketAddr}; pub struct Bridge; +#[mullvad_management_interface::async_trait] impl Command for Bridge { fn name(&self) -> &'static str { "bridge" @@ -24,11 +29,11 @@ impl Command for Bridge { .subcommand(clap::SubCommand::with_name("list").about("List bridge relays")) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { - ("set", Some(set_matches)) => Self::handle_set(set_matches), - ("get", _) => Self::handle_get(), - ("list", _) => Self::list_bridge_relays(), + ("set", Some(set_matches)) => Self::handle_set(set_matches).await, + ("get", _) => Self::handle_get().await, + ("list", _) => Self::list_bridge_relays().await, _ => unreachable!("unhandled command"), } } @@ -145,64 +150,66 @@ fn create_set_state_subcommand() -> clap::App<'static, 'static> { } impl Bridge { - fn handle_set(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn handle_set(matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { ("location", Some(location_matches)) => { - Self::handle_set_bridge_location(location_matches) + Self::handle_set_bridge_location(location_matches).await } ("custom", Some(custom_matches)) => { - Self::handle_bridge_set_custom_settings(custom_matches) + Self::handle_bridge_set_custom_settings(custom_matches).await } - ("state", Some(set_matches)) => Self::handle_set_bridge_state(set_matches), + ("state", Some(set_matches)) => Self::handle_set_bridge_state(set_matches).await, _ => unreachable!("unhandled command"), } } - fn handle_get() -> Result<()> { - let mut rpc = new_rpc_client()?; - let settings = rpc.get_settings()?; - println!("Bridge state - {}", settings.get_bridge_state()); - match settings.bridge_settings { - BridgeSettings::Custom(proxy) => { - match proxy { - openvpn::ProxySettings::Local(local_proxy) => { - Self::print_local_proxy(&local_proxy) - } - openvpn::ProxySettings::Remote(remote_proxy) => { - Self::print_remote_proxy(&remote_proxy) - } - openvpn::ProxySettings::Shadowsocks(shadowsocks_proxy) => { - Self::print_shadowsocks_proxy(&shadowsocks_proxy) - } - }; + async fn handle_get() -> Result<()> { + let mut rpc = new_rpc_client().await?; + let settings = rpc.get_settings(()).await?.into_inner(); + Self::print_state(settings.bridge_state.unwrap()); + match settings.bridge_settings.unwrap().r#type.unwrap() { + BridgeSettingsType::Local(local_proxy) => Self::print_local_proxy(&local_proxy), + BridgeSettingsType::Remote(remote_proxy) => Self::print_remote_proxy(&remote_proxy), + BridgeSettingsType::Shadowsocks(shadowsocks_proxy) => { + Self::print_shadowsocks_proxy(&shadowsocks_proxy) } - BridgeSettings::Normal(constraints) => { - println!("Bridge constraints: {}", constraints); + BridgeSettingsType::Normal(constraints) => { + println!( + "Bridge constraints - {}", + location::format_location(constraints.location.as_ref()) + ); } }; Ok(()) } - fn handle_set_bridge_location(matches: &clap::ArgMatches<'_>) -> Result<()> { - let location = location::get_constraint(matches); - let mut rpc = new_rpc_client()?; - rpc.set_bridge_settings(BridgeSettings::Normal(BridgeConstraints { location }))?; + async fn handle_set_bridge_location(matches: &clap::ArgMatches<'_>) -> Result<()> { + let constraints = location::get_constraint(matches); + let mut rpc = new_rpc_client().await?; + rpc.set_bridge_settings(BridgeSettings { + r#type: Some(BridgeSettingsType::Normal(BridgeConstraints { + location: Some(constraints), + })), + }) + .await?; Ok(()) } - fn handle_set_bridge_state(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn handle_set_bridge_state(matches: &clap::ArgMatches<'_>) -> Result<()> { let state = match matches.value_of("state").unwrap() { - "auto" => BridgeState::Auto, - "on" => BridgeState::On, - "off" => BridgeState::Off, + "auto" => BridgeStateType::Auto as i32, + "on" => BridgeStateType::On as i32, + "off" => BridgeStateType::Off as i32, _ => unreachable!(), }; - let mut rpc = new_rpc_client()?; - rpc.set_bridge_state(state)?; + let mut rpc = new_rpc_client().await?; + rpc.set_bridge_state(BridgeState { state }).await?; Ok(()) } - fn handle_bridge_set_custom_settings(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn handle_bridge_set_custom_settings(matches: &clap::ArgMatches<'_>) -> Result<()> { + use talpid_types::net::openvpn; + if let Some(args) = matches.subcommand_matches("local") { let local_port = value_t!(args.value_of("local-port"), u16).unwrap_or_else(|e| e.exit()); @@ -211,19 +218,24 @@ impl Bridge { let remote_port = value_t!(args.value_of("remote-port"), u16).unwrap_or_else(|e| e.exit()); - let proxy = openvpn::LocalProxySettings { + let local_proxy = openvpn::LocalProxySettings { port: local_port, peer: SocketAddr::new(remote_ip, remote_port), }; - - let packed_proxy = openvpn::ProxySettings::Local(proxy); - + let prost_proxy = LocalProxySettings { + port: local_proxy.port as u32, + peer: local_proxy.peer.to_string(), + }; + let packed_proxy = openvpn::ProxySettings::Local(local_proxy); if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { panic!(error); } - let mut rpc = new_rpc_client()?; - rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy))?; + let mut rpc = new_rpc_client().await?; + rpc.set_bridge_settings(BridgeSettings { + r#type: Some(BridgeSettingsType::Local(prost_proxy)), + }) + .await?; } else if let Some(args) = matches.subcommand_matches("remote") { let remote_ip = value_t!(args.value_of("remote-ip"), IpAddr).unwrap_or_else(|e| e.exit()); @@ -239,20 +251,30 @@ impl Bridge { }), _ => None, }; + let prost_auth = auth.clone().map(|auth| RemoteProxyAuth { + username: auth.username.clone(), + password: auth.password.clone(), + }); let proxy = openvpn::RemoteProxySettings { address: SocketAddr::new(remote_ip, remote_port), auth, }; + let prost_proxy = RemoteProxySettings { + address: proxy.address.to_string(), + auth: prost_auth, + }; let packed_proxy = openvpn::ProxySettings::Remote(proxy); - if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { panic!(error); } - let mut rpc = new_rpc_client()?; - rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy))?; + let mut rpc = new_rpc_client().await?; + rpc.set_bridge_settings(BridgeSettings { + r#type: Some(BridgeSettingsType::Remote(prost_proxy)), + }) + .await?; } else if let Some(args) = matches.subcommand_matches("shadowsocks") { let remote_ip = value_t!(args.value_of("remote-ip"), IpAddr).unwrap_or_else(|e| e.exit()); @@ -266,15 +288,22 @@ impl Bridge { password, cipher, }; + let prost_proxy = ShadowsocksProxySettings { + peer: proxy.peer.to_string(), + password: proxy.password.clone(), + cipher: proxy.cipher.clone(), + }; let packed_proxy = openvpn::ProxySettings::Shadowsocks(proxy); - if let Err(error) = openvpn::validate_proxy_settings(&packed_proxy) { panic!(error); } - let mut rpc = new_rpc_client()?; - rpc.set_bridge_settings(BridgeSettings::Custom(packed_proxy))?; + let mut rpc = new_rpc_client().await?; + rpc.set_bridge_settings(BridgeSettings { + r#type: Some(BridgeSettingsType::Shadowsocks(prost_proxy)), + }) + .await?; } else { unreachable!("unhandled proxy type"); } @@ -283,17 +312,24 @@ impl Bridge { Ok(()) } - fn print_local_proxy(proxy: &openvpn::LocalProxySettings) { + fn print_state(state: BridgeState) { + let state = match BridgeStateType::from_i32(state.state).expect("unknown bridge state") { + BridgeStateType::Auto => "auto", + BridgeStateType::On => "on", + BridgeStateType::Off => "off", + }; + println!("Bridge state - {}", state); + } + + fn print_local_proxy(proxy: &LocalProxySettings) { println!("proxy: local"); println!(" local port: {}", proxy.port); - println!(" peer IP: {}", proxy.peer.ip()); - println!(" peer port: {}", proxy.peer.port()); + println!(" peer address: {}", proxy.peer); } - fn print_remote_proxy(proxy: &openvpn::RemoteProxySettings) { + fn print_remote_proxy(proxy: &RemoteProxySettings) { println!("proxy: remote"); - println!(" server IP: {}", proxy.address.ip()); - println!(" server port: {}", proxy.address.port()); + println!(" server address: {}", proxy.address); if let Some(ref auth) = proxy.auth { println!(" auth username: {}", auth.username); @@ -303,47 +339,43 @@ impl Bridge { } } - fn print_shadowsocks_proxy(proxy: &openvpn::ShadowsocksProxySettings) { + fn print_shadowsocks_proxy(proxy: &ShadowsocksProxySettings) { println!("proxy: Shadowsocks"); - println!(" peer IP: {}", proxy.peer.ip()); - println!(" peer port: {}", proxy.peer.port()); + println!(" peer address: {}", proxy.peer); println!(" password: {}", proxy.password); println!(" cipher: {}", proxy.cipher); } - fn list_bridge_relays() -> Result<()> { - let mut rpc = new_rpc_client()?; - let mut locations = rpc.get_relay_locations()?; + async fn list_bridge_relays() -> Result<()> { + let mut rpc = new_rpc_client().await?; + let mut locations = rpc.get_relay_locations(()).await?.into_inner(); - locations.countries = locations - .countries - .into_iter() - .filter_map(|mut country| { - country.cities = country - .cities - .into_iter() - .filter_map(|mut city| { - city.relays - .retain(|relay| relay.active && !relay.bridges.is_empty()); - if !city.relays.is_empty() { - Some(city) - } else { - None - } - }) - .collect(); - if !country.cities.is_empty() { - Some(country) - } else { - None - } - }) - .collect(); + let mut countries = Vec::new(); + + while let Some(mut country) = locations.message().await? { + country.cities = country + .cities + .into_iter() + .filter_map(|mut city| { + city.relays.retain(|relay| { + relay.active + && relay.bridges.is_some() + && !relay.bridges.as_ref().unwrap().shadowsocks.is_empty() + }); + if !city.relays.is_empty() { + Some(city) + } else { + None + } + }) + .collect(); + if !country.cities.is_empty() { + countries.push(country); + } + } - locations - .countries - .sort_by(|c1, c2| natord::compare_ignore_case(&c1.name, &c2.name)); - for mut country in locations.countries { + countries.sort_by(|c1, c2| natord::compare_ignore_case(&c1.name, &c2.name)); + for mut country in countries { country .cities .sort_by(|c1, c2| natord::compare_ignore_case(&c1.name, &c2.name)); diff --git a/mullvad-cli/src/cmds/connect.rs b/mullvad-cli/src/cmds/connect.rs index 89cc0f2d2e..6fc7072580 100644 --- a/mullvad-cli/src/cmds/connect.rs +++ b/mullvad-cli/src/cmds/connect.rs @@ -3,6 +3,7 @@ use talpid_types::ErrorExt; pub struct Connect; +#[mullvad_management_interface::async_trait] impl Command for Connect { fn name(&self) -> &'static str { "connect" @@ -13,9 +14,9 @@ impl Command for Connect { .about("Command the client to start establishing a VPN tunnel") } - fn run(&self, _matches: &clap::ArgMatches<'_>) -> Result<()> { - let mut rpc = new_rpc_client()?; - if let Err(e) = rpc.connect() { + async fn run(&self, _: &clap::ArgMatches<'_>) -> Result<()> { + let mut rpc = new_rpc_client().await?; + if let Err(e) = rpc.connect_tunnel(()).await { eprintln!("{}", e.display_chain()); } Ok(()) diff --git a/mullvad-cli/src/cmds/disconnect.rs b/mullvad-cli/src/cmds/disconnect.rs index bf85f80310..f976b96fab 100644 --- a/mullvad-cli/src/cmds/disconnect.rs +++ b/mullvad-cli/src/cmds/disconnect.rs @@ -2,6 +2,7 @@ use crate::{new_rpc_client, Command, Result}; pub struct Disconnect; +#[mullvad_management_interface::async_trait] impl Command for Disconnect { fn name(&self) -> &'static str { "disconnect" @@ -12,9 +13,9 @@ impl Command for Disconnect { .about("Command the client to disconnect the VPN tunnel") } - fn run(&self, _matches: &clap::ArgMatches<'_>) -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.disconnect()?; + async fn run(&self, _: &clap::ArgMatches<'_>) -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.disconnect_tunnel(()).await?; Ok(()) } } diff --git a/mullvad-cli/src/cmds/lan.rs b/mullvad-cli/src/cmds/lan.rs index 15d30cde50..dd84851f33 100644 --- a/mullvad-cli/src/cmds/lan.rs +++ b/mullvad-cli/src/cmds/lan.rs @@ -3,6 +3,7 @@ use clap::value_t_or_exit; pub struct Lan; +#[mullvad_management_interface::async_trait] impl Command for Lan { fn name(&self) -> &'static str { "lan" @@ -27,12 +28,12 @@ impl Command for Lan { ) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { if let Some(set_matches) = matches.subcommand_matches("set") { let allow_lan = value_t_or_exit!(set_matches.value_of("policy"), String); - self.set(allow_lan == "allow") + self.set(allow_lan == "allow").await } else if let Some(_matches) = matches.subcommand_matches("get") { - self.get() + self.get().await } else { unreachable!("No lan command given"); } @@ -40,16 +41,16 @@ impl Command for Lan { } impl Lan { - fn set(&self, allow_lan: bool) -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.set_allow_lan(allow_lan)?; + async fn set(&self, allow_lan: bool) -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.set_allow_lan(allow_lan).await?; println!("Changed local network sharing setting"); Ok(()) } - fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client()?; - let allow_lan = rpc.get_settings()?.allow_lan; + async fn get(&self) -> Result<()> { + let mut rpc = new_rpc_client().await?; + let allow_lan = rpc.get_settings(()).await?.into_inner().allow_lan; println!( "Local network sharing setting: {}", if allow_lan { "allow" } else { "block" } diff --git a/mullvad-cli/src/cmds/reconnect.rs b/mullvad-cli/src/cmds/reconnect.rs index 0cc7f6bfea..ecc0b089c0 100644 --- a/mullvad-cli/src/cmds/reconnect.rs +++ b/mullvad-cli/src/cmds/reconnect.rs @@ -3,6 +3,7 @@ use talpid_types::ErrorExt; pub struct Reconnect; +#[mullvad_management_interface::async_trait] impl Command for Reconnect { fn name(&self) -> &'static str { "reconnect" @@ -12,9 +13,9 @@ impl Command for Reconnect { clap::SubCommand::with_name(self.name()).about("Command the client to reconnect") } - fn run(&self, _matches: &clap::ArgMatches<'_>) -> Result<()> { - let mut rpc = new_rpc_client()?; - if let Err(e) = rpc.reconnect() { + async fn run(&self, _: &clap::ArgMatches<'_>) -> Result<()> { + let mut rpc = new_rpc_client().await?; + if let Err(e) = rpc.reconnect_tunnel(()).await { eprintln!("{}", e.display_chain()); } Ok(()) diff --git a/mullvad-cli/src/cmds/relay.rs b/mullvad-cli/src/cmds/relay.rs index c4e8c72c81..b23f8638d3 100644 --- a/mullvad-cli/src/cmds/relay.rs +++ b/mullvad-cli/src/cmds/relay.rs @@ -6,19 +6,19 @@ use std::{ str::FromStr, }; -use mullvad_types::{ - relay_constraints::{ - Constraint, OpenVpnConstraints, RelayConstraintsUpdate, RelaySettingsUpdate, - WireguardConstraints, - }, - ConnectionConfig, CustomTunnelEndpoint, -}; -use talpid_types::net::{ - all_of_the_internet, openvpn, wireguard, Endpoint, TransportProtocol, TunnelType, +use mullvad_management_interface::types::{ + connection_config::{self, OpenvpnConfig, WireguardConfig}, + relay_settings, relay_settings_update, ConnectionConfig, CustomRelaySettings, + NormalRelaySettingsUpdate, OpenvpnConstraints, RelaySettingsUpdate, TransportProtocol, + TransportProtocolConstraint, TunnelType, TunnelTypeConstraint, TunnelTypeUpdate, + WireguardConstraints, }; +use mullvad_types::relay_constraints::Constraint; +use talpid_types::net::all_of_the_internet; pub struct Relay; +#[mullvad_management_interface::async_trait] impl Command for Relay { fn name(&self) -> &'static str { "relay" @@ -155,15 +155,15 @@ impl Command for Relay { ) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { if let Some(set_matches) = matches.subcommand_matches("set") { - self.set(set_matches) + self.set(set_matches).await } else if matches.subcommand_matches("get").is_some() { - self.get() + self.get().await } else if matches.subcommand_matches("list").is_some() { - self.list() + self.list().await } else if matches.subcommand_matches("update").is_some() { - self.update() + self.update().await } else { unreachable!("No relay command given"); } @@ -171,54 +171,73 @@ impl Command for Relay { } impl Relay { - fn update_constraints(&self, update: RelaySettingsUpdate) -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.update_relay_settings(update)?; + async fn update_constraints(&self, update: RelaySettingsUpdate) -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.update_relay_settings(update).await?; println!("Relay constraints updated"); Ok(()) } - fn set(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn set(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { if let Some(custom_matches) = matches.subcommand_matches("custom") { - self.set_custom(custom_matches) + self.set_custom(custom_matches).await } else if let Some(location_matches) = matches.subcommand_matches("location") { - self.set_location(location_matches) + self.set_location(location_matches).await } else if let Some(tunnel_matches) = matches.subcommand_matches("tunnel") { - self.set_tunnel(tunnel_matches) + self.set_tunnel(tunnel_matches).await } else if let Some(tunnel_matches) = matches.subcommand_matches("tunnel-protocol") { - self.set_tunnel_protocol(tunnel_matches) + self.set_tunnel_protocol(tunnel_matches).await } else { unreachable!("No set relay command given"); } } - fn set_custom(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn set_custom(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { let custom_endpoint = match matches.subcommand() { ("openvpn", Some(openvpn_matches)) => Self::read_custom_openvpn_relay(openvpn_matches), ("wireguard", Some(wg_matches)) => Self::read_custom_wireguard_relay(wg_matches), (_unknown_tunnel, _) => unreachable!("No set relay command given"), }; - self.update_constraints(RelaySettingsUpdate::CustomTunnelEndpoint(custom_endpoint)) + + self.update_constraints(RelaySettingsUpdate { + r#type: Some(relay_settings_update::Type::Custom(custom_endpoint)), + }) + .await } - fn read_custom_openvpn_relay(matches: &clap::ArgMatches<'_>) -> CustomTunnelEndpoint { + fn read_custom_openvpn_relay(matches: &clap::ArgMatches<'_>) -> CustomRelaySettings { let host = value_t!(matches.value_of("host"), String).unwrap_or_else(|e| e.exit()); let port = value_t!(matches.value_of("port"), u16).unwrap_or_else(|e| e.exit()); let username = value_t!(matches.value_of("username"), String).unwrap_or_else(|e| e.exit()); let password = value_t!(matches.value_of("password"), String).unwrap_or_else(|e| e.exit()); - let protocol = - value_t!(matches.value_of("protocol"), TransportProtocol).unwrap_or_else(|e| e.exit()); - CustomTunnelEndpoint::new( + let protocol = value_t!(matches.value_of("protocol"), String).unwrap_or_else(|e| e.exit()); + + let protocol = match protocol.as_str() { + "udp" => TransportProtocol::Udp, + "tcp" => TransportProtocol::Tcp, + _ => clap::Error::with_description( + "unknown transport protocol", + clap::ErrorKind::ValueValidation, + ) + .exit(), + }; + + CustomRelaySettings { host, - ConnectionConfig::OpenVpn(openvpn::ConnectionConfig { - endpoint: Endpoint::new(Ipv4Addr::UNSPECIFIED, port, protocol), - username, - password, + config: Some(ConnectionConfig { + config: Some(connection_config::Config::Openvpn(OpenvpnConfig { + address: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port).to_string(), + protocol: protocol as i32, + username, + password, + })), }), - ) + } } - fn read_custom_wireguard_relay(matches: &clap::ArgMatches<'_>) -> CustomTunnelEndpoint { + fn read_custom_wireguard_relay(matches: &clap::ArgMatches<'_>) -> CustomRelaySettings { + use connection_config::wireguard_config; + let host = value_t!(matches.value_of("host"), String).unwrap_or_else(|e| e.exit()); let port = value_t!(matches.value_of("port"), u16).unwrap_or_else(|e| e.exit()); let addresses = values_t!(matches.values_of("addr"), IpAddr).unwrap_or_else(|e| e.exit()); @@ -239,26 +258,37 @@ impl Relay { if private_key_str.trim().is_empty() { eprintln!("Expected to read private key from standard input"); } - let private_key = Self::validate_wireguard_key(&private_key_str).into(); - let peer_public_key = Self::validate_wireguard_key(&peer_key_str).into(); - + let private_key = Self::validate_wireguard_key(&private_key_str); + let peer_public_key = Self::validate_wireguard_key(&peer_key_str); - CustomTunnelEndpoint::new( + CustomRelaySettings { host, - ConnectionConfig::Wireguard(wireguard::ConnectionConfig { - tunnel: wireguard::TunnelConfig { - private_key, - addresses, - }, - peer: wireguard::PeerConfig { - public_key: peer_public_key, - allowed_ips: all_of_the_internet(), - endpoint: SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port), - }, - ipv4_gateway, - ipv6_gateway, + config: Some(ConnectionConfig { + config: Some(connection_config::Config::Wireguard(WireguardConfig { + tunnel: Some(wireguard_config::TunnelConfig { + private_key: private_key.to_vec(), + addresses: addresses + .iter() + .map(|address| address.to_string()) + .collect(), + }), + peer: Some(wireguard_config::PeerConfig { + public_key: peer_public_key.to_vec(), + allowed_ips: all_of_the_internet() + .iter() + .map(|address| address.to_string()) + .collect(), + endpoint: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port) + .to_string(), + }), + ipv4_gateway: ipv4_gateway.to_string(), + ipv6_gateway: ipv6_gateway + .as_ref() + .map(|addr| addr.to_string()) + .unwrap_or_default(), + })), }), - ) + } } fn validate_wireguard_key(key_str: &str) -> [u8; 32] { @@ -280,16 +310,21 @@ impl Relay { key } - fn set_location(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn set_location(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { let location_constraint = location::get_constraint(matches); - self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { - location: Some(location_constraint), - ..Default::default() - })) + self.update_constraints(RelaySettingsUpdate { + r#type: Some(relay_settings_update::Type::Normal( + NormalRelaySettingsUpdate { + location: Some(location_constraint), + ..Default::default() + }, + )), + }) + .await } - fn set_tunnel(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn set_tunnel(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { let vpn_protocol = matches.value_of("vpn protocol").unwrap(); let port = parse_port_constraint(matches.value_of("port").unwrap())?; let protocol = parse_protocol_constraint(matches.value_of("transport protocol").unwrap()); @@ -299,79 +334,161 @@ impl Relay { if let Constraint::Only(TransportProtocol::Tcp) = protocol { return Err(Error::InvalidCommand("WireGuard does not support TCP")); } - self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { - location: None, - tunnel_protocol: None, - wireguard_constraints: Some(WireguardConstraints { port }), - ..Default::default() - })) + self.update_constraints(RelaySettingsUpdate { + r#type: Some(relay_settings_update::Type::Normal( + NormalRelaySettingsUpdate { + wireguard_constraints: Some(WireguardConstraints { + port: port.unwrap_or(0) as u32, + }), + ..Default::default() + }, + )), + }) + .await } "openvpn" => { - self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { - location: None, - tunnel_protocol: None, - openvpn_constraints: Some(OpenVpnConstraints { port, protocol }), - ..Default::default() - })) + self.update_constraints(RelaySettingsUpdate { + r#type: Some(relay_settings_update::Type::Normal( + NormalRelaySettingsUpdate { + openvpn_constraints: Some(OpenvpnConstraints { + port: port.unwrap_or(0) as u32, + protocol: protocol.option().map(|protocol| { + TransportProtocolConstraint { + protocol: protocol as i32, + } + }), + }), + ..Default::default() + }, + )), + }) + .await } _ => unreachable!(), } } - fn set_tunnel_protocol(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { - let tunnel_protocol = match matches.value_of("tunnel protocol").unwrap() { - "wireguard" => Constraint::Only(TunnelType::Wireguard), - "openvpn" => Constraint::Only(TunnelType::OpenVpn), - "any" => Constraint::Any, + async fn set_tunnel_protocol(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + let tunnel_type = match matches.value_of("tunnel protocol").unwrap() { + "wireguard" => Some(TunnelType::Wireguard), + "openvpn" => Some(TunnelType::Openvpn), + "any" => None, _ => unreachable!(), }; - self.update_constraints(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { - tunnel_protocol: Some(tunnel_protocol), - ..Default::default() - })) + self.update_constraints(RelaySettingsUpdate { + r#type: Some(relay_settings_update::Type::Normal( + NormalRelaySettingsUpdate { + tunnel_type: Some(TunnelTypeUpdate { + tunnel_type: tunnel_type.map(|tunnel_type| TunnelTypeConstraint { + tunnel_type: tunnel_type as i32, + }), + }), + ..Default::default() + }, + )), + }) + .await } - fn get(&self) -> Result<()> { - let mut rpc = new_rpc_client()?; - let constraints = rpc.get_settings()?.get_relay_settings(); - println!("Current constraints: {}", constraints); + async fn get(&self) -> Result<()> { + let mut rpc = new_rpc_client().await?; + let constraints = rpc + .get_settings(()) + .await? + .into_inner() + .relay_settings + .unwrap(); + + print!("Current constraints: "); + + match constraints.endpoint.unwrap() { + relay_settings::Endpoint::Normal(settings) => match settings.tunnel_type { + None => { + println!( + "Any tunnel protocol with OpenVPN over {} and WireGuard over {} in {}", + Self::format_openvpn_constraints(settings.openvpn_constraints.as_ref()), + Self::format_wireguard_constraints(settings.wireguard_constraints.as_ref()), + location::format_location(settings.location.as_ref()) + ); + } + Some(constraint) => match TunnelType::from_i32(constraint.tunnel_type).unwrap() { + TunnelType::Wireguard => { + println!( + "WireGuard over {} in {}", + Self::format_wireguard_constraints( + settings.wireguard_constraints.as_ref() + ), + location::format_location(settings.location.as_ref()) + ); + } + TunnelType::Openvpn => { + println!( + "OpenVPN over {} in {}", + Self::format_openvpn_constraints(settings.openvpn_constraints.as_ref()), + location::format_location(settings.location.as_ref()) + ); + } + }, + }, + + relay_settings::Endpoint::Custom(settings) => { + let config = settings.config.unwrap(); + match config.config.unwrap() { + connection_config::Config::Openvpn(config) => { + println!( + "custom OpenVPN relay - {} {}", + config.address, + Self::format_transport_protocol(Some( + TransportProtocol::from_i32(config.protocol).unwrap() + )), + ); + } + connection_config::Config::Wireguard(config) => { + let peer = config.peer.unwrap(); + println!( + "custom WireGuard relay - {} with public key {}", + peer.endpoint, + base64::encode(&peer.public_key), + ); + } + } + } + } Ok(()) } - fn list(&self) -> Result<()> { - let mut rpc = new_rpc_client()?; - let mut locations = rpc.get_relay_locations()?; + async fn list(&self) -> Result<()> { + let mut rpc = new_rpc_client().await?; + let mut locations = rpc.get_relay_locations(()).await?.into_inner(); - locations.countries = locations - .countries - .into_iter() - .filter_map(|mut country| { - country.cities = country - .cities - .into_iter() - .filter_map(|mut city| { - city.relays - .retain(|relay| relay.active && !relay.tunnels.is_empty()); - if !city.relays.is_empty() { - Some(city) - } else { - None - } - }) - .collect(); - if !country.cities.is_empty() { - Some(country) - } else { - None - } - }) - .collect(); + let mut countries = Vec::new(); - locations - .countries - .sort_by(|c1, c2| natord::compare_ignore_case(&c1.name, &c2.name)); - for mut country in locations.countries { + while let Some(mut country) = locations.message().await? { + country.cities = country + .cities + .into_iter() + .filter_map(|mut city| { + city.relays.retain(|relay| { + relay.active + && relay.tunnels.is_some() + && !(relay.tunnels.as_ref().unwrap().openvpn.is_empty() + && relay.tunnels.as_ref().unwrap().wireguard.is_empty()) + }); + if !city.relays.is_empty() { + Some(city) + } else { + None + } + }) + .collect(); + if !country.cities.is_empty() { + countries.push(country); + } + } + + countries.sort_by(|c1, c2| natord::compare_ignore_case(&c1.name, &c2.name)); + for mut country in countries { country .cities .sort_by(|c1, c2| natord::compare_ignore_case(&c1.name, &c2.name)); @@ -384,8 +501,9 @@ impl Relay { city.name, city.code, city.latitude, city.longitude ); for relay in &city.relays { - let supports_openvpn = !relay.tunnels.openvpn.is_empty(); - let supports_wireguard = !relay.tunnels.wireguard.is_empty(); + let tunnels = relay.tunnels.as_ref().unwrap(); + let supports_openvpn = !tunnels.openvpn.is_empty(); + let supports_wireguard = !tunnels.wireguard.is_empty(); let support_msg = match (supports_openvpn, supports_wireguard) { (true, true) => "OpenVPN and WireGuard", (true, false) => "OpenVPN", @@ -403,11 +521,52 @@ impl Relay { Ok(()) } - fn update(&self) -> Result<()> { - new_rpc_client()?.update_relay_locations()?; + async fn update(&self) -> Result<()> { + new_rpc_client().await?.update_relay_locations(()).await?; println!("Updating relay list in the background..."); Ok(()) } + + fn format_transport_protocol(protocol: Option<TransportProtocol>) -> &'static str { + match protocol { + None => "any transport protocol", + Some(TransportProtocol::Udp) => "UDP", + Some(TransportProtocol::Tcp) => "TCP", + } + } + + fn format_port(port: u32) -> String { + if port != 0 { + format!("port {}", port) + } else { + "any port".to_string() + } + } + + fn format_openvpn_constraints(constraints: Option<&OpenvpnConstraints>) -> String { + if let Some(constraints) = constraints { + format!( + "{} over {}", + Self::format_port(constraints.port), + Self::format_transport_protocol( + constraints + .protocol + .clone() + .map(|protocol| TransportProtocol::from_i32(protocol.protocol).unwrap()) + ) + ) + } else { + "any port over any transport protocol".to_string() + } + } + + fn format_wireguard_constraints(constraints: Option<&WireguardConstraints>) -> String { + if let Some(constraints) = constraints { + Self::format_port(constraints.port) + } else { + "any port".to_string() + } + } } diff --git a/mullvad-cli/src/cmds/reset.rs b/mullvad-cli/src/cmds/reset.rs index d859d1e436..bb10a3f0fe 100644 --- a/mullvad-cli/src/cmds/reset.rs +++ b/mullvad-cli/src/cmds/reset.rs @@ -2,6 +2,7 @@ use crate::{new_rpc_client, Command, Result}; use std::io::stdin; pub struct Reset; +#[mullvad_management_interface::async_trait] impl Command for Reset { fn name(&self) -> &'static str { "factory-reset" @@ -11,10 +12,10 @@ impl Command for Reset { clap::SubCommand::with_name(self.name()).about("Reset settings, caches and logs") } - fn run(&self, _matches: &clap::ArgMatches<'_>) -> Result<()> { - let mut rpc = new_rpc_client()?; + async fn run(&self, _: &clap::ArgMatches<'_>) -> Result<()> { + let mut rpc = new_rpc_client().await?; if Self::receive_confirmation() { - if rpc.factory_reset().is_err() { + if rpc.factory_reset(()).await.is_err() { eprintln!("FAILED TO PERFORM FACTORY RESET"); } else { #[cfg(target_os = "linux")] diff --git a/mullvad-cli/src/cmds/split_tunnel/linux.rs b/mullvad-cli/src/cmds/split_tunnel/linux.rs index 95b172eb6b..5903d84f68 100644 --- a/mullvad-cli/src/cmds/split_tunnel/linux.rs +++ b/mullvad-cli/src/cmds/split_tunnel/linux.rs @@ -3,6 +3,7 @@ use clap::value_t_or_exit; pub struct SplitTunnel; +#[mullvad_management_interface::async_trait] impl Command for SplitTunnel { fn name(&self) -> &'static str { "split-tunnel" @@ -15,9 +16,9 @@ impl Command for SplitTunnel { .subcommand(create_pid_subcommand()) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { - ("pid", Some(pid_matches)) => Self::handle_pid_cmd(pid_matches), + ("pid", Some(pid_matches)) => Self::handle_pid_cmd(pid_matches).await, _ => unreachable!("unhandled comand"), } } @@ -38,27 +39,40 @@ fn create_pid_subcommand() -> clap::App<'static, 'static> { } impl SplitTunnel { - fn handle_pid_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn handle_pid_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { ("add", Some(matches)) => { let pid = value_t_or_exit!(matches.value_of("pid"), i32); - new_rpc_client()?.add_split_tunnel_process(pid)?; + new_rpc_client() + .await? + .add_split_tunnel_process(pid) + .await?; Ok(()) } ("delete", Some(matches)) => { let pid = value_t_or_exit!(matches.value_of("pid"), i32); - new_rpc_client()?.remove_split_tunnel_process(pid)?; + new_rpc_client() + .await? + .remove_split_tunnel_process(pid) + .await?; Ok(()) } ("clear", Some(_)) => { - new_rpc_client()?.clear_split_tunnel_processes()?; + new_rpc_client() + .await? + .clear_split_tunnel_processes(()) + .await?; Ok(()) } ("list", Some(_)) => { - let pids = new_rpc_client()?.get_split_tunnel_processes()?; + let mut pids_stream = new_rpc_client() + .await? + .get_split_tunnel_processes(()) + .await? + .into_inner(); println!("Excluded PIDs:"); - for pid in pids.iter() { + while let Some(pid) = pids_stream.message().await? { println!(" {}", pid); } diff --git a/mullvad-cli/src/cmds/status.rs b/mullvad-cli/src/cmds/status.rs index ffa834134e..b0c4d40ad0 100644 --- a/mullvad-cli/src/cmds/status.rs +++ b/mullvad-cli/src/cmds/status.rs @@ -1,11 +1,21 @@ -use crate::{new_rpc_client, Command, Error, Result}; -use futures::{Future, Stream}; -use mullvad_ipc_client::DaemonRpcClient; -use mullvad_types::{auth_failed::AuthFailed, states::TunnelState, DaemonEvent}; -use talpid_types::tunnel::{ErrorState, ErrorStateCause}; +use crate::{format::print_keygen_event, new_rpc_client, Command, Error, Result}; +use mullvad_management_interface::{ + types::{ + daemon_event::Event as EventType, + error_state::{ + firewall_policy_error::ErrorType as FirewallPolicyErrorType, Cause as ErrorStateCause, + FirewallPolicyError, GenerationError, + }, + ErrorState, ProxyType, TransportProtocol, TunnelEndpoint, TunnelState, TunnelType, + }, + ManagementServiceClient, +}; +use mullvad_types::auth_failed::AuthFailed; +use std::fmt::Write; pub struct Status; +#[mullvad_management_interface::async_trait] impl Command for Status { fn name(&self) -> &'static str { "status" @@ -31,127 +41,234 @@ impl Command for Status { ) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { - let mut rpc = new_rpc_client()?; - let state = rpc.get_state()?; + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + let mut rpc = new_rpc_client().await?; + let state = rpc.get_tunnel_state(()).await?.into_inner(); print_state(&state); if matches.is_present("location") { - print_location(&mut rpc)?; + print_location(&mut rpc).await?; } if let Some(listen_matches) = matches.subcommand_matches("listen") { let verbose = listen_matches.is_present("verbose"); - let subscription = rpc - .daemon_event_subscribe() - .wait() - .map_err(Error::CantSubscribe)?; - for event in subscription.wait() { - match event? { - DaemonEvent::TunnelState(new_state) => { + + let mut events = rpc.events_listen(()).await?.into_inner(); + + while let Some(event) = events.message().await? { + match event.event.unwrap() { + EventType::TunnelState(new_state) => { print_state(&new_state); - use self::TunnelState::*; - match new_state { - Connected { .. } | Disconnected => { + use mullvad_management_interface::types::tunnel_state::State::*; + match new_state.state.unwrap() { + Connected(..) | Disconnected(..) => { if matches.is_present("location") { - print_location(&mut rpc)?; + print_location(&mut rpc).await?; } } _ => {} } } - DaemonEvent::Settings(settings) => { + EventType::Settings(settings) => { if verbose { println!("New settings: {:#?}", settings); } } - DaemonEvent::RelayList(relay_list) => { + EventType::RelayList(relay_list) => { if verbose { println!("New relay list: {:#?}", relay_list); } } - DaemonEvent::AppVersionInfo(app_version_info) => { + EventType::VersionInfo(app_version_info) => { if verbose { println!("New app version info: {:#?}", app_version_info); } } - DaemonEvent::WireguardKey(key_event) => { + EventType::KeyEvent(key_event) => { if verbose { - println!("{}", key_event); + print!("Key event: "); + print_keygen_event(&key_event); } } } } } + Ok(()) } } fn print_state(state: &TunnelState) { - use self::TunnelState::*; + use mullvad_management_interface::types::{tunnel_state, tunnel_state::State::*}; + print!("Tunnel status: "); - match state { - Error(reason) => print_error_state(reason), - Connected { endpoint, .. } => { - println!("Connected to {}", endpoint); + match state.state.as_ref().unwrap() { + Error(error) => print_error_state(error.error_state.as_ref().unwrap()), + Connected(tunnel_state::Connected { relay_info }) => { + let endpoint = relay_info + .as_ref() + .unwrap() + .tunnel_endpoint + .as_ref() + .unwrap(); + println!("Connected to {}", format_endpoint(&endpoint)); } - Connecting { endpoint, .. } => println!("Connecting to {}...", endpoint), - Disconnected => println!("Disconnected"), + Connecting(tunnel_state::Connecting { relay_info }) => { + let endpoint = relay_info + .as_ref() + .unwrap() + .tunnel_endpoint + .as_ref() + .unwrap(); + println!("Connecting to {}...", format_endpoint(&endpoint)); + } + Disconnected(_) => println!("Disconnected"), Disconnecting(_) => println!("Disconnecting..."), } } +fn format_endpoint(endpoint: &TunnelEndpoint) -> String { + let mut out = format!( + "{} {} over {}", + match TunnelType::from_i32(endpoint.tunnel_type).expect("unknown tunnel protocol") { + TunnelType::Wireguard => "WireGuard", + TunnelType::Openvpn => "OpenVPN", + }, + endpoint.address, + format_protocol( + TransportProtocol::from_i32(endpoint.protocol).expect("unknown transport protocol") + ), + ); + + if let Some(ref proxy) = endpoint.proxy { + write!( + &mut out, + " via {} {} over {}", + match ProxyType::from_i32(proxy.proxy_type).expect("unknown proxy type") { + ProxyType::Shadowsocks => "Shadowsocks", + ProxyType::Custom => "custom bridge", + }, + proxy.address, + format_protocol( + TransportProtocol::from_i32(proxy.protocol).expect("unknown transport protocol") + ), + ) + .unwrap(); + } + + out +} + fn print_error_state(error_state: &ErrorState) { - if !error_state.is_blocking() { + if error_state.blocking_error.is_some() { eprintln!("Mullvad daemon failed to setup firewall rules!"); eprintln!("Deamon cannot block traffic from flowing, non-local traffic will leak"); } - print_blocked_reason(error_state.cause()); -} - -fn print_blocked_reason(reason: &ErrorStateCause) { - match reason { - ErrorStateCause::AuthFailed(ref auth_failure) => { - let auth_failure_str = auth_failure - .as_ref() - .map(|s| s.as_str()) - .unwrap_or("Account authentication failed"); - println!("Blocked: {}", AuthFailed::from(auth_failure_str)); - } - #[cfg(target_os = "linux")] - ErrorStateCause::SetFirewallPolicyError(error) => { + match ErrorStateCause::from_i32(error_state.cause) { + Some(ErrorStateCause::AuthFailed) => { println!( "Blocked: {}", - ErrorStateCause::SetFirewallPolicyError(error.clone()) + AuthFailed::from(error_state.auth_fail_reason.as_ref()) ); + } + #[cfg(target_os = "linux")] + Some(ErrorStateCause::SetFirewallPolicyError) => { + println!("Blocked: {}", error_state_to_string(error_state)); println!("Your kernel might be terribly out of date or missing nftables"); } - other => println!("Blocked: {}", other), + _ => println!("Blocked: {}", error_state_to_string(error_state)), } } -fn print_location(rpc: &mut DaemonRpcClient) -> Result<()> { - let location = match rpc.get_current_location()? { - Some(loc) => loc, - None => { - println!("Location data unavailable"); - return Ok(()); +fn error_state_to_string(error_state: &ErrorState) -> String { + use ErrorStateCause::*; + + let error_str = match ErrorStateCause::from_i32(error_state.cause).expect("unknown error cause") + { + AuthFailed => { + return if error_state.auth_fail_reason.is_empty() { + "Authentication with remote server failed".to_string() + } else { + format!( + "Authentication with remote server failed: {}", + error_state.auth_fail_reason + ) + }; + } + Ipv6Unavailable => "Failed to configure IPv6 because it's disabled in the platform", + SetFirewallPolicyError => { + return policy_error_to_string(error_state.policy_error.as_ref().unwrap()) } + SetDnsError => "Failed to set system DNS server", + StartTunnelError => "Failed to start connection to remote server", + TunnelParameterError => { + return format!( + "Failure to generate tunnel parameters: {}", + tunnel_parameter_error_to_string(error_state.parameter_error) + ); + } + IsOffline => "This device is offline, no tunnels can be established", + TapAdapterProblem => "A problem with the TAP adapter has been detected", + #[cfg(target_os = "android")] + VpnPermissionDenied => "The Android VPN permission was denied when creating the tunnel", + #[cfg(not(target_os = "android"))] + _ => unreachable!("unknown error cause"), }; - if let Some(hostname) = location.hostname { - println!("Relay: {}", hostname); + + error_str.to_string() +} + +fn tunnel_parameter_error_to_string(parameter_error: i32) -> &'static str { + match GenerationError::from_i32(parameter_error).expect("unknown generation error") { + GenerationError::NoMatchingRelay => "Failure to select a matching tunnel relay", + GenerationError::NoMatchingBridgeRelay => "Failure to select a matching bridge relay", + GenerationError::NoWireguardKey => "No wireguard key available", + GenerationError::CustomTunnelHostResolutionError => { + "Can't resolve hostname for custom tunnel host" + } } - if let Some(ipv4) = location.ipv4 { - println!("IPv4: {}", ipv4); +} + +fn policy_error_to_string(policy_error: &FirewallPolicyError) -> String { + let cause = match FirewallPolicyErrorType::from_i32(policy_error.r#type) + .expect("unknown policy error") + { + FirewallPolicyErrorType::Generic => return "Failed to set firewall policy".to_string(), + FirewallPolicyErrorType::Locked => format!( + "An application prevented the firewall policy from being set: {} (pid {})", + policy_error.lock_name, policy_error.lock_pid + ), + }; + format!("Failed to set firewall policy: {}", cause) +} + +async fn print_location(rpc: &mut ManagementServiceClient) -> Result<()> { + let location = rpc.get_current_location(()).await; + let location = match location { + Ok(response) => response.into_inner(), + Err(status) => { + if status.code() == mullvad_management_interface::Code::NotFound { + println!("Location data unavailable"); + return Ok(()); + } else { + return Err(Error::GrpcClientError(status)); + } + } + }; + if !location.hostname.is_empty() { + println!("Relay: {}", location.hostname); + } + if !location.ipv4.is_empty() { + println!("IPv4: {}", location.ipv4); } - if let Some(ipv6) = location.ipv6 { - println!("IPv6: {}", ipv6); + if !location.ipv6.is_empty() { + println!("IPv6: {}", location.ipv6); } print!("Location: "); - if let Some(city) = location.city { - print!("{}, ", city); + if !location.city.is_empty() { + print!("{}, ", location.city); } println!("{}", location.country); @@ -161,3 +278,10 @@ fn print_location(rpc: &mut DaemonRpcClient) -> Result<()> { ); Ok(()) } + +fn format_protocol(protocol: TransportProtocol) -> &'static str { + match protocol { + TransportProtocol::Udp => "UDP", + TransportProtocol::Tcp => "TCP", + } +} diff --git a/mullvad-cli/src/cmds/tunnel.rs b/mullvad-cli/src/cmds/tunnel.rs index 5349174d4c..528ebd005e 100644 --- a/mullvad-cli/src/cmds/tunnel.rs +++ b/mullvad-cli/src/cmds/tunnel.rs @@ -1,10 +1,10 @@ -use crate::{new_rpc_client, Command, Result}; +use crate::{format::print_keygen_event, new_rpc_client, Command, Error, Result}; use clap::value_t; - -use mullvad_types::settings::TunnelOptions; +use mullvad_management_interface::types::{Timestamp, TunnelOptions}; pub struct Tunnel; +#[mullvad_management_interface::async_trait] impl Command for Tunnel { fn name(&self) -> &'static str { "tunnel" @@ -19,11 +19,11 @@ impl Command for Tunnel { .subcommand(create_ipv6_subcommand()) } - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { - ("openvpn", Some(openvpn_matches)) => Self::handle_openvpn_cmd(openvpn_matches), - ("wireguard", Some(wg_matches)) => Self::handle_wireguard_cmd(wg_matches), - ("ipv6", Some(ipv6_matches)) => Self::handle_ipv6_cmd(ipv6_matches), + ("openvpn", Some(openvpn_matches)) => Self::handle_openvpn_cmd(openvpn_matches).await, + ("wireguard", Some(wg_matches)) => Self::handle_wireguard_cmd(wg_matches).await, + ("ipv6", Some(ipv6_matches)) => Self::handle_ipv6_cmd(ipv6_matches).await, _ => { unreachable!("unhandled comand"); } @@ -104,40 +104,42 @@ fn create_ipv6_subcommand() -> clap::App<'static, 'static> { } impl Tunnel { - fn handle_openvpn_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn handle_openvpn_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { - ("mssfix", Some(mssfix_matches)) => Self::handle_openvpn_mssfix_cmd(mssfix_matches), + ("mssfix", Some(mssfix_matches)) => { + Self::handle_openvpn_mssfix_cmd(mssfix_matches).await + } _ => unreachable!("unhandled command"), } } - fn handle_openvpn_mssfix_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn handle_openvpn_mssfix_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { - ("get", Some(_)) => Self::process_openvpn_mssfix_get(), - ("unset", Some(_)) => Self::process_openvpn_mssfix_unset(), - ("set", Some(set_matches)) => Self::process_openvpn_mssfix_set(set_matches), + ("get", Some(_)) => Self::process_openvpn_mssfix_get().await, + ("unset", Some(_)) => Self::process_openvpn_mssfix_unset().await, + ("set", Some(set_matches)) => Self::process_openvpn_mssfix_set(set_matches).await, _ => unreachable!("unhandled command"), } } - fn handle_wireguard_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn handle_wireguard_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { match matches.subcommand() { ("mtu", Some(matches)) => match matches.subcommand() { - ("get", _) => Self::process_wireguard_mtu_get(), - ("set", Some(matches)) => Self::process_wireguard_mtu_set(matches), - ("unset", _) => Self::process_wireguard_mtu_unset(), + ("get", _) => Self::process_wireguard_mtu_get().await, + ("set", Some(matches)) => Self::process_wireguard_mtu_set(matches).await, + ("unset", _) => Self::process_wireguard_mtu_unset().await, _ => unreachable!("unhandled command"), }, ("key", Some(matches)) => match matches.subcommand() { - ("check", _) => Self::process_wireguard_key_check(), - ("regenerate", _) => Self::process_wireguard_key_generate(), + ("check", _) => Self::process_wireguard_key_check().await, + ("regenerate", _) => Self::process_wireguard_key_generate().await, ("rotation-interval", Some(matches)) => match matches.subcommand() { - ("get", _) => Self::process_wireguard_rotation_interval_get(), + ("get", _) => Self::process_wireguard_rotation_interval_get().await, ("set", Some(matches)) => { - Self::process_wireguard_rotation_interval_set(matches) + Self::process_wireguard_rotation_interval_set(matches).await } - ("reset", _) => Self::process_wireguard_rotation_interval_reset(), + ("reset", _) => Self::process_wireguard_rotation_interval_reset().await, _ => unreachable!("unhandled command"), }, _ => unreachable!("unhandled command"), @@ -147,140 +149,152 @@ impl Tunnel { } } - fn process_wireguard_mtu_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options()?; + async fn process_wireguard_mtu_get() -> Result<()> { + let tunnel_options = Self::get_tunnel_options().await?; + let mtu = tunnel_options.wireguard.unwrap().mtu; println!( "mtu: {}", - tunnel_options - .wireguard - .mtu - .map(|mtu| mtu.to_string()) - .unwrap_or_else(|| "unset".to_owned()) + if mtu != 0 { + mtu.to_string() + } else { + "unset".to_string() + }, ); Ok(()) } - fn process_wireguard_mtu_set(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn process_wireguard_mtu_set(matches: &clap::ArgMatches<'_>) -> Result<()> { let mtu = value_t!(matches.value_of("mtu"), u16).unwrap_or_else(|e| e.exit()); - let mut rpc = new_rpc_client()?; - rpc.set_wireguard_mtu(Some(mtu))?; + let mut rpc = new_rpc_client().await?; + rpc.set_wireguard_mtu(mtu as u32).await?; println!("Wireguard MTU has been updated"); Ok(()) } - fn process_wireguard_mtu_unset() -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.set_wireguard_mtu(None)?; + async fn process_wireguard_mtu_unset() -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.set_wireguard_mtu(0).await?; println!("Wireguard MTU has been unset"); Ok(()) } - fn process_wireguard_key_check() -> Result<()> { - let mut rpc = new_rpc_client()?; - match rpc.get_wireguard_key()? { - Some(key) => { - println!("Current key : {}", &key.key); - println!( - "Key created on : {}", - &key.created.with_timezone(&chrono::offset::Local) - ); - } - None => { - println!("No key is set"); - return Ok(()); + async fn process_wireguard_key_check() -> Result<()> { + let mut rpc = new_rpc_client().await?; + let key = rpc.get_wireguard_key(()).await; + let key = match key { + Ok(response) => Some(response.into_inner()), + Err(status) => { + if status.code() == mullvad_management_interface::Code::NotFound { + None + } else { + return Err(Error::GrpcClientError(status)); + } } }; + if let Some(key) = key { + println!("Current key : {}", base64::encode(&key.key)); + println!( + "Key created on : {}", + Self::format_key_timestamp(&key.created.unwrap()) + ); + } else { + println!("No key is set"); + return Ok(()); + } - let is_valid = rpc.verify_wireguard_key()?; + let is_valid = rpc.verify_wireguard_key(()).await?.into_inner(); println!("Key is valid for use with current account: {}", is_valid); Ok(()) } - fn process_wireguard_key_generate() -> Result<()> { - let mut rpc = new_rpc_client()?; - let result = rpc - .generate_wireguard_key() - .map_err(|e| crate::Error::RpcClientError(e))?; - println!("{}", result); + async fn process_wireguard_key_generate() -> Result<()> { + let mut rpc = new_rpc_client().await?; + let keygen_event = rpc.generate_wireguard_key(()).await?; + print_keygen_event(&keygen_event.into_inner()); Ok(()) } - fn process_wireguard_rotation_interval_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options()?; - println!( - "Rotation interval: {} hour(s)", - tunnel_options - .wireguard - .automatic_rotation - .map(|interval| interval.to_string()) - .unwrap_or_else(|| "default".to_owned()) - ); + async fn process_wireguard_rotation_interval_get() -> Result<()> { + let tunnel_options = Self::get_tunnel_options().await?; + match tunnel_options.wireguard.unwrap().automatic_rotation { + Some(interval) => { + println!("Rotation interval: {} hour(s)", interval.interval); + } + None => println!("Rotation interval: default"), + } Ok(()) } - fn process_wireguard_rotation_interval_set(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn process_wireguard_rotation_interval_set(matches: &clap::ArgMatches<'_>) -> Result<()> { let rotate_interval = value_t!(matches.value_of("interval"), u32).unwrap_or_else(|e| e.exit()); - let mut rpc = new_rpc_client()?; - rpc.set_wireguard_rotation_interval(Some(rotate_interval))?; + let mut rpc = new_rpc_client().await?; + rpc.set_wireguard_rotation_interval(rotate_interval).await?; println!("Set key rotation interval: {} hour(s)", rotate_interval); Ok(()) } - fn process_wireguard_rotation_interval_reset() -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.set_wireguard_rotation_interval(None)?; + async fn process_wireguard_rotation_interval_reset() -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.reset_wireguard_rotation_interval(()).await?; println!("Set key rotation interval: default"); Ok(()) } - fn handle_ipv6_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn handle_ipv6_cmd(matches: &clap::ArgMatches<'_>) -> Result<()> { if matches.subcommand_matches("get").is_some() { - Self::process_ipv6_get() + Self::process_ipv6_get().await } else if let Some(m) = matches.subcommand_matches("set") { - Self::process_ipv6_set(m) + Self::process_ipv6_set(m).await } else { unreachable!("unhandled command"); } } - fn process_openvpn_mssfix_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options()?; + async fn process_openvpn_mssfix_get() -> Result<()> { + let tunnel_options = Self::get_tunnel_options().await?; + let mssfix = tunnel_options.openvpn.unwrap().mssfix; println!( "mssfix: {}", - tunnel_options - .openvpn - .mssfix - .map_or_else(|| "unset".to_owned(), |v| v.to_string()) + if mssfix != 0 { + mssfix.to_string() + } else { + "unset".to_string() + }, ); Ok(()) } - fn get_tunnel_options() -> Result<TunnelOptions> { - let mut rpc = new_rpc_client()?; - Ok(rpc.get_settings()?.tunnel_options) + async fn get_tunnel_options() -> Result<TunnelOptions> { + let mut rpc = new_rpc_client().await?; + Ok(rpc + .get_settings(()) + .await? + .into_inner() + .tunnel_options + .unwrap()) } - fn process_openvpn_mssfix_unset() -> Result<()> { - let mut rpc = new_rpc_client()?; - rpc.set_openvpn_mssfix(None)?; + async fn process_openvpn_mssfix_unset() -> Result<()> { + let mut rpc = new_rpc_client().await?; + rpc.set_openvpn_mssfix(0).await?; println!("mssfix parameter has been unset"); Ok(()) } - fn process_openvpn_mssfix_set(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn process_openvpn_mssfix_set(matches: &clap::ArgMatches<'_>) -> Result<()> { let new_value = value_t!(matches.value_of("mssfix"), u16).unwrap_or_else(|e| e.exit()); - let mut rpc = new_rpc_client()?; - rpc.set_openvpn_mssfix(Some(new_value))?; + let mut rpc = new_rpc_client().await?; + rpc.set_openvpn_mssfix(new_value as u32).await?; println!("mssfix parameter has been updated"); Ok(()) } - fn process_ipv6_get() -> Result<()> { - let tunnel_options = Self::get_tunnel_options()?; + async fn process_ipv6_get() -> Result<()> { + let tunnel_options = Self::get_tunnel_options().await?; println!( "IPv6: {}", - if tunnel_options.generic.enable_ipv6 { + if tunnel_options.generic.unwrap().enable_ipv6 { "on" } else { "off" @@ -289,12 +303,20 @@ impl Tunnel { Ok(()) } - fn process_ipv6_set(matches: &clap::ArgMatches<'_>) -> Result<()> { + async fn process_ipv6_set(matches: &clap::ArgMatches<'_>) -> Result<()> { let enabled = matches.value_of("enable").unwrap() == "on"; - let mut rpc = new_rpc_client()?; - rpc.set_enable_ipv6(enabled)?; - println!("IPv6 setting has been updated"); + let mut rpc = new_rpc_client().await?; + rpc.set_enable_ipv6(enabled).await?; + if enabled { + println!("Enabled IPv6"); + } else { + println!("Disabled IPv6"); + } Ok(()) } + + fn format_key_timestamp(timestamp: &Timestamp) -> String { + chrono::NaiveDateTime::from_timestamp(timestamp.seconds, timestamp.nanos as u32).to_string() + } } diff --git a/mullvad-cli/src/cmds/version.rs b/mullvad-cli/src/cmds/version.rs index 4e0f3a0d8b..0d66411283 100644 --- a/mullvad-cli/src/cmds/version.rs +++ b/mullvad-cli/src/cmds/version.rs @@ -2,6 +2,7 @@ use crate::{new_rpc_client, Command, Result}; pub struct Version; +#[mullvad_management_interface::async_trait] impl Command for Version { fn name(&self) -> &'static str { "version" @@ -12,23 +13,24 @@ impl Command for Version { .about("Shows current version, and the currently supported versions") } - fn run(&self, _: &clap::ArgMatches<'_>) -> Result<()> { - let mut rpc = new_rpc_client()?; - let current_version = rpc.get_current_version()?; + async fn run(&self, _: &clap::ArgMatches<'_>) -> Result<()> { + let mut rpc = new_rpc_client().await?; + let current_version = rpc.get_current_version(()).await?.into_inner(); println!("Current version: {}", current_version); - let version_info = rpc.get_version_info()?; + let version_info = rpc.get_version_info(()).await?.into_inner(); println!("\tIs supported: {}", version_info.supported); - match version_info.suggested_upgrade { - Some(version) => println!("\tSuggested update: {}", version), - None => println!("\tNo newer version is available"), + if !version_info.suggested_upgrade.is_empty() { + println!("\tSuggested update: {}", version_info.suggested_upgrade); + } else { + println!("\tNo newer version is available"); } if !version_info.latest_stable.is_empty() { println!("\tLatest stable version: {}", version_info.latest_stable); } - let settings = rpc.get_settings()?; + let settings = rpc.get_settings(()).await?.into_inner(); if settings.show_beta_releases { println!("\t Latest beta version: {}", version_info.latest_beta); }; diff --git a/mullvad-cli/src/format.rs b/mullvad-cli/src/format.rs new file mode 100644 index 0000000000..4f69d05317 --- /dev/null +++ b/mullvad-cli/src/format.rs @@ -0,0 +1,20 @@ +use mullvad_management_interface::types::KeygenEvent; + +pub fn print_keygen_event(key_event: &KeygenEvent) { + use mullvad_management_interface::types::keygen_event::KeygenEvent as EventType; + + match EventType::from_i32(key_event.event).unwrap() { + EventType::NewKey => { + println!( + "New WireGuard key: {}", + base64::encode(&key_event.new_key.as_ref().unwrap().key) + ); + } + EventType::TooManyKeys => { + println!("Account has too many keys already"); + } + EventType::GenerationFailure => { + println!("Failed to generate new WireGuard key"); + } + } +} diff --git a/mullvad-cli/src/location.rs b/mullvad-cli/src/location.rs index 20853893b8..21e6a1c35c 100644 --- a/mullvad-cli/src/location.rs +++ b/mullvad-cli/src/location.rs @@ -1,4 +1,4 @@ -use mullvad_types::relay_constraints::{Constraint, LocationConstraint}; +use mullvad_management_interface::types::RelayLocation; pub fn get_subcommand() -> clap::App<'static, 'static> { clap::SubCommand::with_name("location") @@ -22,24 +22,33 @@ pub fn get_subcommand() -> clap::App<'static, 'static> { ) } -pub fn get_constraint(matches: &clap::ArgMatches<'_>) -> Constraint<LocationConstraint> { +pub fn get_constraint(matches: &clap::ArgMatches<'_>) -> RelayLocation { let country_original = matches.value_of("country").unwrap(); let country = country_original.to_lowercase(); let city = matches.value_of("city").map(str::to_lowercase); let hostname = matches.value_of("hostname").map(str::to_lowercase); match (country_original, city, hostname) { - ("any", None, None) => Constraint::Any, + ("any", None, None) => RelayLocation::default(), ("any", ..) => clap::Error::with_description( "City can't be given when selecting 'any' country", clap::ErrorKind::InvalidValue, ) .exit(), - (_, None, None) => Constraint::Only(LocationConstraint::Country(country)), - (_, Some(city), None) => Constraint::Only(LocationConstraint::City(country, city)), - (_, Some(city), Some(hostname)) => { - Constraint::Only(LocationConstraint::Hostname(country, city, hostname)) - } + (_, None, None) => RelayLocation { + country, + ..Default::default() + }, + (_, Some(city), None) => RelayLocation { + country, + city, + ..Default::default() + }, + (_, Some(city), Some(hostname)) => RelayLocation { + country, + city, + hostname, + }, (..) => clap::Error::with_description( "Invalid country, city and hostname combination given", clap::ErrorKind::InvalidValue, @@ -48,6 +57,22 @@ pub fn get_constraint(matches: &clap::ArgMatches<'_>) -> Constraint<LocationCons } } +pub fn format_location(location: Option<&RelayLocation>) -> String { + if let Some(location) = location { + if !location.hostname.is_empty() { + return format!( + "city {}, {}, hostname {}", + location.city, location.country, location.hostname + ); + } else if !location.city.is_empty() { + return format!("city {}, {}", location.city, location.country); + } else if !location.country.is_empty() { + return format!("country {}", location.country); + } + } + "any location".to_string() +} + fn country_code_validator(code: String) -> std::result::Result<(), String> { if code.len() == 2 || code == "any" { Ok(()) diff --git a/mullvad-cli/src/main.rs b/mullvad-cli/src/main.rs index 3d8d2a4aef..356a50ce69 100644 --- a/mullvad-cli/src/main.rs +++ b/mullvad-cli/src/main.rs @@ -1,11 +1,14 @@ #![deny(rust_2018_idioms)] use clap::{crate_authors, crate_description}; -use mullvad_ipc_client::{new_standalone_ipc_client, DaemonRpcClient}; +use mullvad_management_interface::async_trait; use std::{collections::HashMap, io}; use talpid_types::ErrorExt; +pub use mullvad_management_interface::{self, new_rpc_client}; + mod cmds; +mod format; mod location; pub const BIN_NAME: &str = "mullvad"; @@ -18,26 +21,20 @@ pub enum Error { #[error(display = "Failed to connect to daemon")] DaemonNotRunning(#[error(source)] io::Error), - #[error(display = "Can't subscribe to daemon states")] - CantSubscribe(#[error(source)] mullvad_ipc_client::PubSubError), + #[error(display = "Management interface error")] + ManagementInterfaceError(#[error(source)] mullvad_management_interface::Error), #[error(display = "Failed to communicate with mullvad-daemon over RPC")] - RpcClientError(#[error(source)] mullvad_ipc_client::Error), + GrpcClientError(#[error(source)] mullvad_management_interface::Status), /// The given command is not correct in some way #[error(display = "Invalid command: {}", _0)] InvalidCommand(&'static str), } -pub fn new_rpc_client() -> Result<DaemonRpcClient> { - match new_standalone_ipc_client(&mullvad_paths::get_rpc_socket_path()) { - Err(e) => Err(Error::DaemonNotRunning(e)), - Ok(client) => Ok(client), - } -} - -fn main() { - let exit_code = match run() { +#[tokio::main] +async fn main() { + let exit_code = match run().await { Ok(_) => 0, Err(error) => { eprintln!("{}", error.display_chain()); @@ -47,7 +44,7 @@ fn main() { std::process::exit(exit_code); } -fn run() -> Result<()> { +async fn run() -> Result<()> { env_logger::init(); let commands = cmds::get_commands(); @@ -85,7 +82,7 @@ fn run() -> Result<()> { } (sub_name, Some(sub_matches)) => { if let Some(cmd) = commands.get(sub_name) { - cmd.run(sub_matches) + cmd.run(sub_matches).await } else { unreachable!("No command matched"); } @@ -109,10 +106,11 @@ fn build_cli(commands: &HashMap<&'static str, Box<dyn Command>>) -> clap::App<'s .subcommands(commands.values().map(|cmd| cmd.clap_subcommand())) } +#[async_trait] pub trait Command { fn name(&self) -> &'static str; fn clap_subcommand(&self) -> clap::App<'static, 'static>; - fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()>; + async fn run(&self, matches: &clap::ArgMatches<'_>) -> Result<()>; } diff --git a/mullvad-daemon/Cargo.toml b/mullvad-daemon/Cargo.toml index a33f808ec2..f752b677a3 100644 --- a/mullvad-daemon/Cargo.toml +++ b/mullvad-daemon/Cargo.toml @@ -16,11 +16,6 @@ fern = { version = "0.5", features = ["colored"] } futures01 = { package = "futures", version = "0.1" } futures = { package = "futures", version = "0.3", features = [ "compat" ]} ipnetwork = "0.16" -jsonrpc-client-core = "0.5" -jsonrpc-core = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } -jsonrpc-ipc-server = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } -jsonrpc-macros = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } -jsonrpc-pubsub = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } lazy_static = "1.0" log = "0.4" log-panics = "2.0.0" @@ -35,14 +30,15 @@ tokio-retry = "0.2" tokio-timer = "0.1" uuid = { version = "0.7", features = ["v4"] } -mullvad-ipc-client = { path = "../mullvad-ipc-client" } mullvad-paths = { path = "../mullvad-paths" } mullvad-types = { path = "../mullvad-types" } mullvad-rpc = { path = "../mullvad-rpc" } talpid-core = { path = "../talpid-core" } -talpid-ipc = { path = "../talpid-ipc" } talpid-types = { path = "../talpid-types" } +mullvad-management-interface = { path = "../mullvad-management-interface" } +triggered = "0.1.1" + [target.'cfg(target_os="android")'.dependencies] android_logger = "0.8" diff --git a/mullvad-daemon/src/logging.rs b/mullvad-daemon/src/logging.rs index 88fd387c5d..58e10cd3b7 100644 --- a/mullvad-daemon/src/logging.rs +++ b/mullvad-daemon/src/logging.rs @@ -26,15 +26,12 @@ pub enum Error { pub const SILENCED_CRATES: &[&str] = &[ "h2", - "jsonrpc_core", - // jsonrpc_core does some logging under the "rpc" target as well. - "rpc", "tokio_core", "tokio_io", "tokio_proto", "tokio_reactor", "tokio_threadpool", - "jsonrpc_ws_server", + "tokio_util", "want", "ws", "mio", diff --git a/mullvad-daemon/src/main.rs b/mullvad-daemon/src/main.rs index 0b02dfbe5e..adb7c39f0a 100644 --- a/mullvad-daemon/src/main.rs +++ b/mullvad-daemon/src/main.rs @@ -86,8 +86,15 @@ fn run_platform(_config: &cli::Config, log_dir: Option<PathBuf>) -> Result<(), S } fn run_standalone(log_dir: Option<PathBuf>) -> Result<(), String> { - if rpc_uniqueness_check::is_another_instance_running() { - return Err("Another instance of the daemon is already running".to_owned()); + { + let mut runtime = tokio02::runtime::Builder::new() + .basic_scheduler() + .enable_all() + .build() + .map_err(|e| e.display_chain())?; + if runtime.block_on(rpc_uniqueness_check::is_another_instance_running()) { + return Err("Another instance of the daemon is already running".to_owned()); + } } if !running_as_admin() { diff --git a/mullvad-daemon/src/management_interface.rs b/mullvad-daemon/src/management_interface.rs index d593f0aa9d..c8ec8445d0 100644 --- a/mullvad-daemon/src/management_interface.rs +++ b/mullvad-daemon/src/management_interface.rs @@ -1,892 +1,1667 @@ -use crate::{BoxFuture, DaemonCommand, DaemonCommandSender, EventListener}; -use jsonrpc_core::{ - futures::{future, sync, Future}, - Error, ErrorCode, MetaIoHandler, Metadata, +use crate::{DaemonCommand, DaemonCommandSender, EventListener}; +use futures::compat::Future01CompatExt; +use futures01::{future, sync, Future}; +use mullvad_management_interface::{ + types::{self, daemon_event, management_service_server::ManagementService}, + Code, Request, Response, Status, }; -use jsonrpc_ipc_server; -use jsonrpc_macros::{build_rpc_trait, metadata, pubsub}; -use jsonrpc_pubsub::{PubSubHandler, PubSubMetadata, Session, SubscriptionId}; use mullvad_paths; use mullvad_rpc::{rest::Error as RestError, StatusCode}; use mullvad_types::{ - account::{AccountData, AccountToken, VoucherSubmission}, + account::AccountToken, location::GeoIpLocation, - relay_constraints::{BridgeSettings, BridgeState, RelaySettingsUpdate}, - relay_list::RelayList, - settings::Settings, + relay_constraints::{ + BridgeConstraints, BridgeSettings, BridgeState, Constraint, LocationConstraint, + OpenVpnConstraints, RelayConstraintsUpdate, RelaySettings, RelaySettingsUpdate, + WireguardConstraints, + }, + relay_list::{Relay, RelayList, RelayListCountry}, + settings::{Settings, TunnelOptions}, states::{TargetState, TunnelState}, - version, wireguard, DaemonEvent, + version, wireguard, ConnectionConfig, }; use parking_lot::RwLock; use std::{ - collections::{hash_map::Entry, HashMap}, - sync::Arc, + cmp, + sync::{mpsc, Arc}, +}; +use talpid_types::{ + net::{TransportProtocol, TunnelType}, + ErrorExt, }; -use talpid_ipc; -use talpid_types::ErrorExt; -use uuid; - -pub const INVALID_VOUCHER_CODE: i64 = -400; -pub const VOUCHER_USED_ALREADY_CODE: i64 = -401; -pub const INVALID_ACCOUNT_CODE: i64 = -200; - - -build_rpc_trait! { - pub trait ManagementInterfaceApi { - type Metadata; - - /// Creates and sets a new account - #[rpc(meta, name = "create_new_account")] - fn create_new_account(&self, Self::Metadata) -> BoxFuture<String, Error>; - - /// Fetches and returns metadata about an account. Returns an error on non-existing - /// accounts. - #[rpc(meta, name = "get_account_data")] - fn get_account_data(&self, Self::Metadata, AccountToken) -> BoxFuture<AccountData, Error>; - - #[rpc(meta, name = "get_www_auth_token")] - fn get_www_auth_token(&self, Self::Metadata) -> BoxFuture<String, Error>; - - /// Submit voucher to add time to account - #[rpc(meta, name = "submit_voucher")] - fn submit_voucher(&self, Self::Metadata, String) -> BoxFuture<VoucherSubmission, Error>; - - /// Returns available countries. - #[rpc(meta, name = "get_relay_locations")] - fn get_relay_locations(&self, Self::Metadata) -> BoxFuture<RelayList, Error>; - - /// Triggers a relay list update - #[rpc(meta, name = "update_relay_locations")] - fn update_relay_locations(&self, Self::Metadata) -> BoxFuture<(), Error>; - - /// Set which account to connect with. - #[rpc(meta, name = "set_account")] - fn set_account(&self, Self::Metadata, Option<AccountToken>) -> BoxFuture<(), Error>; - - /// Update constraints put on the type of tunnel connection to use - #[rpc(meta, name = "update_relay_settings")] - fn update_relay_settings( - &self, - Self::Metadata, RelaySettingsUpdate - ) -> BoxFuture<(), Error>; - - /// Set if the client should allow communication with the LAN while in secured state. - #[rpc(meta, name = "set_allow_lan")] - fn set_allow_lan(&self, Self::Metadata, bool) -> BoxFuture<(), Error>; - - /// Set whether to enable the beta program. - #[rpc(meta, name = "set_show_beta_releases")] - fn set_show_beta_releases(&self, Self::Metadata, bool) -> BoxFuture<(), Error>; - - /// Set if the client should allow network communication when in the disconnected state. - #[rpc(meta, name = "set_block_when_disconnected")] - fn set_block_when_disconnected(&self, Self::Metadata, bool) -> BoxFuture<(), Error>; - - /// Set if the daemon should automatically establish a tunnel on start or not. - #[rpc(meta, name = "set_auto_connect")] - fn set_auto_connect(&self, Self::Metadata, bool) -> BoxFuture<(), Error>; - - /// Try to connect if disconnected, or do nothing if already connecting/connected. - #[rpc(meta, name = "connect")] - fn connect(&self, Self::Metadata) -> BoxFuture<(), Error>; - - /// Disconnect the VPN tunnel if it is connecting/connected. Does nothing if already - /// disconnected. - #[rpc(meta, name = "disconnect")] - fn disconnect(&self, Self::Metadata) -> BoxFuture<(), Error>; - - /// Reconnect if connecting/connected, or do nothing if disconnected. - #[rpc(meta, name = "reconnect")] - fn reconnect(&self, Self::Metadata) -> BoxFuture<(), Error>; - - /// Returns the current state of the Mullvad client. Changes to this state will - /// be announced to subscribers of `new_state`. - #[rpc(meta, name = "get_state")] - fn get_state(&self, Self::Metadata) -> BoxFuture<TunnelState, Error>; - - /// Performs a geoIP lookup and returns the current location as perceived by the public - /// internet. - #[rpc(meta, name = "get_current_location")] - fn get_current_location(&self, Self::Metadata) -> BoxFuture<Option<GeoIpLocation>, Error>; - - /// Makes the daemon exit its main loop and quit. - #[rpc(meta, name = "shutdown")] - fn shutdown(&self, Self::Metadata) -> BoxFuture<(), Error>; - - /// Saves the target tunnel state and enters a blocking state. The state is restored - /// upon restart. - #[rpc(meta, name = "prepare_restart")] - fn prepare_restart(&self, Self::Metadata) -> BoxFuture<(), Error>; - - /// Get previously used account tokens from the account history - #[rpc(meta, name = "get_account_history")] - fn get_account_history(&self, Self::Metadata) -> BoxFuture<Vec<AccountToken>, Error>; - - /// Remove given account token from the account history - #[rpc(meta, name = "remove_account_from_history")] - fn remove_account_from_history(&self, Self::Metadata, AccountToken) -> BoxFuture<(), Error>; - - /// Removes all accounts from history, removing any associated keys in the process - #[rpc(meta, name = "clear_account_history")] - fn clear_account_history(&self, Self::Metadata) -> BoxFuture<(), Error>; - - /// Sets openvpn's mssfix parameter - #[rpc(meta, name = "set_openvpn_mssfix")] - fn set_openvpn_mssfix(&self, Self::Metadata, Option<u16>) -> BoxFuture<(), Error>; - - /// Sets proxy details for OpenVPN - #[rpc(meta, name = "set_bridge_settings")] - fn set_bridge_settings(&self, Self::Metadata, BridgeSettings) -> BoxFuture<(), Error>; - - /// Sets bridge state - #[rpc(meta, name = "set_bridge_state")] - fn set_bridge_state(&self, Self::Metadata, BridgeState) -> BoxFuture<(), Error>; - - /// Set if IPv6 is enabled in the tunnel - #[rpc(meta, name = "set_enable_ipv6")] - fn set_enable_ipv6(&self, Self::Metadata, bool) -> BoxFuture<(), Error>; - - /// Set MTU for wireguard tunnels - #[rpc(meta, name = "set_wireguard_mtu")] - fn set_wireguard_mtu(&self, Self::Metadata, Option<u16>) -> BoxFuture<(), Error>; - - /// Set automatic key rotation interval for wireguard tunnels - #[rpc(meta, name = "set_wireguard_rotation_interval")] - fn set_wireguard_rotation_interval(&self, Self::Metadata, Option<u32>) -> BoxFuture<(), Error>; - - /// Returns the current daemon settings - #[rpc(meta, name = "get_settings")] - fn get_settings(&self, Self::Metadata) -> BoxFuture<Settings, Error>; - - /// Generates new wireguard key for current account - #[rpc(meta, name = "generate_wireguard_key")] - fn generate_wireguard_key(&self, Self::Metadata) -> BoxFuture<wireguard::KeygenEvent, Error>; - - /// Retrieve a public key for current account if the account has one. - #[rpc(meta, name = "get_wireguard_key")] - fn get_wireguard_key(&self, Self::Metadata) -> BoxFuture<Option<wireguard::PublicKey>, Error>; - - /// Verify if current wireguard key is still valid - #[rpc(meta, name = "verify_wireguard_key")] - fn verify_wireguard_key(&self, Self::Metadata) -> BoxFuture<bool, Error>; - /// Retreive version of the app - #[rpc(meta, name = "get_current_version")] - fn get_current_version(&self, Self::Metadata) -> BoxFuture<String, Error>; +#[derive(err_derive::Error, Debug)] +#[error(no_from)] +pub enum Error { + // Unable to start the management interface server + #[error(display = "Unable to start management interface server")] + SetupError(#[error(source)] mullvad_management_interface::Error), - /// Retrieve information about the currently running and latest versions of the app - #[rpc(meta, name = "get_version_info")] - fn get_version_info(&self, Self::Metadata) -> BoxFuture<version::AppVersionInfo, Error>; + // Unable to start the tokio runtime + #[error(display = "Failed to create the tokio runtime")] + TokioRuntimeError(#[error(source)] tokio02::io::Error), +} - /// Remove all configuration and cache files - #[rpc(meta, name = "factory_reset")] - fn factory_reset(&self, Self::Metadata) -> BoxFuture<(), Error>; +struct ManagementServiceImpl { + daemon_tx: DaemonCommandSender, + subscriptions: Arc<RwLock<Vec<EventsListenerSender>>>, +} - /// Retrieve PIDs to exclude from the tunnel - #[rpc(meta, name = "get_split_tunnel_processes")] - fn get_split_tunnel_processes(&self, Self::Metadata) -> BoxFuture<Vec<i32>, Error>; +pub type ServiceResult<T> = std::result::Result<Response<T>, Status>; +type EventsListenerReceiver = + tokio02::sync::mpsc::UnboundedReceiver<Result<types::DaemonEvent, Status>>; +type EventsListenerSender = + tokio02::sync::mpsc::UnboundedSender<Result<types::DaemonEvent, Status>>; - /// Add a process to exclude from the tunnel - #[rpc(meta, name = "add_split_tunnel_process")] - fn add_split_tunnel_process(&self, Self::Metadata, i32) -> BoxFuture<(), Error>; +const INVALID_ACCOUNT_TOKEN_MESSAGE: &str = "No valid account token configured"; +const INVALID_VOUCHER_MESSAGE: &str = "This voucher code is invalid"; +const USED_VOUCHER_MESSAGE: &str = "This voucher code has already been used"; - /// Remove a process excluded from the tunnel - #[rpc(meta, name = "remove_split_tunnel_process")] - fn remove_split_tunnel_process(&self, Self::Metadata, i32) -> BoxFuture<(), Error>; +#[mullvad_management_interface::async_trait] +impl ManagementService for ManagementServiceImpl { + type GetRelayLocationsStream = + tokio02::sync::mpsc::Receiver<Result<types::RelayListCountry, Status>>; + type GetSplitTunnelProcessesStream = + tokio02::sync::mpsc::UnboundedReceiver<Result<i32, Status>>; + type EventsListenStream = EventsListenerReceiver; - /// Clear list of processes to exclude from the tunnel - #[rpc(meta, name = "clear_split_tunnel_processes")] - fn clear_split_tunnel_processes(&self, Self::Metadata) -> BoxFuture<(), Error>; + // Control and get the tunnel state + // - #[pubsub(name = "daemon_event")] { - /// Subscribes to events from the daemon. - #[rpc(name = "daemon_event_subscribe")] - fn daemon_event_subscribe( - &self, - Self::Metadata, - pubsub::Subscriber<DaemonEvent> - ); + async fn connect_tunnel(&self, _: Request<()>) -> ServiceResult<()> { + log::debug!("connect_tunnel"); - /// Unsubscribes from the `daemon_event` event notifications. - #[rpc(name = "daemon_event_unsubscribe")] - fn daemon_event_unsubscribe(&self, SubscriptionId) -> BoxFuture<(), Error>; - } + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetTargetState(tx, TargetState::Secured)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|result| match result { + Ok(()) => Ok(Response::new(())), + Err(()) => Err(Status::new( + Code::Unauthenticated, + INVALID_ACCOUNT_TOKEN_MESSAGE, + )), + }) + .compat() + .await } -} - -pub struct ManagementInterfaceServer { - server: talpid_ipc::IpcServer, - subscriptions: Arc<RwLock<HashMap<SubscriptionId, pubsub::Sink<DaemonEvent>>>>, -} -impl ManagementInterfaceServer { - pub fn start(tunnel_tx: DaemonCommandSender) -> Result<Self, talpid_ipc::Error> { - let rpc = ManagementInterface::new(tunnel_tx); - let subscriptions = rpc.subscriptions.clone(); - - let mut io = PubSubHandler::default(); - io.extend_with(rpc.to_delegate()); - let meta_io: MetaIoHandler<Meta> = io.into(); - let path = mullvad_paths::get_rpc_socket_path(); - let server = talpid_ipc::IpcServer::start_with_metadata( - meta_io, - meta_extractor, - &path.to_string_lossy(), - )?; - Ok(ManagementInterfaceServer { - server, - subscriptions, - }) - } + async fn disconnect_tunnel(&self, _: Request<()>) -> ServiceResult<()> { + log::debug!("disconnect_tunnel"); - pub fn socket_path(&self) -> &str { - self.server.path() + let (tx, _) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetTargetState(tx, TargetState::Unsecured)) + .then(|_| Ok(Response::new(()))) + .compat() + .await } - pub fn event_broadcaster(&self) -> ManagementInterfaceEventBroadcaster { - ManagementInterfaceEventBroadcaster { - subscriptions: self.subscriptions.clone(), - close_handle: Some(self.server.close_handle()), - } + async fn reconnect_tunnel(&self, _: Request<()>) -> ServiceResult<()> { + log::debug!("reconnect_tunnel"); + self.send_command_to_daemon(DaemonCommand::Reconnect) + .map(Response::new) + .compat() + .await } - /// Consumes the server and waits for it to finish. Returns an error if the server exited - /// due to an error. - pub fn wait(self) { - self.server.wait() + async fn get_tunnel_state(&self, _: Request<()>) -> ServiceResult<types::TunnelState> { + log::debug!("get_tunnel_state"); + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::GetState(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|state| Ok(Response::new(convert_state(state)))) + .compat() + .await } -} -/// A handle that allows broadcasting messages to all subscribers of the management interface. -#[derive(Clone)] -pub struct ManagementInterfaceEventBroadcaster { - subscriptions: Arc<RwLock<HashMap<SubscriptionId, pubsub::Sink<DaemonEvent>>>>, - close_handle: Option<talpid_ipc::CloseHandle>, -} + // Control the daemon and receive events + // -impl EventListener for ManagementInterfaceEventBroadcaster { - /// Sends a new state update to all `new_state` subscribers of the management interface. - fn notify_new_state(&self, new_state: TunnelState) { - self.notify(DaemonEvent::TunnelState(new_state)); - } + async fn events_listen(&self, _: Request<()>) -> ServiceResult<Self::EventsListenStream> { + let (tx, rx) = tokio02::sync::mpsc::unbounded_channel(); - /// Sends settings to all `settings` subscribers of the management interface. - fn notify_settings(&self, settings: Settings) { - log::debug!("Broadcasting new settings"); - self.notify(DaemonEvent::Settings(settings)); - } + let mut subscriptions = self.subscriptions.write(); + subscriptions.push(tx); - /// Sends settings to all `settings` subscribers of the management interface. - fn notify_relay_list(&self, relay_list: RelayList) { - log::debug!("Broadcasting new relay list"); - self.notify(DaemonEvent::RelayList(relay_list)); + Ok(Response::new(rx)) } - fn notify_app_version(&self, app_version_info: version::AppVersionInfo) { - log::debug!("Broadcasting new app version info"); - self.notify(DaemonEvent::AppVersionInfo(app_version_info)); + async fn prepare_restart(&self, _: Request<()>) -> ServiceResult<()> { + log::debug!("prepare_restart"); + self.send_command_to_daemon(DaemonCommand::PrepareRestart) + .map(Response::new) + .compat() + .await } - fn notify_key_event(&self, key_event: mullvad_types::wireguard::KeygenEvent) { - log::debug!("Broadcasting new wireguard key event"); - self.notify(DaemonEvent::WireguardKey(key_event)); + async fn shutdown(&self, _: Request<()>) -> ServiceResult<()> { + log::debug!("shutdown"); + self.send_command_to_daemon(DaemonCommand::Shutdown) + .map(Response::new) + .compat() + .await } -} -impl ManagementInterfaceEventBroadcaster { - fn notify(&self, value: DaemonEvent) { - let subscriptions = self.subscriptions.read(); - for sink in subscriptions.values() { - let _ = sink.notify(Ok(value.clone())).wait(); + async fn factory_reset(&self, _: Request<()>) -> ServiceResult<()> { + #[cfg(not(target_os = "android"))] + { + log::debug!("factory_reset"); + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::FactoryReset(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await + } + #[cfg(target_os = "android")] + { + Response::new(()) } } -} -impl Drop for ManagementInterfaceEventBroadcaster { - fn drop(&mut self) { - if let Some(close_handle) = self.close_handle.take() { - close_handle.close(); - } + async fn get_current_version(&self, _: Request<()>) -> ServiceResult<String> { + log::debug!("get_current_version"); + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::GetCurrentVersion(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } -} -struct ManagementInterface { - subscriptions: Arc<RwLock<HashMap<SubscriptionId, pubsub::Sink<DaemonEvent>>>>, - tx: DaemonCommandSender, -} + async fn get_version_info(&self, _: Request<()>) -> ServiceResult<types::AppVersionInfo> { + log::debug!("get_version_info"); -impl ManagementInterface { - pub fn new(tx: DaemonCommandSender) -> Self { - ManagementInterface { - subscriptions: Default::default(), - tx, - } - } + let (tx, rx) = sync::oneshot::channel(); + let app_version_info = self + .send_command_to_daemon(DaemonCommand::GetVersionInfo(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .compat() + .await?; - /// Sends a command to the daemon and maps the error to an RPC error. - fn send_command_to_daemon( - &self, - command: DaemonCommand, - ) -> impl Future<Item = (), Error = Error> { - future::result(self.tx.send(command)).map_err(|_| Error::internal_error()) + Ok(Response::new(convert_version_info(&app_version_info))) } - /// Converts a REST API error for an account into a JSONRPC error for the JSONRPC client. - fn map_rest_account_error(error: RestError) -> Error { - match error { - RestError::ApiError(status, message) - if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN => - { - Error { - code: ErrorCode::from(INVALID_ACCOUNT_CODE), - message, - data: None, - } - } - _ => Error::internal_error(), - } - } -} + // Relays and tunnel constraints + // -impl ManagementInterfaceApi for ManagementInterface { - type Metadata = Meta; + async fn update_relay_locations(&self, _: Request<()>) -> ServiceResult<()> { + log::debug!("update_relay_locations"); + self.send_command_to_daemon(DaemonCommand::UpdateRelayLocations) + .compat() + .await + .map(Response::new) + } - fn create_new_account(&self, _: Self::Metadata) -> BoxFuture<String, Error> { + async fn update_relay_settings( + &self, + request: Request<types::RelaySettingsUpdate>, + ) -> ServiceResult<()> { + log::debug!("update_relay_settings"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::CreateNewAccount(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())) - .and_then(|result| match result { - Ok(account_token) => Ok(account_token), - Err(_) => Err(Error::internal_error()), - }); + let constraints_update = convert_relay_settings_update(&request.into_inner())?; - Box::new(future) + let message = DaemonCommand::UpdateRelaySettings(tx, constraints_update); + self.send_command_to_daemon(message) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn get_account_data( + async fn get_relay_locations( &self, - _: Self::Metadata, - account_token: AccountToken, - ) -> BoxFuture<AccountData, Error> { - log::debug!("get_account_data"); + _: Request<()>, + ) -> ServiceResult<Self::GetRelayLocationsStream> { + log::debug!("get_relay_locations"); + let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetAccountData(tx, account_token)) - .and_then(|_| rx.map_err(|_| Error::internal_error())) - .and_then(|rpc_future| { - rpc_future.map_err(|error: RestError| { + let locations = self + .send_command_to_daemon(DaemonCommand::GetRelayLocations(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .compat() + .await?; + + let (mut stream_tx, stream_rx) = + tokio02::sync::mpsc::channel(cmp::max(1, locations.countries.len())); + + tokio02::spawn(async move { + for country in &locations.countries { + if let Err(error) = stream_tx + .send(Ok(convert_relay_list_country(country))) + .await + { log::error!( - "Unable to get account data from API: {}", + "Error while sending relays to client: {}", error.display_chain() ); - Self::map_rest_account_error(error) - }) - }); - Box::new(future) + } + } + }); + + Ok(Response::new(stream_rx)) } - fn get_www_auth_token(&self, _: Self::Metadata) -> BoxFuture<String, Error> { - log::debug!("get_account_data"); + async fn get_current_location(&self, _: Request<()>) -> ServiceResult<types::GeoIpLocation> { + log::debug!("get_current_location"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetWwwAuthToken(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())) - .and_then(|rpc_future| { - rpc_future.map_err(|error: mullvad_rpc::rest::Error| { - log::error!( - "Unable to get account data from API: {}", - error.display_chain() - ); - Self::map_rest_account_error(error) - }) - }); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::GetCurrentLocation(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|geoip| { + if let Some(geoip) = geoip { + Ok(Response::new(convert_geoip_location(geoip))) + } else { + Err(Status::not_found("no location was found")) + } + }) + .compat() + .await } - fn submit_voucher( + async fn set_bridge_settings( &self, - _: Self::Metadata, - voucher: String, - ) -> BoxFuture<VoucherSubmission, Error> { - log::debug!("submit_voucher"); - let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SubmitVoucher(tx, voucher)) - .and_then(|_| rx.map_err(|_| Error::internal_error())) - .and_then(|f| { - f.map_err(|e| match e { - RestError::ApiError(StatusCode::BAD_REQUEST, message) => { - match &message.as_str() { - &mullvad_rpc::INVALID_VOUCHER => Error { - code: ErrorCode::from(INVALID_VOUCHER_CODE), - message, - data: None, - }, + request: Request<types::BridgeSettings>, + ) -> ServiceResult<()> { + use talpid_types::net; + use types::bridge_settings::Type as BridgeSettingType; - &mullvad_rpc::VOUCHER_USED => Error { - code: ErrorCode::from(VOUCHER_USED_ALREADY_CODE), - message, - data: None, - }, + let settings = request + .into_inner() + .r#type + .ok_or(Status::invalid_argument("no settings provided"))?; - _ => Error::internal_error(), - } - } - _ => Error::internal_error(), + let settings = match settings { + BridgeSettingType::Normal(constraints) => { + let constraint = match constraints.location { + None => Constraint::Any, + Some(location) => convert_proto_location(location), + }; + + BridgeSettings::Normal(BridgeConstraints { + location: constraint, }) - }); - Box::new(future) - } + } + BridgeSettingType::Local(proxy_settings) => { + let peer = proxy_settings + .peer + .parse() + .map_err(|_| Status::invalid_argument("failed to parse peer address"))?; + let proxy_settings = + net::openvpn::ProxySettings::Local(net::openvpn::LocalProxySettings { + port: proxy_settings.port as u16, + peer, + }); + BridgeSettings::Custom(proxy_settings) + } + BridgeSettingType::Remote(proxy_settings) => { + let address = proxy_settings + .address + .parse() + .map_err(|_| Status::invalid_argument("failed to parse IP address"))?; + let auth = proxy_settings.auth.map(|auth| net::openvpn::ProxyAuth { + username: auth.username, + password: auth.password, + }); + let proxy_settings = + net::openvpn::ProxySettings::Remote(net::openvpn::RemoteProxySettings { + address, + auth, + }); + BridgeSettings::Custom(proxy_settings) + } + BridgeSettingType::Shadowsocks(proxy_settings) => { + let peer = proxy_settings + .peer + .parse() + .map_err(|_| Status::invalid_argument("failed to parse peer address"))?; + let proxy_settings = net::openvpn::ProxySettings::Shadowsocks( + net::openvpn::ShadowsocksProxySettings { + peer, + password: proxy_settings.password, + cipher: proxy_settings.cipher, + }, + ); + BridgeSettings::Custom(proxy_settings) + } + }; + + log::debug!("set_bridge_settings({:?})", settings); - fn get_relay_locations(&self, _: Self::Metadata) -> BoxFuture<RelayList, Error> { - log::debug!("get_relay_locations"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetRelayLocations(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetBridgeSettings(tx, settings)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|settings_result| { + settings_result.map_err(|_| Status::internal("internal error")) + }) + .map(Response::new) + .compat() + .await } - fn update_relay_locations(&self, _: Self::Metadata) -> BoxFuture<(), Error> { - log::debug!("update_relay_locations"); - Box::new(self.send_command_to_daemon(DaemonCommand::UpdateRelayLocations)) - } + async fn set_bridge_state(&self, request: Request<types::BridgeState>) -> ServiceResult<()> { + use types::bridge_state::State; - fn set_account( - &self, - _: Self::Metadata, - account_token: Option<AccountToken>, - ) -> BoxFuture<(), Error> { - log::debug!("set_account"); + let bridge_state = match State::from_i32(request.into_inner().state) { + Some(State::Auto) => BridgeState::Auto, + Some(State::On) => BridgeState::On, + Some(State::Off) => BridgeState::Off, + None => return Err(Status::invalid_argument("unknown bridge state")), + }; + + log::debug!("set_bridge_state({:?})", bridge_state); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetAccount(tx, account_token)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetBridgeState(tx, bridge_state)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|settings_result| { + settings_result.map_err(|_| Status::internal("internal error")) + }) + .map(Response::new) + .compat() + .await } - fn update_relay_settings( - &self, - _: Self::Metadata, - constraints_update: RelaySettingsUpdate, - ) -> BoxFuture<(), Error> { - log::debug!("update_relay_settings"); - let (tx, rx) = sync::oneshot::channel(); + // Settings + // - let message = DaemonCommand::UpdateRelaySettings(tx, constraints_update); - let future = self - .send_command_to_daemon(message) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + async fn get_settings(&self, _: Request<()>) -> ServiceResult<types::Settings> { + log::debug!("get_settings"); + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::GetSettings(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(|settings| Response::new(convert_settings(&settings))) + .compat() + .await } - fn set_allow_lan(&self, _: Self::Metadata, allow_lan: bool) -> BoxFuture<(), Error> { + async fn set_allow_lan(&self, request: Request<bool>) -> ServiceResult<()> { + let allow_lan = request.into_inner(); log::debug!("set_allow_lan({})", allow_lan); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetAllowLan(tx, allow_lan)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetAllowLan(tx, allow_lan)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn set_show_beta_releases(&self, _: Self::Metadata, enabled: bool) -> BoxFuture<(), Error> { + async fn set_show_beta_releases(&self, request: Request<bool>) -> ServiceResult<()> { + let enabled = request.into_inner(); log::debug!("set_show_beta_releases({})", enabled); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetShowBetaReleases(tx, enabled)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetShowBetaReleases(tx, enabled)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn set_block_when_disconnected( - &self, - _: Self::Metadata, - block_when_disconnected: bool, - ) -> BoxFuture<(), Error> { + async fn set_block_when_disconnected(&self, request: Request<bool>) -> ServiceResult<()> { + let block_when_disconnected = request.into_inner(); log::debug!("set_block_when_disconnected({})", block_when_disconnected); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetBlockWhenDisconnected( - tx, - block_when_disconnected, - )) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetBlockWhenDisconnected( + tx, + block_when_disconnected, + )) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn set_auto_connect(&self, _: Self::Metadata, auto_connect: bool) -> BoxFuture<(), Error> { + async fn set_auto_connect(&self, request: Request<bool>) -> ServiceResult<()> { + let auto_connect = request.into_inner(); log::debug!("set_auto_connect({})", auto_connect); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetAutoConnect(tx, auto_connect)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetAutoConnect(tx, auto_connect)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn connect(&self, _: Self::Metadata) -> BoxFuture<(), Error> { - log::debug!("connect"); + async fn set_openvpn_mssfix(&self, request: Request<u32>) -> ServiceResult<()> { + let mssfix = request.into_inner(); + let mssfix = if mssfix != 0 { + Some(mssfix as u16) + } else { + None + }; + log::debug!("set_openvpn_mssfix({:?})", mssfix); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetTargetState(tx, TargetState::Secured)) - .and_then(|_| rx.map_err(|_| Error::internal_error())) - .and_then(|result| match result { - Ok(()) => future::ok(()), - Err(()) => future::err(Error { - code: ErrorCode::ServerError(-900), - message: "No account token configured".to_owned(), - data: None, - }), - }); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetOpenVpnMssfix(tx, mssfix)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn disconnect(&self, _: Self::Metadata) -> BoxFuture<(), Error> { - log::debug!("disconnect"); - let (tx, _) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetTargetState(tx, TargetState::Unsecured)) - .then(|_| future::ok(())); - Box::new(future) + async fn set_wireguard_mtu(&self, request: Request<u32>) -> ServiceResult<()> { + let mtu = request.into_inner(); + let mtu = if mtu != 0 { Some(mtu as u16) } else { None }; + log::debug!("set_wireguard_mtu({:?})", mtu); + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetWireguardMtu(tx, mtu)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn reconnect(&self, _: Self::Metadata) -> BoxFuture<(), Error> { - log::debug!("reconnect"); - let future = self.send_command_to_daemon(DaemonCommand::Reconnect); - Box::new(future) + async fn set_enable_ipv6(&self, request: Request<bool>) -> ServiceResult<()> { + let enable_ipv6 = request.into_inner(); + log::debug!("set_enable_ipv6({})", enable_ipv6); + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetEnableIpv6(tx, enable_ipv6)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn get_state(&self, _: Self::Metadata) -> BoxFuture<TunnelState, Error> { - log::debug!("get_state"); - let (state_tx, state_rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetState(state_tx)) - .and_then(|_| state_rx.map_err(|_| Error::internal_error())); - Box::new(future) - } + // Account management + // - fn get_current_location(&self, _: Self::Metadata) -> BoxFuture<Option<GeoIpLocation>, Error> { - log::debug!("get_current_location"); + async fn create_new_account(&self, _: Request<()>) -> ServiceResult<String> { let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetCurrentLocation(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::CreateNewAccount(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|result| match result { + Ok(account_token) => Ok(Response::new(account_token)), + Err(_) => Err(Status::internal("internal error")), + }) + .compat() + .await } - fn shutdown(&self, _: Self::Metadata) -> BoxFuture<(), Error> { - log::debug!("shutdown"); - Box::new(self.send_command_to_daemon(DaemonCommand::Shutdown)) + async fn set_account(&self, request: Request<AccountToken>) -> ServiceResult<()> { + log::debug!("set_account"); + let account_token = request.into_inner(); + let account_token = if account_token == "" { + None + } else { + Some(account_token) + }; + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::SetAccount(tx, account_token)) + .and_then(|_| { + rx.map(Response::new) + .map_err(|_| Status::internal("internal error")) + }) + .compat() + .await } - fn prepare_restart(&self, _: Self::Metadata) -> BoxFuture<(), Error> { - log::debug!("prepare_restart"); - Box::new(self.send_command_to_daemon(DaemonCommand::PrepareRestart)) + async fn get_account_data( + &self, + request: Request<AccountToken>, + ) -> ServiceResult<types::AccountData> { + log::debug!("get_account_data"); + let account_token = request.into_inner(); + let (tx, rx) = sync::oneshot::channel(); + self.send_command_to_daemon(DaemonCommand::GetAccountData(tx, account_token)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|rpc_future| { + rpc_future + .map(|account_data| { + Response::new(types::AccountData { + expiry: Some(types::Timestamp { + seconds: account_data.expiry.timestamp(), + nanos: 0, + }), + }) + }) + .map_err(|error: RestError| { + log::error!( + "Unable to get account data from API: {}", + error.display_chain() + ); + map_rest_account_error(error) + }) + }) + .compat() + .await } - fn get_account_history(&self, _: Self::Metadata) -> BoxFuture<Vec<AccountToken>, Error> { + async fn get_account_history(&self, _: Request<()>) -> ServiceResult<types::AccountHistory> { + // TODO: this might be a stream log::debug!("get_account_history"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetAccountHistory(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::GetAccountHistory(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(|history| Response::new(types::AccountHistory { token: history })) + .compat() + .await } - fn remove_account_from_history( + async fn remove_account_from_history( &self, - _: Self::Metadata, - account_token: AccountToken, - ) -> BoxFuture<(), Error> { + request: Request<AccountToken>, + ) -> ServiceResult<()> { log::debug!("remove_account_from_history"); + let account_token = request.into_inner(); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::RemoveAccountFromHistory(tx, account_token)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::RemoveAccountFromHistory(tx, account_token)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn clear_account_history(&self, _: Self::Metadata) -> BoxFuture<(), Error> { + async fn clear_account_history(&self, _: Request<()>) -> ServiceResult<()> { log::debug!("clear_account_history"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::ClearAccountHistory(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::ClearAccountHistory(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn set_openvpn_mssfix(&self, _: Self::Metadata, mssfix: Option<u16>) -> BoxFuture<(), Error> { - log::debug!("set_openvpn_mssfix({:?})", mssfix); + async fn get_www_auth_token(&self, _: Request<()>) -> ServiceResult<String> { + log::debug!("get_www_auth_token"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetOpenVpnMssfix(tx, mssfix)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - - Box::new(future) + self.send_command_to_daemon(DaemonCommand::GetWwwAuthToken(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|rpc_future| { + rpc_future + .map(Response::new) + .map_err(|error: mullvad_rpc::rest::Error| { + log::error!( + "Unable to get account data from API: {}", + error.display_chain() + ); + map_rest_account_error(error) + }) + }) + .compat() + .await } - fn set_bridge_settings( + async fn submit_voucher( &self, - _: Self::Metadata, - bridge_settings: BridgeSettings, - ) -> BoxFuture<(), Error> { - log::debug!("set_bridge_settings({:?})", bridge_settings); + request: Request<String>, + ) -> ServiceResult<types::VoucherSubmission> { + log::debug!("submit_voucher"); + let voucher = request.into_inner(); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetBridgeSettings(tx, bridge_settings)) - .and_then(|_| rx.map_err(|_| Error::internal_error())) - .and_then(|settings_result| settings_result.map_err(|_| Error::internal_error())); - - Box::new(future) - } + self.send_command_to_daemon(DaemonCommand::SubmitVoucher(tx, voucher)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .and_then(|f| { + f.map(|submission| { + Response::new(types::VoucherSubmission { + seconds_added: submission.time_added, + new_expiry: Some(types::Timestamp { + seconds: submission.new_expiry.timestamp(), + nanos: 0, + }), + }) + }) + .map_err(|e| match e { + RestError::ApiError(StatusCode::BAD_REQUEST, message) => { + match &message.as_str() { + &mullvad_rpc::INVALID_VOUCHER => { + Status::new(Code::NotFound, INVALID_VOUCHER_MESSAGE) + } - fn set_bridge_state( - &self, - _: Self::Metadata, - bridge_state: BridgeState, - ) -> BoxFuture<(), Error> { - log::debug!("set_bridge_state({:?})", bridge_state); - let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetBridgeState(tx, bridge_state)) - .and_then(|_| rx.map_err(|_| Error::internal_error())) - .and_then(|settings_result| settings_result.map_err(|_| Error::internal_error())); + &mullvad_rpc::VOUCHER_USED => { + Status::new(Code::ResourceExhausted, USED_VOUCHER_MESSAGE) + } - Box::new(future) + _ => Status::internal("internal error"), + } + } + _ => Status::internal("internal error"), + }) + }) + .compat() + .await } - fn set_enable_ipv6(&self, _: Self::Metadata, enable_ipv6: bool) -> BoxFuture<(), Error> { - log::debug!("set_enable_ipv6({})", enable_ipv6); - let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetEnableIpv6(tx, enable_ipv6)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); + // WireGuard key management + // - Box::new(future) - } + async fn set_wireguard_rotation_interval(&self, request: Request<u32>) -> ServiceResult<()> { + let interval = request.into_inner(); - /// Set MTU for wireguard tunnels - fn set_wireguard_mtu(&self, _: Self::Metadata, mtu: Option<u16>) -> BoxFuture<(), Error> { - log::debug!("set_wireguard_mtu({:?})", mtu); - let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetWireguardMtu(tx, mtu)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) - } - - /// Set automatic key rotation interval for wireguard tunnels - fn set_wireguard_rotation_interval( - &self, - _: Self::Metadata, - interval: Option<u32>, - ) -> BoxFuture<(), Error> { log::debug!("set_wireguard_rotation_interval({:?})", interval); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::SetWireguardRotationInterval(tx, interval)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetWireguardRotationInterval( + tx, + Some(interval), + )) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn get_settings(&self, _: Self::Metadata) -> BoxFuture<Settings, Error> { - log::debug!("get_settings"); + async fn reset_wireguard_rotation_interval(&self, _: Request<()>) -> ServiceResult<()> { + log::debug!("reset_wireguard_rotation_interval"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetSettings(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::SetWireguardRotationInterval(tx, None)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn generate_wireguard_key( - &self, - _: Self::Metadata, - ) -> BoxFuture<mullvad_types::wireguard::KeygenEvent, Error> { + async fn generate_wireguard_key(&self, _: Request<()>) -> ServiceResult<types::KeygenEvent> { + // TODO: return error for TooManyKeys, GenerationFailure + // on success, simply return the new key or nil log::debug!("generate_wireguard_key"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GenerateWireguardKey(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::GenerateWireguardKey(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(|event| Response::new(convert_wireguard_key_event(&event))) + .compat() + .await } - fn get_wireguard_key( - &self, - _: Self::Metadata, - ) -> BoxFuture<Option<wireguard::PublicKey>, Error> { + async fn get_wireguard_key(&self, _: Request<()>) -> ServiceResult<types::PublicKey> { log::debug!("get_wireguard_key"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetWireguardKey(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::GetWireguardKey(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .then(|response| match response { + Ok(Some(key)) => Ok(Response::new(convert_public_key(&key))), + Ok(None) => Err(Status::not_found("no WireGuard key was found")), + Err(e) => Err(e), + }) + .compat() + .await } - fn verify_wireguard_key(&self, _: Self::Metadata) -> BoxFuture<bool, Error> { + async fn verify_wireguard_key(&self, _: Request<()>) -> ServiceResult<bool> { log::debug!("verify_wireguard_key"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::VerifyWireguardKey(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::VerifyWireguardKey(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } - fn get_current_version(&self, _: Self::Metadata) -> BoxFuture<String, Error> { - log::debug!("get_current_version"); - let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetCurrentVersion(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - - Box::new(future) - } - - fn get_version_info(&self, _: Self::Metadata) -> BoxFuture<version::AppVersionInfo, Error> { - log::debug!("get_version_info"); - let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::GetVersionInfo(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); + // Split tunneling + // - Box::new(future) - } - - fn factory_reset(&self, _: Self::Metadata) -> BoxFuture<(), Error> { - #[cfg(not(target_os = "android"))] - { - log::debug!("factory_reset"); - let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::FactoryReset(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - - Box::new(future) - } - #[cfg(target_os = "android")] - { - Box::new(future::ok(())) - } - } - - fn get_split_tunnel_processes(&self, _: Self::Metadata) -> BoxFuture<Vec<i32>, Error> { + async fn get_split_tunnel_processes( + &self, + _: Request<()>, + ) -> ServiceResult<Self::GetSplitTunnelProcessesStream> { #[cfg(target_os = "linux")] { log::debug!("get_split_tunnel_processes"); let (tx, rx) = sync::oneshot::channel(); - let future = self + let pids = self .send_command_to_daemon(DaemonCommand::GetSplitTunnelProcesses(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .compat() + .await?; + + let (tx, rx) = tokio02::sync::mpsc::unbounded_channel(); + tokio02::spawn(async move { + for pid in pids { + let _ = tx.send(Ok(pid)); + } + }); + + Ok(Response::new(rx)) } #[cfg(not(target_os = "linux"))] { - Box::new(future::ok(Vec::with_capacity(0))) + let (_, rx) = tokio02::sync::mpsc::unbounded_channel(); + Ok(Response::new(rx)) } } #[cfg(target_os = "linux")] - fn add_split_tunnel_process(&self, _: Self::Metadata, pid: i32) -> BoxFuture<(), Error> { + async fn add_split_tunnel_process(&self, request: Request<i32>) -> ServiceResult<()> { + let pid = request.into_inner(); log::debug!("add_split_tunnel_process"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::AddSplitTunnelProcess(tx, pid)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::AddSplitTunnelProcess(tx, pid)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } #[cfg(not(target_os = "linux"))] - fn add_split_tunnel_process(&self, _: Self::Metadata, _: i32) -> BoxFuture<(), Error> { - Box::new(future::ok(())) + async fn add_split_tunnel_process(&self, _: Request<i32>) -> ServiceResult<()> { + Ok(Response::new(())) } #[cfg(target_os = "linux")] - fn remove_split_tunnel_process(&self, _: Self::Metadata, pid: i32) -> BoxFuture<(), Error> { + async fn remove_split_tunnel_process(&self, request: Request<i32>) -> ServiceResult<()> { + let pid = request.into_inner(); log::debug!("remove_split_tunnel_process"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::RemoveSplitTunnelProcess(tx, pid)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::RemoveSplitTunnelProcess(tx, pid)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } #[cfg(not(target_os = "linux"))] - fn remove_split_tunnel_process(&self, _: Self::Metadata, _: i32) -> BoxFuture<(), Error> { - Box::new(future::ok(())) + async fn remove_split_tunnel_process(&self, _: Request<i32>) -> ServiceResult<()> { + Ok(Response::new(())) } - fn clear_split_tunnel_processes(&self, _: Self::Metadata) -> BoxFuture<(), Error> { + async fn clear_split_tunnel_processes(&self, _: Request<()>) -> ServiceResult<()> { #[cfg(target_os = "linux")] { log::debug!("clear_split_tunnel_processes"); let (tx, rx) = sync::oneshot::channel(); - let future = self - .send_command_to_daemon(DaemonCommand::ClearSplitTunnelProcesses(tx)) - .and_then(|_| rx.map_err(|_| Error::internal_error())); - Box::new(future) + self.send_command_to_daemon(DaemonCommand::ClearSplitTunnelProcesses(tx)) + .and_then(|_| rx.map_err(|_| Status::internal("internal error"))) + .map(Response::new) + .compat() + .await } #[cfg(not(target_os = "linux"))] { - Box::new(future::ok(())) + Ok(Response::new(())) } } +} - - fn daemon_event_subscribe( +impl ManagementServiceImpl { + /// Sends a command to the daemon and maps the error to an RPC error. + fn send_command_to_daemon( &self, - _: Self::Metadata, - subscriber: pubsub::Subscriber<DaemonEvent>, - ) { - log::debug!("daemon_event_subscribe"); - let mut subscriptions = self.subscriptions.write(); - loop { - let id = SubscriptionId::String(uuid::Uuid::new_v4().to_string()); - if let Entry::Vacant(entry) = subscriptions.entry(id.clone()) { - if let Ok(sink) = subscriber.assign_id(id.clone()) { - log::debug!("Accepting new subscription with id {:?}", id); - entry.insert(sink); + command: DaemonCommand, + ) -> impl Future<Item = (), Error = Status> { + future::result( + self.daemon_tx + .send(command) + .map_err(|_| Status::internal("internal error")), + ) + } +} + +fn convert_settings(settings: &Settings) -> types::Settings { + types::Settings { + account_token: settings.get_account_token().unwrap_or_default(), + relay_settings: Some(convert_relay_settings(&settings.get_relay_settings())), + bridge_settings: Some(convert_bridge_settings(&settings.bridge_settings)), + bridge_state: Some(convert_bridge_state(settings.get_bridge_state())), + allow_lan: settings.allow_lan, + block_when_disconnected: settings.block_when_disconnected, + auto_connect: settings.auto_connect, + tunnel_options: Some(convert_tunnel_options(&settings.tunnel_options)), + show_beta_releases: settings.show_beta_releases, + } +} + +fn convert_relay_settings_update( + settings: &types::RelaySettingsUpdate, +) -> Result<RelaySettingsUpdate, Status> { + use mullvad_types::CustomTunnelEndpoint; + use talpid_types::net::{self, openvpn, wireguard}; + use types::{ + connection_config::Config as ProtoConnectionConfig, + relay_settings_update::Type as ProtoUpdateType, + }; + + let update_value = settings + .r#type + .clone() + .ok_or(Status::invalid_argument("missing relay settings"))?; + + match update_value { + ProtoUpdateType::Custom(settings) => { + let config = settings + .config + .ok_or(Status::invalid_argument("missing relay settings"))?; + let config = config + .config + .ok_or(Status::invalid_argument("missing relay settings"))?; + let config = match config { + ProtoConnectionConfig::Openvpn(config) => { + let address = match config.address.parse() { + Ok(address) => address, + Err(_) => return Err(Status::invalid_argument("invalid address")), + }; + + ConnectionConfig::OpenVpn(openvpn::ConnectionConfig { + endpoint: net::Endpoint { + address, + protocol: match types::TransportProtocol::from_i32(config.protocol) { + Some(types::TransportProtocol::Udp) => TransportProtocol::Udp, + Some(types::TransportProtocol::Tcp) => TransportProtocol::Tcp, + None => { + return Err(Status::invalid_argument( + "invalid transport protocol", + )) + } + }, + }, + username: config.username.clone(), + password: config.password.clone(), + }) + } + ProtoConnectionConfig::Wireguard(config) => { + let tunnel = config + .tunnel + .ok_or(Status::invalid_argument("missing tunnel config"))?; + + // Copy the private key to an array + if tunnel.private_key.len() != 32 { + return Err(Status::invalid_argument("invalid private key")); + } + + let mut private_key = [0; 32]; + let buffer = &tunnel.private_key[..private_key.len()]; + private_key.copy_from_slice(buffer); + + let peer = config + .peer + .ok_or(Status::invalid_argument("missing peer config"))?; + + // Copy the public key to an array + if peer.public_key.len() != 32 { + return Err(Status::invalid_argument("invalid public key")); + } + + let mut public_key = [0; 32]; + let buffer = &peer.public_key[..public_key.len()]; + public_key.copy_from_slice(buffer); + + let ipv4_gateway = match config.ipv4_gateway.parse() { + Ok(address) => address, + Err(_) => return Err(Status::invalid_argument("invalid IPv4 gateway")), + }; + let ipv6_gateway = if !config.ipv6_gateway.is_empty() { + let address = match config.ipv6_gateway.parse() { + Ok(address) => address, + Err(_) => return Err(Status::invalid_argument("invalid IPv6 gateway")), + }; + Some(address) + } else { + None + }; + + let endpoint = match peer.endpoint.parse() { + Ok(address) => address, + Err(_) => return Err(Status::invalid_argument("invalid peer address")), + }; + + let mut tunnel_addresses = Vec::new(); + for address in tunnel.addresses { + let address = address + .parse() + .map_err(|_| Status::invalid_argument("invalid address"))?; + tunnel_addresses.push(address); + } + + let mut allowed_ips = Vec::new(); + for address in peer.allowed_ips { + let address = address + .parse() + .map_err(|_| Status::invalid_argument("invalid address"))?; + allowed_ips.push(address); + } + + ConnectionConfig::Wireguard(wireguard::ConnectionConfig { + tunnel: wireguard::TunnelConfig { + private_key: wireguard::PrivateKey::from(private_key), + addresses: tunnel_addresses, + }, + peer: wireguard::PeerConfig { + public_key: wireguard::PublicKey::from(public_key), + allowed_ips, + endpoint, + }, + ipv4_gateway, + ipv6_gateway, + }) } - break; + }; + + Ok(RelaySettingsUpdate::CustomTunnelEndpoint( + CustomTunnelEndpoint { + host: settings.host.clone(), + config, + }, + )) + } + + ProtoUpdateType::Normal(settings) => { + // If `location` isn't provided, no changes are made. + // If `location` is provided, but is an empty vector, + // then the constraint is set to `Constraint::Any`. + let location = settings.location.map(convert_proto_location); + + let tunnel_protocol = if let Some(update) = settings.tunnel_type { + match update.tunnel_type { + Some(constraint) => match types::TunnelType::from_i32(constraint.tunnel_type) { + Some(types::TunnelType::Openvpn) => { + Some(Constraint::Only(TunnelType::OpenVpn)) + } + Some(types::TunnelType::Wireguard) => { + Some(Constraint::Only(TunnelType::Wireguard)) + } + None => return Err(Status::invalid_argument("unknown tunnel protocol")), + }, + None => Some(Constraint::Any), + } + } else { + None + }; + + let transport_protocol = if let Some(ref constraints) = settings.openvpn_constraints { + match &constraints.protocol { + Some(constraint) => { + match types::TransportProtocol::from_i32(constraint.protocol) { + Some(types::TransportProtocol::Udp) => Some(TransportProtocol::Udp), + Some(types::TransportProtocol::Tcp) => Some(TransportProtocol::Tcp), + None => { + return Err(Status::invalid_argument("unknown transport protocol")) + } + } + } + None => None, + } + } else { + None + }; + + Ok(RelaySettingsUpdate::Normal(RelayConstraintsUpdate { + location, + tunnel_protocol, + wireguard_constraints: settings.wireguard_constraints.map(|constraints| { + WireguardConstraints { + port: if constraints.port != 0 { + Constraint::Only(constraints.port as u16) + } else { + Constraint::Any + }, + } + }), + openvpn_constraints: settings.openvpn_constraints.map(|constraints| { + OpenVpnConstraints { + port: if constraints.port != 0 { + Constraint::Only(constraints.port as u16) + } else { + Constraint::Any + }, + protocol: Constraint::from(transport_protocol), + } + }), + })) + } + } +} + +fn convert_relay_settings(settings: &RelaySettings) -> types::RelaySettings { + use types::relay_settings; + + let endpoint = match settings { + RelaySettings::CustomTunnelEndpoint(endpoint) => { + relay_settings::Endpoint::Custom(types::CustomRelaySettings { + host: endpoint.host.clone(), + config: Some(convert_connection_config(&endpoint.config)), + }) + } + RelaySettings::Normal(constraints) => { + relay_settings::Endpoint::Normal(types::NormalRelaySettings { + location: convert_location_constraint(&constraints.location), + tunnel_type: match constraints.tunnel_protocol { + Constraint::Any => None, + Constraint::Only(TunnelType::Wireguard) => Some(types::TunnelType::Wireguard), + Constraint::Only(TunnelType::OpenVpn) => Some(types::TunnelType::Openvpn), + } + .map(|tunnel_type| types::TunnelTypeConstraint { + tunnel_type: i32::from(tunnel_type), + }), + + wireguard_constraints: Some(types::WireguardConstraints { + port: u32::from(constraints.wireguard_constraints.port.unwrap_or(0)), + }), + + openvpn_constraints: Some(types::OpenvpnConstraints { + port: u32::from(constraints.openvpn_constraints.port.unwrap_or(0)), + protocol: constraints + .openvpn_constraints + .protocol + .as_ref() + .option() + .map(|protocol| match protocol { + TransportProtocol::Tcp => types::TransportProtocol::Tcp, + TransportProtocol::Udp => types::TransportProtocol::Udp, + }) + .map(|protocol| types::TransportProtocolConstraint { + protocol: i32::from(protocol), + }), + }), + }) + } + }; + + types::RelaySettings { + endpoint: Some(endpoint), + } +} + +fn convert_connection_config(config: &ConnectionConfig) -> types::ConnectionConfig { + use types::connection_config; + + types::ConnectionConfig { + config: Some(match config { + ConnectionConfig::OpenVpn(config) => { + connection_config::Config::Openvpn(connection_config::OpenvpnConfig { + address: config.endpoint.address.to_string(), + protocol: match config.endpoint.protocol { + TransportProtocol::Tcp => i32::from(types::TransportProtocol::Tcp), + TransportProtocol::Udp => i32::from(types::TransportProtocol::Udp), + }, + username: config.username.clone(), + password: config.password.clone(), + }) } + ConnectionConfig::Wireguard(config) => { + connection_config::Config::Wireguard(connection_config::WireguardConfig { + tunnel: Some(connection_config::wireguard_config::TunnelConfig { + private_key: config.tunnel.private_key.to_bytes().to_vec(), + addresses: config + .tunnel + .addresses + .iter() + .map(|address| address.to_string()) + .collect(), + }), + peer: Some(connection_config::wireguard_config::PeerConfig { + public_key: config.peer.public_key.as_bytes().to_vec(), + allowed_ips: config + .peer + .allowed_ips + .iter() + .map(|address| address.to_string()) + .collect(), + endpoint: config.peer.endpoint.to_string(), + }), + ipv4_gateway: config.ipv4_gateway.to_string(), + ipv6_gateway: config + .ipv6_gateway + .as_ref() + .map(|address| address.to_string()) + .unwrap_or_default(), + }) + } + }), + } +} + +fn convert_bridge_settings(settings: &BridgeSettings) -> types::BridgeSettings { + use talpid_types::net; + use types::bridge_settings::{self, Type as BridgeSettingType}; + + let settings = match settings { + BridgeSettings::Normal(constraints) => { + BridgeSettingType::Normal(types::bridge_settings::BridgeConstraints { + location: convert_location_constraint(&constraints.location), + }) } + BridgeSettings::Custom(proxy_settings) => match proxy_settings { + net::openvpn::ProxySettings::Local(proxy_settings) => { + BridgeSettingType::Local(bridge_settings::LocalProxySettings { + port: u32::from(proxy_settings.port), + peer: proxy_settings.peer.to_string(), + }) + } + net::openvpn::ProxySettings::Remote(proxy_settings) => { + BridgeSettingType::Remote(bridge_settings::RemoteProxySettings { + address: proxy_settings.address.to_string(), + auth: proxy_settings.auth.as_ref().map(|auth| { + bridge_settings::RemoteProxyAuth { + username: auth.username.clone(), + password: auth.password.clone(), + } + }), + }) + } + net::openvpn::ProxySettings::Shadowsocks(proxy_settings) => { + BridgeSettingType::Shadowsocks(bridge_settings::ShadowsocksProxySettings { + peer: proxy_settings.peer.to_string(), + password: proxy_settings.password.clone(), + cipher: proxy_settings.cipher.clone(), + }) + } + }, + }; + + types::BridgeSettings { + r#type: Some(settings), } +} + +fn convert_wireguard_key_event( + event: &mullvad_types::wireguard::KeygenEvent, +) -> types::KeygenEvent { + use mullvad_types::wireguard::KeygenEvent::*; + use types::keygen_event::KeygenEvent as ProtoEvent; - fn daemon_event_unsubscribe(&self, id: SubscriptionId) -> BoxFuture<(), Error> { - log::debug!("daemon_event_unsubscribe"); - let was_removed = self.subscriptions.write().remove(&id).is_some(); - let result = if was_removed { - log::debug!("Unsubscribing id {:?}", id); - future::ok(()) + types::KeygenEvent { + event: match event { + NewKey(_) => i32::from(ProtoEvent::NewKey), + TooManyKeys => i32::from(ProtoEvent::TooManyKeys), + GenerationFailure => i32::from(ProtoEvent::GenerationFailure), + }, + new_key: if let NewKey(key) = event { + Some(convert_public_key(&key)) } else { - future::err(Error { - code: ErrorCode::InvalidParams, - message: "Invalid subscription".to_owned(), - data: None, - }) + None + }, + } +} + +fn convert_public_key(public_key: &wireguard::PublicKey) -> types::PublicKey { + types::PublicKey { + key: public_key.key.as_bytes().to_vec(), + created: Some(types::Timestamp { + seconds: public_key.created.timestamp(), + nanos: 0, + }), + } +} + +fn convert_location_constraint( + location: &Constraint<LocationConstraint>, +) -> Option<types::RelayLocation> { + location.as_ref().option().map(|location| match location { + LocationConstraint::Country(country) => types::RelayLocation { + country: country.to_string(), + ..Default::default() + }, + LocationConstraint::City(country, city) => types::RelayLocation { + country: country.to_string(), + city: city.to_string(), + ..Default::default() + }, + LocationConstraint::Hostname(country, city, hostname) => types::RelayLocation { + country: country.to_string(), + city: city.to_string(), + hostname: hostname.to_string(), + }, + }) +} + +fn convert_bridge_state(state: &BridgeState) -> types::BridgeState { + let state = match state { + BridgeState::Auto => types::bridge_state::State::Auto, + BridgeState::On => types::bridge_state::State::On, + BridgeState::Off => types::bridge_state::State::Off, + }; + types::BridgeState { + state: i32::from(state), + } +} + +fn convert_tunnel_options(options: &TunnelOptions) -> types::TunnelOptions { + use types::tunnel_options::wireguard_options::RotationInterval; + + types::TunnelOptions { + openvpn: Some(types::tunnel_options::OpenvpnOptions { + mssfix: u32::from(options.openvpn.mssfix.unwrap_or_default()), + }), + wireguard: Some(types::tunnel_options::WireguardOptions { + mtu: u32::from(options.wireguard.mtu.unwrap_or_default()), + automatic_rotation: options + .wireguard + .automatic_rotation + .map(|interval| RotationInterval { interval }), + }), + generic: Some(types::tunnel_options::GenericOptions { + enable_ipv6: options.generic.enable_ipv6, + }), + } +} + +fn convert_relay_list_country(country: &RelayListCountry) -> types::RelayListCountry { + let mut proto_country = types::RelayListCountry { + name: country.name.clone(), + code: country.code.clone(), + cities: Vec::with_capacity(country.cities.len()), + }; + + for city in &country.cities { + proto_country.cities.push(types::RelayListCity { + name: city.name.clone(), + code: city.code.clone(), + latitude: city.latitude, + longitude: city.longitude, + relays: city + .relays + .iter() + .map(|relay| convert_relay(relay)) + .collect(), + }); + } + + proto_country +} + +fn convert_relay(relay: &Relay) -> types::Relay { + types::Relay { + hostname: relay.hostname.clone(), + ipv4_addr_in: relay.ipv4_addr_in.to_string(), + ipv6_addr_in: relay + .ipv6_addr_in + .map(|addr| addr.to_string()) + .unwrap_or_default(), + include_in_country: relay.include_in_country, + active: relay.active, + owned: relay.owned, + provider: relay.provider.clone(), + weight: relay.weight, + tunnels: Some(types::RelayTunnels { + openvpn: relay + .tunnels + .openvpn + .iter() + .map(|endpoint| { + let protocol = match endpoint.protocol { + TransportProtocol::Udp => types::TransportProtocol::Udp, + TransportProtocol::Tcp => types::TransportProtocol::Tcp, + }; + types::OpenVpnEndpointData { + port: u32::from(endpoint.port), + protocol: i32::from(protocol), + } + }) + .collect(), + wireguard: relay + .tunnels + .wireguard + .iter() + .map(|endpoint| { + let port_ranges = endpoint + .port_ranges + .iter() + .map(|range| types::PortRange { + first: u32::from(range.0), + last: u32::from(range.1), + }) + .collect(); + types::WireguardEndpointData { + port_ranges, + ipv4_gateway: endpoint.ipv4_gateway.to_string(), + ipv6_gateway: endpoint.ipv6_gateway.to_string(), + public_key: endpoint.public_key.as_bytes().to_vec(), + } + }) + .collect(), + }), + bridges: Some(types::RelayBridges { + shadowsocks: relay + .bridges + .shadowsocks + .iter() + .map(|endpoint| { + let protocol = match endpoint.protocol { + TransportProtocol::Udp => types::TransportProtocol::Udp, + TransportProtocol::Tcp => types::TransportProtocol::Tcp, + }; + types::ShadowsocksEndpointData { + port: u32::from(endpoint.port), + cipher: endpoint.cipher.clone(), + password: endpoint.password.clone(), + protocol: i32::from(protocol), + } + }) + .collect(), + }), + location: relay.location.as_ref().map(|location| types::Location { + country: location.country.clone(), + country_code: location.country_code.clone(), + city: location.city.clone(), + city_code: location.city_code.clone(), + latitude: location.latitude, + longitude: location.longitude, + }), + } +} + +fn convert_state(state: TunnelState) -> types::TunnelState { + use talpid_types::tunnel::{ + ActionAfterDisconnect, ErrorStateCause, FirewallPolicyError, ParameterGenerationError, + }; + use types::{ + error_state::{ + firewall_policy_error::ErrorType as PolicyErrorType, Cause as ProtoErrorCause, + FirewallPolicyError as ProtoFirewallPolicyError, + GenerationError as ProtoGenerationError, + }, + tunnel_state::{self, State as ProtoState}, + }; + use TunnelState::*; + + let map_firewall_error = |firewall_error: &FirewallPolicyError| match firewall_error { + FirewallPolicyError::Generic => ProtoFirewallPolicyError { + r#type: i32::from(PolicyErrorType::Generic), + ..Default::default() + }, + #[cfg(windows)] + FirewallPolicyError::Locked(blocking_app) => { + let (lock_pid, lock_name) = match blocking_app { + Some(app) => (app.pid, app.name.clone()), + None => (0, "".to_string()), + }; + + ProtoFirewallPolicyError { + r#type: i32::from(PolicyErrorType::Locked), + lock_pid, + lock_name, + } + } + }; + + let state = match state { + Disconnected => ProtoState::Disconnected(tunnel_state::Disconnected {}), + Connecting { endpoint, location } => ProtoState::Connecting(tunnel_state::Connecting { + relay_info: Some(types::TunnelStateRelayInfo { + tunnel_endpoint: Some(convert_endpoint(endpoint)), + location: location.map(convert_geoip_location), + }), + }), + Connected { endpoint, location } => ProtoState::Connected(tunnel_state::Connected { + relay_info: Some(types::TunnelStateRelayInfo { + tunnel_endpoint: Some(convert_endpoint(endpoint)), + location: location.map(convert_geoip_location), + }), + }), + Disconnecting(after_disconnect) => ProtoState::Disconnecting(tunnel_state::Disconnecting { + after_disconnect: match after_disconnect { + ActionAfterDisconnect::Nothing => i32::from(types::AfterDisconnect::Nothing), + ActionAfterDisconnect::Block => i32::from(types::AfterDisconnect::Block), + ActionAfterDisconnect::Reconnect => i32::from(types::AfterDisconnect::Reconnect), + }, + }), + Error(error_state) => ProtoState::Error(tunnel_state::Error { + error_state: Some(types::ErrorState { + cause: match error_state.cause() { + ErrorStateCause::AuthFailed(_) => i32::from(ProtoErrorCause::AuthFailed), + ErrorStateCause::Ipv6Unavailable => i32::from(ProtoErrorCause::Ipv6Unavailable), + ErrorStateCause::SetFirewallPolicyError(_) => { + i32::from(ProtoErrorCause::SetFirewallPolicyError) + } + ErrorStateCause::SetDnsError => i32::from(ProtoErrorCause::SetDnsError), + ErrorStateCause::StartTunnelError => { + i32::from(ProtoErrorCause::StartTunnelError) + } + ErrorStateCause::TunnelParameterError(_) => { + i32::from(ProtoErrorCause::TunnelParameterError) + } + ErrorStateCause::IsOffline => i32::from(ProtoErrorCause::IsOffline), + ErrorStateCause::TapAdapterProblem => { + i32::from(ProtoErrorCause::TapAdapterProblem) + } + #[cfg(target_os = "android")] + ErrorStateCause::VpnPermissionDenied => { + i32::from(ProtoErrorCause::VpnPermissionDenied) + } + }, + blocking_error: error_state.block_failure().map(map_firewall_error), + auth_fail_reason: if let ErrorStateCause::AuthFailed(reason) = error_state.cause() { + reason.clone().unwrap_or_default() + } else { + "".to_string() + }, + parameter_error: if let ErrorStateCause::TunnelParameterError(reason) = + error_state.cause() + { + match reason { + ParameterGenerationError::NoMatchingRelay => { + i32::from(ProtoGenerationError::NoMatchingRelay) + } + ParameterGenerationError::NoMatchingBridgeRelay => { + i32::from(ProtoGenerationError::NoMatchingBridgeRelay) + } + ParameterGenerationError::NoWireguardKey => { + i32::from(ProtoGenerationError::NoWireguardKey) + } + ParameterGenerationError::CustomTunnelHostResultionError => { + i32::from(ProtoGenerationError::CustomTunnelHostResolutionError) + } + } + } else { + 0 + }, + policy_error: if let ErrorStateCause::SetFirewallPolicyError(reason) = + error_state.cause() + { + Some(map_firewall_error(reason)) + } else { + None + }, + }), + }), + }; + + types::TunnelState { state: Some(state) } +} + +fn convert_endpoint(endpoint: talpid_types::net::TunnelEndpoint) -> types::TunnelEndpoint { + use talpid_types::net; + + types::TunnelEndpoint { + address: endpoint.endpoint.address.to_string(), + protocol: match endpoint.endpoint.protocol { + TransportProtocol::Tcp => i32::from(types::TransportProtocol::Tcp), + TransportProtocol::Udp => i32::from(types::TransportProtocol::Udp), + }, + tunnel_type: match endpoint.tunnel_type { + net::TunnelType::Wireguard => i32::from(types::TunnelType::Wireguard), + net::TunnelType::OpenVpn => i32::from(types::TunnelType::Openvpn), + }, + proxy: endpoint.proxy.map(|proxy_ep| types::ProxyEndpoint { + address: proxy_ep.endpoint.address.to_string(), + protocol: match proxy_ep.endpoint.protocol { + TransportProtocol::Tcp => i32::from(types::TransportProtocol::Tcp), + TransportProtocol::Udp => i32::from(types::TransportProtocol::Udp), + }, + proxy_type: match proxy_ep.proxy_type { + net::proxy::ProxyType::Shadowsocks => i32::from(types::ProxyType::Shadowsocks), + net::proxy::ProxyType::Custom => i32::from(types::ProxyType::Custom), + }, + }), + } +} + +fn convert_geoip_location(geoip: GeoIpLocation) -> types::GeoIpLocation { + types::GeoIpLocation { + ipv4: geoip.ipv4.map(|ip| ip.to_string()).unwrap_or_default(), + ipv6: geoip.ipv6.map(|ip| ip.to_string()).unwrap_or_default(), + country: geoip.country, + city: geoip.city.unwrap_or_default(), + latitude: geoip.latitude, + longitude: geoip.longitude, + mullvad_exit_ip: geoip.mullvad_exit_ip, + hostname: geoip.hostname.unwrap_or_default(), + bridge_hostname: geoip.bridge_hostname.unwrap_or_default(), + } +} + +fn convert_version_info(version_info: &version::AppVersionInfo) -> types::AppVersionInfo { + types::AppVersionInfo { + supported: version_info.supported, + latest_stable: version_info.latest_stable.clone(), + latest_beta: version_info.latest_beta.clone(), + suggested_upgrade: version_info.suggested_upgrade.clone().unwrap_or_default(), + } +} + +fn convert_proto_location(location: types::RelayLocation) -> Constraint<LocationConstraint> { + if !location.hostname.is_empty() { + Constraint::Only(LocationConstraint::Hostname( + location.country, + location.city, + location.hostname, + )) + } else if !location.city.is_empty() { + Constraint::Only(LocationConstraint::City(location.country, location.city)) + } else if !location.country.is_empty() { + Constraint::Only(LocationConstraint::Country(location.country)) + } else { + Constraint::Any + } +} + +pub struct ManagementInterfaceServer { + subscriptions: Arc<RwLock<Vec<EventsListenerSender>>>, + socket_path: String, + runtime: tokio02::runtime::Runtime, + server_abort_tx: triggered::Trigger, + server_join_handle: Option< + tokio02::task::JoinHandle<std::result::Result<(), mullvad_management_interface::Error>>, + >, +} + +impl ManagementInterfaceServer { + pub fn start(tunnel_tx: DaemonCommandSender) -> Result<Self, Error> { + // TODO: don't spawn a tokio runtime here; make this function async + let mut runtime = tokio02::runtime::Builder::new() + .threaded_scheduler() + .core_threads(1) + .enable_all() + .build() + .map_err(Error::TokioRuntimeError)?; + + let subscriptions = Arc::<RwLock<Vec<EventsListenerSender>>>::default(); + + let socket_path = mullvad_paths::get_rpc_socket_path() + .to_string_lossy() + .to_string(); + + let (server_abort_tx, server_abort_rx) = triggered::trigger(); + let (start_tx, start_rx) = mpsc::channel(); + let server = ManagementServiceImpl { + daemon_tx: tunnel_tx, + subscriptions: subscriptions.clone(), }; - Box::new(result) + let server_join_handle = runtime.spawn(mullvad_management_interface::spawn_rpc_server( + server, + start_tx, + server_abort_rx, + )); + + if let Err(_) = start_rx.recv() { + return Err(runtime + .block_on(server_join_handle) + .expect("Failed to resolve quit handle future") + .map_err(Error::SetupError) + .unwrap_err()); + } + + Ok(ManagementInterfaceServer { + subscriptions, + socket_path, + runtime, + server_abort_tx, + server_join_handle: Some(server_join_handle), + }) + } + + pub fn socket_path(&self) -> &str { + &self.socket_path + } + + pub fn event_broadcaster(&self) -> ManagementInterfaceEventBroadcaster { + ManagementInterfaceEventBroadcaster { + runtime: self.runtime.handle().clone(), + subscriptions: self.subscriptions.clone(), + close_handle: self.server_abort_tx.clone(), + } + } + + /// Consumes the server and waits for it to finish. Returns an error if the server exited + /// due to an error. + pub fn wait(mut self) { + if let Some(server_join_handle) = self.server_join_handle { + if let Err(error) = self.runtime.block_on(server_join_handle) { + log::error!("Management server panic: {:?}", error); + } + } } } +/// A handle that allows broadcasting messages to all subscribers of the management interface. +#[derive(Clone)] +pub struct ManagementInterfaceEventBroadcaster { + runtime: tokio02::runtime::Handle, + subscriptions: Arc<RwLock<Vec<EventsListenerSender>>>, + close_handle: triggered::Trigger, +} + +impl EventListener for ManagementInterfaceEventBroadcaster { + /// Sends a new state update to all `new_state` subscribers of the management interface. + fn notify_new_state(&self, new_state: TunnelState) { + self.notify(types::DaemonEvent { + event: Some(daemon_event::Event::TunnelState(convert_state(new_state))), + }) + } + + /// Sends settings to all `settings` subscribers of the management interface. + fn notify_settings(&self, settings: Settings) { + log::debug!("Broadcasting new settings"); + self.notify(types::DaemonEvent { + event: Some(daemon_event::Event::Settings(convert_settings(&settings))), + }) + } + + /// Sends relays to all subscribers of the management interface. + fn notify_relay_list(&self, relay_list: RelayList) { + log::debug!("Broadcasting new relay list"); + let mut new_list = types::RelayList { + countries: Vec::new(), + }; + new_list.countries.reserve(relay_list.countries.len()); + for country in &relay_list.countries { + new_list.countries.push(convert_relay_list_country(country)); + } + self.notify(types::DaemonEvent { + event: Some(daemon_event::Event::RelayList(new_list)), + }) + } + + fn notify_app_version(&self, app_version_info: version::AppVersionInfo) { + log::debug!("Broadcasting new app version info"); + let new_info = convert_version_info(&app_version_info); + self.notify(types::DaemonEvent { + event: Some(daemon_event::Event::VersionInfo(new_info)), + }) + } -/// The metadata type. There is one instance associated with each connection. In this pubsub -/// scenario they are created by `meta_extractor` by the server on each new incoming -/// connection. -#[derive(Clone, Debug, Default)] -pub struct Meta { - session: Option<Arc<Session>>, + fn notify_key_event(&self, key_event: mullvad_types::wireguard::KeygenEvent) { + log::debug!("Broadcasting new wireguard key event"); + let new_event = convert_wireguard_key_event(&key_event); + self.notify(types::DaemonEvent { + event: Some(daemon_event::Event::KeyEvent(new_event)), + }) + } } -/// Make the `Meta` type possible to use as jsonrpc metadata type. -impl Metadata for Meta {} +impl ManagementInterfaceEventBroadcaster { + fn notify(&self, value: types::DaemonEvent) { + let mut subscriptions = self.subscriptions.write(); + // TODO: using write-lock everywhere. use a mutex instead? + subscriptions.retain(|tx| tx.send(Ok(value.clone())).is_ok()); + } +} -/// Make the `Meta` type possible to use as a pubsub metadata type. -impl PubSubMetadata for Meta { - fn session(&self) -> Option<Arc<Session>> { - self.session.clone() +impl Drop for ManagementInterfaceEventBroadcaster { + fn drop(&mut self) { + self.close_handle.trigger(); } } -/// Metadata extractor function for `Meta`. -fn meta_extractor(context: &jsonrpc_ipc_server::RequestContext<'_>) -> Meta { - Meta { - session: Some(Arc::new(Session::new(context.sender.clone()))), +// Converts a REST API error for an account into a tonic status. +fn map_rest_account_error(error: RestError) -> Status { + match error { + RestError::ApiError(status, message) + if status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN => + { + Status::new(Code::Unauthenticated, message) + } + _ => Status::internal("internal error"), } } diff --git a/mullvad-daemon/src/rpc_uniqueness_check.rs b/mullvad-daemon/src/rpc_uniqueness_check.rs index 7befdb4ba1..76d524c765 100644 --- a/mullvad-daemon/src/rpc_uniqueness_check.rs +++ b/mullvad-daemon/src/rpc_uniqueness_check.rs @@ -1,13 +1,12 @@ -use mullvad_ipc_client::new_standalone_ipc_client; -use mullvad_paths; +use mullvad_management_interface::new_rpc_client; use talpid_types::ErrorExt; /// Checks if there is another instance of the daemon running. /// /// Tries to connect to another daemon and perform a simple RPC call. If it fails, assumes the /// other daemon has stopped. -pub fn is_another_instance_running() -> bool { - match new_standalone_ipc_client(&mullvad_paths::get_rpc_socket_path()) { +pub async fn is_another_instance_running() -> bool { + match new_rpc_client().await { Ok(_) => true, Err(error) => { let msg = diff --git a/mullvad-ipc-client/Cargo.toml b/mullvad-ipc-client/Cargo.toml deleted file mode 100644 index 1107c4f68a..0000000000 --- a/mullvad-ipc-client/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "mullvad-ipc-client" -version = "0.1.0" -authors = ["Mullvad VPN"] -description = "RPC client for Mullvad daemon" -license = "GPL-3.0" -edition = "2018" -publish = false - -[dependencies] -err-derive = "0.2.1" -mullvad-types = { path = "../mullvad-types" } -serde = "1.0" -talpid-ipc = { path = "../talpid-ipc" } -talpid-types = { path = "../talpid-types" } -mullvad-paths = { path = "../mullvad-paths" } -jsonrpc-client-core = { git = "https://github.com/mullvad/jsonrpc-client-rs", rev = "68aac55b" } -jsonrpc-client-ipc = { git = "https://github.com/mullvad/jsonrpc-client-rs", rev = "68aac55b" } -jsonrpc-client-pubsub = { git = "https://github.com/mullvad/jsonrpc-client-rs", rev = "68aac55b" } -tokio = "0.1.15" -tokio-timer = "0.1" -futures = "0.1" -log = "0.4" - - -[target.'cfg(windows)'.dependencies] -winapi = { version = "0.3.5", features = ["accctrl", "aclapi", "securitybaseapi", "winbase", "winerror", "winnt"] } diff --git a/mullvad-ipc-client/src/lib.rs b/mullvad-ipc-client/src/lib.rs deleted file mode 100644 index f1bae521b8..0000000000 --- a/mullvad-ipc-client/src/lib.rs +++ /dev/null @@ -1,281 +0,0 @@ -#![deny(rust_2018_idioms)] - -use futures::sync::oneshot; -use jsonrpc_client_core::{Client, ClientHandle, Future}; -use jsonrpc_client_ipc::IpcTransport; -use mullvad_types::{ - account::{AccountData, AccountToken, VoucherSubmission}, - location::GeoIpLocation, - relay_constraints::{BridgeSettings, BridgeState, RelaySettings, RelaySettingsUpdate}, - relay_list::RelayList, - settings::{Settings, TunnelOptions}, - states::TunnelState, - version::AppVersionInfo, - wireguard, DaemonEvent, -}; -use serde::{Deserialize, Serialize}; -use std::{io, path::Path, thread}; - -static NO_ARGS: [u8; 0] = []; - -pub type Result<T> = std::result::Result<T, jsonrpc_client_core::Error>; -pub use jsonrpc_client_core::{Error, ErrorKind}; -pub use jsonrpc_client_pubsub::Error as PubSubError; - -pub fn new_standalone_ipc_client(path: &impl AsRef<Path>) -> io::Result<DaemonRpcClient> { - let path = path.as_ref().to_string_lossy().to_string(); - - new_standalone_transport(path, |path| { - IpcTransport::new(&path, &tokio::reactor::Handle::default()) - }) -} - -pub fn new_standalone_transport< - F: Send + 'static + FnOnce(String) -> io::Result<T>, - T: jsonrpc_client_core::DuplexTransport + 'static, ->( - rpc_path: String, - transport_func: F, -) -> io::Result<DaemonRpcClient> { - let (tx, rx) = oneshot::channel(); - thread::spawn(move || match spawn_transport(rpc_path, transport_func) { - Err(e) => tx - .send(Err(e)) - .expect("Failed to send error back to caller"), - Ok((client, server_handle, client_handle)) => { - let mut rt = tokio::runtime::current_thread::Runtime::new() - .expect("Failed to start a standalone tokio runtime for mullvad ipc"); - let handle = rt.handle(); - tx.send(Ok((client_handle, server_handle, handle))) - .expect("Failed to send client handle"); - - if let Err(e) = rt.block_on(client) { - log::error!("JSON-RPC client failed: {}", e.description()); - } - } - }); - - rx.wait() - .map_err(|_| io::Error::new(io::ErrorKind::NotFound, "No transport handles returned"))? - .map(|(rpc_client, server_handle, executor)| { - let subscriber = - jsonrpc_client_pubsub::Subscriber::new(executor, rpc_client.clone(), server_handle); - DaemonRpcClient { - rpc_client, - subscriber, - } - }) -} - -fn spawn_transport< - F: Send + FnOnce(String) -> io::Result<T>, - T: jsonrpc_client_core::DuplexTransport + 'static, ->( - address: String, - transport_func: F, -) -> io::Result<( - Client<T, jsonrpc_client_core::server::Server>, - jsonrpc_client_core::server::ServerHandle, - ClientHandle, -)> { - let (server, server_handle) = jsonrpc_client_core::server::Server::new(); - let transport = transport_func(address)?; - let (client, client_handle) = jsonrpc_client_core::Client::with_server(transport, server); - Ok((client, server_handle, client_handle)) -} - -pub struct DaemonRpcClient { - rpc_client: jsonrpc_client_core::ClientHandle, - subscriber: jsonrpc_client_pubsub::Subscriber<tokio::runtime::current_thread::Handle>, -} - - -impl DaemonRpcClient { - pub fn connect(&mut self) -> Result<()> { - self.call("connect", &NO_ARGS) - } - - pub fn disconnect(&mut self) -> Result<()> { - self.call("disconnect", &NO_ARGS) - } - - pub fn reconnect(&mut self) -> Result<()> { - self.call("reconnect", &NO_ARGS) - } - - pub fn create_new_account(&mut self) -> Result<()> { - self.call("create_new_account", &NO_ARGS) - } - - pub fn get_account(&mut self) -> Result<Option<AccountToken>> { - self.call("get_account", &NO_ARGS) - } - - pub fn get_account_data(&mut self, account: AccountToken) -> Result<AccountData> { - self.call("get_account_data", &[account]) - } - - pub fn submit_voucher(&mut self, voucher: String) -> Result<VoucherSubmission> { - self.call("submit_voucher", &[voucher]) - } - - pub fn set_allow_lan(&mut self, allow_lan: bool) -> Result<()> { - self.call("set_allow_lan", &[allow_lan]) - } - - pub fn set_show_beta_releases(&mut self, enabled: bool) -> Result<()> { - self.call("set_show_beta_releases", &[enabled]) - } - - pub fn set_block_when_disconnected(&mut self, block_when_disconnected: bool) -> Result<()> { - self.call("set_block_when_disconnected", &[block_when_disconnected]) - } - - pub fn get_allow_lan(&mut self) -> Result<bool> { - self.call("get_allow_lan", &NO_ARGS) - } - - pub fn set_auto_connect(&mut self, auto_connect: bool) -> Result<()> { - self.call("set_auto_connect", &[auto_connect]) - } - - pub fn get_auto_connect(&mut self) -> Result<bool> { - self.call("get_auto_connect", &NO_ARGS) - } - - pub fn get_current_location(&mut self) -> Result<Option<GeoIpLocation>> { - self.call("get_current_location", &NO_ARGS) - } - - pub fn get_current_version(&mut self) -> Result<String> { - self.call("get_current_version", &NO_ARGS) - } - - pub fn get_relay_locations(&mut self) -> Result<RelayList> { - self.call("get_relay_locations", &NO_ARGS) - } - - pub fn update_relay_locations(&mut self) -> Result<()> { - self.call("update_relay_locations", &NO_ARGS) - } - - pub fn get_relay_settings(&mut self) -> Result<RelaySettings> { - self.call("get_relay_settings", &NO_ARGS) - } - - pub fn get_state(&mut self) -> Result<TunnelState> { - self.call("get_state", &NO_ARGS) - } - - pub fn get_tunnel_options(&mut self) -> Result<TunnelOptions> { - self.call("get_tunnel_options", &NO_ARGS) - } - - pub fn get_settings(&mut self) -> Result<Settings> { - self.call("get_settings", &NO_ARGS) - } - - pub fn generate_wireguard_key(&mut self) -> Result<wireguard::KeygenEvent> { - self.call("generate_wireguard_key", &NO_ARGS) - } - - pub fn get_wireguard_key(&mut self) -> Result<Option<wireguard::PublicKey>> { - self.call("get_wireguard_key", &NO_ARGS) - } - - pub fn verify_wireguard_key(&mut self) -> Result<bool> { - self.call("verify_wireguard_key", &NO_ARGS) - } - - pub fn get_version_info(&mut self) -> Result<AppVersionInfo> { - self.call("get_version_info", &NO_ARGS) - } - - pub fn set_account(&mut self, account: Option<AccountToken>) -> Result<()> { - self.call("set_account", &[account]) - } - - pub fn clear_account_history(&mut self) -> Result<()> { - self.call("clear_account_history", &NO_ARGS) - } - - pub fn set_enable_ipv6(&mut self, enabled: bool) -> Result<()> { - self.call("set_enable_ipv6", &[enabled]) - } - - pub fn set_wireguard_mtu(&mut self, mtu: Option<u16>) -> Result<()> { - self.call("set_wireguard_mtu", &[mtu]) - } - - pub fn set_wireguard_rotation_interval(&mut self, interval: Option<u32>) -> Result<()> { - self.call("set_wireguard_rotation_interval", &[interval]) - } - - pub fn set_openvpn_mssfix(&mut self, mssfix: Option<u16>) -> Result<()> { - self.call("set_openvpn_mssfix", &[mssfix]) - } - - pub fn set_bridge_settings(&mut self, settings: BridgeSettings) -> Result<()> { - self.call("set_bridge_settings", &[settings]) - } - - pub fn set_bridge_state(&mut self, state: BridgeState) -> Result<()> { - self.call("set_bridge_state", &[state]) - } - - pub fn shutdown(&mut self) -> Result<()> { - self.call("shutdown", &NO_ARGS) - } - - pub fn prepare_restart(&mut self) -> Result<()> { - self.call("prepare_restart", &NO_ARGS) - } - - pub fn factory_reset(&mut self) -> Result<()> { - self.call("factory_reset", &NO_ARGS) - } - - pub fn update_relay_settings(&mut self, update: RelaySettingsUpdate) -> Result<()> { - self.call("update_relay_settings", &[update]) - } - - pub fn get_split_tunnel_processes(&mut self) -> Result<Vec<i32>> { - self.call("get_split_tunnel_processes", &NO_ARGS) - } - - pub fn add_split_tunnel_process(&mut self, pid: i32) -> Result<()> { - self.call("add_split_tunnel_process", &[pid]) - } - - pub fn remove_split_tunnel_process(&mut self, pid: i32) -> Result<()> { - self.call("remove_split_tunnel_process", &[pid]) - } - - pub fn clear_split_tunnel_processes(&mut self) -> Result<()> { - self.call("clear_split_tunnel_processes", &NO_ARGS) - } - - - pub fn call<A, O>(&mut self, method: &'static str, args: &A) -> Result<O> - where - A: Serialize + Send + 'static, - O: for<'de> Deserialize<'de> + Send + 'static, - { - self.rpc_client.call_method(method, args).wait() - } - - pub fn daemon_event_subscribe( - &mut self, - ) -> impl Future< - Item = jsonrpc_client_pubsub::Subscription<DaemonEvent>, - Error = jsonrpc_client_pubsub::Error, - > { - self.subscriber.subscribe( - "daemon_event_subscribe".to_string(), - "daemon_event_unsubscribe".to_string(), - "daemon_event".to_string(), - 0, - &NO_ARGS, - ) - } -} diff --git a/mullvad-management-interface/Cargo.toml b/mullvad-management-interface/Cargo.toml new file mode 100644 index 0000000000..31647006b1 --- /dev/null +++ b/mullvad-management-interface/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "mullvad-management-interface" +version = "0.1.0" +authors = ["Mullvad VPN"] +description = "Mullvad VPN IPC. Contains types and functions for IPC clients and servers." +license = "GPL-3.0" +edition = "2018" +publish = false + +[dependencies] +err-derive = "0.2.1" +mullvad-paths = { path = "../mullvad-paths" } +tonic = "0.2" +tower = "0.3" +prost = "0.6" +prost-types = "0.6" +parity-tokio-ipc = "0.7" +futures = "0.3" +tokio = { version = "0.2", features = [ "io-util", "process", "rt-core", "rt-threaded", "stream"] } +triggered = "0.1.1" +winapi = { version = "0.3", features = ["errhandlingapi", "handleapi", "libloaderapi", "ntlsa", "synchapi", "tlhelp32", "winbase", "winerror", "winuser"] } + +[build-dependencies] +tonic-build = { version = "0.2", default-features = false, features = ["transport", "prost"] } diff --git a/mullvad-management-interface/build.rs b/mullvad-management-interface/build.rs new file mode 100644 index 0000000000..e3dd7a8186 --- /dev/null +++ b/mullvad-management-interface/build.rs @@ -0,0 +1,5 @@ +fn main() { + const PROTO_FILE: &str = "proto/management_interface.proto"; + tonic_build::compile_protos(PROTO_FILE).unwrap(); + println!("cargo:rerun-if-changed={}", PROTO_FILE); +} diff --git a/mullvad-management-interface/proto/management_interface.proto b/mullvad-management-interface/proto/management_interface.proto new file mode 100644 index 0000000000..8616163f50 --- /dev/null +++ b/mullvad-management-interface/proto/management_interface.proto @@ -0,0 +1,471 @@ +syntax = "proto3"; + +package mullvad_daemon.management_interface; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +service ManagementService { + // Control and get tunnel state + rpc ConnectTunnel(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc DisconnectTunnel(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc ReconnectTunnel(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc GetTunnelState(google.protobuf.Empty) returns (TunnelState) {} + + // Control the daemon and receive events + rpc EventsListen(google.protobuf.Empty) returns (stream DaemonEvent) {} + rpc PrepareRestart(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc Shutdown(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc FactoryReset(google.protobuf.Empty) returns (google.protobuf.Empty) {} + + rpc GetCurrentVersion(google.protobuf.Empty) returns (google.protobuf.StringValue) {} + rpc GetVersionInfo(google.protobuf.Empty) returns (AppVersionInfo) {} + + // Relays and tunnel constraints + rpc UpdateRelayLocations(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc UpdateRelaySettings(RelaySettingsUpdate) returns (google.protobuf.Empty) {} + rpc GetRelayLocations(google.protobuf.Empty) returns (stream RelayListCountry) {} + rpc GetCurrentLocation(google.protobuf.Empty) returns (GeoIpLocation) {} + rpc SetBridgeSettings(BridgeSettings) returns (google.protobuf.Empty) {} + rpc SetBridgeState(BridgeState) returns (google.protobuf.Empty) {} + + // Settings + rpc GetSettings(google.protobuf.Empty) returns (Settings) {} + rpc SetAllowLan(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} + rpc SetShowBetaReleases(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} + rpc SetBlockWhenDisconnected(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} + rpc SetAutoConnect(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} + rpc SetOpenvpnMssfix(google.protobuf.UInt32Value) returns (google.protobuf.Empty) {} + rpc SetWireguardMtu(google.protobuf.UInt32Value) returns (google.protobuf.Empty) {} + rpc SetEnableIpv6(google.protobuf.BoolValue) returns (google.protobuf.Empty) {} + + // Account management + rpc CreateNewAccount(google.protobuf.Empty) returns (google.protobuf.StringValue) {} + rpc SetAccount(google.protobuf.StringValue) returns (google.protobuf.Empty) {} + rpc GetAccountData(google.protobuf.StringValue) returns (AccountData) {} + rpc GetAccountHistory(google.protobuf.Empty) returns (AccountHistory) {} + rpc RemoveAccountFromHistory(google.protobuf.StringValue) returns (google.protobuf.Empty) {} + rpc ClearAccountHistory(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc GetWwwAuthToken(google.protobuf.Empty) returns (google.protobuf.StringValue) {} + rpc SubmitVoucher(google.protobuf.StringValue) returns (VoucherSubmission) {} + + // WireGuard key management + rpc SetWireguardRotationInterval(google.protobuf.UInt32Value) returns (google.protobuf.Empty) {} + rpc ResetWireguardRotationInterval(google.protobuf.Empty) returns (google.protobuf.Empty) {} + rpc GenerateWireguardKey(google.protobuf.Empty) returns (KeygenEvent) {} + rpc GetWireguardKey(google.protobuf.Empty) returns (PublicKey) {} + rpc VerifyWireguardKey(google.protobuf.Empty) returns (google.protobuf.BoolValue) {} + + // Split tunneling + rpc GetSplitTunnelProcesses(google.protobuf.Empty) returns (stream google.protobuf.Int32Value) {} + rpc AddSplitTunnelProcess(google.protobuf.Int32Value) returns (google.protobuf.Empty) {} + rpc RemoveSplitTunnelProcess(google.protobuf.Int32Value) returns (google.protobuf.Empty) {} + rpc ClearSplitTunnelProcesses(google.protobuf.Empty) returns (google.protobuf.Empty) {} +} + +message RelaySettingsUpdate { + oneof type { + CustomRelaySettings custom = 1; + NormalRelaySettingsUpdate normal = 2; + } +} + +message AccountData { + google.protobuf.Timestamp expiry = 1; +} + +message VoucherSubmission { + uint64 seconds_added = 1; + google.protobuf.Timestamp new_expiry = 2; +} + +enum AfterDisconnect { + NOTHING = 0; + BLOCK = 1; + RECONNECT = 2; +} + +message ErrorState { + enum Cause { + AUTH_FAILED = 0; + IPV6_UNAVAILABLE = 1; + SET_FIREWALL_POLICY_ERROR = 2; + SET_DNS_ERROR = 3; + START_TUNNEL_ERROR = 4; + TUNNEL_PARAMETER_ERROR = 5; + IS_OFFLINE = 6; + TAP_ADAPTER_PROBLEM = 7; + VPN_PERMISSION_DENIED = 8; + } + + enum GenerationError { + NO_MATCHING_RELAY = 0; + NO_MATCHING_BRIDGE_RELAY = 1; + NO_WIREGUARD_KEY = 2; + CUSTOM_TUNNEL_HOST_RESOLUTION_ERROR = 3; + } + + message FirewallPolicyError { + enum ErrorType { + GENERIC = 0; + LOCKED = 1; + } + ErrorType type = 1; + + // LOCKED + uint32 lock_pid = 2; + string lock_name = 3; + } + + Cause cause = 1; + FirewallPolicyError blocking_error = 2; + + // AUTH_FAILED + string auth_fail_reason = 3; + // TUNNEL_PARAMETER_ERROR + GenerationError parameter_error = 4; + // SET_FIREWALL_POLICY_ERROR + FirewallPolicyError policy_error = 5; +} + +message TunnelState { + message Disconnected { + } + message Connecting { + TunnelStateRelayInfo relay_info = 1; + } + message Connected { + TunnelStateRelayInfo relay_info = 1; + } + message Disconnecting { + AfterDisconnect after_disconnect = 1; + } + message Error { + ErrorState error_state = 1; + } + + oneof state { + Disconnected disconnected = 1; + Connecting connecting = 2; + Connected connected = 3; + Disconnecting disconnecting = 4; + Error error = 5; + } +} + +enum TunnelType { + OPENVPN = 0; + WIREGUARD = 1; +} + +message TunnelStateRelayInfo { + TunnelEndpoint tunnel_endpoint = 1; + GeoIpLocation location = 2; +} + +message TunnelEndpoint { + string address = 1; + TransportProtocol protocol = 2; + TunnelType tunnel_type = 3; + ProxyEndpoint proxy = 4; +} + +enum ProxyType { + SHADOWSOCKS = 0; + CUSTOM = 1; +} + +message ProxyEndpoint { + string address = 1; + TransportProtocol protocol = 2; + ProxyType proxy_type = 3; +} + +message GeoIpLocation { + string ipv4 = 1; + string ipv6 = 2; + string country = 3; + string city = 4; + double latitude = 5; + double longitude = 6; + bool mullvad_exit_ip = 7; + string hostname = 8; + string bridge_hostname = 9; +} + +message AccountHistory { + repeated string token = 1; +} + +message BridgeSettings { + message BridgeConstraints { + RelayLocation location = 1; + } + + message LocalProxySettings { + uint32 port = 1; + string peer = 2; + } + message RemoteProxySettings { + string address = 1; + // NOTE: optional + RemoteProxyAuth auth = 2; + } + message RemoteProxyAuth { + string username = 1; + string password = 2; + } + message ShadowsocksProxySettings { + string peer = 1; + string password = 2; + string cipher = 3; + } + + oneof type { + BridgeConstraints normal = 1; + LocalProxySettings local = 2; + RemoteProxySettings remote = 3; + ShadowsocksProxySettings shadowsocks = 4; + } +} + +message RelayLocation { + string country = 1; + string city = 2; + string hostname = 3; +} + +message BridgeState { + enum State { + AUTO = 0; + ON = 1; + OFF = 2; + } + State state = 1; +} + +message Settings { + string account_token = 1; + RelaySettings relay_settings = 2; + BridgeSettings bridge_settings = 3; + BridgeState bridge_state = 4; + bool allow_lan = 5; + bool block_when_disconnected = 6; + bool auto_connect = 7; + TunnelOptions tunnel_options = 8; + bool show_beta_releases = 9; +} + +message RelaySettings { + oneof endpoint { + CustomRelaySettings custom = 1; + NormalRelaySettings normal = 2; + } +} + +message TunnelTypeConstraint { + TunnelType tunnel_type = 1; +} + +message NormalRelaySettings { + RelayLocation location = 1; + TunnelTypeConstraint tunnel_type = 2; + WireguardConstraints wireguard_constraints = 3; + OpenvpnConstraints openvpn_constraints = 4; +} + +// Constraints are only updated for fields that are provided +message NormalRelaySettingsUpdate { + RelayLocation location = 1; + TunnelTypeUpdate tunnel_type = 2; + WireguardConstraints wireguard_constraints = 3; + OpenvpnConstraints openvpn_constraints = 4; +} + +message TunnelTypeUpdate { + TunnelTypeConstraint tunnel_type = 2; +} + +message TransportProtocolConstraint { + TransportProtocol protocol = 1; +} + +message OpenvpnConstraints { + uint32 port = 1; + TransportProtocolConstraint protocol = 2; +} +message WireguardConstraints { + // NOTE: optional + uint32 port = 1; +} + +message CustomRelaySettings { + string host = 1; + ConnectionConfig config = 2; +} + +message ConnectionConfig { + message OpenvpnConfig { + string address = 1; + TransportProtocol protocol = 2; + string username = 3; + string password = 4; + } + message WireguardConfig { + message TunnelConfig { + bytes private_key = 1; + repeated string addresses = 2; + } + message PeerConfig { + bytes public_key = 1; + repeated string allowed_ips = 2; + string endpoint = 3; + } + + TunnelConfig tunnel = 1; + PeerConfig peer = 2; + string ipv4_gateway = 3; + // NOTE: optional + string ipv6_gateway = 4; + } + + oneof config { + OpenvpnConfig openvpn = 1; + WireguardConfig wireguard = 2; + } +} + +message TunnelOptions { + message OpenvpnOptions { + // NOTE: optional + uint32 mssfix = 1; + } + message WireguardOptions { + message RotationInterval { + uint32 interval = 1; + } + + // NOTE: optional + uint32 mtu = 1; + RotationInterval automatic_rotation = 2; + } + message GenericOptions { + bool enable_ipv6 = 1; + } + + OpenvpnOptions openvpn = 1; + WireguardOptions wireguard = 2; + GenericOptions generic = 3; +} + +message PublicKey { + bytes key = 1; + google.protobuf.Timestamp created = 2; +} + +message KeygenEvent { + enum KeygenEvent { + NEW_KEY = 0; + TOO_MANY_KEYS = 1; + GENERATION_FAILURE = 2; + } + KeygenEvent event = 1; + PublicKey new_key = 2; +} + +message AppVersionInfo { + bool supported = 1; + string latest_stable = 2; + string latest_beta = 3; + string suggested_upgrade = 4; +} + +message RelayListCountry { + string name = 1; + string code = 2; + repeated RelayListCity cities = 3; +} + +message RelayListCity { + string name = 1; + string code = 2; + double latitude = 3; + double longitude = 4; + repeated Relay relays = 5; +} + +message Relay { + string hostname = 1; + string ipv4_addr_in = 2; + string ipv6_addr_in = 3; + bool include_in_country = 4; + bool active = 5; + bool owned = 6; + string provider = 7; + fixed64 weight = 8; + RelayTunnels tunnels = 9; + RelayBridges bridges = 10; + Location location = 11; +} + +message Location { + string country = 1; + string country_code = 2; + string city = 3; + string city_code = 4; + double latitude = 5; + double longitude = 6; +} + +message RelayTunnels { + repeated OpenVpnEndpointData openvpn = 1; + repeated WireguardEndpointData wireguard = 2; +} + +message RelayBridges { + repeated ShadowsocksEndpointData shadowsocks = 1; +} + +enum TransportProtocol { + UDP = 0; + TCP = 1; +} + +message ShadowsocksEndpointData { + uint32 port = 1; + string cipher = 2; + string password = 3; + TransportProtocol protocol = 4; +} + +message OpenVpnEndpointData { + uint32 port = 1; + TransportProtocol protocol = 2; +} + +message WireguardEndpointData { + repeated PortRange port_ranges = 1; + string ipv4_gateway = 2; + string ipv6_gateway = 3; + bytes public_key = 4; +} + +message PortRange { + uint32 first = 1; + uint32 last = 2; +} + +message DaemonEvent { + oneof event { + TunnelState tunnel_state = 1; + Settings settings = 2; + RelayList relay_list = 3; + AppVersionInfo version_info = 4; + KeygenEvent key_event = 5; + } +} + +message RelayList { + repeated RelayListCountry countries = 1; +} diff --git a/mullvad-management-interface/src/lib.rs b/mullvad-management-interface/src/lib.rs new file mode 100644 index 0000000000..419e2d48b1 --- /dev/null +++ b/mullvad-management-interface/src/lib.rs @@ -0,0 +1,123 @@ +pub mod types { + tonic::include_proto!("mullvad_daemon.management_interface"); + + pub use prost_types::Timestamp; +} + +use parity_tokio_ipc::Endpoint as IpcEndpoint; +#[cfg(unix)] +use std::{fs, os::unix::fs::PermissionsExt}; +use std::{ + io, + pin::Pin, + task::{Context, Poll}, +}; +use tokio::io::{AsyncRead, AsyncWrite}; +use tonic::transport::{server::Connected, Endpoint, Server, Uri}; +use tower::service_fn; + +pub use tonic::{async_trait, transport::Channel, Code, Request, Response, Status}; + +pub type ManagementServiceClient = + types::management_service_client::ManagementServiceClient<Channel>; +pub use types::management_service_server::{ManagementService, ManagementServiceServer}; + +#[derive(err_derive::Error, Debug)] +#[error(no_from)] +pub enum Error { + #[error(display = "Management RPC server or client error")] + GrpcTransportError(#[error(source)] tonic::transport::Error), + + #[error(display = "Failed to start IPC pipe/socket")] + StartServerError(#[error(source)] io::Error), + + #[error(display = "Failed to initialize pipe/socket security attributes")] + SecurityAttributes(#[error(source)] io::Error), + + #[error(display = "Unable to set permissions for IPC endpoint")] + PermissionsError(#[error(source)] io::Error), +} + +#[cfg(target_os = "windows")] +mod windows_permissions; + +pub async fn new_rpc_client() -> Result<ManagementServiceClient, Error> { + let ipc_path = mullvad_paths::get_rpc_socket_path(); + + // The URI will be ignored + let channel = Endpoint::from_static("lttp://[::]:50051") + .connect_with_connector(service_fn(move |_: Uri| { + IpcEndpoint::connect(ipc_path.clone()) + })) + .await + .map_err(Error::GrpcTransportError)?; + + Ok(ManagementServiceClient::new(channel)) +} + +pub async fn spawn_rpc_server<T: ManagementService>( + service: T, + server_start_tx: std::sync::mpsc::Sender<()>, + abort_rx: triggered::Listener, +) -> std::result::Result<(), Error> { + use futures::stream::TryStreamExt; + use parity_tokio_ipc::SecurityAttributes; + + let socket_path = mullvad_paths::get_rpc_socket_path() + .to_string_lossy() + .to_string(); + + let mut endpoint = IpcEndpoint::new(socket_path.to_string()); + endpoint.set_security_attributes( + SecurityAttributes::allow_everyone_create() + .map_err(Error::SecurityAttributes)? + .set_mode(777) + .map_err(Error::SecurityAttributes)?, + ); + let incoming = endpoint.incoming().map_err(Error::StartServerError)?; + + #[cfg(unix)] + fs::set_permissions(&socket_path, PermissionsExt::from_mode(0o766)) + .map_err(Error::PermissionsError)?; + #[cfg(windows)] + crate::windows_permissions::deny_network_access(&socket_path) + .map_err(Error::PermissionsError)?; + + let _ = server_start_tx.send(()); + + Server::builder() + .add_service(ManagementServiceServer::new(service)) + .serve_with_incoming_shutdown(incoming.map_ok(StreamBox), abort_rx) + .await + .map_err(Error::GrpcTransportError) +} + +#[derive(Debug)] +struct StreamBox<T: AsyncRead + AsyncWrite>(pub T); +impl<T: AsyncRead + AsyncWrite> Connected for StreamBox<T> {} +impl<T: AsyncRead + AsyncWrite + Unpin> AsyncRead for StreamBox<T> { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll<std::io::Result<usize>> { + Pin::new(&mut self.0).poll_read(cx, buf) + } +} +impl<T: AsyncRead + AsyncWrite + Unpin> AsyncWrite for StreamBox<T> { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll<std::io::Result<usize>> { + Pin::new(&mut self.0).poll_write(cx, buf) + } + + fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> { + Pin::new(&mut self.0).poll_flush(cx) + } + + fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> { + Pin::new(&mut self.0).poll_shutdown(cx) + } +} diff --git a/talpid-ipc/src/win.rs b/mullvad-management-interface/src/windows_permissions.rs index 0b39282348..0b39282348 100644 --- a/talpid-ipc/src/win.rs +++ b/mullvad-management-interface/src/windows_permissions.rs diff --git a/mullvad-setup/Cargo.toml b/mullvad-setup/Cargo.toml index 3d113870d4..04da0d49ca 100644 --- a/mullvad-setup/Cargo.toml +++ b/mullvad-setup/Cargo.toml @@ -16,7 +16,10 @@ clap = "2.32" env_logger = "0.7" err-derive = "0.2.1" -mullvad-ipc-client = { path = "../mullvad-ipc-client" } +mullvad-management-interface = { path = "../mullvad-management-interface" } + +tokio = { version = "0.2", features = [ "io-util", "process", "rt-core", "rt-threaded", "stream"] } + mullvad-paths = { path = "../mullvad-paths" } talpid-core = { path = "../talpid-core" } talpid-types = { path = "../talpid-types" } diff --git a/mullvad-setup/src/main.rs b/mullvad-setup/src/main.rs index f8fda169b5..c53394a473 100644 --- a/mullvad-setup/src/main.rs +++ b/mullvad-setup/src/main.rs @@ -1,6 +1,6 @@ use clap::{crate_authors, crate_description, crate_name, SubCommand}; -use mullvad_ipc_client::{new_standalone_ipc_client, DaemonRpcClient}; -use std::{io, process}; +use mullvad_management_interface::new_rpc_client; +use std::process; use talpid_core::firewall::{self, Firewall, FirewallArguments}; use talpid_types::ErrorExt; @@ -8,11 +8,11 @@ pub const PRODUCT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/produc #[derive(err_derive::Error, Debug)] pub enum Error { - #[error(display = "Failed to connect to daemon")] - DaemonConnect(#[error(source)] io::Error), + #[error(display = "Failed to connect to RPC client")] + RpcConnectionError(#[error(source)] mullvad_management_interface::Error), #[error(display = "RPC call failed")] - DaemonRpcError(#[error(source)] mullvad_ipc_client::Error), + DaemonRpcError(#[error(source)] mullvad_management_interface::Status), #[error(display = "This command cannot be run if the daemon is active")] DaemonIsRunning, @@ -21,7 +21,8 @@ pub enum Error { FirewallError(#[error(source)] firewall::Error), } -fn main() { +#[tokio::main] +async fn main() { env_logger::init(); let subcommands = vec![ @@ -44,8 +45,8 @@ fn main() { let matches = app.get_matches(); let result = match matches.subcommand_name().expect("Subcommand has no name") { - "prepare-restart" => prepare_restart(), - "reset-firewall" => reset_firewall(), + "prepare-restart" => prepare_restart().await, + "reset-firewall" => reset_firewall().await, _ => unreachable!("No command matched"), }; @@ -55,14 +56,17 @@ fn main() { } } -fn prepare_restart() -> Result<(), Error> { - let mut rpc = new_rpc_client()?; - rpc.prepare_restart().map_err(Error::DaemonRpcError) +async fn prepare_restart() -> Result<(), Error> { + let mut rpc = new_rpc_client().await?; + rpc.prepare_restart(()) + .await + .map_err(Error::DaemonRpcError)?; + Ok(()) } -fn reset_firewall() -> Result<(), Error> { +async fn reset_firewall() -> Result<(), Error> { // Ensure that the daemon isn't running - if let Ok(_) = new_rpc_client() { + if let Ok(_) = new_rpc_client().await { return Err(Error::DaemonIsRunning); } @@ -74,7 +78,3 @@ fn reset_firewall() -> Result<(), Error> { firewall.reset_policy().map_err(Error::FirewallError) } - -fn new_rpc_client() -> Result<DaemonRpcClient, Error> { - new_standalone_ipc_client(&mullvad_paths::get_rpc_socket_path()).map_err(Error::DaemonConnect) -} diff --git a/mullvad-tests/Cargo.toml b/mullvad-tests/Cargo.toml index 152343c1e9..f840c207eb 100644 --- a/mullvad-tests/Cargo.toml +++ b/mullvad-tests/Cargo.toml @@ -12,13 +12,11 @@ integration-tests = [] [dependencies] duct = "0.13" -mullvad-ipc-client = { path = "../mullvad-ipc-client" } mullvad-paths = { path = "../mullvad-paths" } mullvad-rpc = { path = "../mullvad-rpc" } mullvad-types = { path = "../mullvad-types" } notify = "4.0" openvpn-plugin = { git = "https://github.com/mullvad/openvpn-plugin-rs", branch = "auth-failed-event", features = ["serde"] } -talpid-ipc = { path = "../talpid-ipc" } talpid-types = { path = "../talpid-types" } tempfile = "3.0" jsonrpc-client-core = { git = "https://github.com/mullvad/jsonrpc-client-rs", rev = "68aac55b" } diff --git a/mullvad-types/src/account.rs b/mullvad-types/src/account.rs index a22b574856..0acb0941aa 100644 --- a/mullvad-types/src/account.rs +++ b/mullvad-types/src/account.rs @@ -28,32 +28,3 @@ pub struct VoucherSubmission { #[cfg_attr(target_os = "android", jnix(map = "|expiry| expiry.to_string()"))] pub new_expiry: DateTime<Utc>, } - -/// Mapping of mullvad-api errors to enum variants. Used by frontends to explain why a voucher -/// was rejected by the `/v1/submit-voucher` RPC. -#[derive(err_derive::Error, Debug)] -pub enum VoucherError { - /// Error code -400 - #[error(display = "Bad voucher code")] - BadVoucher, - /// Error code -401 - #[error(display = "Voucher already used")] - VoucherAlreadyUsed, - /// Error code -100 - #[error(display = "Server internal error")] - InternalError, - #[error(display = "Unknown error, {}", _0)] - UnknownError(i64), -} - -impl VoucherError { - /// Create error from RPC error code. - pub fn from_rpc_error_code(err_code: i64) -> VoucherError { - match err_code { - -400 => VoucherError::BadVoucher, - -401 => VoucherError::VoucherAlreadyUsed, - -100 => VoucherError::InternalError, - err => VoucherError::UnknownError(err), - } - } -} diff --git a/mullvad-types/src/custom_tunnel.rs b/mullvad-types/src/custom_tunnel.rs index 12c293c22d..b3cae97347 100644 --- a/mullvad-types/src/custom_tunnel.rs +++ b/mullvad-types/src/custom_tunnel.rs @@ -25,8 +25,8 @@ pub enum Error { #[cfg_attr(target_os = "android", jnix(package = "net.mullvad.mullvadvpn.model"))] #[cfg_attr(target_os = "android", jnix(skip_all))] pub struct CustomTunnelEndpoint { - host: String, - config: ConnectionConfig, + pub host: String, + pub config: ConnectionConfig, } impl CustomTunnelEndpoint { diff --git a/mullvad-types/src/lib.rs b/mullvad-types/src/lib.rs index d77206fc31..e93ab2f606 100644 --- a/mullvad-types/src/lib.rs +++ b/mullvad-types/src/lib.rs @@ -13,23 +13,3 @@ pub mod wireguard; mod custom_tunnel; pub use crate::custom_tunnel::*; - -/// An event sent out from the daemon to frontends. -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum DaemonEvent { - /// The daemon transitioned into a new state. - TunnelState(states::TunnelState), - - /// The daemon settings changed. - Settings(settings::Settings), - - /// The daemon got an updated relay list. - RelayList(relay_list::RelayList), - - /// The daemon got update version info. - AppVersionInfo(version::AppVersionInfo), - - /// Key event - WireguardKey(wireguard::KeygenEvent), -} diff --git a/mullvad-types/src/relay_constraints.rs b/mullvad-types/src/relay_constraints.rs index f8907cba9c..c5d58f621f 100644 --- a/mullvad-types/src/relay_constraints.rs +++ b/mullvad-types/src/relay_constraints.rs @@ -28,6 +28,13 @@ pub enum Constraint<T: fmt::Debug + Clone + Eq + PartialEq> { } impl<T: fmt::Debug + Clone + Eq + PartialEq> Constraint<T> { + pub fn unwrap(self) -> T { + match self { + Constraint::Any => panic!("called `Constraint::unwrap()` on an `Any` value"), + Constraint::Only(value) => value, + } + } + pub fn unwrap_or(self, other: T) -> T { match self { Constraint::Any => other, @@ -42,12 +49,36 @@ impl<T: fmt::Debug + Clone + Eq + PartialEq> Constraint<T> { } } + pub fn map<U: fmt::Debug + Clone + Eq + PartialEq, F: FnOnce(T) -> U>( + self, + f: F, + ) -> Constraint<U> { + match self { + Constraint::Any => Constraint::Any, + Constraint::Only(value) => Constraint::Only(f(value)), + } + } + pub fn is_any(&self) -> bool { match self { Constraint::Any => true, Constraint::Only(_value) => false, } } + + pub fn as_ref(&self) -> Constraint<&T> { + match self { + Constraint::Any => Constraint::Any, + Constraint::Only(ref value) => Constraint::Only(value), + } + } + + pub fn option(self) -> Option<T> { + match self { + Constraint::Any => None, + Constraint::Only(value) => Some(value), + } + } } impl<T: fmt::Debug + Clone + Eq + PartialEq> Default for Constraint<T> { @@ -67,6 +98,15 @@ impl<T: fmt::Debug + Clone + Eq + PartialEq> Match<T> for Constraint<T> { } } +impl<T: fmt::Debug + Clone + Eq + PartialEq> From<Option<T>> for Constraint<T> { + fn from(value: Option<T>) -> Self { + match value { + Some(value) => Constraint::Only(value), + None => Constraint::Any, + } + } +} + /// Specifies a specific endpoint or [`RelayConstraints`] to use when `mullvad-daemon` selects a /// relay. #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] diff --git a/talpid-core/Cargo.toml b/talpid-core/Cargo.toml index 07ffb7263a..6b40af9465 100644 --- a/talpid-core/Cargo.toml +++ b/talpid-core/Cargo.toml @@ -16,8 +16,6 @@ futures01 = { package = "futures", version = "0.1" } futures = { package = "futures", version = "0.3", features = [ "compat" ]} hex = "0.4" ipnetwork = "0.16" -jsonrpc-core = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } -jsonrpc-macros = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } lazy_static = "1.0" libc = "0.2" log = "0.4" @@ -26,7 +24,6 @@ os_pipe = "0.8" parking_lot = "0.9" regex = "1.1.0" shell-escape = "0.1" -talpid-ipc = { path = "../talpid-ipc" } talpid-types = { path = "../talpid-types" } tokio-core = "0.1" tokio-executor = "0.1" diff --git a/talpid-core/build.rs b/talpid-core/build.rs index 26b74f154a..c50f3411fd 100644 --- a/talpid-core/build.rs +++ b/talpid-core/build.rs @@ -93,5 +93,7 @@ fn declare_libs_dir(base: &str) { } fn generate_grpc_code() { - tonic_build::compile_protos("../talpid-openvpn-plugin/proto/openvpn_plugin.proto").unwrap(); + const PROTO_FILE: &str = "../talpid-openvpn-plugin/proto/openvpn_plugin.proto"; + tonic_build::compile_protos(PROTO_FILE).unwrap(); + println!("cargo:rerun-if-changed={}", PROTO_FILE); } diff --git a/talpid-ipc/Cargo.toml b/talpid-ipc/Cargo.toml deleted file mode 100644 index 0a15db0f48..0000000000 --- a/talpid-ipc/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "talpid-ipc" -version = "0.1.0" -authors = ["Mullvad VPN"] -description = "IPC client and server for talpid" -license = "GPL-3.0" -edition = "2018" -publish = false - -[dependencies] -err-derive = "0.2.1" -serde = "1.0" -serde_json = "1.0" -log = "0.4" -jsonrpc-core = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } -jsonrpc-pubsub = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } -jsonrpc-ipc-server = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } -tokio = "0.1" -futures = "0.1" - -jsonrpc-client-core = { git = "https://github.com/mullvad/jsonrpc-client-rs", rev = "68aac55b" } -jsonrpc-client-ipc = { git = "https://github.com/mullvad/jsonrpc-client-rs", rev = "68aac55b" } - -[target.'cfg(windows)'.dependencies] -winapi = { version = "0.3", features = ["accctrl", "aclapi", "securitybaseapi", "winbase", "winerror", "winnt"] } - -[dev-dependencies] -assert_matches = "1.0" -env_logger = "0.7" -jsonrpc-macros = { git = "https://github.com/mullvad/jsonrpc", branch = "mullvad-fork" } -uuid = { version = "0.7", features = ["v4"] } -futures = "0.1.23" -tokio = "0.1" diff --git a/talpid-ipc/src/lib.rs b/talpid-ipc/src/lib.rs deleted file mode 100644 index 2ab6d506c3..0000000000 --- a/talpid-ipc/src/lib.rs +++ /dev/null @@ -1,122 +0,0 @@ -#![deny(rust_2018_idioms)] - -use futures::Future; -use std::{io, thread}; - -use jsonrpc_core::{MetaIoHandler, Metadata}; -use jsonrpc_ipc_server::{MetaExtractor, NoopExtractor, SecurityAttributes, Server, ServerBuilder}; - -use std::fmt; - -#[cfg(windows)] -mod win; - -/// An Id created by the Ipc server that the client can use to connect to it -pub type IpcServerId = String; - -#[derive(err_derive::Error, Debug)] -#[error(no_from)] -pub enum Error { - #[error(display = "Unable to start IPC server")] - StartServerError(#[error(source)] io::Error), - - #[error(display = "IPC server thread panicked and never returned a start result")] - ServerThreadPanicError, - - #[error(display = "Error in IPC server")] - IpcServerError(#[error(source)] io::Error), - - #[error(display = "Unable to set permissions for IPC endpoint")] - PermissionsError(#[error(source)] io::Error), -} - - -pub struct IpcServer { - path: String, - server: Server, -} - -impl IpcServer { - pub fn start<M: Metadata + Default>( - handler: MetaIoHandler<M>, - path: &str, - ) -> Result<Self, Error> { - Self::start_with_metadata(handler, NoopExtractor, path) - } - - pub fn start_with_metadata<M, E>( - handler: MetaIoHandler<M>, - meta_extractor: E, - path: &str, - ) -> Result<Self, Error> - where - M: Metadata + Default, - E: MetaExtractor<M>, - { - let security_attributes = - SecurityAttributes::allow_everyone_create().map_err(Error::PermissionsError)?; - let server = ServerBuilder::with_meta_extractor(handler, meta_extractor) - .set_security_attributes(security_attributes) - .start(path) - .map_err(Error::StartServerError) - .and_then(|(fut, start, server)| { - thread::spawn(move || tokio::run(fut)); - if let Some(error) = start - .wait() - .map_err(|_cancelled| Error::ServerThreadPanicError)? - { - return Err(Error::IpcServerError(error)); - } - Ok(server) - }) - .map(|server| IpcServer { - path: path.to_owned(), - server, - })?; - - #[cfg(unix)] - { - use std::{fs, os::unix::fs::PermissionsExt}; - fs::set_permissions(&path, PermissionsExt::from_mode(0o766)) - .map_err(Error::PermissionsError)?; - } - #[cfg(windows)] - win::deny_network_access(path).map_err(Error::PermissionsError)?; - Ok(server) - } - - /// Returns the uds/named pipe path this `IpcServer` is listening on. - pub fn path(&self) -> &str { - &self.path - } - - /// Creates a handle bound to this `IpcServer` that can be used to shut it down. - pub fn close_handle(&self) -> CloseHandle { - CloseHandle(self.server.close_handle()) - } - - /// Consumes the server and waits for it to finish. Get a `CloseHandle` before calling this - /// if you want to be able to shut the server down. - pub fn wait(self) { - self.server.wait(); - } -} - -// FIXME: This custom impl is because `Server` does not implement `Debug` yet: -// https://github.com/paritytech/jsonrpc/pull/195 -impl fmt::Debug for IpcServer { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("IpcServer") - .field("path", &self.path) - .finish() - } -} - -#[derive(Clone)] -pub struct CloseHandle(jsonrpc_ipc_server::CloseHandle); - -impl CloseHandle { - pub fn close(self) { - self.0.close(); - } -} diff --git a/talpid-ipc/tests/ipc-client-server.rs b/talpid-ipc/tests/ipc-client-server.rs deleted file mode 100644 index 6be8349487..0000000000 --- a/talpid-ipc/tests/ipc-client-server.rs +++ /dev/null @@ -1,85 +0,0 @@ -// TODO fix these tests on Windows -#![cfg(not(windows))] - -use assert_matches::assert_matches; -use futures::{sync::oneshot, Future}; - -use jsonrpc_client_core::{Error as ClientError, Transport}; -use jsonrpc_core::{Error, IoHandler}; -use jsonrpc_macros::build_rpc_trait; -use std::{ - sync::{mpsc, Mutex}, - time::Duration, -}; - -build_rpc_trait! { - pub trait TestApi { - #[rpc(name = "foo")] - fn foo(&self, i64) -> Result<(), Error>; - } -} - -struct ApiImpl { - tx: Mutex<mpsc::Sender<i64>>, -} - -impl TestApi for ApiImpl { - fn foo(&self, i: i64) -> Result<(), Error> { - self.tx.lock().unwrap().send(i).unwrap(); - Ok(()) - } -} - -#[test] -fn can_call_rpcs_on_server() { - env_logger::init(); - - let (server, rx) = create_server(); - let server_path = server.path().to_owned(); - let client = create_client(server_path); - - let _result: () = client.call_method("foo", &[97]).wait().unwrap(); - assert_eq!(Ok(97), rx.recv_timeout(Duration::from_millis(500))); - - let result: Result<(), ClientError> = client.call_method("invalid_method", &[0]).wait(); - assert_matches!(result, Err(_)); - server.close_handle().close(); -} - -#[test] -#[should_panic] -fn ipc_client_invalid_url() { - let _client = create_client("INVALID ID".to_owned()); -} - -fn create_server() -> (talpid_ipc::IpcServer, mpsc::Receiver<i64>) { - let (tx, rx) = mpsc::channel(); - let rpc = ApiImpl { tx: Mutex::new(tx) }; - let mut io = IoHandler::new(); - io.extend_with(rpc.to_delegate()); - - let uuid = uuid::Uuid::new_v4().to_string(); - let ipc_path = if cfg!(windows) { - format!(r"\\.\pipe\ipc-test-{}", uuid) - } else { - format!("/tmp/ipc-test-{}", uuid) - }; - let server = talpid_ipc::IpcServer::start(io.into(), &ipc_path).unwrap(); - (server, rx) -} - -fn create_client(ipc_path: String) -> jsonrpc_client_core::ClientHandle { - use std::thread; - let (tx, rx) = oneshot::channel(); - - thread::spawn(move || { - let (client, client_handle) = - jsonrpc_client_ipc::IpcTransport::new(&ipc_path, &tokio::reactor::Handle::default()) - .expect("failed to construct a transport") - .into_client(); - tx.send(client_handle).unwrap(); - client.wait().unwrap(); - }); - - rx.wait().expect("Failed to construct a valid client") -} diff --git a/talpid-openvpn-plugin/build.rs b/talpid-openvpn-plugin/build.rs index acfa18a87c..08c9e4944d 100644 --- a/talpid-openvpn-plugin/build.rs +++ b/talpid-openvpn-plugin/build.rs @@ -1,5 +1,7 @@ fn main() { - tonic_build::compile_protos("proto/openvpn_plugin.proto").unwrap(); + const PROTO_FILE: &str = "proto/openvpn_plugin.proto"; + tonic_build::compile_protos(PROTO_FILE).unwrap(); + println!("cargo:rerun-if-changed={}", PROTO_FILE); #[cfg(windows)] { diff --git a/talpid-types/src/tunnel.rs b/talpid-types/src/tunnel.rs index 00e5bcad50..c803a489a7 100644 --- a/talpid-types/src/tunnel.rs +++ b/talpid-types/src/tunnel.rs @@ -68,6 +68,10 @@ impl ErrorState { pub fn cause(&self) -> &ErrorStateCause { &self.cause } + + pub fn block_failure(&self) -> Option<&FirewallPolicyError> { + self.block_failure.as_ref() + } } |
