diff options
38 files changed, 4658 insertions, 103 deletions
diff --git a/Cargo.lock b/Cargo.lock index c02b5d23b3..9ba618673a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + +[[package]] name = "aead" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -59,6 +65,15 @@ dependencies = [ ] [[package]] +name = "aes-kw" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fa2b352dcefb5f7f3a5fb840e02665d311d878955380515e4fd50095dd3d8c" +dependencies = [ + "aes", +] + +[[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -161,6 +176,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + +[[package]] name = "arrayref" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -168,9 +196,9 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "assert-json-diff" @@ -199,19 +227,28 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] [[package]] +name = "async-tempfile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb90d9834a8015109afc79f1f548223a0614edcbab62fb35b62d4b707e975e7" +dependencies = [ + "tokio", +] + +[[package]] name = "async-trait" version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -332,6 +369,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + +[[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -344,6 +387,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] name = "blake3" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -357,6 +409,12 @@ dependencies = [ ] [[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -366,6 +424,15 @@ dependencies = [ ] [[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -375,6 +442,35 @@ dependencies = [ ] [[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bstr" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "buffer-redux" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8acf87c5b9f5897cd3ebb9a327f420e0cae9dd4e5c1d2e36f2c84c571a58f1" +dependencies = [ + "memchr", +] + +[[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -399,6 +495,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] +name = "cacao" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5952f0958672e4aa8fc706d01905c56af57759e078c53a6fddf4a13361943e7a" +dependencies = [ + "block", + "core-foundation", + "core-graphics", + "dispatch", + "lazy_static", + "libc", + "objc", + "objc_id", + "os_info", + "url", +] + +[[package]] name = "camellia" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -409,6 +523,15 @@ dependencies = [ ] [[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher", +] + +[[package]] name = "cbindgen" version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -417,8 +540,8 @@ dependencies = [ "heck 0.4.1", "indexmap 1.9.3", "log", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "serde", "serde_json", "syn 1.0.109", @@ -435,8 +558,8 @@ dependencies = [ "heck 0.4.1", "indexmap 2.2.6", "log", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "serde", "serde_json", "syn 2.0.89", @@ -460,6 +583,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher", +] + +[[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -563,8 +695,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -586,6 +718,23 @@ dependencies = [ ] [[package]] +name = "cmac" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8543454e3c3f5126effff9cd44d562af4e31fb8ce1cc0d3dcd8f084515dbc1aa" +dependencies = [ + "cipher", + "dbl", + "digest", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] name = "colorchoice" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -623,6 +772,18 @@ dependencies = [ ] [[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + +[[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -651,6 +812,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] name = "cpufeatures" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -660,6 +845,12 @@ dependencies = [ ] [[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + +[[package]] name = "crc32fast" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -678,6 +869,25 @@ dependencies = [ ] [[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -734,7 +944,8 @@ dependencies = [ "cfg-if", "cpufeatures", "curve25519-dalek-derive", - "fiat-crypto", + "digest", + "fiat-crypto 0.2.8", "rustc_version", "subtle", "zeroize", @@ -746,8 +957,8 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -769,8 +980,8 @@ checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "strsim", "syn 2.0.89", ] @@ -782,7 +993,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", - "quote", + "quote 1.0.36", "syn 2.0.89", ] @@ -806,6 +1017,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] +name = "dbl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2735a791158376708f9347fe8faba9667589d82427ef3aed6794a8981de3d9" +dependencies = [ + "generic-array", +] + +[[package]] name = "dbus" version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -817,12 +1037,23 @@ dependencies = [ ] [[package]] +name = "deflate" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" +dependencies = [ + "adler32", + "byteorder", +] + +[[package]] name = "der" version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -841,8 +1072,8 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302ccf094df1151173bb6f5a2282fcd2f45accd5eae1bdf82dcbfefbc501ad5c" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 1.0.109", ] @@ -862,8 +1093,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" dependencies = [ "darling", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -878,12 +1109,43 @@ dependencies = [ ] [[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", + "unicode-xid 0.2.6", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + +[[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -910,17 +1172,39 @@ dependencies = [ ] [[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] name = "displaydoc" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] [[package]] +name = "dsa" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48bc224a9084ad760195584ce5abb3c2c34a225fa312a128ad245a6b412b7689" +dependencies = [ + "digest", + "num-bigint-dig", + "num-traits", + "pkcs8", + "rfc6979", + "sha2", + "signature", + "zeroize", +] + +[[package]] name = "duct" version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -933,14 +1217,30 @@ dependencies = [ ] [[package]] +name = "eax" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9954fabd903b82b9d7a68f65f97dc96dd9ad368e40ccc907a7c19d53e6bfac28" +dependencies = [ + "aead", + "cipher", + "cmac", + "ctr", + "subtle", +] + +[[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", + "digest", "elliptic-curve", + "rfc6979", "signature", + "spki", ] [[package]] @@ -954,6 +1254,31 @@ dependencies = [ ] [[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "ed448-goldilocks" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87b5fa9e9e3dd5fe1369f380acd3dcdfa766dbd0a1cd5b048fb40e38a6a78e79" +dependencies = [ + "fiat-crypto 0.1.20", + "hex", + "subtle", +] + +[[package]] name = "either" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -971,6 +1296,9 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", + "pem-rfc7468", + "pkcs8", "rand_core 0.6.4", "sec1", "subtle", @@ -978,14 +1306,29 @@ dependencies = [ ] [[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] name = "enum-as-inner" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ "heck 0.4.1", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -1004,8 +1347,8 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -1103,6 +1446,12 @@ dependencies = [ [[package]] name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + +[[package]] +name = "fiat-crypto" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e" @@ -1142,6 +1491,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1213,8 +1577,8 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -1294,6 +1658,16 @@ dependencies = [ ] [[package]] +name = "gif" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471d90201b3b223f3451cd4ad53e34295f16a1df17b1edf3736d47761c3981af" +dependencies = [ + "color_quant", + "lzw", +] + +[[package]] name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1642,6 +2016,22 @@ dependencies = [ ] [[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] name = "hyper-util" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1796,12 +2186,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] [[package]] +name = "idea" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" +dependencies = [ + "cipher", +] + +[[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1829,6 +2228,24 @@ dependencies = [ ] [[package]] +name = "image" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebdff791af04e30089bde8ad2a632b86af433b40c04db8d70ad4b21487db7a6a" +dependencies = [ + "byteorder", + "gif", + "jpeg-decoder", + "lzw", + "num-derive", + "num-iter", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", +] + +[[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1849,6 +2266,15 @@ dependencies = [ ] [[package]] +name = "inflate" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" +dependencies = [ + "adler32", +] + +[[package]] name = "inotify" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1891,6 +2317,38 @@ dependencies = [ ] [[package]] +name = "insta" +version = "1.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1b125e30d93896b365e156c33dadfffab45ee8400afcbba4752f59de08a86" +dependencies = [ + "console", + "linked-hash-map", + "once_cell", + "pin-project", + "serde", + "similar", +] + +[[package]] +name = "installer-downloader" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-trait", + "cacao", + "insta", + "mullvad-update", + "native-windows-gui", + "nsvg", + "objc_id", + "serde", + "tokio", + "windows-sys 0.52.0", + "winres", +] + +[[package]] name = "internet-checksum" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1900,8 +2358,8 @@ checksum = "fc6d6206008e25125b1f97fbe5d309eb7b85141cf9199d52dbd3729a1584dd16" name = "intersection-derive" version = "0.0.0" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -1965,6 +2423,12 @@ dependencies = [ ] [[package]] +name = "iter-read" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071ed4cc1afd86650602c7b11aa2e1ce30762a1c27193201cb5cee9c6ebb1294" + +[[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2027,12 +2491,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "002f4dfe6d97ae88c33f3489c0d31ffc6f81d9a492de98ff113b127d73bafff8" dependencies = [ "heck 0.4.1", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 1.0.109", ] [[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + +[[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2042,6 +2515,17 @@ dependencies = [ ] [[package]] +name = "json-canon" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447ae153a2bd47d61acc0d131295408e32ef87ed9785825a6f4ecef85afc0edb" +dependencies = [ + "ryu-js", + "serde", + "serde_json", +] + +[[package]] name = "json5" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2053,6 +2537,20 @@ dependencies = [ ] [[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] name = "keccak" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2096,6 +2594,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" @@ -2124,6 +2625,12 @@ dependencies = [ [[package]] name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + +[[package]] +name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" @@ -2168,9 +2675,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "log-panics" @@ -2197,6 +2704,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd" [[package]] +name = "lzw" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] name = "match_cfg" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2277,6 +2799,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] name = "miniz_oxide" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2696,6 +3224,27 @@ dependencies = [ ] [[package]] +name = "mullvad-update" +version = "0.0.0" +dependencies = [ + "anyhow", + "async-tempfile", + "async-trait", + "chrono", + "ed25519-dalek", + "hex", + "json-canon", + "mockito", + "mullvad-version", + "pgp", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "tokio", +] + +[[package]] name = "mullvad-version" version = "0.0.0" dependencies = [ @@ -2710,6 +3259,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" [[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "native-windows-gui" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7003a669f68deb6b7c57d74fff4f8e533c44a3f0b297492440ef4ff5a28454" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "newline-converter", + "plotters", + "plotters-backend", + "stretch", + "winapi", + "winapi-build", +] + +[[package]] name = "natord" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2736,7 +3318,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6813fde79b646e47e7ad75f480aa80ef76a5d9599e2717407961531169ee38b" dependencies = [ - "quote", + "quote 1.0.36", "syn 2.0.89", "syn-mid", ] @@ -2808,6 +3390,15 @@ dependencies = [ ] [[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] name = "nftnl" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2886,6 +3477,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" [[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] name = "notify" version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2914,19 +3515,118 @@ dependencies = [ ] [[package]] +name = "nsvg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfa50149c05ca80b01c6a30452084a98d96279f911df8b6840bd18b068cc120" +dependencies = [ + "cc", + "image", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm 0.2.8", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "serde", + "smallvec", + "zeroize", +] + +[[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] +name = "num-derive" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee314c74bd753fc86b4780aa9475da469155f3848473a261d2d18e35245a784e" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] name = "num-traits" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", - "libm", + "libm 0.2.8", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", ] [[package]] @@ -3032,6 +3732,15 @@ dependencies = [ ] [[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] name = "object" version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3041,10 +3750,22 @@ dependencies = [ ] [[package]] +name = "ocb3" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c196e0276c471c843dd5777e7543a36a298a4be942a2a688d8111cd43390dedb" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + +[[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "opaque-debug" @@ -3053,6 +3774,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2 1.0.92", + "quote 1.0.36", + "syn 2.0.89", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "openvpn-plugin" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3070,6 +3835,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] +name = "os_info" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6520c8cc998c5741ee68ec1dc369fc47e5f0ea5320018ecf2a1ccd6328f48b" +dependencies = [ + "log", + "serde", + "windows-sys 0.52.0", +] + +[[package]] name = "os_pipe" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3098,6 +3874,8 @@ checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ "ecdsa", "elliptic-curve", + "primeorder", + "sha2", ] [[package]] @@ -3109,6 +3887,21 @@ dependencies = [ "ecdsa", "elliptic-curve", "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2", ] [[package]] @@ -3149,6 +3942,17 @@ dependencies = [ ] [[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] name = "paste" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3172,6 +3976,15 @@ dependencies = [ ] [[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3206,8 +4019,8 @@ checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687" dependencies = [ "pest", "pest_meta", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -3245,6 +4058,73 @@ dependencies = [ ] [[package]] +name = "pgp" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1877a97fd422433220ad272eb008ec55691944b1200e9eb204e3cb2cb69d34e9" +dependencies = [ + "aes", + "aes-gcm", + "aes-kw", + "argon2", + "base64 0.22.1", + "bitfield", + "block-padding", + "blowfish", + "bstr", + "buffer-redux", + "byteorder", + "camellia", + "cast5", + "cfb-mode", + "chrono", + "cipher", + "const-oid", + "crc24", + "curve25519-dalek", + "derive_builder", + "derive_more", + "des", + "digest", + "dsa", + "eax", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "flate2", + "generic-array", + "hex", + "hkdf", + "idea", + "iter-read", + "k256", + "log", + "md-5", + "nom", + "num-bigint-dig", + "num-traits", + "num_enum", + "ocb3", + "p256", + "p384", + "p521", + "rand 0.8.5", + "ripemd", + "rsa", + "sha1", + "sha1-checked", + "sha2", + "sha3", + "signature", + "smallvec", + "thiserror 1.0.59", + "twofish", + "x25519-dalek", + "x448", + "zeroize", +] + +[[package]] name = "phf" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3297,8 +4177,8 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -3315,6 +4195,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3331,6 +4222,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] name = "pnet_base" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3354,8 +4263,8 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "regex", "syn 2.0.89", ] @@ -3366,8 +4275,8 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "regex", "syn 2.0.89", ] @@ -3415,6 +4324,18 @@ dependencies = [ ] [[package]] +name = "png" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54b9600d584d3b8a739e1662a595fab051329eff43f20e7d8cc22872962145b" +dependencies = [ + "bitflags 1.3.2", + "deflate", + "inflate", + "num-iter", +] + +[[package]] name = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3465,7 +4386,7 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.92", "syn 2.0.89", ] @@ -3479,6 +4400,24 @@ dependencies = [ ] [[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3556,8 +4495,8 @@ checksum = "19de2de2a00075bf566bee3bd4db014b11587e84184d3f7a791bc17f1a8e9e48" dependencies = [ "anyhow", "itertools 0.12.1", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -3569,8 +4508,8 @@ checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" dependencies = [ "anyhow", "itertools 0.12.1", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -3662,11 +4601,20 @@ dependencies = [ [[package]] name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.92", ] [[package]] @@ -3760,6 +4708,26 @@ dependencies = [ ] [[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] name = "redox_syscall" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3822,18 +4790,23 @@ checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", + "futures-channel", "futures-core", "futures-util", + "h2 0.4.4", "http 1.1.0", "http-body", "http-body-util", "hyper", "hyper-rustls", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -3845,7 +4818,9 @@ dependencies = [ "serde_json", "serde_urlencoded", "sync_wrapper 1.0.1", + "system-configuration 0.6.1", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.0", "tower-service", "url", @@ -3867,6 +4842,16 @@ dependencies = [ ] [[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] name = "ring" version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3876,7 +4861,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.14", "libc", - "spin", + "spin 0.9.8", "untrusted", "windows-sys 0.52.0", ] @@ -3901,12 +4886,41 @@ dependencies = [ ] [[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest", +] + +[[package]] name = "rs-release" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21efba391745f92fc14a5cccb008e711a1a3708d8dacd2e69d88d5de513c117a" [[package]] +name = "rsa" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] name = "rtnetlink" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4056,6 +5070,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] +name = "ryu-js" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" + +[[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4065,6 +5085,21 @@ dependencies = [ ] [[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4089,11 +5124,35 @@ dependencies = [ "base16ct", "der", "generic-array", + "pkcs8", "subtle", "zeroize", ] [[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "semver" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4130,8 +5189,8 @@ version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -4180,6 +5239,17 @@ dependencies = [ ] [[package]] +name = "sha1-checked" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" +dependencies = [ + "digest", + "sha1", + "zeroize", +] + +[[package]] name = "sha2" version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4239,7 +5309,7 @@ dependencies = [ "serde_urlencoded", "shadowsocks-crypto", "socket2", - "spin", + "spin 0.9.8", "thiserror 1.0.59", "tokio", "tokio-tfo", @@ -4298,7 +5368,7 @@ dependencies = [ "serde", "shadowsocks", "socket2", - "spin", + "spin 0.9.8", "thiserror 1.0.59", "tokio", "windows-sys 0.59.0", @@ -4341,6 +5411,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core 0.6.4", ] @@ -4393,6 +5464,12 @@ dependencies = [ [[package]] name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" @@ -4417,6 +5494,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] +name = "stretch" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0dc6d20ce137f302edf90f9cd3d278866fd7fb139efca6f246161222ad6d87" +dependencies = [ + "lazy_static", + "libm 0.1.4", +] + +[[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4446,12 +5533,23 @@ dependencies = [ [[package]] name = "syn" +version = "0.15.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "unicode-ident", ] @@ -4461,8 +5559,8 @@ version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "unicode-ident", ] @@ -4472,8 +5570,8 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5dc35bb08dd1ca3dfb09dce91fd2d13294d6711c88897d9a9d60acf39bce049" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -4498,8 +5596,8 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -4511,7 +5609,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", "core-foundation", - "system-configuration-sys", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys 0.6.0", ] [[package]] @@ -4525,6 +5634,16 @@ dependencies = [ ] [[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] name = "talpid-core" version = "0.0.0" dependencies = [ @@ -4554,7 +5673,7 @@ dependencies = [ "resolv-conf", "serde", "serde_json", - "system-configuration", + "system-configuration 0.5.1", "talpid-dbus", "talpid-macos", "talpid-net", @@ -4695,7 +5814,7 @@ dependencies = [ "netlink-sys", "nix 0.28.0", "rtnetlink", - "system-configuration", + "system-configuration 0.5.1", "talpid-types", "talpid-windows", "thiserror 2.0.9", @@ -4866,8 +5985,8 @@ version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -4877,8 +5996,8 @@ version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -4950,12 +6069,22 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] [[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] name = "tokio-rustls" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5124,9 +6253,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" dependencies = [ "prettyplease", - "proc-macro2", + "proc-macro2 1.0.92", "prost-build", - "quote", + "quote 1.0.36", "syn 2.0.89", ] @@ -5193,8 +6322,8 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -5260,6 +6389,15 @@ dependencies = [ ] [[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher", +] + +[[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5297,6 +6435,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5353,6 +6509,12 @@ dependencies = [ ] [[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5417,8 +6579,8 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", "wasm-bindgen-shared", ] @@ -5441,7 +6603,7 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ - "quote", + "quote 1.0.36", "wasm-bindgen-macro-support", ] @@ -5451,8 +6613,8 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", "wasm-bindgen-backend", "wasm-bindgen-shared", @@ -5537,6 +6699,12 @@ dependencies = [ ] [[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5618,8 +6786,8 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -5629,8 +6797,8 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -5651,8 +6819,8 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -5662,8 +6830,8 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -6137,6 +7305,17 @@ dependencies = [ ] [[package]] +name = "x448" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd07d4fae29e07089dbcacf7077cd52dce7760125ca9a4dd5a35ca603ffebb" +dependencies = [ + "ed448-goldilocks", + "hex", + "rand_core 0.5.1", +] + +[[package]] name = "yoke" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6154,8 +7333,8 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", "synstructure", ] @@ -6175,8 +7354,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", "synstructure", ] @@ -6196,8 +7375,8 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] @@ -6218,7 +7397,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.92", + "quote 1.0.36", "syn 2.0.89", ] diff --git a/Cargo.toml b/Cargo.toml index 6214b68134..17e3920961 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ members = [ "mullvad-setup", "mullvad-types", "mullvad-types/intersection-derive", + "mullvad-update", "mullvad-version", "talpid-core", "talpid-dbus", @@ -47,6 +48,7 @@ members = [ "tunnel-obfuscation", "wireguard-go-rs", "windows-installer", + "installer-downloader", ] # Default members dictate what is built when running `cargo build` in the root directory. # This is set to a minimal set of packages to speed up the build process and avoid building diff --git a/installer-downloader/Cargo.toml b/installer-downloader/Cargo.toml new file mode 100644 index 0000000000..25799200d3 --- /dev/null +++ b/installer-downloader/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "installer-downloader" +description = "A secure minimal web installer for the Mullvad app" +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[build-dependencies] +anyhow = "1.0" +winres = "0.1" +windows-sys = { version = "0.52.0", features = ["Win32_System", "Win32_System_LibraryLoader", "Win32_System_SystemServices"] } + +[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies] +anyhow = "1.0" +tokio = { version = "1", features = ["full"] } +async-trait = "0.1" + +mullvad-update = { path = "../mullvad-update" } + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { workspace = true, features = ["Win32_UI", "Win32_UI_WindowsAndMessaging", "Win32_Graphics", "Win32_Graphics_Gdi"] } +native-windows-gui = { version = "1.0.12", features = ["image-decoder"] } + +[target.'cfg(target_os = "macos")'.dependencies] +cacao = "0.3.2" +nsvg = "0.5.1" +objc_id = "0.1" + +[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dev-dependencies] +serde = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["test-util"] } +insta = { version = "1.42", features = ["yaml"] } + +[package.metadata.winres] +LegalCopyright = "(c) 2025 Mullvad VPN AB" diff --git a/installer-downloader/assets/logo-icon.png b/installer-downloader/assets/logo-icon.png Binary files differnew file mode 100644 index 0000000000..d80eedadc2 --- /dev/null +++ b/installer-downloader/assets/logo-icon.png diff --git a/installer-downloader/assets/logo-icon.svg b/installer-downloader/assets/logo-icon.svg new file mode 100644 index 0000000000..e488329ac1 --- /dev/null +++ b/installer-downloader/assets/logo-icon.svg @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg version="1.1" id="logo_00000183244726354177486890000003471613280854675596_" + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 252 252" + style="enable-background:new 0 0 252 252;" xml:space="preserve"> +<style type="text/css"> + .fur{fill-rule:evenodd;clip-rule:evenodd;fill:#D2943B;} + .nose{fill-rule:evenodd;clip-rule:evenodd;fill:#FFCD86;} + .helmet{fill-rule:evenodd;clip-rule:evenodd;fill:#FFD524;} + .lamp-light{fill:#FFFFFF;} + .lamp-ring{fill:#192E45;} +</style> +<g id="mole"> + <g id="body"> + <path id="fur" class="fur" d="M23.9,96.6L16.8,111l9.6-13.4c0,0.1-0.6,19.3-0.6,19.3l2.7-14.5c7.2,13.7,23.1,34.8,45.4,50.5 + c1.7,1.2,3.9,3.3,4.6,4.1c0,0,21.3,10.8,54.1-14.2c0.3-0.2,0.7-0.4,1-0.6l0.5,0.3l7.2,4.8c-2.5-0.7-7.3-1.9-7.3-1.9 + c-15.8,18.2-41.5,21.2-55.3,12c-0.6-0.4-4,1-4.6,3c-0.4,1.1,0,2.3,0.5,3.2c2.8,5.2,7,4.7,5,10.8c-1.4,3.3-3.4,6.5-5.6,9.5 + c-4.6,6.2-11.8,11.7-11.1,15c32.6,40.2,106.1,34.6,134.1-1.3c-0.4-5.2-8.6-7.7-14.3-20.4c1.6,0.5,4,1.2,4,1.1 + c0-0.1-6.8-11.1-7.1-12.2l4.4,0.3c0,0-5.8-7.2-6-7.9l5.9-0.8c0,0-7.4-8.5-7.5-9.2l7.5,1.2l-8.2-9.9h3.9l-4.6-6.7l-37.3-14.7 + c-14.3-8.9-27-19.8-36.6-28.2l-19.3-9.4C63.3,79.4,46,79.9,35.4,82l6.8-11.6l-9.9,11.8C31.7,82,31,81.8,31,81.8l0.7-15l-3.3,14 + L23.9,96.6z"/> + <path id="nose" class="nose" d="M28.4,80.8c-4.9-2.3-10.6,1-11.6,5.5c-1.2,4.3,1.8,9.6,7.1,10.3C28.9,93.5,33.2,85.5,28.4,80.8z"/> + </g> + <g id="helmet"> + <path id="helmet" class="helmet" d="M101.2,69.9c-1.5-4.1-1.1-9.4,1-14.4c3-6.9,8.7-11.5,14.1-11.5c1.1,0,2.1,0.2,3.1,0.6 + c3.1-2.8,6.7-5.1,10.7-6.7c22-8.8,54.3,6.9,62.6,28.5c4,10.5,2.8,21.9-0.6,32.4c-2.8,8.6-13,21-9.2,30.3 + c-1.5-0.4-32.8-11-41.8-15.8c-14.1-8.8-26.7-19.6-36.2-27.9l-0.3-0.3l-32-15.2c-0.4-0.2-0.8-0.4-1.1-0.6 + C75.9,69.5,93.4,71.6,101.2,69.9"/> + <g id="lamp"> + + <ellipse id="lamp-light" transform="matrix(0.4007 -0.9162 0.9162 0.4007 12.8553 140.4693)" class="lamp-light" cx="113.8" cy="60.4" rx="13.6" ry="8.1"/> + <path id="lamp-ring" class="lamp-ring" d="M120.1,46.1c-5.3-2.3-12.4,2.2-15.9,10.1s-1.9,16.1,3.4,18.5c5.3,2.3,12.4-2.2,15.9-10.1 + S125.4,48.5,120.1,46.1z M120.6,63.4c-2.8,6.3-8.1,10.1-11.8,8.5c-3.8-1.7-4.6-8.1-1.8-14.5c2.8-6.3,8.1-10.1,11.9-8.5 + C122.6,50.6,123.4,57.1,120.6,63.4z"/> + </g> + </g> +</g> +</svg> diff --git a/installer-downloader/assets/logo-text.png b/installer-downloader/assets/logo-text.png Binary files differnew file mode 100644 index 0000000000..993083c7b1 --- /dev/null +++ b/installer-downloader/assets/logo-text.png diff --git a/installer-downloader/assets/logo-text.svg b/installer-downloader/assets/logo-text.svg new file mode 100644 index 0000000000..c3296186c2 --- /dev/null +++ b/installer-downloader/assets/logo-text.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generator: Adobe Illustrator 23.0.6, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg id="Mullvad_VPN_Logo_Positive" width="959.5" height="103.7" version="1.1" viewBox="0 0 959.5 103.7" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"> +<style type="text/css"> + .st4{fill:#FFFFFF;} +</style> +<g transform="translate(-381.4,-143.1)"> + <path class="st4" d="m381.4 144.7c0-0.9 0.6-1.5 1.5-1.5h17.8c1.2 0 2 0.4 2.4 1.5l25.8 58.2h0.6l25.3-58.2c0.4-1 1.2-1.5 2.4-1.5h17.5c0.9 0 1.5 0.6 1.5 1.5v99c0 0.9-0.6 1.5-1.5 1.5h-17c-0.9 0-1.5-0.6-1.5-1.5v-57.4h-0.6l-18.9 43c-0.6 1.4-1.5 2-2.8 2h-10.3c-1.3 0-2.2-0.6-2.8-2l-18.9-43h-0.6v57.4c0 0.9-0.6 1.5-1.5 1.5h-16.9c-0.9 0-1.5-0.6-1.5-1.5z"/> + <path class="st4" d="m498.4 207.7v-63c0-0.9 0.6-1.5 1.5-1.5h19c0.9 0 1.5 0.6 1.5 1.5v63.6c0 12.1 6.8 19 17.1 19 10.2 0 16.9-6.9 16.9-19v-63.6c0-0.9 0.6-1.5 1.5-1.5h19c0.9 0 1.5 0.6 1.5 1.5v63c0 25.3-16.2 39.1-39 39.1s-39-13.8-39-39.1z"/> + <path class="st4" d="m598.6 144.7c0-0.9 0.6-1.5 1.5-1.5h19c0.9 0 1.5 0.6 1.5 1.5v79.9c0 0.6 0.3 0.9 0.9 0.9h45c0.9 0 1.5 0.6 1.5 1.5v16.6c0 0.9-0.6 1.5-1.5 1.5h-66.4c-0.9 0-1.5-0.6-1.5-1.5z"/> + <path class="st4" d="m684.1 144.7c0-0.9 0.6-1.5 1.5-1.5h19c0.9 0 1.5 0.6 1.5 1.5v79.9c0 0.6 0.3 0.9 0.9 0.9h45c0.9 0 1.5 0.6 1.5 1.5v16.6c0 0.9-0.6 1.5-1.5 1.5h-66.4c-0.9 0-1.5-0.6-1.5-1.5z"/> + <path class="st4" d="m785.1 245.1c-1 0-1.6-0.6-1.9-1.5l-32.4-98.8c-0.3-1.1 0.3-1.6 1.3-1.6h19.5c1 0 1.7 0.4 2 1.5l20.4 66.9h0.4l19.8-66.9c0.3-1 0.9-1.5 1.9-1.5h19.3c0.9 0 1.5 0.6 1.2 1.6l-32.4 98.8c-0.3 0.9-0.9 1.5-1.8 1.5z"/> + <path class="st4" d="m872.5 144.7c0.3-0.9 0.9-1.5 2-1.5h18.9c1 0 1.6 0.6 1.9 1.5l34.5 99c0.3 0.9 0 1.5-1 1.5h-19.5c-1 0-1.7-0.5-2-1.5l-5.8-17.8h-35.7l-5.7 17.8c-0.3 1-0.9 1.5-2 1.5h-19.6c-1 0-1.3-0.6-1-1.5zm23 62.4-11.5-35.7h-0.4l-11.5 35.7z"/> + <path class="st4" d="m944.7 144.7c0-0.9 0.6-1.5 1.5-1.5h37.9c17.8 0 30.3 7.6 35.2 22.9 1.8 5.7 2.7 11.4 2.7 28s-0.9 22.3-2.7 28c-4.9 15.3-17.4 22.9-35.2 22.9h-37.9c-0.9 0-1.5-0.6-1.5-1.5zm22.9 80.8h11.5c10.2 0 16.3-3 18.9-11.2 1-3 1.7-6.9 1.7-20.1s-0.6-17.1-1.7-20.1c-2.5-8.2-8.7-11.2-18.9-11.2h-11.5c-0.6 0-0.9 0.3-0.9 0.9v60.9c0 0.5 0.3 0.8 0.9 0.8z"/> + <path class="st4" d="m1102.5 245.1c-1.1 0-1.7-0.6-2-1.5l-32.4-98.8c-0.3-1.1 0.3-1.6 1.3-1.6h19.5c1 0 1.7 0.4 2 1.5l20.4 66.9h0.4l19.8-66.9c0.3-1 0.9-1.5 1.9-1.5h19.3c0.9 0 1.5 0.6 1.2 1.6l-32.4 98.8c-0.3 0.9-0.9 1.5-1.8 1.5z"/> + <path class="st4" d="m1170 245.1c-0.9 0-1.5-0.6-1.5-1.5v-99c0-0.9 0.6-1.5 1.5-1.5h39.6c22.2 0 35.5 13.3 35.5 32.8 0 19.2-13.5 32.7-35.5 32.7h-18.1c-0.6 0-0.9 0.3-0.9 0.9v34c0 0.9-0.6 1.5-1.5 1.5h-19.1zm53.1-69.1c0-8.2-5.5-13.8-14.8-13.8h-16.8c-0.6 0-0.9 0.3-0.9 0.9v25.6c0 0.6 0.3 0.9 0.9 0.9h16.8c9.2 0.1 14.8-5.3 14.8-13.6z"/> + <path class="st4" d="m1262.2 144.7c0-0.9 0.6-1.5 1.5-1.5h18c1 0 2 0.4 2.5 1.5l36 64.2h0.8v-64.2c0-0.9 0.6-1.5 1.5-1.5h16.9c0.9 0 1.5 0.6 1.5 1.5v99c0 0.9-0.6 1.5-1.5 1.5h-17.8c-1.2 0-2-0.5-2.5-1.5l-36.1-64h-0.8v64c0 0.9-0.6 1.5-1.5 1.5h-16.9c-0.9 0-1.5-0.6-1.5-1.5v-99z"/> +</g> +</svg> diff --git a/installer-downloader/build.rs b/installer-downloader/build.rs new file mode 100644 index 0000000000..475a864999 --- /dev/null +++ b/installer-downloader/build.rs @@ -0,0 +1,30 @@ +use anyhow::Context; +use std::env; + +fn main() -> anyhow::Result<()> { + let target_os = env::var("CARGO_CFG_TARGET_OS").context("Missing 'CARGO_CFG_TARGET_OS")?; + + match target_os.as_str() { + "windows" => win_main(), + _ => Ok(()), + } +} + +fn win_main() -> anyhow::Result<()> { + let mut res = winres::WindowsResource::new(); + + res.set_language(make_lang_id( + windows_sys::Win32::System::SystemServices::LANG_ENGLISH as u16, + windows_sys::Win32::System::SystemServices::SUBLANG_ENGLISH_US as u16, + )); + + println!("cargo:rerun-if-changed=loader.manifest"); + res.set_manifest_file("loader.manifest"); + + res.compile().context("Failed to compile resources") +} + +// Sourced from winnt.h: https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-makelangid +fn make_lang_id(p: u16, s: u16) -> u16 { + (s << 10) | p +} diff --git a/installer-downloader/convert-assets.py b/installer-downloader/convert-assets.py new file mode 100644 index 0000000000..ede39abe21 --- /dev/null +++ b/installer-downloader/convert-assets.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +# Convert svg assets to png assets + +from cairosvg import svg2png + +svg2png(url="assets/logo-icon.svg", write_to="assets/logo-icon.png", output_width=32) +svg2png(url="assets/logo-text.svg", write_to="assets/logo-text.png", output_width=122) diff --git a/installer-downloader/loader.manifest b/installer-downloader/loader.manifest new file mode 100644 index 0000000000..feedbf9bc0 --- /dev/null +++ b/installer-downloader/loader.manifest @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> +<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> + <security> + <requestedPrivileges> + <requestedExecutionLevel + level="requireAdministrator" + uiAccess="false"/> + </requestedPrivileges> + </security> +</trustInfo> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="Mullvad.MullvadVPN.Installer" + type="win32" +/> +<description>Web installer</description> +<dependency> + <dependentAssembly> + <assemblyIdentity + type="win32" + name="Microsoft.Windows.Common-Controls" + version="6.0.0.0" + processorArchitecture="*" + publicKeyToken="6595b64144ccf1df" + language="*" + /> + </dependentAssembly> +</dependency> +<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!--The ID below indicates app support for Windows 10 and Windows 11 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + </application> +</compatibility> +</assembly> diff --git a/installer-downloader/src/cacao_impl/delegate.rs b/installer-downloader/src/cacao_impl/delegate.rs new file mode 100644 index 0000000000..80d080932b --- /dev/null +++ b/installer-downloader/src/cacao_impl/delegate.rs @@ -0,0 +1,119 @@ +use std::sync::{Arc, Mutex}; + +use cacao::{control::Control, layout::Layout}; + +use super::ui::{Action, AppWindow}; +use crate::delegate::{AppDelegate, AppDelegateQueue}; + +impl AppDelegate for AppWindow { + type Queue = Queue; + + fn on_download<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + let cb = Self::sync_callback(callback); + self.download_button.button.set_action(move || { + let cb = Action::DownloadClick(cb.clone()); + cacao::appkit::App::<super::ui::AppImpl, _>::dispatch_main(cb); + }); + } + + fn on_cancel<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + let cb = Self::sync_callback(callback); + self.cancel_button.button.set_action(move || { + let cb = Action::CancelClick(cb.clone()); + cacao::appkit::App::<super::ui::AppImpl, _>::dispatch_main(cb); + }); + } + + fn set_status_text(&mut self, text: &str) { + self.status_text.set_text(text); + } + + fn set_download_text(&mut self, text: &str) { + self.download_text.set_text(text); + } + + fn show_download_progress(&mut self) { + self.progress.set_hidden(false); + } + + fn hide_download_progress(&mut self) { + self.progress.set_hidden(true); + } + + fn set_download_progress(&mut self, complete: u32) { + self.progress.set_value(complete as f64); + } + + fn show_download_button(&mut self) { + self.download_button.button.set_hidden(false); + } + + fn hide_download_button(&mut self) { + self.download_button.button.set_hidden(true); + } + + fn enable_download_button(&mut self) { + self.download_button.button.set_enabled(true); + } + + fn disable_download_button(&mut self) { + self.download_button.button.set_enabled(false); + } + + fn show_cancel_button(&mut self) { + self.cancel_button.button.set_hidden(false); + } + + fn hide_cancel_button(&mut self) { + self.cancel_button.button.set_hidden(true); + } + + fn enable_cancel_button(&mut self) { + self.cancel_button.button.set_enabled(true); + } + + fn disable_cancel_button(&mut self) { + self.cancel_button.button.set_enabled(false); + } + + fn show_beta_text(&mut self) { + self.beta_link.set_hidden(false); + self.beta_link_preface.set_hidden(false); + } + + fn hide_beta_text(&mut self) { + self.beta_link.set_hidden(true); + self.beta_link_preface.set_hidden(true); + } + + fn queue(&self) -> Self::Queue { + Queue {} + } +} + +impl AppWindow { + // NOTE: We need this horrible lock because Dispatcher demands Sync, but AppDelegate does not require Sync + fn sync_callback( + callback: impl Fn() + Send + 'static, + ) -> Arc<Mutex<Box<dyn Fn() + Send + 'static>>> { + Arc::new(Mutex::new(Box::new(move || callback()))) + } +} + +/// This simply mutates the UI on the main thread using the GCD +pub struct Queue {} + +impl AppDelegateQueue<AppWindow> for Queue { + fn queue_main<F: FnOnce(&mut AppWindow) + 'static + Send>(&self, callback: F) { + // NOTE: We need this horrible lock because Dispatcher demands Sync + let cb: Mutex<Option<Box<dyn FnOnce(&mut AppWindow) + Send>>> = + Mutex::new(Some(Box::new(callback))); + cacao::appkit::App::<super::ui::AppImpl, _>::dispatch_main(Action::QueueMain(cb)); + } +} diff --git a/installer-downloader/src/cacao_impl/mod.rs b/installer-downloader/src/cacao_impl/mod.rs new file mode 100644 index 0000000000..89cd3b1402 --- /dev/null +++ b/installer-downloader/src/cacao_impl/mod.rs @@ -0,0 +1,19 @@ +use std::sync::Mutex; + +use cacao::appkit::App; +use ui::{Action, AppImpl, AppWindow}; + +mod delegate; +mod ui; + +pub fn main() { + let app = App::new("net.mullvad.downloader", AppImpl::default()); + + let cb: Mutex<Option<Box<dyn FnOnce(&mut AppWindow) + Send + 'static>>> = + Mutex::new(Some(Box::new(|self_| { + crate::controller::initialize_controller(self_); + }))); + cacao::appkit::App::<ui::AppImpl, _>::dispatch_main(Action::QueueMain(cb)); + + app.run(); +} diff --git a/installer-downloader/src/cacao_impl/ui.rs b/installer-downloader/src/cacao_impl/ui.rs new file mode 100644 index 0000000000..85f7554c05 --- /dev/null +++ b/installer-downloader/src/cacao_impl/ui.rs @@ -0,0 +1,335 @@ +use std::cell::RefCell; +use std::sync::{Arc, LazyLock, Mutex, RwLock}; + +use cacao::appkit::window::{Window, WindowConfig, WindowDelegate}; +use cacao::appkit::{App, AppDelegate}; +use cacao::button::Button; +use cacao::color::Color; +use cacao::image::{Image, ImageView}; +use cacao::layout::{Layout, LayoutConstraint}; +use cacao::notification_center::Dispatcher; +use cacao::objc::{class, msg_send, sel, sel_impl}; +use cacao::progress::ProgressIndicator; +use cacao::text::{AttributedString, Label}; +use cacao::view::View; +use objc_id::Id; + +use crate::resource::{ + BANNER_DESC, BETA_LINK_TEXT, BETA_PREFACE_DESC, CANCEL_BUTTON_TEXT, DOWNLOAD_BUTTON_TEXT, + WINDOW_HEIGHT, WINDOW_TITLE, WINDOW_WIDTH, +}; + +/// Logo render in the banner +const LOGO_IMAGE_DATA: &[u8] = include_bytes!("../../assets/logo-icon.svg"); + +/// Logo banner text +const LOGO_TEXT_DATA: &[u8] = include_bytes!("../../assets/logo-text.svg"); + +/// Banner background color: #192e45 +static BANNER_COLOR: LazyLock<Color> = LazyLock::new(|| { + let r = 0x19 as f64 / 255.; + let g = 0x2e as f64 / 255.; + let b = 0x45 as f64 / 255.; + let a = 1.; + + // NOTE: colorWithCalibratedRed is used by cacao by default, but it renders a different color + // than it does for background color of the image. I believe this is because the + // calibrated uses the current color profile. + // Maybe using calibrated colors is more correct? Rendering different colors *definitely* + // is not. + let id = + unsafe { Id::from_retained_ptr(msg_send![class!(NSColor), colorWithRed:r green:g blue:b alpha:a]) }; + Color::Custom(Arc::new(RwLock::new(id))) +}); + +static LOGO: LazyLock<Image> = LazyLock::new(|| Image::with_data(LOGO_IMAGE_DATA)); +static LOGO_TEXT: LazyLock<Image> = LazyLock::new(|| Image::with_data(LOGO_TEXT_DATA)); + +pub struct AppImpl { + window: Window<AppWindowWrapper>, +} + +impl Default for AppImpl { + fn default() -> Self { + Self { + window: Window::with(WindowConfig::default(), AppWindowWrapper::default()), + } + } +} + +impl AppDelegate for AppImpl { + fn did_finish_launching(&self) { + App::activate(); + + self.window.show(); + + let delegate = self.window.delegate.as_ref().unwrap(); + delegate.inner.borrow().layout(); + } + + fn should_terminate_after_last_window_closed(&self) -> bool { + true + } +} + +/// Dispatcher actions +pub enum Action { + /// User clicked the download button + DownloadClick(Arc<Mutex<Box<dyn Fn() + Send>>>), + /// User clicked the cancel button + CancelClick(Arc<Mutex<Box<dyn Fn() + Send>>>), + /// Run callback on main thread + QueueMain(Mutex<Option<Box<dyn for<'a> FnOnce(&'a mut AppWindow) + Send>>>), +} + +impl Dispatcher for AppImpl { + type Message = Action; + + fn on_ui_message(&self, message: Self::Message) { + let delegate = self.window.delegate.as_ref().unwrap(); + match message { + Action::DownloadClick(cb) => { + let cb = cb.lock().unwrap(); + cb(); + } + Action::CancelClick(cb) => { + let cb = cb.lock().unwrap(); + cb(); + } + Action::QueueMain(cb) => { + // NOTE: We assume that this won't panic because they will never run simultaneously + let mut borrowed = delegate.inner.borrow_mut(); + let cb = cb.lock().unwrap().take().unwrap(); + cb(&mut borrowed); + } + } + } + + fn on_background_message(&self, _message: Self::Message) { + // TODO + } +} + +#[derive(Default)] +pub struct AppWindowWrapper { + pub inner: RefCell<AppWindow>, +} + +#[derive(Default)] +pub struct AppWindow { + pub content: View, + + pub banner: View, + pub banner_logo_view: ImageView, + pub banner_logo_text_view: ImageView, + pub banner_desc: Label, + + pub main_view: View, + + pub download_button: DownloadButton, + pub cancel_button: CancelButton, + + pub progress: ProgressIndicator, + + pub status_text: Label, + pub download_text: Label, + + pub beta_link_preface: Label, + pub beta_link: Label, +} + +pub struct DownloadButton { + pub button: Button, +} + +impl Default for DownloadButton { + fn default() -> Self { + let button = Button::new(DOWNLOAD_BUTTON_TEXT); + Self { button } + } +} + +pub struct CancelButton { + pub button: Button, +} + +impl Default for CancelButton { + fn default() -> Self { + let button = Button::new(CANCEL_BUTTON_TEXT); + Self { button } + } +} + +impl AppWindow { + pub fn layout(&self) { + self.banner_logo_view.set_image(&LOGO); + self.banner_logo_text_view.set_image(&LOGO_TEXT); + self.banner.set_background_color(&*BANNER_COLOR); + + self.banner.add_subview(&self.banner_logo_view); + self.banner.add_subview(&self.banner_logo_text_view); + + self.content.add_subview(&self.banner); + self.content.add_subview(&self.main_view); + + self.main_view.add_subview(&self.progress); + self.progress.set_hidden(true); + self.progress.set_indeterminate(false); + + self.banner_desc.set_text(BANNER_DESC); + self.banner_desc.set_text_color(Color::SystemWhite); + self.banner.add_subview(&self.banner_desc); + self.banner_desc + .set_line_break_mode(cacao::text::LineBreakMode::WrapWords); + + LayoutConstraint::activate(&[ + self.banner_logo_view + .bottom + .constraint_equal_to(&self.banner_desc.top) + .offset(-8.), + self.banner_logo_view + .left + .constraint_equal_to(&self.banner.left) + .offset(24.), + self.banner_logo_view + .width + .constraint_equal_to_constant(32.0f64), + self.banner_logo_view + .height + .constraint_equal_to_constant(32.0f64), + self.banner_desc + .left + .constraint_equal_to(&self.banner_logo_view.left), + self.banner_desc + .bottom + .constraint_equal_to(&self.banner.bottom) + .offset(-16.), + self.banner_desc + .right + .constraint_equal_to(&self.banner.right) + .offset(-24.), + ]); + LayoutConstraint::activate(&[ + self.banner_logo_text_view + .top + .constraint_equal_to(&self.banner_logo_view.top) + .offset(9.4), + self.banner_logo_text_view + .left + .constraint_equal_to(&self.banner_logo_view.right) + .offset(12.), + self.banner_logo_text_view + .width + .constraint_equal_to_constant(122.), + self.banner_logo_text_view + .height + .constraint_equal_to_constant(13.), + ]); + + LayoutConstraint::activate(&[ + self.banner.left.constraint_equal_to(&self.content.left), + self.banner.right.constraint_equal_to(&self.content.right), + self.banner.top.constraint_equal_to(&self.content.top), + self.banner.height.constraint_equal_to_constant(122.), + ]); + + LayoutConstraint::activate(&[ + self.main_view.left.constraint_equal_to(&self.content.left), + self.main_view + .right + .constraint_equal_to(&self.content.right), + self.main_view.top.constraint_equal_to(&self.banner.bottom), + self.main_view + .bottom + .constraint_equal_to(&self.content.bottom), + ]); + + self.main_view.add_subview(&self.status_text); + self.main_view.add_subview(&self.download_text); + self.main_view.add_subview(&self.download_button.button); + self.main_view.add_subview(&self.cancel_button.button); + + self.beta_link_preface.set_text(BETA_PREFACE_DESC); + self.main_view.add_subview(&self.beta_link_preface); + + let mut attr_text = AttributedString::new(&BETA_LINK_TEXT); + attr_text.set_text_color(Color::Link, 0..BETA_LINK_TEXT.len() as isize); + + self.beta_link.set_attributed_text(attr_text); + self.main_view.add_subview(&self.beta_link); + + LayoutConstraint::activate(&[ + self.status_text + .top + .constraint_greater_than_or_equal_to(&self.main_view.top) + .offset(24.), + self.status_text + .center_x + .constraint_equal_to(&self.main_view.center_x), + self.download_text + .top + .constraint_equal_to(&self.status_text.bottom) + .offset(16.), + self.download_text + .center_x + .constraint_equal_to(&self.main_view.center_x), + self.download_button + .button + .center_x + .constraint_equal_to(&self.main_view.center_x), + self.download_button + .button + .top + .constraint_equal_to(&self.status_text.bottom) + .offset(16.), + self.progress + .top + .constraint_equal_to(&self.download_button.button.top) + .offset(32.), + self.progress + .left + .constraint_equal_to(&self.main_view.left) + .offset(30.), + self.progress + .right + .constraint_equal_to(&self.main_view.right) + .offset(-30.), + self.progress.height.constraint_equal_to_constant(16.0f64), + self.cancel_button + .button + .center_x + .constraint_equal_to(&self.main_view.center_x), + self.cancel_button + .button + .top + .constraint_equal_to(&self.progress.bottom) + .offset(16.), + self.beta_link_preface + .bottom + .constraint_equal_to(&self.main_view.bottom) + .offset(-24.), + self.beta_link_preface + .left + .constraint_equal_to(&self.main_view.left) + .offset(24.), + self.beta_link + .bottom + .constraint_equal_to(&self.beta_link_preface.bottom), + self.beta_link + .left + .constraint_equal_to(&self.beta_link_preface.right), + ]); + } +} + +impl WindowDelegate for AppWindowWrapper { + const NAME: &'static str = "MullvadInstallerDelegate"; + + fn did_load(&mut self, window: Window) { + window.set_title(WINDOW_TITLE); + window.set_minimum_content_size(WINDOW_WIDTH as f64, WINDOW_HEIGHT as f64); + window.set_maximum_content_size(WINDOW_WIDTH as f64, WINDOW_HEIGHT as f64); + window.set_content_size(WINDOW_WIDTH as f64, WINDOW_HEIGHT as f64); + window.set_content_view(&self.inner.borrow().content); + } +} diff --git a/installer-downloader/src/controller.rs b/installer-downloader/src/controller.rs new file mode 100644 index 0000000000..10c3519bd3 --- /dev/null +++ b/installer-downloader/src/controller.rs @@ -0,0 +1,210 @@ +//! This module implements the actual logic performed by different UI components. + +use crate::delegate::{AppDelegate, AppDelegateQueue}; +use crate::resource; +use crate::ui_downloader::{UiAppDownloader, UiAppDownloaderParameters, UiProgressUpdater}; + +use mullvad_update::api::Version; +use mullvad_update::{ + api::{self, VersionInfoProvider}, + app::{self, AppDownloaderFactory}, +}; + +use std::future::Future; +use tokio::sync::{mpsc, oneshot}; + +/// Actions handled by an async worker task in [handle_action_messages]. +enum TaskMessage { + SetVersionInfo(api::VersionInfo), + BeginDownload, + Cancel, +} + +/// See the [module-level docs](self). +pub struct AppController {} + +/// Public entry function for registering a [AppDelegate]. +pub fn initialize_controller<T: AppDelegate + 'static>(delegate: &mut T) { + use mullvad_update::api::LatestVersionInfoProvider; + use mullvad_update::app::HttpAppDownloader; + + // App downloader (factory) to use + type DownloaderFactory<T> = HttpAppDownloader<UiProgressUpdater<T>, UiProgressUpdater<T>>; + // Version info provider to use + type VersionInfoProvider = LatestVersionInfoProvider; + + AppController::initialize::<_, DownloaderFactory<T>, VersionInfoProvider>(delegate) +} + +impl AppController { + /// Initialize [AppController] using the provided delegate. + /// + /// Providing the downloader and version info fetcher as type arguments, they're decoupled from + /// the logic of [AppController], allowing them to be mocked. + pub fn initialize<Delegate, DownloaderFactory, VersionProvider>(delegate: &mut Delegate) + where + Delegate: AppDelegate + 'static, + VersionProvider: VersionInfoProvider + 'static, + DownloaderFactory: + AppDownloaderFactory<Parameters = UiAppDownloaderParameters<Delegate>> + 'static, + { + delegate.hide_download_progress(); + delegate.show_download_button(); + delegate.disable_download_button(); + delegate.hide_cancel_button(); + delegate.hide_beta_text(); + + let (task_tx, task_rx) = mpsc::channel(1); + tokio::spawn(handle_action_messages::<Delegate, DownloaderFactory>( + delegate.queue(), + task_rx, + )); + delegate.set_status_text(resource::FETCH_VERSION_DESC); + tokio::spawn(fetch_app_version_info::<Delegate, VersionProvider>( + delegate, + task_tx.clone(), + )); + Self::register_user_action_callbacks(delegate, task_tx); + } + + fn register_user_action_callbacks<T: AppDelegate + 'static>( + delegate: &mut T, + task_tx: mpsc::Sender<TaskMessage>, + ) { + let tx = task_tx.clone(); + delegate.on_download(move || { + let _ = tx.try_send(TaskMessage::BeginDownload); + }); + let tx = task_tx.clone(); + delegate.on_cancel(move || { + let _ = tx.try_send(TaskMessage::Cancel); + }); + } +} + +/// Background task that fetches app version data. +fn fetch_app_version_info<Delegate, VersionProvider>( + delegate: &mut Delegate, + download_tx: mpsc::Sender<TaskMessage>, +) -> impl Future<Output = ()> +where + Delegate: AppDelegate, + VersionProvider: VersionInfoProvider, +{ + let queue = delegate.queue(); + + async move { + // TODO: handle errors, retry + let Ok(version_info) = VersionProvider::get_version_info().await else { + queue.queue_main(move |self_| { + self_.set_status_text("Failed to fetch version info"); + }); + return; + }; + let _ = download_tx.try_send(TaskMessage::SetVersionInfo(version_info)); + } +} + +/// Async worker that handles actions such as initiating a download, cancelling it, and updating +/// labels. +async fn handle_action_messages<Delegate, DownloaderFactory>( + queue: Delegate::Queue, + mut rx: mpsc::Receiver<TaskMessage>, +) where + Delegate: AppDelegate + 'static, + DownloaderFactory: + AppDownloaderFactory<Parameters = UiAppDownloaderParameters<Delegate>> + 'static, +{ + let mut version_info = None; + let mut active_download = None; + + while let Some(msg) = rx.recv().await { + match msg { + TaskMessage::SetVersionInfo(new_version_info) => { + let version_label = format_latest_version(&new_version_info.stable); + let has_beta = new_version_info.beta.is_some(); + queue.queue_main(move |self_| { + self_.set_status_text(&version_label); + self_.enable_download_button(); + if has_beta { + self_.show_beta_text(); + } + }); + version_info = Some(new_version_info); + } + TaskMessage::BeginDownload => { + if active_download.is_some() { + continue; + } + let Some(version_info) = version_info.clone() else { + continue; + }; + + let (tx, rx) = oneshot::channel(); + queue.queue_main(move |self_| { + // TODO: Select appropriate URLs + let Some(app_url) = version_info.stable.urls.first() else { + return; + }; + let Some(signature_url) = version_info.stable.signature_urls.first() else { + return; + }; + let app_size = version_info.stable.size; + + self_.set_download_text(""); + self_.hide_download_button(); + self_.hide_beta_text(); + self_.show_cancel_button(); + self_.enable_cancel_button(); + self_.show_download_progress(); + + let downloader = DownloaderFactory::new_downloader(UiAppDownloaderParameters { + signature_url: signature_url.to_owned(), + app_url: app_url.to_owned(), + app_size, + sig_progress: UiProgressUpdater::new(self_.queue()), + app_progress: UiProgressUpdater::new(self_.queue()), + }); + + let ui_downloader = UiAppDownloader::new(self_, downloader); + let _ = tx.send(tokio::spawn(async move { + let _ = app::install_and_upgrade(ui_downloader).await; + })); + }); + active_download = rx.await.ok(); + } + TaskMessage::Cancel => { + let Some(active_download) = active_download.take() else { + continue; + }; + active_download.abort(); + let _ = active_download.await; + + let (version_label, has_beta) = if let Some(version_info) = &version_info { + ( + format_latest_version(&version_info.stable), + version_info.beta.is_some(), + ) + } else { + ("".to_owned(), false) + }; + + queue.queue_main(move |self_| { + self_.set_status_text(&version_label); + self_.set_download_text(""); + self_.show_download_button(); + if has_beta { + self_.show_beta_text(); + } + self_.hide_cancel_button(); + self_.hide_download_progress(); + self_.set_download_progress(0); + }); + } + } + } +} + +fn format_latest_version(version: &Version) -> String { + format!("{}: {}", resource::LATEST_VERSION_PREFIX, version.version) +} diff --git a/installer-downloader/src/delegate.rs b/installer-downloader/src/delegate.rs new file mode 100644 index 0000000000..beca2e6282 --- /dev/null +++ b/installer-downloader/src/delegate.rs @@ -0,0 +1,72 @@ +//! Framework-agnostic module that hooks up a UI to actions + +pub use crate::ui_downloader::UiProgressUpdater; + +/// Trait implementing high-level UI actions +pub trait AppDelegate { + /// Queue lets us perform actions from other threads + type Queue: AppDelegateQueue<Self>; + + /// Register click handler for the download button + fn on_download<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static; + + /// Register click handler for the cancel button + fn on_cancel<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static; + + /// Set status text + fn set_status_text(&mut self, text: &str); + + /// Set download text + fn set_download_text(&mut self, text: &str); + + /// Show download progress bar + fn show_download_progress(&mut self); + + /// Hide download progress bar + fn hide_download_progress(&mut self); + + /// Update download progress bar + fn set_download_progress(&mut self, complete: u32); + + /// Enable download button + fn enable_download_button(&mut self); + + /// Disable download button + fn disable_download_button(&mut self); + + /// Show download button + fn show_download_button(&mut self); + + /// Hide download button + fn hide_download_button(&mut self); + + /// Show cancel button + fn show_cancel_button(&mut self); + + /// Hide cancel button + fn hide_cancel_button(&mut self); + + /// Enable cancel button + fn enable_cancel_button(&mut self); + + /// Disable cancel button + fn disable_cancel_button(&mut self); + + /// Show beta text + fn show_beta_text(&mut self); + + /// Hide beta text + fn hide_beta_text(&mut self); + + /// Create queue for scheduling actions on UI thread + fn queue(&self) -> Self::Queue; +} + +/// Schedules actions on the UI thread from other threads +pub trait AppDelegateQueue<T: ?Sized>: Send { + fn queue_main<F: FnOnce(&mut T) + 'static + Send>(&self, callback: F); +} diff --git a/installer-downloader/src/lib.rs b/installer-downloader/src/lib.rs new file mode 100644 index 0000000000..724b546998 --- /dev/null +++ b/installer-downloader/src/lib.rs @@ -0,0 +1,4 @@ +pub mod controller; +pub mod delegate; +pub mod resource; +pub mod ui_downloader; diff --git a/installer-downloader/src/main.rs b/installer-downloader/src/main.rs new file mode 100644 index 0000000000..51f9e303e5 --- /dev/null +++ b/installer-downloader/src/main.rs @@ -0,0 +1,39 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +#[cfg(target_os = "windows")] +mod winapi_impl; + +#[cfg(target_os = "macos")] +mod cacao_impl; + +#[cfg(any(target_os = "windows", target_os = "macos"))] +mod inner { + pub use installer_downloader::controller; + pub use installer_downloader::delegate; + pub use installer_downloader::resource; + + pub fn run() { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to create tokio runtime"); + let _guard = rt.enter(); + + #[cfg(target_os = "windows")] + super::winapi_impl::main(); + + #[cfg(target_os = "macos")] + super::cacao_impl::main(); + } +} + +#[cfg(not(any(target_os = "windows", target_os = "macos")))] +mod inner { + pub fn run() {} +} + +use inner::*; + +fn main() { + run() +} diff --git a/installer-downloader/src/resource.rs b/installer-downloader/src/resource.rs new file mode 100644 index 0000000000..b5f5a0bf85 --- /dev/null +++ b/installer-downloader/src/resource.rs @@ -0,0 +1,47 @@ +//! Shared text and other resources + +/// Window title +pub const WINDOW_TITLE: &str = "Mullvad VPN downloader"; +/// Window width +pub const WINDOW_WIDTH: usize = 600; +/// Window height +pub const WINDOW_HEIGHT: usize = 334; + +/// Text description in the top banner +pub const BANNER_DESC: &str = + "The Mullvad VPN app installer will be downloaded and verified for authenticity."; + +/// Beta preface text +pub const BETA_PREFACE_DESC: &str = "Want to try the new Beta version? "; +/// Beta link text +pub const BETA_LINK_TEXT: &str = "Click here!"; + +/// Dimensions of cancel button (including padding) +pub const CANCEL_BUTTON_SIZE: (usize, usize) = (150, 40); + +/// Download button text +pub const DOWNLOAD_BUTTON_TEXT: &str = "Download & install"; + +/// Dimensions of download button (including padding) +pub const DOWNLOAD_BUTTON_SIZE: (usize, usize) = (150, 40); + +/// Cancel button text +pub const CANCEL_BUTTON_TEXT: &str = "Cancel"; + +/// Displayed while fetching version info from the API +pub const FETCH_VERSION_DESC: &str = "Loading version details..."; + +/// The first part of "Version: 2025.1" +pub const LATEST_VERSION_PREFIX: &str = "Version"; + +/// Displayed while fetching version info from the API failed +pub const FETCH_VERSION_ERROR_DESC: &str = "Couldn't load version details, please try again or make sure you have the latest installer downloader."; + +/// The first part of "Downloading from <some url>... (x%)", displayed during download +pub const DOWNLOADING_DESC_PREFIX: &str = "Downloading from"; + +/// Displayed after completed download +pub const DOWNLOAD_COMPLETE_DESC: &str = "Download complete. Verifying..."; + +/// Displayed after verification +pub const VERIFICATION_SUCCEEDED_DESC: &str = "Verification successful. Starting install..."; diff --git a/installer-downloader/src/ui_downloader.rs b/installer-downloader/src/ui_downloader.rs new file mode 100644 index 0000000000..41b6fa3efb --- /dev/null +++ b/installer-downloader/src/ui_downloader.rs @@ -0,0 +1,142 @@ +//! This module hooks up [AppDelegate]s to arbitrary implementations of [AppDownloader] and +//! [fetch::ProgressUpdater]. + +use crate::{ + delegate::{AppDelegate, AppDelegateQueue}, + resource, +}; +use mullvad_update::{ + app::{self, AppDownloader, AppDownloaderParameters}, + fetch, +}; + +/// [AppDownloader] that delegates the actual work to some underlying `downloader` and uses it to +/// update a UI. +pub struct UiAppDownloader<Delegate: AppDelegate, Downloader> { + downloader: Downloader, + /// Queue used to control the app UI + queue: Delegate::Queue, +} + +/// Parameters for [UiAppDownloader] +pub type UiAppDownloaderParameters<Delegate> = + AppDownloaderParameters<UiProgressUpdater<Delegate>, UiProgressUpdater<Delegate>>; + +impl<Delegate: AppDelegate, Downloader: AppDownloader + Send + 'static> + UiAppDownloader<Delegate, Downloader> +{ + /// Construct a [UiAppDownloader]. + pub fn new(delegate: &Delegate, downloader: Downloader) -> Self { + Self { + downloader, + queue: delegate.queue(), + } + } +} + +#[async_trait::async_trait] +impl<Delegate: AppDelegate, Downloader: AppDownloader + Send + 'static> AppDownloader + for UiAppDownloader<Delegate, Downloader> +{ + async fn download_signature(&mut self) -> Result<(), app::DownloadError> { + if let Err(error) = self.downloader.download_signature().await { + self.queue.queue_main(move |self_| { + self_.set_download_text("ERROR: Failed to retrieve signature."); + self_.enable_download_button(); + self_.hide_cancel_button(); + }); + Err(error) + } else { + Ok(()) + } + } + + async fn download_executable(&mut self) -> Result<(), app::DownloadError> { + match self.downloader.download_executable().await { + Ok(()) => { + self.queue.queue_main(move |self_| { + self_.set_download_text(resource::DOWNLOAD_COMPLETE_DESC); + self_.disable_cancel_button(); + }); + + Ok(()) + } + Err(err) => { + self.queue.queue_main(move |self_| { + self_.set_download_text("ERROR: Download failed. Please try again."); + self_.enable_download_button(); + self_.hide_cancel_button(); + }); + + Err(err) + } + } + } + + async fn verify(&mut self) -> Result<(), app::DownloadError> { + match self.downloader.verify().await { + Ok(()) => { + self.queue.queue_main(move |self_| { + self_.set_download_text(resource::VERIFICATION_SUCCEEDED_DESC); + }); + + Ok(()) + } + Err(error) => { + self.queue.queue_main(move |self_| { + self_.set_download_text("ERROR: Verification failed!"); + }); + + Err(error) + } + } + } +} + +/// Implementation of [fetch::ProgressUpdater] that updates some [AppDelegate]. +pub struct UiProgressUpdater<Delegate: AppDelegate> { + domain: String, + prev_progress: Option<u32>, + queue: Delegate::Queue, +} + +impl<Delegate: AppDelegate> UiProgressUpdater<Delegate> { + pub fn new(queue: Delegate::Queue) -> Self { + Self { + domain: "unknown source".to_owned(), + prev_progress: None, + queue, + } + } +} + +impl<Delegate: AppDelegate + 'static> fetch::ProgressUpdater for UiProgressUpdater<Delegate> { + fn set_progress(&mut self, fraction_complete: f32) { + let value = (100.0 * fraction_complete).min(100.0) as u32; + + if self.prev_progress == Some(value) { + // Unconditionally updating causes flickering + return; + } + + let status = format!( + "{} {}... ({value}%)", + resource::DOWNLOADING_DESC_PREFIX, + self.domain + ); + + self.queue.queue_main(move |self_| { + self_.set_download_progress(value); + self_.set_download_text(&status); + }); + + self.prev_progress = Some(value); + } + + fn set_url(&mut self, url: &str) { + // Parse out domain name + let url = url.strip_prefix("https://").unwrap_or(url); + let (domain, _) = url.split_once('/').unwrap_or((url, "")); + self.domain = domain.to_owned(); + } +} diff --git a/installer-downloader/src/winapi_impl/delegate.rs b/installer-downloader/src/winapi_impl/delegate.rs new file mode 100644 index 0000000000..4414559e7c --- /dev/null +++ b/installer-downloader/src/winapi_impl/delegate.rs @@ -0,0 +1,147 @@ +//! This module implements [AppDelegate] and [Queue], which allows the NWG UI to be hooked up to our +//! generic controller. + +use native_windows_gui::{self as nwg, Event}; +use windows_sys::Win32::UI::WindowsAndMessaging::PostMessageW; + +use super::ui::{AppWindow, QUEUE_MESSAGE}; +use crate::delegate::{AppDelegate, AppDelegateQueue}; + +impl AppDelegate for AppWindow { + type Queue = Queue; + + fn on_download<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + register_click_handler(self.window.handle, self.download_button.handle, callback); + } + + fn on_cancel<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + register_click_handler(self.window.handle, self.cancel_button.handle, callback); + } + + fn set_status_text(&mut self, text: &str) { + self.status_text.set_text(text); + } + + fn set_download_text(&mut self, text: &str) { + if !text.is_empty() { + self.download_text.set_visible(true); + self.download_text.set_text(text); + } else { + self.download_text.set_visible(false); + } + } + + fn show_download_progress(&mut self) { + self.progress_bar.set_visible(true); + } + + fn hide_download_progress(&mut self) { + self.progress_bar.set_visible(false); + } + + fn set_download_progress(&mut self, complete: u32) { + self.progress_bar.set_pos(complete); + } + + fn show_download_button(&mut self) { + self.download_button.set_visible(true); + } + + fn hide_download_button(&mut self) { + self.download_button.set_visible(false); + } + + fn enable_download_button(&mut self) { + self.download_button.set_enabled(true); + } + + fn disable_download_button(&mut self) { + self.download_button.set_enabled(false); + } + + fn show_cancel_button(&mut self) { + self.cancel_button.set_visible(true); + } + + fn hide_cancel_button(&mut self) { + self.cancel_button.set_visible(false); + } + + fn enable_cancel_button(&mut self) { + self.cancel_button.set_enabled(true); + } + + fn disable_cancel_button(&mut self) { + self.cancel_button.set_enabled(false); + } + + fn show_beta_text(&mut self) { + self.beta_prefix.set_visible(true); + self.beta_link.set_visible(true); + } + + fn hide_beta_text(&mut self) { + self.beta_prefix.set_visible(false); + self.beta_link.set_visible(false); + } + + fn queue(&self) -> Self::Queue { + Queue { + main_wnd: self.window.handle, + } + } +} + +/// Register a window message for clicking this button that triggers `callback`. +fn register_click_handler( + parent: nwg::ControlHandle, + button: nwg::ControlHandle, + callback: impl Fn() + 'static, +) { + nwg::bind_event_handler(&button, &parent, move |evt, _, handle| { + if evt == Event::OnButtonClick && handle == button { + callback(); + } + }); +} + +/// Queue sends a window message to the main window containing a [QueueContext], giving us mutable +/// access to the [AppDelegate] on the main UI thread. +/// +/// See [QueueContext] docs for more information. +#[derive(Clone)] +pub struct Queue { + main_wnd: nwg::ControlHandle, +} + +// SAFETY: It is safe to post window messages across threads +unsafe impl Send for Queue {} + +/// The context contains a callback function that is passed as a pointer to the main thread +/// along with a custom window message `QUEUE_MESSAGE`. +/// +/// It must be wrapped in a struct since we cannot pass a fat pointer +/// `*mut dyn for<'a> FnOnce(&'a mut AppWindow) + Send` to `PostMessageW`. +pub struct QueueContext { + pub callback: Box<dyn for<'a> FnOnce(&'a mut AppWindow) + Send>, +} + +impl AppDelegateQueue<AppWindow> for Queue { + fn queue_main<F: FnOnce(&mut AppWindow) + 'static + Send>(&self, callback: F) { + let Some(hwnd) = self.main_wnd.hwnd() else { + return; + }; + let context = QueueContext { + callback: Box::new(callback), + }; + let context_ptr = Box::into_raw(Box::new(context)); + // SAFETY: This is safe since `callback` is Send + unsafe { PostMessageW(hwnd as _, QUEUE_MESSAGE, 0, context_ptr as isize) }; + } +} diff --git a/installer-downloader/src/winapi_impl/mod.rs b/installer-downloader/src/winapi_impl/mod.rs new file mode 100644 index 0000000000..15a9957c00 --- /dev/null +++ b/installer-downloader/src/winapi_impl/mod.rs @@ -0,0 +1,22 @@ +use native_windows_gui as nwg; + +use crate::delegate::{AppDelegate, AppDelegateQueue}; + +mod delegate; +mod ui; + +pub fn main() { + nwg::init().expect("Failed to init Native Windows GUI"); + nwg::Font::set_global_family("Segoe UI").expect("Failed to set default font"); + + let window = ui::AppWindow::default(); + let window = window.layout().unwrap(); + + let queue = window.borrow().queue(); + + queue.queue_main(|window| { + crate::controller::initialize_controller(window); + }); + + nwg::dispatch_thread_events(); +} diff --git a/installer-downloader/src/winapi_impl/ui.rs b/installer-downloader/src/winapi_impl/ui.rs new file mode 100644 index 0000000000..1b4704ad3d --- /dev/null +++ b/installer-downloader/src/winapi_impl/ui.rs @@ -0,0 +1,373 @@ +//! This module handles setting up and rendering changes to the UI + +//use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +use native_windows_gui::{self as nwg, ControlHandle, ImageDecoder, WindowFlags}; + +use windows_sys::Win32::Foundation::COLORREF; +use windows_sys::Win32::Graphics::Gdi::{ + CreateFontIndirectW, SetBkColor, SetBkMode, SetTextColor, COLOR_WINDOW, LOGFONTW, TRANSPARENT, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::WM_CTLCOLORSTATIC; + +use crate::resource::{ + BANNER_DESC, BETA_LINK_TEXT, BETA_PREFACE_DESC, CANCEL_BUTTON_SIZE, CANCEL_BUTTON_TEXT, + DOWNLOAD_BUTTON_SIZE, DOWNLOAD_BUTTON_TEXT, WINDOW_HEIGHT, WINDOW_TITLE, WINDOW_WIDTH, +}; + +use super::delegate::QueueContext; + +static BANNER_IMAGE_DATA: &[u8] = include_bytes!("../../assets/logo-icon.png"); +static BANNER_TEXT_IMAGE_DATA: &[u8] = include_bytes!("../../assets/logo-text.png"); + +const BACKGROUND_COLOR: [u8; 3] = [0x19, 0x2e, 0x45]; +/// Beta link color: #003E92 +const LINK_COLOR: [u8; 3] = [0x00, 0x3e, 0x92]; + +/// Custom window message handler used to adjust the banner text color. +pub const SET_LABEL_HANDLER_ID: usize = 0x10000; +/// Unique ID of the handler used to handle our custom `QUEUE_MESSAGE`. +pub const QUEUE_MESSAGE_HANDLER_ID: usize = 0x10001; +/// Custom window message used to process requests from other threads. +pub const QUEUE_MESSAGE: u32 = 0x10001; +/// Unique ID of the handler for the beta link. +pub const BETA_LINK_HANDLER_ID: usize = 0x10002; + +#[derive(Default)] +pub struct AppWindow { + pub window: nwg::Window, + + pub banner: nwg::ImageFrame, + + pub banner_text: nwg::Label, + pub banner_text_image_bitmap: RefCell<Option<nwg::Bitmap>>, + pub banner_text_image: nwg::ImageFrame, + pub banner_image_bitmap: RefCell<Option<nwg::Bitmap>>, + pub banner_image: nwg::ImageFrame, + + pub cancel_button: nwg::Button, + pub download_button: nwg::Button, + + pub progress_bar: nwg::ProgressBar, + + pub status_text: nwg::Label, + pub download_text: nwg::Label, + + pub beta_prefix: nwg::Label, + pub beta_link: nwg::Label, +} + +impl AppWindow { + /// Set up UI elements, position them, and register window message handlers + /// Note that some additional setup happens in [Self::on_init] + pub fn layout(mut self) -> Result<Rc<RefCell<AppWindow>>, nwg::NwgError> { + nwg::Window::builder() + .size((WINDOW_WIDTH as i32, WINDOW_HEIGHT as i32)) + .center(true) + .title(WINDOW_TITLE) + .flags(WindowFlags::WINDOW) + .build(&mut self.window)?; + + nwg::ImageFrame::builder() + .parent(&self.window) + .background_color(Some(BACKGROUND_COLOR)) + .build(&mut self.banner)?; + + nwg::Label::builder() + .parent(&self.banner) + .background_color(Some(BACKGROUND_COLOR)) + .build(&mut self.banner_text)?; + + nwg::ImageFrame::builder() + .parent(&self.banner) + .background_color(Some(BACKGROUND_COLOR)) + .build(&mut self.banner_image)?; + nwg::ImageFrame::builder() + .parent(&self.banner) + .background_color(Some(BACKGROUND_COLOR)) + .build(&mut self.banner_text_image)?; + + nwg::Button::builder() + .parent(&self.window) + .size(try_pair_into(DOWNLOAD_BUTTON_SIZE).unwrap()) + .text(&DOWNLOAD_BUTTON_TEXT.replace("&", "&&")) + .build(&mut self.download_button)?; + + nwg::Button::builder() + .parent(&self.window) + .size(try_pair_into(CANCEL_BUTTON_SIZE).unwrap()) + .text(CANCEL_BUTTON_TEXT) + .build(&mut self.cancel_button)?; + + nwg::Label::builder() + .parent(&self.window) + .size((320, 32)) + .text("") + .h_align(nwg::HTextAlign::Center) + .build(&mut self.status_text)?; + + nwg::Label::builder() + .parent(&self.window) + .size((480, 32)) + .text("") + .h_align(nwg::HTextAlign::Center) + .build(&mut self.download_text)?; + + nwg::Label::builder() + .parent(&self.window) + .size((240, 24)) + .text(BETA_PREFACE_DESC) + .h_align(nwg::HTextAlign::Left) + .build(&mut self.beta_prefix)?; + nwg::Label::builder() + .parent(&self.window) + .size((128, 24)) + .text(BETA_LINK_TEXT) + .font(Some(&create_link_font()?)) + .h_align(nwg::HTextAlign::Left) + .build(&mut self.beta_link)?; + + const PROGRESS_BAR_MARGIN: i32 = 48; + nwg::ProgressBar::builder() + .parent(&self.window) + .size((WINDOW_WIDTH as i32 - 2 * PROGRESS_BAR_MARGIN, 16)) + .build(&mut self.progress_bar)?; + + const BANNER_HEIGHT: u32 = 102; + + self.banner.set_size(self.window.size().0, BANNER_HEIGHT); + + const LOWER_AREA_YMARGIN: i32 = 48; + const LOWER_AREA_YPADDING: i32 = 16; + const LABEL_YSPACING: i32 = 16; + + self.download_text.set_visible(false); + self.status_text.set_position( + (self.window.size().0 / 2) as i32 - (self.status_text.size().0 / 2) as i32, + BANNER_HEIGHT as i32 + LOWER_AREA_YMARGIN, + ); + self.download_button.set_position( + (self.window.size().0 / 2) as i32 - (self.download_button.size().0 / 2) as i32, + self.status_text.position().1 + 8 + LABEL_YSPACING + LOWER_AREA_YPADDING, + ); + self.download_text.set_position( + (self.window.size().0 / 2) as i32 - (self.download_text.size().0 / 2) as i32, + self.status_text.position().1 + LABEL_YSPACING + LOWER_AREA_YPADDING, + ); + self.progress_bar.set_position( + PROGRESS_BAR_MARGIN, + self.download_text.position().1 + LABEL_YSPACING + LOWER_AREA_YPADDING, + ); + self.cancel_button.set_position( + (self.window.size().0 / 2) as i32 - (self.cancel_button.size().0 / 2) as i32, + self.progress_bar.position().1 + + self.progress_bar.size().1 as i32 + + LOWER_AREA_YPADDING, + ); + + self.beta_prefix.set_position( + 24, + self.window.size().1 as i32 - 24 - self.beta_prefix.size().1 as i32, + ); + self.beta_link.set_position( + self.beta_prefix.position().0 + self.beta_prefix.size().0 as i32, + self.beta_prefix.position().1, + ); + handle_beta_link_messages(&self.window, &self.beta_link, BETA_LINK_HANDLER_ID)?; + + self.window.set_visible(true); + + let event_handle = self.window.handle; + let app = Rc::new(RefCell::new(self)); + + handle_init_and_close_messages(event_handle, app.clone()); + handle_queue_message(event_handle, app.clone())?; + + Ok(app) + } + + /// This function is called when the top-level window has been created + fn on_init(&self) { + if let Err(err) = self.load_banner_image() { + eprintln!("load_banner_image failed: {err}"); + // not fatal, so continue + } + if let Err(err) = self.load_banner_text_image() { + eprintln!("load_banner_text_image failed: {err}"); + // not fatal, so continue + } + + if let Err(err) = handle_banner_label_colors(&self.banner.handle, SET_LABEL_HANDLER_ID) { + eprintln!("handle_banner_label_colors failed: {err}"); + // not fatal, so continue + } + + self.banner_text.set_text(BANNER_DESC); + self.banner_text + .set_position(24, self.banner_image.position().1 + 20); + self.banner_text.set_size( + WINDOW_WIDTH as u32 - self.banner_text.position().0 as u32 - 12, + 64, + ); + } + + /// This function is called when user clicks the "X" + fn on_close(&self) { + nwg::stop_thread_dispatch(); + } + + /// Load the embedded image and display it in `banner_image` + fn load_banner_image(&self) -> Result<(), nwg::NwgError> { + let src = ImageDecoder::new()?.from_stream(BANNER_IMAGE_DATA)?; + let frame = src.frame(0)?; + let size = frame.size(); + let mut img = self.banner_image_bitmap.borrow_mut(); + let bmp = frame.as_bitmap()?; + img.replace(bmp); + + self.banner_image.set_bitmap(img.as_ref()); + self.banner_image.set_position(24, 24); + self.banner_image.set_size(size.0, size.1); + + Ok(()) + } + + /// Load the embedded image and display it in `banner_text_image` + fn load_banner_text_image(&self) -> Result<(), nwg::NwgError> { + let src = ImageDecoder::new()?.from_stream(BANNER_TEXT_IMAGE_DATA)?; + let frame = src.frame(0)?; + let size = frame.size(); + let mut img = self.banner_text_image_bitmap.borrow_mut(); + img.replace(frame.as_bitmap()?); + + self.banner_text_image.set_bitmap(img.as_ref()); + self.banner_text_image.set_position( + self.banner_image.position().0 + self.banner_image.size().0 as i32 + 8, + self.banner_image.position().1 + self.banner_image.size().1 as i32 / 2 + - size.1 as i32 / 2, + ); + self.banner_text_image.set_size(size.0, size.1); + + Ok(()) + } +} + +/// Register a window message handler that ensures that the banner labels are rendered with the +/// correct color +fn handle_banner_label_colors( + banner: &ControlHandle, + handler_id: usize, +) -> Result<nwg::RawEventHandler, nwg::NwgError> { + nwg::bind_raw_event_handler(banner, handler_id, move |_hwnd, msg, w, _p| { + /// This is the RGB() macro except it takes in a slice representing RGB values + pub fn rgb(color: [u8; 3]) -> COLORREF { + color[0] as COLORREF | ((color[1] as COLORREF) << 8) | ((color[2] as COLORREF) << 16) + } + + if msg == WM_CTLCOLORSTATIC { + unsafe { + SetTextColor(w as _, rgb([255, 255, 255])); + SetBkColor(w as _, rgb(BACKGROUND_COLOR)); + } + } + None + }) +} + +/// Register a window message handler for the beta link component +fn handle_beta_link_messages( + parent: &nwg::Window, + link: &nwg::Label, + handler_id: usize, +) -> Result<nwg::RawEventHandler, nwg::NwgError> { + let link_hwnd = link.handle.hwnd().map(|hwnd| hwnd as isize); + nwg::bind_raw_event_handler(&parent.handle, handler_id, move |_hwnd, msg, w, p| { + /// This is the RGB() macro except it takes in a slice representing RGB values + pub fn rgb(color: [u8; 3]) -> COLORREF { + color[0] as COLORREF | ((color[1] as COLORREF) << 8) | ((color[2] as COLORREF) << 16) + } + + if msg == WM_CTLCOLORSTATIC && Some(p) == link_hwnd { + unsafe { + SetBkMode(w as _, TRANSPARENT as _); + SetTextColor(w as _, rgb(LINK_COLOR)); + } + // Out of bounds background + return Some(COLOR_WINDOW as _); + } + + None + }) +} + +/// Register events for [AppWindow::on_init] and [AppWindow::on_close]. +fn handle_init_and_close_messages( + window: impl Into<ControlHandle>, + app: Rc<RefCell<AppWindow>>, +) -> nwg::EventHandler { + let window = window.into(); + nwg::full_bind_event_handler(&window, move |event, _data, handle| match event { + nwg::Event::OnInit if handle == window => { + let app = app.borrow(); + app.on_init(); + } + nwg::Event::OnWindowClose if handle == window => { + let app = app.borrow(); + app.on_close(); + } + _ => (), + }) +} + +/// This handles `QUEUE_MESSAGE` messages, which contain callbacks reachable from +/// pointers to a [super::delegate::QueueContext]. See [super::delegate::QueueContext] +/// and [super::delegate::Queue] for details. +fn handle_queue_message( + window: impl Into<ControlHandle>, + app: Rc<RefCell<AppWindow>>, +) -> Result<nwg::RawEventHandler, nwg::NwgError> { + nwg::bind_raw_event_handler( + &window.into(), + QUEUE_MESSAGE_HANDLER_ID, + move |_hwnd, msg, _w, p| { + if msg == QUEUE_MESSAGE { + // SAFETY: This message is only sent with a boxed sendable function pointer, so we're + // good. See the implementation of `AppDelegateQueue` for `Queue`. + let context = unsafe { Box::from_raw(p as *mut QueueContext) }; + let mut app = app.borrow_mut(); + (context.callback)(&mut app); + } + None + }, + ) +} + +fn try_pair_into<A: TryInto<B>, B>(a: (A, A)) -> Result<(B, B), A::Error> { + Ok((a.0.try_into()?, a.1.try_into()?)) +} + +/// Create a link font +/// TODO: upstream to nwg +fn create_link_font() -> Result<nwg::Font, nwg::NwgError> { + let face_name = "Segoe UI".encode_utf16(); + + let raw_font = unsafe { + let mut logfont: LOGFONTW = std::mem::zeroed(); + logfont.lfUnderline = 1; + + for (dest, src) in logfont.lfFaceName.iter_mut().zip(face_name) { + *dest = src; + } + CreateFontIndirectW(&logfont) + }; + + if raw_font == 0 { + return Err(nwg::NwgError::Unknown); + } + + Ok(nwg::Font { + handle: raw_font as _, + }) +} diff --git a/installer-downloader/tests/controller.rs b/installer-downloader/tests/controller.rs new file mode 100644 index 0000000000..92e290273a --- /dev/null +++ b/installer-downloader/tests/controller.rs @@ -0,0 +1,366 @@ +//! Tests for integrations between UI controller and other components +//! +//! The tests rely on `insta` for snapshot testing. If they fail due to snapshot assertions, +//! then most likely the snapshots need to be updated. The most convenient way to review +//! changes to, and update, snapshots are by running `cargo insta review`. + +use insta::assert_yaml_snapshot; +use installer_downloader::controller::AppController; +use installer_downloader::delegate::{AppDelegate, AppDelegateQueue}; +use installer_downloader::ui_downloader::UiAppDownloaderParameters; +use mullvad_update::api::{Version, VersionInfo, VersionInfoProvider}; +use mullvad_update::app::{AppDownloader, AppDownloaderFactory, DownloadError}; +use mullvad_update::fetch::ProgressUpdater; +use std::sync::{Arc, LazyLock, Mutex}; +use std::time::Duration; +use std::vec::Vec; + +pub struct FakeVersionInfoProvider {} + +static FAKE_VERSION: LazyLock<VersionInfo> = LazyLock::new(|| VersionInfo { + stable: Version { + version: "2025.1".to_owned(), + urls: vec!["https://mullvad.net/fakeapp".to_owned()], + size: 1234, + signature_urls: vec!["https://mullvad.net/fakesig".to_owned()], + }, + beta: None, +}); + +#[async_trait::async_trait] +impl VersionInfoProvider for FakeVersionInfoProvider { + async fn get_version_info() -> anyhow::Result<VersionInfo> { + Ok(FAKE_VERSION.clone()) + } +} + +/// Downloader for which all steps immediately succeed +pub type FakeAppDownloaderHappyPath = FakeAppDownloader<true, true, true>; + +/// Downloader for which the download step fails +pub type FakeAppDownloaderDownloadFail = FakeAppDownloader<true, false, false>; + +/// Downloader for which all but the final verification step succeed +pub type FakeAppDownloaderVerifyFail = FakeAppDownloader<true, true, false>; + +impl<const A: bool, const B: bool, const C: bool> AppDownloaderFactory + for FakeAppDownloader<A, B, C> +{ + type Parameters = UiAppDownloaderParameters<FakeAppDelegate>; + + fn new_downloader(params: Self::Parameters) -> Self { + FakeAppDownloader { params } + } +} + +/// Fake app downloader +/// +/// Parameters: +/// * SIG_SUCCEED - whether fetching the signature succeeds +/// * EXE_SUCCEED - whether fetching the binary succeeds +/// * VERIFY_SUCCEED - whether verifying the signature succeeds +pub struct FakeAppDownloader< + const SIG_SUCCEED: bool, + const EXE_SUCCEED: bool, + const VERIFY_SUCCEED: bool, +> { + params: UiAppDownloaderParameters<FakeAppDelegate>, +} + +#[async_trait::async_trait] +impl<const SIG_SUCCEED: bool, const EXE_SUCCEED: bool, const VERIFY_SUCCEED: bool> AppDownloader + for FakeAppDownloader<SIG_SUCCEED, EXE_SUCCEED, VERIFY_SUCCEED> +{ + async fn download_signature(&mut self) -> Result<(), DownloadError> { + self.params.sig_progress.set_url(&self.params.signature_url); + self.params.sig_progress.set_progress(0.); + if SIG_SUCCEED { + self.params.sig_progress.set_progress(1.); + Ok(()) + } else { + Err(DownloadError::FetchSignature(anyhow::anyhow!( + "fetching signature failed" + ))) + } + } + + async fn download_executable(&mut self) -> Result<(), DownloadError> { + self.params.app_progress.set_url(&self.params.app_url); + self.params.app_progress.set_progress(0.); + if EXE_SUCCEED { + self.params.app_progress.set_progress(1.); + Ok(()) + } else { + Err(DownloadError::FetchApp(anyhow::anyhow!( + "fetching app failed" + ))) + } + } + + async fn verify(&mut self) -> Result<(), DownloadError> { + if VERIFY_SUCCEED { + Ok(()) + } else { + Err(DownloadError::Verification(anyhow::anyhow!( + "verification failed" + ))) + } + } +} + +/// A fake queue that stores callbacks so that tests can run them later. +#[derive(Clone, Default)] +pub struct FakeQueue { + callbacks: Arc<Mutex<Vec<Box<dyn FnOnce(&mut FakeAppDelegate) + Send>>>>, +} + +impl FakeQueue { + /// Run all queued callbacks on the given delegate. + fn run_callbacks(&self, delegate: &mut FakeAppDelegate) { + let mut callbacks = self.callbacks.lock().unwrap(); + for cb in callbacks.drain(..) { + cb(delegate); + } + } +} + +impl AppDelegateQueue<FakeAppDelegate> for FakeQueue { + fn queue_main<F: FnOnce(&mut FakeAppDelegate) + 'static + Send>(&self, callback: F) { + self.callbacks.lock().unwrap().push(Box::new(callback)); + } +} + +/// A fake [AppDelegate] +#[derive(Default)] +pub struct FakeAppDelegate { + /// Callback registered by `on_download` + pub download_callback: Option<Box<dyn Fn() + Send>>, + /// Callback registered by `on_cancel` + pub cancel_callback: Option<Box<dyn Fn() + Send>>, + /// State of delegate + pub state: DelegateState, + /// Queue used to simulate the main thread + pub queue: FakeQueue, +} + +/// A complete state of the UI, including its call history +#[derive(Default, serde::Serialize)] +pub struct DelegateState { + pub status_text: String, + pub download_text: String, + pub download_button_visible: bool, + pub cancel_button_visible: bool, + pub cancel_button_enabled: bool, + pub download_button_enabled: bool, + pub download_progress: u32, + pub download_progress_visible: bool, + pub beta_text_visible: bool, + /// Record of method calls. + pub call_log: Vec<String>, +} + +impl AppDelegate for FakeAppDelegate { + type Queue = FakeQueue; + + fn on_download<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + self.state.call_log.push("on_download".into()); + self.download_callback = Some(Box::new(callback)); + } + + fn on_cancel<F>(&mut self, callback: F) + where + F: Fn() + Send + 'static, + { + self.state.call_log.push("on_cancel".into()); + self.cancel_callback = Some(Box::new(callback)); + } + + fn set_status_text(&mut self, text: &str) { + self.state + .call_log + .push(format!("set_status_text: {}", text)); + self.state.status_text = text.to_owned(); + } + + fn set_download_text(&mut self, text: &str) { + self.state + .call_log + .push(format!("set_download_text: {}", text)); + self.state.download_text = text.to_owned(); + } + + fn show_download_progress(&mut self) { + self.state.call_log.push("show_download_progress".into()); + self.state.download_progress_visible = true; + } + + fn hide_download_progress(&mut self) { + self.state.call_log.push("hide_download_progress".into()); + self.state.download_progress_visible = false; + } + + fn set_download_progress(&mut self, complete: u32) { + self.state + .call_log + .push(format!("set_download_progress: {}", complete)); + self.state.download_progress = complete; + } + + fn show_download_button(&mut self) { + self.state.call_log.push("show_download_button".into()); + self.state.download_button_visible = true; + } + + fn hide_download_button(&mut self) { + self.state.call_log.push("hide_download_button".into()); + self.state.download_button_visible = false; + } + + fn enable_download_button(&mut self) { + self.state.call_log.push("enable_download_button".into()); + self.state.download_button_enabled = true; + } + + fn disable_download_button(&mut self) { + self.state.call_log.push("disable_download_button".into()); + self.state.download_button_enabled = false; + } + + fn show_cancel_button(&mut self) { + self.state.call_log.push("show_cancel_button".into()); + self.state.cancel_button_visible = true; + } + + fn hide_cancel_button(&mut self) { + self.state.call_log.push("hide_cancel_button".into()); + self.state.cancel_button_visible = false; + } + + fn enable_cancel_button(&mut self) { + self.state.call_log.push("enable_cancel_button".into()); + self.state.cancel_button_enabled = true; + } + + fn disable_cancel_button(&mut self) { + self.state.call_log.push("disable_cancel_button".into()); + self.state.cancel_button_enabled = false; + } + + fn show_beta_text(&mut self) { + self.state.call_log.push("show_beta_text".into()); + self.state.beta_text_visible = true; + } + + fn hide_beta_text(&mut self) { + self.state.call_log.push("hide_beta_text".into()); + self.state.beta_text_visible = false; + } + + fn queue(&self) -> Self::Queue { + self.queue.clone() + } +} + +/// Test that the flow starts by fetching app version data +#[tokio::test(start_paused = true)] +async fn test_fetch_version() { + let mut delegate = FakeAppDelegate::default(); + AppController::initialize::<_, FakeAppDownloaderHappyPath, FakeVersionInfoProvider>( + &mut delegate, + ); + + // The app should start out by fetching the current app version + assert_yaml_snapshot!(delegate.state); + + tokio::time::sleep(Duration::from_secs(1)).await; + + // Run UI updates to display the fetched version + let queue = delegate.queue.clone(); + queue.run_callbacks(&mut delegate); + + // The download button and current version should be displayed + assert_yaml_snapshot!(delegate.state); +} + +/// Test that the on_download callback gets registered and, when invoked, +/// properly updates the UI. +#[tokio::test(start_paused = true)] +async fn test_download() { + let mut delegate = FakeAppDelegate::default(); + AppController::initialize::<_, FakeAppDownloaderHappyPath, FakeVersionInfoProvider>( + &mut delegate, + ); + + // Wait for the version info + tokio::time::sleep(Duration::from_secs(1)).await; + + let queue = delegate.queue.clone(); + queue.run_callbacks(&mut delegate); + + // The download button should be available + assert_yaml_snapshot!(delegate.state); + + // Initiate download + let cb = delegate + .download_callback + .take() + .expect("no download callback registered"); + cb(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + // Run queued actions + let queue = delegate.queue.clone(); + queue.run_callbacks(&mut delegate); + + // We should see download progress, and cancellation + assert_yaml_snapshot!(delegate.state); + + // Wait for download + tokio::time::sleep(Duration::from_secs(1)).await; + + let queue = delegate.queue.clone(); + queue.run_callbacks(&mut delegate); + + // Everything including verification should have succeeded + assert_yaml_snapshot!(delegate.state); +} + +/// Test that the install aborts if verification fails +#[tokio::test(start_paused = true)] +async fn test_failed_verification() { + let mut delegate = FakeAppDelegate::default(); + AppController::initialize::<_, FakeAppDownloaderVerifyFail, FakeVersionInfoProvider>( + &mut delegate, + ); + + // Wait for the version info + tokio::time::sleep(Duration::from_secs(1)).await; + + let queue = delegate.queue.clone(); + queue.run_callbacks(&mut delegate); + + // Initiate download + let cb = delegate + .download_callback + .take() + .expect("no download callback registered"); + cb(); + + tokio::time::sleep(Duration::from_secs(1)).await; + + // Wait for queued actions to complete + let queue = delegate.queue.clone(); + queue.run_callbacks(&mut delegate); + + tokio::time::sleep(Duration::from_secs(1)).await; + + let queue = delegate.queue.clone(); + queue.run_callbacks(&mut delegate); + + // Verification failed + assert_yaml_snapshot!(delegate.state); +} diff --git a/installer-downloader/tests/snapshots/controller__download-2.snap b/installer-downloader/tests/snapshots/controller__download-2.snap new file mode 100644 index 0000000000..e86d9055d7 --- /dev/null +++ b/installer-downloader/tests/snapshots/controller__download-2.snap @@ -0,0 +1,31 @@ +--- +source: installer-downloader/tests/controller.rs +expression: delegate.state +snapshot_kind: text +--- +status_text: "Version: 2025.1" +download_text: "" +download_button_visible: false +cancel_button_visible: true +cancel_button_enabled: true +download_button_enabled: true +download_progress: 0 +download_progress_visible: true +beta_text_visible: false +call_log: + - hide_download_progress + - show_download_button + - disable_download_button + - hide_cancel_button + - hide_beta_text + - "set_status_text: Loading version details..." + - on_download + - on_cancel + - "set_status_text: Version: 2025.1" + - enable_download_button + - "set_download_text: " + - hide_download_button + - hide_beta_text + - show_cancel_button + - enable_cancel_button + - show_download_progress diff --git a/installer-downloader/tests/snapshots/controller__download-3.snap b/installer-downloader/tests/snapshots/controller__download-3.snap new file mode 100644 index 0000000000..140e6d6320 --- /dev/null +++ b/installer-downloader/tests/snapshots/controller__download-3.snap @@ -0,0 +1,42 @@ +--- +source: installer-downloader/tests/controller.rs +expression: delegate.state +snapshot_kind: text +--- +status_text: "Version: 2025.1" +download_text: Verification successful. Starting install... +download_button_visible: false +cancel_button_visible: true +cancel_button_enabled: false +download_button_enabled: true +download_progress: 100 +download_progress_visible: true +beta_text_visible: false +call_log: + - hide_download_progress + - show_download_button + - disable_download_button + - hide_cancel_button + - hide_beta_text + - "set_status_text: Loading version details..." + - on_download + - on_cancel + - "set_status_text: Version: 2025.1" + - enable_download_button + - "set_download_text: " + - hide_download_button + - hide_beta_text + - show_cancel_button + - enable_cancel_button + - show_download_progress + - "set_download_progress: 0" + - "set_download_text: Downloading from mullvad.net... (0%)" + - "set_download_progress: 100" + - "set_download_text: Downloading from mullvad.net... (100%)" + - "set_download_progress: 0" + - "set_download_text: Downloading from mullvad.net... (0%)" + - "set_download_progress: 100" + - "set_download_text: Downloading from mullvad.net... (100%)" + - "set_download_text: Download complete. Verifying..." + - disable_cancel_button + - "set_download_text: Verification successful. Starting install..." diff --git a/installer-downloader/tests/snapshots/controller__download.snap b/installer-downloader/tests/snapshots/controller__download.snap new file mode 100644 index 0000000000..45bd1796ad --- /dev/null +++ b/installer-downloader/tests/snapshots/controller__download.snap @@ -0,0 +1,25 @@ +--- +source: installer-downloader/tests/controller.rs +expression: delegate.state +snapshot_kind: text +--- +status_text: "Version: 2025.1" +download_text: "" +download_button_visible: true +cancel_button_visible: false +cancel_button_enabled: false +download_button_enabled: true +download_progress: 0 +download_progress_visible: false +beta_text_visible: false +call_log: + - hide_download_progress + - show_download_button + - disable_download_button + - hide_cancel_button + - hide_beta_text + - "set_status_text: Loading version details..." + - on_download + - on_cancel + - "set_status_text: Version: 2025.1" + - enable_download_button diff --git a/installer-downloader/tests/snapshots/controller__failed_verification.snap b/installer-downloader/tests/snapshots/controller__failed_verification.snap new file mode 100644 index 0000000000..bb92678de9 --- /dev/null +++ b/installer-downloader/tests/snapshots/controller__failed_verification.snap @@ -0,0 +1,42 @@ +--- +source: installer-downloader/tests/controller.rs +expression: delegate.state +snapshot_kind: text +--- +status_text: "Version: 2025.1" +download_text: "ERROR: Verification failed!" +download_button_visible: false +cancel_button_visible: true +cancel_button_enabled: false +download_button_enabled: true +download_progress: 100 +download_progress_visible: true +beta_text_visible: false +call_log: + - hide_download_progress + - show_download_button + - disable_download_button + - hide_cancel_button + - hide_beta_text + - "set_status_text: Loading version details..." + - on_download + - on_cancel + - "set_status_text: Version: 2025.1" + - enable_download_button + - "set_download_text: " + - hide_download_button + - hide_beta_text + - show_cancel_button + - enable_cancel_button + - show_download_progress + - "set_download_progress: 0" + - "set_download_text: Downloading from mullvad.net... (0%)" + - "set_download_progress: 100" + - "set_download_text: Downloading from mullvad.net... (100%)" + - "set_download_progress: 0" + - "set_download_text: Downloading from mullvad.net... (0%)" + - "set_download_progress: 100" + - "set_download_text: Downloading from mullvad.net... (100%)" + - "set_download_text: Download complete. Verifying..." + - disable_cancel_button + - "set_download_text: ERROR: Verification failed!" diff --git a/installer-downloader/tests/snapshots/controller__fetch_version-2.snap b/installer-downloader/tests/snapshots/controller__fetch_version-2.snap new file mode 100644 index 0000000000..45bd1796ad --- /dev/null +++ b/installer-downloader/tests/snapshots/controller__fetch_version-2.snap @@ -0,0 +1,25 @@ +--- +source: installer-downloader/tests/controller.rs +expression: delegate.state +snapshot_kind: text +--- +status_text: "Version: 2025.1" +download_text: "" +download_button_visible: true +cancel_button_visible: false +cancel_button_enabled: false +download_button_enabled: true +download_progress: 0 +download_progress_visible: false +beta_text_visible: false +call_log: + - hide_download_progress + - show_download_button + - disable_download_button + - hide_cancel_button + - hide_beta_text + - "set_status_text: Loading version details..." + - on_download + - on_cancel + - "set_status_text: Version: 2025.1" + - enable_download_button diff --git a/installer-downloader/tests/snapshots/controller__fetch_version.snap b/installer-downloader/tests/snapshots/controller__fetch_version.snap new file mode 100644 index 0000000000..a8c1896189 --- /dev/null +++ b/installer-downloader/tests/snapshots/controller__fetch_version.snap @@ -0,0 +1,23 @@ +--- +source: installer-downloader/tests/controller.rs +expression: delegate.state +snapshot_kind: text +--- +status_text: Loading version details... +download_text: "" +download_button_visible: true +cancel_button_visible: false +cancel_button_enabled: false +download_button_enabled: false +download_progress: 0 +download_progress_visible: false +beta_text_visible: false +call_log: + - hide_download_progress + - show_download_button + - disable_download_button + - hide_cancel_button + - hide_beta_text + - "set_status_text: Loading version details..." + - on_download + - on_cancel diff --git a/mullvad-update/Cargo.toml b/mullvad-update/Cargo.toml new file mode 100644 index 0000000000..f3a067c3af --- /dev/null +++ b/mullvad-update/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "mullvad-update" +description = "Support functions for securely installing or updating Mullvad VPN" +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +anyhow = "1.0" +json-canon = "0.1" +chrono = { workspace = true, features = ["serde"] } +ed25519-dalek = { version = "2.1", default-features = false } +hex = { version = "0.4", default-features = false } +pgp = "0.14.0" +reqwest = { version = "0.12.9", features = ["blocking", "json"] } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +tokio = { version = "1", features = ["full"] } +async-trait = "0.1" + +mullvad-version = { path = "../mullvad-version", features = ["serde"] } + +[dev-dependencies] +async-tempfile = "0.6" +mockito = "1.6.1" +rand = "0.8.5" diff --git a/mullvad-update/mullvad-code-signing.gpg b/mullvad-update/mullvad-code-signing.gpg Binary files differnew file mode 100644 index 0000000000..54fa40bc27 --- /dev/null +++ b/mullvad-update/mullvad-code-signing.gpg diff --git a/mullvad-update/src/api.rs b/mullvad-update/src/api.rs new file mode 100644 index 0000000000..16e578d485 --- /dev/null +++ b/mullvad-update/src/api.rs @@ -0,0 +1,58 @@ +//! Fetch information about app versions from the Mullvad API + +/// See [module-level](self) docs. +#[async_trait::async_trait] +pub trait VersionInfoProvider { + /// Return info about the stable version + async fn get_version_info() -> anyhow::Result<VersionInfo>; +} + +/// Contains information about all versions +#[derive(Debug, Clone)] +pub struct VersionInfo { + /// Stable version info + pub stable: Version, + /// Beta version info + pub beta: Option<Version>, +} + +/// Contains information about a version for the current target +#[derive(Debug, Clone)] +pub struct Version { + /// Version + pub version: String, + /// URLs to use for downloading the app installer + pub urls: Vec<String>, + /// Size of installer, in bytes + pub size: usize, + /// URLs pointing to app PGP signatures + pub signature_urls: Vec<String>, +} + +/// Use hardcoded URL to fetch installer +/// TODO: This is temporary +pub struct LatestVersionInfoProvider; + +#[async_trait::async_trait] +impl VersionInfoProvider for LatestVersionInfoProvider { + async fn get_version_info() -> anyhow::Result<VersionInfo> { + Ok(VersionInfo { + stable: Version { + version: "2025.3".to_string(), + urls: vec!["https://mullvad.net/en/download/app/exe/latest".to_owned()], + size: 200 * 1024 * 1024, + signature_urls: vec![ + "https://mullvad.net/en/download/app/exe/latest/signature".to_owned() + ], + }, + beta: Some(Version { + version: "2025.3-beta1".to_string(), + urls: vec!["https://mullvad.net/en/download/app/exe/latest-beta".to_owned()], + size: 200 * 1024 * 1024, + signature_urls: vec![ + "https://mullvad.net/en/download/app/exe/latest-beta/signature".to_owned(), + ], + }), + }) + } +} diff --git a/mullvad-update/src/app.rs b/mullvad-update/src/app.rs new file mode 100644 index 0000000000..548fcf1681 --- /dev/null +++ b/mullvad-update/src/app.rs @@ -0,0 +1,140 @@ +//! This module implements the flow of downloading and verifying the app signature. + +use std::path::PathBuf; + +use crate::{ + fetch::{self, ProgressUpdater}, + verify::{AppVerifier, PgpVerifier}, +}; + +#[derive(Debug)] +pub enum DownloadError { + FetchSignature(anyhow::Error), + FetchApp(anyhow::Error), + Verification(anyhow::Error), +} + +/// Parameters required to construct an [AppDownloader]. +pub struct AppDownloaderParameters<SigProgress, AppProgress> { + pub signature_url: String, + pub app_url: String, + pub app_size: usize, + pub sig_progress: SigProgress, + pub app_progress: AppProgress, +} + +/// See the [module-level documentation](self). +#[async_trait::async_trait] +pub trait AppDownloader: Send { + /// Download the app signature. + async fn download_signature(&mut self) -> Result<(), DownloadError>; + + /// Download the app binary. + async fn download_executable(&mut self) -> Result<(), DownloadError>; + + /// Verify the app signature. + async fn verify(&mut self) -> Result<(), DownloadError>; +} + +/// Trait for constructing some [AppDownloader]. +pub trait AppDownloaderFactory: AppDownloader { + type Parameters; + + /// Instantiate a new [AppDownloader]. + fn new_downloader(parameters: Self::Parameters) -> Self; +} + +/// Download the app and signature, and verify the app's signature +pub async fn install_and_upgrade(mut downloader: impl AppDownloader) -> Result<(), DownloadError> { + downloader.download_signature().await?; + downloader.download_executable().await?; + downloader.verify().await +} + +#[derive(Clone)] +pub struct HttpAppDownloader<SigProgress, AppProgress> { + signature_url: String, + app_url: String, + app_size: usize, + signature_progress_updater: SigProgress, + app_progress_updater: AppProgress, + // TODO: set permissions + tmp_dir: PathBuf, +} + +impl<SigProgress, AppProgress> HttpAppDownloader<SigProgress, AppProgress> { + const MAX_SIGNATURE_SIZE: usize = 1024; + + pub fn new(parameters: AppDownloaderParameters<SigProgress, AppProgress>) -> Self { + let tmp_dir = std::env::temp_dir(); + Self { + signature_url: parameters.signature_url, + app_url: parameters.app_url, + app_size: parameters.app_size, + signature_progress_updater: parameters.sig_progress, + app_progress_updater: parameters.app_progress, + tmp_dir, + } + } +} + +impl<SigProgress: ProgressUpdater, AppProgress: ProgressUpdater> AppDownloaderFactory + for HttpAppDownloader<SigProgress, AppProgress> +{ + type Parameters = AppDownloaderParameters<SigProgress, AppProgress>; + + fn new_downloader(parameters: Self::Parameters) -> Self + where + Self: Sized, + { + HttpAppDownloader::new(parameters) + } +} + +#[async_trait::async_trait] +impl<SigProgress: ProgressUpdater, AppProgress: ProgressUpdater> AppDownloader + for HttpAppDownloader<SigProgress, AppProgress> +{ + async fn download_signature(&mut self) -> Result<(), DownloadError> { + fetch::get_to_file( + self.sig_path(), + &self.signature_url, + &mut self.signature_progress_updater, + fetch::SizeHint::Maximum(Self::MAX_SIGNATURE_SIZE), + ) + .await + .map_err(DownloadError::FetchSignature) + } + + async fn download_executable(&mut self) -> Result<(), DownloadError> { + fetch::get_to_file( + self.bin_path(), + &self.app_url, + &mut self.app_progress_updater, + // FIXME: use exact size hint + fetch::SizeHint::Maximum(self.app_size), + ) + .await + .map_err(DownloadError::FetchApp) + } + + async fn verify(&mut self) -> Result<(), DownloadError> { + let bin_path = self.bin_path(); + let sig_path = self.sig_path(); + tokio::task::spawn_blocking(move || { + PgpVerifier::verify(bin_path, sig_path).map_err(DownloadError::Verification) + }) + .await + .expect("verifier panicked") + } +} + +impl<SigProgress, AppProgress> HttpAppDownloader<SigProgress, AppProgress> { + fn bin_path(&self) -> PathBuf { + self.tmp_dir.join("temp.exe") + } + + fn sig_path(&self) -> PathBuf { + self.tmp_dir.join("temp.exe.sig") + } +} diff --git a/mullvad-update/src/deserializer.rs b/mullvad-update/src/deserializer.rs new file mode 100644 index 0000000000..5ddb4a45d8 --- /dev/null +++ b/mullvad-update/src/deserializer.rs @@ -0,0 +1,221 @@ +//! Deserializer for version API response format + +use anyhow::Context; +use serde::Deserialize; + +/// JSON response including signature and signed content +/// This type does not implement [serde::Deserialize] to prevent accidental deserialization without +/// signature verification. +pub struct SignedResponse { + /// Signature of the canonicalized JSON of `signed` + pub signature: ResponseSignature, + /// Content signed by `signature` + pub signed: Response, +} + +/// Helper class that leaves the signed data untouched +/// Note that deserializing doesn't verify anything +#[derive(serde::Deserialize)] +struct PartialSignedResponse { + /// Signature of the canonicalized JSON of `signed` + pub signature: ResponseSignature, + /// Content signed by `signature` + pub signed: serde_json::Value, +} + +impl SignedResponse { + /// Deserialize some bytes to JSON, and verify them, including signature and expiry. + /// If successful, the deserialized data is returned. + pub fn deserialize_and_verify(key: VerifyingKey, bytes: &[u8]) -> Result<Self, anyhow::Error> { + Self::deserialize_and_verify_at_time(key, bytes, chrono::Utc::now()) + } + + /// Deserialize some bytes to JSON, and verify them, including signature and expiry. + /// If successful, the deserialized data is returned. + fn deserialize_and_verify_at_time( + key: VerifyingKey, + bytes: &[u8], + current_time: chrono::DateTime<chrono::Utc>, + ) -> Result<Self, anyhow::Error> { + let partial_data: PartialSignedResponse = + serde_json::from_slice(bytes).context("Invalid version JSON")?; + + // Check if the key matches + if partial_data.signature.keyid.0 != key.0 { + anyhow::bail!("Unrecognized key"); + } + + // Serialize to canonical json format + let canon_data = json_canon::to_vec(&partial_data.signed) + .context("Failed to serialize to canonical JSON")?; + + // Check if the data is signed by our key + partial_data + .signature + .keyid + .0 + .verify_strict(&canon_data, &partial_data.signature.sig.0) + .context("Signature verification failed")?; + + // Deserialize the canonical JSON to structured representation + let signed_response: Response = + serde_json::from_slice(&canon_data).context("Failed to deserialize response")?; + + // Reject time if the data has expired + if current_time >= signed_response.expires { + anyhow::bail!( + "Version metadata has expired: valid until {}", + signed_response.expires + ); + } + + Ok(SignedResponse { + signature: partial_data.signature, + signed: signed_response, + }) + } +} + +/// JSON response signature +#[derive(Deserialize)] +pub struct ResponseSignature { + pub keyid: VerifyingKey, + pub sig: Signature, +} + +/// ed25519 verifying key +pub struct VerifyingKey(pub ed25519_dalek::VerifyingKey); + +impl<'de> Deserialize<'de> for VerifyingKey { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let bytes = String::deserialize(deserializer).and_then(|string| { + bytes_from_hex::<D, { ed25519_dalek::PUBLIC_KEY_LENGTH }>(&string) + })?; + let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes).map_err(|_err| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Other("invalid verifying key"), + &"valid ed25519 key", + ) + })?; + Ok(VerifyingKey(key)) + } +} + +/// ed25519 signature +pub struct Signature(pub ed25519_dalek::Signature); + +impl<'de> Deserialize<'de> for Signature { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + let bytes = String::deserialize(deserializer) + .and_then(|string| bytes_from_hex::<D, { ed25519_dalek::SIGNATURE_LENGTH }>(&string))?; + Ok(Signature(ed25519_dalek::Signature::from_bytes(&bytes))) + } +} + +/// Deserialize a hex-encoded string to a bytes array of an exact size +fn bytes_from_hex<'de, D, const SIZE: usize>(key: &str) -> Result<[u8; SIZE], D::Error> +where + D: serde::Deserializer<'de>, +{ + let bytes = hex::decode(key).map_err(|_err| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Other("hex-encoded string"), + &"valid hex", + ) + })?; + if bytes.len() != SIZE { + let expected = format!("hex-encoded string of {SIZE} bytes"); + return Err(serde::de::Error::invalid_length( + bytes.len(), + &expected.as_str(), + )); + } + let mut key = [0u8; SIZE]; + key.copy_from_slice(&bytes); + Ok(key) +} + +/// Signed JSON response, not including the signature +#[derive(Deserialize)] +pub struct Response { + /// When the signature expires + pub expires: chrono::DateTime<chrono::Utc>, + /// Stable version response + pub stable: VersionResponse, + /// Beta version response + pub beta: Option<VersionResponse>, +} + +#[derive(Deserialize)] +pub struct VersionResponse { + /// The current version in this channel + pub current: SpecificVersionResponse, + /// The version being rolled out in this channel + pub next: Option<NextSpecificVersionResponse>, +} + +#[derive(Deserialize)] +pub struct NextSpecificVersionResponse { + /// The percentage of users that should receive the new version. + pub rollout: f32, + #[serde(flatten)] + pub version: SpecificVersionResponse, +} + +#[derive(Deserialize)] +pub struct SpecificVersionResponse { + /// Mullvad app version + pub version: mullvad_version::Version, + /// Changelog entries + pub changelog: String, + /// Installer details for different architectures + pub installers: SpecificVersionArchitectureResponses, +} + +/// Version details for supported architectures +#[derive(Deserialize)] +pub struct SpecificVersionArchitectureResponses { + /// Details for x86 installer + pub x86: Option<SpecificVersionArchitectureResponse>, + /// Details for ARM64 installer + pub arm64: Option<SpecificVersionArchitectureResponse>, +} + +#[derive(Deserialize)] +pub struct SpecificVersionArchitectureResponse { + /// Mirrors that host the artifact + pub urls: Vec<String>, + /// Size of the installer, in bytes + pub size: usize, + /// TODO: hash of the installer, in bytes + pub sha256: String, +} + +#[cfg(test)] +mod test { + use super::*; + + /// Test that a valid signed version response is successfully deserialized and verified + #[test] + fn test_response_deserialization_and_verification() { + const TEST_PUBKEY: &str = + "AEC24A08466F3D6A1EDCDB2AD3C234428AB9D991B6BEA7F53CB9F172E6CB40D8"; + let pubkey = hex::decode(TEST_PUBKEY).unwrap(); + let verifying_key = + ed25519_dalek::VerifyingKey::from_bytes(&pubkey.try_into().unwrap()).unwrap(); + + SignedResponse::deserialize_and_verify_at_time( + VerifyingKey(verifying_key), + include_bytes!("../test-version-response.json"), + // It's 1970 again + chrono::DateTime::UNIX_EPOCH, + ) + .expect("expected valid signed version metadata"); + } +} diff --git a/mullvad-update/src/fetch.rs b/mullvad-update/src/fetch.rs new file mode 100644 index 0000000000..1093915a7a --- /dev/null +++ b/mullvad-update/src/fetch.rs @@ -0,0 +1,496 @@ +//! A downloader that supports range requests and resuming downloads + +use std::{ + path::Path, + pin::Pin, + task::{ready, Poll}, +}; + +use reqwest::header::{HeaderValue, CONTENT_LENGTH, RANGE}; +use tokio::{ + fs::{self, File}, + io::{self, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt, BufWriter}, +}; + +use anyhow::Context; + +/// Receiver of the current progress so far +pub trait ProgressUpdater: Send + 'static { + /// Progress so far + fn set_progress(&mut self, fraction_complete: f32); + + /// URL that is being downloaded + fn set_url(&mut self, url: &str); +} + +// TODO: save file to protected dir so it cannot be tampered with after verification + +/// This describes how to handle files that do not match an expected size +#[derive(Debug, Clone, Copy)] +pub enum SizeHint { + /// Fail if the resulting file does not exactly match the expected size. + Exact(usize), + /// Fail if the resulting file is larger than the specified limit. + Maximum(usize), +} + +/// Download `url` to `file`. If the file already exists, this appends to it, as long +/// as the file pointed to by `url` is larger than it. +/// +/// # Arguments +/// - `progress_updater` - This interface is notified of download progress. +/// - `size_hint` - File size restrictions. +pub async fn get_to_file( + file: impl AsRef<Path>, + url: &str, + progress_updater: &mut impl ProgressUpdater, + size_hint: SizeHint, +) -> anyhow::Result<()> { + let file = create_or_append(file).await?; + let file = BufWriter::new(file); + get_to_writer(file, url, progress_updater, size_hint).await +} + +/// Download `url` to `writer`. +/// +/// # Arguments +/// - `progress_updater` - This interface is notified of download progress. +/// - `size_hint` - File size restrictions. +pub async fn get_to_writer( + mut writer: impl AsyncWrite + AsyncSeek + Unpin, + url: &str, + progress_updater: &mut impl ProgressUpdater, + size_hint: SizeHint, +) -> anyhow::Result<()> { + let client = reqwest::Client::new(); + + progress_updater.set_url(url); + progress_updater.set_progress(0.); + + // Fetch content length first + let response = client.head(url).send().await.context("HEAD failed")?; + if !response.status().is_success() { + return response + .error_for_status() + .map(|_| ()) + .context("Download failed"); + } + + let total_size = response + .headers() + .get(CONTENT_LENGTH) + .context("Missing file size")?; + let total_size: usize = total_size.to_str()?.parse().context("invalid size")?; + check_size_hint(size_hint, total_size)?; + + let already_fetched_bytes = writer + .stream_position() + .await + .context("failed to get existing file size")? + .try_into() + .context("invalid size")?; + + if total_size == already_fetched_bytes { + progress_updater.set_progress(1.); + return Ok(()); + } + if already_fetched_bytes > total_size { + anyhow::bail!("Found existing file that was larger"); + } + + // Fetch content, one range at a time + let mut writer = WriterWithProgress { + writer, + progress_updater, + written_nbytes: already_fetched_bytes, + total_nbytes: total_size, + }; + + for range in RangeIter::new(already_fetched_bytes, total_size) { + let mut response = client + .get(url) + .header(RANGE, range) + .send() + .await + .context("Failed to retrieve range")?; + let status = response.status(); + if !status.is_success() { + return response + .error_for_status() + .map(|_| ()) + .context("Download failed"); + } + + let mut bytes_read = 0; + + while let Some(chunk) = response.chunk().await.context("Failed to read chunk")? { + bytes_read += chunk.len(); + if bytes_read > RangeIter::CHUNK_SIZE { + // Protect against servers responding with more data than expected + anyhow::bail!("Server returned more than chunk-sized bytes"); + } + + writer + .write_all(&chunk) + .await + .context("Failed to write chunk")?; + } + } + + writer.shutdown().await.context("Failed to flush")?; + + Ok(()) +} + +/// This function succeeds if `actual` is allowed according to the [SizeHint]. Otherwise, it +/// returns an error. +fn check_size_hint(hint: SizeHint, actual: usize) -> anyhow::Result<()> { + match hint { + SizeHint::Exact(expected) if actual != expected => { + anyhow::bail!("File size mismatch: expected {expected} bytes, served {actual}") + } + SizeHint::Maximum(limit) if actual > limit => { + anyhow::bail!( + "File size exceeds limit: expected at most {limit} bytes, served {actual}" + ) + } + _ => Ok(()), + } +} + +/// If a file exists, append to it. Otherwise, create a new file +async fn create_or_append(path: impl AsRef<Path>) -> io::Result<File> { + match fs::File::create_new(&path).await { + // New file created + Ok(file) => Ok(file), + // Append to an existing file + Err(_err) => { + let mut file = fs::OpenOptions::new().append(true).open(path).await?; + // Seek to end, or else the seek position might be wrong + file.seek(io::SeekFrom::End(0)).await?; + Ok(file) + } + } +} + +/// Used to download partial content +struct RangeIter { + current: usize, + end: usize, +} + +impl RangeIter { + /// Number of bytes to read per range request + pub const CHUNK_SIZE: usize = 512 * 1024; + + fn new(current: usize, end: usize) -> Self { + Self { current, end } + } +} + +impl Iterator for RangeIter { + type Item = HeaderValue; + + fn next(&mut self) -> Option<Self::Item> { + if self.current > self.end { + return None; + } + let prev = self.current; + + let read_n = self.end.saturating_sub(self.current).min(Self::CHUNK_SIZE); + if read_n == 0 { + return None; + } + + self.current += read_n; + + // NOTE: Subtracting 1 because range includes final byte + let end = self.current - 1; + + Some(HeaderValue::from_str(&format!("bytes={prev}-{end}")).expect("valid range/str")) + } +} + +struct WriterWithProgress<'a, PU: ProgressUpdater, Writer> { + writer: Writer, + progress_updater: &'a mut PU, + written_nbytes: usize, + /// Actual or estimated total number of bytes + total_nbytes: usize, +} + +impl<PU: ProgressUpdater, Writer: AsyncWrite + Unpin> AsyncWrite + for WriterWithProgress<'_, PU, Writer> +{ + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> Poll<Result<usize, io::Error>> { + let file = Pin::new(&mut self.writer); + let nbytes = ready!(file.poll_write(cx, buf))?; + + let total_nbytes = self.total_nbytes; + let total_written = self.written_nbytes + nbytes; + + self.written_nbytes = total_written; + self.progress_updater + .set_progress(total_written as f32 / total_nbytes as f32); + + Poll::Ready(Ok(nbytes)) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll<Result<(), std::io::Error>> { + Pin::new(&mut self.writer).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll<Result<(), std::io::Error>> { + Pin::new(&mut self.writer).poll_shutdown(cx) + } +} + +#[cfg(test)] +mod test { + use std::io::Cursor; + + use async_tempfile::TempDir; + use rand::RngCore; + use tokio::{fs, io::AsyncWriteExt}; + + use super::*; + + #[tokio::test] + async fn test_create_or_append() -> anyhow::Result<()> { + let temp_dir = TempDir::new().await?; + let file_path = temp_dir.join("test"); + + // Write to a new file + const CONTENT: &[u8] = b"very important file"; + + let mut file = create_or_append(&file_path).await?; + file.write_all(CONTENT).await?; + file.flush().await?; + drop(file); + + assert_eq!(fs::read(&file_path).await?, CONTENT); + + // Verify that we can trust the stream position + let mut file = create_or_append(&file_path).await?; + let content_len: u64 = CONTENT.len().try_into()?; + assert_eq!(file.stream_position().await?, content_len); + drop(file); + + // Append some more stuff + const EXTRA: &[u8] = b"my addition"; + + let mut file = create_or_append(&file_path).await?; + file.write_all(EXTRA).await?; + file.flush().await?; + drop(file); + + // Append occurred correctly + const COMPLETE_STRING: &[u8] = b"very important filemy addition"; + assert_eq!(fs::read(file_path).await?, COMPLETE_STRING); + + Ok(()) + } + + #[derive(Default)] + struct FakeProgressUpdater { + complete: f32, + url: String, + } + + impl ProgressUpdater for FakeProgressUpdater { + fn set_progress(&mut self, fraction_complete: f32) { + self.complete = fraction_complete; + } + + fn set_url(&mut self, url: &str) { + self.url = url.to_owned(); + } + } + + /// Test that [get_to_writer] correctly downloads new files + #[tokio::test] + async fn test_fetch_complete() -> anyhow::Result<()> { + // Generate random data + let file_data = Box::leak(Box::new(vec![0u8; 1024 * 1024 + 1])); + rand::thread_rng().fill_bytes(file_data); + + // Start server + let mut server = mockito::Server::new_async().await; + let file_url = format!("{}/my_file", server.url()); + add_file_server_mock(&mut server, "/my_file", file_data); + + // Download the file to `writer` and compare it to `file_data` + let mut writer = Cursor::new(vec![]); + let mut progress_updater = FakeProgressUpdater::default(); + + get_to_writer( + &mut writer, + &file_url, + &mut progress_updater, + SizeHint::Exact(file_data.len()), + ) + .await + .context("Complete download failed")?; + + assert_eq!(progress_updater.url, file_url); + assert_eq!(progress_updater.complete, 1.); + assert_eq!(&mut writer.into_inner(), file_data); + + Ok(()) + } + + /// Test that [get_to_writer] correctly downloads partial files + #[tokio::test] + async fn test_fetch_interrupted() -> anyhow::Result<()> { + // Generate random data + let file_data = Box::leak(Box::new(vec![0u8; 1024 * 1024])); + rand::thread_rng().fill_bytes(file_data); + + // Start server + let mut server = mockito::Server::new_async().await; + let file_url = format!("{}/my_file", server.url()); + add_file_server_mock(&mut server, "/my_file", file_data); + + // Interrupt after exactly half the file has been downloaded + let mut limited_buffer = vec![0u8; file_data.len() / 2].into_boxed_slice(); + let mut writer = Cursor::new(&mut limited_buffer[..]); + + let mut progress_updater = FakeProgressUpdater::default(); + + get_to_writer( + &mut writer, + &file_url, + &mut progress_updater, + SizeHint::Exact(file_data.len()), + ) + .await + .expect_err("Expected interrupted download"); + + assert_eq!(progress_updater.url, file_url); + + let completed = progress_updater.complete; + assert!( + (completed - 0.5).abs() < f32::EPSILON, + "expected half to be completed, got {completed}" + ); + + assert_eq!( + &*limited_buffer, + &file_data[..limited_buffer.len()], + "partial download incorrect" + ); + + // Download the remainder + let writer = limited_buffer.into_vec(); + let partial_len = writer.len(); + let mut writer = Cursor::new(writer); + writer.set_position(partial_len as u64); + + let mut progress_updater = FakeProgressUpdater::default(); + + get_to_writer( + &mut writer, + &file_url, + &mut progress_updater, + SizeHint::Exact(file_data.len()), + ) + .await + .context("Partial download failed")?; + + assert_eq!(progress_updater.url, file_url); + assert_eq!(progress_updater.complete, 1.); + assert_eq!(&mut writer.into_inner(), file_data); + + Ok(()) + } + + /// Create endpoints that serve a file at `url_path` using range requests + fn add_file_server_mock(server: &mut mockito::Server, url_path: &str, data: &'static [u8]) { + // Respond to head requests with file size + server + .mock("HEAD", url_path) + .with_header(CONTENT_LENGTH, &data.len().to_string()) + .create(); + + // Respond to range requests with file + server + .mock("GET", url_path) + .with_body_from_request(|request| { + let range = request.header(RANGE); + let range = range[0].to_str().expect("expected str"); + let (begin, end) = parse_http_range(range).expect("invalid range"); + + data[begin..=end].to_vec() + }) + .create(); + } + + /// Parse a range header value, e.g. "bytes=0-31" + fn parse_http_range(val: &str) -> anyhow::Result<(usize, usize)> { + // parse: bytes=0-31 + let (_, val) = val.split_once('=').context("invalid range header")?; + let (begin, end) = val.split_once('-').context("invalid range")?; + + let begin: usize = begin.parse().context("invalid range begin")?; + let end: usize = end.parse().context("invalid range end")?; + + Ok((begin, end)) + } + + /// Make sure unexpectedly large files are rejected + #[tokio::test] + async fn test_nefarious_sizes() -> anyhow::Result<()> { + // Head length is too large + let mut server = mockito::Server::new_async().await; + let file_url = format!("{}/my_file", server.url()); + server + .mock("HEAD", "/my_file") + .with_header(CONTENT_LENGTH, "2") + .create(); + + get_to_writer( + Cursor::new(vec![]), + &file_url, + &mut FakeProgressUpdater::default(), + SizeHint::Exact(1), + ) + .await + .expect_err("Reject unexpected content length"); + + // Malicious range response + // Serve the entire file rather than the requested range + let file_data = vec![0u8; 2 * RangeIter::CHUNK_SIZE]; + + let mut server = mockito::Server::new_async().await; + let file_url = format!("{}/my_file", server.url()); + server + .mock("HEAD", "/my_file") + .with_header(CONTENT_LENGTH, &file_data.len().to_string()) + .create(); + server + .mock("GET", "/my_file") + .with_body(&file_data) + .create(); + + get_to_writer( + Cursor::new(vec![]), + &file_url, + &mut FakeProgressUpdater::default(), + SizeHint::Exact(file_data.len()), + ) + .await + .expect_err("Reject unexpected chunk sizes"); + + Ok(()) + } +} diff --git a/mullvad-update/src/lib.rs b/mullvad-update/src/lib.rs new file mode 100644 index 0000000000..08069710e0 --- /dev/null +++ b/mullvad-update/src/lib.rs @@ -0,0 +1,7 @@ +//! Support functions for securely installing or updating Mullvad VPN + +pub mod api; +pub mod app; +mod deserializer; +pub mod fetch; +pub mod verify; diff --git a/mullvad-update/src/verify.rs b/mullvad-update/src/verify.rs new file mode 100644 index 0000000000..74d8790c4b --- /dev/null +++ b/mullvad-update/src/verify.rs @@ -0,0 +1,72 @@ +use std::{fs::File, io::BufReader, path::Path}; + +use anyhow::Context; +use pgp::{ + armor, + packet::{Packet, PacketParser}, + types::PublicKeyTrait, + Deserializable, SignedPublicKey, +}; + +/// A verifier of digital file signatures +pub trait AppVerifier: 'static + Clone { + /// Verify `bin_path` using the signature at `sig_path`, and return an error if this fails for + /// any reason. + fn verify(bin_path: impl AsRef<Path>, sig_path: impl AsRef<Path>) -> anyhow::Result<()>; +} + +/// Verification using pgp +#[derive(Clone)] +pub struct PgpVerifier; + +impl PgpVerifier { + const SIGNING_PUBKEY: &[u8] = include_bytes!("../mullvad-code-signing.gpg"); +} + +impl AppVerifier for PgpVerifier { + fn verify(bin_path: impl AsRef<Path>, sig_path: impl AsRef<Path>) -> anyhow::Result<()> { + let pubkey = SignedPublicKey::from_bytes(Self::SIGNING_PUBKEY)?; + + let sig_reader = BufReader::new(File::open(sig_path).context("Open signature file")?); + let signature = PacketParser::new(armor::Dearmor::new(sig_reader)) + .find_map(|packet| { + if let Ok(Packet::Signature(sig)) = packet { + Some(sig) + } else { + None + } + }) + .context("Missing signature")?; + let issuer = signature + .issuer() + .into_iter() + .next() + .context("Find issuer key ID")?; + + // Find subkey used for signing + let subkey = pubkey + .public_subkeys + .iter() + .find(|subkey| &subkey.key_id() == issuer) + .context("Find signing subkey")?; + //subkey.verify(&pubkey)?; + + let bin = BufReader::with_capacity(1024 * 1024, File::open(bin_path)?); + + signature + .verify(subkey, bin) + .context("Verification failed")?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_pgp_signing_pubkey() { + SignedPublicKey::from_bytes(PgpVerifier::SIGNING_PUBKEY).unwrap(); + } +} diff --git a/mullvad-update/test-version-response.json b/mullvad-update/test-version-response.json new file mode 100644 index 0000000000..1dfd397706 --- /dev/null +++ b/mullvad-update/test-version-response.json @@ -0,0 +1,95 @@ +{ + "signature": { + "keyid": "AEC24A08466F3D6A1EDCDB2AD3C234428AB9D991B6BEA7F53CB9F172E6CB40D8", + "sig": "d68ba75006ea3ac249e56849022a7d93603effe26ec0385bac42cf6675fc6e31322cae018a60428d5c670baedd46b59fa2b35a412f1ed285256c64dbafbcb905" + }, + "signed": { + "expires": "2025-07-02T15:33:00Z", + "stable": { + "current": { + "version": "2025.1", + "changelog": "[macos] Adding support for quicfuscator\n[windows] Less bugs", + "installers": { + "x86": { + "urls": [ + "https://appcdn.mullvad.net/desktop/2025.1/MullvadVPN-2025.1-x64.exe" + ], + "size": 123456789, + "sha256": "3b2b1cdcfbab2c87e392f5b31e1fed63b1111027dae71bbfaf4bcef2998c18bc" + }, + "arm64": { + "urls": [ + "https://appcdn.mullvad.net/desktop/2025.1/MullvadVPN-2025.1-x64.exe" + ], + "size": 123456789, + "sha256": "3b2b1cdcfbab2c87e392f5b31e1fed63b1111027dae71bbfaf4bcef2998c18bc" + } + } + }, + "next": { + "rollout": 0.3, + "version": "2025.1", + "changelog": "[macos] Adding support for quicfuscator\n[windows] Less bugs", + "installers": { + "x86": { + "urls": [ + "https://appcdn.mullvad.net/desktop/2025.1/MullvadVPN-2025.1-x64.exe" + ], + "size": 123456789, + "sha256": "3b2b1cdcfbab2c87e392f5b31e1fed63b1111027dae71bbfaf4bcef2998c18bc" + }, + "arm64": { + "urls": [ + "https://appcdn.mullvad.net/desktop/2025.1/MullvadVPN-2025.1-x64.exe" + ], + "size": 123456789, + "sha256": "3b2b1cdcfbab2c87e392f5b31e1fed63b1111027dae71bbfaf4bcef2998c18bc" + } + } + } + }, + "beta": { + "current": { + "version": "2025.1-beta1", + "changelog": "[macos] Adding support for quicfuscator\n[windows] Less bugs", + "installers": { + "x86": { + "urls": [ + "https://appcdn.mullvad.net/desktop/2025.1-beta1/MullvadVPN-2025.1-beta1-x64.exe" + ], + "size": 123456789, + "sha256": "3b2b1cdcfbab2c87e392f5b31e1fed63b1111027dae71bbfaf4bcef2998c18bc" + }, + "arm64": { + "urls": [ + "https://appcdn.mullvad.net/desktop/2025.1-beta1/MullvadVPN-2025.1-beta1-x64.exe" + ], + "size": 123456789, + "sha256": "3b2b1cdcfbab2c87e392f5b31e1fed63b1111027dae71bbfaf4bcef2998c18bc" + } + } + }, + "next": { + "rollout": 0.3, + "version": "2025.1-beta1", + "changelog": "[macos] Adding support for quicfuscator\n[windows] Less bugs", + "installers": { + "x86": { + "urls": [ + "https://appcdn.mullvad.net/desktop/2025.1-beta1/MullvadVPN-2025.1-beta1-x64.exe" + ], + "size": 123456789, + "sha256": "3b2b1cdcfbab2c87e392f5b31e1fed63b1111027dae71bbfaf4bcef2998c18bc" + }, + "arm64": { + "urls": [ + "https://appcdn.mullvad.net/desktop/2025.1-beta1/MullvadVPN-2025.1-beta1-x64.exe" + ], + "size": 123456789, + "sha256": "3b2b1cdcfbab2c87e392f5b31e1fed63b1111027dae71bbfaf4bcef2998c18bc" + } + } + } + } + } +}
\ No newline at end of file |
