Send Tab implementation
This commit is contained in:
Родитель
15b2b5a58f
Коммит
57823e5851
|
@ -50,6 +50,13 @@
|
|||
|
||||
- iOS networking should use the reqwest backend, instead of failing ([#1032](https://github.com/mozilla/application-services/pull/1032))
|
||||
|
||||
## FxA
|
||||
|
||||
### What's new
|
||||
|
||||
- Android only: Added device registration and Firefox Send Tab capability support. Your app can opt into this by calling the `FirefoxAccount.initializeDevice` method. ([#676](https://github.com/mozilla/application-services/pull/676))
|
||||
|
||||
|
||||
# v0.26.0 (_2018-04-17_)
|
||||
|
||||
[Full Changelog](https://github.com/mozilla/application-services/compare/v0.25.2...v0.26.0)
|
||||
|
|
|
@ -67,7 +67,7 @@ version = "0.2.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
|
@ -170,7 +170,7 @@ name = "block-buffer"
|
|||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"block-padding 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"block-padding 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -178,7 +178,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -286,6 +286,17 @@ dependencies = [
|
|||
"webbrowser 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clicolors-control"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
version = "0.0.3"
|
||||
|
@ -302,6 +313,23 @@ dependencies = [
|
|||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"clicolors-control 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"parking_lot 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.3"
|
||||
|
@ -530,6 +558,16 @@ dependencies = [
|
|||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"console 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempfile 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "difference"
|
||||
version = "2.0.0"
|
||||
|
@ -788,6 +826,8 @@ dependencies = [
|
|||
"byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cli-support 0.1.0",
|
||||
"dialoguer 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ece 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ffi-support 0.3.3",
|
||||
"force-viaduct-reqwest 0.1.0",
|
||||
|
@ -804,9 +844,11 @@ dependencies = [
|
|||
"serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"sync15 0.1.0",
|
||||
"untrusted 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"viaduct 0.1.0",
|
||||
"webbrowser 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -818,6 +860,7 @@ dependencies = [
|
|||
"fxa-client 0.1.0",
|
||||
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"prost 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"viaduct 0.1.0",
|
||||
]
|
||||
|
@ -1366,6 +1409,11 @@ dependencies = [
|
|||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.2.2"
|
||||
|
@ -1534,7 +1582,7 @@ dependencies = [
|
|||
"sync15 0.1.0",
|
||||
"tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"url_serde 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
@ -2305,6 +2353,18 @@ dependencies = [
|
|||
"remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.0.7"
|
||||
|
@ -2338,14 +2398,23 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.1"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termios"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
|
@ -2682,6 +2751,15 @@ name = "webbrowser"
|
|||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "webbrowser"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "2.0.1"
|
||||
|
@ -2691,6 +2769,11 @@ dependencies = [
|
|||
"libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.2.8"
|
||||
|
@ -2767,7 +2850,7 @@ dependencies = [
|
|||
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
|
||||
"checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400"
|
||||
"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
|
||||
"checksum block-padding 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d75255892aeb580d3c566f213a2b6fdc1c66667839f45719ee1d30ebf2aea591"
|
||||
"checksum block-padding 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6d4dc3af3ee2e12f3e5d224e5e1e3d73668abbeb69e566d361f7d5563a4fdf09"
|
||||
"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
|
||||
"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
||||
"checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
|
||||
|
@ -2780,8 +2863,10 @@ dependencies = [
|
|||
"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878"
|
||||
"checksum clang-sys 0.28.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4227269cec09f5f83ff160be12a1e9b0262dd1aa305302d5ba296c2ebd291055"
|
||||
"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
|
||||
"checksum clicolors-control 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "73abfd4c73d003a674ce5d2933fca6ce6c42480ea84a5ffe0a2dc39ed56300f9"
|
||||
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||
"checksum colored 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6e9a455e156a4271e12fd0246238c380b1e223e3736663c7a18ed8b6362028a9"
|
||||
"checksum console 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2bf3720d3f3fc30b721ef1ae54e13af3264af4af39dc476a8de56a6ee1e2184b"
|
||||
"checksum constant_time_eq 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8ff012e225ce166d4422e0e78419d901719760f62ae2b7969ca6b564d1b54a9e"
|
||||
"checksum cookie 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1465f8134efa296b4c19db34d909637cb2bf0f7aaf21299e23e18fa29ac557cf"
|
||||
"checksum cookie_store 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b0d2f2ecb21dce00e2453268370312978af9b8024020c7a37ae2cc6dbbe64685"
|
||||
|
@ -2804,6 +2889,7 @@ dependencies = [
|
|||
"checksum csv-core 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa5cdef62f37e6ffe7d1f07a381bc0db32b7a3ff1cac0de56cb0d81e71f53d65"
|
||||
"checksum ctor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3b4c17619643c1252b5f690084b82639dd7fac141c57c8e77a00e0148132092c"
|
||||
"checksum ctrlc 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "630391922b1b893692c6334369ff528dcc3a9d8061ccf4c803aa8f83cb13db5e"
|
||||
"checksum dialoguer 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ad1c29a0368928e78c551354dbff79f103a962ad820519724ef0d74f1c62fa9"
|
||||
"checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
|
||||
"checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c"
|
||||
"checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
|
||||
|
@ -2884,6 +2970,7 @@ dependencies = [
|
|||
"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea"
|
||||
"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
|
||||
"checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba"
|
||||
"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
|
||||
"checksum opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93f5bb2e8e8dec81642920ccff6b61f1eb94fa3020c5a325c9851ff604152409"
|
||||
"checksum openssl 0.10.20 (registry+https://github.com/rust-lang/crates.io-index)" = "5a0d6b781aac4ac1bd6cafe2a2f0ad8c16ae8e1dd5184822a16c50139f8838d9"
|
||||
"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
|
||||
|
@ -2970,10 +3057,12 @@ dependencies = [
|
|||
"checksum syn 0.15.32 (registry+https://github.com/rust-lang/crates.io-index)" = "846620ec526c1599c070eff393bfeeeb88a93afa2513fc3b49f1fea84cf7b0ed"
|
||||
"checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015"
|
||||
"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
||||
"checksum tempfile 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "11ce2fe9db64b842314052e2421ac61a73ce41b898dc8e3750398b219c5fc1e0"
|
||||
"checksum tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b86c784c88d98c801132806dadd3819ed29d8600836c4088e855cdf3e178ed8a"
|
||||
"checksum term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
|
||||
"checksum termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4096add70612622289f2fdcdbd5086dc81c1e2675e6ae58d6c4f62a16c6d7f2f"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dde0593aeb8d47accea5392b39350015b5eccb12c0d98044d856983d89548dea"
|
||||
"checksum termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625"
|
||||
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
|
||||
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
|
||||
"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
|
||||
|
@ -3012,7 +3101,9 @@ dependencies = [
|
|||
"checksum walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9d9d7ed3431229a144296213105a390676cc49c9b6a72bd19f3176c98e129fa1"
|
||||
"checksum want 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "797464475f30ddb8830cc529aaaae648d581f99e2036a928877dfde027ddf6b3"
|
||||
"checksum webbrowser 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "21c311234fd1392a0071c1476cb7a4338dd2479e03e80e2328fbbf2db117b028"
|
||||
"checksum webbrowser 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f08b2530d0f0c96ae77672bd927d07cec2ea33b07d1a8f74cfcf35e61f98fd0e"
|
||||
"checksum which 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b57acb10231b9493c8472b20cb57317d0679a49e0bdbee44b3b803a6473af164"
|
||||
"checksum widestring 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "effc0e4ff8085673ea7b9b2e3c73f6bd4d118810c9009ed8f1e16bd96c331db6"
|
||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770"
|
||||
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||
|
|
|
@ -9,6 +9,7 @@ license = "MPL-2.0"
|
|||
base64 = "0.10.1"
|
||||
byteorder = "1.3.1"
|
||||
bytes = "0.4"
|
||||
ece = "0.1"
|
||||
failure = "0.1.3"
|
||||
hawk = { version = "1.0.5", optional = true }
|
||||
hex = "0.3.2"
|
||||
|
@ -21,6 +22,7 @@ ring = "0.14.5"
|
|||
serde = { version = "1.0.79", features = ["rc"] }
|
||||
serde_derive = "1.0.79"
|
||||
serde_json = "1.0.28"
|
||||
sync15 = { path = "../sync15" }
|
||||
untrusted = "0.6.2"
|
||||
url = "1.7.1"
|
||||
ffi-support = { path = "../support/ffi" }
|
||||
|
@ -30,6 +32,8 @@ rc_crypto = { path = "../support/rc_crypto" }
|
|||
[dev-dependencies]
|
||||
cli-support = { path = "../support/cli" }
|
||||
force-viaduct-reqwest = { path = "../support/force-viaduct-reqwest" }
|
||||
dialoguer = "0.3.0"
|
||||
webbrowser = "0.4.0"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.5"
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package mozilla.appservices.fxaclient
|
||||
|
||||
data class TabHistoryEntry(
|
||||
val title: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
// https://proandroiddev.com/til-when-is-when-exhaustive-31d69f630a8b
|
||||
val <T> T.exhaustive: T
|
||||
get() = this
|
||||
|
||||
sealed class AccountEvent {
|
||||
// A tab with all its history entries (back button).
|
||||
class TabReceived(val from: Device?, val entries: Array<TabHistoryEntry>) : AccountEvent()
|
||||
|
||||
companion object {
|
||||
private fun fromMessage(msg: MsgTypes.AccountEvent): AccountEvent {
|
||||
when (msg.type) {
|
||||
MsgTypes.AccountEvent.AccountEventType.TAB_RECEIVED -> {
|
||||
val data = msg.tabReceivedData
|
||||
return TabReceived(
|
||||
from = if (data.hasFrom()) Device.fromMessage(data.from) else null,
|
||||
entries = data.entriesList.map {
|
||||
TabHistoryEntry(title = it.title, url = it.url)
|
||||
}.toTypedArray()
|
||||
)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
internal fun fromCollectionMessage(msg: MsgTypes.AccountEvents): Array<AccountEvent> {
|
||||
return msg.eventsList.map {
|
||||
fromMessage(it)
|
||||
}.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package mozilla.appservices.fxaclient
|
||||
|
||||
data class Device(
|
||||
val id: String,
|
||||
val displayName: String,
|
||||
val deviceType: Type,
|
||||
val pushSubscription: PushSubscription?,
|
||||
val pushEndpointExpired: Boolean,
|
||||
val isCurrentDevice: Boolean,
|
||||
val lastAccessTime: Long?,
|
||||
val capabilities: List<Capability>
|
||||
) {
|
||||
enum class Capability {
|
||||
SEND_TAB;
|
||||
|
||||
companion object {
|
||||
internal fun fromMessage(msg: MsgTypes.Device.Capability): Capability {
|
||||
return when (msg) {
|
||||
MsgTypes.Device.Capability.SEND_TAB -> SEND_TAB
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
DESKTOP,
|
||||
MOBILE,
|
||||
UNKNOWN;
|
||||
|
||||
companion object {
|
||||
internal fun fromMessage(msg: MsgTypes.Device.Type): Type {
|
||||
return when (msg) {
|
||||
MsgTypes.Device.Type.DESKTOP -> DESKTOP
|
||||
MsgTypes.Device.Type.MOBILE -> MOBILE
|
||||
else -> UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toNumber(): Int {
|
||||
return MsgTypes.Device.Type.DESKTOP.number
|
||||
}
|
||||
}
|
||||
data class PushSubscription(
|
||||
val endpoint: String,
|
||||
val publicKey: String,
|
||||
val authKey: String
|
||||
) {
|
||||
companion object {
|
||||
internal fun fromMessage(msg: MsgTypes.Device.PushSubscription): PushSubscription {
|
||||
return PushSubscription(
|
||||
endpoint = msg.endpoint,
|
||||
publicKey = msg.publicKey,
|
||||
authKey = msg.authKey
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
internal fun fromMessage(msg: MsgTypes.Device): Device {
|
||||
return Device(
|
||||
id = msg.id,
|
||||
displayName = msg.displayName,
|
||||
deviceType = Type.fromMessage(msg.type),
|
||||
pushSubscription = if (msg.hasPushSubscription()) {
|
||||
PushSubscription.fromMessage(msg.pushSubscription)
|
||||
} else null,
|
||||
pushEndpointExpired = msg.pushEndpointExpired,
|
||||
isCurrentDevice = msg.isCurrentDevice,
|
||||
lastAccessTime = if (msg.hasLastAccessTime()) msg.lastAccessTime else null,
|
||||
capabilities = msg.capabilitiesList.map { Capability.fromMessage(it) }
|
||||
)
|
||||
}
|
||||
internal fun fromCollectionMessage(msg: MsgTypes.Devices): Array<Device> {
|
||||
return msg.devicesList.map {
|
||||
fromMessage(it)
|
||||
}.toTypedArray()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,10 +5,12 @@
|
|||
package mozilla.appservices.fxaclient
|
||||
|
||||
import android.util.Log
|
||||
import com.sun.jna.Native
|
||||
import com.sun.jna.Pointer
|
||||
import mozilla.appservices.fxaclient.rust.FxaHandle
|
||||
import mozilla.appservices.fxaclient.rust.LibFxAFFI
|
||||
import mozilla.appservices.fxaclient.rust.RustError
|
||||
import mozilla.appservices.support.toNioDirectBuffer
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/**
|
||||
|
@ -252,6 +254,150 @@ class FirefoxAccount(handle: FxaHandle, persistCallback: PersistCallback?) : Aut
|
|||
}.getAndConsumeRustString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the push subscription details for the current device.
|
||||
* This needs to be called every time a push subscription is modified or expires.
|
||||
*
|
||||
* This performs network requests, and should not be used on the main thread.
|
||||
*
|
||||
* @param endpoint Push callback URL
|
||||
* @param endpoint Public key used to encrypt push payloads
|
||||
* @param endpoint Auth key used to encrypt push payloads
|
||||
*/
|
||||
fun setDevicePushSubscription(endpoint: String, publicKey: String, authKey: String) {
|
||||
rustCall { e ->
|
||||
LibFxAFFI.INSTANCE.fxa_set_push_subscription(this.handle.get(), endpoint, publicKey, authKey, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the display name (as shown in the FxA device manager, or the Send Tab target list)
|
||||
* for the current device.
|
||||
*
|
||||
* This performs network requests, and should not be used on the main thread.
|
||||
*
|
||||
* @param displayName The current device display name
|
||||
*/
|
||||
fun setDeviceDisplayName(displayName: String) {
|
||||
rustCall { e ->
|
||||
LibFxAFFI.INSTANCE.fxa_set_device_name(this.handle.get(), displayName, e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the list of the connected devices in the current account, including the current one.
|
||||
*
|
||||
* This performs network requests, and should not be used on the main thread.
|
||||
*/
|
||||
fun getDevices(): Array<Device> {
|
||||
val devicesBuffer = rustCall { e ->
|
||||
LibFxAFFI.INSTANCE.fxa_get_devices(this.handle.get(), e)
|
||||
}
|
||||
try {
|
||||
val devices = MsgTypes.Devices.parseFrom(devicesBuffer.asCodedInputStream()!!)
|
||||
return Device.fromCollectionMessage(devices)
|
||||
} finally {
|
||||
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(devicesBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves any pending commands for the current device.
|
||||
* This should be called semi-regularly as the main method of commands delivery (push)
|
||||
* can sometimes be unreliable on mobile devices.
|
||||
* If a persist callback is set and the host application failed to process the
|
||||
* returned account events, they will never be seen again.
|
||||
*
|
||||
* This performs network requests, and should not be used on the main thread.
|
||||
*
|
||||
* @return A collection of [AccountEvent] that should be handled by the caller.
|
||||
*/
|
||||
fun pollDeviceCommands(): Array<AccountEvent> {
|
||||
val eventsBuffer = rustCall { e ->
|
||||
LibFxAFFI.INSTANCE.fxa_poll_device_commands(this.handle.get(), e)
|
||||
}
|
||||
this.tryPersistState()
|
||||
try {
|
||||
val events = MsgTypes.AccountEvents.parseFrom(eventsBuffer.asCodedInputStream()!!)
|
||||
return AccountEvent.fromCollectionMessage(events)
|
||||
} finally {
|
||||
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(eventsBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle any incoming push message payload coming from the Firefox Accounts
|
||||
* servers that has been decrypted and authenticated by the Push crate.
|
||||
*
|
||||
* This performs network requests, and should not be used on the main thread.
|
||||
*
|
||||
* @return A collection of [AccountEvent] that should be handled by the caller.
|
||||
*/
|
||||
fun handlePushMessage(payload: String): Array<AccountEvent> {
|
||||
val eventsBuffer = rustCall { e ->
|
||||
LibFxAFFI.INSTANCE.fxa_handle_push_message(this.handle.get(), payload, e)
|
||||
}
|
||||
this.tryPersistState()
|
||||
try {
|
||||
val events = MsgTypes.AccountEvents.parseFrom(eventsBuffer.asCodedInputStream()!!)
|
||||
return AccountEvent.fromCollectionMessage(events)
|
||||
} finally {
|
||||
LibFxAFFI.INSTANCE.fxa_bytebuffer_free(eventsBuffer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the current device is registered with the specified name and device type, with
|
||||
* the required capabilities (at this time only Send Tab).
|
||||
* This method should be called once per "device lifetime".
|
||||
*
|
||||
* This performs network requests, and should not be used on the main thread.
|
||||
*/
|
||||
fun initializeDevice(name: String, deviceType: Device.Type, supportedCapabilities: Set<Device.Capability>) {
|
||||
val capabilitiesBuilder = MsgTypes.Capabilities.newBuilder()
|
||||
supportedCapabilities.forEach {
|
||||
when (it) {
|
||||
Device.Capability.SEND_TAB -> capabilitiesBuilder.addCapability(MsgTypes.Device.Capability.SEND_TAB)
|
||||
}.exhaustive
|
||||
}
|
||||
val buf = capabilitiesBuilder.build()
|
||||
val (nioBuf, len) = buf.toNioDirectBuffer()
|
||||
rustCall { e ->
|
||||
val ptr = Native.getDirectBufferPointer(nioBuf)
|
||||
LibFxAFFI.INSTANCE.fxa_initialize_device(this.handle.get(), name, deviceType.toNumber(), ptr, len, e)
|
||||
}
|
||||
this.tryPersistState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the supported capabilities described earlier in `initializeDevice` are A-OK.
|
||||
* As for now there's only the Send Tab capability, so we ensure the command is registered with the server.
|
||||
* This method should be called at least every time the sync keys change (because Send Tab relies on them).
|
||||
*
|
||||
* This performs network requests, and should not be used on the main thread.
|
||||
*/
|
||||
fun ensureCapabilities() {
|
||||
rustCall { e ->
|
||||
LibFxAFFI.INSTANCE.fxa_ensure_capabilities(this.handle.get(), e)
|
||||
}
|
||||
this.tryPersistState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a single tab to another device identified by its device ID.
|
||||
*
|
||||
* This performs network requests, and should not be used on the main thread.
|
||||
*
|
||||
* @param targetDeviceId The target Device ID
|
||||
* @param title The document title of the tab being sent
|
||||
* @param url The url of the tab being sent
|
||||
*/
|
||||
fun sendSingleTab(targetDeviceId: String, title: String, url: String) {
|
||||
rustCall { e ->
|
||||
LibFxAFFI.INSTANCE.fxa_send_tab(this.handle.get(), targetDeviceId, title, url, e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun close() {
|
||||
val handle = this.handle.getAndSet(0)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@file:Suppress("MaxLineLength")
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
@ -11,7 +12,7 @@ import com.sun.jna.Pointer
|
|||
import java.lang.reflect.Proxy
|
||||
import mozilla.appservices.support.RustBuffer
|
||||
|
||||
@Suppress("FunctionNaming", "FunctionParameterNaming", "TooGenericExceptionThrown")
|
||||
@Suppress("FunctionNaming", "FunctionParameterNaming", "LongParameterList", "TooGenericExceptionThrown")
|
||||
internal interface LibFxAFFI : Library {
|
||||
companion object {
|
||||
private val JNA_LIBRARY_NAME = {
|
||||
|
@ -76,6 +77,29 @@ internal interface LibFxAFFI : Library {
|
|||
fun fxa_complete_oauth_flow(fxa: FxaHandle, code: String, state: String, e: RustError.ByReference)
|
||||
fun fxa_get_access_token(fxa: FxaHandle, scope: String, e: RustError.ByReference): RustBuffer.ByValue
|
||||
|
||||
fun fxa_set_push_subscription(
|
||||
fxa: FxaHandle,
|
||||
endpoint: String,
|
||||
publicKey: String,
|
||||
authKey: String,
|
||||
e: RustError.ByReference
|
||||
)
|
||||
fun fxa_set_device_name(fxa: FxaHandle, displayName: String, e: RustError.ByReference)
|
||||
fun fxa_get_devices(fxa: FxaHandle, e: RustError.ByReference): RustBuffer.ByValue
|
||||
fun fxa_poll_device_commands(fxa: FxaHandle, e: RustError.ByReference): RustBuffer.ByValue
|
||||
fun fxa_handle_push_message(fxa: FxaHandle, jsonPayload: String, e: RustError.ByReference): RustBuffer.ByValue
|
||||
|
||||
fun fxa_initialize_device(
|
||||
fxa: FxaHandle,
|
||||
name: String,
|
||||
type: Int,
|
||||
capabilities_data: Pointer,
|
||||
capabilities_len: Int,
|
||||
e: RustError.ByReference
|
||||
)
|
||||
fun fxa_ensure_capabilities(fxa: FxaHandle, e: RustError.ByReference)
|
||||
fun fxa_send_tab(fxa: FxaHandle, targetDeviceId: String, title: String, url: String, e: RustError.ByReference)
|
||||
|
||||
fun fxa_str_free(string: Pointer)
|
||||
fun fxa_bytebuffer_free(buffer: RustBuffer.ByValue)
|
||||
fun fxa_free(fxa: FxaHandle, err: RustError.ByReference)
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use cli_support::prompt::prompt_string;
|
||||
use dialoguer::Select;
|
||||
use fxa_client::{device, AccountEvent, Config, FirefoxAccount};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
io::{Read, Write},
|
||||
sync::{Arc, Mutex},
|
||||
thread, time,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
static CREDENTIALS_PATH: &'static str = "credentials.json";
|
||||
static CONTENT_SERVER: &'static str = "https://devices2.dev.lcip.org";
|
||||
static CLIENT_ID: &'static str = "3c49430b43dfba77";
|
||||
static REDIRECT_URI: &'static str = "https://devices2.dev.lcip.org/oauth/success/3c49430b43dfba77";
|
||||
static SCOPES: &'static [&'static str] = &["profile", "https://identity.mozilla.com/apps/oldsync"];
|
||||
static DEFAULT_DEVICE_NAME: &'static str = "Bobo device";
|
||||
|
||||
fn load_fxa_creds() -> Result<FirefoxAccount, failure::Error> {
|
||||
let mut file = fs::File::open(CREDENTIALS_PATH)?;
|
||||
let mut s = String::new();
|
||||
file.read_to_string(&mut s)?;
|
||||
Ok(FirefoxAccount::from_json(&s)?)
|
||||
}
|
||||
|
||||
fn load_or_create_fxa_creds(cfg: Config) -> Result<FirefoxAccount, failure::Error> {
|
||||
let acct = load_fxa_creds().or_else(|_e| create_fxa_creds(cfg))?;
|
||||
persist_fxa_state(&acct);
|
||||
Ok(acct)
|
||||
}
|
||||
|
||||
fn persist_fxa_state(acct: &FirefoxAccount) {
|
||||
let json = acct.to_json().unwrap();
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(CREDENTIALS_PATH)
|
||||
.unwrap();
|
||||
write!(file, "{}", json).unwrap();
|
||||
file.flush().unwrap();
|
||||
}
|
||||
|
||||
fn create_fxa_creds(cfg: Config) -> Result<FirefoxAccount, failure::Error> {
|
||||
let mut acct = FirefoxAccount::with_config(cfg);
|
||||
let oauth_uri = acct.begin_oauth_flow(&SCOPES, true)?;
|
||||
|
||||
if webbrowser::open(&oauth_uri.as_ref()).is_err() {
|
||||
println!("Please visit this URL, sign in, and then copy-paste the final URL below.");
|
||||
println!("\n {}\n", oauth_uri);
|
||||
} else {
|
||||
println!("Please paste the final URL below:\n");
|
||||
}
|
||||
|
||||
let redirect_uri: String = prompt_string("Final URL").unwrap();
|
||||
let redirect_uri = Url::parse(&redirect_uri).unwrap();
|
||||
let query_params: HashMap<_, _> = redirect_uri.query_pairs().into_owned().collect();
|
||||
let code = &query_params["code"];
|
||||
let state = &query_params["state"];
|
||||
acct.complete_oauth_flow(&code, &state).unwrap();
|
||||
persist_fxa_state(&acct);
|
||||
Ok(acct)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), failure::Error> {
|
||||
let cfg = Config::new(CONTENT_SERVER, CLIENT_ID, REDIRECT_URI);
|
||||
let mut acct = load_or_create_fxa_creds(cfg.clone())?;
|
||||
|
||||
// Make sure the device and the send-tab command are registered.
|
||||
acct.initialize_device(
|
||||
DEFAULT_DEVICE_NAME,
|
||||
device::Type::Desktop,
|
||||
&[device::Capability::SendTab],
|
||||
)
|
||||
.unwrap();
|
||||
persist_fxa_state(&acct);
|
||||
|
||||
let acct: Arc<Mutex<FirefoxAccount>> = Arc::new(Mutex::new(acct));
|
||||
{
|
||||
let acct = acct.clone();
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
let evts = acct
|
||||
.lock()
|
||||
.unwrap()
|
||||
.poll_device_commands()
|
||||
.unwrap_or_else(|_| vec![]); // Ignore 404 errors for now.
|
||||
persist_fxa_state(&acct.lock().unwrap());
|
||||
for e in evts {
|
||||
match e {
|
||||
AccountEvent::TabReceived((device, payload)) => {
|
||||
let tab = &payload.entries[0];
|
||||
match device {
|
||||
Some(ref d) => {
|
||||
println!("Tab received from {}: {}", d.display_name, tab.url)
|
||||
}
|
||||
None => println!("Tab received: {}", tab.url),
|
||||
};
|
||||
webbrowser::open(&tab.url).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
thread::sleep(time::Duration::from_secs(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Menu:
|
||||
loop {
|
||||
println!("Main menu:");
|
||||
let mut main_menu = Select::new();
|
||||
main_menu.items(&["Set Display Name", "Send a Tab", "Quit"]);
|
||||
main_menu.default(0);
|
||||
let main_menu_selection = main_menu.interact().unwrap();
|
||||
|
||||
match main_menu_selection {
|
||||
0 => {
|
||||
let new_name: String = prompt_string("New display name").unwrap();
|
||||
// Set device display name
|
||||
acct.lock().unwrap().set_device_name(&new_name).unwrap();
|
||||
println!("Display name set to: {}", new_name);
|
||||
}
|
||||
1 => {
|
||||
let devices = acct.lock().unwrap().get_devices().unwrap();
|
||||
let devices_names: Vec<String> =
|
||||
devices.iter().map(|i| i.display_name.clone()).collect();
|
||||
let mut targets_menu = Select::new();
|
||||
targets_menu.default(0);
|
||||
let devices_names_refs: Vec<&str> =
|
||||
devices_names.iter().map(AsRef::as_ref).collect();
|
||||
targets_menu.items(&devices_names_refs);
|
||||
println!("Choose a send-tab target:");
|
||||
let selection = targets_menu.interact().unwrap();
|
||||
let target = &devices[selection];
|
||||
|
||||
// Payload
|
||||
let title: String = prompt_string("Title").unwrap();
|
||||
let url: String = prompt_string("URL").unwrap();
|
||||
acct.lock()
|
||||
.unwrap()
|
||||
.send_tab(&target.id, &title, &url)
|
||||
.unwrap();
|
||||
println!("Tab sent!");
|
||||
}
|
||||
2 => ::std::process::exit(0),
|
||||
_ => panic!("Invalid choice!"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ ffi-support = { path = "../../support/ffi" }
|
|||
log = "0.4.6"
|
||||
lazy_static = "1.3.0"
|
||||
url = "1.7.1"
|
||||
prost = "0.5.0"
|
||||
viaduct = { path = "../../viaduct" }
|
||||
|
||||
[dependencies.fxa-client]
|
||||
|
|
|
@ -11,7 +11,7 @@ use ffi_support::{
|
|||
define_bytebuffer_destructor, define_handle_map_deleter, define_string_destructor, ByteBuffer,
|
||||
ConcurrentHandleMap, ExternError, FfiStr,
|
||||
};
|
||||
use fxa_client::FirefoxAccount;
|
||||
use fxa_client::{device::PushSubscription, msg_types, FirefoxAccount};
|
||||
use std::os::raw::c_char;
|
||||
use url::Url;
|
||||
|
||||
|
@ -268,6 +268,160 @@ pub extern "C" fn fxa_get_access_token(
|
|||
})
|
||||
}
|
||||
|
||||
/// Update the Push subscription information for the current device.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn fxa_set_push_subscription(
|
||||
handle: u64,
|
||||
endpoint: FfiStr<'_>,
|
||||
public_key: FfiStr<'_>,
|
||||
auth_key: FfiStr<'_>,
|
||||
error: &mut ExternError,
|
||||
) {
|
||||
log::debug!("fxa_set_push_subscription");
|
||||
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
|
||||
let ps = PushSubscription {
|
||||
endpoint: endpoint.into_string(),
|
||||
public_key: public_key.into_string(),
|
||||
auth_key: auth_key.into_string(),
|
||||
};
|
||||
// We don't really care about passing back the resulting Device record.
|
||||
// We might in the future though.
|
||||
fxa.set_push_subscription(&ps).map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
/// Update the display name for the current device.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn fxa_set_device_name(
|
||||
handle: u64,
|
||||
display_name: FfiStr<'_>,
|
||||
error: &mut ExternError,
|
||||
) {
|
||||
log::debug!("fxa_set_device_name");
|
||||
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
|
||||
// We don't really care about passing back the resulting Device record.
|
||||
// We might in the future though.
|
||||
fxa.set_device_name(display_name.as_str()).map(|_| ())
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch the devices (including the current one) in the current account.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
|
||||
/// pointer type.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn fxa_get_devices(handle: u64, error: &mut ExternError) -> ByteBuffer {
|
||||
log::debug!("fxa_get_devices");
|
||||
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
|
||||
fxa.get_devices().map(|d| {
|
||||
let devices = d.into_iter().map(|device| device.into()).collect();
|
||||
fxa_client::msg_types::Devices { devices }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Poll and parse available remote commands targeted to our own device.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
|
||||
/// pointer type.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn fxa_poll_device_commands(handle: u64, error: &mut ExternError) -> ByteBuffer {
|
||||
log::debug!("fxa_poll_device_commands");
|
||||
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
|
||||
fxa.poll_device_commands().map(|evs| {
|
||||
let events = evs.into_iter().map(|e| e.into()).collect();
|
||||
fxa_client::msg_types::AccountEvents { events }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle a push payload coming from the Firefox Account servers.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// A destructor [fxa_bytebuffer_free] is provided for releasing the memory for this
|
||||
/// pointer type.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn fxa_handle_push_message(
|
||||
handle: u64,
|
||||
json_payload: FfiStr<'_>,
|
||||
error: &mut ExternError,
|
||||
) -> ByteBuffer {
|
||||
log::debug!("fxa_handle_push_message");
|
||||
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
|
||||
fxa.handle_push_message(json_payload.as_str()).map(|evs| {
|
||||
let events = evs.into_iter().map(|e| e.into()).collect();
|
||||
fxa_client::msg_types::AccountEvents { events }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Initalizes our own device, most of the time this will be called right after logging-in
|
||||
/// for the first time.
|
||||
/// This method is marked un-safe as it reconstitutes an array of capabilities
|
||||
/// from a pointer.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn fxa_initialize_device(
|
||||
handle: u64,
|
||||
name: FfiStr<'_>,
|
||||
device_type: i32,
|
||||
capabilities_data: *const u8,
|
||||
capabilities_len: i32,
|
||||
error: &mut ExternError,
|
||||
) {
|
||||
log::debug!("fxa_initialize_device");
|
||||
ACCOUNTS.call_with_result_mut(error, handle, |fxa| {
|
||||
let buffer = get_buffer(capabilities_data, capabilities_len);
|
||||
let capabilities: fxa_client::msg_types::Capabilities = prost::Message::decode(buffer)?;
|
||||
// This should not fail as device_type i32 representation is derived from our .proto schema.
|
||||
let device_type =
|
||||
msg_types::device::Type::from_i32(device_type).expect("Unknown device type code");
|
||||
fxa.initialize_device(
|
||||
name.as_str(),
|
||||
device_type.into(),
|
||||
&capabilities.to_capabilities_vec(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure that the device capabilities are registered with the server.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn fxa_ensure_capabilities(handle: u64, error: &mut ExternError) {
|
||||
log::debug!("fxa_ensure_capabilities");
|
||||
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.ensure_capabilities())
|
||||
}
|
||||
|
||||
/// Send a tab to another device identified by its Device ID.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn fxa_send_tab(
|
||||
handle: u64,
|
||||
target_device_id: FfiStr<'_>,
|
||||
title: FfiStr<'_>,
|
||||
url: FfiStr<'_>,
|
||||
error: &mut ExternError,
|
||||
) {
|
||||
log::debug!("fxa_send_tab");
|
||||
let target = target_device_id.as_str();
|
||||
let title = title.as_str();
|
||||
let url = url.as_str();
|
||||
ACCOUNTS.call_with_result_mut(error, handle, |fxa| fxa.send_tab(target, title, url))
|
||||
}
|
||||
|
||||
unsafe fn get_buffer<'a>(data: *const u8, len: i32) -> &'a [u8] {
|
||||
assert!(len >= 0, "Bad buffer len: {}", len);
|
||||
if len == 0 {
|
||||
// This will still fail, but as a bad protobuf format.
|
||||
&[]
|
||||
} else {
|
||||
assert!(!data.is_null(), "Unexpected null data pointer");
|
||||
std::slice::from_raw_parts(data, len as usize)
|
||||
}
|
||||
}
|
||||
|
||||
define_handle_map_deleter!(ACCOUNTS, fxa_free);
|
||||
define_string_destructor!(fxa_str_free);
|
||||
define_bytebuffer_destructor!(fxa_bytebuffer_free);
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
Config, FirefoxAccount, StateV2,
|
||||
};
|
||||
use serde_derive::*;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
impl FirefoxAccount {
|
||||
// Initialize state from Firefox Accounts credentials obtained using the
|
||||
|
@ -42,6 +42,9 @@ impl FirefoxAccount {
|
|||
login_state,
|
||||
refresh_token: None,
|
||||
scoped_keys: HashMap::new(),
|
||||
last_handled_command: None,
|
||||
commands_data: HashMap::new(),
|
||||
device_capabilities: HashSet::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pub mod send_tab;
|
|
@ -0,0 +1,190 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/// The Send Tab functionality is backed by Firefox Accounts device commands.
|
||||
/// A device shows it can handle "Send Tab" commands by advertising the "open-uri"
|
||||
/// command in its on own device record.
|
||||
/// This command data bundle contains a one-time generated `PublicSendTabKeys`
|
||||
/// (while keeping locally `PrivateSendTabKeys` containing the private key),
|
||||
/// wrapped by the account oldsync scope `kSync` to form a `SendTabKeysPayload`.
|
||||
///
|
||||
/// When a device sends a tab to another, it decrypts that `SendTabKeysPayload` using `kSync`,
|
||||
/// uses the obtained public key to encrypt the `SendTabPayload` it created that
|
||||
/// contains the tab to send and finally forms the `EncryptedSendTabPayload` that is
|
||||
/// then sent to the target device.
|
||||
use crate::{device::Device, errors::*, scoped_keys::ScopedKey, scopes};
|
||||
use ece::{
|
||||
Aes128GcmEceWebPushImpl, LocalKeyPair, LocalKeyPairImpl, RemoteKeyPairImpl, WebPushParams,
|
||||
};
|
||||
use hex;
|
||||
use serde_derive::*;
|
||||
use sync15::{EncryptedPayload, KeyBundle};
|
||||
|
||||
pub const COMMAND_NAME: &str = "https://identity.mozilla.com/cmd/open-uri";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EncryptedSendTabPayload {
|
||||
/// URL Safe Base 64 encrypted send-tab payload.
|
||||
encrypted: String,
|
||||
}
|
||||
|
||||
impl EncryptedSendTabPayload {
|
||||
pub fn decrypt(self, keys: &PrivateSendTabKeys) -> Result<SendTabPayload> {
|
||||
let encrypted = base64::decode_config(&self.encrypted, base64::URL_SAFE_NO_PAD)?;
|
||||
let private_key = LocalKeyPairImpl::new(&keys.private_key)?;
|
||||
let decrypted =
|
||||
Aes128GcmEceWebPushImpl::decrypt(&private_key, &keys.auth_secret, &encrypted)?;
|
||||
Ok(serde_json::from_slice(&decrypted)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SendTabPayload {
|
||||
pub entries: Vec<TabHistoryEntry>,
|
||||
}
|
||||
|
||||
impl SendTabPayload {
|
||||
pub fn single_tab(title: &str, url: &str) -> Self {
|
||||
SendTabPayload {
|
||||
entries: vec![TabHistoryEntry {
|
||||
title: title.to_string(),
|
||||
url: url.to_string(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
fn encrypt(&self, keys: PublicSendTabKeys) -> Result<EncryptedSendTabPayload> {
|
||||
let bytes = serde_json::to_vec(&self)?;
|
||||
let public_key = base64::decode_config(&keys.public_key, base64::URL_SAFE_NO_PAD)?;
|
||||
let public_key = RemoteKeyPairImpl::from_raw(&public_key);
|
||||
let auth_secret = base64::decode_config(&keys.auth_secret, base64::URL_SAFE_NO_PAD)?;
|
||||
let encrypted = Aes128GcmEceWebPushImpl::encrypt(
|
||||
&public_key,
|
||||
&auth_secret,
|
||||
&bytes,
|
||||
WebPushParams::default(),
|
||||
)?;
|
||||
let encrypted = base64::encode_config(&encrypted, base64::URL_SAFE_NO_PAD);
|
||||
Ok(EncryptedSendTabPayload { encrypted })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct TabHistoryEntry {
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct PrivateSendTabKeys {
|
||||
public_key: Vec<u8>,
|
||||
private_key: Vec<u8>,
|
||||
auth_secret: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PrivateSendTabKeys {
|
||||
pub fn from_random() -> Result<Self> {
|
||||
let (key_pair, auth_secret) = ece::generate_keypair_and_auth_secret()?;
|
||||
let private_key = key_pair.to_raw();
|
||||
let public_key = key_pair.pub_as_raw()?;
|
||||
Ok(Self {
|
||||
public_key,
|
||||
private_key,
|
||||
auth_secret: auth_secret.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SendTabKeysPayload {
|
||||
/// Hex encoded kid.
|
||||
kid: String,
|
||||
/// Base 64 encoded IV.
|
||||
#[serde(rename = "IV")]
|
||||
iv: String,
|
||||
/// Hex encoded hmac.
|
||||
hmac: String,
|
||||
/// Base 64 encoded ciphertext.
|
||||
ciphertext: String,
|
||||
}
|
||||
|
||||
impl SendTabKeysPayload {
|
||||
fn decrypt(self, scoped_key: &ScopedKey) -> Result<PublicSendTabKeys> {
|
||||
let (ksync, kxcs) = extract_oldsync_key_components(scoped_key)?;
|
||||
if hex::decode(self.kid)? != kxcs {
|
||||
return Err(ErrorKind::MismatchedKeys.into());
|
||||
}
|
||||
let key = KeyBundle::from_ksync_bytes(&ksync)?;
|
||||
let encrypted_bso = EncryptedPayload {
|
||||
iv: self.iv,
|
||||
hmac: self.hmac,
|
||||
ciphertext: self.ciphertext,
|
||||
};
|
||||
Ok(encrypted_bso.decrypt_and_parse_payload(&key)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PublicSendTabKeys {
|
||||
/// URL Safe Base 64 encoded push public key.
|
||||
#[serde(rename = "publicKey")]
|
||||
public_key: String,
|
||||
/// URL Safe Base 64 encoded auth secret.
|
||||
#[serde(rename = "authSecret")]
|
||||
auth_secret: String,
|
||||
}
|
||||
|
||||
impl PublicSendTabKeys {
|
||||
fn encrypt(&self, scoped_key: &ScopedKey) -> Result<SendTabKeysPayload> {
|
||||
let (ksync, kxcs) = extract_oldsync_key_components(scoped_key)?;
|
||||
let key = KeyBundle::from_ksync_bytes(&ksync)?;
|
||||
let encrypted_payload = EncryptedPayload::from_cleartext_payload(&key, &self)?;
|
||||
Ok(SendTabKeysPayload {
|
||||
kid: hex::encode(kxcs),
|
||||
iv: encrypted_payload.iv,
|
||||
hmac: encrypted_payload.hmac,
|
||||
ciphertext: encrypted_payload.ciphertext,
|
||||
})
|
||||
}
|
||||
pub fn as_command_data(&self, scoped_key: &ScopedKey) -> Result<String> {
|
||||
let encrypted_public_keys = self.encrypt(scoped_key)?;
|
||||
Ok(serde_json::to_string(&encrypted_public_keys)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PrivateSendTabKeys> for PublicSendTabKeys {
|
||||
fn from(internal: PrivateSendTabKeys) -> Self {
|
||||
Self {
|
||||
public_key: base64::encode_config(&internal.public_key, base64::URL_SAFE_NO_PAD),
|
||||
auth_secret: base64::encode_config(&internal.auth_secret, base64::URL_SAFE_NO_PAD),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_send_command(
|
||||
scoped_key: &ScopedKey,
|
||||
target: &Device,
|
||||
send_tab_payload: &SendTabPayload,
|
||||
) -> Result<serde_json::Value> {
|
||||
let command = target
|
||||
.available_commands
|
||||
.get(COMMAND_NAME)
|
||||
.ok_or_else(|| ErrorKind::UnsupportedCommand(COMMAND_NAME))?;
|
||||
let bundle: SendTabKeysPayload = serde_json::from_str(command)?;
|
||||
let public_keys = bundle.decrypt(scoped_key)?;
|
||||
let encrypted_payload = send_tab_payload.encrypt(public_keys)?;
|
||||
Ok(serde_json::to_value(&encrypted_payload)?)
|
||||
}
|
||||
|
||||
fn extract_oldsync_key_components(oldsync_key: &ScopedKey) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
if oldsync_key.scope != scopes::OLD_SYNC {
|
||||
return Err(ErrorKind::IllegalState(
|
||||
"Only oldsync scoped keys are supported at the moment.",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let kxcs: &str = oldsync_key.kid.splitn(2, '-').collect::<Vec<_>>()[1];
|
||||
let kxcs = base64::decode_config(&kxcs, base64::URL_SAFE_NO_PAD)?;
|
||||
let ksync = oldsync_key.key_bytes()?;
|
||||
Ok((ksync, kxcs))
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pub use crate::http_client::{
|
||||
DeviceLocation as Location, DeviceType as Type, GetDeviceResponse as Device, PushSubscription,
|
||||
};
|
||||
use crate::{
|
||||
commands::{self, send_tab::SendTabPayload},
|
||||
errors::*,
|
||||
http_client::{
|
||||
CommandData, DeviceUpdateRequest, DeviceUpdateRequestBuilder, PendingCommand,
|
||||
UpdateDeviceResponse,
|
||||
},
|
||||
AccountEvent, FirefoxAccount,
|
||||
};
|
||||
use serde_derive::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl FirefoxAccount {
|
||||
/// Fetches the list of devices from the current account including
|
||||
/// the current one.
|
||||
pub fn get_devices(&self) -> Result<Vec<Device>> {
|
||||
let refresh_token = self.get_refresh_token()?;
|
||||
self.client.devices(&self.state.config, &refresh_token)
|
||||
}
|
||||
|
||||
pub fn get_current_device(&self) -> Result<Option<Device>> {
|
||||
Ok(self
|
||||
.get_devices()?
|
||||
.into_iter()
|
||||
.find(|d| d.is_current_device))
|
||||
}
|
||||
|
||||
/// Initalizes our own device, most of the time this will be called right after logging-in
|
||||
/// for the first time.
|
||||
///
|
||||
/// **💾 This method alters the persisted account state.**
|
||||
pub fn initialize_device(
|
||||
&mut self,
|
||||
name: &str,
|
||||
device_type: Type,
|
||||
capabilities: &[Capability],
|
||||
) -> Result<()> {
|
||||
let mut commands = HashMap::new();
|
||||
for capability in capabilities {
|
||||
match capability {
|
||||
Capability::SendTab => {
|
||||
let send_tab_command = self.generate_send_tab_command_data()?;
|
||||
commands.insert(
|
||||
commands::send_tab::COMMAND_NAME.to_owned(),
|
||||
send_tab_command.to_owned(),
|
||||
);
|
||||
self.state.device_capabilities.insert(Capability::SendTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
let update = DeviceUpdateRequestBuilder::new()
|
||||
.display_name(name)
|
||||
.device_type(&device_type)
|
||||
.available_commands(&commands)
|
||||
.build();
|
||||
self.update_device(update)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that the capabilities requested earlier in initialize_device are A-OK.
|
||||
/// As the only capability is Send Tab now, its command is registered with the server.
|
||||
/// Don't forget to also call this if the Sync Keys change as they
|
||||
/// encrypt the Send Tab command data.
|
||||
///
|
||||
/// **💾 This method alters the persisted account state.**
|
||||
pub fn ensure_capabilities(&mut self) -> Result<()> {
|
||||
for capability in self.state.device_capabilities.clone() {
|
||||
match capability {
|
||||
Capability::SendTab => {
|
||||
let send_tab_command = self.generate_send_tab_command_data()?;
|
||||
self.register_command(commands::send_tab::COMMAND_NAME, &send_tab_command)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn invoke_command(
|
||||
&self,
|
||||
command: &str,
|
||||
target: &Device,
|
||||
payload: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
let refresh_token = self.get_refresh_token()?;
|
||||
self.client.invoke_command(
|
||||
&self.state.config,
|
||||
&refresh_token,
|
||||
command,
|
||||
&target.id,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
/// Poll and parse any pending available command for our device.
|
||||
/// This should be called semi-regularly as the main method of
|
||||
/// commands delivery (push) can sometimes be unreliable on mobile devices.
|
||||
///
|
||||
/// **💾 This method alters the persisted account state.**
|
||||
pub fn poll_device_commands(&mut self) -> Result<Vec<AccountEvent>> {
|
||||
let last_command_index = self.state.last_handled_command.unwrap_or(0);
|
||||
// We increment last_command_index by 1 because the server response includes the current index.
|
||||
self.fetch_and_parse_commands(last_command_index + 1, None)
|
||||
}
|
||||
|
||||
/// Retrieve and parse a specific command designated by its index.
|
||||
///
|
||||
/// **💾 This method alters the persisted account state.**
|
||||
pub fn fetch_device_command(&mut self, index: u64) -> Result<AccountEvent> {
|
||||
let mut account_events = self.fetch_and_parse_commands(index, Some(1))?;
|
||||
let account_event = account_events
|
||||
.pop()
|
||||
.ok_or_else(|| ErrorKind::IllegalState("Index fetch came out empty."))?;
|
||||
if !account_events.is_empty() {
|
||||
log::warn!("Index fetch resulted in more than 1 element");
|
||||
}
|
||||
Ok(account_event)
|
||||
}
|
||||
|
||||
fn fetch_and_parse_commands(
|
||||
&mut self,
|
||||
index: u64,
|
||||
limit: Option<u64>,
|
||||
) -> Result<Vec<AccountEvent>> {
|
||||
let refresh_token = self.get_refresh_token()?;
|
||||
let pending_commands =
|
||||
self.client
|
||||
.pending_commands(&self.state.config, refresh_token, index, limit)?;
|
||||
if pending_commands.messages.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
log::info!("Handling {} messages", pending_commands.messages.len());
|
||||
let account_events = self.parse_commands_messages(pending_commands.messages)?;
|
||||
self.state.last_handled_command = Some(pending_commands.index);
|
||||
Ok(account_events)
|
||||
}
|
||||
|
||||
fn parse_commands_messages(&self, messages: Vec<PendingCommand>) -> Result<Vec<AccountEvent>> {
|
||||
let mut account_events: Vec<AccountEvent> = Vec::with_capacity(messages.len());
|
||||
let commands: Vec<_> = messages.into_iter().map(|m| m.data).collect();
|
||||
let devices = self.get_devices()?;
|
||||
for data in commands {
|
||||
match self.parse_command(data, &devices) {
|
||||
Ok((sender, tab)) => account_events.push(AccountEvent::TabReceived((sender, tab))),
|
||||
Err(e) => log::error!("Error while processing command: {}", e),
|
||||
};
|
||||
}
|
||||
Ok(account_events)
|
||||
}
|
||||
|
||||
// Returns SendTabPayload for now because we only receive send-tab commands and
|
||||
// it's way easier, but should probably return AccountEvent or similar in the future.
|
||||
fn parse_command(
|
||||
&self,
|
||||
command_data: CommandData,
|
||||
devices: &[Device],
|
||||
) -> Result<(Option<Device>, SendTabPayload)> {
|
||||
let sender = command_data
|
||||
.sender
|
||||
.and_then(|s| devices.iter().find(|i| i.id == s).cloned());
|
||||
match command_data.command.as_str() {
|
||||
commands::send_tab::COMMAND_NAME => {
|
||||
self.handle_send_tab_command(sender, command_data.payload)
|
||||
}
|
||||
_ => Err(ErrorKind::UnknownCommand(command_data.command).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_device_name(&self, name: &str) -> Result<UpdateDeviceResponse> {
|
||||
let update = DeviceUpdateRequestBuilder::new().display_name(name).build();
|
||||
self.update_device(update)
|
||||
}
|
||||
|
||||
pub fn clear_device_name(&self) -> Result<UpdateDeviceResponse> {
|
||||
let update = DeviceUpdateRequestBuilder::new()
|
||||
.clear_display_name()
|
||||
.build();
|
||||
self.update_device(update)
|
||||
}
|
||||
|
||||
pub fn set_push_subscription(
|
||||
&self,
|
||||
push_subscription: &PushSubscription,
|
||||
) -> Result<UpdateDeviceResponse> {
|
||||
let update = DeviceUpdateRequestBuilder::new()
|
||||
.push_subscription(&push_subscription)
|
||||
.build();
|
||||
self.update_device(update)
|
||||
}
|
||||
|
||||
// TODO: this currently overwrites every other registered command
|
||||
// for the device because the server does not have a `PATCH commands`
|
||||
// endpoint yet.
|
||||
pub(crate) fn register_command(
|
||||
&self,
|
||||
command: &str,
|
||||
value: &str,
|
||||
) -> Result<UpdateDeviceResponse> {
|
||||
let mut commands = HashMap::new();
|
||||
commands.insert(command.to_owned(), value.to_owned());
|
||||
let update = DeviceUpdateRequestBuilder::new()
|
||||
.available_commands(&commands)
|
||||
.build();
|
||||
self.update_device(update)
|
||||
}
|
||||
|
||||
// TODO: this currently deletes every command registered for the device
|
||||
// because the server does not have a `PATCH commands` endpoint yet.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn unregister_command(&self, _: &str) -> Result<UpdateDeviceResponse> {
|
||||
let commands = HashMap::new();
|
||||
let update = DeviceUpdateRequestBuilder::new()
|
||||
.available_commands(&commands)
|
||||
.build();
|
||||
self.update_device(update)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn clear_commands(&self) -> Result<UpdateDeviceResponse> {
|
||||
let update = DeviceUpdateRequestBuilder::new()
|
||||
.clear_available_commands()
|
||||
.build();
|
||||
self.update_device(update)
|
||||
}
|
||||
|
||||
pub(crate) fn replace_device(
|
||||
&self,
|
||||
display_name: &str,
|
||||
device_type: &Type,
|
||||
push_subscription: &Option<PushSubscription>,
|
||||
commands: &HashMap<String, String>,
|
||||
) -> Result<UpdateDeviceResponse> {
|
||||
let mut builder = DeviceUpdateRequestBuilder::new()
|
||||
.display_name(display_name)
|
||||
.device_type(device_type)
|
||||
.available_commands(commands);
|
||||
if let Some(push_subscription) = push_subscription {
|
||||
builder = builder.push_subscription(push_subscription)
|
||||
}
|
||||
self.update_device(builder.build())
|
||||
}
|
||||
|
||||
fn update_device(&self, update: DeviceUpdateRequest<'_>) -> Result<UpdateDeviceResponse> {
|
||||
let refresh_token = self.get_refresh_token()?;
|
||||
self.client
|
||||
.update_device(&self.state.config, refresh_token, update)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub enum Capability {
|
||||
SendTab,
|
||||
}
|
|
@ -69,9 +69,21 @@ pub enum ErrorKind {
|
|||
#[fail(display = "No cached token for scope {}", _0)]
|
||||
NoCachedToken(String),
|
||||
|
||||
#[fail(display = "No cached scoped keys for scope {}", _0)]
|
||||
NoScopedKey(String),
|
||||
|
||||
#[fail(display = "No stored refresh token")]
|
||||
NoRefreshToken,
|
||||
|
||||
#[fail(display = "Could not find a refresh token in the server response")]
|
||||
RefreshTokenNotPresent,
|
||||
|
||||
#[fail(display = "Action requires a prior device registration")]
|
||||
DeviceUnregistered,
|
||||
|
||||
#[fail(display = "Device target is unknown (Device ID: {})", _0)]
|
||||
UnknownTargetDevice(String),
|
||||
|
||||
#[fail(display = "Unrecoverable server error {}", _0)]
|
||||
UnrecoverableServerError(&'static str),
|
||||
|
||||
|
@ -79,7 +91,10 @@ pub enum ErrorKind {
|
|||
InvalidOAuthScopeValue(String),
|
||||
|
||||
#[fail(display = "Illegal state: {}", _0)]
|
||||
IllegalState(String),
|
||||
IllegalState(&'static str),
|
||||
|
||||
#[fail(display = "Unknown command: {}", _0)]
|
||||
UnknownCommand(String),
|
||||
|
||||
#[fail(display = "Empty names")]
|
||||
EmptyOAuthScopeNames,
|
||||
|
@ -111,6 +126,9 @@ pub enum ErrorKind {
|
|||
#[fail(display = "Key agreement failed")]
|
||||
KeyAgreementFailed,
|
||||
|
||||
#[fail(display = "Remote key and local key mismatch")]
|
||||
MismatchedKeys,
|
||||
|
||||
#[fail(display = "Key import failed")]
|
||||
KeyImportFailed,
|
||||
|
||||
|
@ -120,8 +138,11 @@ pub enum ErrorKind {
|
|||
#[fail(display = "Random number generation failure")]
|
||||
RngFailure,
|
||||
|
||||
#[fail(display = "HMAC verification failed")]
|
||||
HmacVerifyFail,
|
||||
#[fail(display = "HMAC mismatch")]
|
||||
HmacMismatch,
|
||||
|
||||
#[fail(display = "Unsupported command: {}", _0)]
|
||||
UnsupportedCommand(&'static str),
|
||||
|
||||
#[fail(
|
||||
display = "Remote server error: '{}' '{}' '{}' '{}' '{}'",
|
||||
|
@ -139,6 +160,9 @@ pub enum ErrorKind {
|
|||
CryptoError(#[fail(cause)] rc_crypto::Error),
|
||||
|
||||
// Basically reimplement error_chain's foreign_links. (Ugh, this sucks)
|
||||
#[fail(display = "http-ece encryption error: {}", _0)]
|
||||
EceError(#[fail(cause)] ece::Error),
|
||||
|
||||
#[fail(display = "Hex decode error: {}", _0)]
|
||||
HexDecodeError(#[fail(cause)] hex::FromHexError),
|
||||
|
||||
|
@ -164,9 +188,15 @@ pub enum ErrorKind {
|
|||
#[fail(display = "Unexpected HTTP status: {}", _0)]
|
||||
UnexpectedStatus(#[fail(cause)] viaduct::UnexpectedStatus),
|
||||
|
||||
#[fail(display = "Sync15 error: {}", _0)]
|
||||
SyncError(#[fail(cause)] sync15::Error),
|
||||
|
||||
#[cfg(feature = "browserid")]
|
||||
#[fail(display = "HAWK error: {}", _0)]
|
||||
HawkError(#[fail(cause)] SyncFailure<hawk::Error>),
|
||||
|
||||
#[fail(display = "Protobuf decode error: {}", _0)]
|
||||
ProtobufDecodeError(#[fail(cause)] prost::DecodeError),
|
||||
}
|
||||
|
||||
macro_rules! impl_from_error {
|
||||
|
@ -192,13 +222,16 @@ macro_rules! impl_from_error {
|
|||
|
||||
impl_from_error! {
|
||||
(CryptoError, rc_crypto::Error),
|
||||
(EceError, ece::Error),
|
||||
(HexDecodeError, ::hex::FromHexError),
|
||||
(Base64Decode, ::base64::DecodeError),
|
||||
(JsonError, ::serde_json::Error),
|
||||
(UTF8DecodeError, ::std::string::FromUtf8Error),
|
||||
(RequestError, viaduct::Error),
|
||||
(UnexpectedStatus, viaduct::UnexpectedStatus),
|
||||
(MalformedUrl, url::ParseError)
|
||||
(MalformedUrl, url::ParseError),
|
||||
(SyncError, ::sync15::Error),
|
||||
(ProtobufDecodeError, prost::DecodeError)
|
||||
}
|
||||
|
||||
#[cfg(feature = "browserid")]
|
||||
|
|
|
@ -12,7 +12,11 @@
|
|||
//!
|
||||
//! None of this is that bad in practice, but much of it is not ideal.
|
||||
|
||||
use crate::{msg_types, AccessTokenInfo, Error, ErrorKind, Profile, ScopedKey};
|
||||
use crate::{
|
||||
commands,
|
||||
device::{Capability as DeviceCapability, Device, PushSubscription, Type as DeviceType},
|
||||
msg_types, send_tab, AccessTokenInfo, AccountEvent, Error, ErrorKind, Profile, ScopedKey,
|
||||
};
|
||||
use ffi_support::{
|
||||
implement_into_ffi_by_delegation, implement_into_ffi_by_protobuf, ErrorCode, ExternError,
|
||||
};
|
||||
|
@ -23,8 +27,8 @@ pub mod error_codes {
|
|||
/// Catch-all error code used for anything that's not a panic or covered by AUTHENTICATION.
|
||||
pub const OTHER: i32 = 1;
|
||||
|
||||
/// Used for `ErrorKind::NotMarried`, `ErrorKind::NoCachedTokens`, and `ErrorKind::RemoteError`'s
|
||||
/// where `code == 401`.
|
||||
/// Used for `ErrorKind::NotMarried`, `ErrorKind::NoCachedTokens`, `ErrorKind::NoScopedKey`
|
||||
/// and `ErrorKind::RemoteError`'s where `code == 401`.
|
||||
pub const AUTHENTICATION: i32 = 2;
|
||||
|
||||
/// Code for network errors.
|
||||
|
@ -35,6 +39,8 @@ fn get_code(err: &Error) -> ErrorCode {
|
|||
match err.kind() {
|
||||
ErrorKind::RemoteError { code: 401, .. }
|
||||
| ErrorKind::NotMarried
|
||||
| ErrorKind::NoRefreshToken
|
||||
| ErrorKind::NoScopedKey(_)
|
||||
| ErrorKind::NoCachedToken(_) => {
|
||||
log::warn!("Authentication error: {:?}", err);
|
||||
ErrorCode::new(error_codes::AUTHENTICATION)
|
||||
|
@ -80,7 +86,7 @@ impl From<ScopedKey> for msg_types::ScopedKey {
|
|||
|
||||
impl From<Profile> for msg_types::Profile {
|
||||
fn from(p: Profile) -> Self {
|
||||
msg_types::Profile {
|
||||
Self {
|
||||
avatar: Some(p.avatar),
|
||||
avatar_default: Some(p.avatar_default),
|
||||
display_name: p.display_name,
|
||||
|
@ -90,7 +96,114 @@ impl From<Profile> for msg_types::Profile {
|
|||
}
|
||||
}
|
||||
|
||||
fn command_to_capability(command: &str) -> Option<msg_types::device::Capability> {
|
||||
match command {
|
||||
commands::send_tab::COMMAND_NAME => Some(msg_types::device::Capability::SendTab),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Device> for msg_types::Device {
|
||||
fn from(d: Device) -> Self {
|
||||
let capabilities = d
|
||||
.available_commands
|
||||
.keys()
|
||||
.filter_map(|c| command_to_capability(c).map(|cc| cc as i32))
|
||||
.collect();
|
||||
Self {
|
||||
id: d.common.id,
|
||||
display_name: d.common.display_name,
|
||||
r#type: Into::<msg_types::device::Type>::into(d.common.device_type) as i32,
|
||||
push_subscription: d.common.push_subscription.map(Into::into),
|
||||
push_endpoint_expired: d.common.push_endpoint_expired,
|
||||
is_current_device: d.is_current_device,
|
||||
last_access_time: d.last_access_time,
|
||||
capabilities,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DeviceType> for msg_types::device::Type {
|
||||
fn from(t: DeviceType) -> Self {
|
||||
match t {
|
||||
DeviceType::Desktop => msg_types::device::Type::Desktop,
|
||||
DeviceType::Mobile => msg_types::device::Type::Mobile,
|
||||
DeviceType::Unknown => msg_types::device::Type::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<msg_types::device::Type> for DeviceType {
|
||||
fn from(t: msg_types::device::Type) -> Self {
|
||||
match t {
|
||||
msg_types::device::Type::Desktop => DeviceType::Desktop,
|
||||
msg_types::device::Type::Mobile => DeviceType::Mobile,
|
||||
msg_types::device::Type::Unknown => DeviceType::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PushSubscription> for msg_types::device::PushSubscription {
|
||||
fn from(p: PushSubscription) -> Self {
|
||||
Self {
|
||||
endpoint: p.endpoint,
|
||||
public_key: p.public_key,
|
||||
auth_key: p.auth_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AccountEvent> for msg_types::AccountEvent {
|
||||
fn from(e: AccountEvent) -> Self {
|
||||
match e {
|
||||
AccountEvent::TabReceived((device, payload)) => Self {
|
||||
r#type: msg_types::account_event::AccountEventType::TabReceived as i32,
|
||||
data: Some(msg_types::account_event::Data::TabReceivedData(
|
||||
msg_types::account_event::TabReceivedData {
|
||||
from: device.map(Into::into),
|
||||
entries: payload.entries.into_iter().map(Into::into).collect(),
|
||||
},
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<send_tab::TabHistoryEntry>
|
||||
for msg_types::account_event::tab_received_data::TabHistoryEntry
|
||||
{
|
||||
fn from(data: send_tab::TabHistoryEntry) -> Self {
|
||||
Self {
|
||||
title: data.title,
|
||||
url: data.url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<msg_types::device::Capability> for DeviceCapability {
|
||||
fn from(cap: msg_types::device::Capability) -> Self {
|
||||
match cap {
|
||||
msg_types::device::Capability::SendTab => DeviceCapability::SendTab,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl msg_types::Capabilities {
|
||||
pub fn to_capabilities_vec(&self) -> Vec<DeviceCapability> {
|
||||
self.capability
|
||||
.iter()
|
||||
.map(|c| msg_types::device::Capability::from_i32(*c).unwrap().into())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
implement_into_ffi_by_protobuf!(msg_types::Profile);
|
||||
implement_into_ffi_by_delegation!(Profile, msg_types::Profile);
|
||||
implement_into_ffi_by_protobuf!(msg_types::AccessTokenInfo);
|
||||
implement_into_ffi_by_delegation!(AccessTokenInfo, msg_types::AccessTokenInfo);
|
||||
implement_into_ffi_by_protobuf!(msg_types::Device);
|
||||
implement_into_ffi_by_delegation!(Device, msg_types::Device);
|
||||
implement_into_ffi_by_delegation!(AccountEvent, msg_types::AccountEvent);
|
||||
implement_into_ffi_by_protobuf!(msg_types::AccountEvent);
|
||||
implement_into_ffi_by_protobuf!(msg_types::Devices);
|
||||
implement_into_ffi_by_protobuf!(msg_types::AccountEvents);
|
||||
|
|
|
@ -28,3 +28,60 @@ message ScopedKey {
|
|||
required string k = 3;
|
||||
required string kid = 4;
|
||||
}
|
||||
|
||||
message Device {
|
||||
message PushSubscription {
|
||||
required string endpoint = 1;
|
||||
required string public_key = 2;
|
||||
required string auth_key = 3;
|
||||
}
|
||||
enum Capability {
|
||||
SEND_TAB = 1;
|
||||
}
|
||||
enum Type {
|
||||
DESKTOP = 1;
|
||||
MOBILE = 2;
|
||||
UNKNOWN = 3;
|
||||
}
|
||||
required string id = 1;
|
||||
required string display_name = 2;
|
||||
required Type type = 3;
|
||||
optional PushSubscription push_subscription = 4;
|
||||
required bool push_endpoint_expired = 5;
|
||||
required bool is_current_device = 6;
|
||||
optional uint64 last_access_time = 7;
|
||||
repeated Capability capabilities = 8;
|
||||
}
|
||||
|
||||
message Devices {
|
||||
repeated Device devices = 1;
|
||||
}
|
||||
|
||||
message Capabilities {
|
||||
repeated Device.Capability capability = 1;
|
||||
}
|
||||
|
||||
// This is basically an enum with associated values,
|
||||
// but it's a bit harder to model in proto2.
|
||||
message AccountEvent {
|
||||
enum AccountEventType {
|
||||
TAB_RECEIVED = 1; // data set to TabReceivedData.
|
||||
}
|
||||
required AccountEventType type = 1;
|
||||
|
||||
message TabReceivedData {
|
||||
message TabHistoryEntry {
|
||||
required string title = 1;
|
||||
required string url = 2;
|
||||
}
|
||||
optional Device from = 1;
|
||||
repeated TabHistoryEntry entries = 2;
|
||||
}
|
||||
oneof data {
|
||||
TabReceivedData tab_received_data = 2;
|
||||
};
|
||||
}
|
||||
|
||||
message AccountEvents {
|
||||
repeated AccountEvent events = 1;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
use crate::{config::Config, errors::*};
|
||||
use serde_derive::*;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use viaduct::{header_names, status_codes, Request, Response};
|
||||
|
||||
#[cfg(feature = "browserid")]
|
||||
|
@ -30,6 +31,28 @@ pub trait FxAClient {
|
|||
profile_access_token: &str,
|
||||
etag: Option<String>,
|
||||
) -> Result<Option<ResponseAndETag<ProfileResponse>>>;
|
||||
fn pending_commands(
|
||||
&self,
|
||||
config: &Config,
|
||||
refresh_token: &str,
|
||||
index: u64,
|
||||
limit: Option<u64>,
|
||||
) -> Result<PendingCommandsResponse>;
|
||||
fn invoke_command(
|
||||
&self,
|
||||
config: &Config,
|
||||
refresh_token: &str,
|
||||
command: &str,
|
||||
target: &str,
|
||||
payload: &serde_json::Value,
|
||||
) -> Result<()>;
|
||||
fn devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>>;
|
||||
fn update_device(
|
||||
&self,
|
||||
config: &Config,
|
||||
refresh_token: &str,
|
||||
update: DeviceUpdateRequest<'_>,
|
||||
) -> Result<UpdateDeviceResponse>;
|
||||
}
|
||||
|
||||
pub struct Client;
|
||||
|
@ -37,14 +60,12 @@ impl FxAClient for Client {
|
|||
fn profile(
|
||||
&self,
|
||||
config: &Config,
|
||||
profile_access_token: &str,
|
||||
access_token: &str,
|
||||
etag: Option<String>,
|
||||
) -> Result<Option<ResponseAndETag<ProfileResponse>>> {
|
||||
let url = config.userinfo_endpoint()?;
|
||||
let mut request = Request::get(url).header(
|
||||
header_names::AUTHORIZATION,
|
||||
format!("Bearer {}", profile_access_token),
|
||||
)?;
|
||||
let mut request =
|
||||
Request::get(url).header(header_names::AUTHORIZATION, bearer_token(access_token))?;
|
||||
if let Some(etag) = etag {
|
||||
request = request.header(header_names::IF_NONE_MATCH, format!("\"{}\"", etag))?;
|
||||
}
|
||||
|
@ -99,21 +120,80 @@ impl FxAClient for Client {
|
|||
Self::make_request(Request::post(url).json(&body))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pending_commands(
|
||||
&self,
|
||||
config: &Config,
|
||||
refresh_token: &str,
|
||||
index: u64,
|
||||
limit: Option<u64>,
|
||||
) -> Result<PendingCommandsResponse> {
|
||||
let url = config.auth_url_path("v1/account/device/commands")?;
|
||||
let mut request = Request::get(url)
|
||||
.header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
|
||||
.query(&[("index", &index.to_string())]);
|
||||
if let Some(limit) = limit {
|
||||
request = request.query(&[("limit", &limit.to_string())])
|
||||
}
|
||||
Ok(Self::make_request(request)?.json()?)
|
||||
}
|
||||
|
||||
fn invoke_command(
|
||||
&self,
|
||||
config: &Config,
|
||||
refresh_token: &str,
|
||||
command: &str,
|
||||
target: &str,
|
||||
payload: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
let body = json!({
|
||||
"command": command,
|
||||
"target": target,
|
||||
"payload": payload
|
||||
});
|
||||
let url = config.auth_url_path("v1/account/devices/invoke_command")?;
|
||||
let request = Request::post(url)
|
||||
.header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
|
||||
.header(header_names::CONTENT_TYPE, "application/json")?
|
||||
.body(body.to_string());
|
||||
Self::make_request(request)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn devices(&self, config: &Config, refresh_token: &str) -> Result<Vec<GetDeviceResponse>> {
|
||||
let url = config.auth_url_path("v1/account/devices")?;
|
||||
let request =
|
||||
Request::get(url).header(header_names::AUTHORIZATION, bearer_token(refresh_token))?;
|
||||
Ok(Self::make_request(request)?.json()?)
|
||||
}
|
||||
|
||||
fn update_device(
|
||||
&self,
|
||||
config: &Config,
|
||||
refresh_token: &str,
|
||||
update: DeviceUpdateRequest<'_>,
|
||||
) -> Result<UpdateDeviceResponse> {
|
||||
let url = config.auth_url_path("v1/account/device")?;
|
||||
let request = Request::post(url)
|
||||
.header(header_names::AUTHORIZATION, bearer_token(refresh_token))?
|
||||
.header(header_names::CONTENT_TYPE, "application/json")?
|
||||
.body(serde_json::to_string(&update)?);
|
||||
Ok(Self::make_request(request)?.json()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn make_oauth_token_request(
|
||||
&self,
|
||||
config: &Config,
|
||||
body: serde_json::Value,
|
||||
) -> Result<OAuthTokenResponse> {
|
||||
let url = config.token_endpoint()?;
|
||||
Self::make_request(Request::post(url).json(&body))?
|
||||
.json()
|
||||
.map_err(Into::into)
|
||||
Ok(Self::make_request(Request::post(url).json(&body))?.json()?)
|
||||
}
|
||||
|
||||
fn make_request(request: Request) -> Result<Response> {
|
||||
|
@ -137,12 +217,183 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
fn bearer_token(token: &str) -> String {
|
||||
format!("Bearer {}", token)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ResponseAndETag<T> {
|
||||
pub response: T,
|
||||
pub etag: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PendingCommandsResponse {
|
||||
pub index: u64,
|
||||
pub last: Option<bool>,
|
||||
pub messages: Vec<PendingCommand>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PendingCommand {
|
||||
pub index: u64,
|
||||
pub data: CommandData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CommandData {
|
||||
pub command: String,
|
||||
pub payload: serde_json::Value, // Need https://github.com/serde-rs/serde/issues/912 to make payload an enum instead.
|
||||
pub sender: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct PushSubscription {
|
||||
#[serde(rename = "pushCallback")]
|
||||
pub endpoint: String,
|
||||
#[serde(rename = "pushPublicKey")]
|
||||
pub public_key: String,
|
||||
#[serde(rename = "pushAuthKey")]
|
||||
pub auth_key: String,
|
||||
}
|
||||
|
||||
/// We use the double Option pattern in this struct.
|
||||
/// The outer option represents the existence of the field
|
||||
/// and the inner option its value or null.
|
||||
/// TL;DR:
|
||||
/// `None`: the field will not be present in the JSON body.
|
||||
/// `Some(None)`: the field will have a `null` value.
|
||||
/// `Some(Some(T))`: the field will have the serialized value of T.
|
||||
#[derive(Serialize)]
|
||||
#[allow(clippy::option_option)]
|
||||
pub struct DeviceUpdateRequest<'a> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "name")]
|
||||
display_name: Option<Option<&'a str>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "type")]
|
||||
device_type: Option<Option<&'a DeviceType>>,
|
||||
#[serde(flatten)]
|
||||
push_subscription: Option<&'a PushSubscription>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "availableCommands")]
|
||||
available_commands: Option<Option<&'a HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum DeviceType {
|
||||
#[serde(rename = "desktop")]
|
||||
Desktop,
|
||||
#[serde(rename = "mobile")]
|
||||
Mobile,
|
||||
#[serde(other)]
|
||||
#[serde(skip_serializing)] // Don't you dare trying.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[allow(clippy::option_option)]
|
||||
pub struct DeviceUpdateRequestBuilder<'a> {
|
||||
device_type: Option<Option<&'a DeviceType>>,
|
||||
display_name: Option<Option<&'a str>>,
|
||||
push_subscription: Option<&'a PushSubscription>,
|
||||
available_commands: Option<Option<&'a HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
impl<'a> DeviceUpdateRequestBuilder<'a> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
device_type: None,
|
||||
display_name: None,
|
||||
push_subscription: None,
|
||||
available_commands: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_subscription(mut self, push_subscription: &'a PushSubscription) -> Self {
|
||||
self.push_subscription = Some(push_subscription);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn available_commands(mut self, available_commands: &'a HashMap<String, String>) -> Self {
|
||||
self.available_commands = Some(Some(available_commands));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_available_commands(mut self) -> Self {
|
||||
self.available_commands = Some(None);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn display_name(mut self, display_name: &'a str) -> Self {
|
||||
self.display_name = Some(Some(display_name));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_display_name(mut self) -> Self {
|
||||
self.display_name = Some(None);
|
||||
self
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn device_type(mut self, device_type: &'a DeviceType) -> Self {
|
||||
self.device_type = Some(Some(device_type));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> DeviceUpdateRequest<'a> {
|
||||
DeviceUpdateRequest {
|
||||
display_name: self.display_name,
|
||||
device_type: self.device_type,
|
||||
push_subscription: self.push_subscription,
|
||||
available_commands: self.available_commands,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceLocation {
|
||||
pub city: Option<String>,
|
||||
pub country: Option<String>,
|
||||
pub state: Option<String>,
|
||||
#[serde(rename = "stateCode")]
|
||||
pub state_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct GetDeviceResponse {
|
||||
#[serde(flatten)]
|
||||
pub common: DeviceResponseCommon,
|
||||
#[serde(rename = "isCurrentDevice")]
|
||||
pub is_current_device: bool,
|
||||
pub location: DeviceLocation,
|
||||
#[serde(rename = "lastAccessTime")]
|
||||
pub last_access_time: Option<u64>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for GetDeviceResponse {
|
||||
type Target = DeviceResponseCommon;
|
||||
fn deref(&self) -> &DeviceResponseCommon {
|
||||
&self.common
|
||||
}
|
||||
}
|
||||
|
||||
pub type UpdateDeviceResponse = DeviceResponseCommon;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceResponseCommon {
|
||||
pub id: String,
|
||||
#[serde(rename = "name")]
|
||||
pub display_name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub device_type: DeviceType,
|
||||
#[serde(flatten)]
|
||||
pub push_subscription: Option<PushSubscription>,
|
||||
#[serde(rename = "availableCommands")]
|
||||
pub available_commands: HashMap<String, String>,
|
||||
#[serde(rename = "pushEndpointExpired")]
|
||||
pub push_endpoint_expired: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct OAuthTokenResponse {
|
||||
pub keys_jwe: Option<String>,
|
||||
|
|
|
@ -114,7 +114,7 @@ impl FxABrowserIDClient for http_client::Client {
|
|||
let xor_key = &bytes[KEY_LENGTH..(KEY_LENGTH * 3)];
|
||||
|
||||
let v_key = hmac::VerificationKey::new(&digest::SHA256, hmac_key);
|
||||
hmac::verify(&v_key, ciphertext, mac_code).map_err(|_| ErrorKind::HmacVerifyFail)?;
|
||||
hmac::verify(&v_key, ciphertext, mac_code).map_err(|_| ErrorKind::HmacMismatch)?;
|
||||
|
||||
let xored_bytes = ciphertext.xored_with(xor_key)?;
|
||||
let wrap_kb = xored_bytes[KEY_LENGTH..(KEY_LENGTH * 2)].to_vec();
|
||||
|
|
|
@ -9,20 +9,28 @@
|
|||
pub use crate::browser_id::{SyncKeys, WebChannelResponse};
|
||||
#[cfg(feature = "browserid")]
|
||||
use crate::login_sm::LoginState;
|
||||
pub use crate::{config::Config, oauth::AccessTokenInfo, profile::Profile};
|
||||
use crate::{
|
||||
commands::send_tab::SendTabPayload,
|
||||
device::{Capability as DeviceCapability, Device},
|
||||
errors::*,
|
||||
oauth::{OAuthFlow, RefreshToken, ScopedKey},
|
||||
oauth::{OAuthFlow, RefreshToken},
|
||||
scoped_keys::ScopedKey,
|
||||
};
|
||||
pub use crate::{config::Config, oauth::AccessTokenInfo, profile::Profile};
|
||||
use lazy_static::lazy_static;
|
||||
use ring::rand::SystemRandom;
|
||||
use serde_derive::*;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "browserid")]
|
||||
mod browser_id;
|
||||
mod commands;
|
||||
mod config;
|
||||
pub mod device;
|
||||
pub mod errors;
|
||||
pub mod ffi;
|
||||
// Include the `msg_types` module, which is generated from msg_types.proto.
|
||||
|
@ -36,11 +44,12 @@ mod oauth;
|
|||
mod profile;
|
||||
mod scoped_keys;
|
||||
pub mod scopes;
|
||||
pub mod send_tab;
|
||||
mod state_persistence;
|
||||
mod util;
|
||||
|
||||
lazy_static! {
|
||||
static ref RNG: SystemRandom = SystemRandom::new();
|
||||
pub static ref RNG: SystemRandom = SystemRandom::new();
|
||||
}
|
||||
|
||||
#[cfg(feature = "browserid")]
|
||||
|
@ -67,6 +76,12 @@ pub(crate) struct StateV2 {
|
|||
login_state: LoginState,
|
||||
refresh_token: Option<RefreshToken>,
|
||||
scoped_keys: HashMap<String, ScopedKey>,
|
||||
last_handled_command: Option<u64>,
|
||||
// Remove serde(default) once we are V3.
|
||||
#[serde(default)]
|
||||
commands_data: HashMap<String, String>,
|
||||
#[serde(default)] // Same
|
||||
device_capabilities: HashSet<DeviceCapability>,
|
||||
}
|
||||
|
||||
impl FirefoxAccount {
|
||||
|
@ -90,6 +105,9 @@ impl FirefoxAccount {
|
|||
login_state: LoginState::Unknown,
|
||||
refresh_token: None,
|
||||
scoped_keys: HashMap::new(),
|
||||
last_handled_command: None,
|
||||
commands_data: HashMap::new(),
|
||||
device_capabilities: HashSet::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -111,7 +129,7 @@ impl FirefoxAccount {
|
|||
self.client = client;
|
||||
}
|
||||
|
||||
/// Restore a `FirefoxAccount` instance from an serialized state
|
||||
/// Restore a `FirefoxAccount` instance from a serialized state
|
||||
/// created using `to_json`.
|
||||
pub fn from_json(data: &str) -> Result<Self> {
|
||||
let state = state_persistence::state_from_json(data)?;
|
||||
|
@ -176,6 +194,38 @@ impl FirefoxAccount {
|
|||
.append_pair("email", &profile.email);
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
/// Handle any incoming push message payload coming from the Firefox Accounts
|
||||
/// servers that has been decrypted and authenticated by the Push crate.
|
||||
///
|
||||
/// Due to iOS platform restrictions, a push notification must always show UI,
|
||||
/// and therefore we only retrieve 1 command per message.
|
||||
///
|
||||
/// **💾 This method alters the persisted account state.**
|
||||
pub fn handle_push_message(&mut self, payload: &str) -> Result<Vec<AccountEvent>> {
|
||||
let payload = serde_json::from_str(payload)?;
|
||||
match payload {
|
||||
PushPayload::CommandReceived(CommandReceivedPushPayload { index, .. }) => {
|
||||
if cfg!(target_os = "ios") {
|
||||
self.fetch_device_command(index).map(|cmd| vec![cmd])
|
||||
} else {
|
||||
self.poll_device_commands()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_refresh_token(&self) -> Result<&str> {
|
||||
match self.state.refresh_token {
|
||||
Some(ref token_info) => Ok(&token_info.token),
|
||||
None => Err(ErrorKind::NoRefreshToken.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AccountEvent {
|
||||
// In the future: ProfileUpdated etc.
|
||||
TabReceived((Option<Device>, SendTabPayload)),
|
||||
}
|
||||
|
||||
pub(crate) struct CachedResponse<T> {
|
||||
|
@ -184,6 +234,21 @@ pub(crate) struct CachedResponse<T> {
|
|||
etag: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "command", content = "data")]
|
||||
pub enum PushPayload {
|
||||
#[serde(rename = "fxaccounts:command_received")]
|
||||
CommandReceived(CommandReceivedPushPayload),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CommandReceivedPushPayload {
|
||||
command: String,
|
||||
index: u64,
|
||||
sender: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -272,4 +337,10 @@ mod tests {
|
|||
.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_push_message() {
|
||||
let json = "{\"version\":1,\"command\":\"fxaccounts:command_received\",\"data\":{\"command\":\"send-tab-recv\",\"index\":1,\"sender\":\"bobo\",\"url\":\"https://mozilla.org\"}}";
|
||||
let _: PushPayload = serde_json::from_str(&json).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::{errors::*, scoped_keys::ScopedKeysFlow, util, FirefoxAccount, RNG};
|
||||
use crate::{
|
||||
errors::*,
|
||||
scoped_keys::{ScopedKey, ScopedKeysFlow},
|
||||
util, FirefoxAccount, RNG,
|
||||
};
|
||||
use rc_crypto::digest;
|
||||
use serde_derive::*;
|
||||
use std::{
|
||||
|
@ -64,7 +68,7 @@ impl FirefoxAccount {
|
|||
};
|
||||
let since_epoch = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_err(|_| ErrorKind::IllegalState("Current date before Unix Epoch.".to_string()))?;
|
||||
.map_err(|_| ErrorKind::IllegalState("Current date before Unix Epoch."))?;
|
||||
let expires_at = since_epoch.as_secs() + resp.expires_in;
|
||||
let token_info = AccessTokenInfo {
|
||||
scope: resp.scope,
|
||||
|
@ -208,12 +212,25 @@ impl FirefoxAccount {
|
|||
// In order to keep 1 and only 1 refresh token alive per client instance,
|
||||
// we also destroy the existing refresh token.
|
||||
if let Some(ref old_refresh_token) = self.state.refresh_token {
|
||||
// Destroying a refresh token also destroys its associated device,
|
||||
// grab the device information for replication later.
|
||||
let device_info = self.get_current_device()?;
|
||||
if let Err(err) = self
|
||||
.client
|
||||
.destroy_oauth_token(&self.state.config, &old_refresh_token.token)
|
||||
{
|
||||
log::warn!("Refresh token destruction failure: {:?}", err);
|
||||
}
|
||||
if let Some(device_info) = device_info {
|
||||
if let Err(err) = self.replace_device(
|
||||
&device_info.display_name,
|
||||
&device_info.device_type,
|
||||
&device_info.push_subscription,
|
||||
&device_info.available_commands,
|
||||
) {
|
||||
log::warn!("Device information restoration failed: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.state.refresh_token = Some(RefreshToken {
|
||||
token: refresh_token,
|
||||
|
@ -223,21 +240,6 @@ impl FirefoxAccount {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ScopedKey {
|
||||
pub kty: String,
|
||||
pub scope: String,
|
||||
/// URL Safe Base 64 encoded key.
|
||||
pub k: String,
|
||||
pub kid: String,
|
||||
}
|
||||
|
||||
impl ScopedKey {
|
||||
pub fn key_bytes(&self) -> Result<Vec<u8>> {
|
||||
Ok(base64::decode_config(&self.k, base64::URL_SAFE_NO_PAD)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RefreshToken {
|
||||
pub token: String,
|
||||
|
|
|
@ -112,6 +112,36 @@ mod tests {
|
|||
panic!("Not implemented yet")
|
||||
}
|
||||
}
|
||||
fn pending_commands(
|
||||
&self,
|
||||
_: &Config,
|
||||
_: &str,
|
||||
_: u64,
|
||||
_: Option<u64>,
|
||||
) -> Result<PendingCommandsResponse> {
|
||||
unimplemented!("Not implemented yet")
|
||||
}
|
||||
fn invoke_command(
|
||||
&self,
|
||||
_: &Config,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: &serde_json::Value,
|
||||
) -> Result<()> {
|
||||
unimplemented!("Not implemented yet")
|
||||
}
|
||||
fn devices(&self, _: &Config, _: &str) -> Result<Vec<GetDeviceResponse>> {
|
||||
unimplemented!("Not implemented yet")
|
||||
}
|
||||
fn update_device(
|
||||
&self,
|
||||
_: &Config,
|
||||
_: &str,
|
||||
_: DeviceUpdateRequest<'_>,
|
||||
) -> Result<UpdateDeviceResponse> {
|
||||
unimplemented!("Not implemented yet")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -2,13 +2,38 @@
|
|||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::{errors::*, FirefoxAccount};
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use rc_crypto::digest;
|
||||
use ring::{aead, agreement, agreement::EphemeralPrivateKey, rand::SecureRandom};
|
||||
use serde_derive::*;
|
||||
use serde_json::{self, json};
|
||||
use untrusted::Input;
|
||||
|
||||
impl FirefoxAccount {
|
||||
pub(crate) fn get_scoped_key(&self, scope: &str) -> Result<&ScopedKey> {
|
||||
self.state
|
||||
.scoped_keys
|
||||
.get(scope)
|
||||
.ok_or_else(|| ErrorKind::NoScopedKey(scope.to_string()).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ScopedKey {
|
||||
pub kty: String,
|
||||
pub scope: String,
|
||||
/// URL Safe Base 64 encoded key.
|
||||
pub k: String,
|
||||
pub kid: String,
|
||||
}
|
||||
|
||||
impl ScopedKey {
|
||||
pub fn key_bytes(&self) -> Result<Vec<u8>> {
|
||||
Ok(base64::decode_config(&self.k, base64::URL_SAFE_NO_PAD)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScopedKeysFlow {
|
||||
private_key: EphemeralPrivateKey,
|
||||
}
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pub const PROFILE: &str = "profile";
|
||||
pub const OLD_SYNC: &str = "https://identity.mozilla.com/apps/oldsync";
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
pub use crate::commands::send_tab::{SendTabPayload, TabHistoryEntry};
|
||||
use crate::{
|
||||
commands::send_tab::{self, EncryptedSendTabPayload, PrivateSendTabKeys, PublicSendTabKeys},
|
||||
errors::*,
|
||||
http_client::GetDeviceResponse,
|
||||
scopes, FirefoxAccount,
|
||||
};
|
||||
|
||||
impl FirefoxAccount {
|
||||
/// Generate the Send Tab command to be registered with the server.
|
||||
///
|
||||
/// **💾 This method alters the persisted account state.**
|
||||
pub(crate) fn generate_send_tab_command_data(&mut self) -> Result<String> {
|
||||
let own_keys: PrivateSendTabKeys =
|
||||
match self.state.commands_data.get(send_tab::COMMAND_NAME) {
|
||||
Some(s) => serde_json::from_str(s)?,
|
||||
None => {
|
||||
let keys = PrivateSendTabKeys::from_random()?;
|
||||
self.state.commands_data.insert(
|
||||
send_tab::COMMAND_NAME.to_owned(),
|
||||
serde_json::to_string(&keys)?,
|
||||
);
|
||||
keys
|
||||
}
|
||||
};
|
||||
let public_keys: PublicSendTabKeys = own_keys.into();
|
||||
let oldsync_key = self.get_scoped_key(scopes::OLD_SYNC)?;
|
||||
public_keys.as_command_data(&oldsync_key)
|
||||
}
|
||||
|
||||
/// Send a single tab to another device designated by its device ID.
|
||||
pub fn send_tab(&self, target_device_id: &str, title: &str, url: &str) -> Result<()> {
|
||||
let devices = self.get_devices()?;
|
||||
let target = devices
|
||||
.iter()
|
||||
.find(|d| d.id == target_device_id)
|
||||
.ok_or_else(|| ErrorKind::UnknownTargetDevice(target_device_id.to_owned()))?;
|
||||
let payload = SendTabPayload::single_tab(title, url);
|
||||
let oldsync_key = self.get_scoped_key(scopes::OLD_SYNC)?;
|
||||
let command_payload = send_tab::build_send_command(&oldsync_key, target, &payload)?;
|
||||
self.invoke_command(send_tab::COMMAND_NAME, target, &command_payload)
|
||||
}
|
||||
|
||||
pub(crate) fn handle_send_tab_command(
|
||||
&self,
|
||||
sender: Option<GetDeviceResponse>,
|
||||
payload: serde_json::Value,
|
||||
) -> Result<(Option<GetDeviceResponse>, SendTabPayload)> {
|
||||
let send_tab_key: PrivateSendTabKeys =
|
||||
match self.state.commands_data.get(send_tab::COMMAND_NAME) {
|
||||
Some(s) => serde_json::from_str(s)?,
|
||||
None => {
|
||||
return Err(ErrorKind::IllegalState(
|
||||
"Cannot find send-tab keys. Has initialize_device been called before?",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
let encrypted_payload: EncryptedSendTabPayload = serde_json::from_value(payload)?;
|
||||
Ok((sender, encrypted_payload.decrypt(&send_tab_key)?))
|
||||
}
|
||||
}
|
|
@ -84,6 +84,9 @@ impl From<StateV1> for Result<StateV2> {
|
|||
login_state: super::login_sm::LoginState::Unknown,
|
||||
refresh_token,
|
||||
scoped_keys: all_scoped_keys,
|
||||
last_handled_command: None,
|
||||
commands_data: HashMap::new(),
|
||||
device_capabilities: HashSet::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче