diff --git a/Cargo.lock b/Cargo.lock index 3e1a565..ec77873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,7 +132,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -351,12 +351,6 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - [[package]] name = "as-slice" version = "0.2.1" @@ -384,7 +378,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.4", + "rand 0.9.5", "raw-window-handle", "serde", "serde_repr", @@ -402,7 +396,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.4", + "rand 0.9.5", "serde", "serde_repr", "tokio", @@ -776,6 +770,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -913,18 +916,18 @@ checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "bytemuck" -version = "1.25.0" +version = "1.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.10.2" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" dependencies = [ "proc-macro2", "quote", @@ -945,9 +948,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "calendrical_calculations" @@ -995,9 +998,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", "jobserver", @@ -1073,7 +1076,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] @@ -1129,7 +1132,6 @@ dependencies = [ [[package]] name = "clipboard_macos" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "objc", "objc-foundation", @@ -1139,22 +1141,12 @@ dependencies = [ [[package]] name = "clipboard_wayland" version = "0.2.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "dnd", "mime 0.1.0", "smithay-clipboard", ] -[[package]] -name = "clipboard_x11" -version = "0.4.2" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" -dependencies = [ - "thiserror 1.0.69", - "x11rb", -] - [[package]] name = "cocoa" version = "0.25.0" @@ -1260,6 +1252,12 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b46dec724fd22199ebde05033a0cbae453bc3b1ecff11eb6a6bb3eec4b90c6a4" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation" version = "0.9.4" @@ -1333,7 +1331,6 @@ dependencies = [ [[package]] name = "cosmic-bg-config" version = "1.0.3" -source = "git+https://github.com/pop-os/cosmic-bg#71bb96b3eaad167c3cb639979d2edb7559866c2f" dependencies = [ "cosmic-config", "derive_setters", @@ -1345,7 +1342,6 @@ dependencies = [ [[package]] name = "cosmic-client-toolkit" version = "0.2.0" -source = "git+https://github.com/pop-os/cosmic-protocols//?rev=32283d7#32283d76a8d0342da74c4cc022a533c52dcf378f" dependencies = [ "bitflags 2.13.0", "cosmic-protocols", @@ -1358,7 +1354,6 @@ dependencies = [ [[package]] name = "cosmic-comp-config" version = "1.0.0" -source = "git+https://github.com/pop-os/cosmic-comp#031fdc389aaa4dac8546e0fc429bbea2295cc00d" dependencies = [ "cosmic-config", "input", @@ -1370,7 +1365,6 @@ dependencies = [ [[package]] name = "cosmic-config" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1391,7 +1385,6 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "quote", "syn 2.0.118", @@ -1405,23 +1398,9 @@ dependencies = [ "zbus", ] -[[package]] -name = "cosmic-dbus-networkmanager" -version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" -dependencies = [ - "bitflags 2.13.0", - "derive_builder", - "jiff", - "procfs", - "zbus", - "zvariant", -] - [[package]] name = "cosmic-freedesktop-icons" version = "0.4.0" -source = "git+https://github.com/pop-os/freedesktop-icons#ab4c57b8e416c6af9297cb04d101889896fd9a92" dependencies = [ "bstr", "btoi", @@ -1435,7 +1414,6 @@ dependencies = [ [[package]] name = "cosmic-idle-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-idle#c95d066b5b640509a6369634b669ca60dc50e168" dependencies = [ "cosmic-config", "serde", @@ -1457,7 +1435,6 @@ dependencies = [ [[package]] name = "cosmic-panel-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-panel#45f03a3864a9385349be3ca13202553280e667d4" dependencies = [ "anyhow", "cosmic-config", @@ -1471,7 +1448,6 @@ dependencies = [ [[package]] name = "cosmic-protocols" version = "0.2.0" -source = "git+https://github.com/pop-os/cosmic-protocols//?rev=32283d7#32283d76a8d0342da74c4cc022a533c52dcf378f" dependencies = [ "bitflags 2.13.0", "wayland-backend", @@ -1485,7 +1461,6 @@ dependencies = [ [[package]] name = "cosmic-randr" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-randr#6e8e795970fa06d434af22775e415b517f7552d3" dependencies = [ "cosmic-protocols", "indexmap 2.14.0", @@ -1499,7 +1474,6 @@ dependencies = [ [[package]] name = "cosmic-randr-shell" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-randr#6e8e795970fa06d434af22775e415b517f7552d3" dependencies = [ "kdl", "slotmap", @@ -1520,7 +1494,6 @@ dependencies = [ "cosmic-bg-config", "cosmic-comp-config", "cosmic-config", - "cosmic-dbus-networkmanager", "cosmic-idle-config", "cosmic-mime-apps", "cosmic-panel-config", @@ -1532,9 +1505,8 @@ dependencies = [ "cosmic-settings-airplane-mode-subscription", "cosmic-settings-audio-client", "cosmic-settings-bluetooth-subscription", - "cosmic-settings-config", + "cosmic-settings-config 0.1.0 (git+https://github.com/pop-os/cosmic-settings-daemon)", "cosmic-settings-daemon-config", - "cosmic-settings-network-manager-subscription", "cosmic-settings-page", "cosmic-settings-sound", "cosmic-settings-upower-subscription", @@ -1562,7 +1534,7 @@ dependencies = [ "locale1", "locales-rs", "mime 0.3.17", - "nm-secret-agent-manager", + "nmrs", "notify", "num-traits", "pwhash", @@ -1570,6 +1542,7 @@ dependencies = [ "ron 0.12.2", "rust-embed", "rustix 1.1.4", + "secret-service", "secure-string", "serde", "slab", @@ -1633,7 +1606,7 @@ dependencies = [ [[package]] name = "cosmic-settings-audio-client" version = "1.0.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#24ab5c28eb5876665752cd5e9405b7de17e2b99a" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#1af414fc6cb9080349847382a66db9fd69dbe964" dependencies = [ "cosmic-settings-audio-core", "dirs", @@ -1649,7 +1622,7 @@ dependencies = [ [[package]] name = "cosmic-settings-audio-core" version = "1.0.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#24ab5c28eb5876665752cd5e9405b7de17e2b99a" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#1af414fc6cb9080349847382a66db9fd69dbe964" dependencies = [ "dirs", "ron 0.12.2", @@ -1672,7 +1645,19 @@ dependencies = [ [[package]] name = "cosmic-settings-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#24ab5c28eb5876665752cd5e9405b7de17e2b99a" +dependencies = [ + "cosmic-config", + "ron 0.11.0", + "serde", + "serde_with", + "tracing", + "xkbcommon 0.9.0", +] + +[[package]] +name = "cosmic-settings-config" +version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#1af414fc6cb9080349847382a66db9fd69dbe964" dependencies = [ "cosmic-config", "ron 0.11.0", @@ -1684,8 +1669,7 @@ dependencies = [ [[package]] name = "cosmic-settings-daemon" -version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" +version = "0.1.1-yoda.1" dependencies = [ "zbus", ] @@ -1693,7 +1677,7 @@ dependencies = [ [[package]] name = "cosmic-settings-daemon-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#24ab5c28eb5876665752cd5e9405b7de17e2b99a" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#1af414fc6cb9080349847382a66db9fd69dbe964" dependencies = [ "cosmic-config", "serde", @@ -1711,24 +1695,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "cosmic-settings-network-manager-subscription" -version = "1.0.7" -dependencies = [ - "bitflags 2.13.0", - "cosmic-dbus-networkmanager", - "futures", - "iced_futures", - "itertools", - "nm-secret-agent-manager", - "secret-service", - "secure-string", - "thiserror 2.0.18", - "tokio", - "tracing", - "zbus", -] - [[package]] name = "cosmic-settings-page" version = "1.0.7" @@ -1788,8 +1754,6 @@ dependencies = [ [[package]] name = "cosmic-text" version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" dependencies = [ "bitflags 2.13.0", "fontdb", @@ -1807,12 +1771,12 @@ dependencies = [ "unicode-linebreak", "unicode-script", "unicode-segmentation", + "unicode-width 0.2.2", ] [[package]] name = "cosmic-theme" version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "almost", "configparser", @@ -1836,6 +1800,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1879,7 +1852,6 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "cryoglyph" version = "0.1.0" -source = "git+https://github.com/iced-rs/cryoglyph.git?rev=e429a025df36ab8145708acb309080ae3deec17a#e429a025df36ab8145708acb309080ae3deec17a" dependencies = [ "cosmic-text", "etagere", @@ -1898,6 +1870,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "crypto-mac" version = "0.10.1" @@ -1926,31 +1907,12 @@ dependencies = [ "uncased", ] -[[package]] -name = "ctor" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" -dependencies = [ - "dtor", -] - [[package]] name = "cursor-icon" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - [[package]] name = "darling" version = "0.21.3" @@ -1971,20 +1933,6 @@ dependencies = [ "darling_macro 0.23.0", ] -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.118", -] - [[package]] name = "darling_core" version = "0.21.3" @@ -2012,17 +1960,6 @@ dependencies = [ "syn 2.0.118", ] -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.118", -] - [[package]] name = "darling_macro" version = "0.21.3" @@ -2113,37 +2050,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.118", -] - [[package]] name = "derive_setters" version = "0.1.9" @@ -2183,10 +2089,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "crypto-common", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + [[package]] name = "dirs" version = "6.0.0" @@ -2243,7 +2160,6 @@ dependencies = [ [[package]] name = "dnd" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "bitflags 2.13.0", "mime 0.1.0", @@ -2276,52 +2192,6 @@ checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" [[package]] name = "dpi" version = "0.1.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" - -[[package]] -name = "drm" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f8a69e60d75ae7dab4ef26a59ca99f2a89d4c142089b537775ae0c198bdcde" -dependencies = [ - "bitflags 2.13.0", - "bytemuck", - "drm-ffi", - "drm-fourcc", - "rustix 0.38.44", -] - -[[package]] -name = "drm-ffi" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41334f8405792483e32ad05fbb9c5680ff4e84491883d2947a4757dc54cb2ac6" -dependencies = [ - "drm-sys", - "rustix 0.38.44", -] - -[[package]] -name = "drm-fourcc" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" - -[[package]] -name = "drm-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d09ff881f92f118b11105ba5e34ff8f4adf27b30dae8f12e28c193af1c83176" -dependencies = [ - "libc", - "linux-raw-sys 0.6.5", -] - -[[package]] -name = "dtor" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" [[package]] name = "dyn-clone" @@ -2446,14 +2316,16 @@ dependencies = [ [[package]] name = "exr" -version = "1.74.0" +version = "1.74.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" dependencies = [ "bit_field", "half", "lebe", "miniz_oxide", + "num-complex", + "pulp", "rayon-core", "smallvec", "zune-inflate", @@ -2829,6 +2701,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +[[package]] +name = "futures-timer" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" + [[package]] name = "futures-util" version = "0.3.32" @@ -2856,16 +2734,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.4", - "windows-link 0.2.1", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -3127,7 +2995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0" dependencies = [ "arrayvec", - "rand 0.8.6", + "rand 0.8.7", "serde", ] @@ -3179,6 +3047,15 @@ dependencies = [ "zbus", ] +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "i18n-config" version = "0.4.8" @@ -3273,7 +3150,6 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "dnd", "iced_accessibility", @@ -3294,7 +3170,6 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "accesskit", "accesskit_winit", @@ -3303,7 +3178,6 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "bitflags 2.13.0", "bytes", @@ -3329,7 +3203,6 @@ dependencies = [ [[package]] name = "iced_debug" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "iced_core", "iced_futures", @@ -3339,7 +3212,6 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "futures", "iced_core", @@ -3353,7 +3225,6 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "bitflags 2.13.0", "bytemuck", @@ -3374,7 +3245,6 @@ dependencies = [ [[package]] name = "iced_program" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "iced_graphics", "iced_runtime", @@ -3383,7 +3253,6 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -3395,7 +3264,6 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -3411,7 +3279,6 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "bytemuck", "cosmic-text", @@ -3428,9 +3295,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ - "as-raw-xcb-connection", "bitflags 2.13.0", "bytemuck", "cosmic-client-toolkit", @@ -3447,19 +3312,16 @@ dependencies = [ "rustc-hash 2.1.3", "rustix 0.38.44", "thiserror 2.0.18", - "tiny-xlib", "wayland-backend", "wayland-client", "wayland-protocols", "wayland-sys", "wgpu", - "x11rb", ] [[package]] name = "iced_widget" version = "0.14.2" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3479,7 +3341,6 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "cosmic-client-toolkit", "cursor-icon", @@ -4027,9 +3888,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd854a95a4ac672fed8c054136039fd32c22cf039ff09ead7280afe920486483" +checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" dependencies = [ "bitflags 2.13.0", "inotify-sys", @@ -4038,9 +3899,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea94e891b3606826e9c998be69ddca42247dad8ad50b1649a5cb7e1c9ae06fd" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" dependencies = [ "libc", ] @@ -4150,9 +4011,9 @@ checksum = "2ceaf4c6c48465bead8cb6a0b7c4ee0c86ecbb31239032b9c66ab9a08d2f3ee1" [[package]] name = "jiff" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" dependencies = [ "defmt", "jiff-static", @@ -4166,9 +4027,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" dependencies = [ "proc-macro2", "quote", @@ -4177,9 +4038,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6142247df1a93c2b3587402a19710be3e6e942f1581a1702e76408f2c21d6590" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" [[package]] name = "jiff-tzdb-platform" @@ -4566,8 +4427,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libcosmic" -version = "1.0.0" -source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" +version = "0.1.0-yoda.2" dependencies = [ "apply", "ashpd 0.12.3", @@ -4575,7 +4435,7 @@ dependencies = [ "cosmic-client-toolkit", "cosmic-config", "cosmic-freedesktop-icons", - "cosmic-settings-config", + "cosmic-settings-config 0.1.0", "cosmic-settings-daemon", "cosmic-theme", "css-color", @@ -4689,12 +4549,6 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" -[[package]] -name = "linux-raw-sys" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4868,9 +4722,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memmap2" @@ -4927,7 +4781,6 @@ dependencies = [ [[package]] name = "mime" version = "0.1.0" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "smithay-clipboard", ] @@ -4938,6 +4791,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime 0.3.17", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -4950,9 +4813,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", @@ -5053,11 +4916,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "nm-secret-agent-manager" -version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" +name = "nmrs" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c427203beda58ac05815b356d64a34eeb2fba180b3077d543ebb1472fc1269" dependencies = [ + "async-trait", + "base64", + "bitflags 2.13.0", + "futures", + "futures-timer", + "log", + "serde", + "thiserror 2.0.18", + "tokio", + "uuid", "zbus", + "zvariant", ] [[package]] @@ -5159,6 +5034,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ + "bytemuck", "num-traits", ] @@ -5190,11 +5066,10 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -5661,7 +5536,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -5927,27 +5802,6 @@ dependencies = [ "yansi", ] -[[package]] -name = "procfs" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" -dependencies = [ - "bitflags 2.13.0", - "procfs-core", - "rustix 1.1.4", -] - -[[package]] -name = "procfs-core" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" -dependencies = [ - "bitflags 2.13.0", - "hex", -] - [[package]] name = "profiling" version = "1.0.18" @@ -5967,6 +5821,29 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + [[package]] name = "pwhash" version = "1.0.0" @@ -5977,7 +5854,7 @@ dependencies = [ "byteorder", "hmac 0.10.1", "md-5", - "rand 0.8.6", + "rand 0.8.7", "sha-1", "sha2 0.9.9", ] @@ -6016,7 +5893,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", - "serde", ] [[package]] @@ -6042,9 +5918,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -6053,9 +5929,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -6138,7 +6014,7 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.4", + "rand 0.9.5", "rand_chacha 0.9.0", "simd_helpers", "thiserror 2.0.18", @@ -6161,6 +6037,15 @@ dependencies = [ "rgb", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.13.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -6208,6 +6093,12 @@ dependencies = [ "font-types", ] +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + [[package]] name = "redox_event" version = "0.4.8" @@ -6269,9 +6160,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", @@ -6281,9 +6172,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" dependencies = [ "aho-corasick", "memchr", @@ -6387,9 +6278,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rust-embed" -version = "8.11.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -6398,10 +6289,11 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.11.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" dependencies = [ + "mime_guess", "proc-macro2", "quote", "rust-embed-utils", @@ -6411,19 +6303,19 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.11.0" +version = "8.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" dependencies = [ - "sha2 0.10.9", + "sha2 0.11.0", "walkdir", ] [[package]] name = "rustc-demangle" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" [[package]] name = "rustc-hash" @@ -6474,9 +6366,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "rustybuzz" @@ -6717,11 +6609,17 @@ checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.9.9" @@ -6730,7 +6628,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -6742,10 +6640,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -6897,7 +6806,6 @@ dependencies = [ [[package]] name = "smithay-clipboard" version = "0.8.0" -source = "git+https://github.com/pop-os/smithay-clipboard?tag=sctk-0.20#859b02c88f45c554049a67c6ddeec1692ce0e20b" dependencies = [ "libc", "raw-window-handle", @@ -6917,9 +6825,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -6928,14 +6836,11 @@ dependencies = [ [[package]] name = "softbuffer" version = "0.4.1" -source = "git+https://github.com/pop-os/softbuffer?tag=cosmic-4.0#c2b2c19ddb38ff17495643699f97cb1f2064a1be" dependencies = [ - "as-raw-xcb-connection", "bytemuck", "cfg_aliases 0.2.1", "cocoa", "core-graphics", - "drm", "fastrand", "foreign-types", "js-sys", @@ -6945,14 +6850,12 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.5.18", "rustix 0.38.44", - "tiny-xlib", "wasm-bindgen", "wayland-backend", "wayland-client", "wayland-sys", "web-sys", "windows-sys 0.52.0", - "x11rb", ] [[package]] @@ -7218,9 +7121,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" dependencies = [ "cfg-if", ] @@ -7289,19 +7192,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tiny-xlib" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" -dependencies = [ - "as-raw-xcb-connection", - "ctor", - "libloading", - "pkg-config", - "tracing", -] - [[package]] name = "tinystr" version = "0.8.3" @@ -7315,9 +7205,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ "tinyvec_macros", ] @@ -7402,7 +7292,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -7423,7 +7313,7 @@ dependencies = [ "indexmap 2.14.0", "toml_datetime", "toml_parser", - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -7432,7 +7322,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.3", + "winnow 1.0.4", ] [[package]] @@ -7742,12 +7632,14 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.4" +version = "1.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" dependencies = [ + "getrandom 0.4.3", "js-sys", "serde_core", + "sha1_smol", "wasm-bindgen", ] @@ -8243,12 +8135,10 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "window_clipboard" version = "0.4.1" -source = "git+https://github.com/pop-os/window_clipboard.git?tag=sctk-0.20#f68595ee0e62fbd6589f4709b5aaa5c3c7ea5f6c" dependencies = [ "clipboard-win", "clipboard_macos", "clipboard_wayland", - "clipboard_x11", "dnd", "mime 0.1.0", "raw-window-handle", @@ -8688,7 +8578,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "bitflags 2.13.0", "cfg_aliases 0.2.1", @@ -8708,13 +8597,11 @@ dependencies = [ "winit-wayland", "winit-web", "winit-win32", - "winit-x11", ] [[package]] name = "winit-android" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "android-activity", "bitflags 2.13.0", @@ -8729,7 +8616,6 @@ dependencies = [ [[package]] name = "winit-appkit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "bitflags 2.13.0", "block2 0.6.2", @@ -8751,7 +8637,6 @@ dependencies = [ [[package]] name = "winit-common" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "memmap2 0.9.11", "objc2 0.6.4", @@ -8759,14 +8644,12 @@ dependencies = [ "smol_str", "tracing", "winit-core", - "x11-dl", "xkbcommon-dl", ] [[package]] name = "winit-core" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "bitflags 2.13.0", "cursor-icon", @@ -8780,7 +8663,6 @@ dependencies = [ [[package]] name = "winit-orbital" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "bitflags 2.13.0", "dpi", @@ -8796,7 +8678,6 @@ dependencies = [ [[package]] name = "winit-uikit" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "bitflags 2.13.0", "block2 0.6.2", @@ -8816,7 +8697,6 @@ dependencies = [ [[package]] name = "winit-wayland" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "ahash", "bitflags 2.13.0", @@ -8842,7 +8722,6 @@ dependencies = [ [[package]] name = "winit-web" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "atomic-waker", "bitflags 2.13.0", @@ -8864,7 +8743,6 @@ dependencies = [ [[package]] name = "winit-win32" version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" dependencies = [ "bitflags 2.13.0", "cursor-icon", @@ -8877,29 +8755,6 @@ dependencies = [ "winit-core", ] -[[package]] -name = "winit-x11" -version = "0.31.0-beta.2" -source = "git+https://github.com/pop-os/winit.git?tag=cosmic-0.14#f737f1f5900b3399b56951305b0f155afc9c9ee5" -dependencies = [ - "bitflags 2.13.0", - "bytemuck", - "calloop", - "cursor-icon", - "dpi", - "libc", - "percent-encoding", - "raw-window-handle", - "rustix 1.1.4", - "smol_str", - "tracing", - "winit-common", - "winit-core", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - [[package]] name = "winnow" version = "0.6.24" @@ -8911,9 +8766,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] @@ -8939,39 +8794,6 @@ dependencies = [ "either", ] -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.4", - "x11rb-protocol", - "xcursor", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - [[package]] name = "xcursor" version = "0.3.10" @@ -8987,7 +8809,6 @@ checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" [[package]] name = "xdg-shell-wrapper-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-panel#45f03a3864a9385349be3ca13202553280e667d4" dependencies = [ "serde", "wayland-protocols-wlr", @@ -9140,9 +8961,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.16.0" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" dependencies = [ "async-broadcast", "async-executor", @@ -9168,7 +8989,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.3", + "winnow 1.0.4", "zbus_macros", "zbus_names", "zvariant", @@ -9200,9 +9021,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.16.0" +version = "5.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -9215,12 +9036,12 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.2" +version = "4.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" dependencies = [ "serde", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant", ] @@ -9239,12 +9060,12 @@ dependencies = [ [[package]] name = "zbus_xml" -version = "5.1.1" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8067892e940ed1727dea64690378601603b31d62dfde019a5335fbb7c0e0ed9" +checksum = "59ab0513c0a66a60a8718d5ad712a5094845a20d95b5c1c81e2bd8e904a52b4f" dependencies = [ - "quick-xml 0.39.4", "serde", + "winnow 1.0.4", "zbus_names", "zvariant", ] @@ -9257,18 +9078,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.53" +version = "0.8.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", @@ -9406,9 +9227,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zune-core" @@ -9451,24 +9272,24 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.12.0" +version = "5.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow 1.0.3", + "winnow 1.0.4", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.12.0" +version = "5.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -9479,13 +9300,17 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" dependencies = [ "proc-macro2", "quote", "serde", "syn 2.0.118", - "winnow 1.0.3", + "winnow 1.0.4", ] + +[[patch.unused]] +name = "winit-x11" +version = "0.31.0-beta.2" diff --git a/Cargo.toml b/Cargo.toml index 4c31c2d..ee9855c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,9 @@ resolver = "3" rust-version = "1.93" [workspace.dependencies] -cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" } +cosmic-randr = { path = "../cosmic-randr/lib" } tokio = { version = "1.49.0", features = ["macros"] } -iced_winit = { git = "https://github.com/pop-os/libcosmic", default-features = false } +iced_winit = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/winit", default-features = false } image = { version = "0.25.10", default-features = false, features = [ "jpeg", "png", @@ -27,29 +27,29 @@ features = [ "tokio", "qr_code", ] -git = "https://github.com/pop-os/libcosmic" +path = "/home/lionel/Projets/COSMIC/libcosmic" [workspace.dependencies.cosmic-config] -git = "https://github.com/pop-os/libcosmic" +path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-config" [workspace.dependencies.cosmic-bg-config] -git = "https://github.com/pop-os/cosmic-bg" +path = "../cosmic-bg/config" [workspace.dependencies.cosmic-comp-config] -git = "https://github.com/pop-os/cosmic-comp" +path = "../cosmic-comp/cosmic-comp-config" features = ["output"] [workspace.dependencies.cosmic-idle-config] -git = "https://github.com/pop-os/cosmic-idle" +path = "../cosmic-idle/cosmic-idle-config" [workspace.dependencies.cosmic-panel-config] -git = "https://github.com/pop-os/cosmic-panel" +path = "../cosmic-panel/cosmic-panel-config" [workspace.dependencies.cosmic-randr-shell] -git = "https://github.com/pop-os/cosmic-randr" +path = "../cosmic-randr/shell" [workspace.dependencies.sctk] -version = "0.20" +version = "1.3.0" package = "smithay-client-toolkit" [profile.dev] @@ -66,33 +66,18 @@ inherits = "release" lto = "off" debug = true -# [patch.'https://github.com/pop-os/cosmic-text'] -# cosmic-text = { git = "https://github.com/pop-os/cosmic-text//", rev = "b017d7c" } - [patch.'https://github.com/pop-os/cosmic-protocols'] -cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols//", rev = "32283d7" } -cosmic-client-toolkit = { git = "https://github.com/pop-os/cosmic-protocols//", rev = "32283d7" } +cosmic-protocols = { path = "../cosmic-protocols" } +cosmic-client-toolkit = { path = "../cosmic-protocols/client-toolkit" } -# [patch.'https://github.com/pop-os/cosmic-settings-daemon'] -# cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon//", branch = "input_nobuild" } +[patch.'https://github.com/pop-os/libcosmic'] +cosmic-config = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-config" } +cosmic-theme = { path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-theme" } +iced_futures = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/futures" } +iced_winit = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/winit" } -# For development and testing purposes -# [patch.'https://github.com/pop-os/libcosmic'] -# libcosmic = { path = "../libcosmic" } -# cosmic-config = { path = "../libcosmic/cosmic-config" } -# cosmic-theme = { path = "../libcosmic/cosmic-theme" } -# iced_futures = { path = "../libcosmic/iced/futures" } -# iced_winit = { path = "../libcosmic/iced/winit" } -# iced_futures = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" } -# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" } -# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" } -# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" } -# iced_winit = { git = "https://github.com/pop-os/libcosmic//", branch = "theme-v2" } - -# [patch.'https://github.com/pop-os/dbus-settings-bindings'] -# cosmic-dbus-networkmanager = { path = "../dbus-settings-bindings/networkmanager" } -# upower_dbus = { path = "../dbus-settings-bindings/upower" } -# nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings//", branch = "nm-secret-agent" } +[patch.'https://github.com/pop-os/cosmic-text.git'] +cosmic-text = { path = "../cosmic-text" } [patch.crates-io] atspi = { git = "https://github.com/wash2/atspi" } diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index 6bf0692..febae53 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -21,8 +21,6 @@ color-eyre = "0.6.5" cosmic-bg-config.workspace = true cosmic-comp-config = { workspace = true, optional = true } cosmic-config.workspace = true -cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } cosmic-idle-config.workspace = true cosmic-panel-config = { workspace = true, optional = true } cosmic-protocols = { git = "https://github.com/pop-os/cosmic-protocols", optional = true } @@ -34,7 +32,6 @@ cosmic-settings-accessibility-subscription = { path = "../subscriptions/accessib cosmic-settings-a11y-manager-subscription = { path = "../subscriptions/a11y-manager", optional = true } cosmic-settings-airplane-mode-subscription = { path = "../subscriptions/airplane-mode", optional = true } cosmic-settings-bluetooth-subscription = { path = "../subscriptions/bluetooth", optional = true } -cosmic-settings-network-manager-subscription = { path = "../subscriptions/network-manager", optional = true } cosmic-settings-sound = { path = "../pages/sound", optional = true } cosmic-settings-upower-subscription = { path = "../subscriptions/upower", optional = true } cosmic-settings-wallpaper = { path = "../pages/wallpapers" } @@ -57,11 +54,13 @@ locale1 = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = sysinfo = { version = "=0.38.0", optional = true } mime-apps = { package = "cosmic-mime-apps", git = "https://github.com/pop-os/cosmic-mime-apps", features = ["tokio"], optional = true } notify = "8.2.0" +nmrs = { version = "3.4.0", optional = true } regex = "1.12.3" ron = "0.12" rust-embed = "8.11.0" sctk = { workspace = true, optional = true } secure-string = "0.3.0" +secret-service = { version = "5.1.0", features = ["rt-tokio-crypto-rust"], optional = true } serde = { version = "1.0.228", features = ["derive"] } slab = "0.4.12" slotmap = "1.1.1" @@ -169,10 +168,9 @@ page-input = [ ] page-legacy-applications = ["cosmic-comp-config", "dep:cosmic-randr"] page-networking = [ - "dep:cosmic-settings-network-manager-subscription", "xdg-portal", - "dep:cosmic-dbus-networkmanager", - "dep:nm-secret-agent-manager", + "dep:nmrs", + "dep:secret-service", "dep:zbus", ] page-power = ["dep:upower_dbus", "dep:zbus"] diff --git a/cosmic-settings/src/pages/desktop/panel/inner.rs b/cosmic-settings/src/pages/desktop/panel/inner.rs index bce638f..ea54337 100644 --- a/cosmic-settings/src/pages/desktop/panel/inner.rs +++ b/cosmic-settings/src/pages/desktop/panel/inner.rs @@ -118,7 +118,7 @@ pub(crate) fn behavior_and_position< .title(§ion.title) .add( settings::item::builder(&descriptions[autohide_label]) - .toggler(panel_config.autohide.is_some(), Message::AutoHidePanel), + .toggler(panel_config.autohide_enabled(), Message::AutoHidePanel), ) .add(settings::item( &descriptions[position], @@ -542,18 +542,10 @@ impl PageInner { Message::AutoHidePanel(enabled) => { if enabled { _ = panel_config.set_exclusive_zone(helper, false); - _ = panel_config.set_autohide( - helper, - Some(AutoHide { - wait_time: 1000, - transition_time: 200, - handle_size: 4, - unhide_delay: 200, - }), - ); + _ = panel_config.set_autohide(helper, AutoHide::OnOverlap); } else { _ = panel_config.set_exclusive_zone(helper, true); - _ = panel_config.set_autohide(helper, None); + _ = panel_config.set_autohide(helper, AutoHide::Never); } } Message::PanelAnchor(i) => { diff --git a/cosmic-settings/src/pages/display/mod.rs b/cosmic-settings/src/pages/display/mod.rs index eba2ee7..95dddf7 100644 --- a/cosmic-settings/src/pages/display/mod.rs +++ b/cosmic-settings/src/pages/display/mod.rs @@ -913,8 +913,8 @@ impl Page { Some(orientation) => self.set_dialog( Randr::Transform(match orientation { 1 => Transform::Rotate90, - 2 => Transform::Flipped180, - 3 => Transform::Flipped270, + 2 => Transform::Rotate180, + 3 => Transform::Rotate270, _ => Transform::Normal, }), &request, diff --git a/cosmic-settings/src/pages/networking/backend.rs b/cosmic-settings/src/pages/networking/backend.rs new file mode 100644 index 0000000..eec2e4e --- /dev/null +++ b/cosmic-settings/src/pages/networking/backend.rs @@ -0,0 +1,1812 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: GPL-3.0-only + +use std::collections::{BTreeMap, HashMap}; +use std::fmt; +use std::net::Ipv4Addr; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; + +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; +use futures::{FutureExt, SinkExt, StreamExt}; +use nmrs::agent::{SecretAgent, SecretAgentFlags, SecretRequest, SecretResponder, SecretSetting}; +use nmrs::raw::zbus; +use nmrs::raw::zvariant::{OwnedObjectPath, OwnedValue, Str, Value}; +use nmrs::{ + ActiveConnection, ConnectType, ConnectionOptions, EapOptions, NetworkEvent, NetworkManager, + SavedConnection, SettingsSummary, WifiKeyMgmt, WifiSecurity, +}; +use secure_string::SecureString; +use tokio::sync::oneshot; + +pub type SSID = Arc; +pub type UUID = Arc; + +const NM_DEST: &str = "org.freedesktop.NetworkManager"; +const NM_PATH: &str = "/org/freedesktop/NetworkManager"; +const NM_IFACE: &str = "org.freedesktop.NetworkManager"; +const SECRET_ID: &str = "com.system76.CosmicSettings.NetworkManager"; + +#[derive(Debug)] +pub enum Error { + Message(String), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Message(message) => f.write_str(message), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(value: nmrs::ConnectionError) -> Self { + Self::Message(value.to_string()) + } +} + +impl From for Error { + fn from(value: zbus::Error) -> Self { + Self::Message(value.to_string()) + } +} + +pub mod hw_address { + #[derive(Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)] + pub struct HwAddress { + octets: Vec, + } + + impl HwAddress { + pub fn from_str(arg: &str) -> Option { + let segments: Vec<&str> = arg.split(':').collect(); + if segments.len() != 6 && segments.len() != 8 { + return None; + } + + let mut octets = Vec::with_capacity(segments.len()); + for segment in segments { + if segment.len() != 2 { + return None; + } + octets.push(u8::from_str_radix(segment, 16).ok()?); + } + + Some(Self { octets }) + } + } + + impl std::fmt::Display for HwAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let hex_parts: Vec = self + .octets + .iter() + .map(|byte| format!("{byte:02x}")) + .collect(); + write!(f, "{}", hex_parts.join(":")) + } + } +} + +pub mod available_wifi { + use std::sync::Arc; + + use nmrs::raw::zvariant::OwnedObjectPath; + + use super::devices::DeviceState; + use super::hw_address::HwAddress; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct AccessPoint { + pub ssid: Arc, + pub strength: u8, + pub state: DeviceState, + pub working: bool, + pub path: OwnedObjectPath, + pub hw_address: HwAddress, + pub secured: bool, + pub wps_push: bool, + pub network_type: NetworkType, + pub interface: Option, + pub bssid: Option, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum NetworkType { + Open, + PskOrSae, + Sae, + EAP, + } +} + +pub mod current_networks { + use std::net::Ipv4Addr; + + use super::devices::ActiveConnectionState; + + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum ActiveConnectionInfo { + Wired { + name: String, + hw_address: String, + speed: u32, + ip_addresses: Vec, + }, + WiFi { + name: String, + ip_addresses: Vec, + hw_address: String, + state: ActiveConnectionState, + strength: u8, + }, + Vpn { + name: String, + ip_addresses: Vec, + }, + } + + impl ActiveConnectionInfo { + pub fn name(&self) -> String { + match self { + Self::Wired { name, .. } | Self::WiFi { name, .. } | Self::Vpn { name, .. } => { + name.clone() + } + } + } + } +} + +pub mod devices { + use std::sync::Arc; + + use nmrs::raw::zvariant::OwnedObjectPath; + use nmrs::{NetworkManager, SavedConnection, SettingsSummary}; + + use super::{Error, active_connection_by_interface, owned_path, saved_matches_device}; + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum DeviceType { + Ethernet, + Wifi, + WifiP2P, + Loopback, + Bluetooth, + Vlan, + WireGuard, + Other(u32), + } + + impl From for DeviceType { + fn from(value: nmrs::DeviceType) -> Self { + match value { + nmrs::DeviceType::Ethernet => Self::Ethernet, + nmrs::DeviceType::Wifi => Self::Wifi, + nmrs::DeviceType::WifiP2P => Self::WifiP2P, + nmrs::DeviceType::Loopback => Self::Loopback, + nmrs::DeviceType::Bluetooth => Self::Bluetooth, + nmrs::DeviceType::Vlan => Self::Vlan, + nmrs::DeviceType::Other(v) => Self::Other(v), + _ => Self::Other(0), + } + } + } + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum DeviceState { + Unknown, + Unmanaged, + Unavailable, + Disconnected, + Prepare, + Config, + NeedAuth, + IpConfig, + IpCheck, + Secondaries, + Activated, + Deactivating, + Failed, + Other(u32), + } + + impl From for DeviceState { + fn from(value: nmrs::DeviceState) -> Self { + match value { + nmrs::DeviceState::Unmanaged => Self::Unmanaged, + nmrs::DeviceState::Unavailable => Self::Unavailable, + nmrs::DeviceState::Disconnected => Self::Disconnected, + nmrs::DeviceState::Prepare => Self::Prepare, + nmrs::DeviceState::Config => Self::Config, + nmrs::DeviceState::NeedAuth => Self::NeedAuth, + nmrs::DeviceState::IpConfig => Self::IpConfig, + nmrs::DeviceState::IpCheck => Self::IpCheck, + nmrs::DeviceState::Secondaries => Self::Secondaries, + nmrs::DeviceState::Activated => Self::Activated, + nmrs::DeviceState::Deactivating => Self::Deactivating, + nmrs::DeviceState::Failed => Self::Failed, + nmrs::DeviceState::Other(0) => Self::Unknown, + nmrs::DeviceState::Other(v) => Self::Other(v), + _ => Self::Unknown, + } + } + } + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum ActiveConnectionState { + Unknown, + Activating, + Activated, + Deactivating, + Deactivated, + Other(u32), + } + + impl From for ActiveConnectionState { + fn from(value: nmrs::ActiveConnectionState) -> Self { + match value { + nmrs::ActiveConnectionState::Activating => Self::Activating, + nmrs::ActiveConnectionState::Activated => Self::Activated, + nmrs::ActiveConnectionState::Deactivating => Self::Deactivating, + nmrs::ActiveConnectionState::Deactivated => Self::Deactivated, + nmrs::ActiveConnectionState::Unknown => Self::Unknown, + nmrs::ActiveConnectionState::Other(v) => Self::Other(v), + _ => Self::Unknown, + } + } + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct DeviceInfo { + pub path: OwnedObjectPath, + pub device_type: DeviceType, + pub interface: String, + pub state: DeviceState, + pub active_connection: Option<(DeviceConnection, ActiveConnectionState)>, + pub available_connections: Vec, + pub known_connections: Vec, + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct DeviceConnection { + pub path: OwnedObjectPath, + pub id: String, + pub uuid: Arc, + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct KnownDeviceConnection { + pub id: String, + pub uuid: Arc, + } + + pub async fn list( + nm: &NetworkManager, + device_type_filter: fn(DeviceType) -> bool, + ) -> Result, Error> { + let (devices_res, saved_res, active_res) = futures::join!( + nm.list_devices(), + nm.list_saved_connections(), + nm.list_active_connections() + ); + + let saved = saved_res?; + let active = active_res.unwrap_or_default(); + let mut out = Vec::new(); + + for device in devices_res? { + let device_type = DeviceType::from(device.device_type); + if !device_type_filter(device_type) { + continue; + } + + let Ok(path) = owned_path(&device.path) else { + continue; + }; + + let matching_saved: Vec<&SavedConnection> = saved + .iter() + .filter(|connection| { + saved_matches_device(connection, device_type, &device.interface) + }) + .collect(); + + let available_connections = matching_saved + .iter() + .map(|connection| DeviceConnection { + path: connection.path.clone(), + id: connection.id.clone(), + uuid: Arc::from(connection.uuid.as_str()), + }) + .collect(); + + let known_connections = matching_saved + .iter() + .map(|connection| KnownDeviceConnection { + id: connection.id.clone(), + uuid: Arc::from(connection.uuid.as_str()), + }) + .collect(); + + let active_connection = + active_connection_by_interface(&active, &device.interface).map(|connection| { + ( + DeviceConnection { + path: connection.path.clone(), + id: connection.id.clone(), + uuid: Arc::from(connection.uuid.as_str()), + }, + connection.state, + ) + }); + + out.push(DeviceInfo { + path, + device_type, + interface: device.interface, + state: DeviceState::from(device.state), + active_connection, + available_connections, + known_connections, + }); + } + + Ok(out) + } + + pub fn is_wifi_summary(summary: &SettingsSummary) -> bool { + matches!(summary, SettingsSummary::Wifi { .. }) + } +} + +#[derive(Debug, Clone)] +pub struct ActiveConnectionRecord { + path: OwnedObjectPath, + id: String, + uuid: String, + state: devices::ActiveConnectionState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NetworkManagerState { + pub wireless_access_points: Vec, + pub active_conns: Vec, + pub known_access_points: Vec, + pub wifi_enabled: bool, + pub airplane_mode: bool, +} + +impl Default for NetworkManagerState { + fn default() -> Self { + Self { + wireless_access_points: Vec::new(), + active_conns: Vec::new(), + known_access_points: Vec::new(), + wifi_enabled: false, + airplane_mode: false, + } + } +} + +impl NetworkManagerState { + pub async fn new(nm: &NetworkManager) -> Result { + let (wifi_state, airplane_mode, active_conns, access_points, saved_connections) = futures::join!( + nm.wifi_state(), + nm.airplane_mode_state(), + nm.list_active_connections(), + nm.list_access_points(None), + nm.list_saved_connections() + ); + + let active_conns = active_conns.unwrap_or_default(); + let saved_connections = saved_connections.unwrap_or_default(); + + let mut wireless_access_points = access_points + .unwrap_or_default() + .into_iter() + .filter(|ap| !ap.ssid.is_empty() && ap.ssid != "") + .map(access_point_from_nmrs) + .fold( + BTreeMap::<(String, SSID), available_wifi::AccessPoint>::new(), + |mut acc, ap| { + let key = (ap.interface.clone().unwrap_or_default(), ap.ssid.clone()); + let replace = acc + .get(&key) + .is_none_or(|existing| existing.strength < ap.strength); + if replace { + acc.insert(key, ap); + } + acc + }, + ) + .into_values() + .collect::>(); + wireless_access_points.sort_by(|a, b| b.strength.cmp(&a.strength)); + + let active_infos = active_connection_infos(active_conns); + let saved_wifi = saved_wifi_profiles(&saved_connections); + + let mut known_access_points = Vec::new(); + for saved in saved_wifi { + let Some((ssid, network_type)) = saved_wifi_identity(saved) else { + continue; + }; + if active_infos.iter().any(|active| active.name() == ssid) { + continue; + } + if let Some(visible) = wireless_access_points + .iter() + .find(|ap| ap.ssid.as_ref() == ssid) + .cloned() + { + known_access_points.push(visible); + continue; + } + + known_access_points.push(available_wifi::AccessPoint { + ssid: Arc::from(ssid), + strength: 0, + state: devices::DeviceState::Unknown, + working: false, + path: slash_path(), + hw_address: hw_address::HwAddress::default(), + secured: !matches!(network_type, available_wifi::NetworkType::Open), + wps_push: false, + network_type, + interface: saved.interface_name.clone(), + bssid: None, + }); + } + + known_access_points.sort_by(|a, b| a.ssid.cmp(&b.ssid)); + known_access_points.dedup_by(|a, b| a.ssid == b.ssid); + + Ok(Self { + wireless_access_points, + active_conns: active_infos, + known_access_points, + wifi_enabled: wifi_state.map(|state| state.enabled).unwrap_or(false), + airplane_mode: airplane_mode + .map(|state| state.is_airplane_mode()) + .unwrap_or(false), + }) + } + + #[allow(dead_code)] + pub fn clear(&mut self) { + self.active_conns = Vec::new(); + self.known_access_points = Vec::new(); + self.wireless_access_points = Vec::new(); + } +} + +#[derive(Debug, Clone)] +pub enum Request { + Activate(OwnedObjectPath, OwnedObjectPath), + ActivateVpn(UUID), + Deactivate(UUID), + Disconnect(SSID), + Forget(SSID), + Authenticate { + ssid: String, + identity: Option, + password: SecureString, + network_type: available_wifi::NetworkType, + secret_tx: Option>, + interface: Option, + }, + GetWiFiCredentials( + SSID, + UUID, + available_wifi::NetworkType, + Option>, + ), + Reload, + Remove(UUID), + SelectAccessPoint( + SSID, + available_wifi::NetworkType, + Option>, + Option, + ), + SetAirplaneMode(bool), + SetWiFi(bool), +} + +#[derive(Debug, Clone)] +pub enum Event { + RequestResponse { + req: Request, + state: NetworkManagerState, + success: bool, + }, + Init { + conn: NetworkManager, + sender: UnboundedSender, + state: NetworkManagerState, + }, + Devices, + WiFiEnabled(bool), + WirelessAccessPoints, + ActiveConns, + WiFiCredentials { + ssid: SSID, + password: Option, + security_type: available_wifi::NetworkType, + }, +} + +pub async fn watch(nm: NetworkManager, mut output: futures::channel::mpsc::Sender) { + let (request_tx, mut request_rx): (UnboundedSender, UnboundedReceiver) = + unbounded(); + + let state = NetworkManagerState::new(&nm).await.unwrap_or_default(); + if output + .send(Event::Init { + conn: nm.clone(), + sender: request_tx, + state, + }) + .await + .is_err() + { + return; + } + + let Ok(events) = nm.network_events().await else { + while let Some(req) = request_rx.next().await { + let event = request_response(&nm, req, false).await; + _ = output.send(event).await; + } + return; + }; + + let mut events = events.fuse(); + loop { + futures::select! { + req = request_rx.next().fuse() => { + let Some(req) = req else { + break; + }; + let event = handle_request(&nm, req).await; + if output.send(event).await.is_err() { + break; + } + } + event = events.next() => { + let Some(event) = event else { + break; + }; + let message = match event { + Ok(NetworkEvent::AccessPointsChanged) => Some(Event::WirelessAccessPoints), + Ok(NetworkEvent::DeviceChanged { .. } | NetworkEvent::SettingsChanged(_)) => Some(Event::Devices), + Ok(NetworkEvent::ActiveConnectionsChanged) => Some(Event::ActiveConns), + Ok(NetworkEvent::WirelessEnabledChanged) => Some(Event::WiFiEnabled( + nm.wifi_state().await.map(|state| state.enabled).unwrap_or(false), + )), + Ok(NetworkEvent::ConnectivityChanged | NetworkEvent::NetworkManagerRestarted) => Some(Event::ActiveConns), + Ok(_) => None, + Err(err) => { + tracing::error!(%err, "network event stream error"); + None + } + }; + + if let Some(message) = message + && output.send(message).await.is_err() + { + break; + } + } + } + } +} + +async fn handle_request(nm: &NetworkManager, req: Request) -> Event { + let success = match &req { + Request::Activate(device_path, connection_path) => activate_connection( + nm, + connection_path.clone(), + device_path.clone(), + slash_path(), + ) + .await + .is_ok(), + Request::ActivateVpn(uuid) => nm.connect_vpn_by_uuid(uuid).await.is_ok(), + Request::Deactivate(uuid) => deactivate_by_uuid(nm, uuid).await.is_ok(), + Request::Disconnect(ssid) => disconnect_wifi_by_ssid(nm, ssid).await.is_ok(), + Request::Forget(ssid) => nm.forget(ssid).await.is_ok(), + Request::Authenticate { + ssid, + identity, + password, + network_type, + secret_tx, + interface, + } => { + let result = authenticate_wifi( + nm, + ssid, + identity.as_deref(), + password, + *network_type, + interface.as_deref(), + ) + .await; + if let Err(err) = &result { + tracing::error!( + %err, + %ssid, + ?network_type, + ?interface, + "wifi authentication failed" + ); + } + let success = result.is_ok(); + + if success + && let Some(secret_tx) = secret_tx + && let Ok(Some(uuid)) = nm.get_saved_connection_uuid(ssid).await + { + let (applied_tx, applied_rx) = oneshot::channel(); + let (setting_name, key) = if identity.is_some() { + ("802-1x", "password") + } else { + ("802-11-wireless-security", "psk") + }; + _ = secret_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: setting_name.to_string(), + uuid, + secrets: HashMap::from([(key.to_string(), password.clone())]), + applied_tx, + }) + .await; + _ = tokio::time::timeout(Duration::from_secs(1), applied_rx).await; + } + + success + } + Request::GetWiFiCredentials(ssid, uuid, security_type, secret_tx) => { + let password = if matches!(security_type, available_wifi::NetworkType::Open) { + None + } else if let Some(secret_tx) = secret_tx { + let (resp_tx, resp_rx) = oneshot::channel(); + let (setting_name, key) = + if matches!(security_type, available_wifi::NetworkType::EAP) { + ("802-1x", "password") + } else { + ("802-11-wireless-security", "psk") + }; + _ = secret_tx + .send(nm_secret_agent::Request::GetSecrets { + setting_name: setting_name.to_string(), + uuid: uuid.to_string(), + resp_tx, + }) + .await; + tokio::time::timeout(Duration::from_secs(10), resp_rx) + .await + .ok() + .and_then(Result::ok) + .and_then(|mut secrets| secrets.remove(key)) + } else { + None + }; + + return Event::WiFiCredentials { + ssid: ssid.clone(), + password, + security_type: *security_type, + }; + } + Request::Reload => true, + Request::Remove(uuid) => nm.delete_saved_connection(uuid).await.is_ok(), + Request::SelectAccessPoint(ssid, network_type, _secret_tx, interface) => { + let result = select_access_point(nm, ssid, *network_type, interface.as_deref()).await; + if let Err(err) = &result { + tracing::error!( + %err, + %ssid, + ?network_type, + ?interface, + "wifi access point selection failed" + ); + } + result.is_ok() + } + Request::SetAirplaneMode(enabled) => nm.set_airplane_mode(*enabled).await.is_ok(), + Request::SetWiFi(enabled) => nm.set_wireless_enabled(*enabled).await.is_ok(), + }; + + request_response(nm, req, success).await +} + +async fn request_response(nm: &NetworkManager, req: Request, success: bool) -> Event { + Event::RequestResponse { + req, + success, + state: NetworkManagerState::new(nm).await.unwrap_or_default(), + } +} + +async fn select_access_point( + nm: &NetworkManager, + ssid: &str, + network_type: available_wifi::NetworkType, + interface: Option<&str>, +) -> Result<(), Error> { + match network_type { + available_wifi::NetworkType::Open => { + nm.connect(ssid, interface, WifiSecurity::Open).await?; + } + available_wifi::NetworkType::PskOrSae => { + nm.connect(ssid, interface, WifiSecurity::WpaPsk { psk: String::new() }) + .await?; + } + available_wifi::NetworkType::Sae | available_wifi::NetworkType::EAP => { + let Some(saved) = find_saved_wifi(nm, ssid, interface).await? else { + return Err(Error::Message("no saved Wi-Fi connection".to_string())); + }; + let device = saved + .interface_name + .as_deref() + .or(interface) + .map(|interface| nm.get_device_by_interface(interface)) + .ok_or_else(|| { + Error::Message("no Wi-Fi interface for saved connection".to_string()) + })? + .await?; + let ap = find_access_point_path(nm, ssid, interface) + .await + .unwrap_or_else(slash_path); + activate_connection(nm, saved.path, device, ap).await?; + } + } + + Ok(()) +} + +async fn authenticate_wifi( + nm: &NetworkManager, + ssid: &str, + identity: Option<&str>, + password: &SecureString, + network_type: available_wifi::NetworkType, + interface: Option<&str>, +) -> Result<(), Error> { + match network_type { + available_wifi::NetworkType::Open => { + nm.connect(ssid, interface, WifiSecurity::Open).await? + } + available_wifi::NetworkType::PskOrSae => { + nm.connect( + ssid, + interface, + WifiSecurity::WpaPsk { + psk: password.unsecure().to_string(), + }, + ) + .await?; + } + available_wifi::NetworkType::Sae => { + connect_sae(nm, ssid, password.unsecure(), interface).await?; + } + available_wifi::NetworkType::EAP => { + nm.connect( + ssid, + interface, + WifiSecurity::WpaEap { + opts: EapOptions::new(identity.unwrap_or_default(), password.unsecure()), + }, + ) + .await?; + } + } + + Ok(()) +} + +async fn connect_sae( + nm: &NetworkManager, + ssid: &str, + password: &str, + interface: Option<&str>, +) -> Result<(), Error> { + let mut settings = nmrs::builders::WifiConnectionBuilder::new(ssid) + .wpa_psk(password) + .autoconnect(true) + .ipv4_auto() + .ipv6_auto() + .build(); + + if let Some(security) = settings.get_mut("802-11-wireless-security") { + security.insert("key-mgmt", Value::from("sae")); + security.remove("auth-alg"); + } + + let ap_path = find_access_point_path(nm, ssid, interface) + .await + .ok_or_else(|| Error::Message(format!("access point {ssid} not found")))?; + nm.add_and_activate_connection(settings, interface, Some(ap_path.as_str())) + .await?; + + Ok(()) +} + +async fn disconnect_wifi_by_ssid(nm: &NetworkManager, ssid: &str) -> Result<(), Error> { + for active in nm.list_active_connections().await? { + if let ActiveConnection::Wifi(wifi) = active + && wifi.ssid == ssid + { + return deactivate_by_uuid(nm, &wifi.uuid).await; + } + } + + Ok(()) +} + +async fn deactivate_by_uuid(nm: &NetworkManager, uuid: &str) -> Result<(), Error> { + if nm.disconnect_vpn_by_uuid(uuid).await.is_ok() { + return Ok(()); + } + + let active_path = find_active_connection_path(nm, uuid).await?; + let proxy = zbus::Proxy::new(nm.dbus_connection(), NM_DEST, NM_PATH, NM_IFACE).await?; + proxy + .call_method("DeactivateConnection", &(active_path,)) + .await?; + + Ok(()) +} + +async fn activate_connection( + nm: &NetworkManager, + connection_path: OwnedObjectPath, + device_path: OwnedObjectPath, + specific_path: OwnedObjectPath, +) -> Result<(), Error> { + let proxy = zbus::Proxy::new(nm.dbus_connection(), NM_DEST, NM_PATH, NM_IFACE).await?; + proxy + .call_method( + "ActivateConnection", + &(connection_path, device_path, specific_path), + ) + .await?; + Ok(()) +} + +async fn find_active_connection_path( + nm: &NetworkManager, + uuid: &str, +) -> Result { + let proxy = zbus::Proxy::new(nm.dbus_connection(), NM_DEST, NM_PATH, NM_IFACE).await?; + let paths: Vec = proxy.get_property("ActiveConnections").await?; + + for path in paths { + let active = zbus::Proxy::new( + nm.dbus_connection(), + NM_DEST, + path.clone(), + "org.freedesktop.NetworkManager.Connection.Active", + ) + .await?; + let active_uuid: String = active.get_property("Uuid").await?; + if active_uuid == uuid { + return Ok(path); + } + } + + Err(Error::Message(format!( + "active connection {uuid} not found" + ))) +} + +async fn find_access_point_path( + nm: &NetworkManager, + ssid: &str, + interface: Option<&str>, +) -> Option { + nm.list_access_points(interface) + .await + .ok()? + .into_iter() + .filter(|ap| ap.ssid == ssid) + .max_by_key(|ap| ap.strength) + .map(|ap| ap.path) +} + +async fn find_saved_wifi( + nm: &NetworkManager, + ssid: &str, + interface: Option<&str>, +) -> Result, Error> { + Ok(nm + .list_saved_connections() + .await? + .into_iter() + .find(|connection| { + if let Some(interface) = interface + && connection + .interface_name + .as_deref() + .is_some_and(|i| i != interface) + { + return false; + } + saved_wifi_identity(connection).is_some_and(|(saved_ssid, _)| saved_ssid == ssid) + })) +} + +pub async fn wifi_connection_settings( + nm: NetworkManager, +) -> Result, Box>, Error> { + Ok(nm + .list_saved_connections() + .await? + .into_iter() + .filter_map(|connection| { + saved_wifi_identity(&connection) + .map(|(ssid, _)| (ssid.into_boxed_str(), connection.uuid.into_boxed_str())) + }) + .collect()) +} + +pub async fn import_openvpn(path: impl AsRef) -> Result<(), Error> { + use nmrs::builders::{OpenVpnBuilder, build_openvpn_connection}; + + let nm = NetworkManager::new().await?; + let config = OpenVpnBuilder::from_ovpn_file(path)?.build()?; + let settings = build_openvpn_connection(&config, &ConnectionOptions::new(false))?; + nm.add_connection(settings).await?; + Ok(()) +} + +pub async fn import_wireguard(path: impl AsRef, name: &str) -> Result<(), Error> { + use nmrs::builders::WireGuardBuilder; + + let nm = NetworkManager::new().await?; + let config = parse_wireguard_config(path.as_ref(), name)?; + let mut builder = WireGuardBuilder::new(&config.name) + .private_key(config.private_key) + .address(config.address) + .add_peers(config.peers) + .autoconnect(false); + + if !config.dns.is_empty() { + builder = builder.dns(config.dns); + } + if let Some(mtu) = config.mtu { + builder = builder.mtu(mtu); + } + + nm.add_connection(builder.build()?).await?; + Ok(()) +} + +struct ParsedWireGuard { + name: String, + private_key: String, + address: String, + dns: Vec, + mtu: Option, + peers: Vec, +} + +fn parse_wireguard_config(path: &Path, name: &str) -> Result { + let contents = std::fs::read_to_string(path) + .map_err(|err| Error::Message(format!("failed to read {}: {err}", path.display())))?; + let mut section = ""; + let mut private_key = None; + let mut address = None; + let mut dns = Vec::new(); + let mut mtu = None; + let mut peers = Vec::new(); + let mut peer_public_key: Option = None; + let mut peer_endpoint: Option = None; + let mut peer_allowed_ips: Vec = Vec::new(); + let mut peer_preshared_key: Option = None; + let mut peer_keepalive: Option = None; + + let flush_peer = |peers: &mut Vec, + public_key: &mut Option, + endpoint: &mut Option, + allowed_ips: &mut Vec, + preshared_key: &mut Option, + keepalive: &mut Option| + -> Result<(), Error> { + if public_key.is_none() + && endpoint.is_none() + && allowed_ips.is_empty() + && preshared_key.is_none() + && keepalive.is_none() + { + return Ok(()); + } + + let public_key = public_key + .take() + .ok_or_else(|| Error::Message("WireGuard peer is missing PublicKey".to_string()))?; + let endpoint = endpoint + .take() + .ok_or_else(|| Error::Message("WireGuard peer is missing Endpoint".to_string()))?; + if allowed_ips.is_empty() { + return Err(Error::Message( + "WireGuard peer is missing AllowedIPs".to_string(), + )); + } + + let mut peer = nmrs::WireGuardPeer::new(public_key, endpoint, std::mem::take(allowed_ips)); + if let Some(psk) = preshared_key.take() { + peer = peer.with_preshared_key(psk); + } + if let Some(interval) = keepalive.take() { + peer = peer.with_persistent_keepalive(interval); + } + peers.push(peer); + Ok(()) + }; + + for raw_line in contents.lines() { + let line = raw_line + .split_once('#') + .map_or(raw_line, |(line, _)| line) + .trim(); + if line.is_empty() { + continue; + } + + if line.starts_with('[') && line.ends_with(']') { + if section.eq_ignore_ascii_case("Peer") { + flush_peer( + &mut peers, + &mut peer_public_key, + &mut peer_endpoint, + &mut peer_allowed_ips, + &mut peer_preshared_key, + &mut peer_keepalive, + )?; + } + section = &line[1..line.len() - 1]; + continue; + } + + let Some((key, value)) = line.split_once('=') else { + continue; + }; + let key = key.trim(); + let value = value.trim(); + + if section.eq_ignore_ascii_case("Interface") { + match key.to_ascii_lowercase().as_str() { + "privatekey" => private_key = Some(value.to_string()), + "address" => { + address = value + .split(',') + .map(str::trim) + .find(|value| value.contains('.')) + .or_else(|| value.split(',').map(str::trim).next()) + .map(ToOwned::to_owned); + } + "dns" => dns.extend( + value + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned), + ), + "mtu" => mtu = value.parse().ok(), + _ => {} + } + } else if section.eq_ignore_ascii_case("Peer") { + match key.to_ascii_lowercase().as_str() { + "publickey" => peer_public_key = Some(value.to_string()), + "endpoint" => peer_endpoint = Some(value.to_string()), + "allowedips" => { + peer_allowed_ips.extend( + value + .split(',') + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToOwned::to_owned), + ); + } + "presharedkey" => peer_preshared_key = Some(value.to_string()), + "persistentkeepalive" => peer_keepalive = value.parse().ok(), + _ => {} + } + } + } + + if section.eq_ignore_ascii_case("Peer") { + flush_peer( + &mut peers, + &mut peer_public_key, + &mut peer_endpoint, + &mut peer_allowed_ips, + &mut peer_preshared_key, + &mut peer_keepalive, + )?; + } + + Ok(ParsedWireGuard { + name: name.to_string(), + private_key: private_key + .ok_or_else(|| Error::Message("WireGuard config is missing PrivateKey".to_string()))?, + address: address + .ok_or_else(|| Error::Message("WireGuard config is missing Address".to_string()))?, + dns, + mtu, + peers, + }) +} + +fn active_connection_infos( + active: Vec, +) -> Vec { + let mut infos = active + .into_iter() + .filter_map(|connection| match connection { + ActiveConnection::Wired(wired) => Some(current_networks::ActiveConnectionInfo::Wired { + name: wired.id, + hw_address: wired.hw_address.unwrap_or_default(), + speed: wired.speed_mbps.unwrap_or_default(), + ip_addresses: parse_ipv4_list([wired.ip4_address]), + }), + ActiveConnection::Wifi(wifi) => Some(current_networks::ActiveConnectionInfo::WiFi { + name: wifi.ssid, + ip_addresses: parse_ipv4_list([wifi.ip4_address]), + hw_address: wifi.bssid.unwrap_or_default(), + state: devices::ActiveConnectionState::from(wifi.state), + strength: wifi.strength.unwrap_or_default(), + }), + ActiveConnection::Vpn(vpn) => Some(current_networks::ActiveConnectionInfo::Vpn { + name: vpn.id, + ip_addresses: parse_ipv4_list([vpn.ip4_address]), + }), + ActiveConnection::Other(_) => None, + _ => None, + }) + .collect::>(); + + infos.sort_by(|a, b| { + let helper = |conn: ¤t_networks::ActiveConnectionInfo| match conn { + current_networks::ActiveConnectionInfo::Vpn { name, .. } => format!("0{name}"), + current_networks::ActiveConnectionInfo::Wired { name, .. } => format!("1{name}"), + current_networks::ActiveConnectionInfo::WiFi { name, .. } => format!("2{name}"), + }; + helper(a).cmp(&helper(b)) + }); + + infos +} + +fn access_point_from_nmrs(ap: nmrs::AccessPoint) -> available_wifi::AccessPoint { + let network_type = match ap.security.preferred_connect_type() { + ConnectType::Open | ConnectType::Owe => available_wifi::NetworkType::Open, + ConnectType::Eap => available_wifi::NetworkType::EAP, + ConnectType::Psk => available_wifi::NetworkType::PskOrSae, + ConnectType::Sae => available_wifi::NetworkType::Sae, + _ => available_wifi::NetworkType::PskOrSae, + }; + + available_wifi::AccessPoint { + ssid: Arc::from(ap.ssid.as_str()), + strength: ap.strength, + state: devices::DeviceState::from(ap.device_state), + working: false, + path: ap.path, + hw_address: hw_address::HwAddress::from_str(&ap.bssid).unwrap_or_default(), + secured: !ap.security.is_open(), + wps_push: ap.security.wps, + network_type, + interface: Some(ap.interface), + bssid: Some(ap.bssid), + } +} + +fn saved_wifi_profiles(saved: &[SavedConnection]) -> impl Iterator { + saved.iter().filter(|connection| { + connection.connection_type == "802-11-wireless" + && matches!(connection.summary, SettingsSummary::Wifi { .. }) + }) +} + +fn saved_wifi_identity(saved: &SavedConnection) -> Option<(String, available_wifi::NetworkType)> { + let SettingsSummary::Wifi { ssid, security, .. } = &saved.summary else { + return None; + }; + if ssid.is_empty() { + return None; + } + + let network_type = match security.as_ref().map(|s| s.key_mgmt) { + None | Some(WifiKeyMgmt::None | WifiKeyMgmt::Owe | WifiKeyMgmt::OweTransitionMode) => { + available_wifi::NetworkType::Open + } + Some(WifiKeyMgmt::WpaEap) => available_wifi::NetworkType::EAP, + Some(WifiKeyMgmt::Wep | WifiKeyMgmt::WpaPsk) => available_wifi::NetworkType::PskOrSae, + Some(WifiKeyMgmt::Sae) => available_wifi::NetworkType::Sae, + Some(_) => available_wifi::NetworkType::PskOrSae, + }; + + Some((ssid.clone(), network_type)) +} + +fn saved_matches_device( + connection: &SavedConnection, + device_type: devices::DeviceType, + interface: &str, +) -> bool { + let type_matches = match device_type { + devices::DeviceType::Ethernet => connection.connection_type == "802-3-ethernet", + devices::DeviceType::Wifi => connection.connection_type == "802-11-wireless", + devices::DeviceType::WireGuard => connection.connection_type == "wireguard", + _ => false, + }; + + type_matches + && connection + .interface_name + .as_deref() + .is_none_or(|name| name == interface) +} + +fn active_connection_by_interface( + active: &[ActiveConnection], + interface: &str, +) -> Option { + for connection in active { + match connection { + ActiveConnection::Wired(wired) if wired.interface.as_deref() == Some(interface) => { + return Some(ActiveConnectionRecord { + path: slash_path(), + id: wired.id.clone(), + uuid: wired.uuid.clone(), + state: devices::ActiveConnectionState::from(wired.state), + }); + } + ActiveConnection::Wifi(wifi) if wifi.interface.as_deref() == Some(interface) => { + return Some(ActiveConnectionRecord { + path: slash_path(), + id: wifi.id.clone(), + uuid: wifi.uuid.clone(), + state: devices::ActiveConnectionState::from(wifi.state), + }); + } + ActiveConnection::Vpn(vpn) if vpn.interface.as_deref() == Some(interface) => { + return Some(ActiveConnectionRecord { + path: slash_path(), + id: vpn.id.clone(), + uuid: vpn.uuid.clone(), + state: devices::ActiveConnectionState::from(vpn.state), + }); + } + _ => {} + } + } + + None +} + +fn parse_ipv4_list(addresses: impl IntoIterator>) -> Vec { + addresses + .into_iter() + .flatten() + .filter_map(|address| { + address + .split_once('/') + .map_or(address.as_str(), |(address, _)| address) + .parse() + .ok() + }) + .collect() +} + +fn slash_path() -> OwnedObjectPath { + OwnedObjectPath::try_from("/").expect("slash is a valid object path") +} + +fn owned_path(path: &str) -> Result { + OwnedObjectPath::try_from(path) + .map_err(|err| Error::Message(format!("invalid object path {path}: {err}"))) +} + +pub mod nm_secret_agent { + use super::*; + + pub type SecretSender = + Arc>>>; + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum PasswordFlag { + None = 0, + AgentOwned = 1, + NotSaved = 2, + NotRequired = 4, + } + + #[derive(Clone, Debug)] + pub struct Error(pub String); + + impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } + } + + impl std::error::Error for Error {} + + #[derive(Debug, Clone)] + pub enum Event { + RequestSecret { + uuid: String, + name: String, + description: Option, + previous: SecureString, + tx: SecretSender, + }, + CancelGetSecrets { + uuid: String, + name: String, + }, + Failed(Error), + } + + #[derive(Debug)] + pub enum Request { + SetSecrets { + setting_name: String, + uuid: String, + secrets: HashMap, + applied_tx: oneshot::Sender<()>, + }, + GetSecrets { + setting_name: String, + uuid: String, + resp_tx: oneshot::Sender>, + }, + } + + pub fn secret_agent_stream( + identifier: impl AsRef, + rx: tokio::sync::mpsc::Receiver, + ) -> impl futures::Stream { + let identifier = identifier.as_ref().to_string(); + cosmic::iced::stream::channel( + 4, + move |mut msg_tx: futures::channel::mpsc::Sender| async move { + if let Err(error) = secret_agent_stream_impl(&identifier, msg_tx.clone(), rx).await + { + _ = msg_tx.send(Event::Failed(error)).await; + } + }, + ) + } + + async fn secret_agent_stream_impl( + identifier: &str, + mut msg_tx: futures::channel::mpsc::Sender, + mut rx: tokio::sync::mpsc::Receiver, + ) -> Result<(), Error> { + unlock_collection().await?; + + let (_handle, mut requests) = SecretAgent::builder() + .with_identifier(identifier) + .register() + .await + .map_err(|err| Error(err.to_string()))?; + + loop { + futures::select! { + request = rx.recv().fuse() => { + let Some(request) = request else { + break; + }; + handle_control_request(request).await?; + } + request = requests.next().fuse() => { + let Some(request) = request else { + break; + }; + handle_secret_request(&mut msg_tx, request).await?; + } + } + } + + Ok(()) + } + + async fn handle_control_request(request: Request) -> Result<(), Error> { + match request { + Request::SetSecrets { + setting_name, + uuid, + secrets, + applied_tx, + } => { + if secrets.is_empty() { + delete_secrets(&uuid).await?; + } else { + store_secrets(&uuid, &setting_name, &secrets).await?; + } + _ = applied_tx.send(()); + } + Request::GetSecrets { + setting_name, + uuid, + resp_tx, + } => { + let secrets = get_secrets(&uuid, &setting_name).await?; + _ = resp_tx.send(secrets); + } + } + + Ok(()) + } + + async fn handle_secret_request( + msg_tx: &mut futures::channel::mpsc::Sender, + request: SecretRequest, + ) -> Result<(), Error> { + let setting_name = setting_name(&request.setting); + if !request.flags.contains(SecretAgentFlags::REQUEST_NEW) { + let stored = get_secrets(&request.connection_uuid, setting_name).await?; + if !stored.is_empty() { + return respond_with_stored(request, stored).await; + } + } + + if !request + .flags + .intersects(SecretAgentFlags::ALLOW_INTERACTION | SecretAgentFlags::REQUEST_NEW) + { + request + .responder + .no_secrets() + .await + .map_err(|err| Error(err.to_string()))?; + return Ok(()); + } + + let (tx, rx) = oneshot::channel(); + let tx = Arc::new(tokio::sync::Mutex::new(Some(tx))); + let description = match &request.setting { + SecretSetting::WifiPsk { ssid } => Some(ssid.clone()), + SecretSetting::WifiEap { identity, method } => Some( + format!( + "{} {}", + identity.clone().unwrap_or_default(), + method.clone().unwrap_or_default() + ) + .trim() + .to_string(), + ), + SecretSetting::Vpn { + service_type, + user_name, + } => Some(user_name.clone().unwrap_or_else(|| service_type.clone())), + SecretSetting::Other(setting) => Some(setting.clone()), + _ => None, + } + .filter(|value| !value.is_empty()); + + let event_name = match &request.setting { + SecretSetting::WifiPsk { .. } | SecretSetting::WifiEap { .. } => { + request.connection_uuid.clone() + } + _ => request.connection_id.clone(), + }; + + let response_kind = ResponseKind::from(&request); + let uuid = request.connection_uuid.clone(); + msg_tx + .send(Event::RequestSecret { + uuid: uuid.clone(), + name: event_name, + description, + previous: SecureString::from(""), + tx, + }) + .await + .map_err(|err| Error(err.to_string()))?; + + match rx.await { + Ok(secret) => { + let mut stored = HashMap::new(); + let key = response_kind.key(); + stored.insert(key.to_string(), secret.clone()); + store_secrets(&uuid, setting_name, &stored).await?; + response_kind.respond(request.responder, secret).await?; + } + Err(_) => { + request + .responder + .cancel() + .await + .map_err(|err| Error(err.to_string()))?; + } + } + + Ok(()) + } + + enum ResponseKind { + WifiPsk, + WifiEap { identity: Option }, + Vpn { key: String }, + Raw { setting_name: String, key: String }, + } + + impl ResponseKind { + fn from(request: &SecretRequest) -> Self { + match &request.setting { + SecretSetting::WifiPsk { .. } => Self::WifiPsk, + SecretSetting::WifiEap { identity, .. } => Self::WifiEap { + identity: identity.clone(), + }, + SecretSetting::Vpn { .. } => Self::Vpn { + key: request + .hints + .iter() + .find(|hint| !hint.contains(':')) + .cloned() + .unwrap_or_else(|| "password".to_string()), + }, + SecretSetting::Other(setting_name) => Self::Raw { + setting_name: setting_name.clone(), + key: request + .hints + .first() + .cloned() + .unwrap_or_else(|| "password".to_string()), + }, + _ => Self::Raw { + setting_name: setting_name(&request.setting).to_string(), + key: "password".to_string(), + }, + } + } + + fn key(&self) -> &str { + match self { + Self::WifiPsk => "psk", + Self::WifiEap { .. } => "password", + Self::Vpn { key } | Self::Raw { key, .. } => key.as_str(), + } + } + + async fn respond( + self, + responder: SecretResponder, + secret: SecureString, + ) -> Result<(), Error> { + match self { + Self::WifiPsk => responder + .wifi_psk(secret.unsecure().to_string()) + .await + .map_err(|err| Error(err.to_string())), + Self::WifiEap { identity } => responder + .wifi_eap(identity, secret.unsecure().to_string()) + .await + .map_err(|err| Error(err.to_string())), + Self::Vpn { key } => responder + .vpn_secrets(HashMap::from([(key, secret.unsecure().to_string())])) + .await + .map_err(|err| Error(err.to_string())), + Self::Raw { setting_name, key } => { + let mut inner = HashMap::new(); + inner.insert( + key, + OwnedValue::from(Str::from(secret.unsecure().to_string())), + ); + responder + .raw(setting_name, inner) + .await + .map_err(|err| Error(err.to_string())) + } + } + } + } + + async fn respond_with_stored( + request: SecretRequest, + stored: HashMap, + ) -> Result<(), Error> { + match &request.setting { + SecretSetting::WifiPsk { .. } => { + if let Some(psk) = stored.get("psk") { + request + .responder + .wifi_psk(psk.unsecure().to_string()) + .await + .map_err(|err| Error(err.to_string()))?; + } else { + request + .responder + .no_secrets() + .await + .map_err(|err| Error(err.to_string()))?; + } + } + SecretSetting::WifiEap { identity, .. } => { + if let Some(password) = stored.get("password") { + request + .responder + .wifi_eap(identity.clone(), password.unsecure().to_string()) + .await + .map_err(|err| Error(err.to_string()))?; + } else { + request + .responder + .no_secrets() + .await + .map_err(|err| Error(err.to_string()))?; + } + } + SecretSetting::Vpn { .. } => { + let secrets = stored + .into_iter() + .map(|(key, value)| (key, value.unsecure().to_string())) + .collect(); + request + .responder + .vpn_secrets(secrets) + .await + .map_err(|err| Error(err.to_string()))?; + } + _ => { + let setting_name = setting_name(&request.setting).to_string(); + let inner = stored + .into_iter() + .map(|(key, value)| { + ( + key, + OwnedValue::from(Str::from(value.unsecure().to_string())), + ) + }) + .collect(); + request + .responder + .raw(setting_name, inner) + .await + .map_err(|err| Error(err.to_string()))?; + } + } + + Ok(()) + } + + fn setting_name(setting: &SecretSetting) -> &'static str { + match setting { + SecretSetting::WifiPsk { .. } => "802-11-wireless-security", + SecretSetting::WifiEap { .. } => "802-1x", + SecretSetting::Vpn { .. } => "vpn", + SecretSetting::Gsm => "gsm", + SecretSetting::Cdma => "cdma", + SecretSetting::Pppoe => "pppoe", + SecretSetting::Other(_) => "connection", + _ => "connection", + } + } + + async fn unlock_collection() -> Result<(), Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|err| Error(err.to_string()))?; + let collection = ss + .get_default_collection() + .await + .map_err(|err| Error(err.to_string()))?; + if collection + .is_locked() + .await + .map_err(|err| Error(err.to_string()))? + { + collection + .unlock() + .await + .map_err(|err| Error(err.to_string()))?; + } + Ok(()) + } + + async fn store_secrets( + uuid: &str, + setting_name: &str, + secrets: &HashMap, + ) -> Result<(), Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|err| Error(err.to_string()))?; + let collection = ss + .get_default_collection() + .await + .map_err(|err| Error(err.to_string()))?; + + for (name, secret) in secrets { + let mut attributes = HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", uuid); + attributes.insert("setting_name", setting_name); + attributes.insert("name", name.as_str()); + collection + .create_item( + "NetworkManager Secret", + attributes, + secret.unsecure().as_bytes(), + true, + "text/plain", + ) + .await + .map_err(|err| Error(err.to_string()))?; + } + + Ok(()) + } + + async fn get_secrets( + uuid: &str, + setting_name: &str, + ) -> Result, Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|err| Error(err.to_string()))?; + let collection = ss + .get_default_collection() + .await + .map_err(|err| Error(err.to_string()))?; + let mut attributes = HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", uuid); + attributes.insert("setting_name", setting_name); + + let search_items = collection + .search_items(attributes) + .await + .map_err(|err| Error(err.to_string()))?; + let mut secrets = HashMap::new(); + for item in &search_items { + let name = item + .get_attributes() + .await + .map_err(|err| Error(err.to_string()))? + .get("name") + .cloned() + .unwrap_or_else(|| "unknown".to_string()); + let secret = item + .get_secret() + .await + .map_err(|err| Error(err.to_string()))?; + let secret = String::from_utf8(secret).map_err(|err| Error(err.to_string()))?; + secrets.insert(name, SecureString::from(secret)); + } + Ok(secrets) + } + + async fn delete_secrets(uuid: &str) -> Result<(), Error> { + let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) + .await + .map_err(|err| Error(err.to_string()))?; + let collection = ss + .get_default_collection() + .await + .map_err(|err| Error(err.to_string()))?; + let mut attributes = HashMap::new(); + attributes.insert("application", SECRET_ID); + attributes.insert("uuid", uuid); + + let search_items = collection + .search_items(attributes) + .await + .map_err(|err| Error(err.to_string()))?; + for item in &search_items { + item.delete().await.map_err(|err| Error(err.to_string()))?; + } + Ok(()) + } +} diff --git a/cosmic-settings/src/pages/networking/mod.rs b/cosmic-settings/src/pages/networking/mod.rs index f9b4a4c..84748ce 100644 --- a/cosmic-settings/src/pages/networking/mod.rs +++ b/cosmic-settings/src/pages/networking/mod.rs @@ -1,24 +1,24 @@ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only +pub mod backend; pub mod vpn; pub mod wifi; pub mod wired; -use std::ffi::OsStr; use std::process::Stdio; use std::sync::Arc; use anyhow::Context; use cosmic::{Apply, Element, Task, widget}; -use cosmic_dbus_networkmanager::interface::enums::{DeviceState, DeviceType}; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use cosmic_settings_network_manager_subscription as network_manager; use cosmic_settings_page::{self as page, Section, section}; use futures::{SinkExt, StreamExt}; use secure_string::SecureString; use slotmap::SlotMap; +use self::backend as network_manager; +use self::backend::devices::{DeviceState, DeviceType}; + pub type SecretSender = Arc>>>; static NM_CONNECTION_EDITOR: &str = "nm-connection-editor"; @@ -37,8 +37,8 @@ pub struct Page { pub enum Message { /// An error occurred. Error(String), - /// Successfully connected to the system dbus. - NetworkManagerConnect(zbus::Connection), + /// Successfully connected to NetworkManager. + NetworkManagerConnect(nmrs::NetworkManager), /// Open the wifi settings page with the selected device. OpenPage { page: page::Entity, @@ -143,6 +143,7 @@ impl page::Page for Page { fl!("network-device-state", "unavailable") } DeviceState::Unknown => fl!("network-device-state", "unknown"), + DeviceState::Other(_) => fl!("network-device-state", "unknown"), DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"), }, "preferences-wireless-symbolic", @@ -190,6 +191,7 @@ impl page::Page for Page { fl!("network-device-state", "unplugged") } DeviceState::Unknown => fl!("network-device-state", "unknown"), + DeviceState::Other(_) => fl!("network-device-state", "unknown"), DeviceState::Unmanaged => fl!("network-device-state", "unmanaged"), }, "preferences-wired-symbolic", @@ -225,9 +227,9 @@ impl page::Page for Page { fn on_enter(&mut self) -> cosmic::Task { if self.nm_task.is_none() { return cosmic::Task::future(async move { - zbus::Connection::system() + nmrs::NetworkManager::new() .await - .context("failed to create system dbus connection") + .context("failed to connect to NetworkManager") .map_or_else( |why| Message::Error(why.to_string()), Message::NetworkManagerConnect, @@ -310,33 +312,47 @@ impl Page { Task::none() } - fn connect(&mut self, conn: zbus::Connection) -> Task { + fn connect(&mut self, nm: nmrs::NetworkManager) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| async move { - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(why) => { - tracing::error!( - why = why.to_string(), - "failed to connect to network_manager" - ); - - return futures::future::pending().await; + match network_manager::devices::list(&nm, |_| true).await { + Ok(devices) => { + _ = sender + .send(crate::pages::Message::Networking(Message::UpdateDevices( + devices.into_iter().map(Arc::new).collect(), + ))) + .await; } - }; + Err(why) => { + _ = sender + .send(crate::pages::Message::Networking(Message::Error( + why.to_string(), + ))) + .await; + } + } - let mut devices_changed = - std::pin::pin!(network_manager.receive_devices_changed().await.then( - |_| async { - match network_manager::devices::list(&conn, |_| true).await { - Ok(devices) => Message::UpdateDevices( + let Ok(events) = nm.network_events().await else { + return futures::future::pending().await; + }; + let devices_changed = events.filter_map(|event| async { + match event { + Ok(nmrs::NetworkEvent::DeviceChanged { .. }) + | Ok(nmrs::NetworkEvent::SettingsChanged(_)) + | Ok(nmrs::NetworkEvent::NetworkManagerRestarted) => { + match network_manager::devices::list(&nm, |_| true).await { + Ok(devices) => Some(Message::UpdateDevices( devices.into_iter().map(Arc::new).collect(), - ), - Err(why) => Message::Error(why.to_string()), + )), + Err(why) => Some(Message::Error(why.to_string())), } } - )); + Ok(_) => None, + Err(why) => Some(Message::Error(why.to_string())), + } + }); + futures::pin_mut!(devices_changed); while let Some(message) = devices_changed.next().await { _ = sender @@ -353,16 +369,6 @@ impl Page { } } -async fn nm_add_vpn_file>(type_: &str, path: P) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args(["connection", "import", "type", type_, "file"]) - .arg(path) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} - async fn nm_add_wired() -> Result<(), String> { nm_connection_editor(&["--type=802-3-ethernet", "-c"]).await } diff --git a/cosmic-settings/src/pages/networking/vpn/mod.rs b/cosmic-settings/src/pages/networking/vpn/mod.rs index ff85a8f..6c2c9e7 100644 --- a/cosmic-settings/src/pages/networking/vpn/mod.rs +++ b/cosmic-settings/src/pages/networking/vpn/mod.rs @@ -1,8 +1,6 @@ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only -pub mod nmcli; - use std::collections::HashMap; use std::sync::{Arc, LazyLock}; @@ -14,17 +12,16 @@ use cosmic::widget::space::horizontal as horizontal_space; use cosmic::widget::text_input::focus; use cosmic::widget::{self, icon}; use cosmic::{Apply, Element, Task, task}; -use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo; -use cosmic_settings_network_manager_subscription::nm_secret_agent::{self, PasswordFlag}; -use cosmic_settings_network_manager_subscription::{ - self as network_manager, NetworkManagerState, UUID, -}; use cosmic_settings_page::{self as page, Section, section}; use futures::{FutureExt, SinkExt, StreamExt}; use indexmap::IndexMap; use secure_string::SecureString; use tokio::sync::Mutex; +use super::backend as network_manager; +use super::backend::current_networks::ActiveConnectionInfo; +use super::backend::nm_secret_agent::{self, PasswordFlag}; +use super::backend::{NetworkManagerState, UUID}; use crate::pages::networking::SecretSender; pub static SECURE_INPUT_VPN: LazyLock = LazyLock::new(widget::Id::unique); @@ -60,8 +57,8 @@ pub enum Message { NetworkManager(network_manager::Event), /// An update from the secret agent SecretAgent(network_manager::nm_secret_agent::Event), - /// Successfully connected to the system dbus. - NetworkManagerConnect(zbus::Connection), + /// Successfully connected to NetworkManager. + NetworkManagerConnect(nmrs::NetworkManager), /// Updates the password text input PasswordUpdate(SecureString), /// Refresh devices and their connection profiles @@ -181,7 +178,7 @@ pub enum VpnDialog { #[derive(Debug)] pub struct NmState { - conn: zbus::Connection, + conn: nmrs::NetworkManager, sender: futures::channel::mpsc::UnboundedSender, active_conns: Vec, devices: Vec, @@ -371,20 +368,22 @@ impl page::Page for Page { let (tx, rx) = tokio::sync::mpsc::channel(4); self.secret_tx = Some(tx); if self.nm_task.is_none() { - return cosmic::Task::batch([cosmic::task::future(async move { - zbus::Connection::system() - .await - .context("failed to create system dbus connection") - .map_or_else( - |why| Message::Error(ErrorKind::DbusConnection, why.to_string()), - Message::NetworkManagerConnect, - ) - }), - cosmic::Task::stream( - cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent", rx), - ) - .map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))), - ]); + return cosmic::Task::batch([ + cosmic::task::future(async move { + nmrs::NetworkManager::new() + .await + .context("failed to connect to NetworkManager") + .map_or_else( + |why| Message::Error(ErrorKind::DbusConnection, why.to_string()), + Message::NetworkManagerConnect, + ) + }), + cosmic::Task::stream(nm_secret_agent::secret_agent_stream( + "com.system76.CosmicSettings.VPN.NetworkManager.SecretAgent", + rx, + )) + .map(|m| crate::pages::Message::Vpn(Message::SecretAgent(m))), + ]); } cosmic::Task::none() @@ -495,9 +494,8 @@ impl Page { Message::WireGuardConfig => { if let Some(VpnDialog::WireGuardName(device, filename, path)) = self.dialog.take() { return cosmic::task::future(async move { - let new_path = path.replace(&filename, &device); - _ = std::fs::rename(&path, &new_path); - match super::nm_add_vpn_file("wireguard", new_path).await { + let _ = filename; + match network_manager::import_wireguard(path, &device).await { Ok(_) => Message::Refresh, Err(why) => Message::Error(ErrorKind::Config, why.to_string()), } @@ -510,18 +508,12 @@ impl Page { if let Some(settings) = self.known_connections.get(&uuid) { let settings = match settings { ConnectionSettings::Vpn(settings) => settings, - ConnectionSettings::Wireguard { id } => { - let connection_name = id.clone(); - return cosmic::task::future(async move { - if let Err(why) = nmcli::connect(&connection_name).await { - return Message::Error( - ErrorKind::Connect, - format!("failed to connect to WireGuard VPN: {why}"), - ); - } - - Message::Refresh - }); + ConnectionSettings::Wireguard { .. } => { + if let Some(NmState { ref sender, .. }) = self.nm_state { + _ = sender + .unbounded_send(network_manager::Request::ActivateVpn(uuid)); + } + return Task::none(); } }; @@ -541,28 +533,10 @@ impl Page { return task::message(Message::FocusSecureInput); } _ => { - let connection_name = settings.id.clone(); - let username = settings.username.clone(); - return cosmic::task::future(async move { - if let Err(why) = nmcli::connect(&connection_name).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some(( - ErrorKind::Connect, - format!("failed to connect to VPN: {why}"), - )), - id: connection_name.clone(), - uuid, - username: username.clone(), - description: None, - password: SecureString::from(""), - password_hidden: true, - // TODO grab from the current dialog - tx: Arc::new(Mutex::new(None)), - }); - } - - Message::Refresh - }); + if let Some(NmState { ref sender, .. }) = self.nm_state { + _ = sender + .unbounded_send(network_manager::Request::ActivateVpn(uuid)); + } } } } @@ -629,89 +603,55 @@ impl Page { }; if let VpnDialog::Password { - id, + id: _, uuid, username, password, tx, .. } = dialog + && let Some(NmState { ref sender, .. }) = self.nm_state { let username_unwrapped = username.clone().unwrap_or_default(); - let task = self.activate_with_password( - id.clone(), - uuid.clone(), - username_unwrapped.clone(), - password.clone(), - ); let sec_tx = self.secret_tx.clone(); - return task - .then(move |_| { - let sec_tx = sec_tx.clone(); - let uuid = uuid.clone(); - let username = username.clone(); - let password = password.clone(); - let tx = tx.clone(); - let id = id.clone(); - Task::future(async move { - let mut guard = tx.lock().await; - if let Some(sender) = guard.take() { - let _ = sender.send(password); - } else { - // apply password and username then - if let Some(sec_tx) = sec_tx { - let (applied_tx, applied_rx) = - tokio::sync::oneshot::channel(); - if let Err(err) = sec_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name: "vpn".to_string(), - uuid: uuid.to_string(), - secrets: HashMap::from_iter([ - // username and password - ( - "username".to_string(), - username.clone().unwrap_or_default().into(), - ), - ("password".to_string(), password.clone()), - ]), - applied_tx, - }) - .await - { - tracing::error!(%err, "failed to apply secret"); - } - // wait max 1s for the applied signal - if let Err(err) = tokio::time::timeout( - std::time::Duration::from_secs(1), - applied_rx, - ) - .await - { - tracing::error!(%err, "failed to apply secret"); - } - } - // activate - if let Err(why) = nmcli::connect(&id).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some(( - ErrorKind::Connect, - format!("failed to connect to VPN: {why}"), - )), - id: id.clone(), - uuid, - username: username.clone(), - description: None, - password, - password_hidden: true, - tx: Arc::new(Mutex::new(None)), - }); - } + let nm_sender = sender.clone(); + return Task::future(async move { + let mut guard = tx.lock().await; + if let Some(sender) = guard.take() { + let _ = sender.send(password); + } else { + if let Some(sec_tx) = sec_tx { + let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); + if let Err(err) = sec_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: "vpn".to_string(), + uuid: uuid.to_string(), + secrets: HashMap::from_iter([ + ("username".to_string(), username_unwrapped.into()), + ("password".to_string(), password), + ]), + applied_tx, + }) + .await + { + tracing::error!(%err, "failed to apply secret"); } + if let Err(err) = tokio::time::timeout( + std::time::Duration::from_secs(1), + applied_rx, + ) + .await + { + tracing::error!(%err, "failed to apply secret"); + } + } + _ = nm_sender + .unbounded_send(network_manager::Request::ActivateVpn(uuid)); + } - Message::Refresh - }) - }) - .map(crate::app::Message::from); + Message::Refresh + }) + .map(crate::app::Message::from); } } Message::RetryWithPassword => { @@ -720,53 +660,39 @@ impl Page { }; if let VpnDialog::Password { - id, + id: _, uuid, username, password, .. } = dialog + && let Some(NmState { ref sender, .. }) = self.nm_state { let username_unwrapped = username.unwrap_or_default(); let sec_tx = self.secret_tx.clone(); - let task = self.activate_with_password( - id.clone(), - uuid.clone(), - username_unwrapped.clone(), - password.clone(), - ); - return task - .then(move |_| { - let sec_tx = sec_tx.clone(); - let uuid = uuid.clone(); - let username = username_unwrapped.clone(); - let password = password.clone(); - Task::future(async move { - if let Some(sec_tx) = sec_tx { - let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); - let _ = sec_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name: "vpn".to_string(), - uuid: uuid.to_string(), - secrets: HashMap::from_iter([ - // username and password - ("username".to_string(), username.clone().into()), - ("password".to_string(), password.clone()), - ]), - applied_tx, - }) - .await; - // wait max 1s for the applied signal - let _ = tokio::time::timeout( - std::time::Duration::from_secs(1), - applied_rx, - ) + let nm_sender = sender.clone(); + return Task::future(async move { + if let Some(sec_tx) = sec_tx { + let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); + let _ = sec_tx + .send(nm_secret_agent::Request::SetSecrets { + setting_name: "vpn".to_string(), + uuid: uuid.to_string(), + secrets: HashMap::from_iter([ + ("username".to_string(), username_unwrapped.into()), + ("password".to_string(), password), + ]), + applied_tx, + }) + .await; + let _ = + tokio::time::timeout(std::time::Duration::from_secs(1), applied_rx) .await; - } - Message::Activate(uuid) - }) - }) - .map(crate::app::Message::from); + } + _ = nm_sender.unbounded_send(network_manager::Request::ActivateVpn(uuid)); + Message::Refresh + }) + .map(crate::app::Message::from); } } Message::UsernameUpdate(user) => { @@ -873,56 +799,14 @@ impl Page { Task::none() } - fn activate_with_password( - &mut self, - connection_name: String, - uuid: Arc, - username: String, - password: SecureString, - ) -> Task { - cosmic::task::future(async move { - if let Err(why) = nmcli::set_username(&connection_name, &username).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some((ErrorKind::WithPassword("username"), why.to_string())), - id: connection_name.clone(), - uuid, - username: Some(username), - description: None, - password, - password_hidden: true, - tx: Arc::new(Mutex::new(None)), - }); - } - - if let Err(why) = nmcli::add_fallback(&connection_name).await { - return Message::VpnDialogError(VpnDialog::Password { - error: Some((ErrorKind::Config, why.to_string())), - id: connection_name.clone(), - uuid, - username: Some(username), - password, - description: None, - password_hidden: true, - tx: Arc::new(Mutex::new(None)), - }); - } - - Message::Refresh - }) - } - - fn connect(&mut self, conn: zbus::Connection) -> Task { + fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| async move { let (tx, mut rx) = futures::channel::mpsc::channel(1); let watchers = std::pin::pin!(async move { - futures::join!( - network_manager::watch(conn.clone(), tx.clone()), - network_manager::active_conns::watch(conn.clone(), tx.clone()), - network_manager::devices::watch(conn, true, tx) - ) + network_manager::watch(conn, tx).await; }); let forwarder = std::pin::pin!(async move { @@ -1141,7 +1025,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -fn update_state(conn: zbus::Connection) -> Task { +fn update_state(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -1150,7 +1034,7 @@ fn update_state(conn: zbus::Connection) -> Task { }) } -fn update_devices(conn: zbus::Connection) -> Task { +fn update_devices(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { let filter = |device_type| matches!(device_type, network_manager::devices::DeviceType::WireGuard); @@ -1208,7 +1092,9 @@ fn add_network() -> Task { ); }; - super::nm_add_vpn_file("openvpn", path).await + network_manager::import_openvpn(path) + .await + .map_err(|why| why.to_string()) }; match result { @@ -1223,108 +1109,96 @@ fn add_network() -> Task { .apply(cosmic::task::future) } -fn connection_settings(conn: zbus::Connection) -> Task { - let settings = async move { - let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?; - - _ = settings.load_connections(&[]).await; - - let settings = settings - // Get a list of known connections. - .list_connections() - .await? - // Prepare for wrapping in a concurrent stream. - .into_iter() - .map(|conn| async move { conn }) - // Create a concurrent stream for each connection. - .apply(futures::stream::FuturesOrdered::from_iter) - // Concurrently fetch settings for each connection, and filter for VPN. - .filter_map(|conn| async move { - let settings = conn.get_settings().await.ok()?; - - let connection = settings.get("connection")?; - - match connection - .get("type")? - .downcast_ref::() - .ok()? - .as_str() - { - "vpn" => (), - - "wireguard" => { - let id = connection.get("id")?.downcast_ref::().ok()?; - let uuid = connection.get("uuid")?.downcast_ref::().ok()?; - return Some((Arc::from(uuid), ConnectionSettings::Wireguard { id })); - } - - _ => return None, - } - - let vpn = settings.get("vpn")?; - let id = connection.get("id")?.downcast_ref::().ok()?; - let uuid = connection.get("uuid")?.downcast_ref::().ok()?; - - let (connection_type, username, password_flag) = vpn - .get("data") - .and_then(|data| data.downcast_ref::().ok()) - .map(|dict| { - let (mut connection_type, mut password_flag) = (None, None); - let mut username = vpn - .get("user-name") - .and_then(|u| u.downcast_ref::().ok()); - if dict - .get::(&String::from("connection-type")) - .ok() - .flatten() - .as_deref() - // may be "password" or "password-tls" - .is_some_and(|p| p.starts_with("password")) - { - connection_type = Some(ConnectionType::Password); - username = Some(username.unwrap_or_default()); - - password_flag = dict - .get::(&String::from("password-flags")) - .ok() - .flatten() - .and_then(|value| match value.as_str() { - "0" => Some(PasswordFlag::None), - "1" => Some(PasswordFlag::AgentOwned), - "2" => Some(PasswordFlag::NotSaved), - "4" => Some(PasswordFlag::NotRequired), - _ => None, - }); - } - - (connection_type, username, password_flag) - }) - .unwrap_or_default(); - - Some(( - Arc::from(uuid), - ConnectionSettings::Vpn(VpnConnectionSettings { - id, - connection_type, - password_flag, - username, - }), - )) - }) - // Reduce the settings list into - .fold(IndexMap::new(), |mut set, (uuid, data)| async move { - set.insert(uuid, data); - set - }) - .await; - - Ok::<_, zbus::Error>(settings) - }; - +fn connection_settings(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { + let settings = async move { + let vpns = conn.list_vpn_connections().await?; + let mut settings = IndexMap::new(); + + for vpn in vpns { + let uuid = Arc::from(vpn.uuid.as_str()); + let data = match vpn.vpn_type { + nmrs::VpnType::WireGuard { .. } => ConnectionSettings::Wireguard { id: vpn.id }, + nmrs::VpnType::OpenVpn { + connection_type, + user_name, + password_flags, + .. + } => ConnectionSettings::Vpn(VpnConnectionSettings { + id: vpn.id, + username: user_name, + connection_type: connection_type + .filter(|ct| { + matches!( + ct, + nmrs::OpenVpnConnectionType::Password + | nmrs::OpenVpnConnectionType::PasswordTls + ) + }) + .map(|_| ConnectionType::Password), + password_flag: Some(password_flag(password_flags.0)), + }), + nmrs::VpnType::OpenConnect { + user_name, + password_flags, + .. + } + | nmrs::VpnType::StrongSwan { + user_name, + password_flags, + .. + } + | nmrs::VpnType::Pptp { + user_name, + password_flags, + .. + } + | nmrs::VpnType::L2tp { + user_name, + password_flags, + .. + } => ConnectionSettings::Vpn(VpnConnectionSettings { + id: vpn.id, + username: user_name, + connection_type: Some(ConnectionType::Password), + password_flag: Some(password_flag(password_flags.0)), + }), + nmrs::VpnType::Generic { + user_name, + password_flags, + .. + } => ConnectionSettings::Vpn(VpnConnectionSettings { + id: vpn.id, + username: user_name, + connection_type: Some(ConnectionType::Password), + password_flag: Some(password_flag(password_flags.0)), + }), + _ => ConnectionSettings::Vpn(VpnConnectionSettings { + id: vpn.id, + username: None, + connection_type: None, + password_flag: None, + }), + }; + settings.insert(uuid, data); + } + + Ok::<_, nmrs::ConnectionError>(settings) + }; + settings.await.map_or_else( |why| Message::Error(ErrorKind::ConnectionSettings, why.to_string()), Message::KnownConnections, ) }) } + +fn password_flag(value: u32) -> PasswordFlag { + match value { + 0 => PasswordFlag::None, + 1 => PasswordFlag::AgentOwned, + 2 => PasswordFlag::NotSaved, + 4 => PasswordFlag::NotRequired, + _ => PasswordFlag::AgentOwned, + } +} diff --git a/cosmic-settings/src/pages/networking/vpn/nmcli.rs b/cosmic-settings/src/pages/networking/vpn/nmcli.rs deleted file mode 100644 index c759c14..0000000 --- a/cosmic-settings/src/pages/networking/vpn/nmcli.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: GPL-3.0-only - -use cosmic::Apply; -use std::process::Stdio; - -pub async fn set_username(connection_name: &str, username: &str) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args(["con", "mod", connection_name, "vpn.user-name", username]) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} - -pub async fn add_fallback(connection_name: &str) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args([ - "con", - "mod", - connection_name, - "+vpn.data", - "data-ciphers=AES-256-GCM:AES-128-GCM:CHACHA20-POLY1305:AES-256-CBC:AES-128-CBC", - ]) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} - -pub async fn connect(connection_name: &str) -> Result<(), String> { - tokio::process::Command::new("nmcli") - .args(["con", "up", connection_name]) - .stderr(Stdio::piped()) - .output() - .await - .apply(crate::utils::map_stderr_output) -} diff --git a/cosmic-settings/src/pages/networking/wifi.rs b/cosmic-settings/src/pages/networking/wifi.rs index 53c0a37..a715fda 100644 --- a/cosmic-settings/src/pages/networking/wifi.rs +++ b/cosmic-settings/src/pages/networking/wifi.rs @@ -13,16 +13,15 @@ use cosmic::widget::space::horizontal; use cosmic::widget::text_input::focus; use cosmic::widget::{self, column, icon}; use cosmic::{Apply, Element, Task, task}; -use cosmic_settings_network_manager_subscription::available_wifi::{AccessPoint, NetworkType}; -use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo; -use cosmic_settings_network_manager_subscription::{ - self as network_manager, NetworkManagerState, nm_secret_agent, -}; use cosmic_settings_page::{self as page, Section, section}; use futures::{SinkExt, StreamExt}; use secure_string::SecureString; use tokio::sync::Mutex; +use super::backend as network_manager; +use super::backend::available_wifi::{AccessPoint, NetworkType}; +use super::backend::current_networks::ActiveConnectionInfo; +use super::backend::{NetworkManagerState, nm_secret_agent}; use crate::pages::networking::SecretSender; pub static SECURE_INPUT_WIFI: LazyLock = LazyLock::new(widget::Id::unique); @@ -55,8 +54,8 @@ pub enum Message { NetworkManager(network_manager::Event), /// An update from the secret agent SecretAgent(network_manager::nm_secret_agent::Event), - /// Successfully connected to the system dbus. - NetworkManagerConnect(zbus::Connection), + /// Successfully connected to NetworkManager. + NetworkManagerConnect(nmrs::NetworkManager), /// Request an auth dialog PasswordRequest(network_manager::SSID), /// Update the password from the dialog @@ -102,6 +101,7 @@ enum WiFiDialog { ssid: network_manager::SSID, identity: Option, password: SecureString, + network_type: NetworkType, password_hidden: bool, tx: SecretSender, }, @@ -141,7 +141,7 @@ pub struct Page { #[derive(Debug)] pub struct NmState { - conn: zbus::Connection, + conn: nmrs::NetworkManager, sender: futures::channel::mpsc::UnboundedSender, state: network_manager::NetworkManagerState, devices: Vec, @@ -294,19 +294,23 @@ impl page::Page for Page { let (tx, rx) = tokio::sync::mpsc::channel(4); self.secret_tx = Some(tx); if self.nm_task.is_none() { - return Task::batch(vec![cosmic::Task::future(async move { - zbus::Connection::system() - .await - .context("failed to create system dbus connection") - .map_or_else( - |why| Message::Error(why.to_string()), - Message::NetworkManagerConnect, - ) - .apply(crate::pages::Message::WiFi) - }), cosmic::Task::stream( - cosmic_settings_network_manager_subscription::nm_secret_agent::secret_agent_stream("com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent", rx), - ) - .map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m)))]); + return Task::batch(vec![ + cosmic::Task::future(async move { + nmrs::NetworkManager::new() + .await + .context("failed to connect to NetworkManager") + .map_or_else( + |why| Message::Error(why.to_string()), + Message::NetworkManagerConnect, + ) + .apply(crate::pages::Message::WiFi) + }), + cosmic::Task::stream(nm_secret_agent::secret_agent_stream( + "com.system76.CosmicSettings.WiFi.NetworkManager.SecretAgent", + rx, + )) + .map(|m| crate::pages::Message::WiFi(Message::SecretAgent(m))), + ]); } Task::none() @@ -352,15 +356,22 @@ impl Page { } match req { - network_manager::Request::Authenticate { ssid, identity, .. } => { + network_manager::Request::Authenticate { + ssid, + identity, + network_type, + .. + } => { if success { self.connecting.remove(ssid.as_str()); } else { + self.connecting.remove(ssid.as_str()); // Request to retry self.dialog = Some(WiFiDialog::Password { ssid: ssid.into(), identity, password: SecureString::from(""), + network_type, password_hidden: true, tx: Arc::new(Mutex::new(None)), }); @@ -377,11 +388,13 @@ impl Page { if success || matches!(network_type, NetworkType::Open) { self.connecting.remove(ssid.as_ref()); } else { + self.connecting.remove(ssid.as_ref()); self.dialog = Some(WiFiDialog::Password { ssid, identity: matches!(network_type, NetworkType::EAP) .then(String::new), password: SecureString::from(""), + network_type, password_hidden: true, tx: Arc::new(Mutex::new(None)), }); @@ -449,6 +462,7 @@ impl Page { let qr_string = if let Some(ref pass) = password { let security = match security_type { NetworkType::PskOrSae => "WPA", + NetworkType::Sae => "WPA", NetworkType::EAP => "WPA", NetworkType::Open => "", }; @@ -521,6 +535,7 @@ impl Page { ssid, identity: matches!(ap.network_type, NetworkType::EAP).then(String::new), password: SecureString::from(""), + network_type: ap.network_type, password_hidden: true, tx: Arc::new(Mutex::new(None)), }); @@ -544,6 +559,7 @@ impl Page { ssid, identity, password, + network_type, tx, .. } = dialog @@ -562,6 +578,7 @@ impl Page { ssid: ssid.to_string(), identity, password, + network_type, secret_tx, interface, }); @@ -614,6 +631,7 @@ impl Page { } Message::Disconnect(ssid) => { self.close_popup_and_apply_updates(); + self.connecting.remove(ssid.as_ref()); if let Some(nm) = self.nm_state.as_mut() { _ = nm .sender @@ -627,6 +645,7 @@ impl Page { Message::Forget(ssid) => { self.dialog = None; self.close_popup_and_apply_updates(); + self.connecting.remove(ssid.as_ref()); if let Some(nm) = self.nm_state.as_mut() { _ = nm .sender @@ -648,6 +667,9 @@ impl Page { } } Message::WiFiEnable(enable) => { + if !enable { + self.connecting.clear(); + } if let Some(nm) = self.nm_state.as_mut() { _ = nm .sender @@ -656,7 +678,9 @@ impl Page { } } Message::CancelDialog => { - self.dialog = None; + if let Some(WiFiDialog::Password { ssid, .. }) = self.dialog.take() { + self.connecting.remove(ssid.as_ref()); + } } Message::Error(why) => { tracing::error!(why); @@ -715,15 +739,20 @@ impl Page { password: previous, password_hidden: true, identity: matches!(ap.network_type, NetworkType::EAP).then(String::new), + network_type: ap.network_type, tx, }); return task::message(Message::FocusSecureInput); } nm_secret_agent::Event::CancelGetSecrets { uuid: _, name: _ } => { - self.dialog = self - .dialog - .take() - .filter(|d| !matches!(d, &WiFiDialog::Password { .. })); + match self.dialog.take() { + Some(WiFiDialog::Password { ssid, .. }) => { + self.connecting.remove(ssid.as_ref()); + } + other => { + self.dialog = other; + } + } } nm_secret_agent::Event::Failed(error) => { tracing::error!(%error, "secret agent failure"); @@ -731,15 +760,18 @@ impl Page { ssid, password, identity, + network_type, .. }) = self.dialog.take() { + self.connecting.remove(ssid.as_ref()); self.dialog = Some(WiFiDialog::Password { password, password_hidden: true, tx: Arc::new(Mutex::new(None)), ssid, identity, + network_type, }); return task::message(Message::FocusSecureInput); } @@ -770,19 +802,14 @@ impl Page { Task::none() } - fn connect(&mut self, conn: zbus::Connection) -> Task { + fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| async move { let (tx, mut rx) = futures::channel::mpsc::channel(1); let watchers = std::pin::pin!(async move { - futures::join!( - network_manager::watch(conn.clone(), tx.clone()), - network_manager::active_conns::watch(conn.clone(), tx.clone()), - network_manager::wireless_enabled::watch(conn.clone(), tx.clone()), - network_manager::watch_connections_changed(conn, tx) - ); + network_manager::watch(conn, tx).await; }); let forwarder = std::pin::pin!(async move { @@ -832,6 +859,12 @@ impl Page { /// Withholds updates if the view more popup is displayed. fn update_state(&mut self, state: NetworkManagerState) { + for active in &state.active_conns { + if let ActiveConnectionInfo::WiFi { name, .. } = active { + self.connecting.remove(name.as_str()); + } + } + if let Some(ref mut nm_state) = self.nm_state { if self.view_more_popup.is_some() { self.withheld_state = Some(state); @@ -1271,51 +1304,9 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -fn connection_settings(conn: zbus::Connection) -> Task { - let settings = async move { - let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?; - - _ = settings.load_connections(&[]).await; - - let settings = settings - // Get a list of known connections. - .list_connections() - .await? - // Prepare for wrapping in a concurrent stream. - .into_iter() - .map(|conn| async move { conn }) - // Create a concurrent stream for each connection. - .apply(futures::stream::FuturesOrdered::from_iter) - // Concurrently fetch settings for each connection. - .filter_map(|conn| async move { - conn.get_settings() - .await - .map(network_manager::Settings::new) - .ok() - }) - // Reduce the settings list into a SSID->UUID map. - .fold(BTreeMap::new(), |mut set, settings| async move { - if let Some(ref wifi) = settings.wifi - && let Some(ssid) = wifi - .ssid - .clone() - .and_then(|ssid| String::from_utf8(ssid).ok()) - && let Some(ref connection) = settings.connection - && let Some(uuid) = connection.uuid.clone() - { - set.insert(ssid.into(), uuid.into()); - return set; - } - - set - }) - .await; - - Ok::<_, zbus::Error>(settings) - }; - +fn connection_settings(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { - settings + network_manager::wifi_connection_settings(conn) .await .context("failed to get connection settings") .map_or_else( @@ -1326,7 +1317,7 @@ fn connection_settings(conn: zbus::Connection) -> Task { }) } -pub fn update_state(conn: zbus::Connection) -> Task { +pub fn update_state(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -1335,7 +1326,7 @@ pub fn update_state(conn: zbus::Connection) -> Task { }) } -pub fn update_devices(conn: zbus::Connection) -> Task { +pub fn update_devices(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { let filter = |device_type| matches!(device_type, network_manager::devices::DeviceType::Wifi); diff --git a/cosmic-settings/src/pages/networking/wired.rs b/cosmic-settings/src/pages/networking/wired.rs index 104d4a0..3a1bf86 100644 --- a/cosmic-settings/src/pages/networking/wired.rs +++ b/cosmic-settings/src/pages/networking/wired.rs @@ -10,12 +10,14 @@ use cosmic::iced::{Alignment, Length}; use cosmic::widget::space::horizontal as horizontal_space; use cosmic::widget::{self, icon}; use cosmic::{Apply, Element, Task}; -use cosmic_dbus_networkmanager::interface::enums::DeviceState; -use cosmic_settings_network_manager_subscription::current_networks::ActiveConnectionInfo; -use cosmic_settings_network_manager_subscription::{self as network_manager, NetworkManagerState}; use cosmic_settings_page::{self as page, Section, section}; use futures::{SinkExt, StreamExt}; +use super::backend as network_manager; +use super::backend::NetworkManagerState; +use super::backend::current_networks::ActiveConnectionInfo; +use super::backend::devices::DeviceState; + pub type ConnectionId = Arc; #[derive(Clone, Debug)] @@ -32,8 +34,8 @@ pub enum Message { Error(String), /// An update from the network manager daemon NetworkManager(network_manager::Event), - /// Successfully connected to the system dbus. - NetworkManagerConnect(zbus::Connection), + /// Successfully connected to NetworkManager. + NetworkManagerConnect(nmrs::NetworkManager), /// Refresh devices and their connection profiles Refresh, /// Create a dialog to ask for confirmation of removal. @@ -91,7 +93,7 @@ pub struct Page { #[derive(Debug)] pub struct NmState { - conn: zbus::Connection, + conn: nmrs::NetworkManager, sender: futures::channel::mpsc::UnboundedSender, active_conns: Vec, devices: Vec>, @@ -154,9 +156,9 @@ impl page::Page for Page { fn on_enter(&mut self) -> cosmic::Task { if self.nm_task.is_none() { return cosmic::task::future(async move { - zbus::Connection::system() + nmrs::NetworkManager::new() .await - .context("failed to create system dbus connection") + .context("failed to connect to NetworkManager") .map_or_else( |why| Message::Error(why.to_string()), Message::NetworkManagerConnect, @@ -356,18 +358,14 @@ impl Page { Task::none() } - fn connect(&mut self, conn: zbus::Connection) -> Task { + fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| async move { let (tx, mut rx) = futures::channel::mpsc::channel(1); let watchers = std::pin::pin!(async move { - futures::join!( - network_manager::watch(conn.clone(), tx.clone()), - network_manager::active_conns::watch(conn.clone(), tx.clone()), - network_manager::devices::watch(conn, true, tx) - ) + network_manager::watch(conn, tx).await; }); let forwarder = std::pin::pin!(async move { @@ -622,7 +620,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -fn update_state(conn: zbus::Connection) -> Task { +fn update_state(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -631,7 +629,7 @@ fn update_state(conn: zbus::Connection) -> Task { }) } -fn update_devices(conn: zbus::Connection) -> Task { +fn update_devices(conn: nmrs::NetworkManager) -> Task { cosmic::task::future(async move { let filter = |device_type| matches!(device_type, network_manager::devices::DeviceType::Ethernet); diff --git a/cosmic-settings/src/pages/sound/mod.rs b/cosmic-settings/src/pages/sound/mod.rs index 65b7b43..6f15634 100644 --- a/cosmic-settings/src/pages/sound/mod.rs +++ b/cosmic-settings/src/pages/sound/mod.rs @@ -3,20 +3,19 @@ pub mod device_profiles; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::Arc; - use cosmic::iced::{self, Alignment, Length, window}; use cosmic::widget::space::horizontal as horizontal_space; use cosmic::widget::{self, settings}; use cosmic::{Apply, Element, Task, surface}; use cosmic_config::{Config, ConfigGet, ConfigSet}; -use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy}; +use cosmic_settings_audio_client::{self as audio_client, Client, CosmicAudioProxy}; use cosmic_settings_page::{self as page, Section, section}; use cosmic_settings_sound::model; use futures::executor::block_on; use slotmap::SlotMap; +use std::sync::atomic::AtomicU32; +use std::sync::{Arc, atomic}; +use std::time::Duration; const AUDIO_CONFIG: &str = "com.system76.CosmicAudio"; const AMPLIFICATION_SINK: &str = "amplification_sink"; @@ -26,6 +25,8 @@ const AMPLIFICATION_SOURCE: &str = "amplification_source"; pub enum Message { /// Updates for the model. Model(cosmic_settings_sound::Message), + /// Reattach the client + ReattachClient(Arc), /// Change the default output. SetDefaultSink(usize), /// Change the default input output. @@ -63,9 +64,12 @@ impl From for crate::Message { pub struct Page { entity: page::Entity, device_profiles: page::Entity, - client: Option>>, + client: Option, model: model::Model, sound_config: Option, + applied_sink_volume: Arc, + applied_sink_volume_balance: Arc, + applied_source_volume: Arc, amplification_sink: bool, amplification_source: bool, } @@ -84,6 +88,9 @@ impl Default for Page { ..model::Model::default() }, sound_config: None, + applied_sink_volume: Arc::new(AtomicU32::new(0)), + applied_sink_volume_balance: Arc::new(AtomicU32::new(0)), + applied_source_volume: Arc::new(AtomicU32::new(0)), amplification_sink: false, amplification_source: false, } @@ -174,7 +181,7 @@ impl Page { Message::Model(cosmic_settings_sound::Message::Client(client)) => { if let Some(client) = Arc::into_inner(client) { - self.client = Some(Rc::new(RefCell::new(client))); + self.client = Some(client); self.model = model::Model { text: model::Text { hd_audio: fl!("sound-hd-audio"), @@ -185,13 +192,19 @@ impl Page { } } + Message::ReattachClient(client) => { + if let Some(client) = Arc::into_inner(client) { + self.client = Some(client); + } + } + Message::SetDefaultSink(pos) => { if let Some(&pos) = self.model.sinks.sorted_index.get(pos) && let Some(&node_id) = self.model.sinks.id.get(pos as usize) && let Some(client) = self.client.as_mut() { block_on(async { - _ = client.borrow_mut().conn.set_default(node_id, true).await; + _ = client.conn.set_default(node_id, true).await; }); } } @@ -202,7 +215,7 @@ impl Page { && let Some(client) = self.client.as_mut() { block_on(async { - _ = client.borrow_mut().conn.set_default(node_id, true).await; + _ = client.conn.set_default(node_id, true).await; }); } } @@ -210,7 +223,7 @@ impl Page { Message::ToggleSinkMute => { if let Some(ref mut client) = self.client { block_on(async { - _ = client.borrow_mut().conn.sink_mute_toggle().await; + _ = client.conn.sink_mute_toggle().await; }); } } @@ -218,40 +231,64 @@ impl Page { Message::ToggleSourceMute => { if let Some(ref mut client) = self.client { block_on(async { - _ = client.borrow_mut().conn.source_mute_toggle().await; + _ = client.conn.source_mute_toggle().await; }); } } Message::SetSinkVolume(volume) => { - if let Some(ref mut client) = self.client { - self.model.active_sink.volume = volume; - self.model.active_sink.volume_text = volume.to_string(); - block_on(async { - _ = client.borrow_mut().conn.set_sink_volume(volume).await; + self.applied_sink_volume + .store(volume, atomic::Ordering::Relaxed); + self.model.active_sink.volume = volume; + self.model.active_sink.volume_text = volume.to_string(); + if let Some(mut client) = self.client.take() { + let volume = Arc::clone(&self.applied_sink_volume); + return cosmic::Task::future(async move { + tokio::time::sleep(Duration::from_millis(128)).await; + _ = client + .conn + .set_sink_volume(volume.load(atomic::Ordering::Relaxed)) + .await; + Message::ReattachClient(Arc::new(client)).into() }); } } Message::SetSourceVolume(volume) => { - if let Some(ref mut client) = self.client { - self.model.active_source.volume = volume; - self.model.active_source.volume_text = volume.to_string(); - block_on(async { - _ = client.borrow_mut().conn.set_source_volume(volume).await; + self.applied_source_volume + .store(volume, atomic::Ordering::Relaxed); + self.model.active_source.volume = volume; + self.model.active_source.volume_text = volume.to_string(); + if let Some(mut client) = self.client.take() { + let volume = Arc::clone(&self.applied_source_volume); + return cosmic::Task::future(async move { + tokio::time::sleep(Duration::from_millis(128)).await; + _ = client + .conn + .set_sink_volume(volume.load(atomic::Ordering::Relaxed)) + .await; + Message::ReattachClient(Arc::new(client)).into() }); } } Message::SetSinkBalance(balance) => { - if let Some((client, sink_id)) = self.client.as_ref().zip(self.model.default_sink) { - self.model.active_sink.balance = Some(balance); - block_on(async { + self.applied_sink_volume_balance + .store((balance * 1000.0) as u32, atomic::Ordering::Relaxed); + self.model.active_sink.balance = Some(balance); + if let Some((mut client, sink_id)) = self.client.take().zip(self.model.default_sink) + { + let balance = Arc::clone(&self.applied_sink_volume_balance); + return cosmic::Task::future(async move { + tokio::time::sleep(Duration::from_millis(128)).await; _ = client - .borrow_mut() .conn - .set_node_volume_balance(sink_id, Some(balance)) + .set_node_volume_balance( + sink_id, + Some(balance.load(atomic::Ordering::Relaxed) as f32 / 1000.0), + ) .await; + Message::ReattachClient(Arc::new(client)).into() }); } } diff --git a/subscriptions/network-manager/Cargo.toml b/subscriptions/network-manager/Cargo.toml deleted file mode 100644 index e40a2a8..0000000 --- a/subscriptions/network-manager/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "cosmic-settings-network-manager-subscription" -version = "1.0.7" -edition = "2024" -license = "MPL-2.0" -rust-version.workspace = true -publish = true - -[dependencies] -cosmic-dbus-networkmanager = { git = "https://github.com/pop-os/dbus-settings-bindings" } -secret-service = { version = "5.1.0", features = ["rt-tokio-crypto-rust"] } -nm-secret-agent-manager = { git = "https://github.com/pop-os/dbus-settings-bindings" } -futures = "0.3.32" -iced_futures = { git = "https://github.com/pop-os/libcosmic" } -itertools = "0.14.0" -secure-string = "0.3.0" -thiserror = "2.0.18" -tokio = "1.49.0" -tracing = "0.1.44" -zbus = { version = "5.13.2", features = ["tokio"] } -bitflags = "2.11.0" diff --git a/subscriptions/network-manager/LICENSE.md b/subscriptions/network-manager/LICENSE.md deleted file mode 100644 index 8dc5b15..0000000 --- a/subscriptions/network-manager/LICENSE.md +++ /dev/null @@ -1,359 +0,0 @@ -Mozilla Public License Version 2.0 -================================== - -## 1. Definitions - -### 1.1. "Contributor" -means each individual or legal entity that creates, contributes to -the creation of, or owns Covered Software. - -### 1.2. "Contributor Version" -means the combination of the Contributions of others (if any) used -by a Contributor and that particular Contributor's Contribution. - -### 1.3. "Contribution" -means Covered Software of a particular Contributor. - -### 1.4. "Covered Software" -means Source Code Form to which the initial Contributor has attached -the notice in Exhibit A, the Executable Form of such Source Code -Form, and Modifications of such Source Code Form, in each case -including portions thereof. - -### 1.5. "Incompatible With Secondary Licenses" -means - -+ (a) that the initial Contributor has attached the notice described -in Exhibit B to the Covered Software; or - -+ (b) that the Covered Software was made available under the terms of -version 1.1 or earlier of the License, but not also under the -terms of a Secondary License. - -### 1.6. "Executable Form" -means any form of the work other than Source Code Form. - -### 1.7. "Larger Work" -means a work that combines Covered Software with other material, in -a separate file or files, that is not Covered Software. - -### 1.8. "License" -means this document. - -### 1.9. "Licensable" -means having the right to grant, to the maximum extent possible, -whether at the time of the initial grant or subsequently, any and -all of the rights conveyed by this License. - -### 1.10. "Modifications" -means any of the following: - -+ (a) any file in Source Code Form that results from an addition to, -deletion from, or modification of the contents of Covered -Software; or - -+ (b) any new file in Source Code Form that contains any Covered -Software. - -### 1.11. "Patent Claims" of a Contributor -means any patent claim(s), including without limitation, method, -process, and apparatus claims, in any patent Licensable by such -Contributor that would be infringed, but for the grant of the -License, by the making, using, selling, offering for sale, having -made, import, or transfer of either its Contributions or its -Contributor Version. - -### 1.12. "Secondary License" -means either the GNU General Public License, Version 2.0, the GNU -Lesser General Public License, Version 2.1, the GNU Affero General -Public License, Version 3.0, or any later versions of those -licenses. - -### 1.13. "Source Code Form" -means the form of the work preferred for making modifications. - -### 1.14. "You" (or "Your") -means an individual or a legal entity exercising rights under this -License. For legal entities, "You" includes any entity that -controls, is controlled by, or is under common control with You. For -purposes of this definition, "control" means (a) the power, direct -or indirect, to cause the direction or management of such entity, -whether by contract or otherwise, or (b) ownership of more than -fifty percent (50%) of the outstanding shares or beneficial -ownership of such entity. - -## 2. License Grants and Conditions - -### 2.1. Grants - -Each Contributor hereby grants You a world-wide, royalty-free, -non-exclusive license: - -+ (a) under intellectual property rights (other than patent or trademark) -Licensable by such Contributor to use, reproduce, make available, -modify, display, perform, distribute, and otherwise exploit its -Contributions, either on an unmodified basis, with Modifications, or -as part of a Larger Work; and - -+ (b) under Patent Claims of such Contributor to make, use, sell, offer -for sale, have made, import, and otherwise transfer either its -Contributions or its Contributor Version. - -### 2.2. Effective Date - -The licenses granted in Section 2.1 with respect to any Contribution -become effective for each Contribution on the date the Contributor first -distributes such Contribution. - -### 2.3. Limitations on Grant Scope - -The licenses granted in this Section 2 are the only rights granted under -this License. No additional rights or licenses will be implied from the -distribution or licensing of Covered Software under this License. -Notwithstanding Section 2.1(b) above, no patent license is granted by a -Contributor: - -+ (a) for any code that a Contributor has removed from Covered Software; -or - -+ (b) for infringements caused by: (i) Your and any other third party's -modifications of Covered Software, or (ii) the combination of its -Contributions with other software (except as part of its Contributor -Version); or - -+ (c) under Patent Claims infringed by Covered Software in the absence of -its Contributions. - -This License does not grant any rights in the trademarks, service marks, -or logos of any Contributor (except as may be necessary to comply with -the notice requirements in Section 3.4). - -### 2.4. Subsequent Licenses - -No Contributor makes additional grants as a result of Your choice to -distribute the Covered Software under a subsequent version of this -License (see Section 10.2) or under the terms of a Secondary License (if -permitted under the terms of Section 3.3). - -### 2.5. Representation - -Each Contributor represents that the Contributor believes its -Contributions are its original creation(s) or it has sufficient rights -to grant the rights to its Contributions conveyed by this License. - -### 2.6. Fair Use - -This License is not intended to limit any rights You have under -applicable copyright doctrines of fair use, fair dealing, or other -equivalents. - -### 2.7. Conditions - -Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted -in Section 2.1. - -## 3. Responsibilities - -### 3.1. Distribution of Source Form - -All distribution of Covered Software in Source Code Form, including any -Modifications that You create or to which You contribute, must be under -the terms of this License. You must inform recipients that the Source -Code Form of the Covered Software is governed by the terms of this -License, and how they can obtain a copy of this License. You may not -attempt to alter or restrict the recipients' rights in the Source Code -Form. - -### 3.2. Distribution of Executable Form - -If You distribute Covered Software in Executable Form then: - -+ (a) such Covered Software must also be made available in Source Code -Form, as described in Section 3.1, and You must inform recipients of -the Executable Form how they can obtain a copy of such Source Code -Form by reasonable means in a timely manner, at a charge no more -than the cost of distribution to the recipient; and - -+ (b) You may distribute such Executable Form under the terms of this -License, or sublicense it under different terms, provided that the -license for the Executable Form does not attempt to limit or alter -the recipients' rights in the Source Code Form under this License. - -### 3.3. Distribution of a Larger Work - -You may create and distribute a Larger Work under terms of Your choice, -provided that You also comply with the requirements of this License for -the Covered Software. If the Larger Work is a combination of Covered -Software with a work governed by one or more Secondary Licenses, and the -Covered Software is not Incompatible With Secondary Licenses, this -License permits You to additionally distribute such Covered Software -under the terms of such Secondary License(s), so that the recipient of -the Larger Work may, at their option, further distribute the Covered -Software under the terms of either this License or such Secondary -License(s). - -### 3.4. Notices - -You may not remove or alter the substance of any license notices -(including copyright notices, patent notices, disclaimers of warranty, -or limitations of liability) contained within the Source Code Form of -the Covered Software, except that You may alter any license notices to -the extent required to remedy known factual inaccuracies. - -### 3.5. Application of Additional Terms - -You may choose to offer, and to charge a fee for, warranty, support, -indemnity or liability obligations to one or more recipients of Covered -Software. However, You may do so only on Your own behalf, and not on -behalf of any Contributor. You must make it absolutely clear that any -such warranty, support, indemnity, or liability obligation is offered by -You alone, and You hereby agree to indemnify every Contributor for any -liability incurred by such Contributor as a result of warranty, support, -indemnity or liability terms You offer. You may include additional -disclaimers of warranty and limitations of liability specific to any -jurisdiction. - -## 4. Inability to Comply Due to Statute or Regulation - -If it is impossible for You to comply with any of the terms of this -License with respect to some or all of the Covered Software due to -statute, judicial order, or regulation then You must: (a) comply with -the terms of this License to the maximum extent possible; and (b) -describe the limitations and the code they affect. Such description must -be placed in a text file included with all distributions of the Covered -Software under this License. Except to the extent prohibited by statute -or regulation, such description must be sufficiently detailed for a -recipient of ordinary skill to be able to understand it. - -## 5. Termination - -5.1. The rights granted under this License will terminate automatically -if You fail to comply with any of its terms. However, if You become -compliant, then the rights granted under this License from a particular -Contributor are reinstated (a) provisionally, unless and until such -Contributor explicitly and finally terminates Your grants, and (b) on an -ongoing basis, if such Contributor fails to notify You of the -non-compliance by some reasonable means prior to 60 days after You have -come back into compliance. Moreover, Your grants from a particular -Contributor are reinstated on an ongoing basis if such Contributor -notifies You of the non-compliance by some reasonable means, this is the -first time You have received notice of non-compliance with this License -from such Contributor, and You become compliant prior to 30 days after -Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent -infringement claim (excluding declaratory judgment actions, -counter-claims, and cross-claims) alleging that a Contributor Version -directly or indirectly infringes any patent, then the rights granted to -You by any and all Contributors for the Covered Software under Section -2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all -end user license agreements (excluding distributors and resellers) which -have been validly granted by You or Your distributors under this License -prior to termination shall survive termination. - - -## 6. Disclaimer of Warranty - -**Covered Software is provided under this License on an "as is" -basis, without warranty of any kind, either expressed, implied, or -statutory, including, without limitation, warranties that the -Covered Software is free of defects, merchantable, fit for a -particular purpose or non-infringing. The entire risk as to the -quality and performance of the Covered Software is with You. -Should any Covered Software prove defective in any respect, You -(not any Contributor) assume the cost of any necessary servicing, -repair, or correction. This disclaimer of warranty constitutes an -essential part of this License. No use of any Covered Software is -authorized under this License except under this disclaimer.** - - -#7. Limitation of Liability - -**Under no circumstances and under no legal theory, whether tort -(including negligence), contract, or otherwise, shall any -Contributor, or anyone who distributes Covered Software as -permitted above, be liable to You for any direct, indirect, -special, incidental, or consequential damages of any character -including, without limitation, damages for lost profits, loss of -goodwill, work stoppage, computer failure or malfunction, or any -and all other commercial damages or losses, even if such party -shall have been informed of the possibility of such damages. This -limitation of liability shall not apply to liability for death or -personal injury resulting from such party's negligence to the -extent applicable law prohibits such limitation. Some -jurisdictions do not allow the exclusion or limitation of -incidental or consequential damages, so this exclusion and -limitation may not apply to You.** - - -## 8. Litigation - -Any litigation relating to this License may be brought only in the -courts of a jurisdiction where the defendant maintains its principal -place of business and such litigation shall be governed by laws of that -jurisdiction, without reference to its conflict-of-law provisions. -Nothing in this Section shall prevent a party's ability to bring -cross-claims or counter-claims. - -## 9. Miscellaneous - -This License represents the complete agreement concerning the subject -matter hereof. If any provision of this License is held to be -unenforceable, such provision shall be reformed only to the extent -necessary to make it enforceable. Any law or regulation which provides -that the language of a contract shall be construed against the drafter -shall not be used to construe this License against a Contributor. - -## 10. Versions of the License - -### 10.1. New Versions - -Mozilla Foundation is the license steward. Except as provided in Section -10.3, no one other than the license steward has the right to modify or -publish new versions of this License. Each version will be given a -distinguishing version number. - -### 10.2. Effect of New Versions - -You may distribute the Covered Software under the terms of the version -of the License under which You originally received the Covered Software, -or under the terms of any subsequent version published by the license -steward. - -### 10.3. Modified Versions - -If you create software not governed by this License, and you want to -create a new license for such software, you may create and use a -modified version of this License if you rename the license and remove -any references to the name of the license steward (except to note that -such modified license differs from this License). - -### 10.4. Distributing Source Code Form that is Incompatible With Secondary -Licenses - -If You choose to distribute Source Code Form that is Incompatible With -Secondary Licenses under the terms of this version of the License, the -notice described in Exhibit B of this License must be attached. - -## Exhibit A - Source Code Form License Notice - - - This Source Code Form is subject to the terms of the Mozilla Public - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular -file, then You may include the notice in a location (such as a LICENSE -file in a relevant directory) where a recipient would be likely to look -for such a notice. - -You may add additional accurate notices of copyright ownership. - -## Exhibit B - "Incompatible With Secondary Licenses" Notice - - - This Source Code Form is "Incompatible With Secondary Licenses", as - defined by the Mozilla Public License, v. 2.0. - diff --git a/subscriptions/network-manager/src/active_conns.rs b/subscriptions/network-manager/src/active_conns.rs deleted file mode 100644 index 63af460..0000000 --- a/subscriptions/network-manager/src/active_conns.rs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::Wrapper; - -use super::Event; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use iced_futures::{Subscription, stream}; -use std::fmt::Debug; -use std::hash::Hash; -use zbus::Connection; - -#[derive(Debug, Clone)] -pub enum State { - Continue(Connection), - Error, -} - -pub fn active_conns_subscription( - id: I, - conn: Connection, -) -> iced_futures::Subscription { - Subscription::run_with(Wrapper { id, conn: conn }, |Wrapper { id: _id, conn }| { - let conn = conn.clone(); - stream::channel(50, move |output| async move { - watch(conn, output).await; - futures::future::pending().await - }) - }) -} - -pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender) { - let mut state = State::Continue(conn); - - loop { - state = start_listening(state, &mut output).await; - } -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - let conn = match state { - State::Continue(conn) => conn, - State::Error => futures::future::pending().await, - }; - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(why) => { - tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager"); - return State::Error; - } - }; - - let mut active_conns_changed = network_manager.receive_active_connections_changed().await; - active_conns_changed.next().await; - - while let (Some(_change), _) = futures::future::join( - active_conns_changed.next(), - tokio::time::sleep(tokio::time::Duration::from_secs(1)), - ) - .await - { - _ = output.send(Event::ActiveConns).await; - } - - State::Continue(conn) -} diff --git a/subscriptions/network-manager/src/available_wifi.rs b/subscriptions/network-manager/src/available_wifi.rs deleted file mode 100644 index 0653ae1..0000000 --- a/subscriptions/network-manager/src/available_wifi.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use cosmic_dbus_networkmanager::device::wireless::WirelessDevice; -use cosmic_dbus_networkmanager::interface::enums::{ApFlags, ApSecurityFlags, DeviceState}; - -use futures::StreamExt; -use itertools::Itertools; -use std::collections::HashMap; -use std::sync::Arc; -use zbus::zvariant::ObjectPath; - -use super::hw_address::HwAddress; - -pub async fn handle_wireless_device( - device: WirelessDevice<'_>, - hw_address: Option, -) -> zbus::Result> { - device.request_scan(HashMap::new()).await?; - - let mut scan_changed = device.receive_last_scan_changed().await; - - if let Some(t) = scan_changed.next().await - && let Ok(-1) = t.get().await - { - tracing::error!("wireless device scan errored"); - return Ok(Default::default()); - } - - let access_points = device.get_access_points().await?; - - let state: DeviceState = device - .upcast() - .await - .and_then(|dev| dev.cached_state()) - .unwrap_or_default() - .map(|s| s.into()) - .unwrap_or_else(|| DeviceState::Unknown); - - // Sort by strength and remove duplicates - let mut aps = HashMap::::new(); - for ap in access_points { - let (ssid_res, strength_res) = futures::join!(ap.ssid(), ap.strength()); - - if let Some((ssid, strength)) = ssid_res.ok().zip(strength_res.ok()) { - let ssid = String::from_utf8_lossy(&ssid.clone()).into_owned(); - if let Some(access_point) = aps.get(&ssid) - && access_point.strength > strength - { - continue; - } - - let Ok(flags) = ap.rsn_flags().await else { - continue; - }; - let network_type = if flags.intersects(ApSecurityFlags::KEY_MGMT_802_1X) { - NetworkType::EAP - } else if flags.intersects(ApSecurityFlags::KEY_MGMTPSK | ApSecurityFlags::KEY_MGMT_SAE) - { - NetworkType::PskOrSae - } else if flags.intersects(ApSecurityFlags::KEY_MGMT_OWE) || flags.is_empty() { - NetworkType::Open - } else { - continue; - }; - - aps.insert( - ssid.clone(), - AccessPoint { - ssid: Arc::from(ssid), - strength, - state, - working: false, - path: ap.inner().path().to_owned(), - secured: !ap.wpa_flags().await?.is_empty(), - wps_push: ap.flags().await?.contains(ApFlags::WPS_PBC), - network_type, - hw_address: hw_address - .as_ref() - .and_then(|str_addr| HwAddress::from_str(str_addr)) - .unwrap_or_default(), - }, - ); - } - } - - let aps = aps - .into_values() - .sorted_by(|a, b| b.strength.cmp(&a.strength)) - .collect(); - - Ok(aps) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AccessPoint { - pub ssid: Arc, - pub strength: u8, - pub state: DeviceState, - pub working: bool, - pub path: ObjectPath<'static>, - pub hw_address: HwAddress, - pub secured: bool, - pub wps_push: bool, - pub network_type: NetworkType, -} - -// TODO do we want to support eap methods other than peap in the applet? -// Then we'd need a dropdown for the eap method, -// and tls requires a cert instead of a password -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NetworkType { - Open, - PskOrSae, - EAP, -} diff --git a/subscriptions/network-manager/src/current_networks.rs b/subscriptions/network-manager/src/current_networks.rs deleted file mode 100644 index 45ac95d..0000000 --- a/subscriptions/network-manager/src/current_networks.rs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use cosmic_dbus_networkmanager::active_connection::ActiveConnection; -use cosmic_dbus_networkmanager::device::SpecificDevice; -use cosmic_dbus_networkmanager::interface::enums::ActiveConnectionState; -use std::net::Ipv4Addr; - -pub async fn active_connections( - active_connections: Vec>, -) -> zbus::Result> { - let mut info = Vec::::with_capacity(active_connections.len()); - for connection in active_connections { - let ipv4 = connection - .ip4_config() - .await? - .address_data() - .await - .unwrap_or_default(); - let addresses: Vec<_> = ipv4.iter().map(|d| d.address).collect(); - let state = connection - .state() - .await - .unwrap_or(ActiveConnectionState::Unknown); - - if connection.vpn().await.unwrap_or_default() { - info.push(ActiveConnectionInfo::Vpn { - name: connection.id().await?, - ip_addresses: addresses.clone(), - }); - continue; - } - for device in connection.devices().await.unwrap_or_default() { - match device - .downcast_to_device() - .await - .ok() - .and_then(|inner| inner) - { - Some(SpecificDevice::Wired(wired_device)) => { - info.push(ActiveConnectionInfo::Wired { - name: connection.id().await?, - hw_address: wired_device.hw_address().await?, - speed: wired_device.speed().await?, - ip_addresses: addresses.clone(), - }); - } - Some(SpecificDevice::Wireless(wireless_device)) => { - if let Ok(access_point) = wireless_device.active_access_point().await { - info.push(ActiveConnectionInfo::WiFi { - name: String::from_utf8_lossy(&access_point.ssid().await?).into_owned(), - ip_addresses: addresses.clone(), - hw_address: wireless_device.hw_address().await?, - state, - strength: access_point.strength().await.unwrap_or_default(), - }); - } - } - Some(SpecificDevice::WireGuard(_)) => { - info.push(ActiveConnectionInfo::Vpn { - name: connection.id().await?, - ip_addresses: addresses.clone(), - }); - } - _ => {} - } - } - } - - info.sort_by(|a, b| { - let helper = |conn: &ActiveConnectionInfo| match conn { - ActiveConnectionInfo::Vpn { name, .. } => format!("0{name}"), - ActiveConnectionInfo::Wired { name, .. } => format!("1{name}"), - ActiveConnectionInfo::WiFi { name, .. } => format!("2{name}"), - }; - helper(a).cmp(&helper(b)) - }); - - Ok(info) -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ActiveConnectionInfo { - Wired { - name: String, - hw_address: String, - speed: u32, - ip_addresses: Vec, - }, - WiFi { - name: String, - ip_addresses: Vec, - hw_address: String, - state: ActiveConnectionState, - strength: u8, - }, - Vpn { - name: String, - ip_addresses: Vec, - }, -} - -impl ActiveConnectionInfo { - pub fn name(&self) -> String { - match &self { - Self::Wired { name, .. } => name.clone(), - Self::WiFi { name, .. } => name.clone(), - Self::Vpn { name, .. } => name.clone(), - } - } -} diff --git a/subscriptions/network-manager/src/devices.rs b/subscriptions/network-manager/src/devices.rs deleted file mode 100644 index 4dbcdb6..0000000 --- a/subscriptions/network-manager/src/devices.rs +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::Event; -pub use cosmic_dbus_networkmanager::interface::enums::{ - ActiveConnectionState, DeviceState, DeviceType, -}; - -use core::hash; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use iced_futures::{self, Subscription, stream}; -use std::fmt::Debug; -use std::hash::Hash; -use std::sync::Arc; -use zbus::Connection; -use zbus::zvariant::ObjectPath; - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct DeviceInfo { - pub path: ObjectPath<'static>, - pub device_type: DeviceType, - pub interface: String, - pub state: DeviceState, - pub active_connection: Option<(DeviceConnection, ActiveConnectionState)>, - pub available_connections: Vec, - pub known_connections: Vec, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct DeviceConnection { - pub path: ObjectPath<'static>, - pub id: String, - pub uuid: Arc, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct KnownDeviceConnection { - pub id: String, - pub uuid: Arc, -} - -pub async fn list( - conn: &zbus::Connection, - device_type_filter: fn(DeviceType) -> bool, -) -> zbus::Result> { - let nm = NetworkManager::new(conn).await?; - - let (devices, nm_settings) = futures::try_join!(nm.devices(), nm.settings())?; - - let connection_settings: &Vec<_> = &futures::stream::FuturesOrdered::from_iter( - nm_settings - .list_connections() - .await? - .into_iter() - .map(|connection| async move { connection.get_settings().await }), - ) - .filter_map(|res| async move { res.ok() }) - .collect() - .await; - - let device_iter = devices.into_iter().map(|device| async move { - let (interface, hw_address, device_type, state, available_connections) = - futures::try_join!( - device.interface(), - device.hw_address(), - device.device_type(), - device.state(), - device.available_connections() - ) - .ok()?; - - if !device_type_filter(device_type) { - return None; - } - - if hw_address.is_empty() { - return None; - } - - let (active_connection, available_connections) = futures::join!( - async { - let connection = device.active_connection().await?; - - let (id, uuid, state) = - futures::try_join!(connection.id(), connection.uuid(), connection.state())?; - - Ok::<_, zbus::Error>(( - DeviceConnection { - id, - uuid: Arc::from(uuid), - path: connection.inner().path().to_owned(), - }, - state, - )) - }, - futures::stream::FuturesOrdered::from_iter(available_connections.into_iter().map( - |conn| async move { - let path = conn.inner().path().to_owned(); - - let settings = conn.get_settings().await.ok()?; - - let id = settings - .get("connection")? - .get("id")? - .downcast_ref::() - .ok()?; - - let uuid = settings["connection"] - .get("uuid")? - .downcast_ref::() - .ok()?; - - Some(DeviceConnection { - id, - uuid: Arc::from(uuid), - path, - }) - } - ),) - .filter_map(|res| async move { res }) - .collect::>() - ); - - let known_connections = connection_settings - .iter() - .flat_map(|conn_settings| { - let connection = conn_settings.get("connection")?; - - let interface_name = connection - .get("interface-name")? - .downcast_ref::() - .ok()?; - - if interface_name != interface { - return None; - } - - let id = connection.get("id")?.downcast_ref::().ok()?; - let uuid = connection.get("uuid")?.downcast_ref::().ok()?; - - Some(KnownDeviceConnection { - uuid: Arc::from(uuid), - id, - }) - }) - .collect(); - - Some(DeviceInfo { - path: device.inner().path().to_owned(), - device_type, - interface, - state, - active_connection: active_connection.ok(), - known_connections, - available_connections, - }) - }); - - let devices_info = futures::stream::FuturesOrdered::from_iter(device_iter) - .filter_map(|res| async move { res }) - .collect::>() - .await; - - Ok(devices_info) -} - -pub fn subscription( - id: I, - has_popup: bool, - conn: Connection, -) -> iced_futures::Subscription { - struct Wrapper { - id: I, - has_popup: bool, - conn: Connection, - } - impl Hash for Wrapper { - fn hash(&self, state: &mut H) { - self.id.hash(state); - self.has_popup.hash(state); - } - } - Subscription::run_with( - Wrapper { - id, - has_popup, - conn, - }, - |Wrapper { - id, - has_popup, - conn, - }| { - let conn = conn.clone(); - let has_popup = *has_popup; - stream::channel(50, move |output| async move { - watch(conn, has_popup, output).await; - futures::future::pending().await - }) - }, - ) -} - -pub async fn watch( - conn: zbus::Connection, - has_popup: bool, - mut output: futures::channel::mpsc::Sender, -) { - let mut state = State::Continue(conn); - - loop { - state = start_listening(state, has_popup, &mut output).await; - } -} - -#[derive(Debug, Clone)] -pub enum State { - Continue(Connection), - Error, -} - -async fn start_listening( - state: State, - has_popup: bool, - output: &mut futures::channel::mpsc::Sender, -) -> State { - let conn = match state { - State::Continue(conn) => conn, - State::Error => futures::future::pending().await, - }; - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(why) => { - tracing::error!( - why = why.to_string(), - "failed to connect to network_manager" - ); - return State::Error; - } - }; - - let mut devices_changed = network_manager.receive_devices_changed().await; - - let secs = if has_popup { 4 } else { 60 }; - - while let (Some(_change), _) = futures::future::join( - devices_changed.next(), - tokio::time::sleep(tokio::time::Duration::from_secs(secs)), - ) - .await - { - _ = output.send(Event::Devices).await; - } - - State::Continue(conn) -} diff --git a/subscriptions/network-manager/src/hw_address.rs b/subscriptions/network-manager/src/hw_address.rs deleted file mode 100644 index eb49769..0000000 --- a/subscriptions/network-manager/src/hw_address.rs +++ /dev/null @@ -1,102 +0,0 @@ -#[derive(Clone, PartialEq, Eq, Default, Debug, PartialOrd, Ord)] -pub struct HwAddress { - octets: Vec, -} - -impl HwAddress { - pub fn from_str(arg: &str) -> Option { - let segments: Vec<&str> = arg.split(":").collect(); - - // Only accept 6-byte (EUI-48) or 8-byte (EUI-64) addresses - if segments.len() != 6 && segments.len() != 8 { - return None; - } - - let mut octets: Vec = Vec::new(); - for segment in segments { - if segment.len() != 2 { - return None; - } - let byte: u8 = u8::from_str_radix(segment, 16).ok()?; - octets.push(byte); - } - - Some(HwAddress { octets }) - } -} - -impl std::fmt::Display for HwAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let hex_parts: Vec = self - .octets - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect(); - write!(f, "{}", hex_parts.join(":")) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_valid_6_byte_mac() { - let mac: &str = "00:11:22:33:44:55"; - let hw_addr: HwAddress = HwAddress::from_str(mac).expect("should parse valid MAC"); - - // Access the internal octets field - assert_eq!(hw_addr.octets.len(), 6); - assert_eq!(hw_addr.octets, vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55]); - } - - #[test] - fn test_display_6_byte_mac() { - let hw_addr: HwAddress = HwAddress { - octets: vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55], - }; - - assert_eq!(format!("{}", hw_addr), "00:11:22:33:44:55"); - } - - #[test] - fn test_parse_valid_8_byte_mac() { - let mac: &str = "00:11:22:33:44:55:66:77"; - let hw_addr: HwAddress = HwAddress::from_str(mac).expect("should parse valid EUI-64 MAC"); - - assert_eq!(hw_addr.octets.len(), 8); - assert_eq!( - hw_addr.octets, - vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77] - ); - } - - #[test] - fn test_display_8_byte_mac() { - let hw_addr: HwAddress = HwAddress { - octets: vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77], - }; - - assert_eq!(format!("{}", hw_addr), "00:11:22:33:44:55:66:77"); - } - - #[test] - fn test_reject_invalid_length_macs() { - let invalid_macs: Vec<(&str, &str)> = vec![ - ("00", "1-byte MAC"), - ("00:11:22:33", "4-byte MAC"), - ("00:11:22:33:44", "5-byte MAC"), - ("00:11:22:33:44:55:66", "7-byte MAC"), - ("00:11:22:33:44:55:66:77:88", "9-byte MAC"), - ("00:11:22:33:44:55:66:77:88:99:aa:bb", "12-byte MAC"), - ]; - - for (mac, description) in invalid_macs { - assert!( - HwAddress::from_str(mac).is_none(), - "should reject {}", - description - ); - } - } -} diff --git a/subscriptions/network-manager/src/lib.rs b/subscriptions/network-manager/src/lib.rs deleted file mode 100644 index 675b7bf..0000000 --- a/subscriptions/network-manager/src/lib.rs +++ /dev/null @@ -1,1169 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -pub mod active_conns; -pub mod available_wifi; -pub mod current_networks; -pub mod devices; -pub mod hw_address; -pub mod nm_secret_agent; -pub mod wireless_enabled; - -use std::collections::HashMap; -use std::fmt::Debug; -use std::hash::Hash; -use std::sync::Arc; -use std::time::Duration; - -use available_wifi::NetworkType; -pub use cosmic_dbus_networkmanager as dbus; -pub use dbus::settings::connection::Settings; - -use cosmic_dbus_networkmanager::device::SpecificDevice; -use cosmic_dbus_networkmanager::interface::enums::{ - self, ActiveConnectionState, DeviceType, NmConnectivityState, -}; -use cosmic_dbus_networkmanager::interface::settings::connection::ConnectionSettingsProxy; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use cosmic_dbus_networkmanager::settings::NetworkManagerSettings; -use cosmic_dbus_networkmanager::settings::connection::Connection; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}; -use futures::{FutureExt, SinkExt, StreamExt}; -use iced_futures::{Subscription, stream}; -use secure_string::SecureString; -use tokio::process::Command; -use zbus::zvariant::{self, ObjectPath, Value}; - -use self::available_wifi::{AccessPoint, handle_wireless_device}; -use self::current_networks::{ActiveConnectionInfo, active_connections}; - -pub type SSID = Arc; -pub type UUID = Arc; - -pub(crate) struct Wrapper { - id: I, - conn: zbus::Connection, -} - -impl Hash for Wrapper { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("failed to list bluetooth devices with rfkill")] - BluetoothRfkillList(std::io::Error), - #[error("failed to activate connection")] - ConnectionActivate, - #[error("no wifi devices found")] - NoWiFiDevices, - #[error("zbus error")] - Zbus(#[from] zbus::Error), - #[error("missing connection field")] - MissingField(&'static str), -} - -#[derive(Debug)] -pub enum State { - Ready(zbus::Connection), - Waiting(zbus::Connection, UnboundedReceiver), - Finished, -} - -/// Reloads state on available connection changes. -pub async fn watch_connections_changed( - conn: zbus::Connection, - mut output: futures::channel::mpsc::Sender, -) { - let Ok(nm) = NetworkManager::new(&conn).await else { - return; - }; - - let mut device_stream = nm.receive_devices_changed().await; - - loop { - // Emits updates when available connections changes. - let connections_changed = std::pin::pin!(async { - let devices = nm.devices().await.unwrap_or_default(); - - let mut connection_streams = - futures::stream::FuturesUnordered::from_iter(devices.into_iter().map( - |device| async move { device.receive_available_connections_changed().await }, - )) - .collect::>() - .await; - - let mut available_connections = futures::stream::FuturesUnordered::from_iter( - connection_streams - .iter_mut() - .map(|stream| async { stream.next().await }), - ); - - loop { - _ = futures::join!( - available_connections.next(), - tokio::time::sleep(Duration::from_secs(3)) - ); - - // TODO: although it should consume the stream, the stream is never empty. - // while available_connections.next().now_or_never().is_some() {} - - let state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - - _ = output - .send(Event::RequestResponse { - req: Request::Reload, - state, - success: true, - }) - .await; - } - }); - - // Reload the connection streams whenever devices change. - futures::future::select(connections_changed, device_stream.next()).await; - } -} - -pub fn subscription( - id: I, - conn: zbus::Connection, -) -> iced_futures::Subscription { - struct Wrapper { - id: I, - conn: zbus::Connection, - } - impl Hash for Wrapper { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } - } - Subscription::run_with(Wrapper { id, conn }, |Wrapper { id, conn }| { - let conn = conn.clone(); - stream::channel( - 50, - |output: futures::channel::mpsc::Sender| async move { - watch(conn, output).await; - futures::future::pending().await - }, - ) - }) -} - -pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender) { - let mut state = State::Ready(conn); - - loop { - state = start_listening(state, &mut output).await; - } -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - match state { - State::Ready(conn) => { - let (tx, rx) = unbounded(); - - if output - .send(Event::Init { - conn: conn.clone(), - sender: tx, - state: NetworkManagerState::new(&conn).await.unwrap_or_default(), - }) - .await - .is_ok() - { - State::Waiting(conn, rx) - } else { - State::Finished - } - } - State::Waiting(conn, mut rx) => { - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(_) => return State::Finished, - }; - - match rx.next().await { - Some(Request::Deactivate(uuid)) => { - let mut success = false; - for c in network_manager - .active_connections() - .await - .unwrap_or_default() - { - if c.uuid().await.unwrap_or_default().as_str() == uuid.as_ref() - && network_manager.deactivate_connection(&c).await.is_ok() - { - success = true; - if let Ok(ActiveConnectionState::Deactivated) = c.state().await { - break; - } else { - let mut changed = c.receive_state_changed().await; - _ = tokio::time::timeout(Duration::from_secs(5), async move { - loop { - if let Some(next) = changed.next().await - && let Ok(ActiveConnectionState::Deactivated) = - next.get().await.map(ActiveConnectionState::from) - { - break; - } - } - }) - .await; - } - break; - } - } - - _ = request_response(&conn, Request::Deactivate(uuid.clone()), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::Disconnect(ssid)) => { - let mut success = false; - for c in network_manager - .active_connections() - .await - .unwrap_or_default() - { - if c.id().await.unwrap_or_default().as_str() == ssid.as_ref() - && network_manager.deactivate_connection(&c).await.is_ok() - { - success = true; - if let Ok(ActiveConnectionState::Deactivated) = c.state().await { - break; - } else { - let mut changed = c.receive_state_changed().await; - _ = tokio::time::timeout(Duration::from_secs(5), async move { - loop { - if let Some(next) = changed.next().await - && let Ok(ActiveConnectionState::Deactivated) = - next.get().await.map(ActiveConnectionState::from) - { - break; - } - } - }) - .await; - } - break; - } - } - - _ = request_response(&conn, Request::Disconnect(ssid.clone()), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::SetAirplaneMode(airplane_mode)) => { - // wifi - let mut success = network_manager - .set_wireless_enabled(!airplane_mode) - .await - .is_ok(); - // bluetooth - success = success - && Command::new("rfkill") - .arg(if airplane_mode { "block" } else { "unblock" }) - .arg("bluetooth") - .output() - .await - .is_ok(); - - let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - state.airplane_mode = if success { - airplane_mode - } else { - !airplane_mode - }; - if state.airplane_mode { - state.wifi_enabled = false; - } - - _ = output - .send(Event::RequestResponse { - req: Request::SetAirplaneMode(airplane_mode), - success, - state, - }) - .await; - } - - Some(Request::SetWiFi(enabled)) => { - let success = network_manager.set_wireless_enabled(enabled).await.is_ok(); - - let mut state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - - state.wifi_enabled = if success { enabled } else { !enabled }; - - if state.wifi_enabled { - tokio::time::sleep(Duration::from_secs(3)).await; - } - - _ = request_response(&conn, Request::SetWiFi(enabled), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::Authenticate { - ssid, - identity, - password, - secret_tx, - interface, - }) => { - let nm_state = NetworkManagerState::new(&conn).await.unwrap_or_default(); - - let success = nm_state - .connect_wifi( - &conn, - &ssid, - identity.as_deref(), - Some(password.unsecure()), - secret_tx.clone(), - if identity.is_some() { - NetworkType::EAP - } else { - NetworkType::PskOrSae - }, - interface.clone(), - ) - .await - .is_ok(); - - _ = output - .send(Event::RequestResponse { - req: Request::Authenticate { - ssid: ssid.clone(), - identity: identity.clone(), - password: password.clone(), - secret_tx: secret_tx.clone(), - interface, - }, - success, - state: NetworkManagerState::new(&conn).await.unwrap_or_default(), - }) - .await; - } - - Some(Request::SelectAccessPoint(ssid, network_type, secret_tx, interface)) => { - if matches!(network_type, NetworkType::Open) { - attempt_wifi_connection(&conn, ssid, network_type, output, None, interface) - .await; - } else { - // For secured networks, check if we have saved credentials - let has_saved = has_saved_wifi_credentials(&conn, &ssid).await; - - if !has_saved { - return State::Waiting(conn, rx); - } - - // We have saved credentials, attempt connection - attempt_wifi_connection( - &conn, - ssid, - network_type, - output, - secret_tx, - interface, - ) - .await; - } - } - - Some(Request::Activate(device_path, connection_path)) => { - let mut success = true; - - if let Err(why) = network_manager - .activate_connection_by_paths(&connection_path, &device_path) - .await - { - tracing::error!( - ?why, - "failed to activate connection on {device_path:?} to {connection_path}" - ); - success = false; - }; - - _ = request_response( - &conn, - Request::Activate(device_path, connection_path), - success, - ) - .then(|event| output.send(event)) - .await; - } - - Some(Request::Reload) => { - _ = output - .send(request_response(&conn, Request::Reload, true).await) - .await; - } - - Some(Request::Remove(uuid)) => { - let s = match NetworkManagerSettings::new(&conn).await { - Ok(s) => s, - Err(why) => { - tracing::error!(?why, "error getting network manager settings"); - _ = output - .send(Event::RequestResponse { - req: Request::Forget(uuid.clone()), - success: false, - state: NetworkManagerState::new(&conn) - .await - .unwrap_or_default(), - }) - .await; - - return State::Waiting(conn, rx); - } - }; - - let known_conns = s.list_connections().await.unwrap_or_default(); - let mut success = false; - for c in known_conns { - let settings = c.get_settings().await.ok().unwrap_or_default(); - - let c_uuid = settings - .get("connection") - .and_then(|conn| conn.get("uuid")) - .and_then(|uuid| uuid.downcast_ref::().ok()) - .unwrap_or_default(); - - if uuid.as_ref() == c_uuid.as_str() { - _ = c.delete().await; - success = true; - } - } - - _ = request_response(&conn, Request::Remove(uuid.clone()), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::Forget(ssid)) => { - let s = match NetworkManagerSettings::new(&conn).await { - Ok(s) => s, - Err(why) => { - tracing::error!(?why, "error getting network manager settings"); - _ = output - .send(Event::RequestResponse { - req: Request::Forget(ssid.clone()), - success: false, - state: NetworkManagerState::new(&conn) - .await - .unwrap_or_default(), - }) - .await; - - return State::Waiting(conn, rx); - } - }; - - let known_conns = s.list_connections().await.unwrap_or_default(); - let mut success = false; - for c in known_conns { - let settings = c.get_settings().await.ok().unwrap_or_default(); - let s = Settings::new(settings); - if s.wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok()) - .is_some_and(|s| s == ssid.as_ref()) - { - _ = c.delete().await; - success = true; - break; - } - } - - _ = request_response(&conn, Request::Forget(ssid.clone()), success) - .then(|event| output.send(event)) - .await; - } - - Some(Request::GetWiFiCredentials(ssid, uuid, security_type, secret_tx)) => { - let s = match NetworkManagerSettings::new(&conn).await { - Ok(s) => s, - Err(why) => { - tracing::error!(?why, "error getting network manager settings"); - return State::Waiting(conn, rx); - } - }; - - match security_type { - NetworkType::Open => { - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password: None, - security_type, - }) - .await; - } - t => { - let (tx, rx) = tokio::sync::oneshot::channel(); - let setting_name = if matches!(t, NetworkType::PskOrSae) { - "802-11-wireless-security" - } else { - "802-1x" - }; - let pw_key = if matches!(t, NetworkType::PskOrSae) { - "psk" - } else { - "password" - }; - if let Some(secret_tx) = secret_tx { - let _ = secret_tx - .send(nm_secret_agent::Request::GetSecrets { - setting_name: setting_name.to_string(), - uuid: uuid.to_string(), - resp_tx: tx, - }) - .await; - let _ = tokio::time::timeout(Duration::from_secs(10), async move { - if let Some(password) = - rx.await.ok().and_then(|mut secrets| secrets.remove(pw_key)) - { - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password: Some(password), - security_type, - }) - .await; - } else { - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password: None, - security_type, - }) - .await; - } - }) - .await; - } else { - let known_conns = s.list_connections().await.unwrap_or_default(); - for c in known_conns { - let settings = c.get_settings().await.ok().unwrap_or_default(); - let settings_parsed = Settings::new(settings.clone()); - - if let Some(saved_ssid) = settings_parsed - .wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|s| String::from_utf8(s).ok()) - && saved_ssid == ssid.as_ref() - { - let password = c - .get_secrets("802-11-wireless-security") - .await - .ok() - .and_then(|secrets| { - // Look for PSK password - secrets - .get("802-11-wireless-security") - .and_then(|sec| sec.get("psk")) - .and_then(|v| { - v.downcast_ref::().ok() - }) - .map(|s| s.to_string()) - .or_else(|| { - // Fallback to WEP key - secrets - .get("802-11-wireless-security") - .and_then(|sec| sec.get("wep-key0")) - .and_then(|v| { - v.downcast_ref::().ok() - }) - .map(|s| s.to_string()) - }) - }); - - _ = output - .send(Event::WiFiCredentials { - ssid: ssid.clone(), - password: password.map(SecureString::from), - security_type, - }) - .await; - break; - } - } - } - } - } - } - - None => { - return State::Finished; - } - }; - - State::Waiting(conn, rx) - } - State::Finished => futures::future::pending().await, - } -} - -async fn request_response(conn: &zbus::Connection, req: Request, success: bool) -> Event { - Event::RequestResponse { - req, - success, - state: NetworkManagerState::new(conn).await.unwrap_or_default(), - } -} - -async fn has_saved_wifi_credentials(conn: &zbus::Connection, ssid: &str) -> bool { - let Ok(nm_settings) = NetworkManagerSettings::new(conn).await else { - return false; - }; - - let known_conns = nm_settings.list_connections().await.unwrap_or_default(); - - for connection in known_conns { - if let Ok(settings) = connection.get_settings().await { - let settings = Settings::new(settings); - if let Some(saved_ssid) = settings - .wifi - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok()) - && saved_ssid == ssid - { - return true; - } - } - } - - false -} - -async fn attempt_wifi_connection( - conn: &zbus::Connection, - ssid: SSID, - network_type: NetworkType, - output: &mut futures::channel::mpsc::Sender, - secret_tx: Option>, - interface: Option, -) { - let state = NetworkManagerState::new(conn).await.unwrap_or_default(); - let success = if let Err(err) = state - .connect_wifi( - conn, - ssid.as_ref(), - None, - None, - secret_tx, - network_type, - interface.clone(), - ) - .await - { - tracing::error!("Failed to connect to access point: {:?}", err); - false - } else { - true - }; - - _ = request_response( - conn, - Request::SelectAccessPoint(ssid, network_type, None, interface), - success, - ) - .then(|event| output.send(event)) - .await; -} - -#[derive(Debug, Clone)] -pub enum Request { - /// Activate a device's connection profile - Activate(ObjectPath<'static>, ObjectPath<'static>), - /// Deactivate a connection - Deactivate(UUID), - /// Disconnect from an access point. - Disconnect(SSID), - /// Forget a known access point. - Forget(SSID), - /// Create a connection to a new access point. - Authenticate { - ssid: String, - identity: Option, - password: SecureString, - secret_tx: Option>, - interface: Option, - }, - /// Get WiFi credentials for a known access point. - GetWiFiCredentials( - SSID, - UUID, - NetworkType, - Option>, - ), - /// Signal to reload the service. - Reload, - /// Remove a connection profile. - Remove(UUID), - /// Connect to a known access point. - SelectAccessPoint( - SSID, - NetworkType, - Option>, - Option, - ), - /// Toggle airplaine mode. - SetAirplaneMode(bool), - /// Toggle WiFi enablement. - SetWiFi(bool), -} - -#[derive(Debug, Clone)] -pub enum Event { - RequestResponse { - req: Request, - state: NetworkManagerState, - success: bool, - }, - Init { - conn: zbus::Connection, - sender: UnboundedSender, - state: NetworkManagerState, - }, - Devices, - WiFiEnabled(bool), - WirelessAccessPoints, - ActiveConns, - WiFiCredentials { - ssid: SSID, - password: Option, - security_type: NetworkType, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NetworkManagerState { - pub wireless_access_points: Vec, - pub active_conns: Vec, - pub known_access_points: Vec, - pub wifi_enabled: bool, - pub airplane_mode: bool, - pub connectivity: NmConnectivityState, -} - -impl Default for NetworkManagerState { - fn default() -> Self { - Self { - wireless_access_points: Vec::new(), - active_conns: Vec::new(), - known_access_points: Vec::new(), - wifi_enabled: false, - airplane_mode: false, - connectivity: NmConnectivityState::Unknown, - } - } -} - -impl NetworkManagerState { - pub async fn new(conn: &zbus::Connection) -> Result { - let network_manager = NetworkManager::new(conn).await?; - let mut this = Self::default(); - - this.refresh_wifi_state(conn, &network_manager).await?; - - Ok(this) - } - - pub async fn refresh_wifi_state( - &mut self, - conn: &zbus::Connection, - network_manager: &NetworkManager<'_>, - ) -> Result<(), Error> { - let (airplane_mode, wireless_enabled, settings_res) = futures::join!( - Command::new("rfkill") - .arg("list") - .arg("bluetooth") - .output() - .then(|res| async move { - let Ok(output) = res else { - return false; - }; - - std::str::from_utf8(&output.stdout) - .ok() - .is_some_and(|stdout| stdout.contains("Soft blocked: yes")) - }), - network_manager - .wireless_enabled() - .then(|res| async move { res.unwrap_or_default() }), - NetworkManagerSettings::new(conn) - ); - - self.wifi_enabled = wireless_enabled; - self.airplane_mode = airplane_mode && !self.wifi_enabled; - - let settings = settings_res?; - - _ = settings.load_connections(&[]).await; - - let (known_conns, active_conns, devices, connectivity) = futures::join!( - settings.list_connections(), - network_manager.active_connections(), - network_manager.devices(), - network_manager.connectivity(), - ); - - let devices = devices.unwrap_or_default(); - let known_conns = known_conns.unwrap_or_default(); - - let (active_conns, wireless_access_points) = futures::join!( - // Retrieve active connections. - async move { - let mut active_conns = active_connections(active_conns.unwrap_or_default()) - .await - .unwrap_or_default(); - - active_conns.sort_by(|a, b| { - let helper = |conn: &ActiveConnectionInfo| match conn { - ActiveConnectionInfo::Vpn { name, .. } => format!("0{name}"), - ActiveConnectionInfo::Wired { name, .. } => format!("1{name}"), - ActiveConnectionInfo::WiFi { name, .. } => format!("2{name}"), - }; - helper(a).cmp(&helper(b)) - }); - - active_conns - }, - // Retrieve all access points, and sort by strength. - async move { - let mut wireless_access_points = futures::stream::FuturesUnordered::from_iter( - devices.iter().map(|device| async move { - if let Ok(Some(SpecificDevice::Wireless(wireless_device))) = - device.downcast_to_device().await - { - handle_wireless_device(wireless_device, device.hw_address().await.ok()) - .await - .unwrap_or_default() - } else { - Vec::new() - } - }), - ) - .fold( - Vec::with_capacity(devices.len()), - |mut access_points, mut f| async move { - access_points.append(&mut f); - access_points - }, - ) - .await; - - wireless_access_points.sort_by(|a, b| b.strength.cmp(&a.strength)); - wireless_access_points - } - ); - - // Concurrently get - let known_ssid: Vec> = futures::stream::FuturesOrdered::from_iter( - known_conns.into_iter().map(|c| async move { - let s = c.get_settings().await.ok()?; - let s = Settings::new(s); - let curr_ssid = s - .wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok())?; - - Some(Arc::from(curr_ssid)) - }), - ) - .filter_map(|c| async move { c }) - .collect() - .await; - - self.known_access_points = wireless_access_points - .iter() - .filter(|a| { - known_ssid.contains(&a.ssid) - && !active_conns.iter().any(|ac| ac.name() == a.ssid.as_ref()) - }) - .cloned() - .collect(); - - self.wireless_access_points = wireless_access_points; - self.active_conns = active_conns; - self.connectivity = connectivity?; - - Ok(()) - } - - #[allow(dead_code)] - pub fn clear(&mut self) { - self.active_conns = Vec::new(); - self.known_access_points = Vec::new(); - self.wireless_access_points = Vec::new(); - } - - async fn connect_wifi( - &self, - conn: &zbus::Connection, - ssid: &str, - identity: Option<&str>, - password: Option<&str>, - mut secret_tx: Option>, - network_type: NetworkType, - interface: Option, - ) -> Result<(), Error> { - secret_tx = secret_tx.filter(|tx| !tx.is_closed()); - let nm = NetworkManager::new(conn).await?; - - for c in nm.active_connections().await.unwrap_or_default() { - if self - .wireless_access_points - .iter() - .any(|w| Ok(Some(w.ssid.as_ref())) == c.cached_id().as_ref().map(|v| v.as_deref())) - { - _ = nm.deactivate_connection(&c).await; - break; - } - } - - let mut conn_settings: HashMap<&str, HashMap<&str, zvariant::Value>> = HashMap::from([ - ( - "802-11-wireless", - HashMap::from([("ssid", Value::Array(ssid.as_bytes().into()))]), - ), - ( - "connection", - HashMap::from([ - ("id", Value::Str(ssid.into())), - ("type", Value::Str("802-11-wireless".into())), - ]), - ), - ]); - - if let Some(identity) = identity { - conn_settings.insert( - "802-1x", - HashMap::from([ - ("identity", Value::Str(identity.into())), - // most common default - ("eap", Value::Array(vec!["peap"].into())), - // most common default - ("phase2-auth", Value::Str("mschapv2".into())), - ]), - ); - if secret_tx.is_none() { - conn_settings - .get_mut("802-1x") - .unwrap() - .insert("password", Value::Str(password.unwrap_or("").into())); - } - let wireless = conn_settings.get_mut("802-11-wireless").unwrap(); - wireless.insert("security", Value::Str("802-11-wireless-security".into())); - wireless.insert("mode", Value::Str("infrastructure".into())); - conn_settings.insert( - "802-11-wireless-security", - HashMap::from([("key-mgmt", Value::Str("wpa-eap".into()))]), - ); - } else if let Some(pass) = password { - let entry = conn_settings.entry("802-11-wireless-security").or_default(); - _ = entry.insert("key-mgmt", Value::Str("wpa-psk".into())); - if secret_tx.is_none() { - _ = entry.insert("psk", Value::Str(pass.into())); - } - } - - let devices = nm.devices().await?; - for device in devices { - if !matches!( - device.device_type().await.unwrap_or(DeviceType::Other), - DeviceType::Wifi - ) || (interface.is_some() && interface != device.interface().await.ok()) - { - continue; - } - - let s = NetworkManagerSettings::new(conn).await?; - let known_conns = s.list_connections().await.unwrap_or_default(); - let mut known_conn = None; - for c in known_conns { - let settings = c.get_settings().await.ok().unwrap_or_default(); - - let s = Settings::new(settings); - if let Some(cur_ssid) = s - .wifi - .clone() - .and_then(|w| w.ssid) - .and_then(|ssid| String::from_utf8(ssid).ok()) - && cur_ssid == ssid - { - known_conn = Some(c); - break; - } - } - - let known_conn = if let Some(known_conn) = known_conn { - if (secret_tx.is_none() && password.is_some()) || identity.is_some() { - known_conn.update(conn_settings).await.unwrap(); - } - known_conn - } else { - let settings = nm.settings().await?; - let object_path = settings.add_connection(conn_settings).await?; - let known_connection = Connection::from( - ConnectionSettingsProxy::builder(settings.inner().connection()) - .path(object_path)? - .build() - .await?, - ); - let settings = known_connection.get_settings().await?; - let uuid = String::try_from( - settings - .get("connection") - .ok_or_else(|| Error::MissingField("connection"))? - .get("uuid") - .ok_or_else(|| Error::MissingField("uuid"))? - .clone(), - ) - .map_err(zbus::Error::Variant)?; - - if let Some((pass, secret_tx)) = password.zip(secret_tx.as_ref()) { - let pass = SecureString::from(pass); - let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); - - let _ = secret_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name: if identity.is_some() { - "802-1x".into() - } else { - "802-11-wireless-security".into() - }, - uuid, - secrets: if identity.is_some() { - HashMap::from([("password".to_string(), pass)]) - } else { - HashMap::from([("psk".to_string(), pass)]) - }, - applied_tx, - }) - .await; - if let Err(err) = applied_rx.await { - tracing::error!("Failed to set secret. {err:?}"); - } - } - - known_connection - }; - - if let Some(pass) = password { - let pass = SecureString::from(pass); - if let Some(secret_tx) = secret_tx.as_ref() { - let settings = known_conn.get_settings().await?; - let uuid = String::try_from( - settings - .get("connection") - .ok_or_else(|| Error::MissingField("connection"))? - .get("uuid") - .ok_or_else(|| Error::MissingField("uuid"))? - .clone(), - ) - .map_err(zbus::Error::Variant)?; - let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); - let setting_name: String = if identity.is_some() { - "802-1x".into() - } else { - "802-11-wireless-security".into() - }; - let _ = secret_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name, - uuid, - secrets: if identity.is_some() { - HashMap::from([("password".to_string(), pass)]) - } else { - HashMap::from([("psk".to_string(), pass)]) - }, - applied_tx, - }) - .await; - if let Err(err) = applied_rx.await { - tracing::error!("Failed to set secret. {err:?}"); - } - } - } - - let active_conn = nm.activate_connection(&known_conn, &device).await?; - let mut changes = active_conn.receive_state_changed().await; - _ = tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; - let mut count = 5; - loop { - let state = active_conn.state().await; - if let Ok(enums::ActiveConnectionState::Activated) = state { - return Ok(()); - } else if let Ok(enums::ActiveConnectionState::Deactivated) = state { - break; - } - - if let Ok(Some(s)) = - tokio::time::timeout(Duration::from_secs(20), changes.next()).await - { - let state = s.get().await.unwrap_or_default().into(); - if matches!(state, enums::ActiveConnectionState::Activated) { - return Ok(()); - } - }; - - count -= 1; - if count <= 0 { - break; - } - } - if let Some(secret_tx) = secret_tx - && !matches!(network_type, NetworkType::Open) - { - let settings = known_conn.get_settings().await?; - let uuid = String::try_from( - settings - .get("connection") - .ok_or_else(|| Error::MissingField("connection"))? - .get("uuid") - .ok_or_else(|| Error::MissingField("uuid"))? - .clone(), - ) - .map_err(zbus::Error::Variant)?; - let (applied_tx, applied_rx) = tokio::sync::oneshot::channel(); - let setting_name: String = if identity.is_some() { - "802-1x".into() - } else { - "802-11-wireless-security".into() - }; - if let Err(err) = secret_tx - .send(nm_secret_agent::Request::SetSecrets { - setting_name, - uuid, - secrets: Default::default(), - applied_tx, - }) - .await - { - tracing::error!( - "Failed to reset access point secrets after failed activation: {err:?}" - ); - } else if let Err(err) = applied_rx.await { - tracing::error!( - "Failed to reset access point secrets after failed activation: {err:?}" - ); - } - } - return Err(Error::ConnectionActivate); - } - - Err(Error::NoWiFiDevices) - } -} diff --git a/subscriptions/network-manager/src/nm_secret_agent.rs b/subscriptions/network-manager/src/nm_secret_agent.rs deleted file mode 100644 index 330c594..0000000 --- a/subscriptions/network-manager/src/nm_secret_agent.rs +++ /dev/null @@ -1,738 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::fmt::Debug; -use std::sync::Arc; - -use bitflags::bitflags; -use cosmic_dbus_networkmanager::interface::settings::connection::ConnectionSettingsProxy; -use futures::{SinkExt, Stream}; -use secure_string::SecureString; -use tokio::sync::oneshot; -use zbus::zvariant::{OwnedValue, Str}; -use zbus::{ObjectServer, fdo}; - -pub type SecretSender = Arc>>>; - -pub const SECRET_ID: &str = "com.system76.CosmicSettings.NetworkManager"; -pub const DBUS_PATH: &str = "/org/freedesktop/NetworkManager/SecretAgent"; - -bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub struct GetSecretsFlags: u32 { - /// No special behavior. - /// By default no user interaction is allowed and secrets must come - /// from persistent storage, otherwise an error is returned. - const NONE = 0x0; - - /// Allows interaction with the user (eg. prompt via UI). - const ALLOW_INTERACTION = 0x1; - - /// Explicitly request new secrets from the user. - /// Implies ALLOW_INTERACTION. - const REQUEST_NEW = 0x2; - - /// Request was initiated by a user action (via D-Bus). - const USER_REQUESTED = 0x4; - - /// Internal flag, not part of the public D-Bus API. - const ONLY_SYSTEM = 0x8000_0000; - - /// Internal flag, not part of the public D-Bus API. - const NO_ERRORS = 0x4000_0000; - } -} - -#[derive(thiserror::Error, Clone, Debug)] -pub enum Error { - #[error("zbus error")] - Zbus(#[from] zbus::Error), - #[error("listening for secret agent closed")] - RecvError(#[from] oneshot::error::RecvError), - #[error("secret service error")] - SecretService(#[from] Arc), - #[error("no password found for identifier: {0}")] - NoPasswordForIdentifier(String), - #[error("utf8 error")] - Utf8Error(#[from] std::string::FromUtf8Error), -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum PasswordFlag { - /// The system is responsible for providing and storing this secret. - None = 0, - /// A user-session secret agent is responsible for providing and storing - /// this secret; when it is required, agents will be asked to provide it. - AgentOwned = 1, - /// This secret should not be saved but should be requested from the user - /// each time it is required. This flag should be used for One-Time-Pad - /// secrets, PIN codes from hardware tokens, or if the user simply does not - /// want to save the secret. - NotSaved = 2, - /// in some situations it cannot be automatically determined that a secret is required or not. This flag hints that the secret is not required and should not be requested from the user. - NotRequired = 4, -} - -#[derive(Debug, Clone)] -pub struct SecretHint { - pub key: String, - pub message: Option, -} - -fn parse_hints(hints: Vec) -> Vec { - hints - .into_iter() - // fold message hints into previous hints - .fold(Vec::new(), |mut acc, hint| { - if let Some((key, msg)) = hint.split_once(':') { - if let Some(last) = acc.last_mut() { - last.message = Some(format!("{}: {}", key, msg)); - } - } else { - acc.push(SecretHint { - key: hint, - message: None, - }); - } - acc - }) -} - -#[derive(Debug, Clone)] -pub enum Event { - RequestSecret { - uuid: String, - name: String, - description: Option, - previous: SecureString, - tx: SecretSender, - }, - CancelGetSecrets { - uuid: String, - name: String, - }, - Failed(Error), -} - -#[derive(Debug)] -pub enum Request { - SetSecrets { - setting_name: String, - uuid: String, - secrets: HashMap, - applied_tx: oneshot::Sender<()>, - }, - GetSecrets { - setting_name: String, - uuid: String, - resp_tx: oneshot::Sender>, - }, -} - -pub fn secret_agent_stream( - identifier: impl AsRef, - rx: tokio::sync::mpsc::Receiver, -) -> impl Stream { - iced_futures::stream::channel( - 4, - move |mut msg_tx: futures::channel::mpsc::Sender| async move { - if let Err(e) = secret_agent_stream_impl(identifier.as_ref(), msg_tx.clone(), rx).await - { - let _ = msg_tx.send(Event::Failed(e)).await; - } - }, - ) -} - -async fn secret_agent_stream_impl( - identifier: &str, - msg_tx: futures::channel::mpsc::Sender, - mut rx: tokio::sync::mpsc::Receiver, -) -> Result<(), Error> { - // fail early if we can't connect, closing the channel - { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - if collection.is_locked().await.map_err(Arc::new)? { - collection.unlock().await.map_err(Arc::new)?; - } - } - // register the secret agent with NetworkManager - let proxy = - nm_secret_agent_manager::AgentManagerProxy::builder(&zbus::Connection::system().await?) - .path("/org/freedesktop/NetworkManager/AgentManager")? - .build() - .await?; - - let _ = ObjectServer::at( - proxy.inner().connection().object_server(), - DBUS_PATH, - SettingsSecretAgent { tx: msg_tx }, - ) - .await?; - - proxy.register_with_capabilities(identifier, 1).await?; - - while let Some(request) = rx.recv().await { - match request { - Request::SetSecrets { - setting_name, - uuid, - secrets, - applied_tx, - } => { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - - if secrets.is_empty() { - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &uuid); - let search_items = collection - .search_items(attributes) - .await - .map_err(Arc::new)?; - - for item in &search_items { - item.delete().await.map_err(Arc::new)?; - } - let _ = applied_tx.send(()); - - continue; - } - - for (name, secret) in &secrets { - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &uuid); - attributes.insert("setting_name", &setting_name); - attributes.insert("name", name); - let _item = collection - .create_item( - "NetworkManager Secret", - attributes, - secret.unsecure().as_bytes(), - true, - "text/plain", - ) - .await - .map_err(Arc::new)?; - } - let _ = applied_tx.send(()); - } - Request::GetSecrets { - setting_name, - uuid, - resp_tx, - } => { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &uuid); - attributes.insert("setting_name", &setting_name); - - let search_items = collection - .search_items(attributes) - .await - .map_err(Arc::new)?; - - let mut secrets = HashMap::new(); - for item in &search_items { - let name = item - .get_attributes() - .await - .map_err(Arc::new)? - .get("name") - .cloned() - .unwrap_or_else(|| "unknown".to_string()); - let secret = item.get_secret().await.map_err(Arc::new)?; - let secret: String = String::from_utf8(secret)?; - secrets.insert(name, SecureString::from(secret)); - } - let _ = resp_tx.send(secrets); - } - } - } - Ok(()) -} - -fn parse_secret_flag(value: &str) -> PasswordFlag { - match value { - "0" => PasswordFlag::None, - "1" => PasswordFlag::AgentOwned, - "2" => PasswordFlag::NotSaved, - "4" => PasswordFlag::NotRequired, - _ => PasswordFlag::AgentOwned, - } -} - -fn setting_has_always_ask(setting: zbus::zvariant::Dict) -> bool { - for (key, value) in setting.iter() { - let Ok(key) = key.downcast_ref::() else { - continue; - }; - let Ok(value) = value.downcast_ref::() else { - continue; - }; - // we only care about "-flags" - if !key.ends_with("-flags") { - continue; - } - - if parse_secret_flag(value.as_str()) == PasswordFlag::NotSaved { - return true; - } - } - - false -} - -fn has_always_ask(setting: Option) -> bool { - setting.map(setting_has_always_ask).unwrap_or(false) -} - -fn is_connection_always_ask(connection: &HashMap>) -> bool { - let conn_setting = match connection.get("connection") { - Some(s) => s, - None => return false, - }; - - let conn_type = match conn_setting - .get("type") - .and_then(|v| v.downcast_ref::().ok()) - { - Some(t) => t, - None => return false, - }; - - // Primary setting (vpn, wifi, ethernet, etc) - if has_always_ask( - connection - .get(&conn_type) - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - - match conn_type.as_str() { - "802-11-wireless" => { - if has_always_ask( - connection - .get("802-11-wireless-security") - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - if has_always_ask( - connection - .get("802-1x") - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - } - - "802-3-ethernet" => { - if has_always_ask( - connection - .get("pppoe") - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - if has_always_ask( - connection - .get("802-1x") - .and_then(|d| d.get("data")) - .and_then(|data| data.downcast_ref::().ok()), - ) { - return true; - } - } - - _ => {} - } - - false -} - -#[derive(Debug)] -pub struct SettingsSecretAgent { - tx: futures::channel::mpsc::Sender, -} - -#[zbus::interface(name = "org.freedesktop.NetworkManager.SecretAgent")] -impl SettingsSecretAgent { - /// CancelGetSecrets method - async fn cancel_get_secrets( - &mut self, - connection_path: zbus::zvariant::ObjectPath<'_>, - setting_name: String, - ) -> fdo::Result<()> { - let conn = ConnectionSettingsProxy::builder( - &zbus::Connection::system() - .await - .map_err(|_| fdo::Error::Failed("failed to get uuid".to_string()))?, - ) - .path(connection_path)? - .build() - .await - .map_err(|e| fdo::Error::Failed(e.to_string()))?; - - let uuid = conn - .get_settings() - .await - .map_err(|e| fdo::Error::Failed(e.to_string()))? - .get("connection") - .and_then(|m| m.get("uuid")) - .and_then(|v| v.downcast_ref::().ok()) - .ok_or_else(|| fdo::Error::Failed("failed to get uuid".to_string()))? - .to_string(); - if let Err(e) = self - .tx - .clone() - .send(Event::CancelGetSecrets { - uuid, - name: setting_name, - }) - .await - && e.is_disconnected() - { - return Err(fdo::Error::Failed( - "failed to send cancel message".to_string(), - )); - } - - Ok(()) - } - - /// DeleteSecrets method - async fn delete_secrets( - &self, - connection: HashMap>, - connection_path: zbus::zvariant::ObjectPath<'_>, - ) -> fdo::Result<()> { - match self.delete_secrets_inner(connection, connection_path).await { - Ok(_) => Ok(()), - Err(err) => Err(fdo::Error::Failed(err.to_string())), - } - } - - /// GetSecrets method - async fn get_secrets( - &mut self, - connection: HashMap>, - connection_path: zbus::zvariant::ObjectPath<'_>, - setting_name: String, - hints: Vec, - flags: u32, - ) -> HashMap> { - self.get_secrets_inner(connection, connection_path, setting_name, hints, flags) - .await - .unwrap_or_default() - } - - /// SaveSecrets method - async fn save_secrets( - &self, - connection: HashMap>, - _connection_path: zbus::zvariant::ObjectPath<'_>, - ) -> fdo::Result<()> { - match self.save_secrets_inner(connection).await { - Ok(_) => Ok(()), - Err(err) => Err(fdo::Error::Failed(err.to_string())), - } - } -} - -impl SettingsSecretAgent { - pub async fn get_secrets_inner( - &mut self, - connection: HashMap>, - connection_path: zbus::zvariant::ObjectPath<'_>, - setting_name: String, - hints: Vec, - flags: u32, - ) -> Result>, Error> { - let flags = GetSecretsFlags::from_bits_truncate(flags); - - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - - let conn_uuid = connection - .get("connection") - .and_then(|m| m.get("uuid")) - .and_then(|v| v.downcast_ref::().ok()) - .ok_or_else(|| Error::NoPasswordForIdentifier(setting_name.clone()))? - .to_string(); - - let conn = - ConnectionSettingsProxy::builder(&zbus::Connection::system().await.map_err(|_| { - Error::Zbus(fdo::Error::Failed("failed to get uuid".to_string()).into()) - })?) - .path(connection_path)? - .build() - .await - .map_err(|e| Error::Zbus(fdo::Error::Failed(e.to_string()).into()))?; - let settings = conn.get_settings().await?; - let is_vpn = settings - .get("connection") - .and_then(|m| m.get("type")) - .and_then(|v| v.downcast_ref::().ok()) - .is_some_and(|t| t == "vpn"); - let is_always_ask = is_connection_always_ask(&settings); - - let mut setting_attributes = std::collections::HashMap::new(); - setting_attributes.insert("application", SECRET_ID); - setting_attributes.insert("uuid", &conn_uuid); - setting_attributes.insert("setting_name", &setting_name); - - let search_items = collection - .search_items(setting_attributes.clone()) - .await - .map_err(Arc::new)?; - let mut result = HashMap::new(); - let mut setting = HashMap::new(); - - if hints.is_empty() { - for item in &search_items { - let name = item - .get_attributes() - .await - .map_err(Arc::new)? - .get("name") - .cloned() - .unwrap_or_else(|| "unknown".to_string()); - let secret = item.get_secret().await.map_err(Arc::new)?; - let secret: String = String::from_utf8(secret)?; - setting.insert(name, zbus::zvariant::OwnedValue::from(Str::from(secret))); - } - result.insert(setting_name, setting); - Ok(result) - } else { - let hints = parse_hints(hints); - let mut requested = HashSet::new(); - - for SecretHint { key, message } in &hints { - if requested.contains(key) { - continue; - } - requested.insert(key); - if flags.contains(GetSecretsFlags::REQUEST_NEW) - && flags.contains(GetSecretsFlags::ALLOW_INTERACTION) - || is_always_ask - { - // request the secret via the message channel - let (resp_tx, resp_rx) = oneshot::channel(); - // msg begins after ":" - let actual_hint = message.as_ref().map(|m| { - m.split_once(":") - .map(|(_, msg)| msg.trim().to_string()) - .unwrap_or(m.clone()) - }); - if let Err(e) = self - .tx - .clone() - .send(Event::RequestSecret { - uuid: conn_uuid.clone(), - name: setting_name.clone(), - description: actual_hint.clone(), - previous: String::new().into(), - tx: Arc::new(tokio::sync::Mutex::new(Some(resp_tx))), - }) - .await - && e.is_disconnected() - { - continue; - } else if let Ok(secret) = resp_rx.await { - let mut named_attribute = setting_attributes.clone(); - named_attribute.insert("name", key); - let _item = collection - .create_item( - "NetworkManager Secret", - named_attribute, - secret.unsecure().as_bytes(), - true, - "text/plain", - ) - .await - .map_err(Arc::new)?; - - setting.insert( - key.clone(), - zbus::zvariant::OwnedValue::from(Str::from(secret.unsecure())), - ); - } - } else if !is_always_ask { - let mut pos = None; - let mut pos_with_message = None; - for item in &search_items { - let attributes = item.get_attributes().await.map_err(Arc::new)?; - if let Some(value) = attributes.get("name") - && value == key - { - if let Some(saved_message) = attributes.get("message") { - if message.as_ref().is_some_and(|msg| msg == saved_message) { - pos_with_message = Some(item); - } - break; - } else { - pos = Some(item); - } - } - } - - if let Some(item) = pos_with_message.or(pos) { - let secret = item.get_secret().await.map_err(Arc::new)?; - let secret: String = String::from_utf8(secret)?; - if is_vpn { - // ask anyway, but offer the previous one as a hint - let (resp_tx, resp_rx) = oneshot::channel(); - let actual_hint = message.as_ref().map(|m| { - m.split_once(":") - .map(|(_, msg)| msg.trim().to_string()) - .unwrap_or(m.clone()) - }); - if let Err(e) = self - .tx - .clone() - .send(Event::RequestSecret { - uuid: conn_uuid.clone(), - name: setting_name.clone(), - description: actual_hint.clone(), - previous: SecureString::from(secret.clone()), - tx: Arc::new(tokio::sync::Mutex::new(Some(resp_tx))), - }) - .await - && e.is_disconnected() - { - continue; - } else if let Ok(secret) = resp_rx.await { - let mut named_attribute = setting_attributes.clone(); - named_attribute.insert("name", key); - let _item = collection - .create_item( - "NetworkManager Secret", - named_attribute, - secret.unsecure().as_bytes(), - true, - "text/plain", - ) - .await - .map_err(Arc::new)?; - - setting.insert( - key.clone(), - zbus::zvariant::OwnedValue::from(Str::from(secret.unsecure())), - ); - } - } else { - setting.insert( - key.clone(), - zbus::zvariant::OwnedValue::from(Str::from(secret)), - ); - } - } - } else { - // can't find the secret, and we can't request it, so we just skip it - continue; - } - } - result.insert(setting_name, setting); - Ok(result) - } - } - - pub async fn delete_secrets_inner( - &self, - connection: HashMap>, - _connection_path: zbus::zvariant::ObjectPath<'_>, - ) -> Result<(), Error> { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - - let conn_uuid = connection - .get("connection") - .and_then(|m| m.get("uuid")) - .and_then(|v| v.downcast_ref::().ok()) - .ok_or_else(|| Error::NoPasswordForIdentifier("unknown".to_string()))? - .to_string(); - - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &conn_uuid); - - let search_items = collection - .search_items(attributes) - .await - .map_err(Arc::new)?; - - for item in &search_items { - item.delete().await.map_err(Arc::new)?; - } - Ok(()) - } - - pub async fn save_secrets_inner( - &self, - connection: HashMap>, - ) -> Result<(), Error> { - let ss = secret_service::SecretService::connect(secret_service::EncryptionType::Dh) - .await - .map_err(Arc::new)?; - let collection = ss.get_default_collection().await.map_err(Arc::new)?; - let conn_uuid = connection - .get("connection") - .and_then(|m| m.get("uuid")) - .and_then(|v| v.downcast_ref::().ok()) - .ok_or_else(|| Error::NoPasswordForIdentifier("unknown".to_string()))? - .to_string(); - - let secret: Option<(String, String)> = connection - .get("802-11-wireless-security") - .and_then(|m| m.get("psk")) - .and_then(|v| v.downcast_ref::().ok()) - .map(|password| ("psk".to_string(), password.clone())) - .or_else(|| { - connection - .get("802-1x") - .and_then(|s| s.get("password")) - .and_then(|v| v.downcast_ref::().ok()) - .map(|password| ("802-1x-password".to_string(), password.clone())) - }); - if let Some((name, secret)) = secret { - let mut attributes = std::collections::HashMap::new(); - attributes.insert("application", SECRET_ID); - attributes.insert("uuid", &conn_uuid); - attributes.insert("setting_name", &name); - let _item = collection - .create_item( - "NetworkManager Secret", - attributes, - secret.as_bytes(), - true, - "text/plain", - ) - .await - .map_err(Arc::new)?; - Ok(()) - } else { - Err(Error::NoPasswordForIdentifier("unknown".to_string())) - } - } -} diff --git a/subscriptions/network-manager/src/wireless_enabled.rs b/subscriptions/network-manager/src/wireless_enabled.rs deleted file mode 100644 index 039bbf3..0000000 --- a/subscriptions/network-manager/src/wireless_enabled.rs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -use crate::Wrapper; - -use super::Event; -use cosmic_dbus_networkmanager::nm::NetworkManager; -use futures::{SinkExt, StreamExt}; -use iced_futures::{Subscription, stream}; -use std::fmt::Debug; -use std::hash::Hash; -use zbus::Connection; - -#[derive(Debug, Clone)] -pub enum State { - Continue(Connection), - Error, -} - -pub fn wireless_enabled_subscription( - id: I, - conn: Connection, -) -> iced_futures::Subscription { - Subscription::run_with(Wrapper { id, conn: conn }, |Wrapper { id: _id, conn }| { - let conn = conn.clone(); - stream::channel(50, move |output| async move { - watch(conn, output).await; - futures::future::pending().await - }) - }) -} - -pub async fn watch(conn: zbus::Connection, mut output: futures::channel::mpsc::Sender) { - let mut state = State::Continue(conn); - - loop { - state = start_listening(state, &mut output).await; - } -} - -async fn start_listening( - state: State, - output: &mut futures::channel::mpsc::Sender, -) -> State { - let conn = match state { - State::Continue(conn) => conn, - State::Error => futures::future::pending().await, - }; - - let network_manager = match NetworkManager::new(&conn).await { - Ok(n) => n, - Err(why) => { - tracing::error!(why = why.to_string(), "Failed to connect to NetworkManager"); - return State::Error; - } - }; - - let mut wireless_enabled_changed = network_manager.receive_wireless_enabled_changed().await; - - while let Some(change) = wireless_enabled_changed.next().await { - if let Ok(enable) = change.get().await { - _ = output.send(Event::WiFiEnabled(enable)).await; - } - } - State::Continue(conn) -}