This commit is contained in:
Edouard Oger 2019-02-11 13:49:47 -05:00
Родитель 15b2b5a58f
Коммит 57823e5851
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: A2F740742307674A
29 изменённых файлов: 1871 добавлений и 56 удалений

Просмотреть файл

@ -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)

105
Cargo.lock сгенерированный
Просмотреть файл

@ -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();

Просмотреть файл

@ -132,8 +132,8 @@ mod tests {
) -> Result<String> {
let principal = json!({ "email": email });
let payload = json!({
"principal": principal,
"public-key": serialized_public_key
"principal": principal,
"public-key": serialized_public_key
});
Ok(
SignedJWTBuilder::new(key_pair, issuer, issued_at, expires_at)

Просмотреть файл

@ -68,9 +68,9 @@ impl BrowserIDKeyPair for RSABrowserIDKeyPair {
let n = format!("{}", rsa.n().to_dec_str()?);
let e = format!("{}", rsa.e().to_dec_str()?);
Ok(json!({
"algorithm": "RS",
"n": n,
"e": e
"algorithm": "RS",
"n": n,
"e": e
}))
}
}

Просмотреть файл

@ -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(),
})
}
}

Просмотреть файл

@ -67,4 +67,4 @@ harness = false
[[bench]]
name = "search"
harness = false
harness = false