diff --git a/Cargo.lock b/Cargo.lock index ec77873..3e1a565 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,7 +132,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", - "cpufeatures 0.2.17", + "cpufeatures", ] [[package]] @@ -351,6 +351,12 @@ 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" @@ -378,7 +384,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.5", + "rand 0.9.4", "raw-window-handle", "serde", "serde_repr", @@ -396,7 +402,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand 0.9.5", + "rand 0.9.4", "serde", "serde_repr", "tokio", @@ -770,15 +776,6 @@ 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" @@ -916,18 +913,18 @@ checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" [[package]] name = "bytemuck" -version = "1.25.1" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.11.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", @@ -948,9 +945,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.12.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "calendrical_calculations" @@ -998,9 +995,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.67" +version = "1.2.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" dependencies = [ "find-msvc-tools", "jobserver", @@ -1076,7 +1073,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common 0.1.7", + "crypto-common", "inout", ] @@ -1132,6 +1129,7 @@ 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", @@ -1141,12 +1139,22 @@ 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" @@ -1252,12 +1260,6 @@ 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" @@ -1331,6 +1333,7 @@ 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", @@ -1342,6 +1345,7 @@ 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", @@ -1354,6 +1358,7 @@ dependencies = [ [[package]] name = "cosmic-comp-config" version = "1.0.0" +source = "git+https://github.com/pop-os/cosmic-comp#031fdc389aaa4dac8546e0fc429bbea2295cc00d" dependencies = [ "cosmic-config", "input", @@ -1365,6 +1370,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "1.0.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1385,6 +1391,7 @@ 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", @@ -1398,9 +1405,23 @@ 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", @@ -1414,6 +1435,7 @@ dependencies = [ [[package]] name = "cosmic-idle-config" version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-idle#c95d066b5b640509a6369634b669ca60dc50e168" dependencies = [ "cosmic-config", "serde", @@ -1435,6 +1457,7 @@ dependencies = [ [[package]] name = "cosmic-panel-config" version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-panel#45f03a3864a9385349be3ca13202553280e667d4" dependencies = [ "anyhow", "cosmic-config", @@ -1448,6 +1471,7 @@ 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", @@ -1461,6 +1485,7 @@ 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", @@ -1474,6 +1499,7 @@ dependencies = [ [[package]] name = "cosmic-randr-shell" version = "0.1.0" +source = "git+https://github.com/pop-os/cosmic-randr#6e8e795970fa06d434af22775e415b517f7552d3" dependencies = [ "kdl", "slotmap", @@ -1494,6 +1520,7 @@ dependencies = [ "cosmic-bg-config", "cosmic-comp-config", "cosmic-config", + "cosmic-dbus-networkmanager", "cosmic-idle-config", "cosmic-mime-apps", "cosmic-panel-config", @@ -1505,8 +1532,9 @@ dependencies = [ "cosmic-settings-airplane-mode-subscription", "cosmic-settings-audio-client", "cosmic-settings-bluetooth-subscription", - "cosmic-settings-config 0.1.0 (git+https://github.com/pop-os/cosmic-settings-daemon)", + "cosmic-settings-config", "cosmic-settings-daemon-config", + "cosmic-settings-network-manager-subscription", "cosmic-settings-page", "cosmic-settings-sound", "cosmic-settings-upower-subscription", @@ -1534,7 +1562,7 @@ dependencies = [ "locale1", "locales-rs", "mime 0.3.17", - "nmrs", + "nm-secret-agent-manager", "notify", "num-traits", "pwhash", @@ -1542,7 +1570,6 @@ dependencies = [ "ron 0.12.2", "rust-embed", "rustix 1.1.4", - "secret-service", "secure-string", "serde", "slab", @@ -1606,7 +1633,7 @@ dependencies = [ [[package]] name = "cosmic-settings-audio-client" version = "1.0.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#1af414fc6cb9080349847382a66db9fd69dbe964" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#24ab5c28eb5876665752cd5e9405b7de17e2b99a" dependencies = [ "cosmic-settings-audio-core", "dirs", @@ -1622,7 +1649,7 @@ dependencies = [ [[package]] name = "cosmic-settings-audio-core" version = "1.0.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#1af414fc6cb9080349847382a66db9fd69dbe964" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#24ab5c28eb5876665752cd5e9405b7de17e2b99a" dependencies = [ "dirs", "ron 0.12.2", @@ -1645,19 +1672,7 @@ dependencies = [ [[package]] name = "cosmic-settings-config" version = "0.1.0" -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" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#24ab5c28eb5876665752cd5e9405b7de17e2b99a" dependencies = [ "cosmic-config", "ron 0.11.0", @@ -1669,7 +1684,8 @@ dependencies = [ [[package]] name = "cosmic-settings-daemon" -version = "0.1.1-yoda.1" +version = "0.1.0" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "zbus", ] @@ -1677,7 +1693,7 @@ dependencies = [ [[package]] name = "cosmic-settings-daemon-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#1af414fc6cb9080349847382a66db9fd69dbe964" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#24ab5c28eb5876665752cd5e9405b7de17e2b99a" dependencies = [ "cosmic-config", "serde", @@ -1695,6 +1711,24 @@ 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" @@ -1754,6 +1788,8 @@ 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", @@ -1771,12 +1807,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", @@ -1800,15 +1836,6 @@ 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" @@ -1852,6 +1879,7 @@ 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", @@ -1870,15 +1898,6 @@ 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" @@ -1907,12 +1926,31 @@ 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" @@ -1933,6 +1971,20 @@ 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" @@ -1960,6 +2012,17 @@ 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" @@ -2050,6 +2113,37 @@ 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" @@ -2089,21 +2183,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "crypto-common 0.1.7", + "crypto-common", "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" @@ -2160,6 +2243,7 @@ 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", @@ -2192,6 +2276,52 @@ 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" @@ -2316,16 +2446,14 @@ dependencies = [ [[package]] name = "exr" -version = "1.74.2" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711fe42c9964295e01ee3fba3f9fe0e1d24b98886950d68efe81b1c76e21adf3" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", "lebe", "miniz_oxide", - "num-complex", - "pulp", "rayon-core", "smallvec", "zune-inflate", @@ -2701,12 +2829,6 @@ 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" @@ -2734,6 +2856,16 @@ 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" @@ -2995,7 +3127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0" dependencies = [ "arrayvec", - "rand 0.8.7", + "rand 0.8.6", "serde", ] @@ -3047,15 +3179,6 @@ 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" @@ -3150,6 +3273,7 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "dnd", "iced_accessibility", @@ -3170,6 +3294,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "accesskit", "accesskit_winit", @@ -3178,6 +3303,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "bitflags 2.13.0", "bytes", @@ -3203,6 +3329,7 @@ dependencies = [ [[package]] name = "iced_debug" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "iced_core", "iced_futures", @@ -3212,6 +3339,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "futures", "iced_core", @@ -3225,6 +3353,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "bitflags 2.13.0", "bytemuck", @@ -3245,6 +3374,7 @@ dependencies = [ [[package]] name = "iced_program" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "iced_graphics", "iced_runtime", @@ -3253,6 +3383,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -3264,6 +3395,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -3279,6 +3411,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "bytemuck", "cosmic-text", @@ -3295,7 +3428,9 @@ 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", @@ -3312,16 +3447,19 @@ 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", @@ -3341,6 +3479,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "cosmic-client-toolkit", "cursor-icon", @@ -3888,9 +4027,9 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.4" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153be1941a183ec9ccd095ddbe17a8b8d435ef6c76e9e02451b933c3999af2c8" +checksum = "dd854a95a4ac672fed8c054136039fd32c22cf039ff09ead7280afe920486483" dependencies = [ "bitflags 2.13.0", "inotify-sys", @@ -3899,9 +4038,9 @@ dependencies = [ [[package]] name = "inotify-sys" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +checksum = "9ea94e891b3606826e9c998be69ddca42247dad8ad50b1649a5cb7e1c9ae06fd" dependencies = [ "libc", ] @@ -4011,9 +4150,9 @@ checksum = "2ceaf4c6c48465bead8cb6a0b7c4ee0c86ecbb31239032b9c66ab9a08d2f3ee1" [[package]] name = "jiff" -version = "0.2.32" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" dependencies = [ "defmt", "jiff-static", @@ -4027,9 +4166,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.32" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" dependencies = [ "proc-macro2", "quote", @@ -4038,9 +4177,9 @@ dependencies = [ [[package]] name = "jiff-tzdb" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" +checksum = "6142247df1a93c2b3587402a19710be3e6e942f1581a1702e76408f2c21d6590" [[package]] name = "jiff-tzdb-platform" @@ -4427,7 +4566,8 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libcosmic" -version = "0.1.0-yoda.2" +version = "1.0.0" +source = "git+https://github.com/pop-os/libcosmic#3b85b6e8359308d31162c9a3d732ad1e2848bcfc" dependencies = [ "apply", "ashpd 0.12.3", @@ -4435,7 +4575,7 @@ dependencies = [ "cosmic-client-toolkit", "cosmic-config", "cosmic-freedesktop-icons", - "cosmic-settings-config 0.1.0", + "cosmic-settings-config", "cosmic-settings-daemon", "cosmic-theme", "css-color", @@ -4549,6 +4689,12 @@ 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" @@ -4722,9 +4868,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.3" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" @@ -4781,6 +4927,7 @@ 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", ] @@ -4791,16 +4938,6 @@ 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" @@ -4813,9 +4950,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -4916,23 +5053,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" [[package]] -name = "nmrs" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c427203beda58ac05815b356d64a34eeb2fba180b3077d543ebb1472fc1269" +name = "nm-secret-agent-manager" +version = "0.1.0" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ - "async-trait", - "base64", - "bitflags 2.13.0", - "futures", - "futures-timer", - "log", - "serde", - "thiserror 2.0.18", - "tokio", - "uuid", "zbus", - "zvariant", ] [[package]] @@ -5034,7 +5159,6 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ - "bytemuck", "num-traits", ] @@ -5066,10 +5190,11 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.46" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ + "autocfg", "num-integer", "num-traits", ] @@ -5536,7 +5661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.7", + "rand 0.8.6", ] [[package]] @@ -5802,6 +5927,27 @@ 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" @@ -5821,29 +5967,6 @@ 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" @@ -5854,7 +5977,7 @@ dependencies = [ "byteorder", "hmac 0.10.1", "md-5", - "rand 0.8.7", + "rand 0.8.6", "sha-1", "sha2 0.9.9", ] @@ -5893,6 +6016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" dependencies = [ "memchr", + "serde", ] [[package]] @@ -5918,9 +6042,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -5929,9 +6053,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.5" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -6014,7 +6138,7 @@ dependencies = [ "num-traits", "paste", "profiling", - "rand 0.9.5", + "rand 0.9.4", "rand_chacha 0.9.0", "simd_helpers", "thiserror 2.0.18", @@ -6037,15 +6161,6 @@ 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" @@ -6093,12 +6208,6 @@ 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" @@ -6160,9 +6269,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.13.0" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -6172,9 +6281,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -6278,9 +6387,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rust-embed" -version = "8.12.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -6289,11 +6398,10 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.12.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" dependencies = [ - "mime_guess", "proc-macro2", "quote", "rust-embed-utils", @@ -6303,19 +6411,19 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.12.0" +version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ - "sha2 0.11.0", + "sha2 0.10.9", "walkdir", ] [[package]] name = "rustc-demangle" -version = "0.1.28" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" [[package]] name = "rustc-hash" @@ -6366,9 +6474,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.23" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustybuzz" @@ -6609,17 +6717,11 @@ checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "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" @@ -6628,7 +6730,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest 0.9.0", "opaque-debug", ] @@ -6640,21 +6742,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "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" @@ -6806,6 +6897,7 @@ 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", @@ -6825,9 +6917,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.5" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -6836,11 +6928,14 @@ 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", @@ -6850,12 +6945,14 @@ 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]] @@ -7121,9 +7218,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.10" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] @@ -7192,6 +7289,19 @@ 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" @@ -7205,9 +7315,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.12.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -7292,7 +7402,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.4", + "winnow 1.0.3", ] [[package]] @@ -7313,7 +7423,7 @@ dependencies = [ "indexmap 2.14.0", "toml_datetime", "toml_parser", - "winnow 1.0.4", + "winnow 1.0.3", ] [[package]] @@ -7322,7 +7432,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.4", + "winnow 1.0.3", ] [[package]] @@ -7632,14 +7742,12 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.5" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.3", "js-sys", "serde_core", - "sha1_smol", "wasm-bindgen", ] @@ -8135,10 +8243,12 @@ 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", @@ -8578,6 +8688,7 @@ 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", @@ -8597,11 +8708,13 @@ 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", @@ -8616,6 +8729,7 @@ 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", @@ -8637,6 +8751,7 @@ 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", @@ -8644,12 +8759,14 @@ 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", @@ -8663,6 +8780,7 @@ 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", @@ -8678,6 +8796,7 @@ 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", @@ -8697,6 +8816,7 @@ 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", @@ -8722,6 +8842,7 @@ 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", @@ -8743,6 +8864,7 @@ 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", @@ -8755,6 +8877,29 @@ 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" @@ -8766,9 +8911,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -8794,6 +8939,39 @@ 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" @@ -8809,6 +8987,7 @@ 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", @@ -8961,9 +9140,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.17.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28b97f866896a4be7aefd2b5a8e01bb6773d19a775d54ab28b4d094b9a4480e" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" dependencies = [ "async-broadcast", "async-executor", @@ -8989,7 +9168,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 1.0.4", + "winnow 1.0.3", "zbus_macros", "zbus_names", "zvariant", @@ -9021,9 +9200,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.17.0" +version = "5.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e05ad887425eecf5e8384dc2406a4a9313eb73468712fc1cdea362eb4fe0469" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -9036,12 +9215,12 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.3" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039ca249fee9559680f3a9f05b55e0761fee51af4f6c1e7d8c1f31e549721d2" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 1.0.4", + "winnow 1.0.3", "zvariant", ] @@ -9060,12 +9239,12 @@ dependencies = [ [[package]] name = "zbus_xml" -version = "5.2.0" +version = "5.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ab0513c0a66a60a8718d5ad712a5094845a20d95b5c1c81e2bd8e904a52b4f" +checksum = "a8067892e940ed1727dea64690378601603b31d62dfde019a5335fbb7c0e0ed9" dependencies = [ + "quick-xml 0.39.4", "serde", - "winnow 1.0.4", "zbus_names", "zvariant", ] @@ -9078,18 +9257,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71" dependencies = [ "proc-macro2", "quote", @@ -9227,9 +9406,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.23" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zune-core" @@ -9272,24 +9451,24 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.13.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf057bb00bf5c9ad77abb6147b0ca4818236a1858416e9d988e40d6322fefa7" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow 1.0.4", + "winnow 1.0.3", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.13.0" +version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8118ca6bda77bfc0ab51d660db0c955f2505eef854c9a449435bccb616933b31" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -9300,17 +9479,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.5.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" dependencies = [ "proc-macro2", "quote", "serde", "syn 2.0.118", - "winnow 1.0.4", + "winnow 1.0.3", ] - -[[patch.unused]] -name = "winit-x11" -version = "0.31.0-beta.2" diff --git a/Cargo.toml b/Cargo.toml index ee9855c..4c31c2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,9 @@ resolver = "3" rust-version = "1.93" [workspace.dependencies] -cosmic-randr = { path = "../cosmic-randr/lib" } +cosmic-randr = { git = "https://github.com/pop-os/cosmic-randr" } tokio = { version = "1.49.0", features = ["macros"] } -iced_winit = { path = "/home/lionel/Projets/COSMIC/libcosmic/iced/winit", default-features = false } +iced_winit = { git = "https://github.com/pop-os/libcosmic", default-features = false } image = { version = "0.25.10", default-features = false, features = [ "jpeg", "png", @@ -27,29 +27,29 @@ features = [ "tokio", "qr_code", ] -path = "/home/lionel/Projets/COSMIC/libcosmic" +git = "https://github.com/pop-os/libcosmic" [workspace.dependencies.cosmic-config] -path = "/home/lionel/Projets/COSMIC/libcosmic/cosmic-config" +git = "https://github.com/pop-os/libcosmic" [workspace.dependencies.cosmic-bg-config] -path = "../cosmic-bg/config" +git = "https://github.com/pop-os/cosmic-bg" [workspace.dependencies.cosmic-comp-config] -path = "../cosmic-comp/cosmic-comp-config" +git = "https://github.com/pop-os/cosmic-comp" features = ["output"] [workspace.dependencies.cosmic-idle-config] -path = "../cosmic-idle/cosmic-idle-config" +git = "https://github.com/pop-os/cosmic-idle" [workspace.dependencies.cosmic-panel-config] -path = "../cosmic-panel/cosmic-panel-config" +git = "https://github.com/pop-os/cosmic-panel" [workspace.dependencies.cosmic-randr-shell] -path = "../cosmic-randr/shell" +git = "https://github.com/pop-os/cosmic-randr" [workspace.dependencies.sctk] -version = "1.3.0" +version = "0.20" package = "smithay-client-toolkit" [profile.dev] @@ -66,18 +66,33 @@ 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 = { path = "../cosmic-protocols" } -cosmic-client-toolkit = { path = "../cosmic-protocols/client-toolkit" } +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" } -[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" } +# [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/cosmic-text.git'] -cosmic-text = { path = "../cosmic-text" } +# 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.crates-io] atspi = { git = "https://github.com/wash2/atspi" } diff --git a/cosmic-settings/Cargo.toml b/cosmic-settings/Cargo.toml index febae53..6bf0692 100644 --- a/cosmic-settings/Cargo.toml +++ b/cosmic-settings/Cargo.toml @@ -21,6 +21,8 @@ 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 } @@ -32,6 +34,7 @@ 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" } @@ -54,13 +57,11 @@ 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" @@ -168,9 +169,10 @@ page-input = [ ] page-legacy-applications = ["cosmic-comp-config", "dep:cosmic-randr"] page-networking = [ + "dep:cosmic-settings-network-manager-subscription", "xdg-portal", - "dep:nmrs", - "dep:secret-service", + "dep:cosmic-dbus-networkmanager", + "dep:nm-secret-agent-manager", "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 ea54337..bce638f 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_enabled(), Message::AutoHidePanel), + .toggler(panel_config.autohide.is_some(), Message::AutoHidePanel), ) .add(settings::item( &descriptions[position], @@ -542,10 +542,18 @@ impl PageInner { Message::AutoHidePanel(enabled) => { if enabled { _ = panel_config.set_exclusive_zone(helper, false); - _ = panel_config.set_autohide(helper, AutoHide::OnOverlap); + _ = panel_config.set_autohide( + helper, + Some(AutoHide { + wait_time: 1000, + transition_time: 200, + handle_size: 4, + unhide_delay: 200, + }), + ); } else { _ = panel_config.set_exclusive_zone(helper, true); - _ = panel_config.set_autohide(helper, AutoHide::Never); + _ = panel_config.set_autohide(helper, None); } } Message::PanelAnchor(i) => { diff --git a/cosmic-settings/src/pages/display/mod.rs b/cosmic-settings/src/pages/display/mod.rs index 95dddf7..eba2ee7 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::Rotate180, - 3 => Transform::Rotate270, + 2 => Transform::Flipped180, + 3 => Transform::Flipped270, _ => Transform::Normal, }), &request, diff --git a/cosmic-settings/src/pages/networking/backend.rs b/cosmic-settings/src/pages/networking/backend.rs deleted file mode 100644 index eec2e4e..0000000 --- a/cosmic-settings/src/pages/networking/backend.rs +++ /dev/null @@ -1,1812 +0,0 @@ -// 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 84748ce..f9b4a4c 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 NetworkManager. - NetworkManagerConnect(nmrs::NetworkManager), + /// Successfully connected to the system dbus. + NetworkManagerConnect(zbus::Connection), /// Open the wifi settings page with the selected device. OpenPage { page: page::Entity, @@ -143,7 +143,6 @@ 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", @@ -191,7 +190,6 @@ 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", @@ -227,9 +225,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 { - nmrs::NetworkManager::new() + zbus::Connection::system() .await - .context("failed to connect to NetworkManager") + .context("failed to create system dbus connection") .map_or_else( |why| Message::Error(why.to_string()), Message::NetworkManagerConnect, @@ -312,47 +310,33 @@ impl Page { Task::none() } - fn connect(&mut self, nm: nmrs::NetworkManager) -> Task { + fn connect(&mut self, conn: zbus::Connection) -> Task { if self.nm_task.is_none() { let (canceller, task) = crate::utils::forward_event_loop(move |mut sender| async move { - 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; - } + let network_manager = match NetworkManager::new(&conn).await { + Ok(n) => n, Err(why) => { - _ = sender - .send(crate::pages::Message::Networking(Message::Error( - why.to_string(), - ))) - .await; - } - } + tracing::error!( + why = why.to_string(), + "failed to connect to network_manager" + ); - let Ok(events) = nm.network_events().await else { - return futures::future::pending().await; + 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( + + 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( devices.into_iter().map(Arc::new).collect(), - )), - Err(why) => Some(Message::Error(why.to_string())), + ), + Err(why) => 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 @@ -369,6 +353,16 @@ 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 6c2c9e7..ff85a8f 100644 --- a/cosmic-settings/src/pages/networking/vpn/mod.rs +++ b/cosmic-settings/src/pages/networking/vpn/mod.rs @@ -1,6 +1,8 @@ // Copyright 2024 System76 // SPDX-License-Identifier: GPL-3.0-only +pub mod nmcli; + use std::collections::HashMap; use std::sync::{Arc, LazyLock}; @@ -12,16 +14,17 @@ 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); @@ -57,8 +60,8 @@ pub enum Message { NetworkManager(network_manager::Event), /// An update from the secret agent SecretAgent(network_manager::nm_secret_agent::Event), - /// Successfully connected to NetworkManager. - NetworkManagerConnect(nmrs::NetworkManager), + /// Successfully connected to the system dbus. + NetworkManagerConnect(zbus::Connection), /// Updates the password text input PasswordUpdate(SecureString), /// Refresh devices and their connection profiles @@ -178,7 +181,7 @@ pub enum VpnDialog { #[derive(Debug)] pub struct NmState { - conn: nmrs::NetworkManager, + conn: zbus::Connection, sender: futures::channel::mpsc::UnboundedSender, active_conns: Vec, devices: Vec, @@ -368,22 +371,20 @@ 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 { - 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))), - ]); + 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))), + ]); } cosmic::Task::none() @@ -494,8 +495,9 @@ impl Page { Message::WireGuardConfig => { if let Some(VpnDialog::WireGuardName(device, filename, path)) = self.dialog.take() { return cosmic::task::future(async move { - let _ = filename; - match network_manager::import_wireguard(path, &device).await { + let new_path = path.replace(&filename, &device); + _ = std::fs::rename(&path, &new_path); + match super::nm_add_vpn_file("wireguard", new_path).await { Ok(_) => Message::Refresh, Err(why) => Message::Error(ErrorKind::Config, why.to_string()), } @@ -508,12 +510,18 @@ impl Page { if let Some(settings) = self.known_connections.get(&uuid) { let settings = match settings { ConnectionSettings::Vpn(settings) => settings, - ConnectionSettings::Wireguard { .. } => { - if let Some(NmState { ref sender, .. }) = self.nm_state { - _ = sender - .unbounded_send(network_manager::Request::ActivateVpn(uuid)); - } - return Task::none(); + 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 + }); } }; @@ -533,10 +541,28 @@ impl Page { return task::message(Message::FocusSecureInput); } _ => { - if let Some(NmState { ref sender, .. }) = self.nm_state { - _ = sender - .unbounded_send(network_manager::Request::ActivateVpn(uuid)); - } + 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 + }); } } } @@ -603,55 +629,89 @@ 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(); - 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"); + 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)), + }); + } } - 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 => { @@ -660,39 +720,53 @@ 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 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) + 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, + ) .await; - } - _ = nm_sender.unbounded_send(network_manager::Request::ActivateVpn(uuid)); - Message::Refresh - }) - .map(crate::app::Message::from); + } + Message::Activate(uuid) + }) + }) + .map(crate::app::Message::from); } } Message::UsernameUpdate(user) => { @@ -799,14 +873,56 @@ impl Page { Task::none() } - fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { + 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 { 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 { - network_manager::watch(conn, tx).await; + 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) + ) }); let forwarder = std::pin::pin!(async move { @@ -1025,7 +1141,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -fn update_state(conn: nmrs::NetworkManager) -> Task { +fn update_state(conn: zbus::Connection) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -1034,7 +1150,7 @@ fn update_state(conn: nmrs::NetworkManager) -> Task { }) } -fn update_devices(conn: nmrs::NetworkManager) -> Task { +fn update_devices(conn: zbus::Connection) -> Task { cosmic::task::future(async move { let filter = |device_type| matches!(device_type, network_manager::devices::DeviceType::WireGuard); @@ -1092,9 +1208,7 @@ fn add_network() -> Task { ); }; - network_manager::import_openvpn(path) - .await - .map_err(|why| why.to_string()) + super::nm_add_vpn_file("openvpn", path).await }; match result { @@ -1109,96 +1223,108 @@ fn add_network() -> Task { .apply(cosmic::task::future) } -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(); +fn connection_settings(conn: zbus::Connection) -> Task { + let settings = async move { + let settings = network_manager::dbus::settings::NetworkManagerSettings::new(&conn).await?; - 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 { + _ = 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, - 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)), + password_flag, + username, }), - 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); - } + )) + }) + // Reduce the settings list into + .fold(IndexMap::new(), |mut set, (uuid, data)| async move { + set.insert(uuid, data); + set + }) + .await; - Ok::<_, nmrs::ConnectionError>(settings) - }; + Ok::<_, zbus::Error>(settings) + }; + cosmic::task::future(async move { 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 new file mode 100644 index 0000000..c759c14 --- /dev/null +++ b/cosmic-settings/src/pages/networking/vpn/nmcli.rs @@ -0,0 +1,38 @@ +// 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 a715fda..53c0a37 100644 --- a/cosmic-settings/src/pages/networking/wifi.rs +++ b/cosmic-settings/src/pages/networking/wifi.rs @@ -13,15 +13,16 @@ 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); @@ -54,8 +55,8 @@ pub enum Message { NetworkManager(network_manager::Event), /// An update from the secret agent SecretAgent(network_manager::nm_secret_agent::Event), - /// Successfully connected to NetworkManager. - NetworkManagerConnect(nmrs::NetworkManager), + /// Successfully connected to the system dbus. + NetworkManagerConnect(zbus::Connection), /// Request an auth dialog PasswordRequest(network_manager::SSID), /// Update the password from the dialog @@ -101,7 +102,6 @@ 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: nmrs::NetworkManager, + conn: zbus::Connection, sender: futures::channel::mpsc::UnboundedSender, state: network_manager::NetworkManagerState, devices: Vec, @@ -294,23 +294,19 @@ 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 { - 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))), - ]); + 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)))]); } Task::none() @@ -356,22 +352,15 @@ impl Page { } match req { - network_manager::Request::Authenticate { - ssid, - identity, - network_type, - .. - } => { + network_manager::Request::Authenticate { ssid, identity, .. } => { 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)), }); @@ -388,13 +377,11 @@ 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)), }); @@ -462,7 +449,6 @@ 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 => "", }; @@ -535,7 +521,6 @@ 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)), }); @@ -559,7 +544,6 @@ impl Page { ssid, identity, password, - network_type, tx, .. } = dialog @@ -578,7 +562,6 @@ impl Page { ssid: ssid.to_string(), identity, password, - network_type, secret_tx, interface, }); @@ -631,7 +614,6 @@ 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 @@ -645,7 +627,6 @@ 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 @@ -667,9 +648,6 @@ impl Page { } } Message::WiFiEnable(enable) => { - if !enable { - self.connecting.clear(); - } if let Some(nm) = self.nm_state.as_mut() { _ = nm .sender @@ -678,9 +656,7 @@ impl Page { } } Message::CancelDialog => { - if let Some(WiFiDialog::Password { ssid, .. }) = self.dialog.take() { - self.connecting.remove(ssid.as_ref()); - } + self.dialog = None; } Message::Error(why) => { tracing::error!(why); @@ -739,20 +715,15 @@ 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: _ } => { - match self.dialog.take() { - Some(WiFiDialog::Password { ssid, .. }) => { - self.connecting.remove(ssid.as_ref()); - } - other => { - self.dialog = other; - } - } + self.dialog = self + .dialog + .take() + .filter(|d| !matches!(d, &WiFiDialog::Password { .. })); } nm_secret_agent::Event::Failed(error) => { tracing::error!(%error, "secret agent failure"); @@ -760,18 +731,15 @@ 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); } @@ -802,14 +770,19 @@ impl Page { Task::none() } - fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { + fn connect(&mut self, conn: zbus::Connection) -> 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 { - network_manager::watch(conn, tx).await; + 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) + ); }); let forwarder = std::pin::pin!(async move { @@ -859,12 +832,6 @@ 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); @@ -1304,9 +1271,51 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -fn connection_settings(conn: nmrs::NetworkManager) -> Task { +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) + }; + cosmic::task::future(async move { - network_manager::wifi_connection_settings(conn) + settings .await .context("failed to get connection settings") .map_or_else( @@ -1317,7 +1326,7 @@ fn connection_settings(conn: nmrs::NetworkManager) -> Task }) } -pub fn update_state(conn: nmrs::NetworkManager) -> Task { +pub fn update_state(conn: zbus::Connection) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -1326,7 +1335,7 @@ pub fn update_state(conn: nmrs::NetworkManager) -> Task { }) } -pub fn update_devices(conn: nmrs::NetworkManager) -> Task { +pub fn update_devices(conn: zbus::Connection) -> 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 3a1bf86..104d4a0 100644 --- a/cosmic-settings/src/pages/networking/wired.rs +++ b/cosmic-settings/src/pages/networking/wired.rs @@ -10,14 +10,12 @@ 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)] @@ -34,8 +32,8 @@ pub enum Message { Error(String), /// An update from the network manager daemon NetworkManager(network_manager::Event), - /// Successfully connected to NetworkManager. - NetworkManagerConnect(nmrs::NetworkManager), + /// Successfully connected to the system dbus. + NetworkManagerConnect(zbus::Connection), /// Refresh devices and their connection profiles Refresh, /// Create a dialog to ask for confirmation of removal. @@ -93,7 +91,7 @@ pub struct Page { #[derive(Debug)] pub struct NmState { - conn: nmrs::NetworkManager, + conn: zbus::Connection, sender: futures::channel::mpsc::UnboundedSender, active_conns: Vec, devices: Vec>, @@ -156,9 +154,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 { - nmrs::NetworkManager::new() + zbus::Connection::system() .await - .context("failed to connect to NetworkManager") + .context("failed to create system dbus connection") .map_or_else( |why| Message::Error(why.to_string()), Message::NetworkManagerConnect, @@ -358,14 +356,18 @@ impl Page { Task::none() } - fn connect(&mut self, conn: nmrs::NetworkManager) -> Task { + fn connect(&mut self, conn: zbus::Connection) -> 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 { - network_manager::watch(conn, tx).await; + 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) + ) }); let forwarder = std::pin::pin!(async move { @@ -620,7 +622,7 @@ fn popup_button(message: Message, text: &str) -> Element<'_, Message> { .into() } -fn update_state(conn: nmrs::NetworkManager) -> Task { +fn update_state(conn: zbus::Connection) -> Task { cosmic::task::future(async move { match NetworkManagerState::new(&conn).await { Ok(state) => Message::UpdateState(state), @@ -629,7 +631,7 @@ fn update_state(conn: nmrs::NetworkManager) -> Task { }) } -fn update_devices(conn: nmrs::NetworkManager) -> Task { +fn update_devices(conn: zbus::Connection) -> 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 6f15634..65b7b43 100644 --- a/cosmic-settings/src/pages/sound/mod.rs +++ b/cosmic-settings/src/pages/sound/mod.rs @@ -3,19 +3,20 @@ 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, Client, CosmicAudioProxy}; +use cosmic_settings_audio_client::{self as audio_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"; @@ -25,8 +26,6 @@ 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. @@ -64,12 +63,9 @@ 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, } @@ -88,9 +84,6 @@ 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, } @@ -181,7 +174,7 @@ impl Page { Message::Model(cosmic_settings_sound::Message::Client(client)) => { if let Some(client) = Arc::into_inner(client) { - self.client = Some(client); + self.client = Some(Rc::new(RefCell::new(client))); self.model = model::Model { text: model::Text { hd_audio: fl!("sound-hd-audio"), @@ -192,19 +185,13 @@ 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.conn.set_default(node_id, true).await; + _ = client.borrow_mut().conn.set_default(node_id, true).await; }); } } @@ -215,7 +202,7 @@ impl Page { && let Some(client) = self.client.as_mut() { block_on(async { - _ = client.conn.set_default(node_id, true).await; + _ = client.borrow_mut().conn.set_default(node_id, true).await; }); } } @@ -223,7 +210,7 @@ impl Page { Message::ToggleSinkMute => { if let Some(ref mut client) = self.client { block_on(async { - _ = client.conn.sink_mute_toggle().await; + _ = client.borrow_mut().conn.sink_mute_toggle().await; }); } } @@ -231,64 +218,40 @@ impl Page { Message::ToggleSourceMute => { if let Some(ref mut client) = self.client { block_on(async { - _ = client.conn.source_mute_toggle().await; + _ = client.borrow_mut().conn.source_mute_toggle().await; }); } } Message::SetSinkVolume(volume) => { - 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() + 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; }); } } Message::SetSourceVolume(volume) => { - 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() + 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; }); } } Message::SetSinkBalance(balance) => { - 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; + 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 { _ = client + .borrow_mut() .conn - .set_node_volume_balance( - sink_id, - Some(balance.load(atomic::Ordering::Relaxed) as f32 / 1000.0), - ) + .set_node_volume_balance(sink_id, Some(balance)) .await; - Message::ReattachClient(Arc::new(client)).into() }); } } diff --git a/subscriptions/network-manager/Cargo.toml b/subscriptions/network-manager/Cargo.toml new file mode 100644 index 0000000..e40a2a8 --- /dev/null +++ b/subscriptions/network-manager/Cargo.toml @@ -0,0 +1,21 @@ +[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 new file mode 100644 index 0000000..8dc5b15 --- /dev/null +++ b/subscriptions/network-manager/LICENSE.md @@ -0,0 +1,359 @@ +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 new file mode 100644 index 0000000..63af460 --- /dev/null +++ b/subscriptions/network-manager/src/active_conns.rs @@ -0,0 +1,70 @@ +// 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 new file mode 100644 index 0000000..0653ae1 --- /dev/null +++ b/subscriptions/network-manager/src/available_wifi.rs @@ -0,0 +1,116 @@ +// 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 new file mode 100644 index 0000000..45ac95d --- /dev/null +++ b/subscriptions/network-manager/src/current_networks.rs @@ -0,0 +1,111 @@ +// 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 new file mode 100644 index 0000000..4dbcdb6 --- /dev/null +++ b/subscriptions/network-manager/src/devices.rs @@ -0,0 +1,257 @@ +// 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 new file mode 100644 index 0000000..eb49769 --- /dev/null +++ b/subscriptions/network-manager/src/hw_address.rs @@ -0,0 +1,102 @@ +#[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 new file mode 100644 index 0000000..675b7bf --- /dev/null +++ b/subscriptions/network-manager/src/lib.rs @@ -0,0 +1,1169 @@ +// 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 new file mode 100644 index 0000000..330c594 --- /dev/null +++ b/subscriptions/network-manager/src/nm_secret_agent.rs @@ -0,0 +1,738 @@ +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 new file mode 100644 index 0000000..039bbf3 --- /dev/null +++ b/subscriptions/network-manager/src/wireless_enabled.rs @@ -0,0 +1,66 @@ +// 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) +}